wendkeep 0.6.1 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0] — 2026-07-05
8
+
9
+ Ergonomics: the loop without hand-editing files.
10
+
11
+ ### Added
12
+ - **`change status`** — one screen: tasks (done/open with `[req:]`/`[sensor:]`), sensor
13
+ evidence, verdict state (ok / stale / incomplete / absent), mutation round.
14
+ - **`change done <id>` / `undone <id>`** — tick tasks from the CLI (exact-id anchored).
15
+ - **`change diff`** — dry-run preview of the spec promotion (`+` ADDED / `~` MODIFIED /
16
+ `-` REMOVED / `!` warnings) without touching `07-Specs`.
17
+ - **`spec list` / `spec show <capability>`** — read-only views over the living specs.
18
+ - **`sensors list`** — the sensors from `wendkeep.sensors.json`; a **JSON Schema** for the
19
+ file now ships in the package (`schema/`) and the init seed points `$schema` at it.
20
+ - README: "the loop in five minutes" worked example.
21
+
22
+ ### Fixed
23
+ - `change` subcommands without a positional argument no longer mistake the `--vault` value
24
+ for a slug.
25
+
7
26
  ## [0.6.1] — 2026-07-05
8
27
 
9
28
  Hardening: CI + real-world gate holes found by self-audit.
@@ -141,6 +160,7 @@ Initial release — the capture engine, extracted from a system in daily product
141
160
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
142
161
 
143
162
  <!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
