Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .cards/backlog.jsonl

Large diffs are not rendered by default.

30 changes: 28 additions & 2 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,40 @@
{
"name": "cards-serve",
"runtimeExecutable": "./cards",
"runtimeArgs": ["serve", "--workspace", "examples/demo-workspace", "--port", "8788"],
"runtimeArgs": [
"serve",
"--workspace",
"examples/demo-workspace",
"--port",
"8788"
],
"port": 8788
},
{
"name": "cards-preview",
"runtimeExecutable": "go",
"runtimeArgs": ["run", "./cmd/cards", "serve", "--workspace", "./.cards", "--port", "8799"],
"runtimeArgs": [
"run",
"./cmd/cards",
"serve",
"--workspace",
"./.cards",
"--port",
"8799"
],
"port": 8799
},
{
"name": "cards-8080",
"runtimeExecutable": "./cards",
"runtimeArgs": [
"serve",
"--workspace",
"./.cards",
"--port",
"8080"
],
"port": 8080
}
]
}
171 changes: 171 additions & 0 deletions docs/plans/2026-09-06-sprint-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Sprint plan 2026-09-06 — Finish the agent write loop; start the docs refresh

> Produced by the /sprint-plan workflow (ground → candidates → falsification → plan) on
> 2026-09-06, then extended by hand for the second thread the workflow did not cover
> (the README and site refresh). Board state re-read against the live server on
> `:8787` the same day. Two threads, run in parallel; the second one has one
> sequencing dependency on the first.

## Status as of 2026-09-06

- `in_progress` and `review` are both **empty**. WIP limit on `in_progress` is 3.
- Sprint A from [`2026-08-08`](2026-08-08-sprint-plan.md) is **one-third done**:
`0f7be7f6` (comment alias) shipped in `90b838d` and closed today. `069ec1d1`
and `c0102825` are still `todo`.
- The init → adopt happy path was simulated end to end and its three gaps fixed on
`fix/happy-path-simulation` (`8920121`): a fresh workspace's `default_user` can now
own cards, `list -q` prints nothing on an empty result, and the adoption playbook
carries the ladder JSON it used to only describe. Card `8150dda9`, closed.
- Bookkeeping from the 08-31 note is done: `ea7ea2a3` (run-extensions / do /
extensions list) was verified shipped and closed; the mkdocs `not_in_nav` and MCP
README path drive-bys landed in `5910c51`. `mkdocs build --strict` is clean.

## Thread 1 — agent write loop (engineering)

**Theme sentence.** An agent can claim, update and read back a card in one round-trip
each, without a preceding GET and without dumping full work logs into its context
window.

This is Sprint A's remaining two cards plus the one identity bug the happy-path
simulation exposed, and one new card that proves the theme as a whole. The workflow
verified all eight code claims behind it; none were refuted. It was the only one of
three candidates whose verdict came back *survives* (the other two are recorded under
*Rejected* below).

| # | Card | Commitment | Status |
|---|---|---|---|
| 1 | `1c877e6c` | TakeNext runs the same owner check as PATCH/claim (`checkUserExists`, which since `8920121` accepts definition-declared users), **and** the demo's `review-bot` gets declared/registered in the same PR so the demo does not regress | `todo` (promoted from backlog today) |
| 2 | `069ec1d1` | `--if-match latest` on patch / claim / release, re-read service-side under the write lock; explicit `--version N` keeps exit code 4 on stale | `todo` |
| 3 | `c0102825` | `--md` output for get / list; extract `cardMarkdown` off the TUI model into a shared renderer; short ids; no work_log entries or comment bodies in list output — **shrink valve** | `todo` |
| 4 | `13935666` | One integration test: take-next → patch `--if-match latest` → get `--md`, three calls, zero GETs, plus the empty-pool exit | `backlog`, gated on decision 4 |

**Order.** 1 before 2 (both touch the claim path; two PRs editing `claimWithRetry`
at once is the risk). 3 is independent and can run beside them. 4 last, by
construction. The cut rule from 08-08 still holds: under pressure `c0102825` is dropped,
not shrunk.

