choda-deck 0.1.1 → 0.2.3

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/README.md CHANGED
@@ -46,7 +46,11 @@ npx choda-deck
46
46
 
47
47
  Requires Node.js >= 20.
48
48
 
49
- ## Wire it into Claude Code
49
+ ## Wire it into your MCP client
50
+
51
+ choda-deck speaks stock MCP stdio — works with any client that supports the protocol. Pick the one you use:
52
+
53
+ ### Claude Code
50
54
 
51
55
  Add to `.claude.json` (user-level) or `.mcp.json` (project-level):
52
56
 
@@ -67,6 +71,63 @@ Add to `.claude.json` (user-level) or `.mcp.json` (project-level):
67
71
 
68
72
  Restart Claude Code → the `choda-tasks` MCP server is online.
69
73
 
74
+ ### Claude Desktop
75
+
76
+ Edit `claude_desktop_config.json` (same `mcpServers` schema as above):
77
+
78
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
79
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
80
+ - **Linux:** `~/.config/Claude/claude_desktop_config.json`
81
+
82
+ Quit + reopen Claude Desktop. The hammer icon shows `choda-tasks` connected.
83
+
84
+ ### GitHub Copilot (VS Code)
85
+
86
+ Create `.vscode/mcp.json` in your workspace (or add to User Settings):
87
+
88
+ ```json
89
+ {
90
+ "servers": {
91
+ "choda-tasks": {
92
+ "type": "stdio",
93
+ "command": "npx",
94
+ "args": ["-y", "choda-deck"],
95
+ "env": {
96
+ "CHODA_DATA_DIR": "/absolute/path/to/data",
97
+ "CHODA_CONTENT_ROOT": "/absolute/path/to/your/notes-or-vault"
98
+ }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Note: Copilot uses `servers` (not `mcpServers`) and requires `"type": "stdio"`. Reload VS Code window → tools appear in Copilot Chat under agent mode.
105
+
106
+ ### Other clients (Cursor, Continue, Zed, …)
107
+
108
+ Any MCP-compatible client works. Use the `command` / `args` / `env` triple — drop it into whatever the client calls its MCP config block.
109
+
110
+ ## CLI
111
+
112
+ `choda-deck` ships a read-only CLI that talks to the same SQLite store directly — no AI in the loop, no MCP roundtrip. Use it to verify state, script automations, or pipe to `jq`.
113
+
114
+ ```bash
115
+ choda-deck --help # show all subcommands
116
+ choda-deck task list --status TODO --json # script-friendly
117
+ choda-deck task show TASK-669 # body + linked conversations
118
+ choda-deck inbox list --project choda-deck
119
+ choda-deck knowledge list
120
+ choda-deck knowledge show ADR-020-embedding-architecture
121
+ choda-deck project context choda-deck # AI's session_start view
122
+ choda-deck mcp serve # start the MCP stdio server
123
+ ```
124
+
125
+ Pass `--json` to any read command for machine-readable output. Plain text is the default for humans.
126
+
127
+ ### Reading freshness
128
+
129
+ The CLI opens SQLite in WAL mode for shared reads. While the MCP server is actively writing, a CLI read may see a snapshot from a few seconds ago — re-run after 1-2s if state looks stale. See knowledge entry `sqlite-wal-read-consistency` for details.
130
+
70
131
  ## Tools
71
132
 
72
133
  All tools are namespaced `mcp__choda-tasks__<name>`. Claude calls them on your behalf — you never invoke them directly.
@@ -74,13 +135,14 @@ All tools are namespaced `mcp__choda-tasks__<name>`. Claude calls them on your b
74
135
  | Domain | Tools | What it does |
75
136
  |---|---|---|
76
137
  | **Project** | `project_add`, `project_list`, `project_context` | Multi-project setup. Each project has its own task list and metadata. |
77
- | **Workspace** | `workspace_add`, `workspace_list`, `workspace_archive`, `workspace_remove` | Sub-scope inside a project (e.g. `frontend`, `backend`, `infra`). Knowledge entries can be scoped to a workspace. |
78
- | **Task** | `task_create`, `task_list`, `task_update`, `task_context`, `tasks_update_batch` | TODO → READY → IN-PROGRESS → DONE/BLOCKED. Each task has body + acceptance criteria + labels + priority. |
138
+ | **Workspace** | `workspace_add`, `workspace_list`, `workspace_archive` | Sub-scope inside a project (e.g. `frontend`, `backend`, `infra`). Knowledge entries can be scoped to a workspace. |
139
+ | **Task** | `task_create`, `task_list`, `task_update`, `task_context` | TODO → READY → IN-PROGRESS → DONE/BLOCKED. Each task has body + acceptance criteria + labels + priority. |
79
140
  | **Session** | `session_start`, `session_checkpoint`, `session_end`, `session_resume`, `session_list` | Bind a work session to a task. Checkpoint progress so the next session resumes with full context. |
80
141
  | **Conversation** | `conversation_open`, `conversation_add`, `conversation_decide`, `conversation_close`, `conversation_reopen`, `conversation_list`, `conversation_read`, `conversation_poll` | Structured threads (e.g. FE/BE alignment, ADR debate). `decide` logs the resolution. |
81
- | **Inbox** | `inbox_add`, `inbox_research`, `inbox_convert`, `inbox_ready`, `inbox_archive`, `inbox_delete`, `inbox_list`, `inbox_get`, `inbox_update` | Capture-now, decide-later. Items move `raw` → `researching` → `ready` → `converted` (to a task) or `archived`. |
82
- | **Knowledge** | `knowledge_create`, `knowledge_list`, `knowledge_get`, `knowledge_search`, `knowledge_update`, `knowledge_verify`, `knowledge_register_existing`, `knowledge_delete` | ADRs / decision logs with frontmatter. `refs[]` tracks implementation files + commit SHAs → staleness banner when code drifts. |
142
+ | **Inbox** | `inbox_add`, `inbox_research`, `inbox_convert`, `inbox_ready`, `inbox_archive`, `inbox_list`, `inbox_get`, `inbox_update` | Capture-now, decide-later. Items move `raw` → `researching` → `ready` → `converted` (to a task) or `archived`. |
143
+ | **Knowledge** | `knowledge_create`, `knowledge_list`, `knowledge_get`, `knowledge_search`, `knowledge_update`, `knowledge_verify`, `knowledge_delete` | ADRs / decision logs with frontmatter. `refs[]` tracks implementation files + commit SHAs → staleness banner when code drifts. |
83
144
  | **Backup** | `backup_create`, `backup_list`, `backup_restore` | Daily auto-backup of the SQLite DB. Manual create + restore when you need to roll back. |
145
+ | **Ops** | `stats_report`, `cleanup_worktree_orphans` | Tool-usage telemetry (per-tool calls / error rate / dead-in-window) + worktree GC. |
84
146
 
85
147
  ## Common workflows
86
148
 
@@ -140,8 +202,12 @@ Claude : (knowledge_verify) → flags ADR-020 as potentially stale (refs SHA mis
140
202
  |---|---|---|
141
203
  | `CHODA_DATA_DIR` | _required_ | SQLite DB, artifacts, and backups directory. Created on first run. |
142
204
  | `CHODA_CONTENT_ROOT` | _optional_ | Root for knowledge / vault content lookup. |
205
+ | `CHODA_BACKEND` | `sqlite` | Storage backend (ADR-030). `sqlite` (local file) or `postgres` (remote, k8s-friendly). |
206
+ | `CHODA_PG_URL` | _required when `CHODA_BACKEND=postgres`_ | Postgres connection string (e.g. `postgres://user:pass@host:5432/db`). |
207
+ | `CHODA_PG_POOL_SIZE` | `10` | Postgres connection pool max size. Tune for concurrent HTTP requests. |
208
+ | `CHODA_EMBEDDING_PROVIDER` | `local` | `local` (transformers.js MiniLM-L6) or `noop` (disable embedding-backed search). |
143
209
 
