queue-kit — a git-native, agent-readable task tracker
One Markdown file is the tracker: sections are queues, ### headings are tasks, each heading’s kebab-case slug is the task’s handle and anchor, 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 ### entry headings:
- 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. A deferred body is free prose in which four bold-lead-in fields recur, each answering a question a later scope asks:Deliverable— what landing looks like; a convention no gate reads.Why design-pending— what the open design actually is; a convention no gate reads.Cost while deferred— required, the Gap-disposition rule’s costing. Its class rides the tag line’s[cost:]tag (§The tag algebra), and the field carries the prose that class summarises. Its bold lead-in is line-local like a tag: split across a hard wrap, it is invisible to the scanner and the entry reads as uncosted.Filed <date> by <stage>— the closing provenance line: the date, the filer, and at most one attribution, the authority that directed the entry and its channel. The entry’s route is the promoting commit’s to state: the inbox that captured it, the stage that promoted it, the dispositions tried first. History answers each, and a copy on the entry ages with every move. A convention no gate reads: a route rides as a trailing clause beside the attribution, where a clause-level reader reds evidence marks and costings as often.
An entry’s prose may also carry a
Surfaced <date>mark, recording when the premise was observed.check-queue-entry-budget(§check-queue-entry-budget) holds the cost field, requires every top-level deferred entry to resolve a defer date (below), and caps the entry’s total size in its configured unit. A body may carry lifecycle-kit’s inferred marker on a line of its own (lifecycle-kit/SPEC.md §templates/stages/); queue-kit reads it nowhere. - 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 bare slug line per exit, with no tag and no disposition token, and prose about what happened left to 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: delivery is recorded by the landing commit’s type, where every reader that cares about it 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). - Any other section (an iteration header, a lessons section) is outside the grammar and ignored by every gate except the file-wide hygiene axes.
The defer date of an entry is its Surfaced <date> mark when present, else the date on its Filed <date> provenance line. The mark is read off a body line, case-sensitive, with the ISO date directly after it on the same line. A reflow that wraps the date, a lowercase mark or a word between mark and date resolves nothing, and the entry is undated. Surfaced is the better premise-rot datum and Filed the available fallback, so no entry owes a migration.
One definition has three readers: drift-kit’s deferred-age KPI, the queue-index arm’s --icebox-candidates, and check-queue-entry-budget’s assertion (E), the last two through one shared adapter (§The shared queue adapters). A kit that cannot depend on queue-kit re-implements the predicate, and both ends cite this section. drift-kit does, because depending on this kit would close a cross-kit cycle. gate-sdk’s check-gate-exemption-tasks does for a stronger reason: gate-sdk is the substrate every kit vendors, so depending on queue-kit would make a queue format a precondition for running any gate. The holder roster and the residue accounting live at gate-sdk/SPEC.md §check-gate-exemption-tasks alone, since a second hand-kept copy would owe every holder change two edits in two SPECs.
An entry in a task section is a third-level heading whose whole text is its slug, ### <slug>:
- Its lead line, also called its tag line, is the first non-blank line after the heading when that line consists only of bracketed tags separated by spaces. It carries every tag the entry has; an entry with no tags has none.
- Its body is the paragraphs that follow, up to the next heading of the same or a shallower level or a
---rule, and the first body paragraph is the entry’s summary. Each paragraph is one line where the consumer keeps its markdown unwrapped, and a blank line separates the heading, the tag line and every paragraph. - A sub-task is a
#### <slug>heading inside its parent’s extent, with the same grammar one level down. - Slugs match
[a-z0-9][a-z0-9-]*in one global, unique namespace across active, deferred, icebox and sub-tasks. A slug is the task’s stable handle for its whole life and its heading anchor:[blocked-by:], a same-file link and the done-section line all carry it verbatim. A heading in a task section that is not a valid slug is malformed. The slug grammar is kit mechanism, not config.
The done section keeps its bare - <slug> bullets, one line per exit. Lessons keep their bullets, and any other section is outside the grammar.
Section order sequences an iteration; it does not rank it. The default ("New Features" "Technical Debt") is read as selection order (§Layout and configuration), but not because features outrank debt. Position starves nothing: by the drain rule an active entry’s residency is one iteration, since 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 states 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.
Queue and backlog
The two words name two halves of one thing, and each owns its half. Queue is the governed file and its drain mechanics: the sections and entry grammar a parser selects work by, the counts the queue-counts arm reports, the drain-entry exemption, and the per-entry cap and its credits (QUEUE_KIT_ENTRY_CAP, QUEUE_KIT_ENTRY_CREDIT_MAX). Backlog is the accumulating mass of unlanded work read as a quantity over time: its growth and its aging, which drift-kit trends (drift-kit/SPEC.md kpi-gate-backlog, the backlog-aging findings) and the closing stage reviews and evicts. A queue mechanic is judged against the file as it stands; a backlog reading is judged against its trend. Collapsing the two would lose that distinction and leave kpi-gate-backlog naming something other than the backlog. The file’s own name is not vocabulary at all: it is consumer config (QUEUE_KIT_QUEUE_FILE), so renaming it moves a knob and no word.
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 deferred entries that went dormant: filed, carried, and neither recurred nor promoted for the age the tier’s knob sets, each compressed to its heading and one sentence. Dormancy is evidence of age, so the tier has one entry route, deferred → icebox at the closing stage’s eviction; a newly filed finding has no such evidence and never lands here directly (below).
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. 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. On the done exit the slug stays visible on a bare done line. A landing’s moots are looked for at the closing stage’s moot sweep (lifecycle-kit/templates/stages/close.md), over the iteration’s exits and commit range. A closed ruling’s moots are found by the session that reads the ruling, because no range contains them. The commit that moves a slug to Done rewrites every[<slug>] (#<slug>)in the file to`<slug>`, the retired citation (§The tag algebra), and--queue donemakes it with the move. - 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 tag-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 the shared adapters (§The shared queue adapters), 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. Two routes into it do not read like the criterion that covers them, so they are named; neither is a class needing its own state:
- 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 landed-unit criterion exactly.
- A slug mooted by supersession, where the work became unnecessary rather than folded into a sibling, is covered by whichever criterion did the mooting: the landed unit or the closed ruling.
One entry is outside every exit above: a [roadmap:]-tagged one. Reversing it stays the ruling authority’s, and no drain may retire it — not by the done exit, not by the icebox, not as a wontfix. The tag is a standing statement about direction rather than a property of the work, so a session judging the work finished or unnecessary is judging the wrong thing.
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 heading and one sentence:
### <slug>
<one sentence: what it is, and why it is dormant>
- No state tag. Section membership is the state, for this tier as for the deferred section; a tag restating its own section is the two-sources defect. What generalizes across the amendment lifecycle is the design-pending section set, not a tag. The “why it is dormant” clause is written where it fits and otherwise left to the tier — membership already declares it.
- No sub-tasks, no tag line and one sentence of body. Grouping is presentation on a surface whose entire purpose is minimum residency, and a sub-task or a second paragraph is body the shape rule rejects.
- An eviction drops every tag. The
[cost:]and[surface:]tags are the deferred board’s (§The tag algebra), and an entry with no tag line has no reader for either (§check-deferred-board-tags). A return to the deferred section on recurrence declares both again, as any filing does. - A
[roadmap:]-tagged entry is not icebox-eligible, and the rule needs no new code. The roadmap parse walks the live task sections, the tier included, andcheck-roadmap-freshreds any[roadmap:]entry carrying no[roadmap-summary:]tag, which an icebox entry, having no tag line, has nowhere to put. Kept inside the walk, a[roadmap:]tag that drifted into the icebox reds rather than silently dropping a public commitment. 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 otherwise. The eligibility rule binds either way. - The narrative is recoverable, not discarded. The commit that iceboxes an entry contains the removed body in its own diff, so recovery is
git log -p -G'<slug>' -- <queue-file>, which depends on no commit-message convention. It is-G, which matches diff lines, and never-S, which lists a commit only when the slug’s occurrence count changes. An eviction or a Done move keeps the slug in the file, so its count is unchanged and-Sdoes not list the evicting commit.-Galso lists every earlier commit that touched a line naming the slug; the evicting commit is the newest of them that removes the body. Nothing is copied into the queue to point at it. - Recovery is mandatory before any ruling on the entry. The sentence carries neither the deliverable nor the blocking design question, so promoting a dormant entry, or ruling that a build subsumed one, is judged against the recovered body.
Eligibility is judged at the closing stage over the deferred section. An entry is eligible when:
- its defer date is at least
QUEUE_KIT_ICEBOX_AGE_DAYSold, the limb that makes the tier a demotion on evidence rather than a filing preference, since a younger entry has not had the chance to recur; - its
[cost:]class is in the low class,event/loworonce/low(§The tag algebra); - it carries no
[roadmap:]tag and no[not-icebox-eligible:]tag (§The tag algebra); - it has no live promotion trigger.
What counts as live is deliberately narrow, since a wider reading lets almost no entry qualify. A live trigger is a live, unbuilt queue slug the entry names, a [recurrence:] array whose newest date, its last element, is no older than QUEUE_KIT_ICEBOX_AGE_DAYS, or a [roadmap:] tag. A trigger 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, the round trip the tier conserves.
A recurrence ages on the same knob as the entry. Once the window passes with no newer date, the finding had its chance to recur and did not, which is the dormancy evidence the age limb reads. One knob serves both limbs because both ask whether the entry has had the window to recur, and a second number could only disagree with the first. A later recurrence returns the entry with a fresh date. Only eligibility ages the date: the scope pre-emption count and the recurrence KPI read every date as history. The [not-icebox-eligible:] limb stands beside the live-trigger limb rather than widening what counts as live, because the tag records a fact no trigger carries.
A self-citing set does not manufacture its own liveness. Where a consumer classifies a set of entries dormant as a class, a citation from one member of the set to another is not a live trigger; only the blocks-nothing criterion governs. Without this, the live-slug limb inverts the eligibility rule exactly where a class disposition is most wanted: each member names another live unbuilt slug, so the larger and better cross-referenced the set, the more certainly none of it qualifies. A member that genuinely re-fires returns on a real recurrence, a trigger from outside the set that this clause leaves untouched. A [roadmap:] tag or a [recurrence:] array inside the age window stays live inside such a set as outside one. Where an earlier limb blocks first, the clause’s antecedent never obtains and it does not fire, which is the clause working as written.
The class is the tag line’s [cost:] tag, never the word the cost field opens with. An entry with no tag, or with a value outside the closed grammar, has declared no class and so is not in the low class. The --icebox-candidates arm still lists it as (unclassed) (§The queue-index arm), so the missing input is seen and supplied from the prose. A queue written against the retired opener vocabulary migrates by the prose, not by the word: low, zero and cosmetic each map to a */low class and bounded to once/*. The half the word does not give is read from the field’s prose, and so is the whole class wherever that prose describes a cost the word understated.
That class is not re-declared to unblock an eviction. A session may not rewrite the [cost:] tag of an entry its class keeps off the --icebox-candidates worklist to read low. Re-declaring makes an entry look evictable without making it dormant, which emulates the oracle rather than running it. An entry whose true cost is high is correctly blocked by this rule. A tag that was simply mis-declared is corrected as ordinary repair, needing no ruling. check-deferred-board-tags (§check-deferred-board-tags) enforces the tag’s shape and never adjudicates a cost’s truth, so waiting on it answers nothing.
Nothing files directly into the icebox, neither a gap-inbox bullet at the drain nor a closing stage’s own capture. The tier’s evidence is age without recurrence, which a bullet triaged on the day it is drained cannot carry, and a direct route makes the tier a cheap sink, dormant by class rather than by evidence and re-read by every triage. A drained bullet not worth carrying is a discard with cause; one worth carrying is a deferred entry, which the next eligible close evicts if it stays dormant. A pool that fills faster than it drains is fixed by the scoping stage’s ranking (lifecycle-kit/templates/stages/scope.md), not by a lower landing tier.
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 rather than config; the one exception is the consumer-named harvest vocabulary below. Every tag sits on its entry’s tag line, directly under the heading, or on a lesson bullet’s own line: the only lines the parsing tools scan (enforced by check-tag-lead-line). A free-text tag value, such as a roadmap summary or an exemption’s reason, holds no [ or ], so a summary cannot carry a markdown link. This SPEC writes a link’s shape with a space between ] and (, the form check-md-refs reads as prose rather than a link; the link itself has none.
Outside the queue file, living prose claims a task’s queue membership with one grammar: a markdown link whose target resolves to the queue file and whose fragment is slug-shaped, [<slug>] (<path-to-queue>#<slug>), which check-queue-slug-liveness resolves against the live slug set on the configured prose surfaces (§check-queue-slug-liveness).
Inside the queue file, a reference from one entry’s body to another entry is a citation of it: the relation an author already drew while writing about something else. A reference to a live entry is a same-file link, [<slug>] (#<slug>), and a reference to a retired slug (done or departed) is a single-backticked token, the retired citation. check-task-names assertion R holds both halves, a dangling link and a backticked live slug each red. The claim on a prose surface and the citation are deliberately different: the prose link 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 §The queue-edges arm. The rules below make citations parseable, each covering a shape a live corpus contains:
- Resolution is against the live slug set —
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 (§The queue-edges arm). A citation resolving to a retired slug is an edge, marked retired; the two sets are disjoint by construction. - An unresolved backticked 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). No report over the remainder and no gate on it can exist. Once the retired slugs are taken out, what is left is overwhelmingly not citations at all. It holds 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. A listing of it is unusable, and a red on it would fire on correct prose.
- A marker an author maintains by hand to separate landed from live citations is refused, and the answer follows from the two refusals below rather than adding a third. Such a marker is the maintained roster under a new spelling: it would need writing on every citation a corpus has already written, and re-writing at every disposition by a session remembering to. The link form is not that marker. Its reader is the resolving gate plus any human clicking, and its one rewrite is forced, mechanical and made once, at the Done move (§The icebox tier), where assertion R reds the link the move would leave dangling. The retired edge is still derived (§The queue-edges arm’s retired block), which is the form both refusals leave open.
- 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.- A block that names no slug is no edge and no citation. An entry held by something it only describes — “the roster entry” — is unreachable to this arm and to every grep, so recovering the subject costs a history search. The arm cannot close that: a description matches nothing by construction, which leaves the authoring rule as the whole remedy. A block worth a later session’s reach names its subject, in the tag or inline.
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. Decisively, a kit literal of one project’s relational verbs would ship that project’s vocabulary as everyone’s: the provenance seam, the reason [roadmap:]’s horizons and tracks are consumer-configured arrays. A relation’s kind rides the citing line itself, quoted verbatim (§The queue-edges arm), so there is no vocabulary to leak and no consumer config to invent.
A relates: <kind> <slug> declaration is refused, precise and kind-carrying as it is, on three counts:
- every citation a corpus has already written would need hand-authoring, the maintained roster Derivation-first forbids;
- those lines would land inside entries measured against
check-queue-entry-budget’s size cap, where sub-tasks do not relieve a parent’s budget, so precision would be paid for in evictions; - a hand-declared edge can be forgotten in exactly the moment it matters, whereas a prose citation is written because the author was already thinking about the relation.
The derived grammar costs no line in any entry and reads any corpus that already cross-references, with no migration.
[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.[spec: <file>]— spec-ready pointer. queue-kit parses and displays it; the amendment semantics (section-wide enforcement, promotion rules) are canon-kit’s amendment lifecycle and land with that 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 tag-line reason is a terse marker, not the audit trail:check-tag-lead-linepins the tag to the tag line. Wherecheck-queue-wrapis registered, it caps the tag’s width while its tag-line discount covers only[cost:]and[surface:], never this tag, whose free-text reason is unbounded (§check-queue-wrap). So a long tag line 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.[observed-by: <producer>]— observation-predicate marker: the entry’s completion predicate is an observation of a remote run rather than a tree state, and<producer>names the run that produces it. It is lead-line-scoped and<producer>is non-empty by grammar, an authoring rule with no gate behind it, because the value is opaque to every gate: no arm branches on it and no knob carries a producer roster. That is the slot-not-vocabulary seam[roadmap:]’s fields take, for a sharper reason: one project’s CI run names as a kit literal would ship its pipeline layout to every adopter. The value’s one reader is a human at the promotion transition (lifecycle-kit’s scope template, selecting the push-placement branch or the split). No second field holds the run id, which is produced after the tag is written and so would have no reader when minted. Placement semantics are lifecycle-kit’s, the split[drain-exempt:]takes:check-stage-entryassertion B reads the tag’s presence to select a refusal message, and lifecycle-kit/SPEC.md §The state machine owns the push-placement obligation the tag turns on.[cost: <recurrence>/<magnitude>]— deferred cost class: when an entry’s cost while deferred is paid, then how much, so a reader ranks or selects the pool off lead lines rather than bodies.<recurrence>is one of four classes.sessionis paid by every session that loads or runs the affected surface: an always-loaded token cost, a hook, a battery that reds at every commit.iterationis paid at least once by every iteration’s stage walk: a re-read at every scope or close, a wait at every close, a leg on every push.eventis paid when a named event other than a stage walk occurs: a release, an adopter’s install, a touch of the affected surface, an exposure being exercised.onceis a fixed cost that does not grow while the entry is deferred.<magnitude>ishighorlow, judged against the entry’s ownCost while deferredprose, never the word that prose opens with. Every top-level deferred entry carries exactly one and no active entry carries any (§check-deferred-board-tags). The session filing a deferred entry writes this tag and[surface:]together, whichever stage it runs in; a promotion drops both (canon-kit/SPEC.md §The amendment lifecycle), and so does an eviction (§The icebox tier).[surface: <entry>]— primary-surface marker: one top-level entry of the repository root (a kit root, an owned root directory, a root file) naming what the entry’s work mainly changes, so a reader groups the pool by shared surface off lead lines. The value is valid exactly when that entry exists in the root’s directory listing, so the consumer’s own root configures the value set, with no knob and no kit literal presuming a layout. The axis is surface, not kit: a root directory that is not a kit (a site, an installer, a crate, the command bindings) is a bundling key too. A multi-surface entry declares its primary surface only, since bundling wants one join key. The scoping stage’s same-surface fill reads it (lifecycle-kit/templates/stages/scope.md). Carried where[cost:]is, by the same gate.[cap-credit: +<n><unit> <YYYY-MM-DD> <grantor> <reason>]— a per-entry size credit on a deferred entry, raising that entry’s limit under §check-queue-entry-budget assertion A toQUEUE_KIT_ENTRY_CAPplus<n>.<unit>is the cap’s own (cporlines).<grantor>names the granting role in the consumer’s own authority vocabulary, and the kit enumerates none.<reason>is non-empty and states what the cap would otherwise have cost. At most one per entry, lead-line-scoped. Written only on a grant by the role §check-queue-entry-budget names. Honest limit: no gate can tell a granted credit from a self-issued one, the limit every inline provenance statement in this format carries (§check-queue-entry-budget assertion D states provenance inline for the same reason): the claim is read, never verified. The grant date and grantor put the claim where a later close reads it.[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, and its two field values come from the consumer-configuredQUEUE_KIT_HORIZONSandQUEUE_KIT_TRACKSarrays. That is the fixed-spelling/configured-values split[spec:]carries, so one project’s roadmap posture never ships as everyone’s. It is one tag with two slash-joined fields 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. An entry carries at most one[roadmap:]tag; an untagged entry, the normal case, is not projected. 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, falling out of the summary rule. An icebox entry has no tag line to carry[roadmap-summary:], so a[roadmap:]tag that drifted into the tier reds on assertion C whereverQUEUE_KIT_ROADMAP_FILEis set (§The icebox tier states that limit). Field validity ischeck-roadmap-fresh’s assertion B.[roadmap-summary: <sentence>]— the single sentence the public page prints for a[roadmap:]-tagged entry. Spelling is fixed mechanism, and it cannot collide with the[roadmap:scan, whose token its own spelling does not contain.[recurrence: <YYYY-MM-DD>, <YYYY-MM-DD>, …]— an array: one tag per entry, holding every re-filing date, comma-separated and appended in order, never rewritten. A second recurrence appends to it and never adds a second tag. The initial filing is not a recurrence and is not listed. Its count is the number of elements, which lifecycle-kit’s scope pre-emption rule (lifecycle-kit/SPEC.md §Layout and configuration,LIFECYCLE_KIT_RECURRENCE_THRESHOLD) and drift-kit’skpi-incident-recurrence(drift-kit/SPEC.md §Bundled KPIs) read. Its newest date is the last element, which the recurrence age limb reads (§The icebox tier).[not-icebox-eligible: <YYYY-MM-DD> <grounds>]— the date the standing fact was recorded and a non-empty grounds clause stating the fact in brief or pointing at the paragraph, ruling or decision holding it.
The last three are field tags, each with its producers, its at-most-one-per-entry rule and its readers, and each a field tag by canon-kit’s attribute test (canon-kit/SPEC.md §The amendment lifecycle) because its readers read the tag line. The heading above the tag line names the entry, and check-queue-hygiene’s duplicate axis does not read tag lines, so none of them needs a slug field: two entries stamped the same day carry identical tags and are told apart by their headings.
The summary 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, whitespace runs collapsed, so no sentence heuristic decides what a public page inherits. One tag, not a block, so no block-end rule can silently truncate it. check-roadmap-fresh’s assertion C makes the two roadmap tags mandatory for each other in both directions.
The [cost:] set is closed, and its two readers partition it. The scoping stage ranks session/* and iteration/* first, session before iteration and high before low (lifecycle-kit/templates/stages/scope.md). The icebox’s low class is event/low and once/low (§The icebox tier), so such a value is never icebox-eligible. It is one tag with two slash-joined fields, on [roadmap:]’s precedent: scope picks those by recurrence alone, no reader reads magnitude alone and the icebox reads both, so the pair is one attribute. The values are kit-fixed, because both partitions are kit mechanism (queue-kit’s icebox eligibility, lifecycle-kit’s rank tiers): a configured set would need a configured partition, moving an eligibility rule into config. The tag carries the class and the Cost while deferred field keeps the prose, still required (§check-queue-entry-budget) and still where impact is read for a shortlist. It is a field tag by canon-kit’s attribute test (canon-kit/SPEC.md §The amendment lifecycle), because its readers scan lead lines alone.
[surface:] reads the listing, not a curated root roster. Reading gate-sdk’s check-root-tiering allowlist would couple a queue-kit gate to a gate-sdk consumer file for a set the tree already yields. Wherever that gate is green the two sets coincide, and the one extra a listing admits, untracked root scratch, is a value no filer names. The iteration lead’s shared-surface batching is not a reader (lifecycle-kit/templates/lead.md §Economics — batch, and compact where it pays): it cuts batches from promoted entries by the files their amendments name, a finer key than a root entry.
[observed-by:] is not redundant with [precondition-ok:]. That tag is an opt-out for a different gate, deliberately not lead-line-scoped, with a contractually opaque reason, so overloading it would bind two independent conventions to one string and stop either changing without the other. No landed-but-unobservable marker exists either: it would name a state the placement rule stops producing. If the demote fallback (lifecycle-kit/SPEC.md §Deviation transitions) is ever measured as the common path, that marker is the fix.
No queue-index recurrence mode exists. drift-kit could not call one without the cycle its own KPI already records, leaving lifecycle-kit’s rule as its only caller, and §The queue-index arm resists a further mode because folding jobs together gives one tool two output grammars. Each reader pairs the heading with its tag line on its own, citing this section.
One array with appended dates, not one tag per recurrence. A tag per recurrence grows an entry by a whole tag each time against check-queue-entry-budget’s cap, so a machine writer could push a deferred entry past the cap and red a later, unrelated session’s commit. 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. The array costs one date per re-filing, under 1% of the default cap. The at-most-one-tag rule bounds it, not a width: a slug recorded as recurring many times without a promotion is a governance failure the scoping stage’s pre-emption rule surfaces.
The recurrence tag is session-written under judgment. Every stamp, the closing stage’s gap-inbox drain’s and a direct one alike, is written with --queue recur (§The queue verbs), so the verb is the declaration’s one writer and the judgment stays the session’s. Any session that judges a recurrence is obliged to stamp one. 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). The tag is hand-read, and an entry with no tag has not recurred. It has no presence gate, since a scanner cannot demand a tag written under judgment, so existing entries are not back-filled.
The [not-icebox-eligible:] tag declares a standing exclusion, not a promotion trigger: the entry stays out of the icebox whatever its age and cost until the fact is withdrawn (§The icebox tier’s eligibility rule), and the worklist prints it as the row’s cause (§The queue-index arm). At most one per entry — a second standing ground extends the grounds clause and keeps the first date, because a tag per ground would grow an entry against the cap, the refusal recorded above for a tag per recurrence.
Who writes the tag and who removes it. The session recording the standing fact on the entry writes it in the same commit as the grounds. Routinely that is a closing stage keeping a worklist row in Deferred for a reason no trigger carries; equally, it is any session landing a direction or decision that an entry stays deferred. Whoever may revise the fact removes it, by the classes of lifecycle-kit/SPEC.md §The steering vocabulary: a direction at a later scoping or authoring stage, a decision by the role that made it, a ruling only through the consult skill. A worklist row is never grounds for deleting the tag.
It is a declared tag, never a scan for a prose marker. A standing fact argued in prose has no fixed spelling, so a phrase list misses the next spelling, and a matcher loose enough to catch it also matches prose that merely discusses eligibility. Nor is it the retired ruling tag (below) returning: that tag restated provenance the body carried, while this one has a reader, the worklist arm, and states a fact that arm cannot derive. The honest limit: nothing stops a session declaring every entry ineligible. The grounds clause is mandatory, and the worklist row carries the date and the head of the grounds under the arm’s cause cap, the whole tag on the row’s own tag line. So the review starts on a row every close already reads. That review is the only check; no gate judges grounds.
A direction’s class and date are stated inline beside its content, never as a tag. The sentence recording it carries both, operator direction, <date>. A lead’s own decision is marked as a decision, because an unmarked one reads as the operator’s word and freezes a call that should stay revisable. The classes are lifecycle-kit/SPEC.md §The steering vocabulary; the authority vocabulary is the consumer’s, named on its own always-loaded surface, and the kit enumerates none. The [ruled:] tag is retired, and §check-queue-entry-budget assertion (D) refuses one: it restated facts the body carries inline and spent a counted line of a capped entry on each ruling. The inline form costs nothing and has the same reader, a later session or an operator relying on or revising a recorded direction. It is the relaying party’s own word about a channel the tracked record cannot reach. So it raises the cost of inventing a direction without making an invented one detectable, and no governed surface may state it as verification.
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 (the outbound channel) —
QUEUE_KIT_LESSON_TAGS(array of bare tag names, default empty) is the one configured exception to fixed spelling. The tag names, their sinks and their handling are consumer rule content, and 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 through the lesson-sink arm (§The lesson-sink arm), 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.
The section is consumer-optional and ships on. A consumer that leaves it out of its queue and out of QUEUE_KIT_REQUIRED_SECTIONS runs without the lesson channel, and every reader of the section reads no lesson, which is that consumer’s off. The default required set names it, so an adopter keeps the channel until it chooses otherwise.
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). Neither are the entry-history arm (§check-queue-entry-budget), the queue-history arm (§The queue-history arm), the roadmap-lag arm (§The roadmap-lag arm) and the --queue verbs (§The queue verbs). They sit outside check-gate-fixture-coverage’s derived set for the same reason, so they take bespoke tests rather than a good/+bad/ pair: gate-tests/entry-history.test.sh, gate-tests/queue-verbs.test.sh for the verbs and the history arm over one seeded history, and gate-tests/roadmap-lag.test.sh for the roadmap-lag arm.
Config is a knob file: copy templates/queue-config.knobs into the gates dir as queue-config.knobs (or point QUEUE_KIT_KNOB_FILE elsewhere) and set any knob below; defaults fill what the file leaves unset. queue-kit’s knobs are static: the binary resolves them in process from its own defaults table and the consumer’s knob file; bash gate-sdk/bin/run-gates.sh --emit knob-roster prints each one with its shape and rendered default. A gitignored queue-config.local.knobs in the gates dir is the home for a private value a tracked file cannot carry. The grammar, that .local overlay, the environment-over-file precedence for a scalar, and the refusals — a set QUEUE_KIT_KNOB_FILE that does not exist, a left-behind queue-config.sh or queue-config.local.sh, a non-empty file named by the retired QUEUE_KIT_CONFIG_FILE — are gate-sdk/SPEC.md §The knob file’s. The kit’s table validator refuses a malformed config at exit 2 with every finding — a broken grammar must not gate anything. A derived default below is written as ${NAME} for the knob it reads, or ${NAME:-<default>} so the roster’s rendered literal reads as agreement. 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_CAP— default4300cp: the per-entry size capcheck-queue-entry-budgetassertion A holds over the deferred section, as a count and a unit.<n>cpmeasures code points,<n>linesmeasures counted lines, andoffdisables assertion A while assertions B to E still run. A value outside that grammar is malformed config (exit 2).linesis a sound measure only where every line is width-bounded, so choose it besidecheck-queue-wrapand never besidecheck-md-unwrapped.QUEUE_KIT_ENTRY_CREDIT_MAX— default2150cp, oroff: the largest[cap-credit:]one entry may carry, in the cap’s unit.offdisables credits, and then any credit tag is red. The default is half the default cap, so a credited entry stays within one and a half caps. A maximum in another unit than the cap’s admits no credit, so a consumer choosinglinessets this knob in lines too.QUEUE_KIT_ICEBOX_AGE_DAYS— positive integer, default30; the age window for thequeue-indexarm’s--icebox-candidates: the defer-date filter and the recurrence-trigger limb (§The icebox tier), and nothing else.QUEUE_KIT_SLUG_MAX— default30, oroff: the code-point ceiling on a slug that enters the live set (check-task-conservationassertion B). A slug live atHEADis grandfathered at any length. A rename is a new slug, so it is bound.QUEUE_KIT_WRAP_BUDGET— default100(check-queue-wrapgate floor). It takes nooff: the budget is that gate’s one red condition, sooffwould be unregistering the gate, which the registry already offers.QUEUE_KIT_PROSE_LEADS— array of column-0 lead tokens exempt from the hygiene gate’s no-prose axis, default("Protocol:").QUEUE_KIT_PLACEHOLDER_TOKENS— array of placeholder tokenscheck-task-namesreds where an entry would stand, matched case-insensitively, default the shipped set (none,n/a,nil,tbd, a dash, …;--emit knob-rosterprints it whole); empty turns the arm off.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 link into the queue on a named surface is then a checked membership claim (§check-queue-slug-liveness). Widening it widens that gate’s trigger as well as its scanned corpus: the descriptor carriesknob:QUEUE_KIT_PROSE_SURFACE_GLOBSrather than a transcription of the value, so the kit names the knob and the value stays the consumer’s (gate-sdk/SPEC.md §The# graph:manifest). The rule reaching it is the one reaching canon-kit’s corpus knobs from another kit, which is why that rule is stated over walks rather than as a roster of members.QUEUE_KIT_CITATION_SURFACE_GLOBS— array of repo-root-relative globs naming the governed surfaces on whichcheck-queue-slug-livenessassertion B reds a retired slug citation, default empty (clean skip). It is a separate knob fromQUEUE_KIT_PROSE_SURFACE_GLOBSbecause the surfaces that cite work are not the surfaces that claim membership; the descriptor couplesknob:QUEUE_KIT_CITATION_SURFACE_GLOBSon the same ground. A consumer with a canon-kit manifest aliases it to that set.QUEUE_KIT_PRECONDITION_REGEX— the forward-precondition trigger set forcheck-queue-prose-precondition, default = the shipped phrase set.QUEUE_KIT_PRECONDITION_PAST_REGEX— the past-tense narrationcheck-queue-prose-preconditionstrips before its trigger matches, default = the shipped phrase set;offstrips nothing. A consumer who adds a forward verb to the trigger adds its past form here.QUEUE_KIT_REQUIRED_SECTIONS— array of the##headings no section knob names that must each appear exactly once (check-queue-sections), default = the iteration header (Iteration:, prefix-matched for its dynamic suffix) plusLessons Learned. A trailing:marks a prefix-matched heading; every other entry is matched exactly. Every section knob’s name — each active section, the deferred, the done and a configured icebox — is unioned in by derivation, never by a second consumer list. A section configured but absent from the file would otherwise let every assertion scoped to it pass open, the exact fail-open classcheck-queue-sectionsexists to stop. So a section renamed through its own knob carries the floor with it. A section name ending in:is malformed config, since the union would prefix-match a name every section reader matches exactly. The union is the reader’s (§The shared queue adapters), not the knob’s, so the knob holds exactly what the consumer wrote, and a binding that restates a section name is harmless. LeavingLessons Learnedout is how a consumer turns the lesson channel off (§The tag algebra); binding the knob empty also drops the iteration header from the floor.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 by the lesson-sink arm, which owns resolution and the fail-open default (§The lesson-sink arm). 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 the kit’s table 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 the table validator exits 2, per the broken-grammar contract above. 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 takes the same independent-knob shape in every kit that reads the tier: QUEUE_KIT_ICEBOX_SECTION here, CANON_KIT_ICEBOX_SECTION for the amendment lifecycle, DRIFT_KIT_ICEBOX_SECTION for the queue KPIs. 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
The shared queue adapters
native/src/queue.rs is the sole holder of the adapters every queue reader shares: the section matchers, the slug and tag extraction helpers, the defer-date parse (DeferMarks) and the roadmap parse. Both sides of every section boundary must parse identically, and one shared adapter removes that drift axis. It holds values and adapters only, never gate structure (gate-sdk’s lib/gate.sh rule). The knobs they read resolve from the kit’s static table (§Layout and configuration).
Validation is the table’s. The kit validator runs at the kit’s first knob read in a process. An empty or non-numeric knob is a broken grammar, as is a section name ending in :. The roadmap vocabulary is validated as a pair (§Layout and configuration), not per array, because the failure the check exists to stop is one array set alone. Findings are reported together under one malformed-config lead line, then exit 2 (gate-sdk/SPEC.md §The knob file).
The section names are derived here, never configured twice. required_sections() composes the set check-queue-sections reads, each name once: the titled (:-ending) entries of QUEUE_KIT_REQUIRED_SECTIONS, then each active section, the deferred, a non-empty icebox and the done section, then the list’s other entries. A non-empty QUEUE_KIT_ICEBOX_SECTION also joins the shared task set. An empty one makes every icebox matcher match nothing, so each reader degrades to “no icebox” rather than to “every section”.
That degradation is the reader’s to complete. A match against an empty pattern matches every line, so an unguarded reader of an optional section matcher degrades to exactly the “every section” reading. Every such reader tests the empty name explicitly, as is_icebox does.
QUEUE_TASK_SECTIONS (Sections::task_sections) is that composition itself, exposed: the active sections, the deferred section and a configured icebox, in configured order. The task-section matcher is built from it, and a reader that needs the sections individually reads the list (§The queue-counts arm). A reader recomposing the set from the three knobs is the drift axis a shared adapter removes.
roadmap_entries is the single [roadmap:] and [roadmap-summary:] parse, shared by the roadmap arm and check-roadmap-fresh so the emitter and the gate cannot disagree about what an entry claims. It walks the live task sections in queue order and returns one typed record per entry carrying either tag on its tag line. The record’s five fields are the [roadmap:] count, its raw field text, the slug, the [roadmap-summary:] count and that tag’s text, an absent field empty. Emitting on either marking is what lets assertion C see a dead summary, on an entry the roadmap tag has fallen off.
The in-body citation tokens (§The tag algebra), backtick_slugs and link_slugs (a same-file link’s slug-shaped fragment), are shared by the same rule. The queue-edges arm reads both for edges, check-task-names for assertion R, and check-queue-slug-liveness for assertions B and C, beside md_links, the link parse its assertion A resolves. Only the token grammar is shared; the arm keeps its attribution walk. The name-live test (tracked_stems and name_live_at, §The queue-edges arm) is here too: the arm marks a retired target with it and assertion B exempts a live name with it.
The history walk is shared. walk_history pipes the commits touching the queue file, newest first, through one git cat-file --batch child, and reads a revision older than the heading grammar through the migration arm (§The queue-migrate arm). Off it, the entry-history arm measures each revision (§check-queue-entry-budget), transitions reads each revision’s place, and the roadmap-lag arm reads a done slug’s last live entry (§The roadmap-lag arm).
recurrence_array is the strict [recurrence:] parse (§The tag algebra): the value split on ,, each token trimmed, every token a calendar-valid YYYY-MM-DD, its month 01 to 12 and its day within that month under Gregorian leap years. It returns the dates in order, or the first token that fails, and an empty value fails. recurrence_dates keeps its contract for the counting readers, dropping a malformed token, and check-queue-hygiene’s recurrence axis keeps such a token from reaching them.
The retired-set derivation is here because two arms read it, the queue-edges arm and entry-history (§check-queue-entry-budget). A history walk with its own spelling of an entry would let them disagree about which slugs ever existed. It brings git with it: these readers degrade to an empty retired set as §The queue-edges arm declares, and no other adapter here gains a program requirement.
The entry grammar is here by the same rule. entry_heading (a ### <slug> entry or a #### <slug> sub-task), is_tag_line and entries are read by every in-crate queue reader, live_slugs and the edge aggregator’s attribution walk among them. entries yields every entry in the task sections with its tag line and its extent, heading to the next heading of the same or a shallower level. The history walk asks the same question of revisions that may predate the heading grammar, so it reads both leads: entry_heading, and bullet_slug, the retired - **<slug>** lead kept for that walk and the migration arm alone (§The queue-migrate arm). One grammar keeps the retired set and the live set agreeing about what an entry is.
The consumer’s private values ride the gitignored queue-config.local.knobs overlay in the gates directory. It outranks the tracked file and stays in the gates directory even when QUEUE_KIT_KNOB_FILE relocates the tracked one (gate-sdk/SPEC.md §The knob file). A private sink value tracked in queue-config.knobs would itself be the leak, so the name is tracked and the value gitignored, the split msg-patterns.local.list makes. The overlay is optional, and its absence is fail-open.
The queue-index arm
A non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), reached as run-gates.sh --emit queue-index [<flags>] [<queue-file>] and registered in the emitter table as --emit-queue-index. It stays outside --list and owes no .gate descriptor, no gates.list registration and no good/+bad/ fixture pair. No shell shim dispatches to it: a shim would keep interpreted lines and add a second entry point into the emission path, which the class forbids.
The arm is configured, and its reads are declared beside it in the arm table. Its queue-kit knobs resolve in process from the kit’s static table, and GATE_SDK_QUEUE_FILE, from which QUEUE_KIT_QUEUE_FILE’s default derives, from gate-sdk’s, so a consumer override reaches it through the knob file. A hardcoded top-level flag would have no row, so the knob-file derivation, check-reads-couples and check-gate-substrate-parity could not see its read of QUEUE_KIT_ICEBOX_SECTION. The derived section matchers are the adapters’ internal spelling of these knobs, never a configuration surface, so none is a declared read. This paragraph settles a caller that reaches an arm as an arm. An in-process call from a hook module is neither the front end nor a direct binary invoke, so it owes its own answer, which delegation-kit/SPEC.md §The statusline arm gives.
The class’s dispositions live here, and §The queue-counts arm, §The queue-edges arm and §The lesson-sink arm take them rather than re-deciding them:
- the arm is a member of the arm table, and declares the knobs it reads there;
- the derived section matchers are not a configuration surface;
- a mode rides the arm’s own argv tail rather than earning a second arm;
- every mode’s stdout grammar is a contract, and an error path’s exit code collapses to 2.
One member does not take the --emit- spelling. §The lesson-sink arm’s child’s exit status becomes its own, and an emitting arm collapses every error to 2 and every success to 0. It is an Arm::Run member of the same table, which is why the table is keyed by flag rather than by family (gate-sdk/SPEC.md §The non-gate arm).
No arm takes -h/--help. Usage for a non-gate arm lives in run-gates.sh --help and queue-kit/README.md, and a per-arm flag would be a second home for one sentence. So -h and --help land on the unknown-option refusal like any unrecognised flag, and that refusal prints this arm’s usage block, the roster of its modes (gate-sdk/SPEC.md §The non-gate arm).
The arm has three modes, index, extent and icebox-candidates, selected from its argv tail; --collapse-deferred is a flag within index, not a mode. A fourth mode is refused below. A --extent slug that resolves to nothing exits 2 with the arm’s error, as every emitter error does. A distinct code would widen the class’s return contract for an error path no caller reads, since extent’s caller is a session reading two integers.
The index mode is the queue’s compact surface for task selection. It prints the iteration header line if present, then every top-level active entry as a one-line title plus tags, marked ready (•) or blocked (✗) from the [blocked-by:] tag alone. The title is the entry’s summary, its first body paragraph with each markdown link rendered as its text, capped at a module constant. An entry with no body paragraph renders as the bare slug rather than a dangling —. [blocked-by:] and [drain-exempt: <reason>] are re-echoed after the title, the two tags a picking session acts on without opening the body. A deferred row re-echoes [cost:] then [surface:], the two tags a ranking session reads the pool by (§The tag algebra). The session-context step prints the full deferred listing only at the scoping and closing stages, so those tokens are paid there alone.
--extent <slug> prints the inclusive line range of one entry, heading through body. A parent slug spans its whole subtree, and the boundary is the next heading of the same or a shallower level, ---, or EOF. The range includes the trailing blank, so a range deletion leaves no double blank. --collapse-deferred replaces the deferred listing with a one-line tally, (top): N; the deferred section has no subsections, since a ### there is an entry.
A configured icebox contributes exactly one line, <section>: N entries, to both index renderings and never an entry listing; an unconfigured one emits nothing. This output is embedded in the always-loaded session-context brief, so listing the tier would re-import the tokens the tier exists to remove, while a count keeps it visible. The tally never reaches extent, whose contract is two integers and nothing else.
--icebox-candidates prints the closing stage’s eviction worklist over the deferred section. It lists each entry whose defer date (§The queue format) is older than QUEUE_KIT_ICEBOX_AGE_DAYS and whose tag line’s [cost:] class is in the low class (§The icebox tier). A row carries that class and the entry’s size in the cap’s unit, as check-queue-entry-budget’s walk measures it (code points under QUEUE_KIT_ENTRY_CAP=off). An entry with no defer date lists as (undated), and one with no class (no tag, or a value outside the closed grammar) as (unclassed): an absent input appears rather than vanishing. The class is read off the tag line alone, never the Cost while deferred field, so the arm parses no prose.
Each row is led by the index’s mark: • for an entry §The icebox tier’s eligibility rule admits, ✗ for one it excludes. An excluded row prints its cause in place of the class, since a categorical exclusion has already settled the row. The cause is the first exclusion that holds, in this order:
- the
[roadmap:]tag; - a
[not-icebox-eligible:]tag (§The tag algebra), printed[standing] <date> — <grounds>, with(undated)for a missing date and(ungrounded)for missing grounds; - a
[recurrence:]array whose newest date, its last, is inside the age window, printed[recurrence] re-filed <date> — live triggerso the closing stage sees when the window closes; - the first live slug the entry names, in file order, standing for the class.
A cause someone wrote outranks a trigger the arm infers, so the standing tag precedes the two inferred triggers. Every cause prints its class prefix whole and caps only its variable tail, so the class deciding the row is never cut; a cut tail is on the tag line under the row’s slug heading. The cap is a module constant, not a knob: QUEUE_KIT_ATTEND_CAP budgets the always-loaded brief, and this worklist is read once at close. A slug counts as named only as a whole token, its neighbouring bytes outside the slug alphabet, because a hit inside a longer slug is a different entry.
Both filters are non-load-bearing: in a projection tool’s worklist, miscalibration lengthens or shortens a review list and never changes a disposition. The judgment is the closing stage’s, held to §The icebox tier’s eligibility rule, so the age threshold needs no policy owner. A mis-declared class shifts the list and never the ruling. A pool younger than the age threshold yields an empty worklist, which is correct and no reason to seed the tier by hand.
The worklist prints the tier’s categorical exclusions rather than filtering them, by the same rule that lists (undated) and (unclassed). All three triggers are mechanically decidable: the [roadmap:] tag and the [recurrence:] array are tag-line tags, and a named live slug is the shared adapter’s liveness test (§check-task-names). Dropping the row would turn an advisory projection into a silent disposition. It would also deny the closing stage a row §The icebox tier requires it to recover before ruling. The arm reports and the closing stage rules, and an excluded row carries its reason so no close re-derives it.
The arm writes nothing. The moves an extent bounds are §The queue verbs’.
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 is civil-day arithmetic on the operator’s local today, never UTC’s, read as drift-kit reads it (drift-kit/SPEC.md §Bundled KPIs). The arm spawns no date -d, and a today it cannot read refuses rather than guessing a zone.
This is a task-selection surface over entry headings and tag lines. §The queue-edges arm walks entry bodies to aggregate citations, a different question over the same file. They stay two arms, and the fourth mode stays refused, because folding jobs together gives one arm two output grammars.
The queue-counts arm
A non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), reached as run-gates.sh --emit queue-counts [--by <tag>] [<queue-file>], registered as --emit-queue-counts and taking §The queue-index arm’s dispositions. Its declared reads are four: the queue file and the three section knobs. QUEUE_KIT_DONE_SECTION is not among them, since Done is not a task section and the arm makes no such read.
One job: the size of each set, for a caller that wants the queue’s shape rather than its contents. It emits <key><TAB><count> lines and nothing else, one output grammar, which --by keeps. The key is the task-section name, in configured order. The optional [queue-file] positional is an argument the rule itself consumes, falling back to a knob, on gate-sdk/SPEC.md §The non-gate arm’s distinguishing test.
Under --by <tag> the key is <section>/<value>, the slash compound [roadmap: <horizon>/<track>] uses for a pair whose halves mean nothing apart. A field tag yields its first lead-line value, a bare tag its own name, and an entry carrying neither (none), so an absent input appears as §The queue-index arm’s (undated) does. Values order by first appearance in queue order. An empty section emits no line under --by, while the default key still reports its zero. The arm enumerates no tag name, so §The tag algebra stays the vocabulary’s one owner and an unknown tag reports every entry under (none).
The section set is derived, never listed: it is QUEUE_TASK_SECTIONS (§The shared queue adapters). A configured icebox is in because it is a live section, Done is out, and a consumer who renamed their sections gets their own names back. Nothing here enumerates a section name, which keeps the vocabulary inside this kit when a caller outside it renders the numbers.
The counted unit is the ### entry, the unit §The queue-index arm lists, so the two cannot report different sizes for one queue. A #### sub-task counts with its parent.
A second arm, not a fourth mode on the queue-index arm. A tally keyed by section name is a different job with a different grammar, and folding jobs together gives one arm two output grammars, the refusal that also keeps §The queue-edges arm separate. --by is not that fold: it changes the partition key and leaves one job and one grammar. The in-process caller (delegation-kit/SPEC.md §The statusline arm) receives the unchanged default.
Two callers. delegation-kit’s statusline arm calls the rendering in process at each statusline fire (delegation-kit/SPEC.md §The statusline arm). It resolves the four knobs above from the kit’s static table, and the queue file’s default from gate-sdk’s. A session invokes the arm through the front end, at the command queue-kit/README.md documents. The rendering resolves its knobs through walk::knob_scalar and knob_array, which return Result and never exit, so a malformed queue config is an Err the caller absorbs and the counter group vanishes.
The queue-edges arm
The inbound-citation aggregator: a non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), reached as run-gates.sh --emit queue-edges [--inbound <slug>] [<queue-file>], registered as --emit-queue-edges and taking §The queue-index arm’s dispositions. It reads the queue, writes stdout only, and mutates nothing. Its declared reads are §The queue-counts arm’s four.
--emit queue-edges [--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 entry’s outbound edges are its own body, which a session asking is already reading, so an outbound view has no reader. Inbound is the direction invisible without a whole-file scan. Nothing else sums an entry’s inbound edges into its cost and benefit, so a survey reading every sibling individually can still misrank a unit several of them depend on. The arm is that sum, which also makes splitting an entry safe: a split scatters an entry’s weight across siblings, and this adds it back up.
Targets print in queue order, 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, which is the normal case, not a finding. A --inbound slug that is neither live nor retired exits 2 with a message on stderr, so silence means “no inbound edges” and nothing else. Its callers, two stage steps a session reads, read no exit code, the --extent ground (§The queue-index arm).
The resolution set is the live slugs plus the file’s retired ones, and a citation of a retired slug is an edge like any other, marked as such. Retired targets print after the live 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, and an invented one would be a datum with no reader.
A retired slug and a retired name are different questions, and the block answers both. The history test says an entry is gone, and where its name shipped as live mechanism, citations of it are current prose. A target whose slug is the basename, less its final extension, of a tracked file renders as <slug> (<N> inbound, retired — name live at <path>). <path> is the first such file in tracked order, a further match noted as (+<M>). The input is the tree listing, not a curated roster, on the ground §The tag algebra gives for the [surface:] value set. The test under-claims: a live name that is no file’s own stem is unmarked. It adds evidence and filters nothing.
A retired slug is one that headed an entry in some earlier revision of the queue file and heads none now. Before the heading grammar, the retired bullet grammar’s bold lead is its heading. That discriminator keeps the report readable: every backticked token resolving to no live entry would be dominated by commit SHAs, content digests and ordinary words, because the token grammar [a-z0-9][a-z0-9-]* is no slug detector. The retired minority of that remainder is the half a reader wants, since a citation sequencing a session against work that is gone falls in it.
One arm, one job. The job is aggregate in-body citations by target, and a retired target is a target whose entry was disposed of, not a second question, so §The queue-counts arm’s refusal holds.
The retired set is derived from the file’s own history, so nothing is maintained. It lives in the shared adapter, which entry-history reads too (§check-queue-entry-budget). One git log -p --format= -- <queue-file> pass at start-up matches added, removed and context lines against the entry grammar §The shared queue adapters owns, the retired bullet lead read beside it. git is the arm’s one program requirement, recorded in prose rather than a --needs declaration for the reason §The lesson-sink arm states. The pass costs an arm’s budget, not a gate’s: well under a second on a queue file of roughly fifteen hundred revisions, scaling with history depth rather than queue size.
Two degradations are declared. A queue file outside a git work tree, or an absent git, yields an empty retired set, and the arm prints its live block alone, which misleads no reader. A shallow clone, or a history rewritten under the file, reports fewer retired slugs and never more: history is whatever the clone holds, so the report under-claims and never invents. A queue file in a repository git refuses is neither: every reader exits 2 on the crosser’s refusal (gate-sdk/SPEC.md §The crate’s crosser), since an empty set there would read each retired slug as never having existed.
Each edge carries the citing entry’s slug and its citing line verbatim, surrounding whitespace stripped. The relation’s kind is already written there, in the citing author’s words, so no relation vocabulary is declared. A passing mention is visibly one once its line is on screen, so a reader discards it at a glance: recall is what the surface needs, and its noise self-identifies.
A citation is attributed to the nearest preceding entry heading, so a sub-task cites in its own name, and a tag line contributes its [blocked-by:] tags alone. In a body line both a same-file link and a backticked token are read, and resolution decides the block. A link names a live entry by assertion R’s rule (§check-task-names), and a backticked token a retired one. The token grammar and the name-live test are the shared adapter’s (§The shared queue adapters); the attribution walk has this arm as its one reader and stays here.
No tracked projection, and no freshness gate. A tracked artifact needs a reader who cannot run the arm, and the one consumer is a session with a shell; the roadmap is tracked because its audience is outside the repo. A committed copy of edges over a repo’s highest-churn file would restale on nearly every queue edit, a per-commit regeneration tax for no reader. Derivation-first’s generate-and-freshness-gate clause governs a needed copy, and this one is not. The queue-index arm is ungated for the same reason.
Not a gate, either. An entry with no inbound edges is normal, and one with many is no defect. Redding a backticked token that resolves to no live slug is refused, since those are overwhelmingly legitimate citations of landed work (§The tag algebra). check-queue-slug-liveness owns liveness where a dead reference is a false claim: its assertion B reds a retired citation on the governed citation surfaces, and never inside the queue file, where it would red good prose. A retired edge here is a finding to read, never a violation.
Two named readers, at two transitions, because a report nobody reads is the failure this listing ends:
- scope, at its ranking survey — the primary: a citation of disposed work is a false premise at the point a session decides what to promote. Scope already runs this arm there, so the retired block rides that call.
- close, at the gap-inbox drain — the corrective transition, where an instance found is fixed inline on its owning entry. The drain reads a different file, so this is a new command in that step, and no new mechanism.
Neither stage gains a gate and neither gains a refusal.
The queue-history arm
One entry’s life, read off the queue file’s history: a non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), run-gates.sh --emit queue-history <slug> [<queue-file>], registered under --emit-queue-history and taking §The queue-index arm’s dispositions. It reads the queue and its history, writes stdout only, and mutates nothing.
- A transition is a commit at which the slug’s place changed. A place is the task section heading its entry, the done section when a bare done line carries it, or
(absent). Each transition carries the commit, its committer date, its subject, and the place before and after. - The walk is entry-history’s, one adapter both arms read (§The shared queue adapters). It stops at the first revision, walking back, in which the slug is absent after being present; the move out of that revision is the filing. A slug neither live, done nor retired is refused before any blob is read.
- The report is the slug on its own line, then one indented row per transition, oldest first:
<YYYY-MM-DD> <short-sha> <from> -> <to> <subject>. Oldest first, because the question it answers is the entry’s story, filed to promoted to done; entry-history’s newest-first order answers a different one, the most recent fall. - Exit 0 with the report, and 2 on a usage error, a refused slug, or a repository git refuses (gate-sdk/SPEC.md §The crate’s crosser). There is no 1, on entry-history’s ground. The declared reads are the queue file and the four section knobs a place reads,
QUEUE_KIT_DONE_SECTIONamong them. - Degradations, the retired set’s own (§The queue-edges arm): a file outside a git work tree, or no
git, yields no transitions and prints the slug alone; a repository git refuses exits 2; a shallow clone under-claims.
Why a separate arm. entry-history reports falls in counted size, four fields and no verdict. A section transition is a different question over the same walk, and folding it in would give that arm two output grammars (§The queue-counts arm’s refusal). Its readers are a session reconstructing an entry’s life, and the demote and thaw verbs (§The queue verbs).
The queue-migrate arm
A consumer upgrading queue-kit meets the entry grammar’s change from bullets to headings (§The queue format), so the kit ships its migration: a non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), run-gates.sh --emit queue-migrate [--write] <file>, registered under --emit-queue-migrate. It declares the four section knobs it rewrites within. The arm converts as follows.
- Each task-section bullet entry,
- **<slug>** …, becomes### <slug>, and an indented bold-slug sub-task becomes#### <slug>. - Every bracketed tag from the lead line goes onto one tag line directly under the heading. Each
recurrence:,roadmap-summary:andnot-icebox-eligible:body line joins that tag line as its tag (§The tag algebra): the self-naming slug is dropped, and the recurrence dates become the comma-separated array. - The lead line’s remaining prose opens the body, a leading
—separator dropped. - A continuation line opening with a list marker or a bold lead-in starts a new paragraph. A bold lead-in is a bold span whose own text ends in
.or:, or any bold span after a paragraph that ends a sentence. The paragraph ends a sentence when its text so far, any lifted declaration line not counted, ends in.,:,?or!past closing emphasis, a backtick, a bracket or a quote. Every other continuation, a bold phrase a wrap left at a line start included, joins its paragraph with one space, its indentation dropped. - A single-backticked slug naming a live entry other than the citing one becomes the same-file link,
[<slug>] (#<slug>)— the remedycheck-task-namesassertion R names, so an upgraded queue does not red on its first commit. - Done bullets, lessons, headings and preambles pass through unchanged, and a file holding no bullet entry is returned as it stands.
The arm prints the rewritten text, or with --write rewrites the file in place, checked. A declaration value holding [ or ] cannot become a tag, so the arm refuses the file at exit 2 and names the line. Its postcondition is checked on every run, not only in a test. After the rewrite the shared adapters must read the same live slugs, the same tags (the former declarations included) and the same done set as the retired grammar read before it. A rewrite that would change any of them is refused with nothing written. A unit test holds the conversion over a queue exercising every rule above.
A reader comparing across the converting commit reads the older side through the arm. The conversion applies only to a revision holding bullet entries and no entry heading. check-task-conservation reads a bullet-grammar HEAD through it, so the converting commit is conserved against its own pre-image and every slug live before it stays grandfathered under assertion B. The entry-history arm measures every older revision the same way, so an entry’s counted history runs through the commit that converted it.
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 and taking §The queue-index arm’s dispositions. 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.
Its table row is what makes its reads visible: without one, the knob-file derivation, check-reads-couples and check-gate-substrate-parity would stay green over an undeclared read. The knob column declares QUEUE_KIT_HORIZONS and QUEUE_KIT_TRACKS as consumer configuration, the provenance seam holding. The crate ships the projection grammar, the headings, the bullet shape and 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, since the shared adapter scopes the scan and an undeclared knob is an unread one. It reads the live task sections only, never the done section, because a shipped item is history, not direction. The icebox is walked on purpose though no icebox entry is projectable: excluded, a [roadmap:] tag 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, since a section that vanishes when it empties reads as a page that forgot it. An empty horizon is its heading alone: a generated section cannot have been forgotten, so its empty body already says nothing is queued there (doctrine-kit’s Absence statements rule). One blank line separates consecutive horizons, filled or empty. - Under each, one bullet per tagged entry, in queue order:
- [<slug>] (<queue-path>#<slug>) *(<track>)* — <summary>, where<queue-path>is the queue file relative to the page’s own directory, so the public page links to the record andcheck-md-refs, which scans the page, resolves each anchor. With no page configured the queue path is printed as the arm read it. - The summary is the entry’s
[roadmap-summary:]tag (§The tag algebra), printed verbatim with whitespace runs collapsed. Nothing else in the entry is projected — not the tag line’s other tags, 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 summary contributes no bullet at all, so unmarked prose cannot reach the page even with the gate bypassed.
The block ends on one blank line, whether or not its last horizon is filled. 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, for a consumer registering check-queue-wrap — the tag line is a shared budget there, and [roadmap:] and its summary compete for it with every other tag the entry carries, so a long summary beside a [spec: <file>] tag can overrun the floor. The relief is a shorter summary, not a wider floor: widening it to make room for a public page would trade the wrap convention for a presentation one. Under check-md-unwrapped the tag line has no width budget and the limit does not arise.
Ordering inside a horizon is queue order, not a rank, and the page’s framing says so. The three-to-five items per horizon band is editorial posture, stated in that framing and not gated. A horizon holding two items is a true state of the queue, and redding 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 three assertions over QUEUE_KIT_ROADMAP_FILE:
- (A) Freshness. The committed marker block byte-matches the roadmap arm’s emission, called in-process on the
check-footprint-fresh/check-value-rollup-freshmodel, and the red output names the regen command. 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 one[roadmap-summary:]tag — 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 summary on an entry whose tag line carries no[roadmap:]tag is a violation too: a dead marking is what a dropped tag looks like from the page’s side, and the roadmap tag is what decides projection.
Assertion B runs before assertion A. 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. The order is a correctness property of the pair, not an implementation accident.
Assertions B and C live here rather than in check-tag-lead-line because this gate already loads the horizon and track vocabularies and the shared [roadmap:] parse. In the lead-line gate they would load a value roster it has no other use for. check-tag-lead-line still governs both tags on their placement axis.
Calibration: QUEUE_KIT_ROADMAP_FILE empty is a clean skip, the 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. Without it, no fixture avoids reading the live queue, and the pair’s verdict would turn on tree state outside the fixture directory.
The fixture pair proves the comparator and the two-argument plumbing, never the emission. Both cases steer assertion A off the live emitter, so the pair would pass an arm with no implementation. The emission’s projection rules are driven by the roadmap arm’s behavioral test, gate-tests/roadmap.test.sh.
The declaration is checks/check-roadmap-fresh.gate — hermetic, precommit, binary-dispatched. Its couples= names every crate module the gate reaches transitively, including the emit module and the two it shares with the emit side. The pre-commit hook’s 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.
The roadmap-lag arm
Which roadmap entries trail the work landing under them: a non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), run-gates.sh --emit roadmap-lag [<queue-file>], registered under --emit-roadmap-lag and taking §The queue-index arm’s dispositions. It reads the queue and its history, writes stdout only, and mutates nothing.
- A slice of an entry P is a slug X in the done section whose entry links P at the newest revision in which X headed one, and which P’s body still cites. That is the bidirectional citation an authorized split writes (§check-queue-entry-budget), read on the child’s side through the history walk because the done move took the child’s body (§The shared queue adapters).
- A row is a top-level entry outside the icebox with at least one slice, whose
[roadmap:]horizon is not the firstQUEUE_KIT_HORIZONSmember or which carries no[roadmap:]tag. It prints as the slug, a tab, the horizon or-, a tab, and its slices comma-joined, in queue order. No row means nothing lags. - Exit 0 with the report, and 2 on a usage error, an unreadable queue file, or no configured horizon, as §The roadmap arm exits. There is no 1: the arm reports and rules nothing.
- Degradations are the retired set’s (§The queue-edges arm). With no git work tree, a repository git refuses, or no
git, no slice can be confirmed, so the arm prints no row and says why on stderr. A shallow clone under-claims.
Its reader is the closing stage, after the moot sweep and before the done section is cleared, which is when the done section holds the iteration’s exits (lifecycle-kit/templates/stages/close.md, step 5). The arm reports and the session rules: it re-tags where an entry’s recorded horizon condition settles the move and routes the rest with the motion read’s findings. A gate is refused: a lagging horizon is a true state of the queue until the party who sets direction moves it, the ground §The roadmap arm gives for not gating a horizon’s size.
Why a separate arm. The roadmap arm’s output is the page’s body, which check-roadmap-fresh byte-compares. A lag report is a second grammar over a second input, the done section and the history walk, so it stands beside that arm and not inside it (§The queue-counts arm’s refusal).
Honest limits. A slice its parent records only in prose has no citation pair, and a reader finds it. Two entries citing each other for another reason read as a split, and so does a split child the moot sweep retired rather than a landing. The row names the slice, so the reading session discards either case at a glance.
The lesson-sink arm
The outbound channel’s router: it 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 on its stdin, so the sink may reformat the body into a downstream backlog’s own grammar. The command’s exit status becomes the arm’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 <workflow-dir>/<tag>-harvest.md. That manual-drain default keeps a fresh clone with no overlay closing cleanly, and preserves the staging file’s documented reclaim path. The consumer close skill invokes the arm, so the tracked skill names the mechanism and never a sink value, and a private sink command lives in the queue-config.local.knobs overlay (§The shared queue adapters).
A non-gate arm outside the --emit- family. It is reached as run-gates.sh --lesson-sink <tag> and registers as an Arm::Run member beside --statusline and --usage-poll (gate-sdk/SPEC.md §The non-gate arm), with a matching case in the front end’s argument grammar (gate-sdk/SPEC.md §run-gates). An emitting arm collapses every error to 2 and every success to 0, which would erase the sink’s status, and Arm::Run returns the code. Its ARM_UNAVAILABLE_STATUS is 2, as --usage-poll’s is and unlike --hook’s or --statusline’s. It gates no tool call, and its caller is a close-stage step whose failure must be visible. It is not a harness-integration arm: no harness event reaches it.
The adopter’s configuration seam holds. QUEUE_KIT_LESSON_SINKS is a keyed knob of the kit’s static table, its value a shell string crossing verbatim and run as bash -c "<value>" with the body on its stdin, and a private value lives in the overlay. gate-sdk/SPEC.md §The knob file’s command-knob bullet governs *_CMD knobs, and this knob is not one. bash is the arm’s one program requirement, recorded in prose because --needs answers over the .gate-declared registry and a non-gate arm carries no descriptor.
The child’s status is reported, not inherited. The arm spawns the sink through proc::run_streamed(…, Stderr::Inherit), whose code() carries the crate’s signal-aware exit-code spelling, so a signal-killed sink reports 128 + n. The sink’s stderr stays on the terminal. Its stdout is captured and re-emitted on the arm’s own stdout, not reached through descriptor inheritance.
stdin is buffered, not streamed: the arm reads the body to completion before it spawns the sink. A lesson body is one queue entry’s prose, so the bound is the queue’s own per-entry cap (§check-queue-entry-budget), and an arbitrarily large input is held whole.
The queue verbs
The queue’s write side: a family of verbs on the gate binary, each one move or stamp on the queue file, each followed by the queue’s own gates. It is reached as run-gates.sh --queue <verb> <operand>…, an Arm::Run member of the arm table whose first operand selects the verb, the shape --hook and --wait-probe carry (gate-sdk/SPEC.md §The non-gate arm). One arm keeps one declared read roster and one exit contract, and each verb is still its own allowlist line for a consumer granting narrowly: Bash(<door> --queue promote *) grants that verb alone. guard-kit’s recommended allowlist grants the front end and the binary door (guard-kit/templates/settings-allow.json), so the verbs are granted wherever it is adopted. The front end’s -*) case passes the flag through unchanged.
A verb is a checked text transform, then the queue’s gates. It computes the new file and checks its postcondition on the shared adapters’ live and done sets (§The shared queue adapters), writing only when that holds — the queue-migrate precedent (§The queue-migrate arm). The whole file is written to a sibling temporary file and renamed over the queue file, so an interrupted verb leaves the old file or the new one. It then spawns its own executable as --run --gates-dir <gates-dir> --for <queue-file>, the argv the front end composes for --for (gate-sdk/SPEC.md §run-gates), and its status carries that verdict. It spawns rather than calling the battery in process, the rule for a dispatching arm, so a member that panics cannot take the verb with it.
- Exit status. 0: the move is written and the post-check is green. 1: the move is written and the post-check is red; its findings are printed, and they are the session’s worklist before commit. 2: nothing is written, whether for a usage error, an unknown verb, a refusal named below or an unreadable queue file. A stamp already present writes nothing, runs no post-check and exits 0. An absent binary exits 2 at the front end.
- Output. One line per act on stdout, naming the slug, the section it left and the one it entered, and each tag the move dropped or added; then the post-check’s own output.
- The postcondition compares
live_slugsanddone_slugsbefore and after the transform, as multisets, against the change each verb states below; a verb stating none leaves the live set unchanged. A transform that breaks it is refused with nothing written, and the refusal names the difference. It is the check that catches a wrong extent, such as an open sub-task riding its parent’s Done move. - Declared reads.
QUEUE_KIT_QUEUE_FILE,QUEUE_KIT_ACTIVE_SECTIONS,QUEUE_KIT_DEFERRED_SECTION,QUEUE_KIT_ICEBOX_SECTIONandQUEUE_KIT_DONE_SECTION, and the runner’s roster with itsEVERY_REGISTERED_KNOBsentinel, as a dispatching arm declares its callee’s reads. - Operand scope. Every verb takes a top-level
###entry, anddoneandrecura####sub-task too. Any other verb given a sub-task refuses and namessplit.
A verb moves an entry; it never decides whether the caller may. Who may write the queue, and when, stays lifecycle-kit’s (lifecycle-kit/SPEC.md §The state machine, §The committed gap inbox), so a mid-iteration session still files a gap rather than calling a verb. A verb never stages and never commits: the move lands in the commit the session is already making, which is where every same-commit rule the moves carry already puts it.
Five verbs read nothing but the current file:
promote <slug> <section> [--spec <file>]. The entry leaves the deferred section and becomes the last entry of<section>, which must name an active section. Its[cost:]and[surface:]tags are dropped, the move §check-deferred-board-tags assertion B checksums.--specwrites[spec: <file>]first on the tag line, replacing one the entry carried and creating the tag line where it had none; every other tag stays. Refused: a slug not in the deferred section, and a<section>that is not active.done <slug>. The entry’s extent leaves whichever task section holds it, and- <slug>becomes the last line of the done section. Every[<slug>] (#<slug>)in the file becomes`<slug>`, the retired citation (§The tag algebra). Postcondition: the slug leaves the live set and joins the done set, and the live set is otherwise unchanged. Refused: a[roadmap:]-tagged entry in the deferred section or the icebox, since no drain may retire one (§The icebox tier). An active roadmap entry that landed takes the move.clear-done. Every bullet under the done section is removed, and the heading stays. Postcondition: the done set is empty and the live set is unchanged. It is the tail of the done section’s lifecycle, the closing stage’s clear (lifecycle-kit/templates/stages/close.md).icebox <slug> <sentence>. The entry becomes### <slug>, a blank line and<sentence>, the last entry of the icebox section, every tag dropped (§The icebox tier’s grammar). Refused: no icebox configured; a slug not in the deferred section; a sentence that is empty or spans a line break; and an entry carrying[roadmap:]or[not-icebox-eligible:], the two exclusions a tag declares. The judged limbs of eligibility stay the closing stage’s, read off--icebox-candidates.recur <slug> [<date>].<date>, by default the local today--emit queue-indexreads (§The queue-index arm), is appended to the entry’s[recurrence:]array, which is created at the end of the tag line when absent. Postcondition: the live and done sets are unchanged. A date equal to the array’s last element writes nothing, since the stamp is idempotent per slug and date. Refused: an array or a<date>the strict parse rejects (§The shared queue adapters), and a date earlier than the array’s last element, because dates are appended in order. A slug found only in the done section is refused too, since it is a new defect and not a recurrence (lifecycle-kit/SPEC.md §The committed gap inbox).
Two verbs restore what an earlier move took. Each finds its move among §The queue-history arm’s transitions and reads the revision before it:
demote <slug> [--cost <class>] [--surface <entry>]. The inverse ofpromote, the move canon-kit/SPEC.md §Merging an amendment step 4 names. The entry leaves its active section for the deferred section, dropping[spec:]and[drain-exempt:], and the verb reads the revision before the newest transition from the deferred section into an active one. The[cost:]and[surface:]values the entry carried there are restored, a flag overriding its tag, and the pair is written first on the tag line. The entry lands after the entry that preceded it there; where that one has left the deferred section the walk tries the one before it, and with none left the entry leads the section. Where no such transition exists — the entry was filed straight into an active section, or history is unavailable — both flags are required and the entry becomes the section’s last; without them the verb refuses.thaw <slug> [--cost <class>] [--surface <entry>] [--date <date>]. The icebox’s return route, taken on a judged recurrence (§The icebox tier). The verb reads the revision before the newest transition from the deferred section into the icebox. The entry’s whole extent there — heading, tag line and body — replaces the one-sentence form at the place the predecessor rule above gives, so the move itself does the recovery §The icebox tier makes mandatory before a ruling. A flag overrides its restored board tag, and<date>, by default the local today, is appended to the[recurrence:]array underrecur’s rules, since the return is a recurrence. Refused with nothing written: no such transition, because there is no body to restore.
split <parent> <child> <parent-sentence>, the child’s tag line and body on stdin, performs the mechanical half of an authorized split (§check-queue-entry-budget). ### <child> and the stdin text are inserted after the parent’s extent, in the parent’s section, and <parent-sentence> becomes the last paragraph of the parent’s own body, before any sub-task. Refused: <child> is not a valid slug, or is already live or done; the stdin text carries no [<parent>] (#<parent>) link; <parent-sentence> carries no [<child>] (#<child>) link. The two links are the bidirectional citation that section requires, so the verb cannot write a split that fails it. Postcondition: the live set gains exactly <child>. The authorization stays with the session that grants it: the verb cannot see a grant, and the refusal to let a blocked session split its own entry is unchanged.
Refused, with grounds. A write mode on an existing read arm: --emit collapses every error to 2, and a verb’s 1, a move written under a red post-check, is the status its caller acts on. A verb that refuses on a red post-check and writes nothing: a Done move leaves every [blocked-by:] on the moved slug stranded, and clearing each one owes a prose rephrase in the same commit (§check-task-names), so a refuse-on-red verb could never make that move.
check-queue-hygiene
Invariant: the queue contains only tasks, tags, and section structure, and its prose points at nothing by a coordinate that goes stale. Six axes:
- No HTML comments — provenance belongs in git history.
- No exact-duplicate lines among the non-blank, non-
---lines, the copy-paste artifact. A tag line is exempt, because two entries may legitimately carry identical tags and the heading above each tells them apart. - No column-0 prose outside an entry. Every column-0 line is a heading, a bullet,
---, a configuredQUEUE_KIT_PROSE_LEADStoken, or a line inside an entry’s extent. Prose between a section heading and its first entry is the shape that carries protocol duplication, and it is banned because semantic duplication is not mechanizable. - No line-number citation. A
<path>:<n>or<path>:<n>-<m>token whose path ends in a dot-extension is red. A queue body cites another file by§Section, by a symbol beside its path, or by a quoted literal. - No count pickaxe over the queue file. An inline code span holding a
git logcommand with-Sand the queue file as its path is red, since §The icebox tier’s recovery is-G. - No malformed recurrence date. A tag line whose
[recurrence:]array fails the shared strict parse (§The shared queue adapters) is red, because each counting reader drops the bad token and undercounts silently.
Calibration: indented lines are never flagged on the prose axis, so a section preamble is indented; the lead-token allowance is a whole-line lead match, not a substring. An entry’s extent is read from the headings alone — a ### or deeper heading opens it, and a #/## heading or a --- rule closes it — so the axis takes no section knob. 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. The citation axes are lexical and bounded. A line number spelled in prose (“lines 642 and 669”) passes, and so does a -S pickaxe over another file, which is correct wherever the literal leaves that file. The queue file is matched by the configured name’s basename, so a recipe written with the default path or with <queue-file> both read. There is no valve, which matches the other axes. A false positive is rephrased, since the axes read prose and every hit has a stable spelling available.
Under a wrapped queue, the duplicate arm’s commonest true positive is a wrapped fragment: promoting several entries in one commit gives them a shared sentence whose wrapping lands one identical line in each. The prose is correct and the collision is an artifact of where it broke, so the fix is to re-wrap, not to reword as the red suggests — and a check-queue-wrap red on the widened line commonly follows. Under an unwrapped queue a paragraph is one line, so a duplicate is a whole repeated paragraph.
check-queue-sections
Invariant: the queue file carries each heading of the required set 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 QUEUE_KIT_REQUIRED_SECTIONS plus every section knob’s name, composed in the reader by required_sections() (§The shared queue adapters), which is what closes the half-configured hole by construction. The # graph: couples the queue file at tier=precommit.
check-queue-entry-budget
Invariant: a deferred entry is a costed filing, bounded on three sides. Above, so it is not an inlined amendment; below, so it is not a flag-and-skip; and 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, and the third about what the cap spends to stay inside it. Five assertions:
- (A) Size. No deferred entry exceeds
QUEUE_KIT_ENTRY_CAPin its unit, and one carrying a[cap-credit:](§The tag algebra) is held to the cap plus its credit. A sub-task nests inside its parent’s extent and is measured as its own entry too.- An entry’s extent is its heading through the line before the next heading of the same or a shallower level, or a
---rule. It is the range thequeue-indexarm’s--extentyields, so the range the gate measures is the range an eviction deletes. - Its size is measured over the whole extent, the heading and the tag line included, and nothing is discounted. In code points, each non-blank line counts its code points with leading and trailing whitespace trimmed, plus one for each break between two of them, so a reflow of the same text moves the size by nothing. In lines, the size is the count of the extent’s lines.
- A credit is red when it is malformed, when its unit differs from the cap’s, when it exceeds
QUEUE_KIT_ENTRY_CREDIT_MAX, when credits areoffor the cap isoff, or when the entry fits the cap without it. That last is a stale credit, and the fix is to delete the tag. An active entry’s credit is inert, since active entries are uncapped, and it is checked again if the entry returns to the deferred section.
- An entry’s extent is its heading through the line before the next heading of the same or a shallower level, or a
- (B) Icebox shape. Every icebox entry is its heading and exactly one lead line, its one sentence; a tag line, a second paragraph or an empty body 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; a sub-task is covered by its parent’s costing. Deliberately not applied to the icebox: a one-sentence 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. - (D) Retired grammar absent. No deferred or icebox entry carries a
[ruled:tag (§The tag algebra): a ruling’s provenance is stated inline, and a tag restating it is the second tier the retirement removed. Refused rather than merely uncounted because the grammar’s inducer was habit, and habit re-mints a spelling the record still shows. - (E) Defer date resolves. Every top-level deferred entry resolves a defer date under §The queue format’s definition. An undated entry is well-formed to every other assertion and silently wrong to both date readers: it never ages out of the icebox-candidates age filter and is invisible to the deferred-age KPI. The walk reads the mark into the innermost entry only: a body line after a sub-task’s heading is the sub-task’s. That is the scoping the icebox-candidates arm gives the same shared parse, so the gate and the arm cannot disagree about which entries are undated. Binds top-level entries only, as (C) does.
Calibration: QUEUE_KIT_ENTRY_CAP defaults to 4300cp, fifty counted lines at a real deferred pool’s median of 85 code points per counted line. It is rounded up to admit every entry a fifty-line cap admitted on that pool with every line counted (largest 4268). The cap keeps compression from regrowing rather than forcing 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 a knob for exactly that reason.
The unit is the consumer’s choice and so is having a cap at all, because which measure bounds a record is policy rather than mechanism: the kit ships the measures, and a consumer binds one or sets off. 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 tag line (§check-tag-lead-line), and a lead-in split by a hard wrap reads as absent.
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 every assertion holds, the clean line is followed by one line per Deferred entry, a sub-task included, naming its headroom to the cap in the configured unit. An entry carrying a standing credit names its headroom to its credited limit instead, marked credit +<n>. The figure is assertion A’s own size, one subtraction away, so it costs no new computation and mints no new name. A session sizing an edit reads it rather than hand-rolling a probe, which would risk a second spelling of the cap, a second parse, and a mismeasured entry boundary reading as a false overrun. A --headroom <slug> mode on the queue-index arm is refused: it would mint a name against that arm’s standing no-fourth-mode refusal.
No collision counter is minted. A count of how often the cap displaced a mandated write would be a second, hand-maintained measurement of what the headroom line already prints. Its criterion would be collision history, where the split test below reads an entry’s composition, so what a firing count would evidence, the split criterion decides directly. The entry-history arm below is not that counter: it answers which commits reduced one named entry, on demand, and counts nothing across the pool. An arm that grew a pool-wide count would re-mint what this paragraph refuses.
No arm reds on the compression act. A breach of the compression contract below has one machine-readable shape: a deferred entry’s counted extent falling in a commit that also lands a ruling line. That is equally the shape of the contract’s own relief, compress by answering, so an arm reding on it cannot tell the correct act from the defect and no such arm is owed. What the refusal binds is the verdict, never the instrument. An instrument that reports those commits and judges none of them asserts nothing, which is why the entry-history arm below stands beside the refusal rather than reversing it.
Why nothing is discounted. The declarations are tag-line tags (§The tag algebra), each at most one per entry, and a recurrence costs one date per re-filing, under 1% of the default cap, so a discount would have nothing to protect. A tag still cannot be spent to seat content, and the cap’s third side above still binds.
A mandated write is the class the cap has no self-served answer for. It is a write on a deferred entry that another governed contract obliges the writing session to make in the commit it is already making, citable to that contract by name. Two instances exist, both already in the tree:
- a judged
recurrence:date and the grounds it is read from, which land in the judging session’s commit because that commit is the audit artifact (lifecycle-kit/SPEC.md §The committed gap inbox); - a ruling recorded onto the entry it rules, its content with its inline provenance, under the recording-in-the-moment rule below and 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 its mechanized instance, and any session recording a ruling — with no new trigger, field or knob. Its consumers are that same session reading this section when assertion A blocks it, the path the gate’s failure text 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, since a class that excludes nothing is not a class: the session’s own evidence for a claim, a further ground for one already made, a cross-reference it would like to add, a correction it could equally make later. 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.
An answer is durable on a governed surface before the compressing commit. Answering a ground means resolving it, and a resolution the compressing commit leaves nowhere is indistinguishable from a discard, the entry’s extent being its only artifact. So the answer lands in the surface that owns it first, and the entry keeps a pointer to it. This is the sibling of the rule canon-kit states for the adjacent act: prose worth more than the entry’s own lifetime belongs in a governed surface before the landing commit (canon-kit/SPEC.md §check-amendment-queue). An entry leaving the queue and an entry shedding half its body are one information event at two scales.
The honest limit is canon-kit’s too: no gate reads this, because the loss is indistinguishable from an ordinary edit. Its routine discharge costs nothing, because a ruling already lands on the entry as a pointer to the record or specification that owns it, never as prose. What this clause adds is that the pointer’s target exists first.
- 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 same-file link in the entry body. That link is already a citation under §The tag algebra and already aggregated by the queue-edges arm, so it needs no declaration of its own. A
relocated: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. 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. The mechanical half is
--queue split(§The queue verbs), which writes the bidirectional citation below and refuses a split lacking it. A session blocked by the cap does not self-serve that, the same lineage as acheck-stage-entryassertion C waiver (lifecycle-kit/SPEC.md §check-stage-entry), so the gate’s failure text cites this section rather than inlining the recipe. The authorizing session may answer the ask with a split, a credit (§The tag algebra’s[cap-credit:]), or a denial, and the split criterion still decides whether a split fits.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. 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. Splitting them mints the ranking peer competing with its own parent that the two-acts paragraph below names as the sharpest displacement shape, so compression by answering stays the relief there.
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 an input to the authorization, read at the collision. The blocked session states which side of the test its entry falls on when it asks, and 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 each firing is answered by the cheapest relief rather than by composition. A denied split is not a dead end. An entry that fails the test still has compression by answering and the self-served relocation, the reliefs it should have been using. 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 same-file link §The tag algebra rules a citation. A
split-of:declaration is refused on 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 link costs no line of its own, because it rides prose the split is writing anyway. Bidirectional, not child-to-parent alone. A parent with no citation reads as an entry whose deliverable set silently shrank, with no surviving statement of what left or why. Its reader re-derives the missing half as a gap — the information loss the compression rule forbids, arriving through a split.The pair is a live edge only while both slugs are live, on §The tag algebra’s resolution rule, and the parent reaches that bound first. A
## Doneentry is a bare slug, so the parent leaves the live set and takes its citation with the rest of its prose, and the Done move rewrites the child’s link to it into a retired citation. That is correct: the parent-side citation exists to stop a live parent reading as shrunk, and a completed parent has no open deliverable set to misread. So 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 stops the sharpest displacement shape — an entry too full to hold its own design half, the half filed as a separate entry competing with its own parent — from becoming routine under a blanket relief.
Dropping the authorization for the narrow act is safe because a mandated write is not discretionary: its obligation belongs to another surface and is citable, so a reader can check the trigger even though no gate reads it. The act is also 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: 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 by the session that needs the room. Assertion A’s bound is the amendment-inlining line above, not a length preference, so raising the number for everyone moves the number without moving the line. The entries that would claim an exception are the likeliest ungoverned amendments, so an exception is granted, never taken. A conditional cap taken by the session that needs it is the self-issued exemption the delegation doctrine names as the standard failure mode. A [cap-credit:] is the conditional cap in the one form that does not collapse into it: the authorization is recorded on the entry, bounded by a knob, and withdrawn automatically once the entry fits the cap without it. Monotonic ruling accretion still takes compression by answering and the self-served relocation first, and never an automatic allowance. The shapes it would want are refused above: a cap conditional on carrying rulings, and ruling-count as a split signal. A credit is for the record whose compression would lose information its reader needs, the case a blocked session states when it asks.
--emit entry-history <slug> — the commits at which one entry’s counted extent fell. The contract above is an authoring contract, so a break in it is silent; this arm makes a break detectable after the fact. It reports one row per commit in which the named entry’s counted extent decreased, newest first. A row is that commit, the size before it, the size after it, and that commit’s subject; each size is in the cap’s configured unit (code points under off) and suffixed with it. Every field has a reader when the report is read: the commit is opened, the two sizes measure what left, and the subject decides whether opening it is worth it. There is no further field because there is no further reader, and in particular there is no verdict column.
It issues no verdict and has no red condition. Whether a given fall was a legitimate compression, a relocation or a discard is left to the reader, who has the commit in hand — the judgment this section concedes no gate can make. The exit contract is two-valued: 0 with the report, and 2 on a usage error, the harness-error class and never a finding. A usage error is an absent slug, or a slug that is neither live nor retired, refused before any history is read. The absence of a 1 is the member’s whole point, and it makes the arm advisory, the tier a surface nothing refuses a close over already occupies (guard-kit/SPEC.md §escalation-guard). It is a non-gate arm of the binary (gate-sdk/SPEC.md §The non-gate arm), registered in no gates.list, carrying no .gate descriptor, and read when its reader asks.
Its reader’s trigger is assertion A’s own failure. The one session that needs to know what has already been compressed out of an entry is the session the cap has just blocked, by construction the session about to compress. So the arm’s invocation is named in that failure’s help text beside the reliefs the text already routes to, with no new trigger, schedule or invocation point. Its second, discretionary consumer is the drain that ranks the deferred pool, for whom a displaced ground is otherwise recoverable only from git.
It mints no second spelling of the count. The extent is the range assertion A measures and the queue-index arm’s --extent yields, and the count is assertion A’s own. The arm calls that computation rather than reproducing it, so a change to what counts moves the cap and this report in one edit. It measures an entry wherever the walk finds it in a task section, so a report follows one entry across a promotion; a bare slug under the done section is not an entry the cap measures and reads as absence. A revision older than the heading grammar is read through the migration arm (§The queue-migrate arm), so a history runs through the commit that converted its queue.
The walk is bounded. Commits touching the queue file are walked newest-first, and the walk stops at the first one carrying no entry for the slug, the entry’s filing commit. The cost therefore scales with the entry’s age rather than the repository’s. The blobs stream through one git cat-file --batch child driven request by request rather than a process per commit, so the walk stops early without having bought the answers it will not read.
A slug the newest commit carries no entry for is not an error: it is a departed entry, and the arm reports it. The addressable domain is the live slugs plus the retired ones, the set §The queue-edges arm derives and §The shared queue adapters holds. That membership is the bound a back-search would otherwise lack, settled from one history pass before a blob is read.
For a departed slug the walk searches back to the newest commit carrying a counted entry. The commit one step newer than that one, the oldest under which the entry is already gone, is the departure commit, named with its subject in the report’s header. The fall rows then run from the last live commit back to the filing commit unchanged. The departure is a header fact rather than a fall row, because absence is not a count of zero — the rule that already reads a bare done-section slug as absence — and reporting it as a decrease would invent a measurement. The row grammar keeps its four fields and still carries no verdict column: which disposition a departure was is read off the commit. The retired set admits a slug on entry shape alone, so a heading or bold bullet that never stood in a task section is addressable and has no counted history at all. That slug refuses at exit 2 after the search, the one case the membership bound does not shorten.
Three honest limits, stated rather than discovered. A fall is attributed to a slug, so an entry renamed mid-history reads as filed at its rename and as departed at it — as does one that spent a stretch outside the task sections. A shallow or rewritten history addresses fewer departed slugs and never more, so the refusal under-claims in the direction §The queue-edges arm already declares for its own derivation. And a commit that compresses one part of an entry while growing another nets out and does not appear: the arm reports the counted total, which is the quantity the cap binds and therefore the quantity a displacement is measured against.
A ruling the operator restates from memory is filed in the moment. A break in the compression rule is silent, and it surfaces as the operator supplying a closed decision from memory because no surface carries it any more. That restatement is when 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 leaves the ruling 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, and the entry is done when that surface carries it. The gate cannot hold this either, for the reason it cannot hold compression: nothing distinguishes a sentence that was recalled from one that was read.
check-deferred-board-tags
Invariant: every top-level deferred entry carries its cost class and its primary surface on its tag line, and no active entry carries either. These are the two tag-line tags §The tag algebra defines, which let a reader rank and group the deferred pool off the board rather than off its bodies. Two assertions:
- (A) Presence and validity. Every top-level deferred entry carries exactly one
[cost:]tag whose value parses in the closed<recurrence>/<magnitude>grammar, and exactly one[surface:]tag whose value names an existing top-level entry of the repository root. Both are spelled with one space after the colon and no whitespace inside the value, the fixed spelling that bounds their width (§check-queue-wrap). - (B) Absent from the active sections. No entry’s tag line in an active section carries either tag. A promotion drops both, and this assertion is the checksum on that move.
Not applied to the icebox: an eviction drops both tags, and an entry with no tag line has no reader for either. A sub-task rides its parent’s tags, the same scope §check-queue-entry-budget’s assertion C gives the cost field.
Calibration: the queue file is the first positional argument, defaulting to QUEUE_KIT_QUEUE_FILE, and the repository root the second, defaulting to the working directory the battery runs from. The surface value set is that root’s one-level listing, with nothing configured (§The tag algebra). The scan reads the tag line alone, the line check-tag-lead-line pins every governed tag to. Fail-closed: an unreadable queue file or repository root exits 2. Opt-in by registration: a separate gate rather than a widened assertion on an existing one, so a consumer that registers it meets it on upgrade and one that does not sees no change. It holds the tags’ shape and never a class’s truth: whether an entry’s cost really is event/low is the filing session’s judgment against the entry’s own prose, and it is reviewed where the class is read.
check-queue-wrap
This is the gate of a consumer that hard-wraps its queue; a consumer that keeps its markdown unwrapped registers check-md-unwrapped instead (canon-kit/SPEC.md §check-md-unwrapped).
Invariant: no line exceeds the QUEUE_KIT_WRAP_BUDGET gate floor (default 100 columns). The tag readers key on the tag line, so a runaway an editor reflows can push a tag onto the next line, where no reader sees it; the tripwire fires before that lands.
The coupling it underwrites is conditional. Where QUEUE_KIT_ENTRY_CAP is in lines, this gate is that cap’s denominator, and deregistering it unbounds the cap. Under cp or off the entry cap is width-independent, and this gate is the wrapping convention’s own tripwire and nothing more.
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.
A tag line’s capacity is per-entry arithmetic, never a flat count. A tag costs its bracketed width plus one space against the budget, and an entry’s tag line holds every tag it carries, so how many fit is measured on the entry, never assumed.
A deferred tag line’s two board tags go uncounted. The line is a top-level deferred entry’s tag line, the first non-blank line under its ### heading when it holds only tags.
- What is removed: one
[cost:]tag whose value parses and one[surface:]tag naming an existing top-level root entry, each with one adjacent space; the gate measures what remains. A malformed class or an absent entry is counted, so the bound holds whether or notcheck-deferred-board-tagsis registered, and that gate reds the malformed tag where it is. - Why only these two: they are bounded, one of each in a closed grammar.
[blocked-by:]repeats, so discounting every governed tag would let a tag line grow without bound, and[drain-exempt:]’s free-text reason is unbounded. - The bound: the widest measured-clean deferred tag line is
QUEUE_KIT_WRAP_BUDGET, plus 23 for the widest cost tag ([cost: iteration/high]) and its space, plus 12 and the length of the longest top-level root entry name for[surface: ]and its space. - The denominator stays: the discount widens only the one tag line, by a bounded amount, and every prose line still wraps at the budget.
- Inheritance: any later gate measuring the width or wrapping of queue lines inherits this discount for these two tags.
Two grammar collisions are this gate’s, met only by a consumer registering it. An icebox entry’s one sentence must fit the budget, because §check-queue-entry-budget assertion B holds it to one line; the heading carries the slug, so the whole budget is the sentence’s. An active tag line carrying a spec tag and long blocker tags can overrun the budget on its own; the relief is QUEUE_KIT_SLUG_MAX, which bounds every new slug’s share of a [blocked-by:] tag. A consumer keeping unwrapped markdown under check-md-unwrapped has no width budget and neither collision.
Refused, with grounds:
- raising
QUEUE_KIT_WRAP_BUDGETfor the whole file — it loosens every line, prose included, and weakens alinesentry cap this gate underwrites; - moving the surface tag into the body — it defeats the reason the tag rides the board;
- shorter spellings — once the tags an entry already carries sit on the line, no spelling of the two fits every entry.
check-tag-lead-line
Invariant: no lead-line-scoped tag sits anywhere but its entry’s tag line, directly under the heading, or a lesson’s bullet line — the only lines its readers scan. Such a tag written into the body silently unblocks a task, voids a drain exemption, or drops a deferred entry’s class, surface or cap credit off the board. It drops a roadmap summary, recurrence or standing exclusion off every reader, or a lesson out of the attention block. The gate guards placement at authoring rather than reflow.
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, since check-queue-prose-precondition honors that tag anywhere in the entry. A governed member needs no live instance, which follows from that criterion rather than relaxing it. The set is a contract over readers, so a tag joins the moment its lead-line reader is named and the fixture pair exercises it. Membership keyed to instance count would disarm itself exactly when the last user drains.
The governed set and its scanned surface widen with the lesson channels:
- in the task sections (active, deferred and a configured icebox),
[blocked-by:]/[spec:]/[drain-exempt:]/[roadmap:]/[observed-by:]/[cost:]/[surface:]/[cap-credit:]/[recurrence:]/[roadmap-summary:]/[not-icebox-eligible:]. Each entry’s tag line is scanned against the rest of its extent, and a sub-task’s tag line is its own lead, not a body line of its parent; - in the
## Lessons Learnedsection, which thequeue-indexarm reads,[attend]and everyQUEUE_KIT_LESSON_TAGSname, whose lead is the lesson bullet.
The gate 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. A table entry naming the tag once cannot desync its matcher from its key. The desync’s silent direction is a key renamed without its regex: every downstream reader agrees 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 (the bundled --emit-enum-sets arm → check-prose-enum). It is the single source for the spelling on both the enforcement and the prose side. The emitter references the table rather than parsing the module as text, and QUEUE_KIT_LESSON_TAGS stays the consumer’s: the arm declares it as a knob and bakes no spelling of it.
Calibration: lead-class rule — a tag of class C on a body line is a violation only when the lead line lacks class C, so 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 are exempt. Single backticks are not an exemption. An entry body discussing a tag it does not carry, as `[roadmap:]` in a sentence about the roadmap arm, reds exactly as a misplaced tag would. So prose about a tag spells its name without the brackets. Widening the exemption to inline code is refused: the entry-body citation grammar puts single backticks to work as a live claim (§The tag algebra), so a scan that skipped them would skip the citation surface too. Residual accepted gap: a same-class duplicate in the body while one stays on the lead — negligible severity, since every reader reads the lead.
check-task-names
Invariant, over the live task sections (active, deferred, and a configured icebox) and the done section:
- every live-section heading is an entry heading whose text is a valid kebab-case slug, unique across the file, and no task-section bullet stands outside every entry;
- every done-section entry is a bare slug only;
- every
[blocked-by: X]on a tag line resolves to a live task, and one pointing at a done slug is flagged stale; - no line standing where an entry would — outside every entry of a task section, as a done slug or as an entry heading — is a placeholder token from
QUEUE_KIT_PLACEHOLDER_TOKENS, since an empty section is its heading alone (doctrine-kit’s Absence statements rule).
Assertion R, references are links. Two things red. A same-file link whose slug-shaped fragment names no entry heading is a dangling reference, and its remedy is backticks. A single-backticked token in a task-section body that names a live slug other than its own entry’s is an unlinked reference, and its remedy is the link, [<slug>] (#<slug>). A link or token inside a fence, and a token inside a bracketed tag, are not read. Self-citation stays in backticks, being narration rather than a relation (§The tag algebra). The queue file is outside canon-kit’s reference manifest, so check-md-refs does not scan it, and this gate holds both halves.
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. A body list item is prose inside its entry; done slugs are validated as tokens but not cross-checked against the live namespace. A placeholder line is reduced before the match: its indentation, a bullet marker, emphasis and a trailing period are stripped, and the compare ignores case. A column-0 bullet the outside-every-entry finding already reports is not reported again. The shipped set is wider than check-prose-tells assertion G’s absence tokens, since this arm judges a line by where it stands and not by what it says: an unfilled marker (tbd) and a done slug’s spelling (n-a) red too. Help texts cite this SPEC.
A Done move fans out in two steps, and the second is the one that surprises. The commit that moves a slug to Done rewrites every [<slug>] (#<slug>) in the file to `<slug>`, which assertion R would otherwise red as dangling, and --queue done makes it with the move. Then every entry blocked on the moved slug reds here on its stranded [blocked-by:]. Clearing it hands that entry’s blocking prose to check-queue-prose-precondition, which has no tag left to honour, so the same commit owes the dependent entry’s precondition a past-tense rephrase.
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. That is assertion A.
Assertion B, the slug ratchet: every live slug in the worktree that is absent from HEAD’s live and done sets is at most QUEUE_KIT_SLUG_MAX code points. The finding names the slug, its length and the ceiling. It binds a new slug and a rename, and grandfathers every slug already live, because a hard cap would force renames onto strings that are entries’ anchors. No HEAD means clean, as for assertion A. off disables B alone.
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 as its ### <slug> heading and body therefore matches neither the done grammar nor the live one, since the done section is no task section, lands in no set, and reds here as a lost task. Dispositioning an entry to done is a rewrite to - <slug>, not a relocation of the entry; the tags are dropped by that reduction rather than swapped. A HEAD written in the retired bullet grammar is read through the migration arm (§The queue-migrate arm), so the commit converting a queue is conserved against its own pre-image.
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), while a repository git refuses exits 2 (gate-sdk/SPEC.md §The crate’s crosser). 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 member’s unit tests hold assertion B’s two cases, a new slug over the ceiling and a grandfathered long one, over the same diff the gate runs.
The gate is a binary subcommand, and git is the one external program it invokes, the sanctioned floor exception. With no fixture case, 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 depends on what the fired clause says, which the finding prints. A precondition the entry waits on takes the real blocker as a tag, a move to the deferred section, or a past-tense rephrase if it is met. The [precondition-ok: <reason>] opt-out (a queue tag, not an HTML comment, so it survives the hygiene gate) is for everything else.
Calibration: the trigger set (QUEUE_KIT_PRECONDITION_REGEX) is deliberately narrow, forward-looking phrasing only, and the default’s phrases are left-bounded on a non-letter so a trigger embedded in a longer word (appending close, delegated on) does not fire. Three rewrites run before matching:
- bracketed tags and links are rewritten away;
- past-tense narration (
QUEUE_KIT_PRECONDITION_PAST_REGEX) is stripped; - the queue’s own state name
design-pendingis stripped in its unbracketed spelling too. The state name ends in a trigger word, so without it an entry narrating a demotion route or an amendment-pairing rule reds for using the queue’s own vocabulary.
The third rewrite is in the member’s source rather than excluded in the regex, for two reasons. POSIX ERE has no lookbehind, so a preceding hyphen cannot be guarded against in-pattern; and the trigger set is consumer config, where a kit-generic defect does not belong.
The gate is 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.
Two shapes match the trigger set without stating a precondition, and the valve is their contract rather than a workaround. A negated clause (“not gated on”, “never waiting on”) and an inverted one, naming this entry as what another waits on (“upstream of”, “blocked on this”), both fire. The gate reads neither negation nor direction, deliberately. Telling who is blocked means parsing a sentence’s subject, and any read the gate could run is a second phrase set of cues that misses the next spelling — the brittle-vocabulary refusal §The tag algebra makes for relational verbs. For those shapes the valve with a stated cause is the whole answer, and its reason names which shape it is. The finding quotes the fired clause from the lowercased, rewritten body, so it is matched text, never the author’s spelling. The match is widened to its enclosing clause, up to the neighbouring ., ; or line end, and capped at 80 code points each side. Blocking grade still stands: a silent pick was attested in production use, and a false positive costs one red and one valve that the finding’s own help routes to.
check-queue-slug-liveness
Invariant: on every prose surface QUEUE_KIT_PROSE_SURFACE_GLOBS names, every markdown link into the queue names a live entry. A link is into the queue when its target resolves, relative to the citing page’s directory, to the queue file and its fragment matches the slug grammar ([a-z0-9][a-z0-9-]*). The live set is the shared adapters’ live_slugs, active + deferred + icebox, so a page citing an iceboxed task stays green, correctly: the task is dormant, not landed. A link 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 link and cite the owning SPEC instead, so a living page can never keep calling a landed task queued. Bold code is prose emphasis and claims nothing. Honest limit: the target is resolved as a relative path; an absolute blob URL to the queue file is not read as a claim.
The link into the queue 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.
Assertion B, the retired citation. On every surface in QUEUE_KIT_CITATION_SURFACE_GLOBS, a single-backtick slug-shaped token naming a retired slug (§The queue-edges arm) whose name is no tracked file’s stem is red, reported by file, line and slug. It is a pointer at disposed work, which the paragraph above already tells prose to replace with the owning SPEC; a slug in the done section is in the retired set, so a governed citation of it reds at the commit that moves it. A double-backtick span is a quoted literal and is not read. The live-name test is the queue-edges arm’s: a slug that shipped as a tracked file’s name is current prose, not a pointer. A line that is history takes <!-- retired-citation-exempt: <reason> --> on it or the line above, the reason mandatory. The marker is this kit’s own rather than canon-kit’s history valves, so the verdict never depends on another kit’s knobs and the kit vendors alone.
The retired set is the shared adapter’s, so a missing git or a shallow clone under-reds and never invents. The live-name test reads the tracked listing, which the descriptor does not couple. A commit deleting the last file named after a retired slug, and touching no citation surface, therefore does not fire the gate at pre-commit, and that citation reds at the next full battery. Coupling every path would run the gate on every commit for an event that nearly always rides a SPEC edit anyway.
Assertion C, the status parenthetical. Inside the queue file, a cited slug — a same-file link or a backticked token — followed by exactly (<status>) (one space allowed before it) must be where the status says. The vocabulary is the configured single-word section names (QUEUE_KIT_DEFERRED_SECTION, QUEUE_KIT_ICEBOX_SECTION, QUEUE_KIT_DONE_SECTION), lowercased, plus retired. A section word agrees when the slug heads an entry in that section; retired agrees when the slug is in the retired set; a token resolving to nothing is not a citation (§The tag algebra) and is skipped. A stale status inverts a reader’s conclusion, and the queue’s own sections are a total oracle for it. A richer parenthetical ((landed …)) or a multi-word section name is outside the vocabulary and never read. The finding names the line, slug, stated status and actual section.
templates/
queue-config.knobs — the consumer knob-file template: comment-only, one # spec: pointer to §Layout and configuration and no override set. The table stays the knob roster’s one owner, with no parallel copy in the template to drift against it, and a seeded copy is a valid knob file as written.
TASK-QUEUE.md — a starter queue skeleton in the kit default: the heading grammar with its tag line and unwrapped paragraphs, the sections in default order, and one example entry per grammar shape 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 gate-sdk/SPEC.md §Consumer smoke owns. It carries lifecycle-kit’s iteration header as inert scaffold, so a combined-tree copy clears the stage gates too.
Shipping it makes this kit the queue format’s owner for an adopter’s install: checkwright init seeds the queue from the template of the first kit in the profile that ships one, and falls back to an inline skeleton only when none does (installer/SPEC.md §What init seeds). The declaration is the artifact, not a roster naming this kit.
Out of scope
The amendment lifecycle around the design-pending sections and [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 and task bodies are rule content and never ship.