@adhd/backlog 0.1.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +80 -47
  2. package/README.md +332 -81
  3. package/api.d.ts +146 -0
  4. package/cli.d.ts +45 -18
  5. package/env.d.ts +23 -3
  6. package/envelope.d.ts +163 -0
  7. package/index.d.ts +11 -10
  8. package/index.js +531 -173
  9. package/index.mjs +30039 -15879
  10. package/install-skill.d.ts +23 -0
  11. package/package.json +50 -15
  12. package/query/card.d.ts +31 -0
  13. package/query/get.d.ts +11 -0
  14. package/query/index.d.ts +67 -0
  15. package/query/markdown.d.ts +11 -0
  16. package/query/query.d.ts +131 -0
  17. package/query/resolve.d.ts +123 -0
  18. package/query/types.d.ts +450 -0
  19. package/query/views/registry.d.ts +43 -0
  20. package/query/views/semantic.d.ts +101 -0
  21. package/query/views/stats.d.ts +109 -0
  22. package/search-shortcut.d.ts +79 -0
  23. package/serve.d.ts +18 -0
  24. package/server.d.ts +139 -4
  25. package/skill/SKILL.md +619 -138
  26. package/store/graph-backlog-store.d.ts +80 -17
  27. package/store/immediate-retry.d.ts +24 -13
  28. package/store/type-policy.d.ts +4 -0
  29. package/store/vocabulary-guard.d.ts +52 -0
  30. package/write/audit.d.ts +36 -0
  31. package/write/bootstrap.d.ts +123 -0
  32. package/write/catalog.d.ts +351 -0
  33. package/write/claim-lease.d.ts +21 -0
  34. package/write/claim.d.ts +80 -0
  35. package/write/create-issue.d.ts +250 -0
  36. package/write/delete.d.ts +39 -0
  37. package/write/embed-drain.d.ts +68 -0
  38. package/write/embedding-observer.d.ts +80 -0
  39. package/write/errors.d.ts +303 -0
  40. package/write/issue-status.d.ts +10 -0
  41. package/write/move.d.ts +70 -0
  42. package/write/relate.d.ts +52 -0
  43. package/write/transition.d.ts +60 -0
  44. package/write/tx.d.ts +344 -0
  45. package/write/update.d.ts +81 -0
  46. package/client.d.ts +0 -174
  47. package/markdown.d.ts +0 -75
  48. package/migration-admin.d.ts +0 -26
  49. package/model.d.ts +0 -437
  50. package/store/audit-log.d.ts +0 -16
  51. package/store/claim.d.ts +0 -24
  52. package/store/crud.d.ts +0 -62
  53. package/store/ids.d.ts +0 -24
  54. package/store/lifecycle.d.ts +0 -36
  55. package/store/mapping.d.ts +0 -101
  56. package/store/mutate-metadata.d.ts +0 -8
  57. package/store/query.d.ts +0 -68
  58. package/store/repo-migration.d.ts +0 -51
  59. package/store/serve-lock.d.ts +0 -42
  60. package/store/structure.d.ts +0 -66
package/skill/SKILL.md CHANGED
@@ -1,148 +1,629 @@
1
1
  ---
2
2
  name: backlog-usage
3
- description: "Use whenever filing, claiming, transitioning, or resolving a backlog item (bug/debt/feature/investigation) in ANY repo on this machine — via the `backlog` CLI/MCP, never by hand-editing a BACKLOG.md file. Also use to check current migration status before assuming markdown vs. the tool is authoritative. Examples: \"log this bug\", \"file a debt item for the flaky test\", \"claim BUG-042\", \"what's still open in this repo\", \"is BACKLOG.md still the source of truth here\"."
3
+ description: 'Use whenever filing, reading, claiming, transitioning, relating, or resolving a backlog issue — or registering a project/component/location — in ANY repo on this machine, via the `adhd-backlog` CLI / `mcp__backlog__*` tools, never by hand-editing a `BACKLOG.md` file. Examples: "log this bug", "file a debt item for the flaky test", "claim that issue", "what''s still open in this repo", "where does this tool live".'
4
4
  ---
5
5
 
6
6
  # `@adhd/backlog` usage
7
7
 