144
- ### Data layout
210
+ ### Data layout (SQLite)
145
211
 
146
212
  ```
147
213
  $CHODA_DATA_DIR/
@@ -150,6 +216,66 @@ $CHODA_DATA_DIR/
150
216
  └── backups/choda-deck-<date>.db ← auto daily, retained
151
217
  ```
152
218
 
219
+ ### Postgres backend
220
+
221
+ The Postgres adapter is full-feature parity with SQLite — all `mcp__choda-tasks__*` tools work against either backend. Use Postgres when running the MCP HTTP transport in k8s (ADR-026 + ADR-030).
222
+
223
+ **Local dev** with the shipped `docker-compose.yml`:
224
+
225
+ ```bash
226
+ docker compose up -d # boots pgvector/pgvector:pg16 on :5432
227
+ export CHODA_BACKEND=postgres
228
+ export CHODA_PG_URL="postgres://choda:choda@localhost:5432/choda"
229
+ pnpm run mcp:http # schema migrates on first connect
230
+ ```
231
+
232
+ **k8s** — minimal `Deployment` + `Secret` shape:
233
+
234
+ ```yaml
235
+ apiVersion: v1
236
+ kind: Secret
237
+ metadata: { name: choda-pg }
238
+ type: Opaque
239
+ stringData:
240
+ CHODA_PG_URL: postgres://choda:CHANGEME@choda-pg.default.svc.cluster.local:5432/choda
241
+ MCP_HTTP_TOKEN: REPLACE_WITH_BASE64URL_32_BYTES
242
+ ---
243
+ apiVersion: apps/v1
244
+ kind: Deployment
245
+ metadata: { name: choda-deck }
246
+ spec:
247
+ replicas: 1
248
+ template:
249
+ spec:
250
+ containers:
251
+ - name: choda
252
+ image: ghcr.io/your-org/choda-deck:latest
253
+ env:
254
+ - { name: CHODA_BACKEND, value: postgres }
255
+ - { name: MCP_TRANSPORT, value: http }
256
+ - { name: MCP_HTTP_BIND, value: 0.0.0.0 }
257
+ envFrom:
258
+ - secretRef: { name: choda-pg }
259
+ ports:
260
+ - containerPort: 7337
261
+ readinessProbe:
262
+ httpGet: { path: /healthz, port: 7337 }
263
+ ```
264
+
265
+ Bring your own Postgres (Cloud SQL, RDS, managed) or run a sidecar `StatefulSet` with the `pgvector/pgvector:pg16` image. Migrations and the pgvector extension setup are idempotent — they run automatically inside `initializeAsync()` on every boot.
266
+
267
+ **Migration from existing SQLite data** — one-shot script:
268
+
269
+ ```bash
270
+ CHODA_PG_URL="postgres://choda:choda@localhost:5432/choda" \
271
+ node scripts/migrate-sqlite-to-postgres.mjs \
272
+ --sqlite $CHODA_DATA_DIR/database/choda-deck.db [--dry-run]
273
+ ```
274
+
275
+ The script is idempotent (skips tables that already have rows; pass `--force` to wipe + reload). Embedding vectors are NOT copied — re-run `scripts/backfill-embeddings.mjs` against the Postgres backend after migration to rebuild them.
276
+
277
+ The cross-device pending-ops sync engine (ADR-030 §2) is **not** in this release — laptop ↔ remote MCP today is "one backend at a time," not a live sync. Pick `CHODA_BACKEND` per process.
278
+
153
279
  ## Architecture