**Then Sprint B**, unchanged from 08-08: `fc92019c` health → `3fd62d32` query endpoint
→ `8afb9008` OpenAPI publication. Its start gate (Sprint A's cards have left `todo`)
is the reason it is not in this batch.

### Verification

- Per card, from the cards' own acceptance: TakeNext rejects an unregistered assignee
with the structured `unknown_user` PATCH already returns; a `go test -race`
concurrent-writer test pins that concurrent `latest` writes still 409; a golden test
for `--md` with the migrated TUI test proving no render drift.
- Batch-level: card 4's test exists and passes. If it does not, the sprint did not ship
its theme.
- Gates on every PR: `go build ./...`, `go vet ./...`, `go test -race ./...`, plus
`go test ./internal/tui ./internal/cli ./internal/core`.

## Thread 2 — README and site refresh (docs)

**Goal.** Fewer words, one storyline, one diagram. The README and the site should tell
the same story in the same order and stop duplicating each other.

**What the survey found** (re-read today, numbers from `wc`):

- `README.md` is 428 lines / 2,218 words. It opens with the same two definitional
paragraphs as the site home, then re-documents install, quick start, project
adoption, configuration, sync, API behaviour, extensions, dev workflow and
releases — most of it a second copy of site pages, with three different
"quick start" blocks and `CARDS_URL` shown both with and without `/v1`.
- `docs/index.md` is 315 lines: a value grid, a doc grid, a themes gallery, three CTA
rows. Its best asset — the side-by-side *definition JSON → rendered screen* — is the
second section, and the actual pitch for the agent audience ("a board beats a
markdown plan") is fourth.
- The nav has 7 tabs and 33 pages. *Guide* mixes concepts, reference and walkthroughs;
*Reference* lists rollout history, design notes and the roadmap next to the spec.
- `get-started.md` (117 lines) is the right shape and stays the spine.
- `using-cards.md` is 635 lines: a 30-line narrative followed by a 440-line catalog of
every verb in CLI, HTTP and MCP — reference material sitting in the guide.
- Visuals: 12 images exist, but nothing shows the whole system on one picture and
nothing shows an agent loop. The README's hero (`media/board.png`) is a dev build
with a stale nav item.

**The storyline** (shared by README and home, in this order):

1. **Hook.** A kanban board that lives in your repo and that your agents can drive.
One screenshot.
2. **Why.** Todos are too little; a hosted tracker is too much. People, scripts and
agents on one board, and a bad write is rejected with what was allowed.
3. **Sixty seconds.** `init` → open the board → one CLI write → point an agent at it.
4. **How it works.** One diagram: definitions in git + SQLite → one service layer →
HTTP, CLI, MCP, web UI, TUI.
5. **Choose your path.** I use the board · I'm wiring an agent · I'm building on it.

| # | Card | Commitment | Sequencing |
|---|---|---|---|
| P | `65f8ae38` | Parent: the storyline, tone rules and the acceptance numbers (README ≤120 lines, nav ≤5 tabs / ≤20 pages, home ≤150 lines, strict build clean) | closes when the four below are done |
| D4 | `a3adf5b5` | Visuals: the one diagram (SVG, both palettes), refreshed screenshots at one size and theme, an agent-loop demo | **start now** |
| D3 | `3651b81b` | Home page rebuilt on the storyline; nav to five tabs (Start · Guide · Agents · Build on it · Project); catalog pages out of the nav; retire the `v0.1.x` badge | **start now**, takes the diagram from D4 |
| D2 | `45a21e3f` | README cut to ≤120 lines: hook, why, three-command start, one short schema example, path links, install, license | after `069ec1d1` and `c0102825`, so the CLI snippet shows the final flags |
| D5 | `f59debb8` | `using-cards.md` split: the working session stays in Guide (≤200 lines); the operations catalog moves under *Build on it* and is deduplicated against `reference/cli.md` | after `069ec1d1`, same reason |

All five are in `backlog` with `parent` links to `65f8ae38`; promoting them to `todo`
is the decision below. D3 is linked `related` to `cddf3086` (consolidating the two
reference docs), which changes what *Build on it* lists but is Sprint C work and stays
out of this batch.

### Verification

- `mkdocs build --strict` on every docs PR (a local venv builds it in about a second;
CI runs the same command in `deploy-pages.yml`).
- Line counts from the acceptance table, checked with `wc -l` in the PR description.
- Every README link resolves against the built site and the repo.
- The three quick-start commands in the README run as written on a fresh checkout.
- A before/after screenshot of the home page above the fold, attached to D3.

## Decisions needed before the first card is claimed

1. **`--md` as a stable contract.** `c0102825` declares it stable, which makes the shape
hard to change after it ships. *Recommendation:* approve the shape now — 8-char
short ids, title / type / status / owner / version, typed fields, link edges; no
work_log entries or comment bodies in `list`, both included in `get`. If that is
not settled, ship it marked experimental for one release.
2. **Where the shared markdown renderer lives.** *Recommendation:* a new
`internal/render` package; `internal/core` should not know about markdown, and the
TUI keeps calling it.
3. **Item 1 is a behaviour break** for any extension worker that claims without
registering. *Recommendation:* accept it with a CHANGELOG entry and declare
`review-bot` in the demo workspace's `workspace.users` — since `8920121` a
declared user is a registered user, so no runtime registration step is needed.
Grep the examples for other unregistered claimers before tightening.
4. **Is card 4 in scope?** *Recommendation:* yes, as its own card. Folding it into
`069ec1d1`'s PR leaves the theme with no independent proof.
5. **CLI-only or an HTTP `If-Match` header too?** *Recommendation:* CLI only this
sprint; the service-side mechanism is written so an `If-Match` header can reuse it
in Sprint B without touching the write path twice.
6. **Promote the docs cards.** D4 and D3 can start today and are independent of the
engineering thread. *Recommendation:* promote both to `todo` now; leave D2 and D5 in
`backlog` until `069ec1d1` merges.
7. **Dispose of `9f626548`** (global request-body cap). Still open from 08-31; commit
`42eeea6` and `3fd62d32`'s locked decisions both reject it under principle 7.
*Recommendation:* mark it wontfix citing that commit.
8. **The Sprint 07-22 tail** (`0fd1b9b7`, `57e1bde9`, `ad67971f`, all blocked) and
`c7a70b64` "SPLIT PARENT". *Recommendation:* close them as superseded unless
someone still wants them; they are the only blocked cards on the board.

## Candidates rejected

- **http-integration-surface** (Sprint B) — verdict *weakened*, and its start gate has
not opened. Two of its five items are broken as filed: `9f626548` contradicts a
decision recorded on its sibling `3fd62d32`, and `d6b50bee`'s actual deliverable lives
on `2f12f16f`. The three solid cards (`fc92019c`, `3fd62d32`, `8afb9008`) are the next
sprint, in that order.
- **definitions-visible-and-editable** (`8903f9aa`, `0707a008`, `3d8ed6d9`) — verdict
*weakened*: `cmd/cards/reload.go` deliberately 409s an existing board file rather than
doubling as an editor, so the board settings editor needs a new PATCH handler plus
schema and concurrency work; three cards carry about four cards of work.

## Unverified

- The companion doc edits the cards call for (`docs/spec/api-surface.md`,
`docs/reference/cli.md`) were flagged as real scope but not sized.
- Line numbers cited on the cards for `cardMarkdown` and `TakeNext` were correct at
planning time and drift; find code by name.
- That `review-bot` is the only unregistered claimer. Verified it is one; not verified
it is the only one.
- The docs acceptance numbers (120 / 150 / 200 lines, 5 tabs, 20 pages) are targets
chosen from today's counts, not measured against a reader.
3 changes: 2 additions & 1 deletion docs/reference/INTEGRATOR-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ type Card struct {
→ *picraft note: "claiming worker in `owner`, body in a custom field" holds
— claim leaves your custom fields untouched.*
- Setting `owner` via PATCH or `claim` requires a registered user (`unknown_user`
422 otherwise). `take-next` currently skips that lookup for `assign_to`/actor
422 otherwise) — registered via `POST /v1/users` / `cards users register`, or
declared in `workspace.users` / `settings.default_user`. `take-next` currently skips that lookup for `assign_to`/actor
ownership — see §5 and backlog card `card_1c877e6ca3e04a24bdd3d2ff90286a84`.
- `claim` is compare-and-set on `version`; claiming a card already owned by a
*different* actor → `409 version_conflict`. `release` sets owner back to `""`.
Expand Down
12 changes: 8 additions & 4 deletions docs/reference/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ type Card struct {
→ *picraft note: D7's "claiming worker in `owner`, body in a custom field" holds
— claim leaves your custom fields untouched.*
- Setting `owner` via PATCH or `claim` requires a registered user (`unknown_user`
422 otherwise). `take-next` currently bypasses that lookup (see §5).
422 otherwise). "Registered" means in the users table (`POST /v1/users`,
`cards users register`) **or** declared in the definitions (`workspace.users`,
or the `settings.default_user` seed) — so a fresh `cards init` workspace's
default user can own cards. `take-next` currently bypasses that lookup (see §5).
- `claim` is compare-and-set on `version`; claiming a card already owned by a
*different* actor → `409 version_conflict`. `release` sets owner back to `""`.

Expand Down Expand Up @@ -258,8 +261,8 @@ no match → `200 { "card": null }`. On a match → `200 { "card": {...} }`.
> (`internal/core/errors.go:141-145`, raised from the CAS path at
> `internal/sqlite/sqlite.go:746` <!-- guard: internal/sqlite/sqlite.go:746 symbol=ErrClaimRaced -->);
> `take-next`/`claim` wrap the attempt in `claimWithRetry`
> (`internal/core/service.go:1587` <!-- guard: internal/core/service.go:1587 symbol=claimWithRetry -->,
> called at `:1552` <!-- guard: internal/core/service.go:1552 symbol=claimWithRetry -->),
> (`internal/core/service.go:1599` <!-- guard: internal/core/service.go:1599 symbol=claimWithRetry -->,
> called at `:1564` <!-- guard: internal/core/service.go:1564 symbol=claimWithRetry -->),
> which retries the next candidate up to 3 times within one call before
> returning `{ card: null }`. Verified by `internal/core/claimretry_test.go`.

Expand Down Expand Up @@ -487,7 +490,8 @@ durable feed entries.
each with its own `CARDS_USER`, with no pre-registration.
- **Ownership is mostly registry-backed.** Setting `owner` via PATCH, or using
`claim` (which makes the actor the owner), requires a registered user
(`POST /v1/users {id, kind}`) or returns `unknown_user`. `take-next` currently
(`POST /v1/users {id, kind}`, or declared in `workspace.users` /
`settings.default_user`) or returns `unknown_user`. `take-next` currently
bypasses that user lookup for `assign_to`/actor ownership; this is an
implementation inconsistency, not an authentication boundary.
- **Stable orchestrator vs ephemeral workers:** both are just actor strings. Use a
Expand Down
92 changes: 90 additions & 2 deletions internal/agentguide/skill/references/project-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ board, no `link_types`, no `.gitignore`. Fine for a first look. For a real
project board, replace the starter definitions — and delete the welcome cards
— *before* creating work. Changing columns while those cards exist fails
validation; `default_board` must name a board file that already exists or the
workspace will not load. Write the JSON in this document into `definitions/`;
don't layer it onto the tutorial.
workspace will not load. Write the files in §2 ("The ladder as files") into
`definitions/`, removing the starter `boards/welcome.json` and
`card-types/task.json` in the same pass; don't layer them onto the tutorial.

## 2. Start minimal and climb only when the board complains

Expand Down Expand Up @@ -78,6 +79,93 @@ all four):
"tag_policy": "locked", "default_board": "engineering" }
```

### The ladder as files

Five files under `.cards/definitions/`. This is the whole starting point; it
loads as written (`cards --workspace .cards workspace show` to confirm).

`workspace.json`:

```json
{
"id": "myproject", "name": "My Project",
"columns": [
{ "id": "backlog", "name": "Backlog" },
{ "id": "todo", "name": "To Do" },
{ "id": "in_progress", "name": "In Progress" },
{ "id": "review", "name": "Review" },
{ "id": "done", "name": "Done" }
],
"tag_set": [],
"link_types": [
{ "id": "part-of", "name": "Part of", "type": "directional" },
{ "id": "depends-on", "name": "Depends on", "type": "directional" },
{ "id": "blocked-by", "name": "Blocked by", "type": "directional" },
{ "id": "related", "name": "Related", "type": "bidirectional" }
],
"settings": {
"default_user": "me",
"enforce_transitions": true, "strict_fields": true,
"tag_policy": "locked", "default_board": "engineering"
}
}
```

`card-types/epic.json`, `card-types/story.json`, `card-types/task.json`:

```json
{ "id": "epic", "name": "Epic", "schema_version": 1,
"fields": [ { "id": "goal", "type": "text", "required": true } ],
"allowed_columns": ["backlog", "todo", "in_progress", "review", "done"] }
```

```json
{ "id": "story", "name": "Story", "schema_version": 1,
"fields": [
{ "id": "outcome", "type": "text", "required": true },
{ "id": "acceptance", "type": "text", "required": true }
],
"allowed_columns": ["backlog", "todo", "in_progress", "review", "done"] }
```

```json
{ "id": "task", "name": "Task", "schema_version": 1,
"fields": [
{ "id": "actions", "type": "text", "required": true },
{ "id": "verify", "type": "string", "required": false },
{ "id": "work_log", "type": "repeating", "display": "feed",
"item_fields": [
{ "id": "commit", "type": "string", "required": true },
{ "id": "notes", "type": "text" }
] }
],
"allowed_columns": ["backlog", "todo", "in_progress", "review", "done"] }
```

`boards/engineering.json`:

```json
{
"id": "engineering", "name": "Engineering",
"columns": ["backlog", "todo", "in_progress", "review", "done"],
"card_type_ids": ["epic", "story", "task"],
"settings": { "enforce_transitions": true },
"wip_limits": { "in_progress": 2 },
"transitions": {
"backlog": ["todo", "in_progress"],
"todo": ["backlog", "in_progress"],
"in_progress": ["todo", "review"],
"review": ["in_progress", "done"],
"done": ["review"]
},
"presentation": { "lane_group_by": "status" }
}
```

The `default_user` declared in settings counts as a registered user, so it can
own cards from the first claim; every other actor registers once with
`cards users register --id <id> --kind agent`.

`strict_fields` and `tag_policy: locked` reject typos instead of silently
creating a second vocabulary. `default_board` stops every surface guessing when
there's more than one board — set it after that board file exists; load rejects
Expand Down
10 changes: 10 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ func TestListOutputModes(t *testing.T) {
}
}
})
t.Run("quiet prints nothing for an empty collection", func(t *testing.T) {
c := newTestClient(t, Config{Quiet: true})
out, err := runCmd(t, c, "list", "--q", "zzz-no-such-card-zzz")
if err != nil {
t.Fatalf("list: %v", err)
}
if out != "" {
t.Errorf("quiet empty list printed %q, want nothing", out)
}
})
t.Run("json pretty-prints the envelope", func(t *testing.T) {
c := newTestClient(t, Config{JSON: true})
out, err := runCmd(t, c, "list")
Expand Down
Loading
Loading