163
+ [0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
144
164
  [0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
145
165
  [0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
146
166
  [0.5.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.5.0
package/README.md CHANGED
@@ -70,8 +70,11 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
70
70
  |---|---|
71
71
  | `wendkeep init` | Set up wendkeep in a project (vault taxonomy + settings + MCP + skills). |
72
72
  | `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
73
- | `wendkeep change <sub>` | Change lifecycle: `new <slug>` / `list` / `show <slug>` / `archive <slug>`. |
74
- | `wendkeep verify [--change s]` | Run the active (or named) change's task sensors; record evidence (the gate). |
73
+ | `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` / `show` / `status` / `done <id>` / `undone <id>` / `diff` / `archive [--force]`. |
74
+ | `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
75
+ | `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
76
+ | `wendkeep sensors list` | Show the sensors from `wendkeep.sensors.json` (JSON Schema included in the package). |
77
+ | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
75
78
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
76
79
  | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
77
80
  | `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
@@ -90,7 +93,37 @@ explore → propose → apply (TDD) → verify → archive
90
93
  - **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
91
94
  - **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
92
95
 
93
- `wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
96
+ `wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
97
+
98
+ ### The loop in five minutes
99
+
100
+ ```bash
101
+ npx wendkeep init --yes # vault + hooks + sensors + skills
102
+ npx wendkeep change new dark-mode # proposta/design/tarefas — change is now active
103
+ ```
104
+
105
+ Edit `tarefas.md` — tag proof and requirement per task:
106
+
107
+ ```markdown
108
+ - [ ] 1.1 toggle persists across sessions [req:UI-1] [sensor:tests]
109
+ ```
110
+
111
+ Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
112
+ `specs/ui/spec.md` (`## ADDED Requirements` → `### Requisito: UI-1 — dark mode toggle`). Then:
113
+
114
+ ```bash
115
+ npx wendkeep change status # one screen: tasks / sensors / verdict
116
+ npx wendkeep change done 1.1 # tick a task from the CLI
117
+ npx wendkeep verify # run the declared sensors -> evidencia.json
118
+ npx wendkeep verify --deep # assemble the verification package
119
+ # the wk-verify skill (fresh, read-only pass) writes verdict.json
120
+ npx wendkeep change diff # preview what will land in 07-Specs
121
+ npx wendkeep change archive dark-mode # gate: sensors + verdict + no open tasks
122
+ ```
123
+
124
+ The archive promotes the delta into the living `07-Specs/ui.md`, mints an ADR, and the
125
+ Obsidian graph now links *session ↔ change ↔ requirement ↔ decision*. A change that names
126
+ no `[req:]` skips the independent verdict — the sensor gate is its proof.
94
127
 
95
128
  ## How it works
96
129
 
package/bin/wendkeep.mjs CHANGED
@@ -43,7 +43,10 @@ Usage:
43
43
  agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
44
44
 
45
45
  wendkeep doctor [--vault P] Run a vault health check.
46
- wendkeep change <sub> Change lifecycle: new <slug> | list | show <slug> | archive <slug>.
46
+ wendkeep change <sub> Change lifecycle: new [--simple] | list | show | status |
47
+ done <id> | undone <id> | diff | archive [--force].
48
+ wendkeep spec <sub> Living specs: list | show <capability>.
49
+ wendkeep sensors list Show the sensors from wendkeep.sensors.json.
47
50
  wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
48
51
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
49
52
  wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
@@ -117,6 +120,16 @@ async function main() {
117
120
  runLesson(rest);
118
121
  break;
119
122
  }
123
+ case 'spec': {
124
+ const { runSpec } = await import('../src/spec.mjs');
125
+ runSpec(rest);
126
+ break;
127
+ }
128
+ case 'sensors': {
129
+ const { runSensors } = await import('../src/sensors.mjs');
130
+ runSensors(rest);
131
+ break;
132
+ }
120
133
  case '--version':
121
134
  case '-v':
122
135
  process.stdout.write(`${version()}\n`);
@@ -123,6 +123,17 @@ export function parseTasks(md) {
123
123
  return tasks;
124
124
  }
125
125
 
126
+ // Toggle a task checkbox by its exact id (0.7.0 ergonomics). Returns false when absent.
127
+ export function setTaskDone(changeDir, taskId, done = true) {
128
+ const path = join(changeDir, 'tarefas.md');
129
+ const md = readFileSync(path, 'utf8');
130
+ const esc = String(taskId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
131
+ const re = new RegExp(`^(-\\s+\\[)( |x)(\\]\\s+${esc}\\s)`, 'm');
132
+ if (!re.test(md)) return false;
133
+ writeFileSync(path, md.replace(re, `$1${done ? 'x' : ' '}$3`), 'utf8');
134
+ return true;
135
+ }
136
+
126
137
  export function listChanges(vaultBase) {
127
138
  const base = join(vaultBase, CHANGES_DIR);
128
139
  const active = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "bin",
12
12
  "src",
13
13
  "hooks",
14
+ "schema",
14
15
  "README.md",
15
16
  "CHANGELOG.md"
16
17
  ],
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json",
4
+ "title": "wendkeep.sensors.json",
5
+ "description": "Native sensor config for the wendkeep harness (harness contract v1.1).",
6
+ "type": "object",
7
+ "required": ["version", "sensors"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "source": { "type": "string" },
12
+ "sensors": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "required": ["id", "command"],
17
+ "properties": {
18
+ "id": { "type": "string", "minLength": 1 },
19
+ "name": { "type": "string" },
20
+ "description": { "type": "string" },
21
+ "severity": { "enum": ["critical", "warning"], "default": "critical" },
22
+ "type": { "enum": ["command", "mutation"], "default": "command" },
23
+ "command": { "type": "string", "minLength": 1 },
24
+ "report": {
25
+ "type": "string",
26
+ "description": "type: mutation only — path (project-relative) to the mutation-testing-elements report."
27
+ }
28
+ },
29
+ "additionalProperties": false
30
+ }
31
+ }
32
+ },
33
+ "additionalProperties": false
34
+ }
package/src/change.mjs CHANGED
@@ -6,10 +6,11 @@ import {
6
6
  activeChange,
7
7
  listChanges,
8
8
  parseTasks,
9
+ setTaskDone,
9
10
  archiveChange,
10
11
  } from '../hooks/change-core.mjs';
11
12
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
12
- import { evaluateVerdict, tasksHashOf } from '../hooks/spec-core.mjs';
13
+ import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
13
14
  import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
14
15
 
15
16
  function resolveVault(argv) {
@@ -27,6 +28,13 @@ function resolveVault(argv) {
27
28
  return isAbsolute(base) ? base : resolve(process.cwd(), base);
28
29
  }
29
30
 
31
+ function opt(argv, name) {
32
+ const i = argv.indexOf(name);
33
+ if (i >= 0) return argv[i + 1];
34
+ const eq = argv.find((a) => a.startsWith(`${name}=`));
35
+ return eq ? eq.slice(name.length + 1) : undefined;
36
+ }
37
+
30
38
  function today() {
31
39
  const d = new Date();
32
40
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
@@ -35,7 +43,8 @@ function today() {
35
43
  export function runChange(argv) {
36
44
  const [sub, ...rest] = argv;
37
45
  const vaultBase = resolveVault(rest);
38
- const slugArg = () => rest.find((a) => !a.startsWith('-'));
46
+ const VALUE_FLAGS = new Set(['--vault', '--change', '--project']);
47
+ const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
39
48
 
40
49
  if (sub === 'new') {
41
50
  const slug = slugArg();
@@ -69,6 +78,76 @@ export function runChange(argv) {
69
78
  process.exit(0);
70
79
  }
71
80
 
81
+ if (sub === 'status') {
82
+ const slug = slugArg() || activeChange(vaultBase);
83
+ if (!slug) { process.stderr.write('wendkeep change status: no change (arg or active)\n'); process.exit(2); }
84
+ const dir = join(vaultBase, '08-Mudanças', slug);
85
+ let tarefasMd;
86
+ try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); }
87
+ catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
88
+ const tasks = parseTasks(tarefasMd);
89
+ const done = tasks.filter((t) => t.done).length;
90
+ process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase) ? ' (ativa)' : ''}\n`);
91
+ let specs = [];
92
+ try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); } catch { /* sem proposta */ }
93
+ process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
94
+ process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
95
+ for (const t of tasks) {
96
+ process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.req ? ` [req:${t.req}]` : ''}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
97
+ }
98
+ let evidence = null;
99
+ try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
100
+ if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
101
+ else process.stdout.write('evidencia: ausente\n');
102
+ const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
103
+ let verdict = null;
104
+ try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
105
+ if (!reqIds.length) process.stdout.write('verdict: não exigido (sem [req:])\n');
106
+ else if (!verdict) process.stdout.write('verdict: ausente — rode `wendkeep verify --deep` + wk-verify\n');
107
+ else {
108
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
109
+ process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
110
+ }
111
+ try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
112
+ process.exit(0);
113
+ }
114
+
115
+ if (sub === 'done' || sub === 'undone') {
116
+ const taskId = slugArg();
117
+ if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
118
+ const slug = opt(rest, '--change') || activeChange(vaultBase);
119
+ if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
120
+ const dir = join(vaultBase, '08-Mudanças', slug);
121
+ let ok = false;
122
+ try { ok = setTaskDone(dir, taskId, sub === 'done'); } catch { /* sem tarefas.md */ }
123
+ if (!ok) { process.stderr.write(`wendkeep change ${sub}: task não encontrada: ${taskId}\n`); process.exit(2); }
124
+ process.stdout.write(`task ${taskId}: ${sub === 'done' ? '[x]' : '[ ]'}\n`);
125
+ process.exit(0);
126
+ }
127
+
128
+ if (sub === 'diff') {
129
+ const slug = slugArg() || activeChange(vaultBase);
130
+ if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
131
+ const dir = join(vaultBase, '08-Mudanças', slug);
132
+ let specs = [];
133
+ try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); }
134
+ catch { process.stderr.write(`wendkeep change diff: not found: ${slug}\n`); process.exit(2); }
135
+ if (!specs.length) { process.stdout.write('diff: sem specs declaradas na proposta\n'); process.exit(0); }
136
+ for (const cap of specs) {
137
+ let delta;
138
+ try { delta = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8')); }
139
+ catch { process.stdout.write(`! sem delta para ${cap}\n`); continue; }
140
+ process.stdout.write(`spec: ${cap}\n`);
141
+ for (const r of delta.added) process.stdout.write(` + ${r.id || r.name} (ADDED)\n`);
142
+ for (const r of delta.modified) process.stdout.write(` ~ ${r.id || r.name} (MODIFIED)\n`);
143
+ for (const k of delta.removed) process.stdout.write(` - ${k} (REMOVED)\n`);
144
+ let living = [];
145
+ try { living = parseRequirements(readFileSync(join(vaultBase, '07-Specs', `${cap}.md`), 'utf8')); } catch { /* nova capability */ }
146
+ for (const w of applyDelta(living, delta).warnings) process.stdout.write(` ! ${w}\n`);
147
+ }
148
+ process.exit(0);
149
+ }
150
+
72
151
  if (sub === 'archive') {
73
152
  const slug = slugArg() || activeChange(vaultBase);
74
153
  if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
@@ -109,6 +188,6 @@ export function runChange(argv) {
109
188
  process.exit(0);
110
189
  }
111
190
 
112
- process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, archive.\n`);
191
+ process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, status, done, undone, diff, archive.\n`);
113
192
  process.exit(2);
114
193
  }
@@ -37,7 +37,7 @@ export function renderSensorsJson(scripts = {}) {
37
37
  });
38
38
  }
39
39
  }
40
- return `${JSON.stringify({ version: 1, source: 'manual', sensors }, null, 2)}\n`;
40
+ return `${JSON.stringify({ $schema: 'https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json', version: 1, source: 'manual', sensors }, null, 2)}\n`;
41
41
  }
42
42
 
43
43
  function readProjectScripts(projectPath) {
@@ -0,0 +1,23 @@
1
+ // `wendkeep sensors list` — read-only view over wendkeep.sensors.json (0.7.0).
2
+ import { resolve } from 'node:path';
3
+ import { loadSensors } from '../hooks/sensors-core.mjs';
4
+
5
+ export function runSensors(argv) {
6
+ const [sub, ...rest] = argv;
7
+ if (sub !== 'list') {
8
+ process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list.\n`);
9
+ process.exit(2);
10
+ }
11
+ let project;
12
+ for (let i = 0; i < rest.length; i += 1) {
13
+ if (rest[i] === '--project') project = rest[++i];
14
+ else if (rest[i].startsWith('--project=')) project = rest[i].slice(10);
15
+ }
16
+ const projectRoot = resolve(project || process.cwd());
17
+ const sensors = loadSensors(projectRoot);
18
+ if (!sensors.length) { process.stdout.write('sensors: (nenhum — crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
19
+ for (const s of sensors) {
20
+ process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
21
+ }
22
+ process.exit(0);
23
+ }
package/src/spec.mjs ADDED
@@ -0,0 +1,53 @@
1
+ // `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { parseRequirements } from '../hooks/spec-core.mjs';
5
+
6
+ function resolveVault(argv) {
7
+ let vault;
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const a = argv[i];
10
+ if (a === '--vault') vault = argv[++i];
11
+ else if (a.startsWith('--vault=')) vault = a.slice(8);
12
+ }
13
+ const base = vault || process.env.OBSIDIAN_VAULT_PATH;
14
+ if (!base) {
15
+ process.stderr.write('wendkeep spec: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
16
+ process.exit(2);
17
+ }
18
+ return isAbsolute(base) ? base : resolve(process.cwd(), base);
19
+ }
20
+
21
+ export function runSpec(argv) {
22
+ const [sub, ...rest] = argv;
23
+ const vaultBase = resolveVault(rest);
24
+ const specsDir = join(vaultBase, '07-Specs');
25
+
26
+ if (sub === 'list') {
27
+ let files = [];
28
+ try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
29
+ if (!files.length) { process.stdout.write('specs: (nenhuma)\n'); process.exit(0); }
30
+ for (const f of files) {
31
+ const md = readFileSync(join(specsDir, f), 'utf8');
32
+ const n = parseRequirements(md).length;
33
+ const upd = md.match(/> Atualizado por .* em (\d{4}-\d{2}-\d{2})/);
34
+ process.stdout.write(`${f.replace(/\.md$/, '')}: ${n} requisito(s)${upd ? ` (atualizado ${upd[1]})` : ''}\n`);
35
+ }
36
+ process.exit(0);
37
+ }
38
+
39
+ if (sub === 'show') {
40
+ const cap = rest.find((a, i) => !a.startsWith('-') && rest[i - 1] !== '--vault');
41
+ if (!cap) { process.stderr.write('wendkeep spec show: missing <capability>\n'); process.exit(2); }
42
+ let md;
43
+ try { md = readFileSync(join(specsDir, `${cap}.md`), 'utf8'); }
44
+ catch { process.stderr.write(`wendkeep spec show: not found: ${cap}\n`); process.exit(2); }
45
+ const reqs = parseRequirements(md);
46
+ process.stdout.write(`${cap}: ${reqs.length} requisito(s)\n`);
47
+ for (const r of reqs) process.stdout.write(` ${r.id ? `${r.id} — ` : ''}${r.name}\n`);
48
+ process.exit(0);
49
+ }
50
+
51
+ process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
52
+ process.exit(2);
53
+ }