8
- `@adhd/backlog` is a graph-backed, multi-agent, cross-repo backlog tool. It is
9
- migrating every repo on this machine off hand-edited `BACKLOG.md` files onto
10
- itself as the source of truth, with `BACKLOG.md` demoted to a generated,
11
- git-visible *projection* of the graph. This skill is the ONLY place the
12
- command surface, protocol, and migration-state mechanism are documented —
13
- the global `CLAUDE.md`/`AGENTS.md` carry just one pointer line to this file.
14
-
15
- ## 1. Check migration status FIRST, every time
16
-
17
- Never trust a hardcoded phase number in this document — it goes stale the
18
- moment a phase advances. Before deciding whether `BACKLOG.md` or the tool is
19
- authoritative for the CURRENT repo, run:
20
-
21
- ```
22
- backlog migration-status
23
- ```
24
-
25
- It reports `{ phase, description, toolIsAuthoritative }`. While
26
- `toolIsAuthoritative` is `false` (phases `not-started`/`phase-1`/`phase-2`),
27
- `BACKLOG.md` (root, per-plan, per-package) is still the real source of truth
28
- in this repo — read/file there by hand as usual, but still prefer `backlog
29
- list-items`/`backlog create-item` for querying and filing so items are
30
- visible cross-repo and get FTS/symbol dedup for free. Once `toolIsAuthoritative`
31
- is `true` (`phase-3` and later), **never hand-edit a `BACKLOG.md` file** —
32
- every one is a generated projection at that point, and a hand-edit will be
33
- silently overwritten (or, once the parity gate is blocking, rejected in CI).
34
-
35
- ## 2. Command surface — 36 ops, one CLI convention
36
-
37
- Every op takes `ctx` as an implicit first argument (never pass it — the CLI/
38
- MCP host supplies it). The CLI's own parameter-naming convention (verified
39
- against real spawned-binary tests, `cli.spec.ts`):
40
-
41
- - **Scalar parameters** get individual kebab-case flags, named after the
42
- parameter itself: `getItem(ctx, repo, humanId)` → `backlog get-item --repo
43
- <repo> --human-id <id>`.
44
- - **Object-shaped parameters** get a single JSON-blob flag, named after the
45
- parameter's own name, kebab-cased: `createItem(ctx, input)` → `backlog
46
- create-item --input '<json>'`.
47
- - The bin name is never part of `argv` — a bare `create-item …`/`get-item …`
48
- resolves without any manual namespace prefix.
49
- - Failures use real process exit codes (never `0` on error — unknown command
50
- → exit `4`, bad flag → exit `2`); errors are JSON on the last stderr line.
51
- - Every op is also available as an MCP tool, named `backlog_client_d_<snake_case_op>`
52
- (e.g. `backlog_client_d_create_item`), once `.mcp.json` wires the server —
53
- same parameters, same semantics.
54
-
55
- | `client.ts` export | CLI form |
56
- |---|---|
57
- | `createItem(ctx, input)` | `backlog create-item --input '<CreateItemInput json>'` |
58
- | `getItem(ctx, repo, humanId)` | `backlog get-item --repo <repo> --human-id <id>` |
59
- | `updateItem(ctx, repo, humanId, patch)` | `backlog update-item --repo <repo> --human-id <id> --patch '<UpdateItemInput json>'` |
60
- | `listItems(ctx, filter?)` | `backlog list-items --filter '<BacklogFilter json>'` |
61
- | `softDeleteItem(ctx, repo, humanId, reason)` | `backlog soft-delete-item --repo <repo> --human-id <id> --reason <text>` |
62
- | `stats(ctx, scope?)` | `backlog stats --scope '<StatsScope json>'` |
63
- | `spotlight(ctx, scope?, limit?)` | `backlog spotlight --scope '<json>' --limit <n>` |
64
- | `readyItems(ctx, scope?)` | `backlog ready-items --scope '<json>'` |
65
- | `blockers(ctx, repo, humanId)` | `backlog blockers --repo <repo> --human-id <id>` |
66
- | `dependencyGraph(ctx, scope?)` | `backlog dependency-graph --scope '<json>'` |
67
- | `topoOrder(ctx, scope?)` | `backlog topo-order --scope '<json>'` |
68
- | `staleClaims(ctx, maxAgeMin, scope?)` | `backlog stale-claims --max-age-min <n> --scope '<json>'` |
69
- | `claimItem(ctx, repo, humanId, by, opts?)` | `backlog claim-item --repo <repo> --human-id <id> --by <identity> --opts '<ClaimOpts json>'` |
70
- | `renewClaim(ctx, repo, humanId, by)` | `backlog renew-claim --repo <repo> --human-id <id> --by <identity>` |
71
- | `releaseClaim(ctx, repo, humanId, by, opts?)` | `backlog release-claim --repo <repo> --human-id <id> --by <identity> --opts '<json>'` |
72
- | `assignItem(ctx, repo, humanId, to, by)` | `backlog assign-item --repo <repo> --human-id <id> --to <identity> --by <identity>` |
73
- | `startWork(ctx, repo, humanId, by)` | `backlog start-work --repo <repo> --human-id <id> --by <identity>` |
74
- | `transitionStatus(ctx, repo, humanId, status, opts)` | `backlog transition-status --repo <repo> --human-id <id> --status <STATUS> --opts '<TransitionOpts json>'` |
75
- | `addCitation(ctx, repo, humanId, citation)` | `backlog add-citation --repo <repo> --human-id <id> --citation '<Citation json>'` |
76
- | `appendNote(ctx, repo, humanId, by, text)` | `backlog append-note --repo <repo> --human-id <id> --by <identity> --text <note>` |
77
- | `resolveItem(ctx, repo, humanId, status, opts)` | `backlog resolve-item --repo <repo> --human-id <id> --status <STATUS> --opts '<json>'` |
78
- | `archiveResolved(ctx, scope, opts?)` | `backlog archive-resolved --scope '<StatsScope json>' --opts '<ArchiveOpts json>'` |
79
- | `addDependency(ctx, repo, humanId, dependsOnHumanId)` | `backlog add-dependency --repo <repo> --human-id <id> --depends-on-human-id <id2>` |
80
- | `removeDependency(ctx, repo, humanId, dependsOnHumanId)` | `backlog remove-dependency --repo <repo> --human-id <id> --depends-on-human-id <id2>` |
81
- | `linkRelated(ctx, repo, humanIdA, humanIdB)` | `backlog link-related --repo <repo> --human-id-a <id1> --human-id-b <id2>` |
82
- | `supersedeItem(ctx, repo, oldHumanId, newInput, reason)` | `backlog supersede-item --repo <repo> --old-human-id <id> --new-input '<CreateItemInput json>' --reason <text>` |
83
- | `splitItem(ctx, repo, parentHumanId, children)` | `backlog split-item --repo <repo> --parent-human-id <id> --children '<CreateItemInput[] json>'` |
84
- | `mergeItems(ctx, repo, keepHumanId, dropHumanId, reason)` | `backlog merge-items --repo <repo> --keep-human-id <id1> --drop-human-id <id2> --reason <text>` |
85
- | `setPriority(ctx, repo, humanId, priority)` | `backlog set-priority --repo <repo> --human-id <id> --priority <PRIORITY>` |
86
- | `attachToPlan(ctx, repo, humanId, planSlug)` | `backlog attach-to-plan --repo <repo> --human-id <id> --plan-slug <slug>` |
87
- | `importFromMarkdown(ctx, input)` | `backlog import-from-markdown --input '<ImportMarkdownInput json>'` |
88
- | `renderToMarkdown(ctx, filter?)` | `backlog render-to-markdown --filter '<BacklogFilter json>'` |
89
- | `exportJson(ctx, filter?)` | `backlog export-json --filter '<json>'` |
90
- | `auditTrail(ctx, repo, humanId)` | `backlog audit-trail --repo <repo> --human-id <id>` |
91
- | `migrationStatus(ctx)` | `backlog migration-status` |
92
- | `setMigrationPhase(ctx, phase)` | `backlog set-migration-phase --phase <PHASE>` (admin-only — only whoever just verified a phase's DoD should call this) |
93
-
94
- Every `<repo>` value is this machine's stable git-remote-derived slug (e.g.
95
- `PseudoSky/adhd`) — never a bare directory name. A worktree agent
96
- (`<repo>/.worktrees/*`) resolves to the SAME main-repo `repo` slug, not a
97
- phantom per-worktree repo.
98
-
99
- ## 3. Claim/renew/release protocol (multi-agent use)
100
-
101
- - Identity is always `${agentName}:${instanceId}` — NEVER a bare role literal
102
- like `"agent"`. Two concurrent agents both claiming as `"implementer"`
103
- defeats the CAS protocol entirely.
104
- - `claimItem` is idempotent for the SAME claimant — always `renewed`, no
105
- contention check.
106
- - A long-running task must call `renewClaim` periodically (default
107
- staleness: 30 minutes).
108
- - Every exit path (done/error/abandoned) calls `releaseClaim`
109
- unconditionally — it is a no-op on an already-unclaimed item, never an
110
- error. Never leave an item claimed after you stop working on it.
111
-
112
- ## 4. Citations — via tool calls, never hand-typed markdown
113
-
114
- The `Citation` type (`{ file, lines?, context? }`) is the structured form of
115
- one bracketed citation entry in the old hand-edited convention. Instead of
116
- typing a `Citations: [...]` line by hand:
117
-
118
- - Call `transitionStatus`/`resolveItem` with `{ citations: [...] }` when
119
- moving into any terminal-done/terminal-workaround status — this is
120
- REQUIRED and enforced (a status transition without citations there is
121
- rejected).
122
- - Call `addCitation` to attach evidence without a status change.
123
-
124
- ## 5. Dedupe before filing — via the tool, not eyeballing
125
-
126
- `createItem` runs a dedupe scan (FTS over title+body, plus exact
127
- symbol/path/errorText metadata match) BEFORE writing, and returns
128
- `duplicateCandidates` alongside `created: false` when a likely match exists.
129
- **Always inspect `duplicateCandidates` first.** Only pass `force: true` after
130
- confirming the candidates are genuinely a distinct issue — never as a way to
131
- skip reading them.
132
-
133
- ## 6. Filing a new item — worked example
134
-
135
- ```
136
- backlog create-item --input '{
137
- "family": "BUG-MYAREA",
138
- "title": "Short, specific summary",
139
- "body": "Full description, root cause if known, evidence.",
140
- "repo": "PseudoSky/adhd",
141
- "projectPath": "packages/domain/my-package",
8
+ `backlog` is a graph-backed, multi-agent backlog tool: issues, a project/
9
+ component/location registry, and the relationships between them, all served
10
+ from one store over four transports (CLI, MCP, HTTP, and in-process). This
11
+ skill is the ONLY place the command surface and calling convention are
12
+ documented — `AGENTS.md`/`CLAUDE.md` carry just a pointer to it.
13
+
14
+ Identity is the global `uid` returned by `create`/`upsertProject`/etc. —
15
+ never a family-scoped human-readable id. A `uid` is stable for the life of its
16
+ node, but a `body` edit replaces that node: as §3 below spells out, `update`
17
+ with a `body` mints a successor with a fresh `uid` and joins the two with a
18
+ `SUPERSEDES` edge. A uid you persisted earlier therefore stays _resolvable_ but
19
+ may no longer be the _live_ one — addressing it returns `conflict` and names
20
+ the successor. Never treat a stored uid as immutable across edits.
21
+
22
+ Every example below was run against `entrypoint/backlog/dist/index.js` — the
23
+ baseline examples on revision `9df2a5c7`, the §8 stats/rollup examples on the
24
+ build that first mounted those ops — and its exact output is what is shown. A
25
+ _globally installed_ `adhd-backlog` may be an older build: in particular
26
+ `gitContext` on `create`/`transition` (§6) exists in the `9df2a5c7` build but
27
+ an older installed build rejects it with `invalid_argument`, and the §8 stats
28
+ ops (`priority-matrix`/`part-of-rollup`/`open-curve`) exist only in a build at
29
+ or after the one that mounted them. Compare the `backlog create` and `backlog
30
+ priority-matrix` lines of `adhd-backlog --help` with §1 before relying on a
31
+ field.
32
+
33
+ ## 1. Command surface — 17 verbs (plus `batch`), one calling convention
34
+
35
+ **Every verb takes a single `--input` flag carrying one JSON object.** There
36
+ are no per-field flags.
37
+
38
+ ```
39
+ adhd-backlog backlog get --input '<IIssueGetInput json>'
40
+ adhd-backlog backlog query --input '<IIssueQueryInput json>'
41
+ adhd-backlog backlog priority-matrix --input '<IPriorityMatrixInput json>'
42
+ adhd-backlog backlog part-of-rollup --input '<IPartOfRollupInput json>'
43
+ adhd-backlog backlog open-curve --input '<IOpenCurveInput json>'
44
+ adhd-backlog backlog lookup --input '{"q": "<tool, file path, or URL>"}'
45
+ adhd-backlog backlog create --input '<ICreateIssueInput json>'
46
+ adhd-backlog backlog update --input '<IUpdateIssueInput json>'
47
+ adhd-backlog backlog transition --input '<ITransitionInput json>'
48
+ adhd-backlog backlog claim --input '<IClaimInput json>'
49
+ adhd-backlog backlog relate --input '<IRelateInput json>'
50
+ adhd-backlog backlog move --input '<IMoveIssueInput json>'
51
+ adhd-backlog backlog upsert-project --input '<IUpsertProjectInput json>'
52
+ adhd-backlog backlog upsert-component --input '<IUpsertComponentInput json>'
53
+ adhd-backlog backlog upsert-location --input '<IUpsertLocationInput json>'
54
+ adhd-backlog backlog rm-location --input '<IRmLocationInput json>'
55
+ adhd-backlog backlog delete --input '<IDeleteIssueInput json>'
56
+ adhd-backlog batch action --input '<IBatchActionInput json>'
57
+ ```
58
+
59
+ The `backlog` segment in front of every verb (and `batch` in front of
60
+ `action`) is the CLI namespace each operation is mounted under, and is the
61
+ form `adhd-backlog --help` prints. The leading segment is optional: the CLI
62
+ accepts both `adhd-backlog get --input …` and
63
+ `adhd-backlog backlog get --input …` (identical). Running `adhd-backlog
64
+ --help` (or an unknown command) prints the exact live shape of every input:
65
+
66
+ ```
67
+ $ adhd-backlog --help
68
+ Available commands:
69
+
70
+ backlog claim { input: { uid: string, by: string, action: 'claim'|'release'|'renew', force?: boolean } }
71
+ backlog create { input: { title: string, body: string, project: string, component?: string, kind?: string, status?: string, priority?: string, citations?: object[], author?: string, assignee?: string, gitContext?: string, by: string, duplicateAction?: 'abort'|'force'|'comment', awaitEmbed?: boolean } }
72
+ backlog delete { input: { uid: string, reason: string, by: string, awaitEmbed?: boolean } }
73
+ backlog get { input: { uid: string, fields?: union[] } | { registry: 'project'|'component'|'location', name: string, filter?: object } }
74
+ backlog lookup { input: { q: string } }
75
+ backlog move { input: { uid: string, toProject?: string, toComponent?: string, by: string } }
76
+ backlog open-curve { input: { filter?: object, at: string[] } }
77
+ backlog part-of-rollup { input: { uid: string } }
78
+ backlog priority-matrix { input: { filter?: object } }
79
+ backlog query { input: { text?: string, filter?: object, fields?: union[], sort?: 'priority'|'updated'|'created'|'relevance'|'textMatch', direction?: 'asc'|'desc', limit?: number, offset?: number, after?: string, view?: 'list'|'ready'|'graph'|'order'|'stale'|'similar'|'overlap'|'projects'|'components'|'locations', format?: 'json'|'markdown', overlapAxis?: 'file'|'project'|'component'|'author', overlapUids?: string[], staleAfterMin?: number } }
80
+ backlog relate { input: { sourceUid: string, targetUid: string, rel: 'relates_to'|'supersedes'|'blocks'|'duplicate_of'|'part_of', action: 'add'|'remove', by: string } }
81
+ backlog rm-location { input: { uid: string, by: string, reason?: string } }
82
+ backlog transition { input: { uid: string, by: string, toStatus: string, note?: string, citations?: object[], gitContext?: string } }
83
+ backlog update { input: { uid: string, by: string, title?: string, body?: string, kind?: string, priority?: string, assignee?: string, author?: string, awaitEmbed?: boolean } }
84
+ backlog upsert-component { input: { project: string, name: string, path?: string, description?: string, by: string } }
85
+ backlog upsert-location { input: { component: string, project?: string, locType: 'path'|'url'|'tool', value: string, by: string } }
86
+ backlog upsert-project { input: { name: string, path?: string, repoUrl?: string, monorepo?: boolean, description?: string, by: string } }
87
+ batch action { input: { operation: 'backlog/get', items: object[], concurrency?: number, mode?: 'parallel'|'serial'|'chained', onItemError?: 'continue'|'abort', itemTimeoutMs?: number } }
88
+ ```
89
+
90
+ Trust that output over anything hardcoded here — it is the live schema, not
91
+ a stale copy of it.
92
+
93
+ ### Special commands — no `--input`, and not verbs
94
+
95
+ `serve`, `install-skill` (alias `install`), `search`, `sandbox-path`, and
96
+ `store-check` are handled before the command table:
97
+
98
+ ```
99
+ adhd-backlog serve [--transport mcp|http|both] [--port N] [--host H]
100
+ adhd-backlog install-skill [--host claude|codex|opencode|all] [--scope user|project]
101
+ adhd-backlog search "<query text>" [--limit n]
102
+ adhd-backlog sandbox-path
103
+ adhd-backlog store-check
104
+ ```
105
+
106
+ `store-check` reports the store vocabulary this build expects versus the kinds
107
+ actually present in the resolved store, exiting non-zero on a mismatch — useful
108
+ after pointing a build at a store written by a different build.
109
+
110
+ `sandbox-path` prints the resolved store location and exits without opening
111
+ it — `{"namespace":"production"|"test"|"sandbox","adhdRoot":"…","dbPath":"…","embeddingEnabled":bool}`.
112
+ Use it to confirm WHICH store a command would touch before running a write.
113
+ Combined with the global `--namespace sandbox` flag (valid before any
114
+ command) it reports the throwaway store that flag would mint, so you can
115
+ check isolation without creating anything.
116
+
117
+ Two conventions apply to every transcript below. **Uids are truncated with
118
+ `…` for readability** — always pass the FULL value the previous call
119
+ returned, never the ellipsis form. And **every invocation prints warnings on
120
+ stderr** (telemetry, the embedding backend, onnxruntime) whether or not it
121
+ succeeded; stdout carries the JSON envelope alone. Parse stdout, key on the
122
+ exit code, and ignore stderr — it is noise, not a failure signal.
123
+
124
+ `search "x" --limit 2` is the argv-flag shortcut for `backlog query --input
125
+ '{"text":"x","limit":2}'` — same envelope, same exit codes, verified:
126
+
127
+ ```
128
+ $ adhd-backlog search "auth module" --limit 5
129
+ {"ok":true,"data":{"view":"list","items":[{"uid":"…","title":"Flaky test in auth module","kind":"issue","status":"closed","priority":"CRITICAL"}],"hasMore":false},"meta":{"total":1,"returned":1,"limit":5}}
130
+ ```
131
+
132
+ `--namespace <value>` is a global flag valid before ANY command — it selects
133
+ which declared store instance to resolve against: `production` (default),
134
+ `test` (a persisted, non-ephemeral store), or `sandbox`. `--namespace
135
+ sandbox` additionally diverts the invocation into a fresh throwaway store
136
+ (`mkdtemp` + its own DB) instead of the real one, writes a real `config.yaml`
137
+ there with `embedding.enabled: false` so a sandboxed run never pays a real
138
+ model-load cost, and prints the path so you can pass `ADHD_ROOT=<path>` to
139
+ reuse it across calls. Use it whenever you want to try a command without
140
+ touching production data — verified:
141
+
142
+ ```
143
+ $ adhd-backlog --namespace sandbox backlog upsert-project --input '{"name":"sandbox-demo","by":"claude:1"}'
144
+ [backlog] --namespace sandbox: isolated store at /var/folders/.../backlog-sandbox-AL1cJH (not auto-deleted — pass ADHD_ROOT=... to reuse it, or remove it yourself when done)
145
+ {"ok":true,"data":{"uid":"3ec19363-cd8c-4479-8d7f-c8e4e9f0948b","created":true,"project":{"uid":"3ec19363-cd8c-4479-8d7f-c8e4e9f0948b","name":"sandbox-demo"}}}
146
+ ```
147
+
148
+ ## 2. The outcome envelope — every verb, every transport
149
+
150
+ Every verb returns one of exactly two shapes, always the same envelope
151
+ regardless of transport:
152
+
153
+ ```jsonc
154
+ { "ok": true, "data": { /* verb-specific payload */ }, "warnings": [], "meta": {} }
155
+ { "ok": false, "error": { "code": "item_not_found", "message": "…", "details": {} } }
156
+ ```
157
+
158
+ Never assume an unwrapped payload — always read `envelope.data`. **This holds
159
+ for every error a VERB itself reports** — a validation failure the operation's
160
+ own logic detects (bad `limit`, unknown filter key, a terminal transition
161
+ missing its citation) always comes back as `{"ok":false,"error":{...}}` on
162
+ stdout, exit non-zero.
163
+
164
+ It does NOT hold for a malformed `--input` on the CLI: a request that fails
165
+ the outer ajv schema check — missing a required field, an unknown property,
166
+ invalid JSON — never reaches a verb's own logic at all. That class prints an
167
+ UNWRAPPED `{"code":"invalid_argument","message":"…","details":[...]}` (no
168
+ `ok` key) to **stderr**, not stdout, still with the matching `invalid_argument`
169
+ exit code below. A caller that only reads stdout per the envelope contract
170
+ gets nothing at all for this — the single most common error shape a bad
171
+ caller hits — so read stderr too whenever stdout is empty and the exit code
172
+ is non-zero. There are exactly nine error codes for a verb's own reported
173
+ failures, and the CLI's process exit code is derived from `error.code`:
174
+
175
+ | code | exit | meaning |
176
+ | --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------- |
177
+ | `not_found` | 4 | a referenced catalog entry (project/component/kind/status/priority) does not exist |
178
+ | `item_not_found` | 1 | the addressed issue `uid` does not exist |
179
+ | `invalid_argument` | 2 | malformed flag or parameter shape |
180
+ | `validation` | 2 | schema rejection — unknown filter key, unknown projection field, over-limit |
181
+ | `store_busy` | 1 | store contention (busy/lease) — `details.retryable`/`retryAfterMs` say whether and how to retry; never hot-loop |
182
+ | `rag_not_configured` | 1 | a semantic/similarity read with no embedding backend, or an empty vector space |
183
+ | `conflict` | 1 | someone else holds the claim, a single-valued relation is taken, or a supersede raced |
184
+ | `precondition_failed` | 1 | a gate refused the write — a terminal transition missing its required citation/note, or an unverifiable citation |
185
+ | `internal` | 1 | unclassified server-side failure |
186
+
187
+ Success is always exit `0`.
188
+
189
+ ### MCP tool names
190
+
191
+ Each verb is also an MCP tool once `.mcp.json` wires the server, named
192
+ `backlog_<verb>` with the verb's own words snake_cased: `backlog_get`,
193
+ `backlog_query`, `backlog_priority_matrix`, `backlog_part_of_rollup`,
194
+ `backlog_open_curve`, `backlog_lookup`, `backlog_create`, `backlog_update`,
195
+ `backlog_transition`, `backlog_claim`, `backlog_relate`, `backlog_move`,
196
+ `backlog_upsert_project`, `backlog_upsert_component`,
197
+ `backlog_upsert_location`, `backlog_rm_location`, `backlog_delete`, plus the
198
+ un-namespaced `batch_action`.
199
+
200
+ ## 3. Issue verbs — worked examples
201
+
202
+ Every mutating verb requires `by` — the acting identity, always
203
+ `${agentName}:${instanceId}`, never a bare role literal like `"agent"`. A
204
+ missing/blank `by` is rejected with `invalid_argument` before any write
205
+ runs.
206
+
207
+ **File a new issue.** `project` is RESOLVE-ONLY — `create` never mints one;
208
+ register it first with `upsert-project` (§4). `component` is also resolve-only
209
+ and defaults to the project's reserved `(root)` component when omitted — pass
210
+ it, or the item is invisible to component-scoped scans (§4, "The filing rule"):
211
+
212
+ ```
213
+ $ adhd-backlog backlog create --input '{
214
+ "title": "Flaky test in auth module",
215
+ "body": "The auth integration test times out intermittently.",
216
+ "project": "demo-project",
217
+ "by": "claude:1",
142
218
  "priority": "HIGH"