154
280
 
155
281
  - **SQLite** (`better-sqlite3`) — single source of truth, file-based, no daemon
@@ -161,7 +287,7 @@ See [`docs/architecture.md`](https://github.com/butterngo/choda-deck/blob/main/d
161
287
 
162
288
  ## Status
163
289
 
164
- `0.1.0` — early, dogfooded daily by the author. API may move before `1.0`. Issues + PRs welcome.
290
+ `0.2.0` — early, dogfooded daily by the author. API may move before `1.0`. Issues + PRs welcome.
165
291
 
166
292
  ## License
167
293
 
@@ -0,0 +1,170 @@
1
+ # MCP Rules
2
+
3
+ Behavioral contract for MCP tools (session + conversation). Edit this file to update compliance rules — no MCP restart needed. Each `## On <tool_name>` section is loaded by the matching tool handler and injected into its response.
4
+
5
+ ## On session_start
6
+
7
+ `session_start` now requires a `taskId` — the task is bound to the session at creation and auto-set to IN-PROGRESS.
8
+
9
+ Before calling `session_start`:
10
+
11
+ 1. Call `task_list` (or `roadmap`) to show the user available tasks, grouped by priority (high → medium → low).
12
+ 2. Wait for the user to pick a task. Do not guess.
13
+ 3. Call `session_start({ projectId, taskId, workspaceId?, cwd? })`. Always pass `cwd` (current shell directory) so the MCP can auto-detect `workspaceId` for projects with registered workspaces — the MCP server's own cwd is fixed and cannot be inferred.
14
+ 4. Echo the `lastSession` block to the user verbatim — resume point, decisions, loose ends, tasks updated, commits. Do not summarize.
15
+ 5. Create a feature branch for the task:
16
+ - Branch name: `feat/<task-id>-<short-slug>` (e.g. `feat/task-564-session-conv-ui`)
17
+ - Required: `git checkout -b feat/<task-id>-<short-slug>`
18
+ - Optional (if user wants parallel worktree): detect repo root via `git rev-parse --show-toplevel`, then `git worktree add <repo-root>.worktrees/<slug> -b feat/<task-id>-<short-slug>`
19
+ - Ask the user whether they want a worktree or just a branch before proceeding.
20
+
21
+ Workspace resolution order (when project has ≥1 workspace registered):
22
+ - explicit `workspaceId` wins
23
+ - else `cwd` is matched against registered workspace cwds (longest prefix wins for nested repos)
24
+ - else MCP throws — pick a workspace explicitly or call `workspace_add`
25
+
26
+ If the project has no workspaces registered, `workspaceId` may be `null` (backward-compatible).
27
+
28
+ Blocking conditions (MCP returns an error):
29
+ - Task not found
30
+ - Task already `DONE` — reopen it with `task_update` first
31
+ - Task already bound to another active session — end that session first
32
+ - Project has workspaces but neither `workspaceId` nor a matching `cwd` was provided
33
+
34
+ ## On session_checkpoint
35
+
36
+ `session_checkpoint` snapshots progress on an active session **without ending it**. Overwrite-in-place — each call replaces the previous checkpoint.
37
+
38
+ When to checkpoint:
39
+
40
+ - Before risky operations (rebase, force-push, schema migration, large refactor)
41
+ - Before context window compaction (when conversation grows long)
42
+ - Every ~30 minutes of active work, or after a meaningful sub-step
43
+ - When pausing work mid-task (lunch, meeting) — so a future `session_resume` recovers state cleanly
44
+
45
+ Required field:
46
+
47
+ - **resumePoint** — one sentence describing exactly where you stopped and what to pick up next
48
+
49
+ Recommended fields (include whichever apply):
50
+
51
+ - **lastConversationId** — most recent conversation touched (resume context)
52
+ - **dirtyFiles** — files edited but not yet committed (so resume knows what's in flight)
53
+ - **lastCommit** — last commit SHA written this session (resume git position)
54
+ - **notes** — free-form context that matters for resume (decisions made, dead ends ruled out)
55
+
56
+ Do not call `session_checkpoint` as a substitute for `session_end`. Checkpoint = pause; end = finalize + handoff.
57
+
58
+ ## On session_resume
59
+
60
+ `session_resume` returns the session row, last checkpoint, linked conversations, and active context sources. Use after crash, restart, or context compaction — not as a way to spawn a new session for the same task.
61
+
62
+ After calling `session_resume`:
63
+
64
+ 1. **Echo the checkpoint summary verbatim** — `resumePoint`, `notes`, `dirtyFiles`, `lastCommit`, `lastConversationId`. Do not summarize. Butter needs the same view the prior session had.
65
+ 2. **Confirm task binding** — name the `taskId` and current status. If the task is no longer IN-PROGRESS, surface the discrepancy before proceeding.
66
+ 3. **Resume from `resumePoint`** — pick up the exact next step. Do not re-plan from scratch.
67
+ 4. **Do not call `session_start`** — resume reactivates the existing session; starting a new one orphans the checkpoint and creates duplicate state.
68
+
69
+ If no checkpoint exists (resumed session was never checkpointed), say so explicitly and ask Butter where to pick up before continuing.
70
+
71
+ ## On session_end
72
+
73
+ When preparing the session_end payload, always include:
74
+
75
+ - **resumePoint** (required) — one sentence describing where you stopped and what the next session should pick up.
76
+ - **tasksUpdated** (required if session had a taskId) — list of task ids and their new status.
77
+ - **decisions** — architectural or implementation decisions made this session. Explicit > implicit.
78
+ - **looseEnds** — genuine ideas that need future research. NOT a catch-all dump. See classification rule below.
79
+ - **commits** — commit SHAs + short message if commits were made.
80
+
81
+ Never end a session with only resumePoint. If the session was trivial (read-only), explicitly note "no changes" in resumePoint or notes.
82
+
83
+ ### Classify each loose end BEFORE writing it
84
+
85
+ Every candidate loose end falls into exactly one of 3 buckets. Pick the bucket first, then route accordingly:
86
+
87
+ 1. **Action item** (has clear owner + acceptance criteria) → call `task_create` directly with status=TODO or READY. Do NOT put it in `looseEnds`. Examples: "PR #5 awaiting merge — on merge delete branch", "companion repo 2 commits ahead — needs push".
88
+ 2. **Dirty-state observation** (untracked file, stale branch, cosmetic shell handle, lingering process) → put it in `notes` field or the commit message. Do NOT put it in `looseEnds`. Examples: "stale worktree at .worktrees/foo", "Windows file handle on dist/", "untracked spike notes in /tmp".
89
+ 3. **Genuine idea needing research** (open question, design uncertainty, "should we…?") → `looseEnds` (this is the legitimate use). Each entry: 1 line, concrete, no acceptance criteria yet. Example: "investigate whether prewarm cache can survive worktree switch".
90
+
91
+ `looseEnds` are auto-converted to inbox entries (status=raw) under the session's project — one entry per item, tagged with the source session/task ID. Butter reviews the inbox in `/daily` and decides which deserve `inbox_convert` → task. If you find yourself dumping action items or observations into `looseEnds`, you skipped step 1 — go back and classify.
92
+
93
+ ### Structured summary payload (ADR-028)
94
+
95
+ `session_end` accepts an optional typed `summary` field. When provided, the server writes one `session_events` row (`event_type='observation'`, `payload.kind='session_summary'`) atomic with the session close. Use it on real implementation sessions — TASK-846 auto-recall and the Dashboard Sessions tab both read this shape.
96
+
97
+ **Required (FE base):**
98
+
99
+ | Field | Type | Notes |
100
+ |---|---|---|
101
+ | `summary` | `string` | One paragraph: what shipped, what stalled, what's next |
102
+ | `tasksDone` | `string[]` | Task IDs marked DONE in this session |
103
+ | `tasksCreated` | `string[]` | Task IDs created in this session |
104
+ | `tasksCancelled` | `string[]` | Task IDs cancelled in this session |
105
+ | `commits` | `string[]` | Format: `"<short-hash> <task-id> <subject>"` |
106
+ | `conversations` | `string[]` | Conversation IDs touched / decided |
107
+ | `openItems` | `string[]` | Carry-forward items — distinct from `looseEnds` (action items already filed as tasks) |
108
+
109
+ **AI-optional — server auto-fills from channels 1+2 (TASK-913):**
110
+
111
+ | Field | Type | Notes |
112
+ |---|---|---|
113
+ | `filesChanged` | `string[]?` | Format: `"<path> (<what changed>)"`. Omit → server appends `"<path> (+N, -M)"` per unique path from `kind='file_modified'` events (channel 1). AI entries kept verbatim; only unseen paths appended. |
114
+ | `acCoverage` | `Record<taskId, string>?` | e.g. `"TASK-X": "5/5 verified (...). 0 deferred."`. Omit → server emits `"N/M verified (<evidence>)"` derived from `kind='ac_check'` events (channel 2), where `M = findAcItems(task.body).length` at session-end. AI entry kept verbatim + ` + K auto-detected` suffix added when events exist. |
115
+
116
+ **Merge rule:** AI input wins — your judgment narrative trumps mechanical aggregation. The aggregator only fills gaps; it never overwrites a value you provided.
117
+
118
+ **Optional (BE extension):**
119
+
120
+ `tasksShipped: Array<{id, title, commits, files, tests, confidence}>`, `tasksNotDone: Array<{id, reason}>`, `testCoverageSummary: string`, `outstandingRisks: string[]`, `branchState: string`.
121
+
122
+ **Canonical example** (mirrors EVT-1779256867162-5 — SESSION-1779246110272-1, TASK-800 PIM FE):
123
+
124
+ ```json
125
+ {
126
+ "summary": "TASK-800 shipped: PIM FE list-screen virtualization landed; PR #482 squash-merged. Two follow-ups carried forward (filter chip a11y, prefetch on hover).",
127
+ "tasksDone": ["TASK-800"],
128
+ "tasksCreated": ["TASK-805", "TASK-806"],
129
+ "tasksCancelled": [],
130
+ "commits": ["a1b2c3d TASK-800 feat(pim-fe): virtualized list with row prefetch"],
131
+ "filesChanged": [
132
+ "src/pim/list/VirtualList.tsx (new — react-window wrapper)",
133
+ "src/pim/list/list-page.tsx (swap table → VirtualList)"
134
+ ],
135
+ "acCoverage": {
136
+ "TASK-800": "6/6 verified (lint+vitest+build+manual smoke @ 10k rows). 0 deferred."
137
+ },
138
+ "conversations": ["CONV-1779246111-1"],
139
+ "openItems": [
140
+ "Filter chip a11y — screenreader announces stale label after clear (TASK-805)",
141
+ "Row prefetch on hover — current eager prefetch wastes ~30% requests (TASK-806)"
142
+ ]
143
+ }
144
+ ```
145
+
146
+ **Narrative-only example** (relies on the channels 1+2 aggregator to fill `filesChanged` + `acCoverage` — minimum viable payload for an agent that has been calling `ac_check` and has the file-edit hook installed):
147
+
148
+ ```json
149
+ {
150
+ "summary": "TASK-913 shipped: session-summary aggregator wires channels 1+2 into session_end. Narrative-only payloads now produce fully populated stored summaries.",
151
+ "tasksDone": ["TASK-913"],
152
+ "tasksCreated": [],
153
+ "tasksCancelled": [],
154
+ "commits": ["d4d4d4d TASK-913 feat: session-summary aggregator"],
155
+ "conversations": [],
156
+ "openItems": []
157
+ }
158
+ ```
159
+
160
+ Stored payload `filesChanged` ends up `["src/core/domain/lifecycle/session-lifecycle-service.ts (+62, -3)", …]` and `acCoverage` `{"TASK-913": "5/5 verified (lint exits 0; vitest 35/35 …)"}` — both server-derived.
161
+
162
+ **Validation:** invalid payload → 400 from MCP, session is NOT closed. Omitting `summary` entirely is fully backward compatible (no observation row created, no error). Schema lives at `src/adapters/mcp/mcp-tools/session-tools.ts` (`sessionSummarySchema`).
163
+
164
+ ## On conversation_read
165
+
166
+ - Read first. If your name is missing from `readBy` on any message, process those messages and reflect them in your reply.
167
+ - State a position. Each turn must be one of: `signoff`, `propose_rewrite` (and rewrite the summary), `abstain_blocked` (name the blocker), `needs_clarification` (ask the question).
168
+ - One live position. Your latest message supersedes your earlier ones — don't argue across messages; rewrite the summary.
169
+ - Stay short. 1500-char cap is enforced. Long content goes to `decisionSummary`, a knowledge entry, or a linked artifact.
170
+ - Decisions own actions. If follow-up work exists, add it to `actions[]` with `assignee` and `linkedTaskId`.