Imported from softwaresalt/intercom (
.github/skills/shipment-reconcile/SKILL.md). Install upstream withnpx skills add softwaresalt/intercom --skill shipment-reconcile. Copyright stays with the author.
Shipment Reconcile
Provides a double-entry (GI/GR) integrity check for shipment manifests. Run
mode: pre before closing a shipment and mode: post after the archive +
restore steps complete. Run mode: safe-close in place of the destructive
cascade backlogit_ship_shipment call to archive only the shipment manifest's
explicit item IDs one artifact at a time, verifying after each that the parent
feature and any unshipped sibling tasks survive — safe-close's own Step 0 first
runs the P-015 verified fully-covered-root classification and, only when every
precondition holds, delegates to the Cascade Close Sub-Procedure instead.
Why safe-close exists.
backlogit_ship_shipmenttreats a shipment as a proxy for its covering feature and cascade-archives the whole feature subtree. For partial-feature shipments (which intentionally exclude the parent feature and some sibling tasks) this is destructive: it archives the parent feature and orphans unshipped siblings that are not in the shipment manifest. Safe-close prevents that corruption instead of merely detecting it after the fact. See P-015 inworkflow-policiesfor the governing policy.
When to Use
- Ship Step 6 closure (mandatory): run
mode: pre, thenmode: safe-closeinstead of the cascadebacklogit_ship_shipmentcall, thenmode: post. Safe-close's own Step 0 selects the close path from the machine-checkable P-015 classification; it archives manifest item IDs individually and never calls the cascade op directly unless that classification confirms the narrow verified fully-covered-root exception, in which case the Cascade Close Sub-Procedure runs (and is itself independently verified) instead. - Ship Step 0.5 (sanity check): pre-mode at intake with
expected_status: queued(oractiveif the shipment was already claimed in a prior session) to catch Stage-side over-inclusion before any build work begins. - Ad-hoc audit: any time an operator suspects manifest drift.
mode: detect-mixed-role(operator-invoked, READ-ONLY, no lock, no mutation): any time an operator wants a diagnostic scan for the queued-with-active-work / mixed-role silently-dropped-claim signature (936C68F3 part 2, re-scoped report-only per 013-DL Addendum G / 112-F) across one shipment or ALL shipments. Composed entirely from EXISTING read-only backlogit reads (backlogit_list_shipments+ per-shipment/per-task status reads viabacklogit_get_shipmentand per-taskbacklogit_get_item). EMITS a report-only diagnostic plus operator-remediation guidance. NEVER mutates, NEVER callsbacklogit_claim_shipmentor any status-write operation, and needs nofile-lockacquisition because no backlog/shipment artifact is ever mutated — the mode's own diagnostic report, audit-log entry, and telemetry event (steps 6–8 below) are additive-only writes to non-backlog-state locations, never applied to a queue/archive item.
Inputs
| Parameter | Required | Values | Notes |
|---|---|---|---|
mode |
yes | pre | post | safe-close | detect-mixed-role |
Controls which check/close/detect phase runs |
shipment_id |
yes for pre/post/safe-close; optional for detect-mixed-role |
e.g. 004-S |
The shipment to reconcile; for detect-mixed-role, omit to scan ALL shipments via backlogit_list_shipments |
expected_status |
pre-mode only | queued | active | done |
queued for fresh intake; active when shipment already claimed in a prior session; done for pre-ship check |
merge_commit_sha |
post-mode and safe-close | git SHA | The merge commit that closed the PR; recorded on archived items for traceability |
Output
A structured reconciliation report stored at
.backlogit/reconcile/{shipment_id}-{mode}-{timestamp}.md.
Every item in the manifest is classified as one of:
| Classification | Pre-Mode Meaning | Post-Mode Meaning |
|---|---|---|
matched |
Queue file present AND declared status matches expected_status |
Archive file present for this item |
pre-archived |
No queue file found but archive file exists — item already archived before this shipment ran; treated as valid | N/A (all items are expected in archive; use matched / missing) |
missing |
No queue or archive file found for this manifest item | Archive file not found for this manifest item |
status-mismatch |
Queue file present but declared status does not match expected_status |
N/A (post-mode does not check status fields) |
orphan |
Queue file declares this shipment_id in its frontmatter but is NOT in the manifest |
N/A (post-mode does not scan queue files) |
Classification semantics are mode-dependent. Pre-mode checks the queue for status correctness; post-mode checks the archive for file presence only.
Shipment-Record-Status Classification (record scope, distinct from the five per-item classifications above)
In addition to the five per-item classifications, pre-mode also classifies the
shipment record's own status against the aggregate status of its manifest
task items. Task-artifact filter (mandatory): the manifest items list
is untyped and may include the covering feature id (e.g. a fallback-assembled
manifest); this classification aggregates only items whose artifact_type is
task (already read from each item's frontmatter during the per-item check
above — no new read), excluding any non-task entry, so a covering feature that
happens to be active/done outside the shipment's own
scope can never be misread as a "conflicting task". This mirrors the task-artifact
filter the Ship agent's intake early-warning already applies to
custom_fields.items (templates/agents/_ship.agent.md.tmpl). Scope: the
three named inconsistency cases below apply only when the
record's own status is queued or blocked — the two
statuses where a manifest task already being active/done
is itself the drift signal (the queued/blocked record has not "caught up" to its
tasks). blocked itself is a non-standard/legacy value: backlogit
1.8.0's ShipmentStatus enum is only queued|active|shipped|abandoned (see
docs/compound/2026-05-07-backlogit-shipment-status-constraints.md) — a
persisted blocked record can still exist in real workspaces from a
historical backlogit move CLI defect that silently accepted invalid status
writes. Pre-mode's record-blocked-with-active-work/record-blocked-with-done-work
cases below classify such a leftover record defensively (it is real data that may
be present); mode: detect-mixed-role's malformed-legacy classification
(defined further below, in the mode's own classification section) reports the
identical underlying fact — a non-standard/legacy status value, never a normal
current-day state — for a different manifest scan. Both agree blocked is never fabricated or
transitioned into/out of; only the case label differs by mode. When the record's
own status is active or done
(or archived), the record is by definition record-consistent for this
check — an active record is the normal in-progress state while its
tasks move queued → active → done, and an
done/archived record reflects a shipment already closed. This is an
explicit scope boundary, not a silent default: the four cases below are
mutually exclusive because they are evaluated in this fixed order and every
record status value maps to exactly one of them.
| Classification | Condition |
|---|---|
record-consistent |
Record status is active or done/archived (always consistent — the normal in-progress/closed lifecycle, out of scope for this check), OR record status is queued/blocked and none of the three inconsistency conditions below match (e.g. record queued with all task items queued) |
record-queued-with-active-work |
Record status is queued AND at least one task-artifact manifest item is active or done — the classic "silently-dropped claim" inconsistency |
record-blocked-with-active-work |
Record status is blocked AND at least one task-artifact manifest item is active. Precedence: when a blocked record has BOTH an active task and a done task, classify here — active work takes precedence over record-blocked-with-done-work below, because it is the more severe/earlier-stage drift signal |
record-blocked-with-done-work |
Record status is blocked AND no task-artifact manifest item is active AND at least one task-artifact manifest item is done |
These four cases are mutually exclusive: every possible record-status value
(queued, active, blocked, done)
is covered — active/done always resolve to
record-consistent, and for queued/blocked the
active-over-done precedence rule (applied only to task-artifact items) guarantees
exactly one of the remaining three cases applies. This check is
detect-and-report only — NO auto-repair: it
never mutates the shipment record or any task; operators must manually
reconcile.
The report ends with a recommendation:
PROCEED— all items arematchedorpre-archivedAND the shipment-record-status classification isrecord-consistent; no action neededHALT — operator reconcile required— one or more missing, status-mismatch, or orphan items, OR a non-record-consistentshipment-record-status classification (pre-mode)HALT — restore archives— missing archive files or unrestored deletions (post-mode)CLOSED— safe-close archived every manifest item individually, archived the shipment record itself, and the protected set (parent feature + unshipped siblings) is intactHALT — cascade detected, revert required— safe-close found a non-manifest artifact (parent feature or a sibling task) archived or deleted; the unintended change must be reverted before any commit
For mode: safe-close, the report also records the protected set (the parent
feature file and every unshipped sibling task file that must survive closure) and,
per manifest item, whether it was matched (archived by this run) or
pre-archived (already archived before this run; skipped to avoid
double-archival and false-positive cascade flags).
Mixed-Role Detection Classification (mode: detect-mixed-role only, distinct from the record-scope classification above)
This classification is separate from the "Shipment-Record-Status
Classification" above: that check compares a single shipment record's own
status against the aggregate active/done state of its manifest tasks (four
record-scope cases). This check instead classifies each manifest task
individually by its per-task ROLE, to precisely describe the
mixed-role "silently-dropped-claim" signature — a queued shipment
record whose manifest tasks have kept progressing to active and
even fully done/archived while the record itself never advanced.
Per 013-DL Addendum G (re-scoped by Copilot PR #304 finding 1), this
classification is used ONLY to DESCRIBE the inconsistency in a report —
NEVER to gate a mutation. There is no --confirm flag because nothing is
ever mutated.
Per-task ALLOWED ROLE (a task-artifact manifest item must be a UNIQUE, NON-CONFLICTING record for exactly one of these three roles):
| Role | Definition |
|---|---|
live-queued |
UNIQUE record in .backlogit/queue/ with status: queued; NO archive record for the same id |
live-active |
UNIQUE record in .backlogit/queue/ with status: active; NO archive record for the same id |
archived-completed(done) |
UNIQUE record in .backlogit/archive/ ONLY, in EITHER valid representation: (a) TERMINAL RELOCATION — status: done (provenance archived_status/archived_from NOT required), OR (b) EXPLICIT ARCHIVAL — status: archived AND archived_status: done AND valid, well-formed archived_from provenance; NO conflicting live queue record for the same id |
Per-item ANOMALY (fail closed — REPORT and HALT on ANY of these; a manifest task satisfying none of these is role-clean):
| Anomaly | Definition |
|---|---|
duplicate |
Same task id present in BOTH .backlogit/queue/ AND .backlogit/archive/ |
conflicting |
Queue status disagrees with the declared role, an archive record exists alongside a live queue record, or a live status: done record is found in the QUEUE (a completed task lives ONLY in archive/, never live-done in queue/) |
missing |
Manifest task id has no file in either .backlogit/queue/ or .backlogit/archive/ |
malformed-provenance |
An archive record with status: archived is missing or has ill-formed archived_status/archived_from (a status: done archive record legitimately carries no provenance and is NOT malformed) |
any-other-archived-status |
An archive record whose status is NEITHER done NOR archived-with-archived_status: done |
orphan |
A queue file declares this shipment_id in its frontmatter but is NOT present in the manifest items list (reuses the pre-mode orphan-scan definition) |
out-of-role |
Task status falls outside the allowed lifecycle set queued | active | done/archived (e.g. a non-lifecycle or otherwise malformed status value) |
torn-partial |
Any other ambiguous, incomplete, or inconsistent signal for the task that cannot be cleanly assigned to a role (e.g. partially-written frontmatter) |
Malformed-legacy shipment record: backlogit 1.8.0 has NO blocked
shipment status (ShipmentStatus is only queued|active|shipped|abandoned).
A shipment record whose persisted status is anything other than a valid 1.8.0
lifecycle value (e.g. a legacy blocked value) is described in the report as
malformed-legacy — REPORT it, HALT, and never fabricate a blocked->queued
or any other transition. This is the same underlying fact the pre-mode
Shipment-Record-Status Classification table above documents for its own
scan (record-blocked-with-active-work/record-blocked-with-done-work
defensively classify a leftover legacy blocked record because it
may exist in real workspaces); the two modes describe the identical
non-standard value under mode-appropriate labels — never treat a persisted
blocked value as a normal current-day state in either mode.
Mixed-role signature: a shipment record queued whose
task-artifact manifest items (filtered by artifact_type to exclude any
non-task entry, e.g. a covering feature id, mirroring the existing
task-artifact filter above) include AT LEAST ONE live-active or
archived-completed(done) role task, with every task otherwise role-clean
(no anomaly), is the reportable "silently-dropped-claim" signature. On ANY
per-item anomaly (duplicate / conflicting / missing / malformed-provenance /
any-other-archived-status / orphan / out-of-role / torn-partial) or any other
ambiguity, REPORT the specific anomaly and HALT — never mutate, never
attempt to resolve or repair.
Detection outcomes (structured audit entry + telemetry event on every
run — see "Mixed-Role Detection Audit + Telemetry" below): exactly one of
DETECTED (scan completed; no mixed-role signature or anomaly found —
record-consistent, nothing to report), REPORTED (scan completed; the
mixed-role signature and/or one or more per-item anomalies were found and
described in the report), or DEGRADED (backlogit was unreachable; the
degraded condition is reported and the scan halts). There is NO
succeeded / repaired / refused / two-active outcome — nothing is ever
mutated or repaired by this mode.
Behavioral Constraints
- Report-and-halt only. This skill NEVER modifies the shipment manifest or queue/archive files outside the safe-close mode's manifest-scoped archival. In pre- and post-mode it only reports; operators must manually reconcile via existing backlog tools and re-invoke Ship Step 6.
- Manifest-scoped mutation only. In
mode: safe-close, the ONLY artifacts this skill may move or archive are the shipment manifest's explicit item IDs and the shipment record itself ({shipment_id}). It must NEVER archive the parent feature or any sibling task that is not in the manifest. It never calls the cascadebacklogit_ship_shipment; the shipment record is closed as its own single artifact. - No prune / no auto-repair. Auto-mutation of the manifest itself is reserved for a future version. Safe-close never prunes the manifest and never auto-deletes non-manifest artifacts; on cascade detection it reverts the unintended change and halts.
- Single-writer lock. When invoked from Ship Step 6, this skill holds the
.backlogit/queue/{shipment_id}.mdfile lock (via thefile-lockskill) for the duration of pre-mode → safe-close → post-mode. See lock protocol in the Required Protocol section below. - Halt on RECONCILE_FAIL. Do not proceed to safe-close unless pre-mode
returns
PROCEED. Do not commit backlog state if safe-close returnsHALT — cascade detected, revert required. Surface the report path to the operator. mode: detect-mixed-roleis strictly READ-ONLY. It NEVER mutates any shipment record or task, NEVER callsbacklogit_claim_shipment(no re-claim, no repair mode — a record-only forward re-claim of a queued-with-active-work shipment is UNSUPPORTED by backlogit 1.8.0; see "Operator-Remediation Guidance" below), and requires NOfile-lockacquisition because no backlog/shipment artifact is ever mutated (its own diagnostic report, audit-log entry, and telemetry event are additive-only writes to non-backlog-state locations). DEGRADED (backlogit unreachable) is REPORTED and the mode HALTS — it never guesses or acts blind.
Required Protocol
Pre-Mode
-
Acquire single-writer lock (Ship Step 6 invocations only, not intake): Invoke the
file-lockskill to acquire.backlogit/queue/{shipment_id}.md. If lock acquisition fails, count as a session stall (circuit-breaker protocol) and prompt the operator. -
Load manifest via
backlogit_get_shipment(shipment_id). Extract theitemslist. -
Check each manifest item:
- Attempt to locate the file at
.backlogit/queue/{id}.* - If found, read its frontmatter (including
statusandartifact_type) and comparestatustoexpected_status— classify asmatchedorstatus-mismatch - If NOT found in queue, check
.backlogit/archive/{id}.*— if archive file exists, classify aspre-archived(valid; item already shipped) — if no file in either location, classify asmissing
- Attempt to locate the file at
-
Orphan scan: Scan
.backlogit/queue/for any files whose YAML frontmatter declaresshipment_id: {shipment_id}but whose ID is NOT present in the manifestitemslist. Classify each such file asorphan. -
Shipment-record-status classification (reuses in-hand data — NO new scan): Using the shipment record's own
statusalready loaded viabacklogit_get_shipmentin step 2, and the manifest items' statuses already read in step 3, classify the record scope per the Shipment-Record-Status Classification table in the Output section above, evaluated in this order. Filter to task artifacts first: the manifestitemslist is untyped and may include the covering feature id (e.g. a fallback-assembled manifest); reuse theartifact_typealready read from each item's frontmatter in step 3 to exclude any non-task entry before aggregating task statuses below — the same task-artifact filter the Ship agent's intake early-warning applies tocustom_fields.items(templates/agents/_ship.agent.md.tmpl) — so a covering feature that isactive/doneoutside the shipment's own manifest scope can never be misread as a "conflicting task" and falsely halt an otherwise-consistent shipment.- Record
activeordone/archived →record-consistent(always — this check is scoped toqueued/blockedrecords only; an active/done record is the normal in-progress/closed lifecycle state, not evaluated further). - Record
queuedAND any manifest task isactiveordone→record-queued-with-active-work. - Record
blockedAND any manifest task isactive→record-blocked-with-active-work(takes precedence over the case below when both an active and a done task are present). - Record
blockedAND no taskactiveAND any manifest task isdone→record-blocked-with-done-work. - Record
queuedorblockedmatching none of the above →record-consistent(e.g. recordqueuedwith all tasksqueued). This step is detect-and-report only — NO auto-repair: it never mutates the shipment record or any task.
- Record
-
Produce report and store at
.backlogit/reconcile/{shipment_id}-{mode}-{timestamp}.md. -
Gate decision:
- If all items are
matchedorpre-archived, no orphans exist, AND the shipment-record-status classification isrecord-consistent→recommendation: PROCEED - If any
missing,status-mismatch, ororphanitems exist, OR the shipment-record-status classification isrecord-queued-with-active-work,record-blocked-with-active-work, orrecord-blocked-with-done-work→recommendation: HALT — operator reconcile required, naming the shipment id, the record's own status, and the conflicting manifest task ids - On
HALT: emit the report path, release the lock, and halt withRECONCILE_FAIL. Do NOT callbacklogit_ship_shipment. - On
PROCEEDfrom Ship Step 6: retain the lock until post-mode completes.
- If all items are
Post-Mode
-
Verify archive presence: List
.backlogit/archive/and confirm a file exists for the shipment itself ({shipment_id}.*). -
Per-item archive check: For every item in the manifest, verify a corresponding archive file exists. If any are absent, flag them in the report.
-
Deleted-file guard (known
backlogit_ship_shipmentquirk — see P-007): Rungit status -- ".backlogit/archive/"and inspect for deletions. If any archive files are reported as deleted, recommendgit restore .backlogit/archive/before the commit step. -
Produce post-mode report per the same schema.
-
Gate decision:
- If all archive files present and no deletions detected →
recommendation: PROCEED - If missing archive files or unrestored deletions detected →
recommendation: HALT — restore archives - On
HALT: release the lock and report. Ship must restore archives before committing.
- If all archive files present and no deletions detected →
-
Release lock (acquired in step 1 of pre-mode): Invoke
file-lockrelease for.backlogit/queue/{shipment_id}.md. If release fails, log a warning — stale locks are operator-recoverable.
Safe-Close Mode
Runs in place of the destructive cascade backlogit_ship_shipment call —
except in the narrow P-015 verified fully-covered-root case selected by
Step 0 below, where the cascade op is the permitted close path and safe-close
steps 1–10 are skipped entirely. Archives only the shipment manifest's explicit
item IDs, one artifact at a time, verifying after each archival that the parent
feature and any unshipped sibling tasks survive. Invoked between pre-mode
(PROCEED) and post-mode, under the lock pre-mode already holds. If invoked
standalone, acquire the lock per pre-mode step 1 first and release it on
completion.
-
Load manifest, snapshot pre-close state, then select close path (P-015 verified fully-covered-root exception — select from the verified check, never from prose alone): safe-close is the default. a. Load the manifest first, regardless of which path is ultimately selected: invoke
backlogit_get_shipment(shipment_id)and extract theitemslist. This load happens here in Step 0 — not deferred to step 1 below — because the classification in (c) and the cascade pre/post-comparison in the Cascade Close Sub-Procedure both require it, and the cascade path skips steps 1–10 entirely. b. Snapshot pre-closeparent_idand declaredstatusfor every task item in the manifest by reading each task's current frontmatter from whichever of.backlogit/queue/or.backlogit/archive/currently contains it — a manifest task item may already be pre-archived when this snapshot runs (see the Cascade Close Sub-Procedure's pre-archived-member preamble below), and its snapshot must still be captured from wherever it actually resides. If a task item's record is found in both locations (an ambiguous/torn state) or in neither (missing), halt immediately withRECONCILE_FAIL_SNAPSHOT_AMBIGUOUSorRECONCILE_FAIL_SNAPSHOT_MISSINGrespectively — never guess which copy or location is authoritative. Retain this snapshot in memory for the duration of this close operation; it is the baseline the Cascade Close Sub-Procedure's step 4 parent-preservation check and step 3 declared-status two-set gate both compare against, and it must be captured before any mutating call (cascade or otherwise) runs, never reconstructed after the fact.Declared
statusis read from the record's own frontmatterstatusfield — never inferred from, nor substituted by, which ofqueue//archive/currently holds the record. A record residing in.backlogit/archive/while declaringstatus: doneis not truly archived; only a declaredstatus: archivedcounts as truly archived for the Cascade Close Sub-Procedure's step 3 gate below — location alone is never sufficient. This declared-status snapshot MUST be captured here, in Step 0(b), before the cascade invocation, for the identical reason already stated forparent_id:statusis the very field the cascade mutates, so a post-close read would reportarchivedfor everything the cascade just archived, collapsingrequired_ids(Cascade Close Sub-Procedure step 3) to empty and silently disabling the completeness check entirely. Never a freshly-read or assumed value. c. Classify the close path: run the machine-checkable classification described in P-015 over the manifestitemsloaded in (a). Workspaces with a Python implementation installed reuse aclassify_shipment_close_path(manifest_items, workspace_backlog_dir)-shaped function (this self-hosting repository's own implementation lives atsrc/autoharness/gates/shipment_closure.py); other workspaces implement the equivalent check directly against.backlogit/queue/+.backlogit/archive/. The cascade close path is permitted only when, for every feature member of the manifest: it is a root (noparent_id); it is fully covered (every one of its descendants — at every depth, not only direct children — enumerated by walking the fullparent_idgraph live from.backlogit/queue/+.backlogit/archive/starting at the feature, is also a manifest member); and, if it enumerates to zero descendants, that childlessness is positively verified against the live workspace (never inferred from an incomplete or failed enumeration) and the feature is additionally terminal (no manifest member declares it as parent). A direct-children-only check is insufficient (155-S, PR #407 review, thread PRRT_kwDORzpWpM6b2MJv): Backlogit's ownreleaseScopeItemIDsrecursively adds every descendant of each manifest item — not just the feature's immediate children — beforecollectArchiveCandidateIDsarchives terminal descendants, so a manifest such as[feature, task]where that task has an out-of-manifest subtask (of anyartifact_type, not onlytask) would otherwise wrongly qualify forCASCADE, and the destructive cascade would archive that subtask before the Cascade Close Sub-Procedure's step 3 gate ever sees it — halting only after the mutation. The manifest must contain nothing beyond the qualifying root feature(s) and their descendants at every depth. If any feature member fails any precondition, the whole manifest falls back to safe-close (steps 1–10 below) — qualification is never per-member, and no feature ID is ever special-cased.When this classification identifies qualifying feature members (i.e. selects
CASCADE): extend the same pre-close declared-status snapshot from (b) — still before the cascade invocation, never after — with each qualifying feature member's own declaredstatusfield, read the identical way (frontmatter's ownstatusfield only, never inferred fromqueue//archive/location). The resulting combined map (manifest task statuses captured in (b), plus qualifying feature statuses added here) is the single pre-close declared-status snapshot the Cascade Close Sub-Procedure's step 3 two-set gate reads from; "qualifying feature members" for that gate means exactly the set this classification determines here — never a separate re-derivation, and independent of how the engine happens to transition any given member.Linked-deliberation snapshot extension (155-S, PR #407 review). Backlogit's own cascade engine (
internal/core/shipment_lifecycle.gocollectArchiveCandidateIDs) appends, for every explicit qualifying feature member, that feature'slinkedDeliberationIDs— collected from the feature'scustom_fields.source_deliberation_id(taken as a complete literal ID string, never regex-scanned), plus any deliberation ID embedded in the feature's description, and any deliberation the feature references — the latter two, and only the latter two, scanned with the engine's owninternal/core.deliberationIDPatternmatcher (given exactly below) — never a broader "any embedded deliberation ID" reading, which can match a substring the engine's own matcher would not — de-duplicated, and restricted to IDs that resolve to an existing artifact whose ownartifact_typeisdeliberation— beforearchiveItemsruns. A qualifying feature with such a live linked deliberation therefore archives it during the same cascade invocation. To keep the two-set gate strict without a blanket allowance for arbitrary IDs, extend Step 0(c)'s classification here, still before the cascade invocation: for each qualifying feature member, independently collect its linked deliberation IDs using exactly those same three engine-defined sources — the literalcustom_fields.source_deliberation_idstring taken as-is, and the description/references text scanned with the identical\b(?:DL\d+|[0-9]+(?:\.[0-9]+)*-DL)\bmatcher, never a wording-level approximation of it — and the identical existence /artifact_type: deliberationvalidation — never any other ID, and never an ID that fails either check. For each validated linked deliberation ID, resolve its record location the identical way Step 0(b) resolves a manifest task item: if found in both.backlogit/queue/and.backlogit/archive/(an ambiguous/torn state) or in neither (missing), halt immediately withRECONCILE_FAIL_SNAPSHOT_AMBIGUOUSorRECONCILE_FAIL_SNAPSHOT_MISSINGrespectively — never guess which copy or location is authoritative, and never computerequired_idsfrom an arbitrary copy before the destructive cascade invocation. Once resolved to its single authoritative location, read its own declaredstatusfield (frontmatter only, never location-inferred) into the same combined pre-close declared-status snapshot as the qualifying feature statuses above. The further-extended combined map (manifest task statuses from (b), qualifying feature statuses, and now qualifying-feature linked-deliberation statuses, all added here in (c)) is what the Cascade Close Sub-Procedure's step 3 two-set gate reads from; "linked deliberation of a qualifying feature member" for that gate means exactly the set this sub-step determines here — never a separate re-derivation, and independent of whether the engine transitions, skips (already truly archived), or otherwise handles any given one of them.- CASCADE selected → skip directly to the Cascade Close Sub-Procedure below (reusing the manifest and snapshot from (a)/(b)/(c) above — do not reload) in place of steps 1–10, then proceed to post-mode.
- SAFE_CLOSE selected (default, including any classifier error, ambiguity, or unresolved precondition) → continue to step 1 below (step 1's own manifest load is idempotent with (a) above — reuse the already-loaded manifest rather than issuing a second call).
-
Load manifest via
backlogit_get_shipment(shipment_id). Extract theitemslist. These IDs are the only artifacts safe-close may move or archive. -
Compute the protected set (partial-feature detection):
- Derive the covering feature ID from the manifest item hierarchy
(e.g. a task
055.002-Tbelongs to feature055-F). - If the covering feature ID is not in the manifest
items, this is a partial-feature shipment. Add the covering feature to the protected set. - Enumerate every task sharing the covering feature's hierarchy prefix whose ID
is not in the manifest
items(the unshipped siblings) by scanning both.backlogit/queue/and.backlogit/archive/(plus the feature file's declared children when available). Add each to the protected set. - Sequence-aware exclusion (serial partial-feature shipments): when a sibling
belongs to a predecessor shipment in the same feature-split sequence, exclude it
from the protected set ONLY when that predecessor shipment record itself has
verified archived provenance
archived_status: shipped(or normalized legacydone). Mere archive-file presence,archived_status: active|queued|blocked|abandoned, genericstatus: archivedwithout shipped/done provenance, or missing/ambiguous provenance are NOT sufficient — in those cases the sibling stays protected fail-closed. - The protected set is the parent feature plus every unshipped sibling task
that MUST remain in
.backlogit/queue/after closure. It is computed from expected IDs, not merely the files currently present in queue, so a sibling or parent that was already wrongly archived is still detected.
- Derive the covering feature ID from the manifest item hierarchy
(e.g. a task
-
Baseline integrity gate (before archiving anything): Run
git status --short -- ".backlogit/"and record the pre-closure working-tree state so any later archival or deletion of a protected-set path can be attributed to this procedure. Then confirm every protected-set member currently exists in.backlogit/queue/. If any protected-set member is already in.backlogit/archive/or missing from the working tree, a cascade has already occurred (or the shipment scope is wrong): halt immediately withHALT — cascade detected, revert required, name the affected artifact IDs, and do NOT archive any manifest item. Thepre-archivedexemption (step 4) applies to manifest items only — never to the protected set. -
Archive each manifest item individually (loop over
itemsONLY):- If the item's file is in
.backlogit/queue/: move it todoneviabacklogit_move_item, then archive that single artifact viabacklogit_archive_item(CLI fallbackbacklogit archive {{id}}). When the backlog registry'sarchive_itemoperation supports commit metadata, record the merge SHA using that tool's configured field (for backlogit,commit_sha); otherwise record the merge SHA in the closure report. Classifymatched. - If the item's file is already in
.backlogit/archive/: classifypre-archivedand skip — do not re-archive. Reusing thepre-archivedclassification prevents false-positive cascade flags on items that were legitimately shipped earlier. - If the item's file is in neither location: classify
missing, halt withRECONCILE_FAIL, and do not continue archiving.
- If the item's file is in
-
Verify-after-each invariant (run immediately after each item's archival):
- Confirm every protected-set member is still present in
.backlogit/queue/— not moved to.backlogit/archive/, not deleted from the working tree. - Run
git status --short -- ".backlogit/"and confirm no protected-set path appears as a deletion, rename intoarchive/, or newarchive/addition beyond the baseline captured in step 3. - The protected set was proven fully present in queue at the baseline gate
(step 3), so any protected-set member now found in
archive/or missing from the working tree is a cascade. There is no pre-archived exemption for the protected set — the exemption in step 4 covers manifest items only.
- Confirm every protected-set member is still present in
-
git-revert-on-cascade: If the invariant fails (a protected-set artifact was archived or deleted by the preceding archival):
- Cascade detected. Immediately restore the unintended change:
git restore -- .backlogit/queue/ .backlogit/archive/for working-tree moves/deletions, orgit revert <commit>if the cascade was already committed. - Re-run the invariant to confirm the protected set is intact again.
- Halt with
HALT — cascade detected, revert required, emit a P-005 violation event (naming the cascaded artifact IDs), and do NOT commit the backlog state. Do not auto-prune the manifest.
- Cascade detected. Immediately restore the unintended change:
-
Final invariant re-check: After the loop completes, re-confirm the full protected set is intact in
.backlogit/queue/. -
Close the shipment record itself (single artifact, non-cascading; authoritative order):
- Move ONLY the live shipment record to
status: shippedvia the generic, non-cascadingbacklogit move <shipment_id> --status shipped. - Re-read and verify the live shipment record now reports
status: shipped. If the record remainsactive, is alreadyarchived, is missing, or resolves to any other shape, halt fail-closed withRECONCILE_FAIL_SHIPMENT_RECORD_LIVE_STATUS. Do NOT auto-retry by calling the cascade op, and do not archive anactiveshipment record. - Archive ONLY the shipment record via
backlogit archive <shipment_id>(single- artifact archive; this stampsarchived_statusfrom the live status at archive time). - Re-read and verify the archived record now reports
archived_status: shipped. A legacyarchived_status: doneis accepted only when it pre-existed as an older, already-correct terminal provenance. Missing archive, live+archived duplication, generic archived-without-provenance, or any non-shipped archived provenance halt fail-closed withRECONCILE_FAIL_SHIPMENT_RECORD_PROVENANCE. - Re-run the verify-after-each invariant (step 5) to confirm the protected set is still intact after the shipment-record close sequence.
- Move ONLY the live shipment record to
-
Produce safe-close report per the same schema, recording the protected set, each item's classification, the shipment-record move-to-shipped verification, the shipment-record archived-provenance verification, and the recommendation.
-
Gate decision:
- All manifest items
matchedorpre-archived, the shipment record archived, and the protected set intact →recommendation: CLOSED. Proceed to post-mode. - Any cascade detected →
recommendation: HALT — cascade detected, revert required(see step 6). Do not proceed to the commit step.
- All manifest items
Cascade Close Sub-Procedure (P-015 verified fully-covered-root exception ONLY)
Runs only when Step 0 of Safe-Close Mode above selects CASCADE, and
reuses the manifest and pre-close parent_id/declared-status snapshot
Step 0 already captured in (a)/(b)/(c) — this sub-procedure never reloads
the manifest or attempts to reconstruct pre-close state after the fact.
Replaces steps 1–10 above entirely for this shipment's closure; there is no
partial mixing of the two paths.
Pre-archived manifest members (expected and tolerated): before invoking
step 1 below, classify each manifest member's location as queued or
pre-archived by checking whether its record currently resides in
.backlogit/queue/ or .backlogit/archive/, and
retain that location set for the cascade-close report (step 5 below). This
location label is descriptive only — it names where the record
currently resides, and is never a substitute for, nor evidence of, the
record's own declared status field. A record residing in
.backlogit/archive/ while declaring status: done is not
truly archived; only a declared status: archived is truly archived for
step 3's two-set gate below. The sole authority for "truly archived" is the
declared-status snapshot Step 0(b)/(c) already captured before this
sub-procedure runs.
A pre-archived (by location) manifest member is expected and tolerated
on this path: it does not disqualify the CASCADE verdict, does
not constitute a classifier ambiguity or unresolved precondition, and
does not authorize a fallback to safe-close. Step 0(c)'s classifier
already resolves each manifest member by scanning both queue/ and
archive/, so archived inputs were already accounted for when the
verdict was selected — this clause states the execution-time consequence
of that fact and does not add a new classifier precondition; Step 0(c)'s
precondition wording is unchanged.
This tolerance applies to manifest members only — it does not
restate, weaken, or cross-apply to the protected set, which has no
pre-archived exemption (see Safe-Close Mode steps 3/5 above). A manifest
that qualifies for CASCADE has no protected set by construction (full
coverage is itself a Step 0(c) precondition), so no protected set arises
on this path.
archived_ids is a transition log, not a manifest echo. The cascade
operation invoked in step 1 below reports, in archived_ids, only the
artifacts it actually transitioned to archived during that invocation
(backlogit engine source, internal/core/shipment_lifecycle.go
archiveItems(): an item whose declared status is already archived is
skipped and never appended to the slice that becomes archived_ids). A
manifest task item, or a qualifying feature member's validated linked
deliberation, that was already truly status: archived before the call
therefore has no transition to report and is correctly absent from
archived_ids — this is expected engine behavior, not an anomaly and not a
cascade failure. This never extends to the shipment record or to a
qualifying feature member itself (155-S, PR #407 review, thread
PRRT_kwDORzpWpM6b0kit): step 3 below makes both unconditionally required
regardless of their own pre-close declared status, so neither can ever be
"correctly absent" the way a task item or linked deliberation can — see
step 3 for the full statement of that rule. The live fail-closed guard
over this result is the two-set allowed_ids / required_ids gate
specified in step 3 below, evaluated against the Step 0(b)/(c) pre-close
declared-status snapshot — never against location, and never against a
post-close re-read.
SUPERSESSION NOTE (155-S, 2026-08-24). This paragraph previously
claimed the cascade operation "is idempotent over pre-archived members",
citing
docs/spikes/2026-08-18-cascade-close-pre-archived-member-behavior.md
as authority for the claim that it "returns [pre-archived members] in
its archived_ids result exactly as it does newly-archived members",
and stated that step 3's exact-match post-condition, "evaluated against
the manifest's full item set", "must never be relaxed". That claim,
and the spike cited for it, are WITHDRAWN. The spike's arms were built
with move --status done, which relocates a record but leaves it
declaring status: done — never truly status: archived — so none of
its arms ever exercised the case this paragraph claimed to cover; its
finding is valid only for relocated-but-done records (see the spike's
own superseded banner). The two safety properties this paragraph
protected — nothing out-of-scope archived, nothing required left
unarchived — are now carried, at full strength, by the two-set gate in
step 3 below, keyed on declared pre-close status rather than a full-set
echo of the manifest.
No-substitution rule: once Step 0 selects CASCADE, that verdict is
final for this closure — between the verdict and step 1's invocation
below, substituting manual safe-close is a P-005 process deviation,
never a permitted fallback, regardless of the manifest's archival state.
This complements, and does not restate or contradict, step 2's separate
rule against falling back to safe-close after a cascade has already
executed: together the two rules close both the pre-execution and
post-execution substitution windows. The asymmetry is intentional and
one-directional — this rule forbids CASCADE -> manual safe-close
substitution only; it grants no license to invoke cascade when Step 0
selects SAFE_CLOSE, which remains governed by the P-015 default
prohibition. If a genuine unhandled error occurs during the cascade
operation, halt and disclose it per the verification steps below — never
silently switch to safe-close instead.
Ship performs no manual per-item archive loop on this path: the cascade operation in step 1 below performs all remaining archival itself, consistent with the "no partial mixing of the two paths" rule above.
- Invoke
backlogit_ship_shipment(shipment_id, merge_commit_sha)directly (CLI:backlogit shipment ship <shipment_id> --sha <merge_commit_sha> --message <merge_commit_message> --author <merge_commit_author>). - Verify the result matches the classifier's own precondition:
returned_idsMUST be empty ([]). A non-emptyreturned_idsmeans the live engine found an unreleased descendant the classifier's live-workspace enumeration did not — this is a TOCTOU/engine-behavior mismatch, not a recoverable state. Halt immediately withHALT — cascade returned non-empty returned_ids, classifier/engine mismatchand emit a P-005 violation; do NOT retry, do NOT fall back to safe-close after a cascade has already executed. - Verify
archived_idsagainst the two-setallowed_ids/required_idsgate (replaces exact full-set equality — see the SUPERSESSION NOTE above and the P-015 policy's own supersession note for why):-
Compute
allowed_ids= the manifest's task items + every qualifying feature member identified by Step 0(c)'s own classification (defined by reference to that determination — never a separate re-derivation — and independent of how the engine happens to transition any given member) + every validated linked deliberation ID of each qualifying feature member captured by Step 0(c)'s linked-deliberation snapshot extension above (same reference-only rule: never a separate re-derivation, and never any ID beyond what that engine-defined, existence-and-artifact_type-validated collection produced) + the shipment record itself. -
Compute
required_ids= the shipment record and every qualifying feature member (both unconditionally — never omitted, and never conditioned on either artifact's own pre-close declared status) + every otherallowed_idsmember (a manifest task item, or a qualifying feature member's validated linked deliberation) that was not trulystatus: archivedin the pre-close declared-status snapshot (Step 0(b) for manifest task items, extended by Step 0(c) for qualifying feature members and their validated linked deliberations — all captured before this step 1 invocation, never a freshly-read or assumed post-close value). -
Two separately-labelled, independently-failing conditions. Neither may be evaluated as a precondition of the other, and the two MUST NOT be merged into a single combined test (conflating two questions into one condition is the documented root cause of external defect
B57F9E24):- Unexpected-artifact check: if
archived_ids - allowed_idsis non-empty, halt withHALT — cascade archived unexpected artifact {id}and emit a P-005 violation. - Missing-required-artifact check: if
required_ids - archived_idsis non-empty, halt withHALT — cascade did not archive required artifact {id}and emit a P-005 violation.
- Unexpected-artifact check: if
-
An
allowed_idsnon-shipment member (a manifest task item, or a qualifying feature member's validated linked deliberation — never the qualifying feature member itself, which is unconditionally required; see below) that was already trulystatus: archivedin the pre-close snapshot MAY be included in or omitted fromarchived_idsby the engine — neither outcome fails either check (it is outsiderequired_idsby construction, and if present inarchived_idsit is still insideallowed_ids). This is exactly the 147-F → archived 027-DL case: 027-DL is a linked deliberation already trulystatus: archivedpre-close, so its absence fromarchived_idsis tolerated by construction, and its presence, if the engine reports it, is equally tolerated. This tolerance never extends to the shipment record itself, which is unconditionally arequired_idsmember per the computation above regardless of its own pre-close declared status: if the shipment record were ever reported pre-close as already trulystatus: archived— an anomalous state for an artifact this same closure step is actively transitioning toshipped— its absence fromarchived_idsstill fails the missing-required-artifact check exactly as any other missingrequired_idsmember would; no engine behavior toward the shipment record ever gets a pass under this tolerance.Nor does it extend to a qualifying feature member itself (155-S, PR #407 review, thread PRRT_kwDORzpWpM6bzlFl). Backlogit's own
ShipShipment(internal/core/shipment_lifecycle.go) unconditionally callssetArtifactStatus(featureID, models.StatusDone, "feature released")for every explicit shipment-member feature — regardless of that feature's own pre-close declared status, including an already trulystatus: archivedone — beforecollectArchiveCandidateIDsruns.setArtifactStatusonly no-ops when the artifact's current status already equals the requested one, and the requested status here isdone, neverarchived, so an already-archived qualifying feature is unconditionally relocated todonefirst, with no terminal-status bypass of the kindcompleteReleaseScopegrants a manifest task item already trulystatus: archived(that task-only skip is exactly what makes the tolerance above valid for tasks, and it has no counterpart in the feature-forcing loop). By the timecollectArchiveCandidateIDsloads the feature, its declared status is therefore alwaysdone, never stillarchived— that function's ownfeature.Status != models.StatusArchivedcheck is always true for it — so the feature is always appended to the candidate listarchiveItemsarchives. A qualifying feature member can therefore never be "correctly absent" fromarchived_idsthe way a truly pre-archived manifest task item or linked deliberation can — its absence is always an anomaly, never expected engine behavior. A qualifying feature member is therefore an unconditionalrequired_idsmember exactly like the shipment record, and is never eligible for this tolerance.
-
- Verify no
parent_idwas cleared: re-read every archived task's frontmatter and confirmparent_idis unchanged from the pre-close snapshot captured in Step 0(b) — never a freshly-read or assumed value, since the field being verified is the very one a cascade could have just cleared. Any cleared or alteredparent_idis a cascade-detection failure equivalent to step 6 of safe-close: halt withHALT — cascade cleared parent_id on {id}, revert requiredand emit a P-005 violation; do NOT commit the mutated backlog state. - Produce cascade-close report recording the classifier's verdict,
qualifying feature IDs, and their validated linked deliberation IDs
(all Step 0(c)), the pre-close declared-status
snapshot (Step 0(b)/(c)), the
backlogit_ship_shipmentresult (shipment_status,archived_ids,returned_ids,commit_sha),allowed_ids,required_ids, and both set differences (archived_ids - allowed_idsandrequired_ids - archived_ids) — so a vacuousrequired_idsis visible in the report rather than silent — and the parent_id-preservation verification outcome (against the Step 0(b) snapshot). - Gate decision:
returned_idsempty,archived_ids - allowed_idsempty (no unexpected artifact archived),required_ids - archived_idsempty (no required artifact left unarchived), and everyparent_idpreserved (against the Step 0(b) snapshot) →recommendation: CLOSED. Proceed to post-mode. Any verification failure above → the correspondingHALT; do not proceed to post-mode or any commit step.
Mixed-Role Detection Mode (mode: detect-mixed-role, operator-invoked, READ-ONLY)
No lock is acquired for this mode — no backlog/shipment artifact is ever
mutated. (The mode's own diagnostic report, audit-log entry, and telemetry
event — steps 6, 8 below — ARE writes, but they are additive-only writes to
non-backlog-state locations, never applied to a queue/archive item, so no
file-lock is required.) This mode is composed entirely from EXISTING
read-only backlogit reads; it introduces NO new gate/CLI code (single
template family: this SKILL's prose only).
-
Enumerate shipments: call
backlogit_list_shipments. Ifshipment_idwas given, narrow to that single shipment; otherwise scan every shipment returned. -
Load each candidate shipment record via
backlogit_get_shipment. Skip (no report entry) any shipment whose record status isactive,shipped,abandoned, or archived — those are the normal in-progress/closed lifecycle shipment-record states and are out of scope for this check (mirrors therecord-consistentscope boundary above). Note:shipped/abandonedare the shipment record's own terminal statuses (ShipmentStatusenum), distinct fromdonewhich is a task-artifact status — a live shipment record is never itselfdone. If a candidate's persisted status is not a valid backlogit 1.8.0 shipment lifecycle value (e.g. a legacyblockedvalue), classify itmalformed-legacy, add it to the report, and continue to the next candidate — never fabricate a transition. Any other unrecognized persisted value is likewisemalformed-legacyrather than silently skipped or silently matched to the queued branch below — every possible persisted value maps to exactly one of: skip (active/shipped/abandoned/archived), scan (queued, step 3), ormalformed-legacy(anything else). -
Filter to task-artifact manifest items: for each remaining
queuedcandidate, read its manifestitemslist and each item's frontmatter (status,artifact_type) via per-item reads (backlogit_get_itemper task id). Exclude any non-task entry (e.g. a covering feature id in a fallback-assembled manifest) before classifying — the same task-artifact filter the Ship agent's intake early-warning applies tocustom_fields.items(templates/agents/_ship.agent.md.tmpl). -
Classify each task-artifact manifest item against the per-task ALLOWED ROLE table and the per-item ANOMALY table in the "Mixed-Role Detection Classification" section above. Locate each id in
.backlogit/queue/and.backlogit/archive/to determine role/anomaly; for an archive hit, readstatus,archived_status, andarchived_fromto distinguish the two valid archived-completed representations frommalformed-provenance/any-other-archived-status. -
Determine the outcome for each candidate:
- If any per-item anomaly was found →
REPORTED, naming the shipment id, the anomalous task id(s), and the specific anomaly for each. - Else if the mixed-role signature is present (at least one
live-activeorarchived-completed(done)role task, all tasks otherwise role-clean) →REPORTED, naming the shipment id, record status, and each task's role. - Else (all tasks role-clean and no mixed-role signature — e.g. a
genuinely fresh
queuedshipment with all-live-queuedtasks) →DETECTEDwith no anomaly/signature to report for that candidate. - If backlogit is unreachable at any point in steps 1–4 →
DEGRADED; report the degraded condition for the affected candidate(s) and HALT the scan. Do not guess or proceed on partial data.
- If any per-item anomaly was found →
-
Emit the report-only diagnostic at
.backlogit/reconcile/{shipment_id-or-"all"}-detect-mixed-role-{timestamp}.md, listing per candidate: the shipment id, its record status, the per-task role classification, any per-item anomaly, and the outcome (DETECTED/REPORTED/DEGRADED). -
Emit Operator-Remediation Guidance inline with any
REPORTEDorDEGRADEDentry — see "Operator-Remediation Guidance" below. This mode performs NO mutation of any kind in response to what it finds. -
Write the audit entry and emit telemetry for every candidate's outcome — see "Mixed-Role Detection Audit + Telemetry" below.
Operator-Remediation Guidance
When this mode reports a mixed-role signature or a per-item anomaly, include this guidance verbatim (adapted with the specific ids/anomaly found) in the report:
autoharness performs NO auto-repair of this inconsistency. A record-only forward re-claim (
queued→activeon the shipment record alone) is UNSUPPORTED by backlogit 1.8.0: evidence (read-only inspection ofC:\Source\GitHub\backlogit, NOT mutated) —ClaimShipment(internal/core/shipment_lifecycle.go) is manifest-wide activation (it moves the shipmentqueued→activeAND THEN activates every still-queuedmanifest member, cascading parent-feature status, with all-or-nothing rollback on any mid-flight failure) and is STRICTLY SINGLE-SHOT (isValidShipmentTransitionininternal/core/shipment.gopermits ONLYqueued→activeandactive→{shipped,abandoned}; a re-claim on an already-activesh
Truncated - read the full file at https://github.com/softwaresalt/intercom/blob/44b1cda5531d34bbe32b9d2b5c277e0a137fece3/.github/skills/shipment-reconcile/SKILL.md.