| name | backlog |
| description | Use when the user invokes /backlog or asks to capture, view, promote, or remove an idea from the project backlog at docs/shield/backlog.json. Triggers on /backlog add|view|remove|promote and on agent-side "capture this as a backlog entry" calls during research/PRD/plan/implement flows. |
Backlog — capture, view, promote, reconcile
The Shield backlog is a single global JSON file at docs/shield/backlog.json that
holds ideas captured at any point during Shield's research → prd → plan → implement
pipeline. Each entry is associated with a feature (the reconciliation key) and
an epic (the removal gate) — either may be proposed-new at capture time.
Output Paths — MANDATORY
| Artifact | Path | Owner |
|---|
| Store | docs/shield/backlog.json | shield/scripts/backlog_store.py |
| Schema | shield/schema/backlog.schema.json | (this skill) |
| Validator | shield/scripts/validate_backlog.py | (this skill) |
| Suggester | shield/backlog/shield_backlog/suggester.py | (this skill) |
| Reconciler | shield/backlog/shield_backlog/reconciler.py | (this skill) |
| Removal log | .shield/backlog-removed.log | reconciler (append-only) |
| Kill switch | .shield.json → backlog.auto_reconcile (default true) | user |
Entry shape (v1)
{
"id": "<uuid4>",
"order": 42,
"kind": "task",
"source": "user",
"feature": "auth",
"epic": "Session mgmt",
"text": "Rotate refresh tokens every 24h"
}
id contract
- Format: uuid4 string (generated via
uuid.uuid4()).
- Why uuid4: collision-free capture with no read-before-write — multiple skills
can mint an id without coordinating.
- Uniqueness: enforced by
validate_backlog.py (named error
duplicate_entry_id), not by the JSON Schema. Draft 2020-12 uniqueItems
is whole-item equality and cannot express property-level uniqueness.
- Used as the key for: promotion (transient runtime arg), manual remove,
eager prune, lazy sweep recovery (
.shield/backlog-removed.log lookup).
schema_version + migration policy
- Top-level
schema_version is an integer (currently 1).
- Read-old / write-new: when a future writer ships, it reads the on-disk
schema_version, in-memory migrates if needed, then writes the new shape.
- Doc-only until schema_version 2: v1 ships no live
migrate() function;
the policy is documented so the migration seam is clear when v2 lands.
- Forward-compat: a doc with
schema_version > current is refused by the
validator (named error schema_version_too_new). Old code never silently
downgrades a newer doc.
Reconciliation invariant: feature name == folder slug
manifest.json features[].name is literally feature_dir.name from
shield/scripts/migrate_outputs.py. The reconciliation key feature therefore
matches both the manifest entry and the path docs/shield/<feature>/.
This invariant is what lets the reconciler ask "does this entry's feature have a
plan.json?" by deriving the path as docs/shield/<entry.feature>/plan.json
without storing the path in the manifest.
Named validator errors
| Code | Cause |
|---|
schema_violation | JSON Schema rejected the doc (default mapping) |
unknown_kind_enum | entries[].kind not in {epic, story, task} |
unknown_source_enum | entries[].source not in {user, agent} |
missing_required_field | A required field is absent |
invalid_id_format | entries[].id is not a uuid4 |
duplicate_entry_id | Two entries share an id (validator, not schema) |
schema_version_too_new | schema_version > 1 |
file_not_found / invalid_json | Usage error (exit 2) |
Local-dev / dry-run loop
To inspect the rendered view against a fixture without mutating the project store:
uv run shield/scripts/validate_backlog.py path/to/backlog.json
SHIELD_BACKLOG_PATH=shield/tests/fixtures/backlog-1.0-valid.json \
uv run shield/scripts/backlog_store.py view
The SHIELD_BACKLOG_PATH env var overrides the default docs/shield/backlog.json
for read-only flows (view + validator). Capture/remove always require an explicit
path argument and refuse to overwrite a missing file.
Cross-references
- TRD §5 / §11: function signatures and locked decisions.
- LLD
backlog-store §4–§8: data model, API contract, concurrency.
- LLD
epic-suggester §5: suggest_* contracts.
- LLD
reconciler §5–§8: reconcile() + RemovalDecision.
View rendering — pinned line format
Every view path (CLI, slash command, agent re-render) MUST use this format so
output is identical across surfaces. Anchored in shield_backlog.view.render().
<order>. [<id-short>] (<feature> / <epic>, <source>) <text>
<status-badges>
id-short is the first segment of the uuid4 (8 hex chars).
- The badge string (PINNED) is one of:
research ✓ prd ✓ plan – — flags derived from manifest artifacts.
not started — feature absent from manifest.json.
- (omitted) —
view invoked without --manifest (no badge line at all).
- An empty backlog renders the single line
Backlog is empty — no entries.
(clear message, not an error).
Capture interface (LOCKED, TRD §11)
from shield_backlog import capture
new_id = capture(
"Rotate refresh tokens every 24h",
kind="task",
feature="auth",
epic="Session mgmt",
source="agent",
)
A BacklogInvalid exception (named code on .code) signals refusal:
.code | Why |
|---|
missing_required_field | text/feature/epic absent or blank |
unknown_source_enum | source not in {user, agent} |
unknown_kind_enum | kind not in {epic, story, task} |
invalid_id_format | (read-side) on-disk id is not a uuid4 |
duplicate_entry_id | (read-side) two on-disk entries share an id |
schema_version_too_new | on-disk schema_version > 1 |
lost_update | compare-before-replace detected a concurrent on-disk change |
invalid_json | on-disk store is not parseable JSON |
source is keyword-only and required — callers cannot accidentally pass a
positional kind and have it bind to source.
Removal triggers (3)
| Trigger | Surface | Recovery log? | Idempotent |
|---|
| Manual | /backlog remove <id> | no | yes (id_not_found is exit 1, but a re-issued remove on an already-gone id is a no-op via the helper) |
| Eager prune | end of /plan or /implement carrying --backlog-ref | yes | yes |
| Lazy sweep | /backlog view (or /backlog sweep) | yes | yes |
Eager and lazy share the same reconciliation engine (shield_backlog.reconciler.reconcile) — no divergent logic.
Wrong-removal recovery procedure
- Flip the kill switch in
.shield.json:
{ "backlog": { "auto_reconcile": false } }
Eager prune and lazy sweep stop; manual remove still works.
- Locate the F9 log line for the suspect removal in your run output. It
carries
entry_id, feature, epic, match_kind, gating_plan_json_path,
and reason.
- Replay the corresponding record from
.shield/backlog-removed.log
(JSON-lines; one record per removal, with the full pre-remove entry under
.entry). Pipe it back through capture() to recover.
- Investigate the root cause: usually an epic-name collision across two
features — surface as ambiguous in the reconciler, but a manual rename
may have created the collision after the fact.
git revert is a second recovery path only for entries that reached a
commit. An entry captured and pruned within one session never appears on
the file at HEAD and is recoverable only via the log above.
Definition of done (per release of the backlog feature)
Audit cadence (fixed)
Run /backlog manually at a monthly cadence (PRD §7). Revisit scope if:
- <70% of entries reach a terminal state (promoted+removed, or manually
removed) within 30 days, OR
- >20% of entries sit untouched for >60 days.
These thresholds are concrete and falsifiable — they trigger a scope review,
not an automated alert.