143
219
  }'
220
+ {"ok":true,"data":{"created":true,"uid":"a61ff0b6-a0f1-4189-9923-671f6cbacd4e","item":{"uid":"a61ff0b6-a0f1-4189-9923-671f6cbacd4e","title":"Flaky test in auth module","kind":"issue","status":"open","priority":"HIGH","project":"020e87f2-…","component":"dcf134ab-…","createdAt":"2026-09-17T01:22:56.056Z","author":"claude:1"}}}
221
+ ```
222
+
223
+ Filing more than a handful of similar issues in a row? Use `batch action`
224
+ (§5) instead of repeating this call.
225
+
226
+ `create` runs a dedupe scan (FTS + semantic, when embeddings are configured)
227
+ BEFORE writing. `duplicateAction` (default `'abort'`) controls what happens
228
+ when the scan surfaces a candidate at/above the project's dedupe threshold:
229
+ `'abort'` — nothing is written, `{created:false, reason:'duplicate-suppressed',
230
+ duplicateCandidates}`; `'force'` — writes a genuinely new issue anyway,
231
+ still reporting `duplicateCandidates`; `'comment'` — no new issue is
232
+ written, a note is attached to the top-scoring candidate instead. A
233
+ zero-candidate scan proceeds to a normal create regardless of
234
+ `duplicateAction`. **Always inspect `duplicateCandidates` before forcing.**
235
+
236
+ **Read one issue.** `get` returns a terse five-field card
237
+ (`uid`/`kind`/`title`/`status`/`priority`) by default — ask for more
238
+ explicitly:
239
+
240
+ ```
241
+ $ adhd-backlog backlog get --input '{"uid":"a61ff0b6-…"}'
242
+ {"ok":true,"data":{"uid":"a61ff0b6-…","title":"Flaky test in auth module","kind":"issue","status":"open","priority":"HIGH"}}
243
+
244
+ $ adhd-backlog backlog get --input '{"uid":"a61ff0b6-…","fields":["body","citations"]}'
245
+ {"ok":true,"data":{"uid":"a61ff0b6-…","body":"The auth integration test times out intermittently.","citations":[]}}
246
+ ```
247
+
248
+ `get`'s own `--help` now renders its full union —
249
+ `{ uid, fields? } | { registry, name, filter? }`. The second form reads one
250
+ registry entry by name directly, e.g.
251
+ `{"registry":"project","name":"demo-project"}` returns the project's
252
+ `{uid, name, path, components, locations}` — the same data `query`'s
253
+ `view:"projects"`/`"components"`/`"locations"` list in bulk (§4).
254
+
255
+ The full field vocabulary is `uid, title, kind, status, priority, project,
256
+ component, createdAt, updatedAt, assignee, author, closedAt` (cheap/plain)
257
+ plus `body, citations, notes, auditTrail, blockers, related, _score,
258
+ _vector` (opt-in only — each costs a genuine extra read, so none is in the
259
+ default card).
260
+
261
+ **Search/filter/page issues:**
262
+
263
+ ```
264
+ $ adhd-backlog backlog query --input '{"filter":{"project":"demo-project","status":"open"},"limit":10}'
265
+ {"ok":true,"data":{"view":"list","items":[{"uid":"a61ff0b6-…","title":"Flaky test in auth module","kind":"issue","status":"open","priority":"HIGH"}],"hasMore":false},"meta":{"total":1,"returned":1,"limit":10}}
266
+ ```
267
+
268
+ `query.view` (default `'list'`) selects the result shape: `list` · `ready` ·
269
+ `graph` · `order` · `stale` · `similar` · `overlap` · `projects` · `components`
270
+ · `locations` (the last three are the registry LIST views — §4). `text` is the
271
+ natural-language form — routed to `filter.semantic` when a populated vector
272
+ space can rank it, or `filter.grep` (keyword FTS) otherwise; never set
273
+ `text` alongside `filter.semantic`/`filter.grep` yourself. Pagination is
274
+ truthful: `meta.total` is the count before `limit`/`offset`, `meta.returned`
275
+ is `data.items.length`, and a page cut short for any reason other than your
276
+ own `limit` sets `meta.truncated`.
277
+
278
+ **Edit an existing issue.** Every field edits in place **except `body`**: a
279
+ `body` change SUPERSEDES the issue, minting a successor node with a fresh
280
+ `uid` and carrying the old node's edges forward. The response's `uid` is the
281
+ successor; the old `uid` becomes a `SUPERSEDES`-linked history node, and
282
+ addressing it returns `conflict` naming the successor (see the example below).
283
+ A caller that persists uids must follow that pointer after any body edit.
284
+ `status` is not editable here — use `transition`:
285
+
286
+ ```
287
+ $ adhd-backlog backlog update --input '{"uid":"a61ff0b6-…","by":"claude:1","priority":"CRITICAL"}'
288
+ {"ok":true,"data":{"uid":"a61ff0b6-…","changed":["priority"]}}
289
+ ```
290
+
291
+ A `body` edit returns the successor's `uid`, and the pre-edit `uid` then
292
+ resolves to a redirect (never to the stale record):
293
+
294
+ ```
295
+ $ adhd-backlog backlog update --input '{"uid":"a61ff0b6-…","by":"claude:1","body":"new body"}'
296
+ {"ok":true,"data":{"uid":"e3b32183-…","changed":["body"]}}
297
+
298
+ $ adhd-backlog backlog get --input '{"uid":"a61ff0b6-…"}'
299
+ {"ok":false,"error":{"code":"conflict","message":"Issue \"a61ff0b6-…\" was superseded by a body edit and is no longer the live issue; it now lives under \"e3b32183-…\"","details":{"retryable":false}}}
300
+ ```
301
+
302
+ **Move an issue to a new status.** A terminal `toStatus` REQUIRES `citations`
303
+ only when the project's policy turns citation enforcement ON — `citationRequired`
304
+ defaults to `false`, so out of the box no citation is required. When a project
305
+ HAS turned it on, citations must be verifiable against the project's own
306
+ filesystem path — an unverifiable citation is rejected with `precondition_failed`:
307
+
308
+ ```
309
+ $ adhd-backlog backlog transition --input '{
310
+ "uid": "a61ff0b6-…", "by": "claude:1", "toStatus": "closed",
311
+ "note": "fixed",
312
+ "citations": [{ "file": "packages/auth/src/index.ts", "lines": "1-1" }]
313
+ }'
314
+ {"ok":true,"data":{"uid":"a61ff0b6-…","fromStatus":"open","toStatus":"closed","transitionUid":"8b802b1b-…"}}
315
+ ```
316
+
317
+ **`toStatus` is an open catalog, not a fixed enum.** `open`/`claimed`/
318
+ `closed` are the conventional names, not the permitted set. An unresolved
319
+ NAME is not an error — it MINTS a new status (`terminal:false`) and the
320
+ transition succeeds, exactly as `create`'s own `status` field behaves. Only a
321
+ uid-SHAPED reference resolving to nothing is rejected, with `not_found`:
322
+
323
+ ```
324
+ $ adhd-backlog backlog transition --input '{"uid":"a61ff0b6-…","by":"claude:1","toStatus":"awaiting-review","note":"n"}'
325
+ {"ok":true,"data":{"uid":"a61ff0b6-…","fromStatus":"open","toStatus":"awaiting-review","transitionUid":"f52b0f50-…"}}
326
+ ```
327
+
328
+ So a typo becomes a real status rather than an error, and the issue silently
329
+ leaves the set `filter.status:"open"` returns. Treat the status name as
330
+ load-bearing input: pass one you can spell, or read the catalog first.
331
+
332
+ **Claim / renew / release protocol (multi-agent use).** Claiming is
333
+ idempotent for the SAME claimant — a second `action:"claim"` from the same
334
+ `by` returns `status:"renewed"` rather than a contention error, so retrying
335
+ after a lost response is always safe. A
336
+ long-running task renews periodically; every exit path releases
337
+ unconditionally (a no-op if already unclaimed):
338
+
339
+ ```
340
+ $ adhd-backlog backlog claim --input '{"uid":"a61ff0b6-…","by":"claude:1","action":"claim"}'
341
+ {"ok":true,"data":{"uid":"a61ff0b6-…","status":"claimed","claimedBy":"claude:1","claimedAt":"2026-09-17T01:24:31.896Z"}}
342
+
343
+ $ adhd-backlog backlog claim --input '{"uid":"a61ff0b6-…","by":"claude:1","action":"renew"}'
344
+ {"ok":true,"data":{"uid":"a61ff0b6-…","status":"renewed","claimedBy":"claude:1","claimedAt":"2026-09-17T01:24:33.468Z"}}
345
+
346
+ $ adhd-backlog backlog claim --input '{"uid":"a61ff0b6-…","by":"claude:1","action":"release"}'
347
+ {"ok":true,"data":{"uid":"a61ff0b6-…","status":"released"}}
348
+ ```
349
+
350
+ **Link two issues.** `rel` is one of `relates_to`, `supersedes`, `blocks`,
351
+ `duplicate_of`, `part_of` — NOT the bare word `"related"`:
352
+
353
+ ```
354
+ $ adhd-backlog backlog relate --input '{"sourceUid":"777c5e33-…","targetUid":"a61ff0b6-…","rel":"relates_to","action":"add","by":"claude:1"}'
355
+ {"ok":true,"data":{"sourceUid":"777c5e33-…","targetUid":"a61ff0b6-…","rel":"relates_to","action":"add","noop":false}}
356
+ ```
357
+
358
+ `noop:true` means `add` found an already-live matching edge, or `remove`
359
+ found none — no edge was written and no audit row produced; never assume
360
+ every call was a fresh write. `targetUid` may belong to a different project
361
+ than `sourceUid`.
362
+
363
+ **Move an issue to a different project/component.** `toProject`/
364
+ `toComponent` are RESOLVE-ONLY, never minted — register the destination
365
+ first with `upsert-project`/`upsert-component` if it doesn't exist yet:
366
+
367
+ ```
368
+ $ adhd-backlog backlog move --input '{"uid":"777c5e33-…","toProject":"demo-project","by":"claude:1"}'
369
+ {"ok":true,"data":{"uid":"777c5e33-…","noop":false,"fromProject":"38b9af5d-…","toProject":"020e87f2-…","fromComponent":"22113591-…","toComponent":"dcf134ab-…"}}
370
+ ```
371
+
372
+ **Soft-delete an issue.** `reason` is REQUIRED. The node is closed off
373
+ bi-temporally, never physically removed — its audit trail and every edge
374
+ pointing at it remain readable:
375
+
376
+ ```
377
+ $ adhd-backlog backlog delete --input '{"uid":"777c5e33-…","reason":"duplicate of tracked work","by":"claude:1"}'
378
+ {"ok":true,"data":{"uid":"777c5e33-…","invalidated":true}}
379
+ ```
380
+
381
+ ## 4. Registry — project / component / location
382
+
383
+ The registry answers **"where does this live, and what do I file the bug
384
+ against?"** in one call, before you `rg`/search for it.
385
+
386
+ ### The model
387
+
388
+ - **project** — one repo/workspace root, registered with its filesystem `path`
389
+ and/or git `repoUrl`. ONE canonical row per logical repo (`adhd`,
390
+ `sox-ecosystem`). Every issue verb RESOLVES a project by name or `uid` and
391
+ **never mints one** — an unknown project name is `not_found` (exit 4).
392
+ - **component** — a path _within_ that project (`entrypoint/backlog`,
393
+ `tools/nx-plugins/build`), resolved-only within its project. `upsert-project`
394
+ mints exactly ONE reserved component, `(root)`, per project; `create`
395
+ defaults an omitted `component` to it. **No other component is ever
396
+ auto-created** — an unknown component name is `not_found`, never a new row.
397
+ - **location** — a tool name, file path, or URL owned by a component; the thing
398
+ `lookup` resolves.
399
+
400
+ ### The filing rule — file every item with the right project AND component
401
+
402
+ A component-less item is not an error: it lands on `(root)` and is then
403
+ **invisible to every component-scoped query** (`filter.component:"…"`), while
404
+ still appearing in a project-scoped one. That is the misfiling signature —
405
+ `get <uid>` finds the item, but the component scan that should list it never
406
+ does. So, before filing:
407
+
408
+ 1. Discover the exact registered names:
409
+ `adhd-backlog query --input '{"view":"projects"}'` and
410
+ `adhd-backlog query --input '{"view":"components","filter":{"project":"<p>"}}'`.
411
+ 2. Register anything missing with `upsert-project`/`upsert-component` FIRST.
412
+ 3. `create` with both `project` and `component`.
413
+
414
+ A component-scoped scan is how a repo's own work is found (e.g.
415
+ `filter.component:"entrypoint/backlog"` for this repo's own items, or project
416
+ `adhd`); an item filed on `(root)` is invisible to it.
417
+
418
+ ### When to use each registry verb
419
+
420
+ | verb | use it when | idempotent key |
421
+ | ------------------ | -------------------------------------------------------------------------------- | ----------------------------- |
422
+ | `upsert-project` | registering/updating a repo or workspace root; also mints its `(root)` component | `name` |
423
+ | `upsert-component` | registering/updating a path _inside_ an already-registered project | `(project, name)` |
424
+ | `upsert-location` | pointing a tool/file/URL at its owning component so `lookup` resolves it | `(component, locType, value)` |
425
+ | `rm-location` | retiring a location (soft-invalidate) | `uid` |
426
+
427
+ All four are create-or-update by that key — never a duplicate row — and all
428
+ require `by`.
429
+
430
+ **Register or update a project** (create-or-update by `name`; also mints the
431
+ project's reserved default component `(root)` on first creation):
432
+
433
+ ```
434
+ $ adhd-backlog backlog upsert-project --input '{"name":"demo-project","path":"/tmp/demo","by":"claude:1"}'
435
+ {"ok":true,"data":{"uid":"020e87f2-…","created":true,"project":{"uid":"020e87f2-…","name":"demo-project","path":"/tmp/demo"}}}
436
+ ```
437
+
438
+ **Register or update a component** (create-or-update by `(project, name)`;
439
+ `project` is resolve-only):
440
+
441
+ ```
442
+ $ adhd-backlog backlog upsert-component --input '{"project":"demo-project","name":"auth-service","path":"packages/auth","by":"claude:1"}'
443
+ {"ok":true,"data":{"uid":"41a61c6d-…","created":true,"component":{"uid":"41a61c6d-…","name":"auth-service","projectUid":"020e87f2-…","path":"packages/auth"}}}
444
+ ```
445
+
446
+ **Register a location** — a tool name, file path, or URL owned by a
447
+ component. A bare component NAME requires `project` to disambiguate it (a
448
+ `uid` never does):
449
+
450
+ ```
451
+ $ adhd-backlog backlog upsert-location --input '{"component":"auth-service","project":"demo-project","locType":"path","value":"packages/auth/src/index.ts","by":"claude:1"}'
452
+ {"ok":true,"data":{"uid":"a4b0dd6b-…","created":true,"location":{"uid":"a4b0dd6b-…","locType":"path","value":"packages/auth/src/index.ts","componentUid":"41a61c6d-…"}}}
453
+ ```
454
+
455
+ **Resolve a tool, file, or URL to its owning project/component** — the
456
+ go-to before searching for "which repo owns this?":
457
+
458
+ ```
459
+ $ adhd-backlog backlog lookup --input '{"q":"packages/auth/src/index.ts"}'
460
+ {"ok":true,"data":{"project":{"uid":"020e87f2-…","name":"demo-project","path":"/tmp/demo"},"component":{"uid":"41a61c6d-…","name":"auth-service","path":"packages/auth"},"location":{"uid":"a4b0dd6b-…","locType":"path","value":"packages/auth/src/index.ts"}}}
461
+ ```
462
+
463
+ `lookup` classifies `q` automatically as a tool name, file path, or URL —
464
+ it only resolves against LOCATIONS already registered via `upsert-location`,
465
+ never against a bare project/component name. An unregistered value returns
466
+ `not_found` (exit 4); a path miss falls back to a suffix/prefix scan before
467
+ giving up, and reports a `hint` when only a partial match was found — never
468
+ a silent empty result.
469
+
470
+ **Remove a location** (soft-invalidate by `uid`):
471
+
472
+ ```
473
+ $ adhd-backlog backlog rm-location --input '{"uid":"a4b0dd6b-…","by":"claude:1","reason":"tool renamed"}'
474
+ {"ok":true,"data":{"uid":"a4b0dd6b-…","invalidated":true}}
475
+ ```
476
+
477
+ **List every project/component/location in the registry** — the go-to for
478
+ "what does this repo have registered?" without hand-rolling a scan. This is
479
+ `query`'s `view:"projects"`/`"components"`/`"locations"` (not a separate
480
+ verb): every live row of that kind, optionally scoped by `filter.project`
481
+ (and, for `locations`, `filter.component`):
482
+
483
+ ```
484
+ $ adhd-backlog backlog query --input '{"view":"projects"}'
485
+ {"ok":true,"data":{"view":"projects","items":[{"uid":"020e87f2-…","name":"demo-project","path":"/tmp/demo"}]}}
486
+
487
+ $ adhd-backlog backlog query --input '{"view":"components","filter":{"project":"demo-project"}}'
488
+ {"ok":true,"data":{"view":"components","items":[{"uid":"41a61c6d-…","name":"auth-service","projectUid":"020e87f2-…","path":"packages/auth"},{"uid":"…","name":"(root)","projectUid":"020e87f2-…"}]}}
489
+
490
+ $ adhd-backlog backlog query --input '{"view":"locations","filter":{"component":"auth-service","project":"demo-project"}}'
491
+ {"ok":true,"data":{"view":"locations","items":[{"uid":"a4b0dd6b-…","locType":"path","value":"packages/auth/src/index.ts","componentUid":"41a61c6d-…"}]}}
492
+ ```
493
+
494
+ ### Filing hazards (real, observed on this machine)
495
+
496
+ 1. **Duplicate project identities.** The same repo can exist as TWO project
497
+ rows — one path-derived, one repo-derived — and an item lands under
498
+ whichever name you pass. Observed split (2026-09-22): `sox-ecosystem` (path)
499
+ vs `PseudoSky/sox-ecosystem` (no path); `claude-agents` (path) vs
500
+ `PseudoSky/claude-agents`; `claude-tools` vs `QuSecure/claude-tools`;
501
+ `dot` vs `id8/dot`. Prefer the row that carries a `path` (and, for an
502
+ active repo, the bulk of the items) — an item under the other row is
503
+ invisible to a query scoped to the first. (`adhd` is already reconciled to
504
+ one row; `PseudoSky/adhd` does not exist.)
505
+ 2. **Store/scope confusion — an item can land in a store nobody reads.**
506
+ `adhd-backlog sandbox-path` reports the store a command will touch. Each
507
+ namespace resolves to its OWN file under `~/.adhd/backlog/<namespace>/data/`
508
+ (default `backlog.db`; that namespace's `config.yaml` may pin another name),
509
+ so the `production` store is never the `test` store. `ADHD_BACKLOG_SCOPE`
510
+ only changes the scope ROOT; an absolute `db.path` from any config layer
511
+ still wins (`db.path ?? files.db`) — run `sandbox-path` to confirm. A build
512
+ that writes a per-repo namespace (e.g. `entrypoint/backlog` on
513
+ `main`) files items that are silently absent from production — no error,
514
+ just a missing row (filed as 49ce83b8). Run `sandbox-path` before a write
515
+ you care about, and file through the production CLI only.
516
+
517
+ ## 5. Batch — N-way fan-out over one operation
518
+
519
+ `batch action` runs the SAME operation over many items. `operation` is the
520
+ mounted operation id, namespaced as `backlog/<verb>` (not the bare verb
521
+ name), and each entry in `items` wraps its payload under `input`:
522
+
523
+ ```
524
+ $ adhd-backlog batch action --input '{
525
+ "operation": "backlog/create",
526
+ "items": [
527
+ { "input": { "title": "Batch item one", "body": "first", "project": "demo-project", "by": "claude:1" } },
528
+ { "input": { "title": "Batch item two", "body": "second", "project": "demo-project", "by": "claude:1" } }
529
+ ]
530
+ }'
531
+ [{"index":0,"status":"fulfilled","value":{"ok":true,"data":{"created":true,"uid":"874dfa26-…", …}}},
532
+ {"index":1,"status":"fulfilled","value":{"ok":true,"data":{"created":true,"uid":"e071b0b8-…", …}}}]
533
+ ```
534
+
535
+ Each result is `{index, status:'fulfilled', value}` or `{index,
536
+ status:'rejected', reason}` — `value`/`reason` is the SAME outcome envelope
537
+ `backlog/<verb>` would return standalone, so a batched item's own `ok`/
538
+ `error.code` still applies. `mode` (`'parallel'` default · `'serial'` ·
539
+ `'chained'`), `onItemError` (`'continue'` default · `'abort'`), and
540
+ `concurrency`/`itemTimeoutMs` govern how the fan-out runs. The valid
541
+ `operation` values are exactly the 17 mounted verbs above (the nine issue
542
+ verbs, the four registry verbs, and the three stats reads of §8), each
543
+ prefixed `backlog/` — passing a bare verb name (`"create"`) is rejected with
544
+ `invalid_argument` naming the full list.
545
+
546
+ ## 6. Citations — structured, not hand-typed markdown
547
+
548
+ A citation is `{ file, lines?, context?, symbol? }`. Pass `citations` on
549
+ `create` or on the `transition` that moves an issue into a terminal status.
550
+ They are required and enforced only when the project's policy demands it:
551
+ `citationRequired` defaults to `false`, so out of the box a terminal
552
+ transition needs no citation (what IS required by default is a `note` —
553
+ `transitionRequiresNote`). When a project HAS turned `citationRequired` on, a
554
+ terminal transition with no citations, or with an unverifiable one, is
555
+ rejected with `precondition_failed`. "Unverifiable" means the file
556
+ could not be confirmed to exist under the project's own registered path —
557
+ never resolved outside it. The verification gate applies only when the
558
+ project HAS a registered path; a project with no `path` cannot hash any
559
+ citation target at all, so its citations are accepted and recorded with
560
+ `sha: "unverified"`.
561
+
562
+ Name a `symbol` on a citation to get best-effort blast-radius enrichment for
563
+ free — the store shells out to `gitnexus impact <symbol>` at write time
564
+ (bounded timeout, never blocks or fails the write) and stamps the citation's
565
+ `blastRadius` when gitnexus is installed and the repo is indexed. Absence of
566
+ `blastRadius` on a citation that named a `symbol` means "not enriched," never
567
+ "confirmed zero blast radius."
568
+
569
+ The item-level **`gitContext`** is separate from a citation's own `context`.
570
+ Pass `gitContext` on `create` (or on a `transition` to update it) to record
571
+ the repo disclosure contract's `<active git context>` — the FIRST element of a
572
+ `Citations:` block (`Citations: [<active git context>, …]`, per the repo
573
+ `AGENTS.md` "Cite what you read"). It is stored on the issue itself, never per
574
+ citation, and a `format:'markdown'` query renders it once at the head of the
575
+ item's `Citations:` block (`Citations: [<active git context>]`, then the
576
+ citation lines). Omit it and nothing is stored and no output changes. A
577
+ citation's own `context` is free-text prose and is never rendered by the
578
+ markdown projection — it cannot carry the git context.
579
+
580
+ ## 7. Verify writes from a NEW process
581
+
582
+ An MCP `backlog_get` served by a long-lived `serve` process can answer out
583
+ of that process's own in-memory/uncheckpointed state. After a write you
584
+ care about, verify by running the `adhd-backlog` CLI in a fresh shell — a
585
+ genuinely new process — rather than re-reading through the same live MCP
586
+ session.
587
+
588
+ ## 8. Stats & rollups
589
+
590
+ Three aggregate read views are first-class mounted ops — `backlog
591
+ priority-matrix`, `backlog part-of-rollup`, `backlog open-curve` (MCP:
592
+ `backlog_priority_matrix` / `backlog_part_of_rollup` / `backlog_open_curve`).
593
+ All three are read-only and take no `by`, and each returns its own shape — a
594
+ matrix, a rollup tree, a time series — so none is a `query.view` member.
595
+
596
+ **Priority matrix** — per-priority counts, scoped by
597
+ `project`/`component`/`kind`/`status`. An omitted `filter.status` scopes to
598
+ OPEN work (unlike `list`, where an omitted status means no restriction); the
599
+ applied scope is echoed on `data.statusScope`, and `unassigned` counts in-scope
600
+ issues carrying no priority:
601
+
602
+ ```
603
+ $ adhd-backlog backlog priority-matrix --input '{}'
604
+ {"ok":true,"data":{"rows":[{"priority":"HIGH","priorityUid":"fb9d525e-…","rank":0,"count":1}],"unassigned":1,"statusScope":"open"}}
605
+ ```
606
+
607
+ **Part-of rollup** — every TRANSITIVE `part_of` descendant of the root issue
608
+ (not just direct children), counted once each regardless of chain depth, split
609
+ into `childrenOpen`/`childrenClosed` (plus the open descendants' uids):
610
+
611
+ ```
612
+ $ adhd-backlog backlog part-of-rollup --input '{"uid":"74c22c35-…"}'
613
+ {"ok":true,"data":{"uid":"74c22c35-…","childrenTotal":1,"childrenOpen":1,"childrenClosed":0,"childrenOpenUids":["db4587ba-…"]}}
614
+ ```
615
+
616
+ **Open curve** — for each sampled ISO-8601 instant, how many in-scope issues
617
+ EXISTED then (exact, via `validAt`) and, of those, how many were OPEN then
618
+ (reconstructed from the audit trail, never the issue's current status):
619
+
620
+ ```
621
+ $ adhd-backlog backlog open-curve --input '{"at":["2020-01-01T00:00:00.000Z"]}'
622
+ {"ok":true,"data":{"points":[{"at":"2020-01-01T00:00:00.000Z","existed":0,"open":0,"closed":0}]}}
144
623
  ```
145
624
 
146
- If the response has `created: false` with non-empty `duplicateCandidates`,
147
- read them before deciding whether to `force: true` or update the existing
148
- item instead (`updateItem`/`appendNote`).
625
+ The same three functions are also exported from the package's query layer
626
+ (`src/query/views/stats.ts`, re-exported by `src/query/index.ts`) for
627
+ in-process consumers — `priorityMatrix(handle, { filter? })`,
628
+ `partOfRollup(handle, { uid })`, `openCurve(handle, { filter?, at })`. That
629
+ in-process surface is described in `README.md` → "Library API".