@anyberg/agent-conventions 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 (42) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/.codex-plugin/plugin.json +6 -0
  4. package/AGENTS.md +24 -0
  5. package/README.md +244 -0
  6. package/agents/code-reviewer.md +36 -0
  7. package/agents/docs-change-steward.md +71 -0
  8. package/agents/feature-planner.md +24 -0
  9. package/agents/implementation.md +35 -0
  10. package/agents/refactoring-planner.md +38 -0
  11. package/agents/repo-search.md +32 -0
  12. package/agents/test-runner.md +34 -0
  13. package/bin/cli.js +311 -0
  14. package/gemini-extension.json +5 -0
  15. package/package.json +47 -0
  16. package/plugin.json +12 -0
  17. package/skills/api-design/SKILL.md +18 -0
  18. package/skills/architecture-planning/SKILL.md +113 -0
  19. package/skills/backlog-management/SKILL.md +73 -0
  20. package/skills/backlog-management/backends/github-issues.md +37 -0
  21. package/skills/backlog-management/backends/markdown.md +34 -0
  22. package/skills/backlog-management/scripts/detect-backend.sh +30 -0
  23. package/skills/backlog-management/scripts/generate-policy.sh +49 -0
  24. package/skills/code-review/SKILL.md +95 -0
  25. package/skills/code-standards/SKILL.md +73 -0
  26. package/skills/docs-standards/SKILL.md +95 -0
  27. package/skills/git-conventions/SKILL.md +95 -0
  28. package/skills/hatch-workflow/SKILL.md +147 -0
  29. package/skills/python-best-practices/SKILL.md +107 -0
  30. package/skills/python-coding-guidelines/SKILL.md +58 -0
  31. package/skills/python-design-patterns/SKILL.md +28 -0
  32. package/skills/rust-best-practices/SKILL.md +171 -0
  33. package/skills/rust-coding-guidelines/SKILL.md +77 -0
  34. package/skills/rust-design-patterns/SKILL.md +83 -0
  35. package/skills/task-workflow/SKILL.md +122 -0
  36. package/skills/tech-debt/SKILL.md +41 -0
  37. package/skills/test-driven-development/SKILL.md +113 -0
  38. package/skills/testing-strategy/SKILL.md +35 -0
  39. package/skills/typescript-coding-guidelines/SKILL.md +55 -0
  40. package/src/plan.js +116 -0
  41. package/src/targets.js +95 -0
  42. package/src/write.js +184 -0
