name: backlog
description: >-
Owns BACKLOG.json — the project-level coordination file consumed by
/base:orient, /base:next, base:project-curator, /base:feature, and
/base:bug. Single authority for the file's format
(plugins/base/schemas/backlog.schema.json) and the canonical write path
(scripts/). Operations: init (scaffold + CLAUDE.md pointer + one-shot
epic seeding), add-finding, add-archive, mark-archive-adr,
resolve <slug>, defer-stamp <slug>, add-epic, list, get,
pick-next, render, query, migrate-v3 (one-shot v2-MD → v3-JSON
conversion). Use when the user wants to bootstrap a backlog, append a
finding outside a /feature or /bug run, close a finding inter-run,
query backlog state, or migrate a legacy v2 BACKLOG.md. All BACKLOG.json
mutations from /base:feature, /base:bug, /base:next, /base:triage,
base:project-curator, triage agent, /base:orient, and
/base:next-epic MUST go through this skill via the Skill tool — direct
shell-outs to the underlying scripts are a contract violation.
user-invocable: true
argument-hint: " [op-specific args] — ops: init | add-finding | enrich-finding | add-archive | mark-archive-adr | resolve | defer-stamp | add-epic | list | get | pick-next | render | query | migrate-v3"
allowed-tools: Read, Bash, AskUserQuestion
Format authority
The canonical format of BACKLOG.json is defined by the JSON Schema at
plugins/base/schemas/backlog.schema.json. All readers and writers cite
that schema rather than restating the rules. Every write script in
scripts/ validates against the schema before committing a mutation —
the file cannot land in a malformed state.
The semantic policy that does not fit in a JSON Schema — resolution
paths, scope axis, scale axis, deferred-reason semantics, tonality —
lives in references/format.md.
Where the writes happen
All mutations go through scripts in ${CLAUDE_SKILL_DIR}/scripts/:
| Operation | Script | Purpose |
|---|---|---|
init |
init.sh |
Scaffold BACKLOG.json + CLAUDE.md pointer; seed epics from specs/epic-*/ |
add-finding |
add-finding.sh |
Append a finding with slug derivation + scope inference + collision handling |
enrich-finding <slug> |
enrich-finding.sh |
In-place update of enrichment fields (motivation, behavior, approach, success_signal) on an existing finding |
add-archive |
add-archive.sh |
Append a free-standing rejection entry to archive[] (no source finding) |
mark-archive-adr |
mark-archive-adr.sh |
Tag existing archive entries with an ADR identifier when a rejection cluster has been promoted |
add-epic |
add-epic.sh |
Append (or update) an epic entry |
resolve <slug> |
resolve.sh |
Close a finding via done, done-mechanical, rejected, or promoted |
defer-stamp <slug> |
defer-stamp.sh |
Set/clear findings[i].deferred from a worker abort |
list |
list.sh |
Filter and print findings (compact, table, or JSON) |
get <slug> |
get.sh |
Fetch a single finding |
pick-next |
pick-next.sh |
Deterministic top-candidate selection for programmatic /base:next auto |
render |
render.sh |
Terminal-friendly view (used by /base:orient) |
query <jq> |
query.sh |
Arbitrary jq expression passthrough |
migrate-v3 |
migrate-v3.sh |
One-shot v2-MD → v3-JSON conversion |
Every script:
- Refuses to act outside a git repository.
- Uses atomic tmp+rename so concurrent runs cannot tear the file.
- Validates against the schema after every write.
- Stamps
updated_atwith an ISO 8601 timestamp. - Requires
jq(a hard dependency; checked at startup).
Consumers (the curator, /base:bug defer-stamp step, /base:feature epic
creation, /base:next un-stamp / resolve dispatch, /base:triage's
agent for retro→backlog promotion, /base:orient render, /base:next-epic
registered-epic read) invoke this skill via the Skill tool — they do not shell out to
the underlying scripts directly. Direct shell-outs to
plugins/base/skills/backlog/scripts/*.sh from outside this skill are a
contract violation: the relative path only resolves when cwd is the
claude-plugins source repo, so consumer-project invocations silently fail
and the workflow falls back to a raw Edit that bypasses schema validation
and atomic-write guarantees. Direct Edit of BACKLOG.json from outside
this skill is the same violation by another path. The schema, atomic-write,
and validation guarantees only hold when every writer takes the
Skill("base:backlog", args: "<op> …") path.
Dispatch
Read $ARGUMENTS. The first whitespace-separated token is the operation;
the rest are op-specific arguments passed through to the script. Run the
matching script with bash, capture output, surface to the user.
init → scripts/init.sh
add-finding [args...] → scripts/add-finding.sh [args...]
enrich-finding <slug> [args...] → scripts/enrich-finding.sh <slug> [args...]
merge-findings [args...] → scripts/merge-findings.sh [args...]
split-finding [args...] → scripts/split-finding.sh [args...]
add-archive [args...] → scripts/add-archive.sh [args...]
mark-archive-adr [args...] → scripts/mark-archive-adr.sh [args...]
add-epic [args...] → scripts/add-epic.sh [args...]
resolve <slug> [args...] → scripts/resolve.sh <slug> [args...]
defer-stamp <slug> [args...] → scripts/defer-stamp.sh <slug> [args...]
list [args...] → scripts/list.sh [args...]
get <slug> [args...] → scripts/get.sh <slug> [args...]
pick-next [args...] → scripts/pick-next.sh [args...]
render [args...] → scripts/render.sh [args...]
query <jq-expr> → scripts/query.sh <jq-expr>
migrate-v3 [args...] → scripts/migrate-v3.sh [args...]
anything else (or empty) → list ops with one-line summaries, exit
The skill is a thin dispatcher: it does not encode validation, format
rules, or routing in prose. Each script is self-documenting via --help
and set -e-driven error surfaces. When the user wants to know what
arguments an op takes, run the script with --help.
Operation summaries
init
Idempotent bootstrap. Creates BACKLOG.json if missing, seeds epics[]
from existing specs/epic-*/ directories on first creation, adds the
project-root CLAUDE.md pointer. Reports one of four outcomes:
"Initialized", "Repaired: …", "Already initialised".
add-finding
Interactive when invoked with no args. With args, takes
--text "<one-line text>" (required), --anchor <path[:line]|path:N-M|->
(required), --motivation "<why this matters>" (required),
--behavior "<what currently happens>" (required),
--approach "<how to address it>" (optional),
--success-signal "<short observable that confirms resolution>" (optional),
--scope <X> (optional; inferred from anchor when omitted),
--slug <slug> (optional; derived from text when omitted),
--created <YYYY-MM-DD> (optional; defaults to today),
--evidence <retro-path>:<finding-anchor> (optional, repeatable; each
appends one entry to the new finding's evidence[]),
--evidence-captured-at <YYYY-MM-DD> (optional; defaults to --created
when omitted; triage callers pass the source retro's completed: date
here so evidence is dated to when the observation was made, not when
triage promoted it),
--kind <enum-value> (optional; closed enum from finding.kind in the
backlog schema — currently only spec-gap, set by
base:mutation-testing / base:property-based-testing when a Real-gap
surviving mutant maps to no acceptance criterion).
--text is the one-line headline used for slug derivation, list rendering,
and terminal display. --motivation/--behavior/--approach/--success-signal
are the structured enrichment context — never collapse one into the other.
For interactive use, prompt the user via AskUserQuestion for text and
anchor, then call add-finding.sh with the collected args. Tonality
rules and slug-derivation refusals are enforced by the script.
enrich-finding <slug>
In-place update of enrichment fields on an existing finding. All four
enrichment flags are optional; supplying none is a no-op (exit 0). Only
the supplied fields are written — unsupplied fields are left unchanged.
Identity fields (slug, anchor, text, created_at, scope, deferred)
are immutable and explicitly refused (exit 1 if attempted).
enrich-finding <slug> --motivation "<why this finding matters>"
enrich-finding <slug> --behavior "<what currently happens>"
enrich-finding <slug> --approach "<how to address it>"
enrich-finding <slug> --success-signal "<short observable confirming resolution>"
enrich-finding <slug> --motivation "..." --behavior "..." --approach "..."
enrich-finding <slug> --add-evidence <retro-path>:<finding-anchor> [...] [--evidence-captured-at <YYYY-MM-DD>]
--add-evidence is append-only and repeatable. Each value is
<retro_path>:<finding_anchor>; captured_at defaults to today, or to
--evidence-captured-at when supplied (triage passes the source retro's
completed: date). Duplicate entries (same retro_path +
finding_anchor + captured_at) are silently deduped.
Exit codes: 0 success (including no-op); 1 invalid input (unknown or
identity-field flag); 3 slug not found (BACKLOG.json is not modified).
Used by /base:next Step 3.5 when finding-researcher (Mode A) returns
enrichment data for a thin finding selected for dispatch, and by the
triage agent on the ENRICHED outcome.
merge-findings
Collapse N findings into one survivor. Used by the triage agent on the
MERGED outcome when a retro item reveals that two or more existing
findings describe the same root issue from different angles.
merge-findings --into <survivor-slug> --from <absorbed-slug> [--from <absorbed-slug> ...]
Behavior:
- Union of all evidence entries (deduped by composite key) lands on the
survivor's
evidence[]. - The absorbed slugs themselves (plus any prior
superseded_slugs[]they carried) are appended to the survivor'ssuperseded_slugs[], deduped. - Soft fields (
motivation,behavior,approach,success_signal) carry over from an absorbed finding to the survivor only when the survivor lacks the field AND exactly one absorbed value exists for it. Conflicting absorbed values leave the survivor's field absent — the caller is expected to rewrite viaenrich-findingafterwards. - The survivor's identity (
slug,anchor,text,created_at,scope,deferred) is never modified. - Absorbed findings are removed from
findings[]atomically with the survivor update.
Exit codes: 0 success; 1 invalid input (missing flags, unknown slug,
self-merge); 2 schema violation in proposed mutation.
split-finding
Split one finding into N children. Used by the triage agent on the
SPLIT outcome when a retro item reveals that an existing finding has
been carrying two-or-more distinct problems under one name.
split-finding --from <parent-slug> \
--into-text "<child-1 text>" --into-anchor <path-or-->|<path:line>|<path:N-M> \
--into-text "<child-2 text>" --into-anchor <...> \
[--into-text "..." --into-anchor "..." ...]
Behavior:
- Each child inherits the parent's
scope;created_atis today. - Each child's slug is derived from its text (with collision suffix), and
the parent's slug becomes the first entry in each child's
superseded_slugs[](any priorsuperseded_slugs[]on the parent are also carried into each child). - The parent's
evidence[]is copied in full to the FIRST child; subsequent children start with emptyevidence[]. Callers who need a different distribution should reattach evidence viaenrich-finding --add-evidenceafter the split. - The parent's soft fields are NOT carried into the children — the split
implies distinct semantics, and the caller is expected to re-articulate
each child via
enrich-finding. - The parent finding is removed from
findings[]atomically with the child insertions.
Exit codes: 0 success (one child slug per line on stdout, in order); 1
invalid input (missing flags, unknown parent slug, fewer than two
child pairs); 2 slug derivation failed for one of the child texts.
add-archive
Append a free-standing rejection entry to archive[]. Used by
base:project-curator's append_rejection action when the rejection has
no in-flight source finding to close — pure shelf-aging knowledge capture.
Takes --text "<one-line rejected approach>" (required),
--reason "<why rejected>" (required), optional --date <YYYY-MM-DD>
(defaults to today).
For rejected-path closure of an existing finding, prefer
resolve <slug> --as rejected --reason "..." — the resolve script does
the archive append plus finding removal atomically.
mark-archive-adr
Tag one or more existing archive entries with an ADR identifier. Used by
base:project-curator's promote_rejections_to_adr action after
Skill("base:adr", ...) has created the supersession ADR. Takes
--adr ADR-NNN (required) and one or more --marker "<text>" arguments
identifying the archive entries by substring match.
add-epic
Append or update an entry in epics[]. Takes --path <specs/epic-foo/>,
--status <PLANNED|IN_PROGRESS|DONE|ESCALATED|UNKNOWN>, optional
--next-action "...".
resolve <slug>
Close a finding via one of four paths:
resolve <slug> --as done-mechanical # remove only
resolve <slug> --as done --target <spec-path> # remove; caller amends spec
resolve <slug> --as rejected --reason "..." # remove + archive
resolve <slug> --as promoted --target <spec-path> # remove; caller creates epic
The interactive form (just resolve <slug>) prompts the user via
AskUserQuestion for which path applies and gathers the missing
arguments, then re-invokes the script with --as.
For done and promoted, the spec amendment / epic creation is the
caller's responsibility. The script only mutates BACKLOG.json.
defer-stamp <slug>
Set or clear findings[i].deferred. Used by /base:bug and
/base:feature workers when they abort with
ABORT:DEFERRED:<reason>:<detail>. The structured field replaces the v2
markdown stamp (a positional [DEFERRED:…] prefix in the bullet text).
defer-stamp <slug> --reason <ENUM> --detail "<text>" [--stamped-at YYYY-MM-DD]
defer-stamp <slug> --clear
Reason enum: spec-gap | already-resolved | escalated | arch-debate-required | legacy-orphan.
list
Filter and print findings.
list [--status open|deferred|all] [--scope <X>] [--scope-prefix <X>]
[--format compact|table|json] [--include-archive]
Defaults: --status open --format compact. The --scope-prefix form
matches the scope token OR the anchor.path prefix — useful for
"show me everything targeting plugins/base/".
get <slug>
Single-finding lookup. --field <name> to print one field as a raw
string.
pick-next
Deterministic top-candidate selector for programmatic dispatch.
pick-next [--scope <X>] [--scope-prefix <X>] [--include-deferred]
[--format slug|json]
Selection: scope filter → exclude deferred → sort by created_at ascending → first. Exit code 1 with "No actionable findings." when the filter is empty.
This is the entrypoint a non-LLM /base:next auto driver uses. The
interactive form of /base:next may still rank with prose reasoning.
render
Terminal-friendly view.
render --format orient # full picture (epics + findings + archive)
render --format short # one-line summary (counts)
/base:orient consumes render --format orient.
query <jq-expr>
Thin passthrough for ad-hoc queries. The expression runs against the full BACKLOG.json document.
migrate-v3
One-shot v2-MD → v3-JSON conversion.
migrate-v3 # ./BACKLOG.md → ./BACKLOG.json, then `git rm BACKLOG.md`
migrate-v3 --dry-run # emit to stdout, don't write
migrate-v3 --keep-md # write JSON, don't delete BACKLOG.md
migrate-v3 --force # overwrite an existing BACKLOG.json
Idempotent. Auto-invoked by /base:orient and /base:next on detection
of a v2 BACKLOG.md without a corresponding BACKLOG.json.
Non-goals (skill-wide)
- No promotion to epic. Promotion is
/base:feature backlog:<slug>'s job — that flow scaffolds the spec dir, callsresolve.sh --as promotedto remove the finding, and callsadd-epic.shto register the new epic. - No batch operations. One finding per
add-findingorresolveinvocation. Batching reintroduces silent-edit failure modes. - No archive entry for
done,done-mechanical, orpromoted. Onlyrejectedwrites toarchive[]. Promotions and resolutions live in the spec or in git, not in the rejection log.
Relationship to other components
base:project-curator— the autonomous write-side at the end of/feature//bug. InvokesSkill("base:backlog", args: "resolve <slug> …"),Skill("base:backlog", args: "add-epic …"), andSkill("base:backlog", args: "mark-archive-adr …")— never the underlying scripts and neverEditonBACKLOG.json.triageagent — the retro → backlog promotion side, invoked once per/base:triagerun. InvokesSkill("base:backlog", args: "add-finding …"),Skill("base:backlog", args: "enrich-finding <slug> …"),Skill("base:backlog", args: "merge-findings …"), andSkill("base:backlog", args: "split-finding …")— the four write surfaces by which retro evidence reaches BACKLOG./base:orient— read-side renderer. Consumesrender --format orient. Auto-invokesmigrate-v3on detection of a v2 BACKLOG.md./base:next— dispatch driver. Auto mode callspick-next; interactive mode readslist --format jsonand ranks with prose reasoning. Auto-invokesmigrate-v3on detection of a v2 BACKLOG.md./base:bugand/base:featureworkers — when aborting withABORT:DEFERRED:<reason>:<detail>, calldefer-stamp.shto record the structured defer state./base:feature backlog:<slug>— the promotion path. Callsresolve.sh --as promotedto atomically remove the source finding, thenadd-epic.shto register the new epic.