package/bin/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-conventions installer.
4
+ *
5
+ * Deliberately dependency-free. This is run through `npx` on machines that have
6
+ * never seen it before, so every dependency is a supply-chain decision made on
7
+ * the user's behalf. Prompts are ~40 lines of readline; that is cheaper than
8
+ * owning a prompt library's transitive tree.
9
+ */
10
+
11
+ import fs from 'node:fs'
12
+ import os from 'node:os'
13
+ import path from 'node:path'
14
+ import readline from 'node:readline/promises'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ import { buildPlan, renderDisclosure } from '../src/plan.js'
18
+ import { SELECTABLE_AGENTS, parseAgentSelection } from '../src/targets.js'
19
+ import {
20
+ applyInstructionWrite, installSkill, linkSkill, readReceipt, removeBlock, writeReceipt,
21
+ } from '../src/write.js'
22
+
23
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
24
+ const PKG = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'))
25
+
26
+ const USAGE = `
27
+ agent-conventions ${PKG.version}
28
+
29
+ npx @anyberg/agent-conventions install (prompts for scope and agents)
30
+ npx @anyberg/agent-conventions uninstall remove exactly what the receipt records
31
+
32
+ Options
33
+ -g, --global install for every project on this machine
34
+ -p, --project install into the current project only
35
+ -a, --agent <list> comma-separated, or "all" (claude-code, codex,
36
+ github-copilot, opencode, cursor, gemini-cli)
37
+ -c, --components <list> skills,instructions (default: both at global scope,
38
+ skills only at project scope)
39
+ --copy copy instead of symlinking
40
+ --replace-symlinks replace an existing instruction symlink with a real
41
+ file. Without this, a symlink is refused rather than
42
+ written through.
43
+ --dry-run print the plan and exit without writing
44
+ -y, --yes skip the confirmation prompt (the plan is still printed)
45
+ -h, --help
46
+ `.trim()
47
+
48
+ function parseArgs(argv) {
49
+ const opts = {
50
+ command: 'install', scope: null, agents: null, components: null,
51
+ copy: false, replaceSymlinks: false, dryRun: false, yes: false, help: false,
52
+ }
53
+ const rest = [...argv]
54
+ while (rest.length) {
55
+ const arg = rest.shift()
56
+ switch (arg) {
57
+ case 'install': opts.command = 'install'; break
58
+ case 'uninstall': case 'remove': opts.command = 'uninstall'; break
59
+ case '-g': case '--global': opts.scope = 'global'; break
60
+ case '-p': case '--project': opts.scope = 'project'; break
61
+ case '-a': case '--agent': opts.agents = rest.shift(); break
62
+ case '-c': case '--components': opts.components = rest.shift(); break
63
+ case '--copy': opts.copy = true; break
64
+ case '--replace-symlinks': opts.replaceSymlinks = true; break
65
+ case '--dry-run': opts.dryRun = true; break
66
+ case '-y': case '--yes': opts.yes = true; break
67
+ case '-h': case '--help': opts.help = true; break
68
+ default:
69
+ if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)
70
+ }
71
+ }
72
+ return opts
73
+ }
74
+
75
+ /**
76
+ * Are we being driven by an agent rather than a person?
77
+ *
78
+ * A prompt nobody can answer is a hang, so anything non-interactive runs with
79
+ * defaults and prints the plan instead of asking.
80
+ */
81
+ function isInteractive() {
82
+ if (process.env.CI) return false
83
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE) return false
84
+ if (process.env.AGENT || process.env.OPENCODE || process.env.CURSOR_AGENT) return false
85
+ return process.stdin.isTTY === true && process.stdout.isTTY === true
86
+ }
87
+
88
+ /** Which agents show evidence of being installed on this machine? */
89
+ function detectAgents(home) {
90
+ const probes = {
91
+ 'claude-code': ['.claude'],
92
+ codex: ['.codex'],
93
+ 'github-copilot': ['.copilot'],
94
+ opencode: ['.config/opencode'],
95
+ cursor: ['.cursor'],
96
+ 'gemini-cli': ['.gemini'],
97
+ }
98
+ return Object.entries(probes)
99
+ .filter(([, dirs]) => dirs.some((d) => fs.existsSync(path.join(home, d))))
100
+ .map(([id]) => id)
101
+ }
102
+
103
+ function resolveAgents(spec, detected) {
104
+ const all = SELECTABLE_AGENTS.map((a) => a.id)
105
+ if (!spec) return detected.length ? detected : all
106
+ if (spec === 'all') return all
107
+ const chosen = spec.split(',').map((s) => s.trim()).filter(Boolean)
108
+ const unknown = chosen.filter((c) => !all.includes(c))
109
+ if (unknown.length) throw new Error(`unknown agent(s): ${unknown.join(', ')}\nknown: ${all.join(', ')}`)
110
+ return chosen
111
+ }
112
+
113
+ async function ask(rl, question, fallback) {
114
+ const answer = (await rl.question(question)).trim()
115
+ return answer === '' ? fallback : answer
116
+ }
117
+
118
+ async function promptForScope(rl) {
119
+ console.log('Where should this be installed?\n')
120
+ console.log(' 1) Project — this repository only')
121
+ console.log(' 2) Global — every project on this machine\n')
122
+ const answer = await ask(rl, 'Choose [1]: ', '1')
123
+ return answer.startsWith('2') || answer.toLowerCase().startsWith('g') ? 'global' : 'project'
124
+ }
125
+
126
+ async function promptForAgents(rl, scope, detected) {
127
+ const all = SELECTABLE_AGENTS.map((a) => a.id)
128
+
129
+ if (scope === 'project') {
130
+ // One write to .agents/skills already serves every agent here, so the only
131
+ // real choice is whether to bridge Claude Code — which reads its own path.
132
+ // Say so explicitly: "all agents" is the default, not something to opt into.
133
+ console.log('\nInstalling for ALL agents. One .agents/skills/ directory covers Codex,')
134
+ console.log('GitHub Copilot, OpenCode, Cursor, Gemini CLI and others.\n')
135
+ console.log('Claude Code is the exception — it reads only .claude/skills/.\n')
136
+ const answer = await ask(rl, 'Also link Claude Code? [Y/n]: ', 'y')
137
+ return answer.toLowerCase().startsWith('n') ? all.filter((a) => a !== 'claude-code') : all
138
+ }
139
+
140
+ console.log('\nInstall for which agents?\n')
141
+ SELECTABLE_AGENTS.forEach((a, i) => {
142
+ const mark = detected.includes(a.id) ? '*' : ' '
143
+ console.log(` ${mark} ${i + 1}) ${a.label}`)
144
+ })
145
+ console.log('\n a) All of the above')
146
+ console.log('\n * = detected on this machine')
147
+ const preset = detected.length ? detected : all
148
+ const answer = await ask(rl, `\nNumbers, "a" for all, or Enter for detected [${preset.join(', ')}]: `, '')
149
+ const { agents, reason } = parseAgentSelection(answer, { preset })
150
+ if (reason === 'unrecognised') {
151
+ console.log(`Nothing recognised in "${answer}" — using the detected set.`)
152
+ }
153
+ return agents
154
+ }
155
+
156
+ async function runInstall(opts) {
157
+ const home = os.homedir()
158
+ const cwd = process.cwd()
159
+ const detected = detectAgents(home)
160
+ const interactive = isInteractive() && !opts.yes && !opts.dryRun
161
+
162
+ let scope = opts.scope
163
+ let agents = opts.agents ? resolveAgents(opts.agents, detected) : null
164
+
165
+ if (interactive) {
166
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
167
+ try {
168
+ if (!scope) scope = await promptForScope(rl)
169
+ if (!agents) agents = await promptForAgents(rl, scope, detected)
170
+ } finally {
171
+ rl.close()
172
+ }
173
+ }
174
+ scope ??= 'project'
175
+ agents ??= resolveAgents(opts.agents, detected)
176
+
177
+ const components = opts.components
178
+ ? opts.components.split(',').map((s) => s.trim()).filter(Boolean)
179
+ : scope === 'global' ? ['skills', 'instructions'] : ['skills']
180
+
181
+ const plan = buildPlan({ packageRoot: PACKAGE_ROOT, scope, components, agents, home, cwd, copy: opts.copy })
182
+
183
+ console.log('\n' + renderDisclosure(plan, { packageVersion: PKG.version }) + '\n')
184
+ if (opts.dryRun) {
185
+ console.log('--dry-run: nothing was written.')
186
+ return 0
187
+ }
188
+
189
+ if (interactive) {
190
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
191
+ let answer
192
+ try {
193
+ answer = await ask(rl, 'Proceed? [y/N]: ', 'n')
194
+ } finally {
195
+ rl.close()
196
+ }
197
+ if (!answer.toLowerCase().startsWith('y')) {
198
+ console.log('Aborted. Nothing was written.')
199
+ return 1
200
+ }
201
+ }
202
+
203
+ const receipt = {
204
+ package: PKG.name, version: PKG.version, installedAt: new Date().toISOString(),
205
+ scope, mode: plan.mode, skills: null, instructions: [],
206
+ }
207
+
208
+ if (plan.skills) {
209
+ const written = []
210
+ for (const skill of plan.skills.sources) {
211
+ const dest = path.join(plan.skills.canonical, skill.name)
212
+ installSkill(skill.dir, dest)
213
+ written.push(dest)
214
+ }
215
+ const links = []
216
+ let mode = plan.mode
217
+ for (const link of plan.skills.links) {
218
+ for (const name of plan.skills.names) {
219
+ const linkPath = path.join(link.dir, name)
220
+ mode = linkSkill(path.join(plan.skills.canonical, name), linkPath, { copy: opts.copy })
221
+ links.push(linkPath)
222
+ }
223
+ }
224
+ receipt.skills = { canonical: plan.skills.canonical, dirs: written, links }
225
+ receipt.mode = mode
226
+ console.log(`Installed ${plan.skills.names.length} skills to ${plan.skills.canonical}`)
227
+ if (links.length) console.log(`Linked ${links.length} into ${plan.skills.links.map((l) => l.dir).join(', ')} (${mode})`)
228
+ }
229
+
230
+ if (plan.instructions.length) {
231
+ const content = fs.readFileSync(path.join(PACKAGE_ROOT, 'AGENTS.md'), 'utf8')
232
+ for (const item of plan.instructions) {
233
+ try {
234
+ const applied = applyInstructionWrite(item, content, { replaceSymlinks: opts.replaceSymlinks })
235
+ receipt.instructions.push({ file: applied.file, applied: applied.applied })
236
+ console.log(`${applied.applied.padEnd(18)} ${applied.file}`)
237
+ } catch (err) {
238
+ console.error(`SKIPPED ${item.file}\n ${err.message}`)
239
+ }
240
+ }
241
+ }
242
+
243
+ writeReceipt(plan.receipt, receipt)
244
+ console.log(`\nReceipt: ${plan.receipt}`)
245
+ // Both forms are printed because either may be the one that works: the
246
+ // published name until the package is on npm, the git specifier after.
247
+ const flag = scope === 'global' ? '-g' : '-p'
248
+ console.log(`Undo: npx ${PKG.name} uninstall ${flag}`)
249
+ console.log(` npx github:aanyberg/agent-conventions uninstall ${flag}`)
250
+ return 0
251
+ }
252
+
253
+ async function runUninstall(opts) {
254
+ const home = os.homedir()
255
+ const cwd = process.cwd()
256
+ const scope = opts.scope ?? 'project'
257
+ const file = path.join(scope === 'global' ? home : cwd, '.agent-conventions.json')
258
+ const receipt = readReceipt(file)
259
+ if (!receipt) {
260
+ console.error(`No receipt at ${file} — nothing recorded as installed at ${scope} scope.`)
261
+ return 1
262
+ }
263
+
264
+ // Only ever remove what the receipt records. Never infer.
265
+ for (const link of receipt.skills?.links ?? []) fs.rmSync(link, { recursive: true, force: true })
266
+ for (const dir of receipt.skills?.dirs ?? []) fs.rmSync(dir, { recursive: true, force: true })
267
+ for (const item of receipt.instructions ?? []) {
268
+ try {
269
+ // Always strip the block and judge by what is left, even for a file this
270
+ // installer created. Keying off `applied === 'create'` and deleting
271
+ // outright would take anything the user added to that file afterwards —
272
+ // a small data-loss case, but the same class as the symlink one.
273
+ const text = fs.readFileSync(item.file, 'utf8')
274
+ const stripped = removeBlock(text)
275
+ if (stripped === '') {
276
+ fs.rmSync(item.file, { force: true })
277
+ console.log(`removed ${item.file}`)
278
+ } else {
279
+ fs.writeFileSync(item.file, stripped, 'utf8')
280
+ console.log(`stripped ${item.file} (kept your content)`)
281
+ }
282
+ } catch (err) {
283
+ console.error(`skipped ${item.file}: ${err.message}`)
284
+ }
285
+ }
286
+ fs.rmSync(file, { force: true })
287
+ console.log(`\nRemoved. Receipt deleted: ${file}`)
288
+ return 0
289
+ }
290
+
291
+ async function main() {
292
+ let opts
293
+ try {
294
+ opts = parseArgs(process.argv.slice(2))
295
+ } catch (err) {
296
+ console.error(err.message)
297
+ return 2
298
+ }
299
+ if (opts.help) {
300
+ console.log(USAGE)
301
+ return 0
302
+ }
303
+ try {
304
+ return opts.command === 'uninstall' ? await runUninstall(opts) : await runInstall(opts)
305
+ } catch (err) {
306
+ console.error(`error: ${err.message}`)
307
+ return 1
308
+ }
309
+ }
310
+
311
+ process.exitCode = await main()
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "agent-conventions",
3
+ "version": "1.0.0",
4
+ "description": "Skills and agents for backlog management, git conventions, code standards, and planning workflows."
5
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@anyberg/agent-conventions",
3
+ "version": "1.0.0",
4
+ "description": "Skills and agents for backlog management, git conventions, code standards, and planning workflows — installable into Claude Code, Codex, GitHub Copilot, OpenCode, Cursor and Gemini CLI.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Alexander Nyberg"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/aanyberg/agent-conventions.git"
13
+ },
14
+ "homepage": "https://github.com/aanyberg/agent-conventions#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/aanyberg/agent-conventions/issues"
17
+ },
18
+ "keywords": [
19
+ "agent-skills",
20
+ "claude-code",
21
+ "codex",
22
+ "copilot",
23
+ "opencode",
24
+ "cursor",
25
+ "conventions"
26
+ ],
27
+ "bin": {
28
+ "agent-conventions": "bin/cli.js"
29
+ },
30
+ "files": [
31
+ "bin",
32
+ "src",
33
+ "skills",
34
+ "agents",
35
+ "AGENTS.md",
36
+ "plugin.json",
37
+ "gemini-extension.json",
38
+ ".claude-plugin",
39
+ ".codex-plugin"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18.17"
43
+ },
44
+ "scripts": {
45
+ "test": "node --test tests-js/"
46
+ }
47
+ }
package/plugin.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "agent-conventions",
4
+ "version": "1.0.0",
5
+ "description": "Skills and agents for backlog management, git conventions, code standards, and planning workflows.",
6
+ "author": {
7
+ "name": "Alexander Nyberg"
8
+ },
9
+ "repository": "https://github.com/aanyberg/agent-conventions",
10
+ "license": "MIT",
11
+ "keywords": ["conventions", "code-standards", "git", "backlog", "planning"]
12
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: api-design
3
+ description: Rules for designing public APIs, managing visibility, backwards compatibility, and API patterns
4
+ ---
5
+
6
+ # API Design & Interfaces
7
+
8
+ **When to check**: designing or modifying public APIs, parameters, or class interfaces.
9
+
10
+ ## Rules
11
+
12
+ - Prefix implementation details with `_` and exclude from `__all__` — keeps the public surface minimal and frees internal code from backward-compatibility obligations.
13
+ - Export commonly-used types/classes from the top-level `vcc.<PROJECT>` package — simplifies user imports and lets internal structure be refactored without breaking consumers.
14
+ - Put a `_: KW_ONLY` marker before optional fields in dataclasses/Pydantic models — callers can't pass defaults positionally, so fields can be added or reordered without breakage.
15
+ - Prefer instance methods when accessing `self` or enabling polymorphism; use module-level functions when no instance state is needed — extract shared logic to private top-level helpers to avoid cross-class duplication.
16
+ - Keep old names as deprecated aliases when renaming public API elements — lets users migrate gradually instead of breaking on upgrade.
17
+ - Return new collections from transform functions instead of mutating inputs — avoids surprising side effects (exceptions: performance-critical paths or functions named `update_*`/`*_inplace`).
18
+ - Don't access or modify private attributes (`_prefixed`) — use public APIs, properties, or constructor parameters, so internal changes don't break callers.
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: architecture-planning
3
+ description: Use when making architectural decisions, designing systems or components, choosing technologies, defining boundaries, or evaluating structural trade-offs. Maintains a living architecture.md and records decisions as ADRs. Language- and stack-agnostic.
4
+ ---
5
+
6
+ # Architecture Planning
7
+
8
+ ## When This Applies
9
+
10
+ Engage this skill when a change touches **structure**, not just implementation:
11
+
12
+ - Introducing or removing a service, module, layer, or boundary
13
+ - Choosing a technology, framework, datastore, protocol, or external dependency
14
+ - Defining how components communicate (sync/async, API style, events, contracts)
15
+ - Data modelling, ownership, and consistency decisions
16
+ - Cross-cutting concerns: auth, observability, error handling, config, security, scaling
17
+ - Anything expensive to reverse later
18
+
19
+ If a task surfaces one of these, **stop and plan here before coding** — this is the "Surface blockers" path from **task-workflow** skill.
20
+
21
+ ## Legacy Layout Migration
22
+
23
+ Older projects may still have `<root>/.agents/architecture.md`. Before reading or editing the architecture doc, check once per project:
24
+
25
+ 1. If `<root>/.agents/architecture.md` exists and `<root>/.planning/architecture.md` does not, create `<root>/.planning/` (if missing) and move the file there.
26
+ 2. Do this silently and automatically — it is additive and safe. Do not ask for confirmation.
27
+
28
+ ## Two Artifacts
29
+
30
+ | Artifact | Location | Purpose |
31
+ |---|---|---|
32
+ | `architecture.md` | in `<root>/.planning/` | Living description of the system **as it is now** |
33
+ | ADR entries | `## Decisions` log in `architecture.md` | Immutable record of **why** a decision was made, with context and alternatives |
34
+
35
+ Rule of thumb: `architecture.md` answers *"how does this work today?"*; ADRs answer *"why is it this way?"*. Keep `architecture.md` current by editing it; never rewrite history in an ADR — supersede it instead.
36
+
37
+ ## architecture.md Structure
38
+
39
+ Keep it short and current. Delete stale sections rather than letting them rot.
40
+
41
+ ```markdown
42
+ # Architecture
43
+
44
+ **Last updated:** <date>
45
+
46
+ ## 1. Overview
47
+ One paragraph: what the system does and its core design philosophy.
48
+
49
+ ## 2. Context & Constraints
50
+ - Business/technical drivers shaping the design
51
+ - Hard constraints (compliance, latency, budget, team size, existing systems)
52
+ - Explicit non-goals — what this system intentionally does NOT do
53
+
54
+ ## 3. System Structure
55
+ - Components/services/modules and their responsibilities
56
+ - A diagram (Mermaid/ASCII) of how they connect
57
+ - Boundaries: what each owns, what it must not reach into
58
+
59
+ ## 4. Data
60
+ - Key entities and ownership (who is the source of truth)
61
+ - Storage choices and why
62
+ - Consistency / migration approach
63
+
64
+ ## 5. Cross-Cutting Concerns
65
+ Auth, observability, error handling, config, security, scaling — one line each, link out for detail.
66
+
67
+ ## 6. Key Decisions
68
+ Index of ADRs with status. Link each.
69
+
70
+ ## 7. Known Trade-offs & Risks
71
+ What was knowingly accepted, and what would force a rethink.
72
+ ```
73
+
74
+ ## ADR Structure
75
+
76
+ One decision per record. Numbered, dated, never deleted.
77
+
78
+ ```markdown
79
+ # ADR-<NNNN>: <short title>
80
+
81
+ **Status:** proposed | accepted | superseded by ADR-<N> | deprecated
82
+ **Date:** <date>
83
+
84
+ ## Context
85
+ The forces at play: problem, constraints, what made this a decision worth recording.
86
+
87
+ ## Decision
88
+ What we chose, stated plainly.
89
+
90
+ ## Alternatives Considered
91
+ Each real option, with why it was rejected. "No alternatives" usually means the analysis is missing.
92
+
93
+ ## Consequences
94
+ What becomes easier, what becomes harder, what we now owe (follow-ups, risks, migration cost).
95
+ ```
96
+
97
+ ## Decision Process
98
+
99
+ Work through these as a dialogue with the user — do not decide unilaterally on structural matters.
100
+
101
+ 1. **Frame the problem.** State what is actually being decided and why now. Separate the decision from the implementation.
102
+ 2. **Surface constraints & drivers.** Quality attributes first (performance, security, scalability, maintainability, cost, team capability). Name the ones that dominate — you cannot maximise all.
103
+ 3. **Generate real alternatives.** At least two genuine options, including "do nothing / defer." Bias toward the simplest thing that satisfies the constraints (KISS, YAGNI).
104
+ 4. **Evaluate against drivers, not preference.** Trade-offs explicitly: what each option costs. Prefer reversible decisions; spend the analysis budget on the irreversible ones.
105
+ 5. **Recommend, then confirm.** Give a clear recommendation with reasoning — not an unranked survey. Get user agreement before recording.
106
+ 6. **Record.** Write the ADR and update the affected `architecture.md` sections in the same change.
107
+
108
+ ## Agent Discipline
109
+
110
+ - A structural decision is a **Blocker** in `/task-workflow` terms — surface it, do not guess.
111
+ - No new service, dependency, or boundary without an ADR and a recommendation the user has confirmed.
112
+ - When a change makes `architecture.md` wrong, update it in the same branch — treat it like a failing test.
113
+ - Superseding a decision: set the old ADR's status to `superseded by ADR-N`, write the new one; never edit the original's reasoning.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: backlog-management
3
+ description: Single interface for every backlog operation. Use whenever work items are listed, created, claimed, released, linked to a PR, de-duplicated, or marked done, cancelled, or blocked, including when another skill or routine says "add to backlog", "update status", "check what is in flight", or "record a follow-up". Resolves the storage backend (GitHub Issues or BACKLOG.md) from `.planning/policy.yml`, never lets callers touch the backend directly.
4
+ ---
5
+
6
+ # Backlog Management
7
+
8
+ One interface, two backends. Callers use the operations in this file. Backend files hold the mechanics.
9
+
10
+ ## 1. Resolve the backend (once per session)
11
+
12
+ If `<root>/.planning/policy.yml` does not exist, run `scripts/generate-policy.sh` first — it creates the file from best-practice defaults (backend auto-detected the same way as step 2 below) and prints what it generated. Report that generation in your first response. It never overwrites an existing file, so this is safe to run unconditionally.
13
+
14
+ Then run `scripts/detect-backend.sh` from the project root, or apply the same order by hand:
15
+
16
+ 1. `.planning/policy.yml` → `backlog.backend`. Explicit value wins.
17
+ 2. Auto-detect `github-issues` only if all hold: `git remote get-url origin` matches `github.com`; `gh auth status` succeeds; `gh api repos/{owner}/{repo} --jq .has_issues` is `true`.
18
+ 3. Otherwise `markdown`.
19
+ 4. Neither usable → stop, report, do nothing.
20
+
21
+ State the resolved backend in your first report line. Load `backends/<backend>.md` for mechanics.
22
+
23
+ ## 2. Status model
24
+
25
+ ```
26
+ backlog → ready → active → in-review → done
27
+ ↘ blocked ↙ ↘ cancelled
28
+ ```
29
+
30
+ | Status | Meaning | Who sets it |
31
+ |---|---|---|
32
+ | `backlog` | Identified, criteria may be thin | sweep, humans, agents recording leftovers |
33
+ | `ready` | Criteria clear and testable, no open decisions | triage or human only |
34
+ | `active` | Claimed, task file and branch exist | claimant |
35
+ | `in-review` | PR open, verification pending | claimant |
36
+ | `blocked` | Needs a human decision or an external dependency; evidence recorded | anyone |
37
+ | `done` | Merged, criteria verified | releaser, after merge-readiness passes |
38
+ | `cancelled` | Will not be done, reason recorded | human or triage |
39
+
40
+ Rules: no `backlog → done`. Autonomous agents select only `ready` (see `policy.yml` `statuses.agent_selectable`). `blocked` is a side state, it keeps the previous state's evidence.
41
+
42
+ ## 3. Operations
43
+
44
+ | Op | Contract |
45
+ |---|---|
46
+ | `list(filter)` | Returns items with id, title, type, priority, status, depends_on, assignee, pr. Filters: status, type, label, text. |
47
+ | `dedupe(topic)` | Search titles and bodies across all states including done/cancelled. Returns matches. Callers must not create when a match exists. |
48
+ | `create(item)` | Fields: title, type, priority (default medium), goal, acceptance_criteria[], depends_on[], evidence, origin. Returns id. Never assigns an ID by guessing, see backend. |
49
+ | `needsDiscussion(item, question)` | Same as create but marked needs-discussion, status backlog, no `ready`. `question` is mandatory. |
50
+ | `claim(id, run_id)` | Atomic: verify status is `ready` and unassigned, set `active`, assign, record run_id and branch. Fails if already claimed. |
51
+ | `setStatus(id, status, note)` | Any non-terminal transition. `blocked` requires a note with evidence, completed work, blocker, next action. |
52
+ | `link(id, pr)` | Attach PR to item and item to PR. Sets `in-review`. |
53
+ | `release(id, done\|cancelled\|backlog, note)` | Terminal or unclaim. `done` only from `in-review` and only after task-workflow merge readiness passed. |
54
+ | `inFlight()` | All `active` and `in-review` items plus their branches and PRs. Used before selecting work. |
55
+ | `nextEligible(n, policy)` | `ready` items, none of whose `depends_on` are open, ordered by priority then age, excluding types outside `policy.autonomous.allowed_types`. |
56
+ | `render()` | Regenerate the human view (`policy.backlog.render_file`). No-op for markdown backend. |
57
+
58
+ ## 4. Item content standard (both backends)
59
+
60
+ Every item has: a one-paragraph Goal, testable Acceptance Criteria as checkboxes, Depends On, Evidence (file paths, line numbers, function names, PR links), Origin (source, created date, originating task or PR). Items without evidence are not `ready`.
61
+
62
+ ## 5. Discipline
63
+
64
+ - Every piece of identified work gets an item, including small chores and leftovers from merge readiness.
65
+ - IDs are permanent, never reused or renumbered.
66
+ - Claim before branch. A branch without a claimed item is a defect.
67
+ - Record leftovers via `create` with origin pointing at the finishing task or PR.
68
+ - Needs-discussion items expire per `policy.backlog.needs_discussion_expiry_days`, triage cancels them with note `stale`.
69
+ - Staleness review: when asked, or when open `backlog` exceeds `policy.backlog.max_open_backlog`, list oldest items for the human to promote, cancel, or reprioritise.
70
+
71
+ ## 6. Legacy migration
72
+
73
+ If `<root>/.agents/backlog.md` exists and `<root>/BACKLOG.md` does not, move it. If `policy.yml` says `github-issues` but `BACKLOG.md` still contains hand-written tables and `.planning/backlog-migration.json` is absent, the migration is incomplete: treat backend as `markdown` for this session and report it.
@@ -0,0 +1,37 @@
1
+ # Backend: github-issues
2
+
3
+ ID = issue number. Labels are the state machine. Closed state carries `done`/`cancelled`.
4
+
5
+ ## Label scheme
6
+
7
+ `type:*`, `priority:*`, `status:*` (backlog, ready, active, in-review, blocked), `ns:*` (optional namespace), `source:*`, `needs-discussion`, `agent-safe`. Exactly one `type:`, one `priority:`, one `status:` per open issue. `done` = closed completed. `cancelled` = closed not planned.
8
+
9
+ ## Body headings (parse and write exactly)
10
+
11
+ `## Goal`, `## Acceptance Criteria` (checkboxes), `## Depends On` (`#n` list or `none`), `## Evidence`, `## Origin` (`source:`, `created:`, `run:`, `task:` or `pr:`), optional `## Open Question`, optional `## Blocker` (evidence, completed work, blocker, next action).
12
+
13
+ ## Operations
14
+
15
+ | Op | Command |
16
+ |---|---|
17
+ | `list` | `gh issue list --state all --limit 500 --json number,title,labels,assignees,state,stateReason,body,createdAt` then filter locally. |
18
+ | `dedupe` | `gh issue list --state all --search "<topic>" --json number,title,state` plus a local title fuzzy match on `list`. |
19
+ | `create` | `gh issue create --title "<title>" --body-file <tmp> --label type:x --label priority:x --label status:backlog --label source:x`. Never pass an ID. |
20
+ | `needsDiscussion` | `create` plus `--label needs-discussion`, body includes `## Open Question`. |
21
+ | `claim` | Read issue. Require `status:ready`, no assignee. Then in one `gh issue edit`: `--add-label status:active --remove-label status:ready --add-assignee @me`. Re-read and verify assignee is you and label flipped, otherwise treat as lost race and abort. Comment `claimed by run <run_id>, branch <branch>`. |
22
+ | `setStatus` | `gh issue edit --add-label status:<new> --remove-label status:<old>`. For `blocked`, also comment with the `## Blocker` block and append it to the body. |
23
+ | `link` | PR body must contain `Closes #<id>`. `gh issue edit --add-label status:in-review --remove-label status:active`. Comment with PR URL. |
24
+ | `release done` | Verify PR merged (`gh pr view --json state`). `gh issue close --reason completed --comment "<note>"`, remove `status:*` labels. |
25
+ | `release cancelled` | `gh issue close --reason "not planned" --comment "<reason>"`. |
26
+ | `release backlog` | Remove assignee, set `status:ready` (if criteria intact) or `status:backlog`, comment why. |
27
+ | `inFlight` | `list` filtered to `status:active` or `status:in-review`, join with `gh pr list --state open --json number,headRefName,body` on `Closes #n`. |
28
+ | `nextEligible` | `list` filtered `status:ready`, `agent-safe`, type in policy, no assignee, all `## Depends On` closed. Sort priority high→low, then createdAt asc. |
29
+ | `render` | `scripts/backlog-render.sh > BACKLOG.md`. Commit only from a scheduled job or as part of a sweep PR, never from a task branch. |
30
+
31
+ ## Labels the migration and triage own
32
+
33
+ `agent-safe` is set only by triage or a human. It means: criteria testable, no product or structural decision, diff expected under policy limits. The implementer treats absence as "not selectable" even if `status:ready`.
34
+
35
+ ## Failure handling
36
+
37
+ `gh` auth or rate-limit error → stop the operation, report, do not fall back to editing `BACKLOG.md`.
@@ -0,0 +1,34 @@
1
+ # Backend: markdown
2
+
3
+ `<root>/BACKLOG.md` is the source of truth. Two tables: live and `## Archive`. Same columns.
4
+
5
+ ```markdown
6
+ | ID | Title | Type | Priority | Status | Assignee | Task File | PR | Depends On | Created | Acceptance Criteria Summary | Notes |
7
+ ```
8
+
9
+ `policy.ids.scheme` decides ID format: `numeric` (`001`) or `prefixed` (`LMS-001`, namespaces in `policy.ids.prefixes`). Default numeric.
10
+
11
+ ## Operations
12
+
13
+ | Op | Mechanics |
14
+ |---|---|
15
+ | `list` | Parse both tables. |
16
+ | `dedupe` | Grep both tables and any `## Needs Discussion` sections, case-insensitive, on title words and Notes. |
17
+ | `create` | `git fetch`, read `origin/main:BACKLOG.md` and every open PR's version (`gh pr list --json headRefName`, `git show origin/<branch>:BACKLOG.md`) if `gh` is available. ID = max across all of them plus one. Append row with `Status: backlog`, `Created: today`. Write the claim and the row in the same commit. |
18
+ | `needsDiscussion` | Append to `## Needs Discussion (<date>)` section at end of file, create the section if the date differs from the last one. Title, 2 to 4 sentence rationale, explicit open question. No ID, no status. |
19
+ | `claim` | Requires ability to commit a one-line change to `main` (bot bypass on `BACKLOG.md` only) or to `.planning/active.json`. Set `Status: active`, `Assignee: <run_id>`, commit `chore(backlog): claim <id>`, push. If push is rejected, pull, re-check the row is still `ready`, retry once, else abort. If no bypass exists, the claim is branch-local and weaker, say so in the run report. |
20
+ | `setStatus` | Edit the row. `blocked` writes the four-part blocker note into Notes. |
21
+ | `link` | Fill `PR` cell, set `in-review`. |
22
+ | `release done` | Row → `done`, `Task File: —`, move to Archive. Happens in the task branch as the last commit before merge, per task-workflow step 9. |
23
+ | `release cancelled` | Row → `cancelled`, reason in Notes, move to Archive. |
24
+ | `inFlight` | Rows with `active` or `in-review`, plus open PRs whose diff touches `BACKLOG.md`. |
25
+ | `nextEligible` | Rows `ready`, no Assignee, `Depends On` all in Archive, ordered priority then Created. |
26
+ | `render` | No-op. |
27
+
28
+ ## Known limits
29
+
30
+ ID collisions and claims are best-effort. If two branches allocate the same ID, the later merge renumbers and fixes cross-references. Prefer the github-issues backend for repos with more than one concurrent agent.
31
+
32
+ ## Compatibility with the sweep's old sectioned layout
33
+
34
+ A file organised as `## <Topic>` sections with `done` rows in place is a legacy layout. Read it as one live table across sections. Do not restructure it during normal operations; restructuring is its own backlog item.