From 892012109215acfa6100fcf60cec7b636733a3d1 Mon Sep 17 00:00:00 2001 From: Jeremy Seitz <7750+somebox@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:24:53 +0200 Subject: [PATCH 1/4] =?UTF-8?q?core,cli,docs:=20fix=20the=20init=E2=86=92l?= =?UTF-8?q?adder=20happy=20path=20found=20by=20simulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walking a stranger through `cards init` → replace the starter with the epic/story/task ladder → create, link, claim, export surfaced three gaps: - Definition-declared users (workspace.users, or the settings.default_user seed) never reach the users table, and the ownership check only consulted the table. A fresh `cards init` workspace's `me` could create cards but not claim them. checkUserExists now accepts declared users, the workspace snapshot merges registered + declared, and the unknown_user hint names `cards users register` alongside POST /v1/users. - `list -q` on an empty collection printed `{"items":[]}`, which a `list -q | while read id` loop would feed straight to the next command. Quiet mode now keys on the presence of `items`, not its length. - project-practices.md told the reader to "write the JSON in this document" but carried only a settings snippet. §2 now has the five ladder files, extracted and verified against a fresh init end to end. Re-pins the two claimWithRetry doc-audit anchors the service edit moved. Co-Authored-By: Claude Fable 5.1 --- docs/reference/INTEGRATOR-REFERENCE.md | 3 +- docs/reference/implementation-status.md | 12 ++- .../skill/references/project-practices.md | 92 ++++++++++++++++++- internal/cli/cli_test.go | 10 ++ internal/cli/client.go | 16 +++- internal/core/declaredusers_test.go | 59 ++++++++++++ internal/core/errors.go | 2 +- internal/core/service.go | 14 ++- internal/core/validate.go | 12 +++ 9 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 internal/core/declaredusers_test.go diff --git a/docs/reference/INTEGRATOR-REFERENCE.md b/docs/reference/INTEGRATOR-REFERENCE.md index 241dcf3..13e549b 100644 --- a/docs/reference/INTEGRATOR-REFERENCE.md +++ b/docs/reference/INTEGRATOR-REFERENCE.md @@ -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 `""`. diff --git a/docs/reference/implementation-status.md b/docs/reference/implementation-status.md index c0ff666..62aa0e1 100644 --- a/docs/reference/implementation-status.md +++ b/docs/reference/implementation-status.md @@ -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 `""`. @@ -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` ); > `take-next`/`claim` wrap the attempt in `claimWithRetry` -> (`internal/core/service.go:1587` , -> called at `:1552` ), +> (`internal/core/service.go:1599` , +> called at `:1564` ), > which retries the next candidate up to 3 times within one call before > returning `{ card: null }`. Verified by `internal/core/claimretry_test.go`. @@ -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 diff --git a/internal/agentguide/skill/references/project-practices.md b/internal/agentguide/skill/references/project-practices.md index f628063..7af4c87 100644 --- a/internal/agentguide/skill/references/project-practices.md +++ b/internal/agentguide/skill/references/project-practices.md @@ -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 @@ -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 --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 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index b26678b..bb57250 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -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") diff --git a/internal/cli/client.go b/internal/cli/client.go index d777a0b..a683763 100644 --- a/internal/cli/client.go +++ b/internal/cli/client.go @@ -252,15 +252,21 @@ func parseErr(data []byte) error { func (c *Client) Print(data []byte, isCollection bool, idPath string) { switch { case c.cfg.Quiet: - // Extract ids from a {"items":[...]} envelope. + // Extract ids from a {"items":[...]} envelope. Presence of the key + // decides, not its length: an empty collection prints nothing, so + // `list -q | while read id` never feeds a literal `{"items":[]}` + // to the next command. var env struct { - Items []map[string]any `json:"items"` + Items json.RawMessage `json:"items"` } if err := json.Unmarshal(data, &env); err == nil && len(env.Items) > 0 { - for _, it := range env.Items { - fmt.Println(idOf(it, idPath)) + var items []map[string]any + if json.Unmarshal(env.Items, &items) == nil { + for _, it := range items { + fmt.Println(idOf(it, idPath)) + } + return } - return } // Fallback: single object id. var m map[string]any diff --git a/internal/core/declaredusers_test.go b/internal/core/declaredusers_test.go new file mode 100644 index 0000000..fe55350 --- /dev/null +++ b/internal/core/declaredusers_test.go @@ -0,0 +1,59 @@ +package core_test + +import ( + "context" + "errors" + "testing" + + "github.com/somebox/cards/internal/core" +) + +// A user declared in the workspace definitions (workspace.users, or the +// settings.default_user seed the loader materializes) never reaches the +// users table on its own. It must still count as registered for ownership, +// or a fresh `cards init` workspace's default user can create cards it cannot +// claim. +func TestClaim_DefinitionDeclaredUserCanOwn(t *testing.T) { + ws, types, boards := testConfig() + ws.Users = []core.User{{ID: "declared", Kind: "human"}} + svc, _ := newTestServiceWith(t, ws, types, boards) + ctx := context.Background() + + c, err := svc.CreateCard(ctx, core.CreateCardRequest{ + TypeID: "task", Title: "Own me", Status: "todo", + Fields: map[string]any{"description": "d"}, Actor: "declared", + }) + if err != nil { + t.Fatalf("create: %v", err) + } + got, err := svc.Claim(ctx, c.ID, core.ClaimRequest{Version: c.Version, Actor: "declared"}) + if err != nil { + t.Fatalf("claim as definition-declared user: %v", err) + } + if got.Owner != "declared" { + t.Errorf("owner = %q, want declared", got.Owner) + } + + // An id that is neither declared nor registered is still rejected. + owner := "nobody" + _, err = svc.PatchCard(ctx, c.ID, core.PatchCardRequest{Version: got.Version, Owner: &owner, Actor: "declared"}) + var ce *core.Error + if !errors.As(err, &ce) || ce.Code != "unknown_user" { + t.Fatalf("patch owner=nobody: err = %v, want unknown_user", err) + } + + // The introspection snapshot lists both registered and declared users. + snap, err := svc.Workspace(ctx) + if err != nil { + t.Fatalf("workspace: %v", err) + } + seen := map[string]bool{} + for _, u := range snap.Workspace.Users { + seen[u.ID] = true + } + for _, want := range []string{"u", "alice", "declared"} { + if !seen[want] { + t.Errorf("snapshot users missing %q: %v", want, snap.Workspace.Users) + } + } +} diff --git a/internal/core/errors.go b/internal/core/errors.go index ccdcd74..bf5176c 100644 --- a/internal/core/errors.go +++ b/internal/core/errors.go @@ -71,7 +71,7 @@ func newUnknownUser(value string) *Error { return &Error{ Code: "unknown_user", Value: value, Message: "Unknown user.", - Hint: "Register first: POST /v1/users", + Hint: "Register first: `cards users register --id ` or POST /v1/users, or declare the user in workspace.json.", HTTPStatus: 422, } } diff --git a/internal/core/service.go b/internal/core/service.go index 97a392d..0fd89c9 100644 --- a/internal/core/service.go +++ b/internal/core/service.go @@ -463,8 +463,20 @@ func (s *Service) commitCard(ctx context.Context, next *Card, evs []*Event) erro // immutable after startup, only Users is refreshed per call. func (s *Service) Workspace(ctx context.Context) (*WorkspaceSnapshot, error) { ws := *s.ws + // Registered users first, then any definition-declared user the store + // does not know — both count as registered for ownership. if users, err := s.store.ListUsers(ctx); err == nil && len(users) > 0 { - ws.Users = users + seen := make(map[string]bool, len(users)) + for _, u := range users { + seen[u.ID] = true + } + merged := append([]User(nil), users...) + for _, u := range s.ws.Users { + if !seen[u.ID] { + merged = append(merged, u) + } + } + ws.Users = merged } curVersions := map[string]int{} for id, ct := range s.types { diff --git a/internal/core/validate.go b/internal/core/validate.go index 7ab3d96..94de59c 100644 --- a/internal/core/validate.go +++ b/internal/core/validate.go @@ -368,7 +368,19 @@ func (s *Service) typeIDsAllowingStatus(candidates []string, status string) []st return out } +// checkUserExists accepts a user that is either registered in the store +// (POST /v1/users, `cards users register`) or declared in the workspace +// definitions — `workspace.users`, or the `settings.default_user` seed the +// loader materializes when that list is empty. Definitions are the declared +// contract, and they never reach the users table on their own, so consulting +// only the store would leave a fresh `cards init` workspace unable to own the +// cards its own default user creates. func (s *Service) checkUserExists(ctx context.Context, userID string) error { + for _, u := range s.ws.Users { + if u.ID == userID { + return nil + } + } users, err := s.store.ListUsers(ctx) if err != nil { // A store failure must not masquerade as "unknown user". From e0b2070a3293864341840c6b2131879345d98098 Mon Sep 17 00:00:00 2001 From: Jeremy Seitz <7750+somebox@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:26:02 +0200 Subject: [PATCH 2/4] cards: record the happy-path simulation fixes on the board Co-Authored-By: Claude Fable 5.1 --- .cards/backlog.jsonl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.cards/backlog.jsonl b/.cards/backlog.jsonl index a2df0be..d9c8867 100644 --- a/.cards/backlog.jsonl +++ b/.cards/backlog.jsonl @@ -32,7 +32,7 @@ {"data":{"id":"card_1aaa461124c04fe0b62e592cb3b10b09","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"UI: header nav — active board, theme label, boards overflow, CTA system","status":"done","fields":{"a11y":"aria-current on nav; theme menu keyboardable; overflow menu accessible.","acceptance":"- Active board is visually and programmatically indicated\n- Current theme readable without opening the menu\n- With 8+ boards, nav does not wrap into unusable multi-line chrome\n- New card / new board look like one CTA system\n","branch":"ui/nav-polish","description":"Nav works for 2 demo boards; will fail for real workspaces.\n\nMUST:\n1) aria-current / .is-active on the active board link\n2) Theme shows current theme name (e.g. ◐ labels), not only menu check\n3) Boards overflow strategy after ~N links (details menu or \"Boards\" dropdown)\n4) Unify + Card / + Board visual language via shared .nav-action metrics (token padding, min-height)\n5) Sync with home polish crumb (81086204) — board page shows path back home\n\nNO theme rewrites. Keep always-dark nav chrome.\n","design_ref":"docs/architecture/DESIGN.md; layout.html app-nav","platforms":["desktop","mobile"],"surface":"nav"},"tags":["feature"],"links":[{"type_id":"parent","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.909732Z"},{"type_id":"related","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.910997Z"},{"type_id":"related","target":"card_810862041d5b410b95cb5365fe0e438c","created_by":"jeremy","created_at":"2026-07-09T23:26:10.944892Z"}],"comments":[{"id":"cm_7f391f0e9b86437a","author":"claude","body":"Sprint 07-10: nice-to-have tail (tracker card_3f225267) — behind the spacing-rules gate (card_72ebfbee). REQUIRED before implementation: attach a wireframe/mockup of the header (active board, theme label, boards overflow, CTA placement) to this card for review. Unify via --role-meta-* tokens; dark-mode + reduced-motion checks on the PR.","created_at":"2026-07-10T02:07:04.475246Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_7a330e5df4044b55","author":"claude","body":"Done, committed directly to main (workflow switched to main-direct). Header board selector: replaced the one-link-per-board row with a 'Cards / ▾' breadcrumb whose
disclosure (mirrors the theme-picker, pure HTML no JS) lists all boards with the active one checkmarked + 'New board' under a divider. Header stays one row at any board count. Removed the now-dead .nav-add '+' and its CSS. Verified in-browser (default + labels themes): panel opens, active board checked, switching navigates, uppercases under labels, zero console errors; full suite green (14 pkgs, hex ratchet raised 179→183 for the nav's fixed-light #fff). Matches the approved mockup.","created_at":"2026-07-11T10:53:09.980826Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_58b7840c94694e4f","author":"claude","body":"P0a verify 2026-07-11: flipped to done (optional READY). Verifying commit cc45f72. Header nav (active board, theme label, boards overflow, CTA) on main. Process note: wireframe-before-impl ACC was not attached; functional ship accepted for drain.","created_at":"2026-07-11T21:30:34.894847Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-09T23:25:58.707547Z","updated_at":"2026-07-11T21:30:34.935289Z","created_by":"jeremy","status_since":"2026-07-11T21:30:34.935289Z"},"type":"card"} {"data":{"id":"card_1af1fd87334f4e078b02e0ab14649446","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Sprint 07-11 P4a: golden-render harness + TypeTheme resolution unification (byte-identical gate)","status":"done","fields":{"acceptance":"Golden snapshots in CI; type-themed boards byte-identical through unification; CardAccentField resolved; precedence chain + define/activate split normative in STYLE-FIELD.md.","branch":"feat/golden-render","description":"Phase 4 of docs/plans/sprint-2026-07-11.md, steps 1-3 — the safety net and refactor BEFORE the style_field feature card.\n\nDO:\n1) Golden-render test harness FIRST — none exists in internal/httpapi today; budgeted new infra, kept small: render the type-themed demo boards through the template pipeline and snapshot the HTML. Capture current output before touching any render path. FALLBACK if the harness proves expensive: snapshot only the type-themed demo boards — enough to catch a re-skin.\n2) Resolve CardAccentField (internal/core/types.go:173, zero consumers; deletion is safe because unknown JSON keys are ignored) — delete it or fold into StyleField. Same commit: write the NORMATIVE precedence text into docs/design/STYLE-FIELD.md — option-theme over type-theme over CSS-default merge order, and the define/activate split (OptionThemes declared on FieldDef, a board OPTS IN via BoardPresentation.StyleField; same card can legitimately render differently on two boards; presentation metadata never branches write paths per CORE-BOUNDARIES 3.2).\n3) Unify the two divergent TypeTheme resolution paths (precomputed cardView vs live typeTheme template-func in card_head) into ONE precomputed per-card function threaded through ViewData — mandatory anyway: style-field theming is per-card-value and cannot ride the id-keyed map. Golden tests from step 1 must pass unchanged.\n\nACCEPTANCE: golden snapshots in CI; existing type-themed boards byte-identical through the unification (no accidental re-skin); CardAccentField resolved; precedence + define/activate are contract text in STYLE-FIELD.md. go test ./internal/httpapi ./internal/core green.","design_ref":"docs/design/STYLE-FIELD.md; internal/httpapi/render.go; templates/card_head","surface":"board"},"links":[{"type_id":"parent","target":"card_1b5289099221445090a54893e379106f","note":"sprint 07-11 phase card","created_by":"claude","created_at":"2026-07-11T21:07:50.961516Z"},{"type_id":"depends-on","target":"card_9174c0ea607a40ef8ebe6cb3b75f2932","created_by":"claude","created_at":"2026-07-11T21:07:51.174799Z"}],"comments":[{"id":"cm_299dc39fdd2b44e1","author":"claude","body":"P4a done 2026-07-11: golden-render harness (testdata/golden); CardAccentField deleted → StyleField/OptionThemes schema prep; TypeTheme unified via resolveCardTheme → ViewData.CardTheme. go test ./internal/httpapi ./internal/core green.","created_at":"2026-07-11T21:43:40.14992Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-11T21:07:50.762417Z","updated_at":"2026-07-11T21:43:40.156699Z","created_by":"claude","status_since":"2026-07-11T21:43:40.156699Z"},"type":"card"} {"data":{"id":"card_1b5289099221445090a54893e379106f","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 2026-07-11: make the composition substrate real (tracker)","status":"done","fields":{"branch":"main","description":"SPRINT TRACKER — plan doc: docs/plans/sprint-2026-07-11.md. The board IS the plan: card bodies carry the executable detail; phase exits update statuses on the card IDs below.\n\nPITCH: Rotate the center of mass back onto the differentiating substrate — the four transports as honest versioned contracts, and the extensions-over-plugins bet. Drain review + retire doc drift first (P0, hard gate), make MCP a first-class transport + raise config validation rigor (P2), then in PARALLEL: live definition authoring with in-browser failure feedback (P3) and enum-driven board legibility (P4), finishing with the north star: a supervised 'service' extension kind (P5). Every schema change this sprint is additive omitempty definition JSON — no SQLite migration, no snapshot-format change.\n\nPHASES → CARDS:\nP0a card_9174c0ea607a40ef8ebe6cb3b75f2932 (review-pile drain, per-card verify)\nP0b card_bac182247e6144f7942c8058787e5619 (doc-truth sync: INTEGRATOR-REFERENCE / SPEC-API-SURFACE / mcp README)\nP2a card_c3c0fac556144a54a3be69d012e93b75 (six MCP tools + parity allowlist test)\nP2b card_8b25cef932d84aa7b90ede8a5921e946 (config semantic validation, warn-tier)\nP3a card_524c5758b3b34e4d814142130cab9eca (hook-supervisor stale-generation bug — cherry-picks forward)\nP3b card_ec61b093d1a444dcb5c915518de5c67f (--watch poller + failure banner + reload contract)\nP4a card_1af1fd87334f4e078b02e0ab14649446 (golden-render harness + TypeTheme unification)\nP4b card_4bdd35866cc54e4e844b5f6d96da6a8e (style_field: color+icon per enum value)\nP5a card_abb13ae015a843f4b00b52a65b81fc1c (lifecycle ADR: RestartPolicy x Autostart x kinds)\nP5b card_23c7070a75114ba9ad14306803293f4f (service supervisor: ready gate, backoff, drain)\nP5c card_eecf0e0303a24b328b9993d547c4df65 (reconcile-on-reload: identity key + decision table)\nCLOSE card_c08a6ead73a04837995484a5ba9177d7 (walkthrough + round-trip + board sync)\nCARRY-OVER card_f20e87d5da85410cbcf18b4ccda9dd5a (board-create idempotency — unfinished 07-10 work; finish narrowly or close-blocker)\n\nSTANDING RULES: P0 gates all M-sized work. No batch-flipping review cards — human git show / CI verification per card. Every schema change stays additive omitempty definition JSON. Phase exits update card statuses. Sprint close exports + commits the JSONL once.\n\nOUT OF SCOPE (deliberate): click-accent-to-filter (follow-up card against shipped style_field) · token-mode auth (AUTH.md frozen; host-shaped until multi-tenant is scheduled) · ACL/permissions in kernel · expose field semantics · outbox/webhooks in-core (future home: an external supervised service) · hook re-declaration on reload (rides P5 reconcile design or follow-up) · DisallowUnknownFields (key leniency is what keeps this sprint's schema changes safe) · orphaned View type · GH-Pages docs site · enum theming beyond one style_field per board · fsnotify.\n\nRISKS: stricter validation is a load-contract change — warn-tier is the pressure valve; validate demo-workspace + known definitions before any rejection ships. Stale-generation bug is live and watcher-amplified — P3a cherry-picks forward even if P3 slips. Watcher + supervisor tests are the flake-prone surfaces — injectable clock / fake child binaries / mac+linux budget are designed in. P5 has the most unknowns at the end of the critical path — ADR lands early; fallback is supervisor-without-reconcile (documented restart-on-reload). Golden harness fallback: snapshot only type-themed demo boards. Pipeline is review-bound: P0 is a hard gate and per-card verification keeps it honest.","kind":"feature"},"comments":[{"id":"cm_4ab53220bd9b43df","author":"claude","body":"Sprint 07-11 tracker close: all phase cards P0a–P5c + close + carry-over f20e87d5 marked done. Remaining review-column holds from P0a stay as documented unmet criteria (not rubber-stamped).","created_at":"2026-07-11T21:58:15.212523Z","edited_at":"0001-01-01T00:00:00Z"}],"version":4,"created_at":"2026-07-11T21:07:50.845459Z","updated_at":"2026-07-12T02:45:42.968983Z","created_by":"claude","status_since":"2026-07-11T21:58:15.21718Z"},"type":"card"} -{"data":{"id":"card_1c877e6ca3e04a24bdd3d2ff90286a84","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"TakeNext: make owner registration consistent","status":"backlog","fields":{"branch":"fix/take-next-owner-validation","description":"Review found an identity-contract inconsistency: owner PATCH and claim reject unregistered users, while TakeNext writes assign_to (or the actor fallback) directly through the atomic store path without the same lookup. Decide and enforce one contract across all ownership mutations. The likely fix is to validate TakeNext ownership and update runnable workers such as review-bot to register once before claiming; preserve arbitrary unregistered actor strings for non-ownership writes. Add service, HTTP, CLI, and extension integration coverage for registered and unknown owners, and synchronize the actor-model docs.","kind":"bug"},"comments":[{"id":"cm_c63fd94283c84501","author":"claude","body":"Re-confirmed against HEAD (2026-07-26 architecture audit) — this card is still accurate and still open.\n\nExact location: `Service.TakeNext` (internal/core/service.go:1465) defaults `assignTo` to the actor and passes it to the claim path with no registry lookup, while `PATCH owner` and `claim` both return `unknown_user` (422). So two of three ownership paths enforce the registry and the third does not.\n\nWorth noting for prioritisation: docs/reference/implementation-status.md §5 has described this as 'an implementation inconsistency, not an authentication boundary' across several audit cycles — it has been *documented* rather than *decided* for months. The decision (enforce everywhere, or exempt take-next deliberately as the ephemeral-worker path) is the actual deliverable; the code change either way is small.\n\nI filed a duplicate of this (`319d6c8a`) during the audit — my board search missed this card. The duplicate is deleted; this card is the one to work. Its scope (validate ownership, update review-bot to register once before claiming, service/HTTP/CLI/extension coverage, sync the actor-model docs) is better than what I wrote.","created_at":"2026-07-26T09:44:27.850306Z","edited_at":"0001-01-01T00:00:00Z"}],"version":2,"created_at":"2026-07-23T21:56:56.472589Z","updated_at":"2026-07-26T09:44:27.850306Z","created_by":"cursor-review","status_since":"2026-07-23T21:56:56.472589Z"},"type":"card"} +{"data":{"id":"card_1c877e6ca3e04a24bdd3d2ff90286a84","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"TakeNext: make owner registration consistent","status":"backlog","fields":{"branch":"fix/take-next-owner-validation","description":"Review found an identity-contract inconsistency: owner PATCH and claim reject unregistered users, while TakeNext writes assign_to (or the actor fallback) directly through the atomic store path without the same lookup. Decide and enforce one contract across all ownership mutations. The likely fix is to validate TakeNext ownership and update runnable workers such as review-bot to register once before claiming; preserve arbitrary unregistered actor strings for non-ownership writes. Add service, HTTP, CLI, and extension integration coverage for registered and unknown owners, and synchronize the actor-model docs.","kind":"bug"},"comments":[{"id":"cm_c63fd94283c84501","author":"claude","body":"Re-confirmed against HEAD (2026-07-26 architecture audit) — this card is still accurate and still open.\n\nExact location: `Service.TakeNext` (internal/core/service.go:1465) defaults `assignTo` to the actor and passes it to the claim path with no registry lookup, while `PATCH owner` and `claim` both return `unknown_user` (422). So two of three ownership paths enforce the registry and the third does not.\n\nWorth noting for prioritisation: docs/reference/implementation-status.md §5 has described this as 'an implementation inconsistency, not an authentication boundary' across several audit cycles — it has been *documented* rather than *decided* for months. The decision (enforce everywhere, or exempt take-next deliberately as the ephemeral-worker path) is the actual deliverable; the code change either way is small.\n\nI filed a duplicate of this (`319d6c8a`) during the audit — my board search missed this card. The duplicate is deleted; this card is the one to work. Its scope (validate ownership, update review-bot to register once before claiming, service/HTTP/CLI/extension coverage, sync the actor-model docs) is better than what I wrote.","created_at":"2026-07-26T09:44:27.850306Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_c11257fe88cf4a64","author":"claude","body":"Premise update from 8920121 (branch fix/happy-path-simulation): checkUserExists now accepts users declared in definitions (workspace.users or the default_user seed) as well as store-registered ones, because a fresh 'cards init' workspace's default user could not claim its own cards. When this card unifies TakeNext's owner path, route it through checkUserExists so the contract is the same everywhere.","created_at":"2026-09-05T13:25:53.780315Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-07-23T21:56:56.472589Z","updated_at":"2026-09-05T13:25:53.780315Z","created_by":"cursor-review","status_since":"2026-07-23T21:56:56.472589Z"},"type":"card"} {"data":{"id":"card_1f9bf7c4b8d24c9b9f1be88f0a6f3451","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint P3 — Theme foundations (stamp seam, validator, contract)","status":"done","fields":{"branch":"httpapi/theme-foundations","description":"Foundation phase. Land precursors that make workspace themes safe.\n\nSteps:\n- [x] Move stylesheet cache stamp from package-level var (internal/httpapi/server.go:30 assetStamp, init once per PROCESS) onto per-generation Server instance state so it rotates on reload. Prove rotation with a cmd/cards integration test.\n- [x] Build a tokenizer/parse-based CSS validator w/ stated threat model: reject scope-escape (balanced 'html[data-theme=x]{} } body{...}'), @import, remote url(). Error payload names theme/file/line/violation. Test against adversarial strings.\n- [x] Document precedence chain, Board.Theme layering, back-compat, workspace-font policy in DESIGN.md + docs/design/THEMES.md.\n\nDemo: feed validator a brace-balanced scope-escaping string -> rejected w/ file/line/violation; reload dev server -> /ui/style.css serves under a fresh stamp.\nExit: stamp on Server instance state (package var removed) + rotation proven; validator rejects the three classes; contract documented; internal/config coverage >51.3%; go test ./... green."},"owner":"jeremy","tags":["feature"],"links":[{"type_id":"depends-on","target":"card_519e1688f06646ff9817268b2ed0c9a7","created_by":"jeremy","created_at":"2026-07-07T20:52:32.163527Z"}],"comments":[{"id":"cm_d7ede75e322e4a37","author":"jeremy","body":"DONE (commit 650f4d3). Stamp seam: assetStamp moved to per-generation httpapi.Server state (package var removed); rotation proven by cmd/cards/TestReloadRotatesStylesheetStamp. Validator: internal/themecss.Validate — braces + scope (catches balanced scope-escape) + @import + remote url(), violations carry {theme,file,line,rule,message}; 13 adversarial tests. Docs: THEMES.md 'Load-time contract' (precedence incl. planned board.presentation.theme layer, Board.Theme two-hook layering, back-compat, font-manifest policy) + DESIGN.md pointer. go test ./... green (14 pkgs), vet clean.\n\nCAVEAT on one exit criterion: 'internal/config coverage >51.3%' does not apply to P3 — P3 adds no config code (the validator is its own package themecss, fully tested). The loader lands in internal/config in P4; that coverage bump belongs there. Not manufacturing config tests for code that doesn't exist yet.","created_at":"2026-07-08T07:46:54.939023Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_8f11b97db2f149d8","author":"jeremy","body":"STATUS CHECK 2026-07-10: description checklist is fully [x]. Code present: themecss/validate.go, per-generation stamp, tests. Candidate for done after a quick acceptance pass / dogfood — no open steps listed.","created_at":"2026-07-09T23:25:58.682161Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-07T20:50:49.789182Z","updated_at":"2026-07-09T23:26:32.362624Z","created_by":"jeremy","status_since":"2026-07-09T23:26:32.362624Z"},"type":"card"} {"data":{"id":"card_21bbb5133f344e0da88701175d17df7f","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P2 — htmx removed, X-Cards-Partial header","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in e783092. htmx issued zero requests (no hx-* ever existed): dead listeners deleted (all real paths call toast() directly), CDN tag removed, HX-Request→X-Cards-Partial renamed atomically (1 Go reader wantsPartial + 4 JS senders), vestigial X-Modal dropped, stale 'htmx UI' comments fixed. Guard TestNoHTMXResidue. Browser-verified: modal fragment loads, SSE refresh, toasts. Found+carded pre-existing bug card_096261c37 (stale save toasts 'Saved').","surface":"board"},"owner":"jeremy","tags":["feature"],"version":5,"created_at":"2026-07-09T08:40:28.037007Z","updated_at":"2026-07-09T08:40:28.597377Z","created_by":"jeremy","status_since":"2026-07-09T08:40:28.597377Z"},"type":"card"} {"data":{"id":"card_21ef721313044726a8ec9907af6cde1d","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Events: board-scoped event model (scope card|board, nullable card_id, board_id)","status":"done","fields":{"branch":"plan/integration","description":"events.card_id is currently NOT NULL, so board-level condition events (lane_drained, wip_exceeded) have no home. Add a scope field (card|board), make card_id nullable, record board_id for board-scoped events. Prereq for condition events. Keep card-scoped events unchanged; update SSE/feed filtering + replay."},"links":[{"type_id":"related","target":"card_bb0552e91e754aa186ebff55de3659c9","note":"part of the integration contract","created_by":"jeremy","created_at":"2026-06-30T17:09:09.822315815Z"},{"type_id":"depends-on","target":"card_c53b453533b041c3b752ac5065c52058","note":"needs seam Step 1 (call sites through commitCard) first","created_by":"local-dev","created_at":"2026-07-01T15:15:34.021476065Z"},{"type_id":"related","target":"card_97b29005221041c5b8dcc18cb937bbdb","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.040632369Z"},{"type_id":"related","target":"card_f0569b62219740b6813acd4e5d6b5f2a","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.057256887Z"},{"type_id":"related","target":"card_6de978482b9e49049561862f21ba087f","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.074341779Z"}],"comments":[{"id":"cm_35d811919178405d","author":"local-dev","body":"Split into three small testable slices per docs/EVENTS.md §12 Step 2: card_97b29005221041c5b8dcc18cb937bbdb (schema/migration only), card_f0569b62219740b6813acd4e5d6b5f2a (BoardEvent + one real usage), card_6de978482b9e49049561862f21ba087f (bus/feed filtering). This card now tracks the umbrella; close it when all three land.","created_at":"2026-07-01T15:15:34.003866448Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_3350a47c3aca44c4","author":"local-dev","body":"Closed: decomposed into slices per docs/EVENTS.md §12 Step 2 — card_97b2… (2a schema/migration), card_f0569… (2b BoardEvent constructor), card_6de9… (2c Bus/Feed filtering by scope+board_id). Implementation lives in the slices (still backlog); this umbrella is closed as a tracking item, NOT built.","created_at":"2026-07-01T19:44:55.105765176Z","edited_at":"0001-01-01T00:00:00Z"}],"version":15,"created_at":"2026-06-30T17:09:09.74993445Z","updated_at":"2026-07-01T19:44:55.105765176Z","created_by":"jeremy","status_since":"2026-07-01T19:44:55.104095193Z"},"type":"card"} @@ -123,6 +123,7 @@ {"data":{"id":"card_7dbed16e18e841e3bde4d35b3488af37","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P6 — chip multiselect (multi enum/user) + policy-aware tag chips","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in 0212631 (this card's own platforms field was set with the new chip control's data shape). multiSelect: chips over the native , primary + keyboard-reachable; empty field advertises 'Choose a file…') with modal-scoped drag-drop that can't collide with the board column-move gesture. Full state machine (idle/dragover/uploading/success/error): client-side >32MiB pre-check, server 413 (artifact_too_large — fixed the MaxBytesReader 500), and version_conflict rendered 'card changed — reload'. Raw-body POST with ?version=N. Shared reloadModal(cardID) helper (refetch-failure + closed/switched-modal guards) — reused by the deferred comments/entries sprint. Board card shows uploaded artifacts as 240px-capped thumbnails / download chips, live via artifact_added SSE. VERIFIED END-TO-END IN A REAL BROWSER: upload persisted (image/png, sha256, version bumped), modal reloaded with the thumbnail, board card rendered it via the confined /v1/artifacts route, and a 40MiB file was blocked client-side with a worded message and no network call. Tests: upload round-trip + stale-409 + oversize-413 + board-render, all on the t.TempDir harness. go test -race ./... green (13 pkgs). Deferred per plan: the rest of the UI cluster + landing the card__secondary theme hook (follow-on sprint, on top of this).","created_at":"2026-07-06T04:05:11.04655Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_9cac75f908bf416f","author":"local-dev","body":"Sprint 2026-07-06 close-out: final gate green — go build ./..., go test -race ./... (all packages, 0 failures), go vet clean. Committed + pushed to origin/main. Moving review -> done. P4 shipped as 465798a; browser-verified end-to-end.","created_at":"2026-07-06T08:52:44.927802Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-06T02:51:16.769657Z","updated_at":"2026-07-06T08:52:44.963062Z","created_by":"local-dev","status_since":"2026-07-06T08:52:44.963062Z"},"type":"card"} {"data":{"id":"card_82d71fdf05524844a622e16ce0ea80b0","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 07-10 P1a: docs + audit reconciliation (htmx→Alpine, INTEGRATOR-REFERENCE)","status":"done","fields":{"branch":"frontend-rebuild","description":"Phase 1a of docs/plans/sprint-2026-07-10.md — make the entry docs tell the truth about the shipped UI 2.0 stack.\n\nSCOPE:\n1) Fix htmx→Alpine prose drift: CLAUDE.md (~lines 20, 71) and docs/architecture/DESIGN.md lede (~line 5) still describe the web UI as htmx; rebuild Phase 2 (e783092) removed it. Leave the historical past-tense comment in render.go alone.\n GATE (verified green 2026-07-10): grep -rEn 'hx-(get|post|put|delete|swap|target|trigger)' internal/httpapi/templates/ returns zero — so this is a prose fix, not a code change in disguise.\n2) INTEGRATOR-REFERENCE.md audit with a verifiable artifact, not a self-attested marker: for each of the 8 sections, list the source paths it describes and run git log --oneline b8dda45..HEAD -- (b8dda45 = last verified, rebuild Phase 3). Paste the per-section commit list into the doc's changelog stanza. Sections with commits but no doc change get an explicit 'reviewed, no change needed ' line.\n\nACCEPTANCE: zero present-tense htmx references in CLAUDE.md / DESIGN.md; INTEGRATOR-REFERENCE changelog carries per-section git-log evidence through HEAD for all 8 sections."},"links":[{"type_id":"parent","target":"card_3f225267e3724f9f8d802772731d3316","note":"sprint 07-10 phase card","created_by":"claude","created_at":"2026-07-10T02:07:37.362008Z"}],"comments":[{"id":"cm_89c3f7721fc945b8","author":"claude","body":"Done via PR #20 (https://github.com/somebox/cards/pull/20, branch docs/p1a-htmx-alpine-audit-reconcile → main). Docs only, no code.\n1) htmx→Alpine: fixed CLAUDE.md:20, CLAUDE.md:71, DESIGN.md:5 (lede contradicted its own §Interactivity layer). render.go:722 historical comment left alone. Gate held: hx-* in templates = zero. Acceptance grep clean.\n2) INTEGRATOR-REFERENCE: added an 'Audit changelog' section with per-section git-log evidence over 8d043ea..HEAD (8d043ea = rebuild Phase 3, the doc's prior verification point b8dda45 after the branch was rebased into main). Finding: the Phase 4–10 commits under this doc's purview were all /ui reference-client (DESIGN.md's domain — §2 has zero /ui routes) + one SSE keepalive (liveness). /v1 API, MCP, events, actor, schema, extensions unchanged (api.go/filters.go no commits in range). All 8 sections carry a reviewed line.","created_at":"2026-07-10T14:46:19.852898Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_b409a161b1a94d35","author":"claude","body":"P0a verify 2026-07-11: flipped to done. Verifying commit 87acf1f (PR #20). Acceptance: htmx→Alpine prose fixed in CLAUDE.md/DESIGN.md; INTEGRATOR-REFERENCE audit changelog present; hx-* in templates = zero.","created_at":"2026-07-11T21:30:34.511879Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-10T02:01:41.345569Z","updated_at":"2026-07-11T21:30:34.550751Z","created_by":"claude","status_since":"2026-07-11T21:30:34.550751Z"},"type":"card"} {"data":{"id":"card_857194eac0b94037b7502cef2cbe283d","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"pi-cards: write surface — create/update/comment/claim/release/take_next + config (P1)","status":"done","fields":{"branch":"feat/write-tools","description":"pi-cards series 4/8 — write surface + config file (P1). Spec §5.5, §6.3 rows 4-9, §7. Blocked by card 3 (read surface).\n\nRead surface is live; this completes P1: the LLM can round-trip a card from chat with full write honesty (§5.5). Also lands `.pi/cards.json` / `~/.pi/agent/cards.json` config loading (§7) and auto-registration of the agent actor.\n\nScope:\n- `src/config.ts`: read user then project `cards.json`, project wins; env overrides per §7 table; defaults `autoExport:\"off\"`, `worklogOnTurn:false`, `work.batchLimit:3`, `work.autoDone:false`. (pi has no per-extension settings API — read the JSON files directly.)\n- `src/tools-write.ts`: `cards_create` (`type_id`, `title`, `fields`, `tags`, `dry_run`; validation errors returned verbatim incl. `valid_options`), `cards_update` (fields/status/owner/tags, `version` + §5.5 retry, `dry_run`), `cards_comment` (markdown body, actor attributed), `cards_append_entry` (repeating fields), `cards_claim`/`cards_release` (409 surfaces current owner), `cards_take_next` (`status`/`type_id`/`board_id` filters; on `{card:null}` re-issue once to disambiguate race vs empty per api-surface §11).\n- First write per session auto-registers the actor: `POST /v1/users` kind `agent` (HTTP) / `cards users register` (CLI), ignoring \"already exists\" (§5.4).\n- Tool results return the server's updated card, never assumed local state.\n\nImplementation notes:\n- `dry_run` passes through; CLI backend forwards `--dry-run` where the verb supports it (confirmed on create/patch only — degrade loudly on other verbs, per spec-gap finding).\n- CLI release uses the `patch --owner \"\"` mapping validated in card 2.\n- Board transitions are enforced server-side (engineering board `enforce_transitions:true`); do not pre-validate in the extension beyond surfacing the structured error.\n- CLI `claim` requires `--version N` — fetch version via `cards_get` first (preflight pattern).\n\nAcceptance:\n- `pi -e ./src/index.ts -p \"create a programming-task card titled X with branch feat/x, then move it to todo\"` against a throwaway copy of the fixture workspace: card created, appears in `cards list`, status todo.\n- Forced conflict: patch with a stale `version` → visible `version_conflict`, single auto-retry succeeds on fresh version (unit + integration test).\n- `cards_take_next --status todo` claims oldest unowned card as the resolved actor (actorSuffix applied); a second call gets the next card or null.\n- `node --test` + typecheck green; config precedence covered by a unit test (project beats user beats env-default).\n\nOut of scope: typed `cards_create_` generated tools (card 8); `cards_history`/`cards_breaches`/`cards_events` (phase-2); worklog automation (cards 6-7).","kind":"feature","work_log":[{"author":"pi","commit_hash":"d8ffad0","entry_id":"ent_6336d00b3af0497d","notes":"Write surface complete on feat/write-tools: 7 write tools (create/update/comment/append_entry/claim/release/take_next) with writeHandlers exported for unit tests, once-per-session actor registration, take_next re-issue-once on null, config withDefaults per §7. Fixture workspace id renamed to pi-cards-fixture (probe id-collision meant fixture runs near a live demo server picked HTTP mode toward the REAL board — now safely CLI). 28/28 tests green. Live LLM acceptance serverless against fixture: cards_create round-trip (8ac514f9), then cards_get→cards_claim landed owner=foz status=in_progress v2. Mid-card recovery: feat/write-tools was first cut from stale main (missing card-3 files) — ff main to feat/read-surface and recut; watch branch hygiene on multi-card runs. [timestamp corrected 2026-07-18T16:26Z: was stamped local(+0200) as UTC]","timestamp":"2026-07-18T17:20:00Z"}]},"owner":"pi","links":[{"type_id":"depends-on","target":"card_470df9d792e449b39615dff05a583bea","created_by":"pi","created_at":"2026-07-18T15:10:46.480318Z"}],"comments":[{"id":"cm_299a0091b6344860","author":"pi","body":"DONE (pi). feat/write-tools @ d8ffad0, 28/28 tests + tsc green. Acceptance: live LLM create round-trip + get→claim serverless against the fixture (owner=foz, in_progress); conflict auto-retry verified in integration+unit tests; take_next oldest-unowned + single re-issue pinned by unit tests; config precedence (project>user>defaults) unit-tested. cards_release on CLI throws unsupported_backend (surfaced verbatim to the LLM — see card 2 comment).","created_at":"2026-07-18T16:24:45.747963Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-18T15:10:27.38293Z","updated_at":"2026-07-18T16:28:25.870966Z","created_by":"pi","status_since":"2026-07-18T16:24:45.795307Z"},"type":"card"} From 31411c69468783509a33a367a0aa99c359d9a13e Mon Sep 17 00:00:00 2001 From: Jeremy Seitz <7750+somebox@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:12:22 +0200 Subject: [PATCH 3/4] cards: close the comment-alias and happy-path simulation cards after review Co-Authored-By: Claude Fable 5.1 --- .cards/backlog.jsonl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cards/backlog.jsonl b/.cards/backlog.jsonl index d9c8867..7f50cc8 100644 --- a/.cards/backlog.jsonl +++ b/.cards/backlog.jsonl @@ -20,7 +20,7 @@ {"data":{"id":"card_0a642f56db8d42529654ffd27008631b","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"UI: harden $store.live reconnect (single socket, generation guard)","status":"done","fields":{"a11y":"If live status is exposed, keep non-color text/status for reconnecting/down (optional UI).","acceptance":"Never more than one EventSource to /v1/events/stream per tab; reconnect resumes with since=lastId; no silent total death of live updates without user-visible hint after sustained failure.","branch":"ui/live-store-hardening","description":"Sprint 07-10 P4 must-ship #4 (tracker card_3f225267). Follow-on to P9 filter-stall fix and closed card 60f2e6a8. DEPENDS ON: thin harness card_0391870a (the gen-guard invariant needs a Node unit test to live in).\n\nRisks still present:\n- onerror schedules open() with overlapping timers if errors cluster\n- no generation token: stale EventSource handlers can deliver after replace\n- live handler exceptions swallowed (empty catch) — swapBoard failures go silent\n- handlers array not idempotent across remounts if stop() not paired\n\nFIX:\n1) WRITTEN CONTRACT first: code comment above $store.live specifying exactly what open()/stop()/onerror/onmessage do w.r.t. the generation counter — stop() closes the ES, bumps the generation, clears pending delivery (not just a token check in onerror). Do NOT change on/off signatures.\n2) reconnectTimer cleared on stop/open; generation++ per open; ignore mismatched es.\n3) assert single es; close previous before assign.\n4) log/count handler errors; toast after N consecutive swap failures.\n5) Decision logic in a pure shouldDeliver(gen, currentGen)-style helper, unit-tested in the Node harness.\n\nOUT OF SCOPE: extending TestSSEKeepalive — a server-side test cannot pin a client-side invariant; the server test guarantees keepalive only.\n\nVERIFY: DevTools — one EventSource across 20 filter toggles; kill server briefly; one reconnect backoff; no duplicate board thrash. DUAL-CONSUMER check recorded here: board tab + /ui/breaches tab, server kill-and-restart, each event delivers once, no stale ES delivers after stop().\n\nACC: never more than one EventSource to /v1/events/stream per tab; reconnect resumes with since=lastId; Node test pins the generation invariant; no silent death of live updates without a user-visible hint after sustained failure.\n\nFOLDED IN from the PR #18 Copilot review (2026-07-10) — two latent handler-lifecycle bugs, benign today because boardPage lives on the persistent .board-view root (init runs once), but exactly the reconnect/remount hazards this card exists to close:\n- $store.live on() is NOT idempotent: start() has a same-boardId/types guard, but on() always pushes a new closure. A re-init (parent initTree, or a future SPA nav path) would stack duplicate debouncedSwap calls per SSE event. FIX with the generation guard: clear/replace handlers on (re)start, or key delivery by generation so stale closures no-op.\n- boardPage.init() adds a window popstate listener with no teardown. Same persistent-root reason it is harmless now; add removal via Alpine $destroy/destroy() so it cannot stack if navigation ever goes through a fragment path.\nBoth should be covered by the written open/stop/onerror/onmessage + handler-lifecycle contract this card already calls for.","design_ref":"components.js $store.live; sse.go keepalive","platforms":["desktop"],"surface":"board"},"tags":["bug"],"links":[{"type_id":"parent","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.913461Z"},{"type_id":"related","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.914596Z"},{"type_id":"depends-on","target":"card_0391870affcb460a8d252e928b635024","note":"gen-guard unit test needs the Node harness","created_by":"claude","created_at":"2026-07-10T02:08:01.65465Z"}],"comments":[{"id":"cm_aef28bbf30ea4f4e","author":"claude","body":"Done + committed to main. $store.live hardened with a generation counter (bumped on open/stop; superseded ES self-silence via shouldDeliver), tracked reconnectTimer, idempotent on()/off(), and destroy() cleanup on boardPage+breachesPage (fixes the two #18-review findings: on() dedup + popstate cleanup). Also fixed a pre-existing backoff bug — resetting backoff in open() defeated the ramp; moved to es.onopen so it truly ramps 500ms→8s (verified: ~4 attempts over 6s down, was ~35). Pure decisions (shouldDeliver/nextBackoff/maxEventId) extracted to helpers.js and unit-tested (tests/js/live.test.cjs). Verified in-browser: connect→kill→restart→delivery, handler count stays 1 throughout, card appears exactly once, backoff ramps then resets. The dropped TestSSEKeepalive-extension idea stands (server test can't pin client reconnect). Paired with the JS harness card_0391870a.","created_at":"2026-07-11T12:49:28.177124Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_acae572d33e14c2c","author":"claude","body":"P0a verify 2026-07-11: flipped to done. Verifying commit 8447cb7. shouldDeliver / generation guard + live.test.cjs pin single-socket reconnect behavior.","created_at":"2026-07-11T21:30:34.783482Z","edited_at":"0001-01-01T00:00:00Z"}],"version":11,"created_at":"2026-07-09T23:25:58.7087Z","updated_at":"2026-07-11T21:30:34.823428Z","created_by":"jeremy","status_since":"2026-07-11T21:30:34.823428Z"},"type":"card"} {"data":{"id":"card_0c59f1106fdf4641b9c4eb15be3d682c","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Events: normalize definition reload event envelopes","status":"backlog","fields":{"branch":"fix/reload-event-contract","description":"Definition reload success/failure currently publish raw Event literals from cmd/cards/reload.go with board_id but without the normal board-event scope/version constructor path. Define their live bus-only envelope explicitly, add typed constructors and fixture/stream tests, remove raw literals, and align event docs without making reload notifications durable unless that is a deliberate contract change.","kind":"bug"},"links":[{"type_id":"related","target":"card_ec61b093d1a444dcb5c915518de5c67f","note":"Contract consistency follow-up from event-doc review","created_by":"claude","created_at":"2026-07-23T21:48:41.143928Z"}],"version":2,"created_at":"2026-07-23T21:48:41.114589Z","updated_at":"2026-07-23T21:48:41.143928Z","created_by":"claude","status_since":"2026-07-23T21:48:41.114589Z"},"type":"card"} {"data":{"id":"card_0f002686730844e09742a5dd00bebefa","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Review minors batch: watch/reload startup races + suppression window; date min/max author UX; conflict-shape consistency","status":"done","fields":{"branch":"fix/review-minors-0711","description":"Batch of small correctness/polish items from the sprint 07-11 post-merge review (b23aff8) — reload/watch startup + config authoring UX. Each is a contained fix; land as one branch of small commits.\n\nRELOAD/WATCH (flagged independently by two reviewers):\n1) setAfterReload ordering race: cmd/cards/serve.go:126 starts the watcher before setAfterReload at :160; plain field write (reload.go:81-83) read by notifyReload — no happens-before. Failure: missed service reconcile on a first-tick reload under --watch --run-extensions. FIX: install before spawning the watcher (one-line move) or make the field atomic.\n2) Watcher not awaited on shutdown: watchCancel deferred but Run not joined (contrast supervisor's supDone, serve.go:163-173); SIGINT during scanOnce can race reloadLocked against store close. FIX: done channel like the supervisor's.\n3) Self-write suppression window swallows a concurrent external edit: while selfWriteGate.active>0, scanOnce absorbs ANY fingerprint and advances lastFP (watch.go:114-118) — an external save during the create-board handler window is silently dropped until the next change. FIX: during active window skip WITHOUT advancing lastFP; only skipFP should advance it.\n4) Stale skipFP can absorb a later legitimate change (watch.go:207-211): cleared only on match; if the first post-create observation differs, it lingers and can swallow an exact-revert later. FIX: one-tick TTL.\n5) Timing-sensitive negatives noted by review: TestSupervisorUsesCurrentGenerationAfterSwap proves the negative with a fixed 500ms sleep; TestWatchSelfWriteSuppressed drains fan-out non-blockingly. No observed flake in 25x -race; tighten opportunistically.\n\nCONFIG AUTHORING UX:\n6) FieldDate min/max units undocumented + hostile errors: values are Unix-seconds floats sharing the number slots (core/types.go:47-48); no author-facing doc; '\"min\": \"2020-01-01\"' yields a raw json.Unmarshal error and runtime rejection prints 'below min 1.5778368e+09' — fails the project's own which-field/which-value/what-was-allowed bar. FIX: document units in DEVELOPER-REFERENCE-SCHEMA-AUTHORING.md; render dates as RFC3339 in the error; consider accepting date strings at load.\n7) Store-level version_conflict returns the ATTEMPTED card, not current DB state (internal/sqlite/sqlite.go:587-588) — matters for the versionless MCP tools under a genuine race. Pre-existing; fix while nearby.\n8) RemoveEntry rejects version 0 as validation error while AppendEntry/UpdateEntry treat omitted version as version_conflict — inconsistent shape; pick one.\n\nVERIFY: go test -race ./cmd/cards ./internal/hooks ./internal/sqlite; go vet.","kind":"bug"},"links":[{"type_id":"related","target":"card_ec61b093d1a444dcb5c915518de5c67f","note":"watch/reload minors from post-merge review","created_by":"claude","created_at":"2026-07-12T00:10:12.130729Z"},{"type_id":"related","target":"card_8b25cef932d84aa7b90ede8a5921e946","note":"date min/max author UX from post-merge review","created_by":"claude","created_at":"2026-07-12T00:10:12.144985Z"}],"comments":[{"id":"cm_b8afd2de79514bf6","author":"claude","body":"DONE on branch fix/review-minors-0711 (commit 9ba9f15, pushed). Items 1-4 (watch/reload): watcher starts after setAfterReload + joined on shutdown; mid-write scans skip without recording; skipFP exact-match with one-tick TTL. Item 6: FieldDate min/max errors render dates + units documented. Item 7: sqlite version_conflict attaches current DB row (same-tx read, attempted-state fallback). Item 8: AppendEntry/UpdateEntry align with RemoveEntry ('version is required' validation error on omitted version). Item 5 (test-robustness notes) left as-is — 25x -race stress showed no flake; noted for opportunistic tightening. go vet clean, full suite green, -race clean on cmd/cards+sqlite+core.","created_at":"2026-07-12T02:49:35.476299Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_ea5be1c802e9433a","author":"claude","body":"Merged to main in bf62070 (maintainer-directed merge 2026-07-12); full suite + vet green on merged main.","created_at":"2026-07-12T03:02:53.853075Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-12T00:10:12.053777Z","updated_at":"2026-07-12T03:02:53.888175Z","created_by":"claude","status_since":"2026-07-12T03:02:53.888175Z"},"type":"card"} -{"data":{"id":"card_0f7be7f689184a9bb2b84c3b1b50c7ff","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"CLI: ergonomic comment alias for agents (comment --body)","status":"review","fields":{"branch":"feat/cli-repeat-fields","description":"Agents routinely fail card updates because the CLI comment surface is easy to get wrong. NARROWED 2026-07-27 during sprint planning: this card is now the comment alias only. The repeating-field sugar that was bundled here is scoped out below, with the conditions under which it may return.\n\nObserved pain (re-verified 2026-07-27 against internal/cli/commands.go:460-476):\n- `cards comment --body B` fails. cmdComment switches on args[0] and requires the literal `add` or `edit`; a card id as the first token falls through to `unknown comment subcommand %q`.\n- Bare `cards comment` has no `--help` listing the subcommands, so the failure gives no route to the working form.\n\nIN SCOPE\n1. Accept `cards comment --body B` as an alias for `cards comment add --body B`. Pure argument parsing: no version semantics, no new HTTP call, same service path, same response.\n2. `cards comment --help` and the usage error both list `add` / `edit` and the alias form.\n3. Document the happy path in docs/reference/cli.md; add CLI tests mirroring the existing short-id parity tests. The alias AND both existing subcommand forms must pass — the alias is additive, not a replacement.\n\nOUT OF SCOPE (the re-scope)\n- Repeating-field sugar (`cards work-log add --notes ...`, or schema-aware flags replacing `--entry-json`). Split out deliberately: the alias above carries no version semantics, while the entry sugar carries two. If it returns as its own card it does so under both constraints:\n (a) flag-to-entry mapping is DERIVED from the card type's FieldDef item_fields, not hardcoded work_log semantics — no bespoke knowledge of `author`/`timestamp`/`commit_hash` outside the schema;\n (b) it does NOT silently fetch the current version. `cards append` keeps requiring `--version N`. The only sanctioned way to omit it is the same explicit `--if-match latest` opt-in landed by card 069ec1d1. A silent auto-GET under an ergonomics banner would be a second, CLI-only concurrency story alongside the real one — rejected.\n- Any change to the work_log item schema, the HTTP contract, or the MCP tool surface.\n\nWhy the split: the original card described one \"cheapest slice\" that was actually two items with different risk. The alias is a pure affordance. The entry sugar is where the CLI could quietly grow its own version semantics; it needs 069ec1d1 to land first so there is one opt-in to point at, not two."},"owner":"claude","comments":[{"id":"cm_bd4eb848d21f4c7d","author":"claude","body":"**Re-scoped 2026-07-27 (sprint planning, agent-cli-ergonomics).**\n\nNarrowed to the comment alias only. The card previously bundled two items with different risk profiles under one \"cheapest slice\": (A) `cards comment --body` as an alias for `comment add` — pure argument parsing, no version semantics; and (B) repeating-field entry sugar that would fill author/timestamp/commit and possibly omit `--version`. Item B is where the CLI could grow a second, CLI-only concurrency story next to the real one, so it is scoped out rather than shipped under an ergonomics banner.\n\nConditions for B to return as its own card are written into the description: flag→entry mapping derived from the type's FieldDef item_fields (not hardcoded work_log knowledge), and no silent version fetch — the only sanctioned omission is the explicit `--if-match latest` opt-in from card 069ec1d1, which must land first.\n\nRe-verified this session: commands.go:460-476 (comment subcommand switch), commands.go:356-370 (append requires --version + --entry-json).\n\nSprint position: item 1 of 3, ships first — no core changes, and it establishes the CLI-test pattern the other two reuse.","created_at":"2026-07-27T09:49:26.507992Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_30daa114d9194192","author":"claude","body":"**Line-citation refinement (2026-08-07).** The description cites commands.go:460-476 for the comment subcommand switch. More precisely: `cmdComment` starts at **:460**, the `add` and `edit` cases are at **:467** and **:481**, and the actual failure an agent hits — `unknown comment subcommand %q` — returns at **:496**. That last line is the one the alias makes unreachable for a bare card id.","created_at":"2026-08-07T06:10:43.854541Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_80050459ddc74e25","author":"claude","body":"Shipped the comment alias. `cards comment --body B` is now the same POST as `comment add`; `comment add` and `comment edit` still work. `cards comment --help` (and the empty-args usage error) list add, edit, and the alias. No version semantics, no HTTP contract change.\n\nTests: TestCommentAliasAndExistingForms (add + alias + flag-before-id + edit), TestCommentHelpAndUsageListTheAlias, TestCLICommentAlias_ShortIDResolves. `go test ./internal/cli ./cmd/cards` pass. Live: `cards comment --help` exits 0 with the three forms.\n\nDocumented in docs/reference/cli.md, cards --help, and the installed skill's cli-reference / SKILL.md recording section.","created_at":"2026-09-02T18:10:21.503317Z","edited_at":"0001-01-01T00:00:00Z"}],"version":11,"created_at":"2026-07-24T01:34:52.27474Z","updated_at":"2026-09-02T18:10:21.733033Z","created_by":"local-dev","status_since":"2026-09-02T18:10:21.733033Z"},"type":"card"} +{"data":{"id":"card_0f7be7f689184a9bb2b84c3b1b50c7ff","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"CLI: ergonomic comment alias for agents (comment --body)","status":"done","fields":{"branch":"feat/cli-repeat-fields","description":"Agents routinely fail card updates because the CLI comment surface is easy to get wrong. NARROWED 2026-07-27 during sprint planning: this card is now the comment alias only. The repeating-field sugar that was bundled here is scoped out below, with the conditions under which it may return.\n\nObserved pain (re-verified 2026-07-27 against internal/cli/commands.go:460-476):\n- `cards comment --body B` fails. cmdComment switches on args[0] and requires the literal `add` or `edit`; a card id as the first token falls through to `unknown comment subcommand %q`.\n- Bare `cards comment` has no `--help` listing the subcommands, so the failure gives no route to the working form.\n\nIN SCOPE\n1. Accept `cards comment --body B` as an alias for `cards comment add --body B`. Pure argument parsing: no version semantics, no new HTTP call, same service path, same response.\n2. `cards comment --help` and the usage error both list `add` / `edit` and the alias form.\n3. Document the happy path in docs/reference/cli.md; add CLI tests mirroring the existing short-id parity tests. The alias AND both existing subcommand forms must pass — the alias is additive, not a replacement.\n\nOUT OF SCOPE (the re-scope)\n- Repeating-field sugar (`cards work-log add --notes ...`, or schema-aware flags replacing `--entry-json`). Split out deliberately: the alias above carries no version semantics, while the entry sugar carries two. If it returns as its own card it does so under both constraints:\n (a) flag-to-entry mapping is DERIVED from the card type's FieldDef item_fields, not hardcoded work_log semantics — no bespoke knowledge of `author`/`timestamp`/`commit_hash` outside the schema;\n (b) it does NOT silently fetch the current version. `cards append` keeps requiring `--version N`. The only sanctioned way to omit it is the same explicit `--if-match latest` opt-in landed by card 069ec1d1. A silent auto-GET under an ergonomics banner would be a second, CLI-only concurrency story alongside the real one — rejected.\n- Any change to the work_log item schema, the HTTP contract, or the MCP tool surface.\n\nWhy the split: the original card described one \"cheapest slice\" that was actually two items with different risk. The alias is a pure affordance. The entry sugar is where the CLI could quietly grow its own version semantics; it needs 069ec1d1 to land first so there is one opt-in to point at, not two."},"owner":"claude","comments":[{"id":"cm_bd4eb848d21f4c7d","author":"claude","body":"**Re-scoped 2026-07-27 (sprint planning, agent-cli-ergonomics).**\n\nNarrowed to the comment alias only. The card previously bundled two items with different risk profiles under one \"cheapest slice\": (A) `cards comment --body` as an alias for `comment add` — pure argument parsing, no version semantics; and (B) repeating-field entry sugar that would fill author/timestamp/commit and possibly omit `--version`. Item B is where the CLI could grow a second, CLI-only concurrency story next to the real one, so it is scoped out rather than shipped under an ergonomics banner.\n\nConditions for B to return as its own card are written into the description: flag→entry mapping derived from the type's FieldDef item_fields (not hardcoded work_log knowledge), and no silent version fetch — the only sanctioned omission is the explicit `--if-match latest` opt-in from card 069ec1d1, which must land first.\n\nRe-verified this session: commands.go:460-476 (comment subcommand switch), commands.go:356-370 (append requires --version + --entry-json).\n\nSprint position: item 1 of 3, ships first — no core changes, and it establishes the CLI-test pattern the other two reuse.","created_at":"2026-07-27T09:49:26.507992Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_30daa114d9194192","author":"claude","body":"**Line-citation refinement (2026-08-07).** The description cites commands.go:460-476 for the comment subcommand switch. More precisely: `cmdComment` starts at **:460**, the `add` and `edit` cases are at **:467** and **:481**, and the actual failure an agent hits — `unknown comment subcommand %q` — returns at **:496**. That last line is the one the alias makes unreachable for a bare card id.","created_at":"2026-08-07T06:10:43.854541Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_80050459ddc74e25","author":"claude","body":"Shipped the comment alias. `cards comment --body B` is now the same POST as `comment add`; `comment add` and `comment edit` still work. `cards comment --help` (and the empty-args usage error) list add, edit, and the alias. No version semantics, no HTTP contract change.\n\nTests: TestCommentAliasAndExistingForms (add + alias + flag-before-id + edit), TestCommentHelpAndUsageListTheAlias, TestCLICommentAlias_ShortIDResolves. `go test ./internal/cli ./cmd/cards` pass. Live: `cards comment --help` exits 0 with the three forms.\n\nDocumented in docs/reference/cli.md, cards --help, and the installed skill's cli-reference / SKILL.md recording section.","created_at":"2026-09-02T18:10:21.503317Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_c904fce411c44818","author":"claude","body":"**What shipped:** `cards comment --body B` is accepted as an alias for `comment add`; `add` and `edit` unchanged, no version semantics. Landed on main in 90b838d. Reviewed 2026-09-06 and accepted.","created_at":"2026-09-06T18:11:52.510053Z","edited_at":"0001-01-01T00:00:00Z"}],"version":13,"created_at":"2026-07-24T01:34:52.27474Z","updated_at":"2026-09-06T18:11:52.541587Z","created_by":"local-dev","status_since":"2026-09-06T18:11:52.541587Z"},"type":"card"} {"data":{"id":"card_0fd1b9b7659749aa96014534cccbd530","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Sprint 07-22 P5 (stretch): schema UI — create-only","status":"backlog","fields":{"branch":"feat/schema-authoring-ui","description":"Sprint 07-22 Phase 5 — STRETCH ONLY (docs/plans/2026-07-22-sprint-plan.md).\n\nNot committed for sprint success. If capacity remains after P1–P4 committed work:\n\nCreate-new card-type from web UI only (no additive PATCH in stretch slice). POST /v1/card-types on reload seam (like POST /v1/boards), not inner httpapi router. Extract writeDefinitionAndReload when that route lands.\n\nBlocked for honest edit UI: PatchCard validates against current type and injects defaults without pinning schema_version — core needs version-aware validation + migrations.field_defaults before PATCH/edit form.\n\nMCP: docs-only reconnect (no per-tool workspace schema_version echo). Depends on P2 only if stretch adds affected-card UX later.\n","kind":"feature"},"links":[{"type_id":"parent","target":"card_4c5326d4395745aa98eb241d4361be31","note":"sprint 07-22 phase card","created_by":"claude","created_at":"2026-07-22T16:50:08.117415Z"},{"type_id":"depends-on","target":"card_670dab61ea454d2dbb3fe39d270f8200","note":"diff check lives in the writeDefinitionAndReload helper","created_by":"claude","created_at":"2026-07-22T16:50:08.164686Z"},{"type_id":"depends-on","target":"card_3fd62d3280aa427faeaf6253b687c4c9","note":"in-form rejection copy queries affected-card counts","created_by":"claude","created_at":"2026-07-22T16:50:08.191071Z"}],"comments":[{"id":"cm_eb77412b17724fe3","author":"cursor-review","body":"2026-07-23 review: confirmed the blocker is broader than the stretch card text: Card.schema_version is stored, but PATCH/append validate against the current loaded type. Docs now state that limitation. Implementation follow-up is card_2ba7539c; keep additive schema editing blocked on it.","created_at":"2026-07-23T21:57:27.03644Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-22T16:49:19.227191Z","updated_at":"2026-07-23T21:57:27.03644Z","created_by":"claude","status_since":"2026-07-22T16:49:19.227191Z"},"type":"card"} {"data":{"id":"card_13d9342fbfdf430cbd9a258f7f4b8589","workspace_id":"demo","type_id":"infra-task","schema_version":1,"title":"Move project board from examples/demo-workspace to .cards/ (standard location)","status":"done","fields":{"branch":"main","description":"The project's live dogfooding board moves to the standard .cards/ workspace at repo root (walk-up resolution). examples/demo-workspace/ stays as example material (docs, screenshots, what 'cards init' scaffolds). Scope: import live state into .cards/; repoint board.sh / dev-server.sh / .air.toml defaults; restart :8787 against .cards; update CLAUDE.md. Acceptance: bare 'cards list' from repo root resolves the project board; :8787 serves .cards; examples untouched as frozen example.","environment":"local"},"owner":"pi","comments":[{"id":"cm_4040ece0e0fd4e59","author":"pi","body":"DONE (pi, ops — retro record). Completed 2026-07-19T10:19:40Z: examples server stopped; .cards/ created with current definitions; all 191 cards imported from the last export; board.sh / dev-server.sh / .air.toml defaults repointed to .cards; :8787 restarted against .cards; bare 'cards list' from repo root resolves the project board via walk-up (verified). examples/demo-workspace untouched (frozen example). CLAUDE.md updates follow in the same commit as this board's export.","created_at":"2026-07-19T10:19:40.315749Z","edited_at":"0001-01-01T00:00:00Z"}],"version":5,"created_at":"2026-07-19T10:19:40.163544Z","updated_at":"2026-07-19T10:19:40.36014Z","created_by":"pi","status_since":"2026-07-19T10:19:40.36014Z"},"type":"card"} {"data":{"id":"card_146260d96f274e5a9f19814e1f981295","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Core/HTTP: card delete (DELETE /v1/cards/:id + tombstone event + idempotency)","status":"done","fields":{"branch":"core/delete","description":"Add a first-class card-delete path so junk/test/superseded cards can be hard-removed instead of being closed-as-done with a comment (the current workaround used during the 2026-07 board cleanup, which hit exactly this gap).\n\nScope:\n- DELETE /v1/cards/:id HTTP route (mirror existing PATCH auth/actor/idempotency: X-Work-Cards-Actor header, Idempotency-Key, optimistic version in ?version= query param).\n- CLI: `cards delete --version N` (serverless + HTTP, like patch/claim).\n- MCP: a `delete_card` tool (the MCP tool list currently has no delete either).\n- Core: Service.RemoveCard(id, version, actor) — atomic. Emit a new `card_removed` durable event (typed constructor in internal/core/events.go per EVENTS.md §4 no-raw-literals rule) carrying the removed card's id/type/title snapshot in Diff for replay/audit. Persist via the existing commitCard seam (or the ClaimAtomic-style atomic path if no card row update is needed — decide and document).\n- Safety: delete is a hard, irreversible mutation. Require the current version (optimistic concurrency) so a stale client cannot delete a card that changed underneath it. Consider a workspace setting to soft-delete (tombstone) vs hard-delete; default to hard-delete + event to match append-only audit design. Refusing to delete a card that still has inbound links is an open question — propose: reject with 409 if any inbound links exist (caller removes links first), OR cascade-remove the card's links but refuse to cascade across cards. Pick one and document in SPEC.md §11.\n- Docs: SPEC.md §11 (add the route), §8 (add card_removed to the event catalog + diff shape), INTEGRATOR-REFERENCE.md §2/§4, MCP.md (tool table), DEVELOPER-REFERENCE.md §9 (CLI). Status: [proposed].\n\nOut of scope: bulk delete, undo/recycle, retention windows.\n\nAcceptance:\n- `go build ./...` and `go test ./...` green with new tests covering: delete by current version succeeds and emits card_removed; stale version returns 409 version_conflict; delete is idempotent under a replayed Idempotency-Key (returns the original 204/200); deleting a card with inbound links behaves per the chosen policy with a test.\n- OpenAPI spec (internal/openapi) updated; `cards delete` works in both serverless and HTTP modes; MCP `delete_card` tool listed and working."},"comments":[{"id":"cm_dba2eeced874416e","author":"local-dev","body":"Shipped: DELETE /v1/cards/:id + card_deleted tombstone + optional ?version= guard + idempotency, cards delete CLI verb, OpenAPI + SPEC updated. Re-evaluates dependents for card_unblocked. Used it to relocate 21 distant backlog cards into docs/ROADMAP.md.","created_at":"2026-07-05T01:50:58.370132Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-01T20:02:20.726934851Z","updated_at":"2026-07-05T01:51:19.162157198Z","created_by":"local-dev","status_since":"2026-07-05T01:51:19.162157198Z"},"type":"card"} @@ -123,7 +123,7 @@ {"data":{"id":"card_7dbed16e18e841e3bde4d35b3488af37","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P6 — chip multiselect (multi enum/user) + policy-aware tag chips","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in 0212631 (this card's own platforms field was set with the new chip control's data shape). multiSelect: chips over the native , primary + keyboard-reachable; empty field advertises 'Choose a file…') with modal-scoped drag-drop that can't collide with the board column-move gesture. Full state machine (idle/dragover/uploading/success/error): client-side >32MiB pre-check, server 413 (artifact_too_large — fixed the MaxBytesReader 500), and version_conflict rendered 'card changed — reload'. Raw-body POST with ?version=N. Shared reloadModal(cardID) helper (refetch-failure + closed/switched-modal guards) — reused by the deferred comments/entries sprint. Board card shows uploaded artifacts as 240px-capped thumbnails / download chips, live via artifact_added SSE. VERIFIED END-TO-END IN A REAL BROWSER: upload persisted (image/png, sha256, version bumped), modal reloaded with the thumbnail, board card rendered it via the confined /v1/artifacts route, and a 40MiB file was blocked client-side with a worded message and no network call. Tests: upload round-trip + stale-409 + oversize-413 + board-render, all on the t.TempDir harness. go test -race ./... green (13 pkgs). Deferred per plan: the rest of the UI cluster + landing the card__secondary theme hook (follow-on sprint, on top of this).","created_at":"2026-07-06T04:05:11.04655Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_9cac75f908bf416f","author":"local-dev","body":"Sprint 2026-07-06 close-out: final gate green — go build ./..., go test -race ./... (all packages, 0 failures), go vet clean. Committed + pushed to origin/main. Moving review -> done. P4 shipped as 465798a; browser-verified end-to-end.","created_at":"2026-07-06T08:52:44.927802Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-06T02:51:16.769657Z","updated_at":"2026-07-06T08:52:44.963062Z","created_by":"local-dev","status_since":"2026-07-06T08:52:44.963062Z"},"type":"card"} {"data":{"id":"card_82d71fdf05524844a622e16ce0ea80b0","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 07-10 P1a: docs + audit reconciliation (htmx→Alpine, INTEGRATOR-REFERENCE)","status":"done","fields":{"branch":"frontend-rebuild","description":"Phase 1a of docs/plans/sprint-2026-07-10.md — make the entry docs tell the truth about the shipped UI 2.0 stack.\n\nSCOPE:\n1) Fix htmx→Alpine prose drift: CLAUDE.md (~lines 20, 71) and docs/architecture/DESIGN.md lede (~line 5) still describe the web UI as htmx; rebuild Phase 2 (e783092) removed it. Leave the historical past-tense comment in render.go alone.\n GATE (verified green 2026-07-10): grep -rEn 'hx-(get|post|put|delete|swap|target|trigger)' internal/httpapi/templates/ returns zero — so this is a prose fix, not a code change in disguise.\n2) INTEGRATOR-REFERENCE.md audit with a verifiable artifact, not a self-attested marker: for each of the 8 sections, list the source paths it describes and run git log --oneline b8dda45..HEAD -- (b8dda45 = last verified, rebuild Phase 3). Paste the per-section commit list into the doc's changelog stanza. Sections with commits but no doc change get an explicit 'reviewed, no change needed ' line.\n\nACCEPTANCE: zero present-tense htmx references in CLAUDE.md / DESIGN.md; INTEGRATOR-REFERENCE changelog carries per-section git-log evidence through HEAD for all 8 sections."},"links":[{"type_id":"parent","target":"card_3f225267e3724f9f8d802772731d3316","note":"sprint 07-10 phase card","created_by":"claude","created_at":"2026-07-10T02:07:37.362008Z"}],"comments":[{"id":"cm_89c3f7721fc945b8","author":"claude","body":"Done via PR #20 (https://github.com/somebox/cards/pull/20, branch docs/p1a-htmx-alpine-audit-reconcile → main). Docs only, no code.\n1) htmx→Alpine: fixed CLAUDE.md:20, CLAUDE.md:71, DESIGN.md:5 (lede contradicted its own §Interactivity layer). render.go:722 historical comment left alone. Gate held: hx-* in templates = zero. Acceptance grep clean.\n2) INTEGRATOR-REFERENCE: added an 'Audit changelog' section with per-section git-log evidence over 8d043ea..HEAD (8d043ea = rebuild Phase 3, the doc's prior verification point b8dda45 after the branch was rebased into main). Finding: the Phase 4–10 commits under this doc's purview were all /ui reference-client (DESIGN.md's domain — §2 has zero /ui routes) + one SSE keepalive (liveness). /v1 API, MCP, events, actor, schema, extensions unchanged (api.go/filters.go no commits in range). All 8 sections carry a reviewed line.","created_at":"2026-07-10T14:46:19.852898Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_b409a161b1a94d35","author":"claude","body":"P0a verify 2026-07-11: flipped to done. Verifying commit 87acf1f (PR #20). Acceptance: htmx→Alpine prose fixed in CLAUDE.md/DESIGN.md; INTEGRATOR-REFERENCE audit changelog present; hx-* in templates = zero.","created_at":"2026-07-11T21:30:34.511879Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-10T02:01:41.345569Z","updated_at":"2026-07-11T21:30:34.550751Z","created_by":"claude","status_since":"2026-07-11T21:30:34.550751Z"},"type":"card"} {"data":{"id":"card_857194eac0b94037b7502cef2cbe283d","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"pi-cards: write surface — create/update/comment/claim/release/take_next + config (P1)","status":"done","fields":{"branch":"feat/write-tools","description":"pi-cards series 4/8 — write surface + config file (P1). Spec §5.5, §6.3 rows 4-9, §7. Blocked by card 3 (read surface).\n\nRead surface is live; this completes P1: the LLM can round-trip a card from chat with full write honesty (§5.5). Also lands `.pi/cards.json` / `~/.pi/agent/cards.json` config loading (§7) and auto-registration of the agent actor.\n\nScope:\n- `src/config.ts`: read user then project `cards.json`, project wins; env overrides per §7 table; defaults `autoExport:\"off\"`, `worklogOnTurn:false`, `work.batchLimit:3`, `work.autoDone:false`. (pi has no per-extension settings API — read the JSON files directly.)\n- `src/tools-write.ts`: `cards_create` (`type_id`, `title`, `fields`, `tags`, `dry_run`; validation errors returned verbatim incl. `valid_options`), `cards_update` (fields/status/owner/tags, `version` + §5.5 retry, `dry_run`), `cards_comment` (markdown body, actor attributed), `cards_append_entry` (repeating fields), `cards_claim`/`cards_release` (409 surfaces current owner), `cards_take_next` (`status`/`type_id`/`board_id` filters; on `{card:null}` re-issue once to disambiguate race vs empty per api-surface §11).\n- First write per session auto-registers the actor: `POST /v1/users` kind `agent` (HTTP) / `cards users register` (CLI), ignoring \"already exists\" (§5.4).\n- Tool results return the server's updated card, never assumed local state.\n\nImplementation notes:\n- `dry_run` passes through; CLI backend forwards `--dry-run` where the verb supports it (confirmed on create/patch only — degrade loudly on other verbs, per spec-gap finding).\n- CLI release uses the `patch --owner \"\"` mapping validated in card 2.\n- Board transitions are enforced server-side (engineering board `enforce_transitions:true`); do not pre-validate in the extension beyond surfacing the structured error.\n- CLI `claim` requires `--version N` — fetch version via `cards_get` first (preflight pattern).\n\nAcceptance:\n- `pi -e ./src/index.ts -p \"create a programming-task card titled X with branch feat/x, then move it to todo\"` against a throwaway copy of the fixture workspace: card created, appears in `cards list`, status todo.\n- Forced conflict: patch with a stale `version` → visible `version_conflict`, single auto-retry succeeds on fresh version (unit + integration test).\n- `cards_take_next --status todo` claims oldest unowned card as the resolved actor (actorSuffix applied); a second call gets the next card or null.\n- `node --test` + typecheck green; config precedence covered by a unit test (project beats user beats env-default).\n\nOut of scope: typed `cards_create_` generated tools (card 8); `cards_history`/`cards_breaches`/`cards_events` (phase-2); worklog automation (cards 6-7).","kind":"feature","work_log":[{"author":"pi","commit_hash":"d8ffad0","entry_id":"ent_6336d00b3af0497d","notes":"Write surface complete on feat/write-tools: 7 write tools (create/update/comment/append_entry/claim/release/take_next) with writeHandlers exported for unit tests, once-per-session actor registration, take_next re-issue-once on null, config withDefaults per §7. Fixture workspace id renamed to pi-cards-fixture (probe id-collision meant fixture runs near a live demo server picked HTTP mode toward the REAL board — now safely CLI). 28/28 tests green. Live LLM acceptance serverless against fixture: cards_create round-trip (8ac514f9), then cards_get→cards_claim landed owner=foz status=in_progress v2. Mid-card recovery: feat/write-tools was first cut from stale main (missing card-3 files) — ff main to feat/read-surface and recut; watch branch hygiene on multi-card runs. [timestamp corrected 2026-07-18T16:26Z: was stamped local(+0200) as UTC]","timestamp":"2026-07-18T17:20:00Z"}]},"owner":"pi","links":[{"type_id":"depends-on","target":"card_470df9d792e449b39615dff05a583bea","created_by":"pi","created_at":"2026-07-18T15:10:46.480318Z"}],"comments":[{"id":"cm_299a0091b6344860","author":"pi","body":"DONE (pi). feat/write-tools @ d8ffad0, 28/28 tests + tsc green. Acceptance: live LLM create round-trip + get→claim serverless against the fixture (owner=foz, in_progress); conflict auto-retry verified in integration+unit tests; take_next oldest-unowned + single re-issue pinned by unit tests; config precedence (project>user>defaults) unit-tested. cards_release on CLI throws unsupported_backend (surfaced verbatim to the LLM — see card 2 comment).","created_at":"2026-07-18T16:24:45.747963Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-18T15:10:27.38293Z","updated_at":"2026-07-18T16:28:25.870966Z","created_by":"pi","status_since":"2026-07-18T16:24:45.795307Z"},"type":"card"} From 4b4fed97d55d495896dc9c269e217012b3cb14d7 Mon Sep 17 00:00:00 2001 From: Jeremy Seitz <7750+somebox@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:29:36 +0200 Subject: [PATCH 4/4] =?UTF-8?q?docs,cards:=20land=20the=2009-06=20sprint?= =?UTF-8?q?=20note=20=E2=80=94=20agent=20write=20loop=20+=20docs=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two threads. Thread 1 finishes Sprint A (069ec1d1, c0102825) plus the TakeNext owner check (1c877e6c, promoted to todo) and one new integration test card proving the three-call loop. Thread 2 files the README/site refresh as a parent card with four children (visuals, home + nav, README cut, using-cards split) in backlog, sequenced so the text lands after the CLI flags it documents. Closes ea7ea2a3 as already shipped. Adds a cards-8080 launch config for serving the project board by name. Co-Authored-By: Claude Fable 5.1 --- .cards/backlog.jsonl | 10 +- .claude/launch.json | 30 ++++- docs/plans/2026-09-06-sprint-plan.md | 171 +++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 docs/plans/2026-09-06-sprint-plan.md diff --git a/.cards/backlog.jsonl b/.cards/backlog.jsonl index 7f50cc8..1cd7d7d 100644 --- a/.cards/backlog.jsonl +++ b/.cards/backlog.jsonl @@ -22,6 +22,7 @@ {"data":{"id":"card_0f002686730844e09742a5dd00bebefa","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Review minors batch: watch/reload startup races + suppression window; date min/max author UX; conflict-shape consistency","status":"done","fields":{"branch":"fix/review-minors-0711","description":"Batch of small correctness/polish items from the sprint 07-11 post-merge review (b23aff8) — reload/watch startup + config authoring UX. Each is a contained fix; land as one branch of small commits.\n\nRELOAD/WATCH (flagged independently by two reviewers):\n1) setAfterReload ordering race: cmd/cards/serve.go:126 starts the watcher before setAfterReload at :160; plain field write (reload.go:81-83) read by notifyReload — no happens-before. Failure: missed service reconcile on a first-tick reload under --watch --run-extensions. FIX: install before spawning the watcher (one-line move) or make the field atomic.\n2) Watcher not awaited on shutdown: watchCancel deferred but Run not joined (contrast supervisor's supDone, serve.go:163-173); SIGINT during scanOnce can race reloadLocked against store close. FIX: done channel like the supervisor's.\n3) Self-write suppression window swallows a concurrent external edit: while selfWriteGate.active>0, scanOnce absorbs ANY fingerprint and advances lastFP (watch.go:114-118) — an external save during the create-board handler window is silently dropped until the next change. FIX: during active window skip WITHOUT advancing lastFP; only skipFP should advance it.\n4) Stale skipFP can absorb a later legitimate change (watch.go:207-211): cleared only on match; if the first post-create observation differs, it lingers and can swallow an exact-revert later. FIX: one-tick TTL.\n5) Timing-sensitive negatives noted by review: TestSupervisorUsesCurrentGenerationAfterSwap proves the negative with a fixed 500ms sleep; TestWatchSelfWriteSuppressed drains fan-out non-blockingly. No observed flake in 25x -race; tighten opportunistically.\n\nCONFIG AUTHORING UX:\n6) FieldDate min/max units undocumented + hostile errors: values are Unix-seconds floats sharing the number slots (core/types.go:47-48); no author-facing doc; '\"min\": \"2020-01-01\"' yields a raw json.Unmarshal error and runtime rejection prints 'below min 1.5778368e+09' — fails the project's own which-field/which-value/what-was-allowed bar. FIX: document units in DEVELOPER-REFERENCE-SCHEMA-AUTHORING.md; render dates as RFC3339 in the error; consider accepting date strings at load.\n7) Store-level version_conflict returns the ATTEMPTED card, not current DB state (internal/sqlite/sqlite.go:587-588) — matters for the versionless MCP tools under a genuine race. Pre-existing; fix while nearby.\n8) RemoveEntry rejects version 0 as validation error while AppendEntry/UpdateEntry treat omitted version as version_conflict — inconsistent shape; pick one.\n\nVERIFY: go test -race ./cmd/cards ./internal/hooks ./internal/sqlite; go vet.","kind":"bug"},"links":[{"type_id":"related","target":"card_ec61b093d1a444dcb5c915518de5c67f","note":"watch/reload minors from post-merge review","created_by":"claude","created_at":"2026-07-12T00:10:12.130729Z"},{"type_id":"related","target":"card_8b25cef932d84aa7b90ede8a5921e946","note":"date min/max author UX from post-merge review","created_by":"claude","created_at":"2026-07-12T00:10:12.144985Z"}],"comments":[{"id":"cm_b8afd2de79514bf6","author":"claude","body":"DONE on branch fix/review-minors-0711 (commit 9ba9f15, pushed). Items 1-4 (watch/reload): watcher starts after setAfterReload + joined on shutdown; mid-write scans skip without recording; skipFP exact-match with one-tick TTL. Item 6: FieldDate min/max errors render dates + units documented. Item 7: sqlite version_conflict attaches current DB row (same-tx read, attempted-state fallback). Item 8: AppendEntry/UpdateEntry align with RemoveEntry ('version is required' validation error on omitted version). Item 5 (test-robustness notes) left as-is — 25x -race stress showed no flake; noted for opportunistic tightening. go vet clean, full suite green, -race clean on cmd/cards+sqlite+core.","created_at":"2026-07-12T02:49:35.476299Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_ea5be1c802e9433a","author":"claude","body":"Merged to main in bf62070 (maintainer-directed merge 2026-07-12); full suite + vet green on merged main.","created_at":"2026-07-12T03:02:53.853075Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-12T00:10:12.053777Z","updated_at":"2026-07-12T03:02:53.888175Z","created_by":"claude","status_since":"2026-07-12T03:02:53.888175Z"},"type":"card"} {"data":{"id":"card_0f7be7f689184a9bb2b84c3b1b50c7ff","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"CLI: ergonomic comment alias for agents (comment --body)","status":"done","fields":{"branch":"feat/cli-repeat-fields","description":"Agents routinely fail card updates because the CLI comment surface is easy to get wrong. NARROWED 2026-07-27 during sprint planning: this card is now the comment alias only. The repeating-field sugar that was bundled here is scoped out below, with the conditions under which it may return.\n\nObserved pain (re-verified 2026-07-27 against internal/cli/commands.go:460-476):\n- `cards comment --body B` fails. cmdComment switches on args[0] and requires the literal `add` or `edit`; a card id as the first token falls through to `unknown comment subcommand %q`.\n- Bare `cards comment` has no `--help` listing the subcommands, so the failure gives no route to the working form.\n\nIN SCOPE\n1. Accept `cards comment --body B` as an alias for `cards comment add --body B`. Pure argument parsing: no version semantics, no new HTTP call, same service path, same response.\n2. `cards comment --help` and the usage error both list `add` / `edit` and the alias form.\n3. Document the happy path in docs/reference/cli.md; add CLI tests mirroring the existing short-id parity tests. The alias AND both existing subcommand forms must pass — the alias is additive, not a replacement.\n\nOUT OF SCOPE (the re-scope)\n- Repeating-field sugar (`cards work-log add --notes ...`, or schema-aware flags replacing `--entry-json`). Split out deliberately: the alias above carries no version semantics, while the entry sugar carries two. If it returns as its own card it does so under both constraints:\n (a) flag-to-entry mapping is DERIVED from the card type's FieldDef item_fields, not hardcoded work_log semantics — no bespoke knowledge of `author`/`timestamp`/`commit_hash` outside the schema;\n (b) it does NOT silently fetch the current version. `cards append` keeps requiring `--version N`. The only sanctioned way to omit it is the same explicit `--if-match latest` opt-in landed by card 069ec1d1. A silent auto-GET under an ergonomics banner would be a second, CLI-only concurrency story alongside the real one — rejected.\n- Any change to the work_log item schema, the HTTP contract, or the MCP tool surface.\n\nWhy the split: the original card described one \"cheapest slice\" that was actually two items with different risk. The alias is a pure affordance. The entry sugar is where the CLI could quietly grow its own version semantics; it needs 069ec1d1 to land first so there is one opt-in to point at, not two."},"owner":"claude","comments":[{"id":"cm_bd4eb848d21f4c7d","author":"claude","body":"**Re-scoped 2026-07-27 (sprint planning, agent-cli-ergonomics).**\n\nNarrowed to the comment alias only. The card previously bundled two items with different risk profiles under one \"cheapest slice\": (A) `cards comment --body` as an alias for `comment add` — pure argument parsing, no version semantics; and (B) repeating-field entry sugar that would fill author/timestamp/commit and possibly omit `--version`. Item B is where the CLI could grow a second, CLI-only concurrency story next to the real one, so it is scoped out rather than shipped under an ergonomics banner.\n\nConditions for B to return as its own card are written into the description: flag→entry mapping derived from the type's FieldDef item_fields (not hardcoded work_log knowledge), and no silent version fetch — the only sanctioned omission is the explicit `--if-match latest` opt-in from card 069ec1d1, which must land first.\n\nRe-verified this session: commands.go:460-476 (comment subcommand switch), commands.go:356-370 (append requires --version + --entry-json).\n\nSprint position: item 1 of 3, ships first — no core changes, and it establishes the CLI-test pattern the other two reuse.","created_at":"2026-07-27T09:49:26.507992Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_30daa114d9194192","author":"claude","body":"**Line-citation refinement (2026-08-07).** The description cites commands.go:460-476 for the comment subcommand switch. More precisely: `cmdComment` starts at **:460**, the `add` and `edit` cases are at **:467** and **:481**, and the actual failure an agent hits — `unknown comment subcommand %q` — returns at **:496**. That last line is the one the alias makes unreachable for a bare card id.","created_at":"2026-08-07T06:10:43.854541Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_80050459ddc74e25","author":"claude","body":"Shipped the comment alias. `cards comment --body B` is now the same POST as `comment add`; `comment add` and `comment edit` still work. `cards comment --help` (and the empty-args usage error) list add, edit, and the alias. No version semantics, no HTTP contract change.\n\nTests: TestCommentAliasAndExistingForms (add + alias + flag-before-id + edit), TestCommentHelpAndUsageListTheAlias, TestCLICommentAlias_ShortIDResolves. `go test ./internal/cli ./cmd/cards` pass. Live: `cards comment --help` exits 0 with the three forms.\n\nDocumented in docs/reference/cli.md, cards --help, and the installed skill's cli-reference / SKILL.md recording section.","created_at":"2026-09-02T18:10:21.503317Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_c904fce411c44818","author":"claude","body":"**What shipped:** `cards comment --body B` is accepted as an alias for `comment add`; `add` and `edit` unchanged, no version semantics. Landed on main in 90b838d. Reviewed 2026-09-06 and accepted.","created_at":"2026-09-06T18:11:52.510053Z","edited_at":"0001-01-01T00:00:00Z"}],"version":13,"created_at":"2026-07-24T01:34:52.27474Z","updated_at":"2026-09-06T18:11:52.541587Z","created_by":"local-dev","status_since":"2026-09-06T18:11:52.541587Z"},"type":"card"} {"data":{"id":"card_0fd1b9b7659749aa96014534cccbd530","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Sprint 07-22 P5 (stretch): schema UI — create-only","status":"backlog","fields":{"branch":"feat/schema-authoring-ui","description":"Sprint 07-22 Phase 5 — STRETCH ONLY (docs/plans/2026-07-22-sprint-plan.md).\n\nNot committed for sprint success. If capacity remains after P1–P4 committed work:\n\nCreate-new card-type from web UI only (no additive PATCH in stretch slice). POST /v1/card-types on reload seam (like POST /v1/boards), not inner httpapi router. Extract writeDefinitionAndReload when that route lands.\n\nBlocked for honest edit UI: PatchCard validates against current type and injects defaults without pinning schema_version — core needs version-aware validation + migrations.field_defaults before PATCH/edit form.\n\nMCP: docs-only reconnect (no per-tool workspace schema_version echo). Depends on P2 only if stretch adds affected-card UX later.\n","kind":"feature"},"links":[{"type_id":"parent","target":"card_4c5326d4395745aa98eb241d4361be31","note":"sprint 07-22 phase card","created_by":"claude","created_at":"2026-07-22T16:50:08.117415Z"},{"type_id":"depends-on","target":"card_670dab61ea454d2dbb3fe39d270f8200","note":"diff check lives in the writeDefinitionAndReload helper","created_by":"claude","created_at":"2026-07-22T16:50:08.164686Z"},{"type_id":"depends-on","target":"card_3fd62d3280aa427faeaf6253b687c4c9","note":"in-form rejection copy queries affected-card counts","created_by":"claude","created_at":"2026-07-22T16:50:08.191071Z"}],"comments":[{"id":"cm_eb77412b17724fe3","author":"cursor-review","body":"2026-07-23 review: confirmed the blocker is broader than the stretch card text: Card.schema_version is stored, but PATCH/append validate against the current loaded type. Docs now state that limitation. Implementation follow-up is card_2ba7539c; keep additive schema editing blocked on it.","created_at":"2026-07-23T21:57:27.03644Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-22T16:49:19.227191Z","updated_at":"2026-07-23T21:57:27.03644Z","created_by":"claude","status_since":"2026-07-22T16:49:19.227191Z"},"type":"card"} +{"data":{"id":"card_13935666521348aaba9d1145b47fa038","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Agent write loop: one integration test — take-next → patch --if-match latest → get --md, zero GETs","status":"backlog","fields":{"branch":"feat/agent-loop-test","description":"The batch-level proof that the agent-write-loop sprint shipped its theme (docs/plans/2026-09-06-sprint-plan.md). One CLI-driven integration test in internal/cli (or cmd/cards) that runs, against a seeded workspace: (1) cards take-next --board engineering -q, (2) cards patch --status review --if-match latest -q, (3) cards get --md — three calls, no intervening GET, and asserts the --md output contains no work_log entries and no comment bodies. Also covers the empty-pool case (take-next returns no card) so the loop's exit path is pinned. Depends on 069ec1d1 (--if-match latest) and c0102825 (--md); 1c877e6c must land first so the claimed owner is a registered/declared user.\n\nVerify: go test ./internal/cli -run TestAgentWriteLoop && go test -race ./...\n\nDecision gate: the plan asks whether this stays a separate card or folds into 069ec1d1's PR. Separate is recommended — without it the theme has no independent proof.","kind":"feature"},"links":[{"type_id":"depends-on","target":"card_069ec1d16a804b3286dfccb64d10d0e4","created_by":"claude","created_at":"2026-09-06T18:27:53.256361Z"},{"type_id":"depends-on","target":"card_c01028257c4b4485a40dfc929b223516","created_by":"claude","created_at":"2026-09-06T18:27:53.29041Z"},{"type_id":"depends-on","target":"card_1c877e6ca3e04a24bdd3d2ff90286a84","created_by":"claude","created_at":"2026-09-06T18:27:53.32159Z"}],"version":4,"created_at":"2026-09-06T18:27:53.219266Z","updated_at":"2026-09-06T18:27:53.32159Z","created_by":"claude","status_since":"2026-09-06T18:27:53.219266Z"},"type":"card"} {"data":{"id":"card_13d9342fbfdf430cbd9a258f7f4b8589","workspace_id":"demo","type_id":"infra-task","schema_version":1,"title":"Move project board from examples/demo-workspace to .cards/ (standard location)","status":"done","fields":{"branch":"main","description":"The project's live dogfooding board moves to the standard .cards/ workspace at repo root (walk-up resolution). examples/demo-workspace/ stays as example material (docs, screenshots, what 'cards init' scaffolds). Scope: import live state into .cards/; repoint board.sh / dev-server.sh / .air.toml defaults; restart :8787 against .cards; update CLAUDE.md. Acceptance: bare 'cards list' from repo root resolves the project board; :8787 serves .cards; examples untouched as frozen example.","environment":"local"},"owner":"pi","comments":[{"id":"cm_4040ece0e0fd4e59","author":"pi","body":"DONE (pi, ops — retro record). Completed 2026-07-19T10:19:40Z: examples server stopped; .cards/ created with current definitions; all 191 cards imported from the last export; board.sh / dev-server.sh / .air.toml defaults repointed to .cards; :8787 restarted against .cards; bare 'cards list' from repo root resolves the project board via walk-up (verified). examples/demo-workspace untouched (frozen example). CLAUDE.md updates follow in the same commit as this board's export.","created_at":"2026-07-19T10:19:40.315749Z","edited_at":"0001-01-01T00:00:00Z"}],"version":5,"created_at":"2026-07-19T10:19:40.163544Z","updated_at":"2026-07-19T10:19:40.36014Z","created_by":"pi","status_since":"2026-07-19T10:19:40.36014Z"},"type":"card"} {"data":{"id":"card_146260d96f274e5a9f19814e1f981295","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Core/HTTP: card delete (DELETE /v1/cards/:id + tombstone event + idempotency)","status":"done","fields":{"branch":"core/delete","description":"Add a first-class card-delete path so junk/test/superseded cards can be hard-removed instead of being closed-as-done with a comment (the current workaround used during the 2026-07 board cleanup, which hit exactly this gap).\n\nScope:\n- DELETE /v1/cards/:id HTTP route (mirror existing PATCH auth/actor/idempotency: X-Work-Cards-Actor header, Idempotency-Key, optimistic version in ?version= query param).\n- CLI: `cards delete --version N` (serverless + HTTP, like patch/claim).\n- MCP: a `delete_card` tool (the MCP tool list currently has no delete either).\n- Core: Service.RemoveCard(id, version, actor) — atomic. Emit a new `card_removed` durable event (typed constructor in internal/core/events.go per EVENTS.md §4 no-raw-literals rule) carrying the removed card's id/type/title snapshot in Diff for replay/audit. Persist via the existing commitCard seam (or the ClaimAtomic-style atomic path if no card row update is needed — decide and document).\n- Safety: delete is a hard, irreversible mutation. Require the current version (optimistic concurrency) so a stale client cannot delete a card that changed underneath it. Consider a workspace setting to soft-delete (tombstone) vs hard-delete; default to hard-delete + event to match append-only audit design. Refusing to delete a card that still has inbound links is an open question — propose: reject with 409 if any inbound links exist (caller removes links first), OR cascade-remove the card's links but refuse to cascade across cards. Pick one and document in SPEC.md §11.\n- Docs: SPEC.md §11 (add the route), §8 (add card_removed to the event catalog + diff shape), INTEGRATOR-REFERENCE.md §2/§4, MCP.md (tool table), DEVELOPER-REFERENCE.md §9 (CLI). Status: [proposed].\n\nOut of scope: bulk delete, undo/recycle, retention windows.\n\nAcceptance:\n- `go build ./...` and `go test ./...` green with new tests covering: delete by current version succeeds and emits card_removed; stale version returns 409 version_conflict; delete is idempotent under a replayed Idempotency-Key (returns the original 204/200); deleting a card with inbound links behaves per the chosen policy with a test.\n- OpenAPI spec (internal/openapi) updated; `cards delete` works in both serverless and HTTP modes; MCP `delete_card` tool listed and working."},"comments":[{"id":"cm_dba2eeced874416e","author":"local-dev","body":"Shipped: DELETE /v1/cards/:id + card_deleted tombstone + optional ?version= guard + idempotency, cards delete CLI verb, OpenAPI + SPEC updated. Re-evaluates dependents for card_unblocked. Used it to relocate 21 distant backlog cards into docs/ROADMAP.md.","created_at":"2026-07-05T01:50:58.370132Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-01T20:02:20.726934851Z","updated_at":"2026-07-05T01:51:19.162157198Z","created_by":"local-dev","status_since":"2026-07-05T01:51:19.162157198Z"},"type":"card"} {"data":{"id":"card_168fa8c54ae64241b880aa0459b6178a","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"UI: live board updates via SSE (htmx event-source)","status":"done","fields":{"branch":"feat/slice4","description":"UI: live board updates via SSE (htmx event-source)","work_log":[{"author":"foz","commit_hash":"slice4","entry_id":"ent_c930e42657be4195","notes":"DONE: board page EventSource, re-fetch + swap #board on mutation events, no polling.","timestamp":"2026-06-26T09:00:00Z"}]},"owner":"foz","tags":["feature"],"comments":[{"id":"cm_09e83e3bbd84430d","author":"foz","body":"Closed: board page opens EventSource on the board's stream; on mutation events re-fetches board HTML and swaps #board. No polling. ago time re-computed after each settle.","created_at":"2026-06-26T09:25:42.396072Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-06-26T08:48:13.713029Z","updated_at":"2026-07-01T19:44:55.092912593Z","created_by":"foz","status_since":"2026-06-26T08:58:02.930205Z"},"type":"card"} @@ -32,7 +33,7 @@ {"data":{"id":"card_1aaa461124c04fe0b62e592cb3b10b09","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"UI: header nav — active board, theme label, boards overflow, CTA system","status":"done","fields":{"a11y":"aria-current on nav; theme menu keyboardable; overflow menu accessible.","acceptance":"- Active board is visually and programmatically indicated\n- Current theme readable without opening the menu\n- With 8+ boards, nav does not wrap into unusable multi-line chrome\n- New card / new board look like one CTA system\n","branch":"ui/nav-polish","description":"Nav works for 2 demo boards; will fail for real workspaces.\n\nMUST:\n1) aria-current / .is-active on the active board link\n2) Theme shows current theme name (e.g. ◐ labels), not only menu check\n3) Boards overflow strategy after ~N links (details menu or \"Boards\" dropdown)\n4) Unify + Card / + Board visual language via shared .nav-action metrics (token padding, min-height)\n5) Sync with home polish crumb (81086204) — board page shows path back home\n\nNO theme rewrites. Keep always-dark nav chrome.\n","design_ref":"docs/architecture/DESIGN.md; layout.html app-nav","platforms":["desktop","mobile"],"surface":"nav"},"tags":["feature"],"links":[{"type_id":"parent","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.909732Z"},{"type_id":"related","target":"card_86515fd2a4784e3793e94885b7be2e7f","created_by":"jeremy","created_at":"2026-07-09T23:26:10.910997Z"},{"type_id":"related","target":"card_810862041d5b410b95cb5365fe0e438c","created_by":"jeremy","created_at":"2026-07-09T23:26:10.944892Z"}],"comments":[{"id":"cm_7f391f0e9b86437a","author":"claude","body":"Sprint 07-10: nice-to-have tail (tracker card_3f225267) — behind the spacing-rules gate (card_72ebfbee). REQUIRED before implementation: attach a wireframe/mockup of the header (active board, theme label, boards overflow, CTA placement) to this card for review. Unify via --role-meta-* tokens; dark-mode + reduced-motion checks on the PR.","created_at":"2026-07-10T02:07:04.475246Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_7a330e5df4044b55","author":"claude","body":"Done, committed directly to main (workflow switched to main-direct). Header board selector: replaced the one-link-per-board row with a 'Cards / ▾' breadcrumb whose
disclosure (mirrors the theme-picker, pure HTML no JS) lists all boards with the active one checkmarked + 'New board' under a divider. Header stays one row at any board count. Removed the now-dead .nav-add '+' and its CSS. Verified in-browser (default + labels themes): panel opens, active board checked, switching navigates, uppercases under labels, zero console errors; full suite green (14 pkgs, hex ratchet raised 179→183 for the nav's fixed-light #fff). Matches the approved mockup.","created_at":"2026-07-11T10:53:09.980826Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_58b7840c94694e4f","author":"claude","body":"P0a verify 2026-07-11: flipped to done (optional READY). Verifying commit cc45f72. Header nav (active board, theme label, boards overflow, CTA) on main. Process note: wireframe-before-impl ACC was not attached; functional ship accepted for drain.","created_at":"2026-07-11T21:30:34.894847Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-09T23:25:58.707547Z","updated_at":"2026-07-11T21:30:34.935289Z","created_by":"jeremy","status_since":"2026-07-11T21:30:34.935289Z"},"type":"card"} {"data":{"id":"card_1af1fd87334f4e078b02e0ab14649446","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Sprint 07-11 P4a: golden-render harness + TypeTheme resolution unification (byte-identical gate)","status":"done","fields":{"acceptance":"Golden snapshots in CI; type-themed boards byte-identical through unification; CardAccentField resolved; precedence chain + define/activate split normative in STYLE-FIELD.md.","branch":"feat/golden-render","description":"Phase 4 of docs/plans/sprint-2026-07-11.md, steps 1-3 — the safety net and refactor BEFORE the style_field feature card.\n\nDO:\n1) Golden-render test harness FIRST — none exists in internal/httpapi today; budgeted new infra, kept small: render the type-themed demo boards through the template pipeline and snapshot the HTML. Capture current output before touching any render path. FALLBACK if the harness proves expensive: snapshot only the type-themed demo boards — enough to catch a re-skin.\n2) Resolve CardAccentField (internal/core/types.go:173, zero consumers; deletion is safe because unknown JSON keys are ignored) — delete it or fold into StyleField. Same commit: write the NORMATIVE precedence text into docs/design/STYLE-FIELD.md — option-theme over type-theme over CSS-default merge order, and the define/activate split (OptionThemes declared on FieldDef, a board OPTS IN via BoardPresentation.StyleField; same card can legitimately render differently on two boards; presentation metadata never branches write paths per CORE-BOUNDARIES 3.2).\n3) Unify the two divergent TypeTheme resolution paths (precomputed cardView vs live typeTheme template-func in card_head) into ONE precomputed per-card function threaded through ViewData — mandatory anyway: style-field theming is per-card-value and cannot ride the id-keyed map. Golden tests from step 1 must pass unchanged.\n\nACCEPTANCE: golden snapshots in CI; existing type-themed boards byte-identical through the unification (no accidental re-skin); CardAccentField resolved; precedence + define/activate are contract text in STYLE-FIELD.md. go test ./internal/httpapi ./internal/core green.","design_ref":"docs/design/STYLE-FIELD.md; internal/httpapi/render.go; templates/card_head","surface":"board"},"links":[{"type_id":"parent","target":"card_1b5289099221445090a54893e379106f","note":"sprint 07-11 phase card","created_by":"claude","created_at":"2026-07-11T21:07:50.961516Z"},{"type_id":"depends-on","target":"card_9174c0ea607a40ef8ebe6cb3b75f2932","created_by":"claude","created_at":"2026-07-11T21:07:51.174799Z"}],"comments":[{"id":"cm_299dc39fdd2b44e1","author":"claude","body":"P4a done 2026-07-11: golden-render harness (testdata/golden); CardAccentField deleted → StyleField/OptionThemes schema prep; TypeTheme unified via resolveCardTheme → ViewData.CardTheme. go test ./internal/httpapi ./internal/core green.","created_at":"2026-07-11T21:43:40.14992Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-11T21:07:50.762417Z","updated_at":"2026-07-11T21:43:40.156699Z","created_by":"claude","status_since":"2026-07-11T21:43:40.156699Z"},"type":"card"} {"data":{"id":"card_1b5289099221445090a54893e379106f","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 2026-07-11: make the composition substrate real (tracker)","status":"done","fields":{"branch":"main","description":"SPRINT TRACKER — plan doc: docs/plans/sprint-2026-07-11.md. The board IS the plan: card bodies carry the executable detail; phase exits update statuses on the card IDs below.\n\nPITCH: Rotate the center of mass back onto the differentiating substrate — the four transports as honest versioned contracts, and the extensions-over-plugins bet. Drain review + retire doc drift first (P0, hard gate), make MCP a first-class transport + raise config validation rigor (P2), then in PARALLEL: live definition authoring with in-browser failure feedback (P3) and enum-driven board legibility (P4), finishing with the north star: a supervised 'service' extension kind (P5). Every schema change this sprint is additive omitempty definition JSON — no SQLite migration, no snapshot-format change.\n\nPHASES → CARDS:\nP0a card_9174c0ea607a40ef8ebe6cb3b75f2932 (review-pile drain, per-card verify)\nP0b card_bac182247e6144f7942c8058787e5619 (doc-truth sync: INTEGRATOR-REFERENCE / SPEC-API-SURFACE / mcp README)\nP2a card_c3c0fac556144a54a3be69d012e93b75 (six MCP tools + parity allowlist test)\nP2b card_8b25cef932d84aa7b90ede8a5921e946 (config semantic validation, warn-tier)\nP3a card_524c5758b3b34e4d814142130cab9eca (hook-supervisor stale-generation bug — cherry-picks forward)\nP3b card_ec61b093d1a444dcb5c915518de5c67f (--watch poller + failure banner + reload contract)\nP4a card_1af1fd87334f4e078b02e0ab14649446 (golden-render harness + TypeTheme unification)\nP4b card_4bdd35866cc54e4e844b5f6d96da6a8e (style_field: color+icon per enum value)\nP5a card_abb13ae015a843f4b00b52a65b81fc1c (lifecycle ADR: RestartPolicy x Autostart x kinds)\nP5b card_23c7070a75114ba9ad14306803293f4f (service supervisor: ready gate, backoff, drain)\nP5c card_eecf0e0303a24b328b9993d547c4df65 (reconcile-on-reload: identity key + decision table)\nCLOSE card_c08a6ead73a04837995484a5ba9177d7 (walkthrough + round-trip + board sync)\nCARRY-OVER card_f20e87d5da85410cbcf18b4ccda9dd5a (board-create idempotency — unfinished 07-10 work; finish narrowly or close-blocker)\n\nSTANDING RULES: P0 gates all M-sized work. No batch-flipping review cards — human git show / CI verification per card. Every schema change stays additive omitempty definition JSON. Phase exits update card statuses. Sprint close exports + commits the JSONL once.\n\nOUT OF SCOPE (deliberate): click-accent-to-filter (follow-up card against shipped style_field) · token-mode auth (AUTH.md frozen; host-shaped until multi-tenant is scheduled) · ACL/permissions in kernel · expose field semantics · outbox/webhooks in-core (future home: an external supervised service) · hook re-declaration on reload (rides P5 reconcile design or follow-up) · DisallowUnknownFields (key leniency is what keeps this sprint's schema changes safe) · orphaned View type · GH-Pages docs site · enum theming beyond one style_field per board · fsnotify.\n\nRISKS: stricter validation is a load-contract change — warn-tier is the pressure valve; validate demo-workspace + known definitions before any rejection ships. Stale-generation bug is live and watcher-amplified — P3a cherry-picks forward even if P3 slips. Watcher + supervisor tests are the flake-prone surfaces — injectable clock / fake child binaries / mac+linux budget are designed in. P5 has the most unknowns at the end of the critical path — ADR lands early; fallback is supervisor-without-reconcile (documented restart-on-reload). Golden harness fallback: snapshot only type-themed demo boards. Pipeline is review-bound: P0 is a hard gate and per-card verification keeps it honest.","kind":"feature"},"comments":[{"id":"cm_4ab53220bd9b43df","author":"claude","body":"Sprint 07-11 tracker close: all phase cards P0a–P5c + close + carry-over f20e87d5 marked done. Remaining review-column holds from P0a stay as documented unmet criteria (not rubber-stamped).","created_at":"2026-07-11T21:58:15.212523Z","edited_at":"0001-01-01T00:00:00Z"}],"version":4,"created_at":"2026-07-11T21:07:50.845459Z","updated_at":"2026-07-12T02:45:42.968983Z","created_by":"claude","status_since":"2026-07-11T21:58:15.21718Z"},"type":"card"} -{"data":{"id":"card_1c877e6ca3e04a24bdd3d2ff90286a84","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"TakeNext: make owner registration consistent","status":"backlog","fields":{"branch":"fix/take-next-owner-validation","description":"Review found an identity-contract inconsistency: owner PATCH and claim reject unregistered users, while TakeNext writes assign_to (or the actor fallback) directly through the atomic store path without the same lookup. Decide and enforce one contract across all ownership mutations. The likely fix is to validate TakeNext ownership and update runnable workers such as review-bot to register once before claiming; preserve arbitrary unregistered actor strings for non-ownership writes. Add service, HTTP, CLI, and extension integration coverage for registered and unknown owners, and synchronize the actor-model docs.","kind":"bug"},"comments":[{"id":"cm_c63fd94283c84501","author":"claude","body":"Re-confirmed against HEAD (2026-07-26 architecture audit) — this card is still accurate and still open.\n\nExact location: `Service.TakeNext` (internal/core/service.go:1465) defaults `assignTo` to the actor and passes it to the claim path with no registry lookup, while `PATCH owner` and `claim` both return `unknown_user` (422). So two of three ownership paths enforce the registry and the third does not.\n\nWorth noting for prioritisation: docs/reference/implementation-status.md §5 has described this as 'an implementation inconsistency, not an authentication boundary' across several audit cycles — it has been *documented* rather than *decided* for months. The decision (enforce everywhere, or exempt take-next deliberately as the ephemeral-worker path) is the actual deliverable; the code change either way is small.\n\nI filed a duplicate of this (`319d6c8a`) during the audit — my board search missed this card. The duplicate is deleted; this card is the one to work. Its scope (validate ownership, update review-bot to register once before claiming, service/HTTP/CLI/extension coverage, sync the actor-model docs) is better than what I wrote.","created_at":"2026-07-26T09:44:27.850306Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_c11257fe88cf4a64","author":"claude","body":"Premise update from 8920121 (branch fix/happy-path-simulation): checkUserExists now accepts users declared in definitions (workspace.users or the default_user seed) as well as store-registered ones, because a fresh 'cards init' workspace's default user could not claim its own cards. When this card unifies TakeNext's owner path, route it through checkUserExists so the contract is the same everywhere.","created_at":"2026-09-05T13:25:53.780315Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-07-23T21:56:56.472589Z","updated_at":"2026-09-05T13:25:53.780315Z","created_by":"cursor-review","status_since":"2026-07-23T21:56:56.472589Z"},"type":"card"} +{"data":{"id":"card_1c877e6ca3e04a24bdd3d2ff90286a84","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"TakeNext: make owner registration consistent","status":"todo","fields":{"branch":"fix/take-next-owner-validation","description":"Review found an identity-contract inconsistency: owner PATCH and claim reject unregistered users, while TakeNext writes assign_to (or the actor fallback) directly through the atomic store path without the same lookup. Decide and enforce one contract across all ownership mutations. The likely fix is to validate TakeNext ownership and update runnable workers such as review-bot to register once before claiming; preserve arbitrary unregistered actor strings for non-ownership writes. Add service, HTTP, CLI, and extension integration coverage for registered and unknown owners, and synchronize the actor-model docs.","kind":"bug"},"comments":[{"id":"cm_c63fd94283c84501","author":"claude","body":"Re-confirmed against HEAD (2026-07-26 architecture audit) — this card is still accurate and still open.\n\nExact location: `Service.TakeNext` (internal/core/service.go:1465) defaults `assignTo` to the actor and passes it to the claim path with no registry lookup, while `PATCH owner` and `claim` both return `unknown_user` (422). So two of three ownership paths enforce the registry and the third does not.\n\nWorth noting for prioritisation: docs/reference/implementation-status.md §5 has described this as 'an implementation inconsistency, not an authentication boundary' across several audit cycles — it has been *documented* rather than *decided* for months. The decision (enforce everywhere, or exempt take-next deliberately as the ephemeral-worker path) is the actual deliverable; the code change either way is small.\n\nI filed a duplicate of this (`319d6c8a`) during the audit — my board search missed this card. The duplicate is deleted; this card is the one to work. Its scope (validate ownership, update review-bot to register once before claiming, service/HTTP/CLI/extension coverage, sync the actor-model docs) is better than what I wrote.","created_at":"2026-07-26T09:44:27.850306Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_c11257fe88cf4a64","author":"claude","body":"Premise update from 8920121 (branch fix/happy-path-simulation): checkUserExists now accepts users declared in definitions (workspace.users or the default_user seed) as well as store-registered ones, because a fresh 'cards init' workspace's default user could not claim its own cards. When this card unifies TakeNext's owner path, route it through checkUserExists so the contract is the same everywhere.","created_at":"2026-09-05T13:25:53.780315Z","edited_at":"0001-01-01T00:00:00Z"}],"version":4,"created_at":"2026-07-23T21:56:56.472589Z","updated_at":"2026-09-06T18:27:53.346763Z","created_by":"cursor-review","status_since":"2026-09-06T18:27:53.346763Z"},"type":"card"} {"data":{"id":"card_1f9bf7c4b8d24c9b9f1be88f0a6f3451","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint P3 — Theme foundations (stamp seam, validator, contract)","status":"done","fields":{"branch":"httpapi/theme-foundations","description":"Foundation phase. Land precursors that make workspace themes safe.\n\nSteps:\n- [x] Move stylesheet cache stamp from package-level var (internal/httpapi/server.go:30 assetStamp, init once per PROCESS) onto per-generation Server instance state so it rotates on reload. Prove rotation with a cmd/cards integration test.\n- [x] Build a tokenizer/parse-based CSS validator w/ stated threat model: reject scope-escape (balanced 'html[data-theme=x]{} } body{...}'), @import, remote url(). Error payload names theme/file/line/violation. Test against adversarial strings.\n- [x] Document precedence chain, Board.Theme layering, back-compat, workspace-font policy in DESIGN.md + docs/design/THEMES.md.\n\nDemo: feed validator a brace-balanced scope-escaping string -> rejected w/ file/line/violation; reload dev server -> /ui/style.css serves under a fresh stamp.\nExit: stamp on Server instance state (package var removed) + rotation proven; validator rejects the three classes; contract documented; internal/config coverage >51.3%; go test ./... green."},"owner":"jeremy","tags":["feature"],"links":[{"type_id":"depends-on","target":"card_519e1688f06646ff9817268b2ed0c9a7","created_by":"jeremy","created_at":"2026-07-07T20:52:32.163527Z"}],"comments":[{"id":"cm_d7ede75e322e4a37","author":"jeremy","body":"DONE (commit 650f4d3). Stamp seam: assetStamp moved to per-generation httpapi.Server state (package var removed); rotation proven by cmd/cards/TestReloadRotatesStylesheetStamp. Validator: internal/themecss.Validate — braces + scope (catches balanced scope-escape) + @import + remote url(), violations carry {theme,file,line,rule,message}; 13 adversarial tests. Docs: THEMES.md 'Load-time contract' (precedence incl. planned board.presentation.theme layer, Board.Theme two-hook layering, back-compat, font-manifest policy) + DESIGN.md pointer. go test ./... green (14 pkgs), vet clean.\n\nCAVEAT on one exit criterion: 'internal/config coverage >51.3%' does not apply to P3 — P3 adds no config code (the validator is its own package themecss, fully tested). The loader lands in internal/config in P4; that coverage bump belongs there. Not manufacturing config tests for code that doesn't exist yet.","created_at":"2026-07-08T07:46:54.939023Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_8f11b97db2f149d8","author":"jeremy","body":"STATUS CHECK 2026-07-10: description checklist is fully [x]. Code present: themecss/validate.go, per-generation stamp, tests. Candidate for done after a quick acceptance pass / dogfood — no open steps listed.","created_at":"2026-07-09T23:25:58.682161Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-07T20:50:49.789182Z","updated_at":"2026-07-09T23:26:32.362624Z","created_by":"jeremy","status_since":"2026-07-09T23:26:32.362624Z"},"type":"card"} {"data":{"id":"card_21bbb5133f344e0da88701175d17df7f","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P2 — htmx removed, X-Cards-Partial header","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in e783092. htmx issued zero requests (no hx-* ever existed): dead listeners deleted (all real paths call toast() directly), CDN tag removed, HX-Request→X-Cards-Partial renamed atomically (1 Go reader wantsPartial + 4 JS senders), vestigial X-Modal dropped, stale 'htmx UI' comments fixed. Guard TestNoHTMXResidue. Browser-verified: modal fragment loads, SSE refresh, toasts. Found+carded pre-existing bug card_096261c37 (stale save toasts 'Saved').","surface":"board"},"owner":"jeremy","tags":["feature"],"version":5,"created_at":"2026-07-09T08:40:28.037007Z","updated_at":"2026-07-09T08:40:28.597377Z","created_by":"jeremy","status_since":"2026-07-09T08:40:28.597377Z"},"type":"card"} {"data":{"id":"card_21ef721313044726a8ec9907af6cde1d","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Events: board-scoped event model (scope card|board, nullable card_id, board_id)","status":"done","fields":{"branch":"plan/integration","description":"events.card_id is currently NOT NULL, so board-level condition events (lane_drained, wip_exceeded) have no home. Add a scope field (card|board), make card_id nullable, record board_id for board-scoped events. Prereq for condition events. Keep card-scoped events unchanged; update SSE/feed filtering + replay."},"links":[{"type_id":"related","target":"card_bb0552e91e754aa186ebff55de3659c9","note":"part of the integration contract","created_by":"jeremy","created_at":"2026-06-30T17:09:09.822315815Z"},{"type_id":"depends-on","target":"card_c53b453533b041c3b752ac5065c52058","note":"needs seam Step 1 (call sites through commitCard) first","created_by":"local-dev","created_at":"2026-07-01T15:15:34.021476065Z"},{"type_id":"related","target":"card_97b29005221041c5b8dcc18cb937bbdb","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.040632369Z"},{"type_id":"related","target":"card_f0569b62219740b6813acd4e5d6b5f2a","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.057256887Z"},{"type_id":"related","target":"card_6de978482b9e49049561862f21ba087f","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:15:34.074341779Z"}],"comments":[{"id":"cm_35d811919178405d","author":"local-dev","body":"Split into three small testable slices per docs/EVENTS.md §12 Step 2: card_97b29005221041c5b8dcc18cb937bbdb (schema/migration only), card_f0569b62219740b6813acd4e5d6b5f2a (BoardEvent + one real usage), card_6de978482b9e49049561862f21ba087f (bus/feed filtering). This card now tracks the umbrella; close it when all three land.","created_at":"2026-07-01T15:15:34.003866448Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_3350a47c3aca44c4","author":"local-dev","body":"Closed: decomposed into slices per docs/EVENTS.md §12 Step 2 — card_97b2… (2a schema/migration), card_f0569… (2b BoardEvent constructor), card_6de9… (2c Bus/Feed filtering by scope+board_id). Implementation lives in the slices (still backlog); this umbrella is closed as a tracking item, NOT built.","created_at":"2026-07-01T19:44:55.105765176Z","edited_at":"0001-01-01T00:00:00Z"}],"version":15,"created_at":"2026-06-30T17:09:09.74993445Z","updated_at":"2026-07-01T19:44:55.105765176Z","created_by":"jeremy","status_since":"2026-07-01T19:44:55.104095193Z"},"type":"card"} @@ -58,6 +59,7 @@ {"data":{"id":"card_34b5ee9588a6449e94c319e223d170e1","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"b1","status":"done","fields":{"branch":"b","description":"d"},"comments":[{"id":"cm_0a8a0b1e350041a2","author":"local-dev","body":"Test/junk data (created by a test user, description 'd'). No DELETE /v1/cards/:id route exists, so closing as done to clear the active backlog; safe to hard-delete once a card-delete endpoint is added.","created_at":"2026-07-01T19:44:55.112550338Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-06-30T22:04:19.585636226Z","updated_at":"2026-07-01T19:44:55.112550338Z","created_by":"bob","status_since":"2026-07-01T19:44:55.110999941Z"},"type":"card"} {"data":{"id":"card_34e45cd96ced4d24a4b2ec499fcd718e","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"UI: manual reorder within a lane (needs rank-field design)","status":"backlog","fields":{"acceptance":"Design note exists (rank field type + lane_sort interaction + renumber strategy + SSE convergence) before any implementation; then drag within a lane persists across reload and other clients converge via SSE. VERIFY: drag a card within a lane, reload, assert position holds; open a second client, assert it converges via SSE; go test ./internal/httpapi green.","description":"PLAIN: let a user drag a card to a new position within a lane and have it stay there across reloads and across other clients. The configured-sort half already ships; this is the manual-rank half, and it is blocked on a rank-field design decision.\n\nTracks the unbuilt half of ROADMAP §9 (UI drag-drop reordering) and §5 (priority/rank field) so it lives on the board instead of only in ROADMAP prose. OUT of sprint 07-10 scope. (Note: ROADMAP prose currently references card ids d44d3e0d / b3e0914b for these two halves — reconcile with this card when it starts.)\n\nConfigured ordering is DONE (?sort= grammar, per-board lane_sort in types.go:185, header sort selector in ui.go:255 — see 'Core/UI: configured card ordering'). This card is the drag-rank half: user drags a card to a position within a lane and it stays there.\n\nBLOCKED ON DESIGN: no rank/priority field exists in any schema (LaneSort only references '-fields.priority' as example grammar; no such field is defined). Needs a design decision first — rank field type (number? lexicographic string?), who owns renumbering, interaction with lane_sort (manual rank presumably a lane_sort mode), and multi-client SSE consistency.","surface":"board"},"comments":[{"id":"cm_77d44e8e33de4191","author":"foz","body":"**Quality review (batch B) — keep backlog (blocked-on-design)**\n\n**Validated:** Configured-ordering half is genuinely DONE — `?sort=` grammar, `LaneSort` (types.go:185), sort selector (ui.go:255) all present. No rank/priority field exists in any schema (the only mention is LaneSort's example grammar `-fields.priority`; no such field is defined). ROADMAP §9 (drag-drop) and §5 (rank field) confirmed; gap is real.\n\n**Issues found:**\n- Card referenced ROADMAP §9 only; §5 (the rank-field prerequisite) was implicit — made explicit.\n- ROADMAP prose cites different card ids for these two halves (d44d3e0d drag-drop, b3e0914b rank field) — id drift vs this backlog card; flagged for reconciliation when work starts.\n- No observable verify checks.\n\n**Changes made:** Added plain-language lead. Added ROADMAP §5 cross-ref with the card-id drift note. Added concrete file:line citations for the shipped configured-ordering half. Folded observable VERIFY steps into acceptance (frontend-task type has no `verify` field): drag → reload → assert; second-client SSE convergence; `go test ./internal/httpapi`.\n\n**Scope verdict:** Keep — correctly blocked on a rank-field design decision before any implementation; no premature UI work.\n\n**Arch verdict:** Aligned — manual rank as a lane_sort mode keeps ordering in the declarative data layer (schemas not magic); SSE convergence respects the multi-client event model.","created_at":"2026-07-19T17:36:51.760648Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-07-10T02:03:47.208157Z","updated_at":"2026-07-19T17:36:51.760648Z","created_by":"claude","status_since":"2026-07-10T02:03:47.208157Z"},"type":"card"} {"data":{"id":"card_350b1bacbd0f4e339a5b67161ea77186","workspace_id":"demo","type_id":"api-task","schema_version":1,"title":"AUTH reference impl: register issues bearer token; --auth token verifies writes","status":"backlog","fields":{"acceptance":"Tests: happy path (200 with valid bearer); 401 on missing/invalid/unknown token (identical generic body — no enumeration); constant-time compare unit test; CRITICAL anti-spoof test: valid bearer + mismatched X-Work-Cards-Actor commits with the AUTHENTICATED actor (assert on the event's actor field); forced write without credentials → 401; --auth none regression: existing httpapi tests pass unchanged; identity-before-idempotency: same Idempotency-Key under two different tokens creates two distinct writes. SPEC-API-SURFACE gains 'Security / Actor' subsection ONLY when this lands; PHILOSOPHY §1/§7 edits land in the same commit train.","api_change":"additive","branch":"main","description":"Reference implementation of docs/design/AUTH.md (frozen 2026-07-10 — the doc is normative for this card; see docs/NOTES.md freeze entry). AUTH.md review has landed (card_61040a3e, done 2026-07-18); the doc is frozen and normative. This card remains parked in backlog until an owner schedules the XL sprint — reopening the design requires impl-discovers-reality, not another investigation pass.\n\nBEHAVIOR (AUTH.md §§1-4):\n- cards serve --auth (proxy mode is a LATER slice, not this card).\n- POST /v1/users issues an opaque high-entropy bearer token, shown once. DB stores hash only (constant-time compare via crypto/subtle). No GET-token endpoint. Rotate = revoke + reissue (cards users token rotate ).\n- Writes under --auth token require Authorization: Bearer; actor = the registered User.ID the token maps to. Reads stay open in v0 (host ACL is the read confidentiality line; --auth-require-read is future work).\n- Under --auth none: today's behavior exactly — ambient hints (X-Work-Cards-Actor → CARDS_USER → DefaultUser), actor may be an unregistered free string, Kind best-effort. DefaultUser is NOT consulted under token.\n- Bootstrap: serverless CLI register always trusted; HTTP POST /v1/users under token gated by an existing valid bearer (v0 stopgap: first CLI-registered user is the admin bit — see card_2680d5f7 for the debt). --save writes ~/.cards/credentials; CARDS_TOKEN env supported (MCP adapter binds identity at process spawn).\n- Identity resolves BEFORE idempotency replay (AUTH.md §5 — keys are per-actor).\n- 401 for ALL failure modes; no user enumeration; rate-silenced failure logs; NEVER log token material.\n- Force never bypasses auth: forced writes still 401 without valid credentials.\n\nNOT THIS CARD: proxy mode, --auth-require-read, sessions/cookies (reference-client pattern — the web UI may store the bearer client-side; that is a separate thin follow-on spike), Basic-auth adapter (optional later: Basic maps user:token onto the same store — one-line mention in AUTH.md §11, do NOT implement in v0), scoped tokens, groups semantics.\n\nDOWNSTREAM: landing this unblocks card_c7a70b64 Child C (wire Service.ResolveActor as the Authenticator default fallback).\n\nBIND WARN INTERACTION: pairs with card_fa239a92 — non-loopback + auth=none gets the extra remediation line; non-loopback + auth=token gets the single WARN only.","endpoint":"HTTP auth middleware on /v1/* and /ui/* writes (no new route)","verify":"go test ./internal/httpapi ./cmd/cards; curl -u flow per acceptance; cards users register --save then cards create without --as → event actor = registered id"},"links":[{"type_id":"parent","target":"card_61040a3e435b4de8bad3227fc440d788","note":"reference implementation of the RFC's Authenticator seam","created_by":"claude","created_at":"2026-07-10T06:26:48.598438Z"},{"type_id":"related","target":"card_61040a3e435b4de8bad3227fc440d788","note":"impl of the interface defined here","created_by":"claude","created_at":"2026-07-10T06:26:48.623659Z"}],"comments":[{"id":"cm_74b58785cab04bfa","author":"local-dev","body":"PARKED 2026-07-18 (sprint 2026-07-18 P1b): confirmed as its own future XL sprint. Obligations on the card: User store, Authenticator wiring, identity-before-idempotency reorder (AUTH.md §§1-4, frozen c6cb17e). Never stack with the read pool (roadmap §3). Stays backlog until an owner schedules it; reopening the design requires impl-discovers-reality, not another investigation pass.","created_at":"2026-07-18T04:57:10.271176Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_2eeec7a6d8724b2b","author":"foz","body":"**Quality review (batch A)**\n\n**Validated**\n- AUTH.md is frozen (2026-07-10, c6cb17e) and normative; `docs/NOTES.md` freeze entry exists. All behavior bullets (§§1-4, §5 identity-before-idempotency) match the doc.\n- NOT-THIS-CARD list (proxy mode, --auth-require-read, sessions, Basic adapter, scoped tokens) is consistent with AUTH.md §11.\n- `acceptance` field is thorough and observable (anti-spoof test, constant-time compare, identity-before-idempotency, 401 uniformity).\n- Pairing with `card_fa239a92` (bind-warn interaction) is consistent.\n\n**Issues found**\n- Obsolete gating claim: \"Backlog until the AUTH.md review lands (card_61040a3e); promotes to todo then.\" The review (61040a3e) landed and was closed DONE on 2026-07-18. The card's own prior comment already says it stays backlog until an owner schedules it — the description contradicted the comment.\n- No cross-ref to `card_c7a70b64` Child C, which explicitly waits on this card (ResolveActor → Authenticator fallback wiring).\n\n**Changes made**\n- Retargeted the gating sentence: AUTH.md review has landed (61040a3e done); card remains parked until an owner schedules the XL sprint; reopening requires impl-discovers-reality.\n- Added a DOWNSTREAM note: landing this unblocks `card_c7a70b64` Child C (Service.ResolveActor as Authenticator default fallback), making the split-parent cross-ref consistent from both sides.\n- Pointed the bootstrap stopgap at `card_2680d5f7` (the debt card) for traceability.\n\n**Scope verdict**: keep-as-is — a correctly-bounded XL reference impl; the NOT-THIS-CARD list keeps it from ballooning.\n**Arch verdict**: aligned — additive HTTP middleware (no new route), hash-only token storage, identity-before-idempotency, and \"force never bypasses auth\" all match philosophy §3/§7/§9 and the stable-contract principle.","created_at":"2026-07-19T17:36:21.957995Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-10T06:25:56.369956Z","updated_at":"2026-07-19T17:36:21.957995Z","created_by":"claude","status_since":"2026-07-10T06:25:56.369956Z"},"type":"card"} +{"data":{"id":"card_3651b81b6f5542f99bf3f91bc5aa0e5f","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Site: storyline home page and a five-tab nav; catalog pages out of the nav","status":"backlog","fields":{"branch":"docs/refresh-site","description":"Rebuild docs/index.md around the storyline (parent card): hook + screenshot, why, the 'Definitions in, tracker out' side-by-side (the strongest existing asset — keep it and move it up), the one diagram from D4, then choose-your-path. Cut the 8-card documentation grid and the 4-card value grid to a single 3-way path chooser; move the themes gallery to guides/themes.md. Restructure mkdocs.yml nav to five tabs: Start (home, get-started) · Guide (concepts, the workflow, card definitions, workspace & boards, themes, walkthroughs) · Agents (mcp, instructions) · Build on it (extensions, events, API surface, data model, query DSL, OpenAPI) · Project (philosophy, built-vs-proposed audit, roadmap, design notes, changelog). Everything else (rollout history, design system, architecture internals, integrator reference until cddf3086 consolidates it, sprint plans) stays built but not_in_nav. Retire the 'v0.1.x · beta' badge text that disagrees with the v0.3.0 releases.\n\nAcceptance: nav ≤5 tabs, ≤20 pages; docs/index.md ≤150 lines; mkdocs build --strict passes; no page that was reachable before becomes a 404 (redirect or link from its old parent).","kind":"design"},"links":[{"type_id":"parent","target":"card_65f8ae3848c046b092cdbbb2da68871d","created_by":"claude","created_at":"2026-09-06T18:27:53.576554Z"},{"type_id":"related","target":"card_cddf308637ea45f2b101c281b3b9f6a7","created_by":"claude","created_at":"2026-09-06T18:27:53.66332Z"}],"version":3,"created_at":"2026-09-06T18:27:53.45546Z","updated_at":"2026-09-06T18:27:53.66332Z","created_by":"claude","status_since":"2026-09-06T18:27:53.45546Z"},"type":"card"} {"data":{"id":"card_3845a8346bee433690ac06ddca376062","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Sprint P4 — Workspace-loaded, discoverable, shareable themes","status":"done","fields":{"branch":"ui/workspace-themes","description":"User-value payoff (depends on P3). Make the theme sharing story real AND usable.\n\nSteps:\n- [x] Load named themes from definitions/themes/.{css,json}; validate at load time through P3 tokenizer guard; render through existing hooks with ZERO template restructuring.\n- [x] Land httpapi.New() signature change + reload.go:99 call site together; replace package-level themeFonts var with Server instance state.\n- [x] Install-by-reload: guided 422-on-malformed (body names file/line/unscoped rule) + per-generation cache-busting; reload regression test green.\n- [x] Make loaded themes discoverable + selectable in the UI nav switcher; unknown ?theme= names fall back to default.\n- [x] Keep theme-blind-templates + CSS-scoping tests green; docs/design/THEMES.md + DESIGN.md match shipped path.\n\nDemo: drop a theme folder into definitions/themes/, POST /v1/workspace/reload, pick it from nav switcher -> board reskins live no rebuild; break scoping + reload -> 422 naming file/line/rule.\nExit: workspace themes load+validate+render; discoverable/selectable in nav; graceful fallback; go test ./... green.","surface":"theme"},"owner":"jeremy","tags":["feature"],"links":[{"type_id":"related","target":"card_06a1c3c62b46496d9e91048ae446fa97","note":"Sprint P4 tracker; detail in target","created_by":"jeremy","created_at":"2026-07-07T20:52:32.112348Z"},{"type_id":"depends-on","target":"card_1f9bf7c4b8d24c9b9f1be88f0a6f3451","created_by":"jeremy","created_at":"2026-07-07T20:52:32.19448Z"}],"comments":[{"id":"cm_82d7ff2854d84295","author":"jeremy","body":"Consolidates existing card_06a1c3c62 (Themes step 2) which carries richer loader/extraction/per-board detail. Use THIS card for sprint P4 status; cross-referenced via link.","created_at":"2026-07-07T20:52:15.599545Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_0449151bb1864ac1","author":"jeremy","body":"DONE (commit fcb9844). Loader (config.loadThemes + themecss validation, warn+skip bad), uiStylesheet concatenation, httpapi.New() signature change across all call sites, themeFonts->per-Server instance, resolveTheme board layer (BoardPresentation.Theme), nav
picker, reload {themes}+{warnings}. Example theme 'ocean' added + verified in-browser (picker lists it, ?theme=ocean applies the gradient nav + tokens, no console errors). config coverage 51.3%->56.5% (P4 exit criterion, landed in the right phase). go test ./... green (14 pkgs), -race+vet clean.\n\nDeferred (NOT in P4 exit criteria): extracting embedded journal/labels blocks to files — tracked in card_3d8ed6d9f2c3474f848d8c0f7d1814b9.\n\nContract reconciliation: plan said '422-on-malformed' but THEMES.md guarantee 3 (documented in P3) says a broken theme warns + degrades to default, NEVER errors. Guarantee 3 wins: a bad theme is skipped with warnings in the 200 reload response (naming file/line/rule), workspace keeps serving. 422 stays for actual config load failures.","created_at":"2026-07-08T09:34:03.040325Z","edited_at":"0001-01-01T00:00:00Z"}],"version":9,"created_at":"2026-07-07T20:50:49.813283Z","updated_at":"2026-07-09T20:01:25.942685Z","created_by":"jeremy","status_since":"2026-07-09T20:01:25.942685Z"},"type":"card"} {"data":{"id":"card_39f0ea14797f4792997ffebe9f887874","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Core: release/unclaim primitive + force-move (transition-exempt)","status":"done","fields":{"branch":"feat/ops","description":"P0 — fixes the one-way-door problem found in dogfooding. (1) POST /cards/:id/release: clears owner, optionally reverts status, EXEMPT from transition enforcement, emits owner_changed+status_changed events. Inverse of claim. (2) Force-move: a `force` flag on PATCH status (or X-Work-Cards-Force header) that bypasses the transition graph but records the force-move in the event diff so the audit trail is honest. Core service + store + tests. No UI yet (separate card). Unblocks claiming-the-wrong-card recovery and retriage.","work_log":[{"author":"pi","commit_hash":"feat/release-force-move","entry_id":"ent_1e1eac82a33f4deb","notes":"Release (POST /cards/:id/release clears owner, inverse of claim) + Force flag on PatchCardRequest (bypasses enforced transitions). Tests added. Dogfooded: triaged 13 deferred cards to backlog.","timestamp":"2026-06-26"}]},"owner":"pi","tags":["bug"],"comments":[{"id":"cm_db138b7292be42b3","author":"foz","body":"PRIORITY P0 — dogfooding blocker. Do first, paired with the UI card. Verified pain: during Slice 3/4 I claimed a seeded card by accident and could not put it back (in_progress→todo is transition_illegal).","created_at":"2026-06-26T11:58:55.850758Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-06-26T11:58:55.849198Z","updated_at":"2026-06-26T13:21:00.674173Z","created_by":"foz","status_since":"2026-06-26T13:21:00.674173Z"},"type":"card"} {"data":{"id":"card_3d8ed6d9f2c3474f848d8c0f7d1814b9","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Extract embedded journal/labels themes into definitions/themes/ files","status":"backlog","fields":{"branch":"ui/extract-builtin-themes","description":"Deferred from Sprint P4 (workspace themes). P4 shipped the loader, per-board assignment, nav picker, install-by-reload, and an example workspace theme (ocean), but did NOT extract the two EMBEDDED themes (journal, labels) out of internal/httpapi/templates/style.css into definitions/themes/.{css,json}.\n\nWhy deferred: extraction is a large, mechanical CSS move (hundreds of lines, one contiguous block per theme) that is not required by P4's exit criteria and carries its own regression risk (the built-ins must keep working identically, and stay embedded as defaults so a bare 'cards init' still has them). Cleaner as its own card.\n\nDeliverable: move the journal + labels blocks out of style.css into example theme files; keep them embedded as defaults (embed.FS) so they load with zero config; convert builtinThemeFonts entries to their .json manifests; prove via a test that the extracted themes pass internal/themecss.Validate and render identically. Once done, builtinThemeFonts can shrink to nothing and all themes flow through one path.","surface":"theme"},"tags":["feature"],"links":[{"type_id":"related","target":"card_3845a8346bee433690ac06ddca376062","note":"deferred extraction from P4","created_by":"jeremy","created_at":"2026-07-08T09:34:03.006147Z"}],"comments":[{"id":"cm_bcb3e907845b4e1e","author":"jeremy","body":"Still correct as backlog behind loader. Do not pull into current UI polish wave — large mechanical CSS move, separate risk. After 06a1c3c6 lands cleanly.","created_at":"2026-07-09T23:25:58.689621Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-07-08T09:34:02.931389Z","updated_at":"2026-07-09T23:25:58.689621Z","created_by":"jeremy","status_since":"2026-07-08T09:34:02.931389Z"},"type":"card"} @@ -66,6 +68,7 @@ {"data":{"id":"card_3fd62d3280aa427faeaf6253b687c4c9","workspace_id":"demo","type_id":"api-task","schema_version":1,"title":"POST /v1/cards/query — JSON filter over HTTP (decided contract + endpoint)","status":"todo","fields":{"acceptance":"1) POST /v1/cards/query accepts a JSON filter body and returns the same page shape as GET /v1/cards; demo: curl resolves a card by its `branch` field with no client-side filtering. 2) Scope stays simple — filters, pagination, basic string matching. No new operators, no complexity bounds, no body cap. 3) A malformed filter still returns the existing structured 422 carrying `field: \"filter\"` (this already works via the take-next path; the new route must not lose it). 4) BOARD SCOPE stated plainly and contract-tested: board `default_filter` is ANDed with any caller filter and never widened, while explicit `type_id`/`status` REPLACE the board's type/column defaults — a test pins that board_id=X plus a status outside X.columns returns those cards, because a board is a view over the cards, not a fence around them. Docs note the mitigation for anyone wanting a hard boundary: put `type_id $in [...]` in default_filter. 5) Handler is NOT wrapped in withActor, matching GET /v1/cards. 6) Docs: query-dsl.md documents the endpoint and records GET ?filter= as declined for encoding and log leakage; api-surface.md gains the endpoint bullet; the implementation-status.md endpoint table lists it. 7) Route, internal/openapi/openapi.go paths() entry and the implementation-status table land in ONE commit — TestOpenAPICoversEveryRoute passes in both directions.","api_change":"additive","branch":"feat/cards-query-endpoint","description":"POST /v1/cards/query with a JSON filter body (declining GET ?filter=). Thin handler -> Service.ListCards; filter compiles in internal/sqlite/filter.go. Originally Sprint 07-22 Phase 2 (docs/plans/2026-07-22-sprint-plan.md) — committed there and never shipped; carried into the http-integration-surface sprint.\n\nScope of the endpoint: **simple queries only** — filters, pagination, basic string matching. It exposes the DSL that already exists; it does not grow one.\n\n## LOCKED DECISIONS (2026-08-08 sprint planning — do not reopen mid-implementation)\n\n**1. BOARD SCOPE IS A VIEW, NOT A FENCE. The old acceptance line was wrong and has been rewritten.**\n\nCards are the source of truth; boards are views on top of them. Boards and cards are not handed out separately — they are parts of one workspace. So a board's column and type lists are DEFAULTS for a query, and naming `type_id` or `status` explicitly replaces them. That is correct behavior for a view, not a leak.\n\nWhat the code actually does — `applyBoardScope` (internal/core/service.go:579-600) folds board scope three ways, and only ONE is a hard AND:\n- `TypeIDIn = b.CardTypeIDs` only if the caller set neither `TypeIDIn` nor `TypeID` (:582-584)\n- `StatusIn = b.Columns` only if the caller set neither `Status` nor `StatusIn` (:587-589)\n- `DefaultFilter` — UNCONDITIONAL AND (:593-599)\n\nThe previous acceptance criterion promised \"narrow-never-widen board isolation\" across all three. That describes a fence, which is not what a board is.\n\nDo NOT change applyBoardScope here. Tightening it to intersect would change GET /v1/cards, MCP list and the TUI, and it carries a live footgun: an empty intersection must yield an empty page, but `TypeIDIn = []` is read by the store as NO FILTER (warning at internal/core/service.go:1524-1526), so a careless intersect WIDENS scope to the whole workspace.\n\nThe contract test states the real behavior plainly: board_id=X plus a status outside X.columns returns those cards. A client author needs to know this, and today nothing tells them. Docs note that a board author who wants a hard type/column boundary puts `type_id $in [...]` / `status $in [...]` in `default_filter`, which already works.\n\n**2. NO COMPLEXITY BOUNDS, NO REQUEST-BODY CAP.** An earlier draft of this card locked filter depth/node/array limits and an http.MaxBytesReader body cap. Both are removed: this is a local, single-tenant, trusted service (philosophy principle 7, YOLO defaults) and hardening it against a hostile caller is permission theater. Keep it simple. If a deployment ever needs limits, that is extension territory — a proxy in front, or an extension — not core.\n\n**3. NO `withActor` WRAPPER.** Symmetry with GET /v1/cards, which is unwrapped: reads do not need actor context. (Do not justify this as \"it would 403\" — withActor only rejects when resolveActor comes back empty across header, env AND default_user; take-next uses it without trouble.)\n\n**4. `GET ?filter=` IS DECLINED, WITH A STATED REASON.** docs/spec/query-dsl.md:29-38 currently says it is \"not currently wired\" — a statement about wiring, not a decision. The rationale for declining lives only in docs/plans/2026-07-22-sprint-plan.md: **encoding and log leakage** — a URL-encoded filter lands in access logs and referrers. Carry that one-liner into the doc so \"declined\" is not a naked editorial claim.\n\n**5. ATOMIC MERGE.** Route + `paths()` entry in internal/openapi/openapi.go + the docs/reference/implementation-status.md endpoint table land in ONE commit. TestOpenAPICoversEveryRoute (internal/httpapi/openapi_coverage_test.go:37) fails in BOTH directions — a documented phantom route or an undocumented real one — so a half-merged card turns CI red for everyone.\n\n## REUSE (verified this session)\n\n- `core.TakeNextRequest` (internal/core/types.go:527-534) is the precedent for a JSON-decodable filter carrier: `Filter map[string]any \\`json:\"filter,omitempty\"\\``. `CardQuery` (types.go:426-453) has NO json tags — it is programmatic only, so a new request struct is required. CardQuery.Filter is at types.go:447.\n- `POST /v1/cards/take-next` (internal/httpapi/server.go:275) proves a static path coexists with /v1/cards/{id} under chi; there is not even a method collision.\n- Structured 422 for a malformed filter already works and is already tested: `core.NewValidationError` (internal/core/errors.go:38); end-to-end example to copy is TestTakeNextMalformedFilterIs422 (internal/httpapi/filter_test.go:10-21); the store-level assertion shape (Code `validation_failed`, Field `filter`) is internal/sqlite/filter_test.go:81-114. Nothing new is needed here — just do not lose it on the new route.\n- openapi.go's nearest template is the /cards/take-next entry (openapi.go:78-88), which already documents a `filter` object with the operator list.\n\n## FIXTURE GAP (the card's named test does not exist)\n\nThe card said `TestBoardDefaultFilter`. The real test is **TestBoardDefaultFilterScope** (internal/core/service_test.go:989-1015), and its `hipri` board is an in-process `core` fixture (service_test.go:61-66) — not reachable from package httpapi_test. NO board with a `default_filter` exists in examples/demo-workspace or .cards/definitions. An HTTP-level scope test therefore needs a new fixture; cheapest is the in-memory `newOffBoardTransitionServer` pattern (internal/httpapi/httpapi_test.go:48-83), NOT a new demo-workspace board (that would ripple into internal/httpapi/render_golden_test.go).\n\nSmall stale comments to fix while here: internal/sqlite/filter.go:16 and :98 say malformed DSL surfaces as \"HTTP 400\"; it is 422 (core/errors.go:38-40). internal/openapi/openapi.go:53 names a nonexistent test `TestPathsCoverEveryRoute` (real name: TestOpenAPICoversEveryRoute).\n\n## OUT OF SCOPE\n\nChanging applyBoardScope. Any query capability beyond filter + pagination + basic string matching. Any CLI surface for the endpoint — demo is curl. Rate limits, body caps, complexity bounds.\n\nNote: docs/spec/data-model.md:198-199 claims `card_type_ids` \"is merged into default_filter as type_id $in [...]\" — the code does not do this, and per decision 1 the code is right. That is a spec drift, recorded on the spec-drift card (2f12f16f), not here.\n\n## SHRINK VALVE\n\nThis card is the sprint's shrink valve. If capacity runs short it is cut and left linked-but-unstarted; health (fc92019c) and OpenAPI publication (8afb9008) carry the sprint's thesis. Cutting it breaks no existing client — the DSL is documented as not wired over HTTP today.","endpoint":"POST /v1/cards/query","verify":"go test ./internal/httpapi -run 'CardsQuery|OpenAPICoversEveryRoute'"},"links":[{"type_id":"related","target":"card_4c5326d4395745aa98eb241d4361be31","note":"carry-over: committed to Sprint 07-22 under this tracker, never shipped; parent is now the http-integration-surface tracker","created_by":"claude","created_at":"2026-08-08T09:50:19.133269Z"}],"comments":[{"id":"cm_873f89e9908642a1","author":"claude","body":"**Scope locked 2026-08-08 (sprint planning, http-integration-surface). Ships SECOND, and is the sprint's shrink valve.**\n\n**The acceptance criterion was rewritten because it described behavior this service does not have.** It promised \"narrow-never-widen board isolation, contract-tested\". Reading applyBoardScope (internal/core/service.go:579-600): only the `default_filter` leg is an unconditional AND (:593-599). The type and column legs are DEFAULTS — applied only if the caller set nothing (:582-589) — so an explicit `status` or `type_id` replaces the board's scope. `board_id=hipri&status=archived` returns cards in a status that is not a hipri column, today, on GET /v1/cards.\n\nWhy we are not fixing that here:\n- It is not a privilege escape. No auth exists; the same caller can GET /v1/cards with no board_id and see everything. A board is a lens, not a boundary (docs/spec/data-model.md:167).\n- Fixing it changes GET /v1/cards, MCP list and the TUI — a behavior-change sprint smuggled into a legibility sprint.\n- The naive fix is dangerous: an empty intersection must produce an empty page, but `TypeIDIn = []` is read by the store as *no filter* (warning at service.go:1524-1526). A botched intersect WIDENS scope to the whole workspace.\n\nSo the test pins the escape as contract instead. That is the honest move for a sprint whose thesis is \"you can build a client without reading Go\" — a client author needs to know this, and today nothing tells them.\n\nRelated spec/code conflict, filed separately, do NOT resolve here: docs/spec/data-model.md:198-199 claims `card_type_ids` \"is merged into default_filter as type_id $in [...]\". If that were literally true the type leg WOULD be a hard AND. The code does not do it. Whichever side moves needs its own changelog entry.\n\nCorrections to the card's own text, verified this session: the named test `TestBoardDefaultFilter` does not exist — it is `TestBoardDefaultFilterScope` (internal/core/service_test.go:989), and its hipri fixture is in-process to package core. No board with a default_filter exists in ANY loadable workspace, so the HTTP-level test needs a new fixture. CardQuery.Filter is types.go:447 (the 07-22 plan says :406); applyBoardScope is service.go:579-600 (the plan says :568-586).\n\nDSL bounds now carry numbers (depth 8 / nodes 64 / $in 100) so an implementer does not invent them mid-sprint, and they go in compileFilter so board filters and take-next get them too — not just this route.","created_at":"2026-08-08T09:48:12.505343Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_92fb37a54668479b","author":"claude","body":"**Citation correction (same session).** An earlier note here cited `docs/spec/data-model.md:167` for \"a board does not own cards\". That line is blank. The wording is at **docs/concepts/index.md:154** (\"A board is a Kanban lens over the workspace's cards. It does not own cards\") and is repeated at **docs/reference/workspace-and-boards.md:89**. The description has been corrected; the reasoning is unaffected.","created_at":"2026-08-08T09:51:26.533403Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_60e4100ff310439e","author":"claude","body":"**Scope corrected 2026-08-08 after review — two decisions removed.**\n\nAn earlier revision of this card locked filter complexity bounds (depth 8 / 64 nodes / 100-item $in) and an http.MaxBytesReader body cap. Both are **removed**. They were hardening against a hostile caller, which contradicts philosophy principle 7 (YOLO defaults — local-only, single-tenant, trusted; no permission theater). Queries here stay simple: filters, pagination, basic string matching. A deployment that genuinely needs limits puts a proxy in front or writes an extension — that is the documented escape hatch, and it keeps the core small.\n\nThe board-scope decision is unchanged but its reasoning is now the right one. The earlier draft argued from \"there is no auth so nothing is leaking\", which is a defensive framing. The actual reason: **cards are the source of truth and boards are views on top of them.** They are not handed out separately; they are parts of one workspace. So a board's columns and types are query defaults, and naming a status explicitly looks past them. That is what a view does.\n\nFollow-on: this also settles the docs/spec/data-model.md:198-199 conflict (it claims card_type_ids is merged into default_filter as a hard AND; the code treats it as a default). The code is right, the spec is drifted — recorded as a seventh item on the spec-drift card 2f12f16f rather than as its own card.","created_at":"2026-08-09T13:19:48.413908Z","edited_at":"0001-01-01T00:00:00Z"}],"version":13,"created_at":"2026-07-22T16:49:19.167876Z","updated_at":"2026-08-09T13:25:08.312141Z","created_by":"claude","status_since":"2026-07-22T16:49:19.167876Z"},"type":"card"} {"data":{"id":"card_440a2bed167f4a769d4058aae5c16ccd","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"UI: theme contract — html[data-theme] hook + named themes + reference default","status":"done","fields":{"branch":"feat/ui","description":"Implement the theme contract documented in docs/DESIGN.md §Theming. Add the html[data-theme=\"\"] hook (workspace/user-selectable named themes); a theme is a token remap + optional scoped rules on the stable hooks (:root tokens, [data-board], .card[data-type], component classes) and never requires markup changes. Class names and data-* attributes become a public API (renames = breaking). The default :root theme is the reference clean-UI implementation themes are judged against. Deliverables: data-theme plumbing (workspace setting -> attr), one example alternate theme shipped as proof, DESIGN.md updated from 'planned' to implemented, and a fix for the half-dead badge contract (--badge-wash unconsumed; TypeTheme.Icon renders as literal text instead of a mask glyph)."},"tags":["feature"],"comments":[{"id":"cm_ecbdf9c4b4134c03","author":"jeremy","body":"Core delivered: html[data-theme] hook implemented (httpapi.resolveTheme — ?theme= sticky cookie + workspace settings.theme default; data-theme on in layout.html, conditional web-font link). First named theme shipped as proof: 'journal' (handwritten/pastel/lined-notebook) — a full token remap + scoped component rules, no markup changes. DESIGN.md theme-contract table updated planned->implemented + named-themes section. RESIDUAL (still open): the badge contract cleanup this card also scoped — --badge-wash is emitted but unconsumed, and TypeTheme.Icon renders as literal text instead of a mask glyph.","created_at":"2026-07-02T14:46:02.11071718Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_0b640af5f3e440fb","author":"jeremy","body":"Badge-contract residual resolved (PR #5, merged): --badge-wash consumed, TypeTheme.Icon no longer literal text. Full card scope (html[data-theme] hook + journal theme + badge contract) now delivered. Moving to review.","created_at":"2026-07-02T15:41:16.11007224Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-02T12:07:25.377357699Z","updated_at":"2026-07-04T22:01:47.511564897Z","created_by":"jeremy","status_since":"2026-07-04T22:01:47.511564897Z"},"type":"card"} {"data":{"id":"card_4455eb345eca47e3836f31dc45611e6a","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Land column-census COUNT(*) fix on main (e8b5565 stranded on core/column-census)","status":"done","fields":{"branch":"core/column-census","description":"Residual from card_d39fd119 (accurate column census, closed done 2026-07-12): the fix (Store.CountCards COUNT(*), countColumn switched off the Limit:500 ListCards scan) exists as commit e8b5565 — but ONLY on branch core/column-census; it is not an ancestor of main. On main, countColumn still undercounts >500-card columns.\n\nDO: rebase/cherry-pick e8b5565 onto current main (the sqlite/service split and b23aff8 have moved underneath it), re-run the regression (TestBreaches* + census tests), land on main. Scope stays exactly the original card's: no wire/API/SPEC change, item-iterating scans + clampCardLimit untouched.\n\nACCEPTANCE: countColumn uses COUNT(*) on main; regression test present; git branch core/column-census deleted after landing.","kind":"bug"},"tags":["bug"],"links":[{"type_id":"related","target":"card_d39fd11975ef40cebeb3feafca2f331c","note":"lands the stranded census commit on main","created_by":"claude","created_at":"2026-07-12T01:54:14.678323Z"}],"comments":[{"id":"cm_34173b11628841b9","author":"claude","body":"DONE: e8b5565 cherry-picked onto main clean (628e9f8), merged in a3dec98. countColumn now COUNT(*) via Store.CountCards; census_test.go regression included; suite + -race green. core/column-census deleted locally; no remote copy existed.","created_at":"2026-07-12T03:04:33.771549Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-12T01:54:14.607416Z","updated_at":"2026-07-12T03:04:33.843818Z","created_by":"claude","status_since":"2026-07-12T03:04:33.843818Z"},"type":"card"} +{"data":{"id":"card_45a21e3fc22d431bb19e6d25e8d66bf9","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"README: cut to ≤120 lines — lead with the board, three-command start, link out","status":"backlog","fields":{"branch":"docs/refresh-readme","description":"Rewrite README.md to the shared storyline (parent card). Keep: one-sentence hook + board screenshot; the why paragraph; init/serve/open; one ≤15-line card-type example with the one-schema-everywhere point; choose-your-path links; install (binary + go install, Gatekeeper note); license. Drop from the README (they live on the site): Adding Cards to a project, Configuration, Syncing across machines, API and runtime behavior, Extensions, Project layout, Documentation and development, Releases — each replaced by one link line. Normalize the CLI examples: the README currently shows CARDS_URL both with and without the /v1 suffix (README.md:165 vs :221; both work, normalizeBase in internal/cli/client.go), pick the bare form everywhere. Write the CLI snippet against Sprint A's outcome (--if-match latest, --md) so it lands after 069ec1d1 and c0102825.\n\nAcceptance: wc -l README.md ≤ 120; every link resolves (site + repo); the three commands in the quick start work on a fresh checkout as written; CHANGELOG unaffected.","kind":"design"},"links":[{"type_id":"parent","target":"card_65f8ae3848c046b092cdbbb2da68871d","created_by":"claude","created_at":"2026-09-06T18:27:53.546008Z"},{"type_id":"depends-on","target":"card_069ec1d16a804b3286dfccb64d10d0e4","created_by":"claude","created_at":"2026-09-06T18:27:53.690669Z"}],"version":3,"created_at":"2026-09-06T18:27:53.420346Z","updated_at":"2026-09-06T18:27:53.690669Z","created_by":"claude","status_since":"2026-09-06T18:27:53.420346Z"},"type":"card"} {"data":{"id":"card_460ff3eca3eb4471950ca4cbe2cbdf2b","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Docs: pi-cards run-observation handover — pi-extension.md update, integration pointer, stale-claims guidance","status":"todo","fields":{"branch":"main","description":"Doc-only follow-ups from the 2026-07-24 pi-cards handover (client-perspective note; no kernel changes requested or accepted).\n\n1. docs/design/pi-extension.md — short additive subsection: observation is a CONVENTION on existing card writes (run-log markers '[pi-run ]' as the first line of work_log notes / comments; events: start | phase= | done | failed; coarse throttled phases), control (cancel/pause/steer) stays process-local in pi, and rich run folding lives in pi-cards' standalone inspector (catch-up + SSE), not cards' TUI. Link pi-cards docs/run-log.md and docs/control-deferred.md. Spec status 'built externally' stays.\n\n2. docs/events/integration.md (Observe or Coordinate) — one example pointer: 'agent run progress as marked work_log notes' — free-text conventions on existing verbs are the sanctioned way for clients to layer semantics without new event types; any consumer of item_appended/comment_added/owner_changed/status_changed can reconstruct runs.\n\n3. Agent-author guidance on stale claims (using-cards.md or integration.md Coordinate): a crashed worker leaves its card in_progress+owned BY DESIGN (take-next skips it; no auto-expiry in core). Clients should sweep their own actor's claims on startup (release to todo with an audit comment — pi-cards does this) or operators release manually.\n\nExplicitly out of scope (keep the boundary): no '[pi-run]' special-casing in the kernel, no ctl event types or runner routes, no validateEntry relaxation, no core claim TTL. Cross-session control stays a deferred pi-cards convention; document it only if a second independent control surface appears.","kind":"design"},"comments":[{"id":"cm_ada806826a8c4ba7","author":"claude","body":"pi-cards responded (2026-07-24) accepting all five points; their close-out will include two doc commitments this card's subsection can link to once shipped: (1) docs/run-log.md will state that inspector catch-up seeds from the materialized card (work_log + comments) with SSE as the live path only — matching 'the materialized card is the durable work product'; (2) docs/run-log.md gains a one-line compatibility rule: existing marker tokens (start/phase=/done/failed) frozen, new tokens additive-only, grammar changes require updating the shared fixtures in test/fixtures/run-log/. They also confirmed terminal comments always carry a human-readable body after the marker (failure paths being tightened). Cross-session control stays deferred on both sides — integration.md documents it only if a second control surface ever appears.","created_at":"2026-07-24T16:30:10.235029Z","edited_at":"0001-01-01T00:00:00Z"}],"version":3,"created_at":"2026-07-24T16:16:47.88054Z","updated_at":"2026-07-24T16:30:10.235029Z","created_by":"claude","status_since":"2026-07-24T16:16:57.030726Z"},"type":"card"} {"data":{"id":"card_469c93e23aa0407f866a5da0eb6aeaa1","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Docs example (stretch): changelog from cards — type_id grouped","status":"backlog","fields":{"branch":"docs/changelog-from-cards","description":"STRETCH (Sprint 07-22 Phase 4 optional) — docs/plans/2026-07-22-sprint-plan.md.\n\nscripts/changelog.sh (or similar): diff sorted backlog.jsonl between two tags; group closed cards by type_id (every card has type_id). Optional sub-group programming-task by kind. Do NOT group only by kind (misc bucket for 5/6 types).\n\nNot required for sprint close — PR-sync (fa6d5c2f) is the committed composition proof.\n","kind":"design"},"comments":[{"id":"cm_8901c45aeb644e9b","author":"foz","body":"**Quality review (batch A)**\n\n**Validated**\n- Seed exists in `docs/using-cards.md` 'Changelogs' — the example groups by `.fields.kind` via `cards list --status done | jq`.\n- `kind` is a real optional field on `programming-task` (`.cards/definitions/card-types/programming-task.json:9`), and `engineering.json` sets `style_field: \"kind\"`. Grouping by kind is grounded, not invented.\n- Snapshot is sorted one-card-per-line (`docs/using-cards.md` 'Conflicts'), so a tag-to-tag git diff is a valid source of newly-closed cards.\n- No `scripts/changelog.sh` exists today — genuine gap.\n\n**Issues found**\n- No done-when / verify.\n- \"since last tag\" mechanism was unspecified — could imply a non-existent `--since-tag` CLI flag.\n\n**Changes made**\n- Made the source explicit: diff the exported `backlog.jsonl` snapshot between two tags (no new CLI flag required).\n- Noted `kind` is optional and added a 'misc' fallback bucket.\n- Added SCOPE / OUT OF SCOPE / DONE WHEN / VERIFY.\n- Confirmed it ships as a documented example, not core behavior.\n\n**Scope verdict**: keep-as-is — one script + one docs snippet; correctly tiny.\n**Arch verdict**: aligned — example script over a built-in subcommand matches philosophy §1 (small core, big composition) and §5 (no engines; a script is an external subscriber).","created_at":"2026-07-19T17:36:21.982906Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_684f92ef005a481a","author":"claude","body":"Sprint 07-22 Phase 4 correction (tracker 4c5326d4, driven by P4 card ad67971f): group the changelog by type_id, NOT the kind enum — kind is defined only on programming-task, so kind-grouping dumps every other card type into a misc bucket that reads as broken on a mixed board. Optionally sub-group programming-task entries by kind. Implementation stays a read-only diff over the sorted backlog.jsonl snapshot between two tags. While here, resolve or explicitly document the incoherence this exposes: the engineering board's presentation.style_field:\"kind\" points at a field 5 of its 6 card types do not define. Title updated accordingly (was: kind-grouped).","created_at":"2026-07-22T16:51:01.73212Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-15T23:02:34.252431Z","updated_at":"2026-07-22T18:30:03.883473Z","created_by":"jeremy","status_since":"2026-07-15T23:02:34.252431Z"},"type":"card"} {"data":{"id":"card_470df9d792e449b39615dff05a583bea","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"pi-cards: read surface — list/get/search tools + text board (P0 exit)","status":"done","fields":{"branch":"feat/read-surface","description":"pi-cards series 3/8 — read surface: cards_list/get/search tools + text board (P0 exit). Spec §5.3, §6.1, §6.3. Blocked by card 2 (client).\n\nClient exists; this completes spec phase P0: the LLM and the user can both read the board from a pi session. Implements the three read tools and upgrades `/cards` from stub to a text board with subcommands, mode-aware (§5.3).\n\nScope:\n- `src/tools.ts`: `pi.registerTool` for `cards_list` (params `board_id`, `status`, `owner`, `type_id`; compact rows), `cards_get` (accepts short-id; full card incl. fields/links/comments/`version`), `cards_search` (`q`; compact rows). Typebox `Type.Object` parameters, `promptSnippet` + `promptGuidelines` naming each tool, output via `truncateHead`, errors thrown with structured cards body (§6.3).\n- `/cards` command (no args): text board grouped by status columns for the configured/default board (`board` config, §7); `/cards board ` switches; `/cards show ` prints one card as markdown (title, status, owner, fields, links, recent comments); `/cards search ` — `ctx.ui.select` picklist in tui/rpc mode, plain list in print/json.\n- `session_start` hook: resolve workspace + backend once via `resolve.ts`, `ctx.ui.setStatus(\"cards\", …)` when `ctx.hasUI`; `/cards connect [url]` re-resolves and reports.\n- `getArgumentCompletions`: subcommands, board ids, card short-ids cached from last list (§6.1).\n- `renderCall`/`renderResult`: compact styled rows using `theme.fg(\"accent\"|\"dim\", …)`.\n\nImplementation notes:\n- One shared `src/session.ts` state module (resolved client, actor, board, short-id cache) — later cards extend it. Follow the todo.ts state pattern (verified): keep reconstructable state in tool-result `details` and rebuild on `session_start`/`session_tree` by walking `ctx.sessionManager.getBranch()`; do NOT rely on `pi.appendEntry` for this.\n- `StringEnum` from `@earendil-works/pi-ai` for the `status` param, populated from the resolved board's columns.\n- Tool `execute(toolCallId, params, signal, onUpdate, ctx)` — `signal`/`onUpdate` may be undefined (verified against types.d.ts).\n- In `print`/`json` mode every path must work with zero prompts.\n\nAcceptance:\n- `pi -e ./src/index.ts -p \"/cards\"` against the demo workspace prints the 5-column text board (backlog→done) and exits 0.\n- `pi -e ./src/index.ts -p \"use the cards_get tool on card \"` returns full card JSON with `version`; `cards_search` for a known title returns the compact row.\n- With `CARDS_URL` unset, the CLI path works serverless against the fixture; with `cards serve` running, HTTP is selected and reported by `/cards connect`.\n- `node --test` + `npm run typecheck` green.\n\nOut of scope: interactive `ctx.ui.custom` board component, SSE (card 5); any mutation tool (card 4); `#id` autocomplete and message renderer (card 5).","kind":"feature","work_log":[{"author":"pi","commit_hash":"5fe5411","entry_id":"ent_5de0446607cd4e3d","notes":"Read surface complete on feat/read-surface: config.ts (user+project cards.json), session.ts (resolveSession: explicit URL→HTTP, probe id-match→HTTP, else CLI; short-id cache), format.ts, tools.ts (cards_list/get/search w/ truncateHead + structured-error rethrow + renderers), command.ts (/cards board|show|search|connect; transcript emit via pi.sendMessage; select picklist in UI modes), index.ts (session_start resolve + status). 21/21 tests green. pi -e acceptance VERIFIED against live demo workspace: text board prints 5 lanes, /cards show 470df9d7, /cards search works, LLM called cards_get and answered status+owner. NOTE: implemented directly after 3 consecutive subagent output-limit failures (agents burned budget pre-write); brief-file approach documented at /tmp/pi-cards-desc/brief-card3.md for retry with bigger-output models. [timestamp corrected 2026-07-18T16:26Z: was stamped local(+0200) as UTC]","timestamp":"2026-07-18T16:45:00Z"}]},"owner":"pi","links":[{"type_id":"depends-on","target":"card_fc89916f87be4907b219d3a0426411ed","created_by":"pi","created_at":"2026-07-18T15:10:46.456714Z"}],"comments":[{"id":"cm_c7221ece49db4d51","author":"pi","body":"DONE (pi). feat/read-surface @ 5fe5411. Acceptance verified live: /cards text board (HTTP mode against :8787), /cards show, /cards search, LLM-driven cards_get. 21/21 tests + tsc green. Deviations: (1) typed StringEnum status param deferred to card 8 (registration happens before workspace resolution); (2) CLI-mode e2e covered by card-2 integration tests + probe-fallback unit logic, not a live pi run (dev server owns :8787); (3) implemented directly — subagent delegation hit output limits 3x.","created_at":"2026-07-18T16:10:38.475229Z","edited_at":"0001-01-01T00:00:00Z"}],"version":8,"created_at":"2026-07-18T15:10:27.333633Z","updated_at":"2026-07-18T16:28:25.826668Z","created_by":"pi","status_since":"2026-07-18T16:10:38.526752Z"},"type":"card"} @@ -99,6 +102,7 @@ {"data":{"id":"card_632b6a1c85b84298a04de945d5f2ee7e","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"pi-cards compatibility follow-ups (Cards c3d8546)","status":"todo","fields":{"branch":"feat/pi-cards-compat-2026","description":"\"Five follow-ups from the Cards c3d8546 assessment:\\n1. SSE cursor preservation \\u2014 ephemeral id-less frames no longer reset the durable cursor; /v1/events catch-up added in board-ui (cards c3d8546, commit 7cd5bd1).\\n2. Paged /v1/events catch-up: page from cursor \\u2192 open SSE \\u2192 page once more to close the feed\\u2194live handoff race.\\n3. CLI stale-claim recovery is now flagged INCOMPLETE \\u2014 owner may remain set until /cards connect to HTTP (until the cards release verb ships; tracked as card_587b2bef\\u2026).\\n4. runBatch calls take-next once (cards strict transition filter makes {card:null} definitive).\\n5. Per-type work_log publish: fetch-then-append with one CAS retry already in place; no code change, audit recorded in CHANGELOG.\"","work_log":[{"author":"foz-pi","commit_hash":"uncommitted","entry_id":"ent_0b27e0249f424589","notes":"[pi-run pr_local_dev_d3adb33f start] compatibility follow-ups implementation","timestamp":"2026-07-25T12:25:00Z"},{"author":"foz-pi","commit_hash":"uncommitted","entry_id":"ent_3c93e6a83d014511","notes":"[pi-run pr_local_dev_d3adb33f done] CLI capability probe + cards release shim landed; HTTP path unchanged; legacy CLI fallback retained for unsupported/absent binaries.","timestamp":"2026-07-25T13:30:00Z"}]},"version":5,"created_at":"2026-07-25T12:21:58.904106Z","updated_at":"2026-07-25T14:54:47.511815Z","created_by":"local-dev","status_since":"2026-07-25T14:54:47.511815Z"},"type":"card"} {"data":{"id":"card_63d914a92d0c412aaca451f13d168871","workspace_id":"demo","type_id":"infra-task","schema_version":1,"title":"CI: build the docs in the PR gate, not only on deploy","status":"backlog","fields":{"branch":"none-yet","description":"`mkdocs build --strict` runs in exactly one place: .github/workflows/deploy-pages.yml:33, on push to main. The PR gate .github/workflows/ci.yml has jobs test / docaudit / lint / frontend and NEVER builds the docs.\n\nConsequence: a PR that breaks a nav entry, a cross-reference or a link goes green, merges, and then fails during deploy. The failure lands on whoever pushes next, not on the author, and the live site keeps serving the last good build so nobody notices quickly.\n\nDo: add a docs job to ci.yml mirroring the deploy job's setup (setup-python, pip install mkdocs-material, mkdocs build --strict). Cheap, but it adds a Python toolchain to every PR — hence its own card rather than a ride-along.\n\nFiled 2026-08-08 during sprint planning. Card 8afb9008 (publish OpenAPI on the docs site) assumed this gate already existed; it does not, and that card accepts post-merge detection deliberately rather than growing to include this.","environment":"ci"},"version":1,"created_at":"2026-08-08T09:49:35.679663Z","updated_at":"2026-08-08T09:49:35.679663Z","created_by":"claude","status_since":"2026-08-08T09:49:35.679663Z"},"type":"card"} {"data":{"id":"card_6545f8cdd12e4331ad836b2e28ef1958","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"CLI/demo: workspace init-vs-target friction + no artifact field in shipped types (Sprint A dogfood)","status":"done","fields":{"branch":"main","description":"ADOPTED AS PHASE 2 of docs/plans/sprint-2026-07-06.md (depends on P1). Original Sprint A dogfood findings, now scoped: turn two built-but-hidden capabilities on so the agent loop works from a fresh install. Deliverables: (1) ONE shared workspace resolver behind all three discovery paths (--workspace/$CARDS_WORKSPACE, init's positional arg, git-repo discovery) accepting BOTH a project root and its .cards child; when both X and X/.cards are valid workspaces, FAIL with a \"did you mean X/.cards?\" hint rather than guessing. Honest scope: --workspace at a project root already fails loudly today — this is accept-either-path convenience + a better hint. (2) An artifact-typed field in the shipped starter card type (schema_version stays 1) so `cards init NEW && cards attach ` works out of the box; one-paragraph upgrade note in the CLI reference for pre-existing workspaces. (3) Sync examples/demo-workspace card types with starter assets AND add a docaudit-pattern Go test that fails `go test ./...` if they diverge — no new CI workflow. (4) cmd/cards/workspace_test.go: every resolver branch (root / .cards / both-valid error / missing) from t.TempDir fixtures built by a mkWorkspace helper. Demo: fresh `cards init proj` then attach by short id via --workspace ./proj; a both-valid dir yields the actionable hint. Done when: fresh-install attach works, sync test green, resolver branches covered, suite green."},"links":[{"type_id":"depends-on","target":"card_b49d550a214547468ef0ea462cf5ea75","note":"P2 after P1: demo uses short-id attach (sprint 2026-07-06)","created_by":"local-dev","created_at":"2026-07-06T02:51:51.776523Z"}],"comments":[{"id":"cm_decdad02e7f84734","author":"local-dev","body":"Adopted as Phase 2 of docs/plans/sprint-2026-07-06.md (commit 40db9c4); description rescoped to the plan's deliverables + exit criteria. Sequenced after P1 (short-id parity) so the fresh-install demo can attach by short id.","created_at":"2026-07-06T02:52:07.039914Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_1a046ab35e074f6a","author":"local-dev","body":"Built + committed as 374723f (Phase 2). One shared workspace resolver behind --workspace/$CARDS_WORKSPACE/serve/init: accepts the project root or its .cards child; both-valid errors with the concrete choices; init refuses a target that is already a workspace. Starter task type ships an 'attachment' artifact field (demo workspace synced; docaudit Go test pins them together), so init→attach works from a fresh install — proven end-to-end by TestInitThenAttach_OutOfTheBox, which attaches BY SHORT ID on a freshly-seeded welcome card. CLI reference documents the pre-existing-workspace upgrade (one additive field, no version bump).","created_at":"2026-07-06T03:33:56.773573Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_e5154373323a44aa","author":"local-dev","body":"Sprint 2026-07-06 close-out: final gate green — go build ./..., go test -race ./... (all packages, 0 failures), go vet clean. Committed + pushed to origin/main. Moving review -> done. P2 shipped as 374723f.","created_at":"2026-07-06T08:52:44.690327Z","edited_at":"0001-01-01T00:00:00Z"}],"version":9,"created_at":"2026-07-05T23:49:09.309358665Z","updated_at":"2026-07-06T08:52:44.728226Z","created_by":"claude-code","status_since":"2026-07-06T08:52:44.728226Z"},"type":"card"} +{"data":{"id":"card_65f8ae3848c046b092cdbbb2da68871d","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Docs refresh: one storyline for the README and the site (parent)","status":"backlog","fields":{"branch":"docs/refresh","description":"The README (428 lines) and the site home (315 lines) open with the same two definitional paragraphs and then diverge: the README re-documents install, quick start, project adoption, configuration, sync, API, extensions, dev workflow and releases — most of it a second copy of site pages — while the site home stacks a value grid, a doc grid, a themes gallery and three CTAs. The nav has 7 tabs and 33 pages; Guide mixes concepts, reference and walkthroughs; Reference carries rollout history, design notes and roadmap next to the spec.\n\nOne storyline, shared by README and site home, in this order:\n1. Hook — a kanban board that lives in your repo and that your agents can drive. One screenshot.\n2. Why — todos are too little, a hosted tracker is too much; people, scripts and agents on one board; bad writes are rejected with what was allowed.\n3. Sixty seconds — init → open the board → one CLI write → point an agent at it (get-started is the spine).\n4. How it works — ONE diagram: definitions (JSON in git) + SQLite → HTTP / CLI / MCP / web UI / TUI.\n5. Choose your path — I use the board (Guide) · I'm wiring an agent (Agents) · I'm building on it (Build on it).\n\nAcceptance (the parent closes when all four children are done):\n- README ≤120 lines; nothing in it that is not also on the site, except install and license.\n- Site nav ≤5 tabs and ≤20 pages in nav; everything else reachable but not_in_nav.\n- Home page ≤150 lines with one diagram and one screenshot above the fold.\n- mkdocs build --strict passes; every README link resolves.\n- Tone: second person, present tense, no sentence over ~25 words in the hook and why sections.\n\nChildren: README cut (D2), home + nav (D3), visuals (D4), Using Cards split (D5). Sequence: D4 and D3 can start now; D2 and D5 land after Sprint A (069ec1d1, c0102825) so CLI examples show the final flags and don't churn.","kind":"design"},"version":1,"created_at":"2026-09-06T18:27:53.389555Z","updated_at":"2026-09-06T18:27:53.389555Z","created_by":"claude","status_since":"2026-09-06T18:27:53.389555Z"},"type":"card"} {"data":{"id":"card_6624625bde1e42bda6e31d6ebb89caec","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P7 — create-card + create-board forms on Alpine","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in bb71d68. createModal + boardCreate Alpine components replace wireCreateModal + wireBoardCreate. Idempotency-Key minted in x-init (triple-click → ONE card, verified). Structured errors → per-field [data-error-for] + valid_options hint + per-chip .is-invalid via multiselect.markInvalid (verified with injected 'watch' value). boardCreate uses server-seeded initial arrays (new ViewData.AllColumnIDs/AllTypeIDs + jsonAttr helper) because Alpine's x-model on checkbox arrays is authoritative and would otherwise uncheck all boxes at boot. All routed through cardsAPI (409/413/network handled). Verified: type picker → form flow, bad enum → field-scoped error, real board create redirects to /ui/boards/, no double-create. go test green, node 14/14, zero console errors.","platforms":["desktop","mobile","tablet"],"surface":"modal"},"owner":"jeremy","tags":["feature"],"version":5,"created_at":"2026-07-09T14:29:49.802839Z","updated_at":"2026-07-09T14:29:49.9982Z","created_by":"jeremy","status_since":"2026-07-09T14:29:49.9982Z"},"type":"card"} {"data":{"id":"card_66abd5e10b5e454183d35efee1bf39b4","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Docs: INTEGRATOR-REFERENCE.md — single-page code-verified integrator reference","status":"done","fields":{"branch":"docs/integrator-reference","description":"Single-page reference requested by picraft: data model, HTTP API, MCP surface, events, actor model, workspace/schema, extensions, non-goals boundary. Verified against source via parallel exploration. Tags [built]/[proposed]/[drift]. Corrects card_kind->type_id and no-board_id assumptions; documents SSE retention guarantee.","work_log":[{"author":"jeremy","commit_hash":"2beec1d","entry_id":"ent_eb16740c95444da2","notes":"Wrote docs/INTEGRATOR-REFERENCE.md from code (4 parallel verification passes). Surfaced doc/code drift -> filed 3 cards (MCP tools, workspace reload, service-kind). CI green.","timestamp":"2026-07-01"},{"author":"foz","commit_hash":"9a9170e071f28ec6855e2b7e2ba3f8b68c37b65b","entry_id":"ent_228b351e3c7149ef","notes":"[pi-run pr_mry89izg_a614a8a8 start] claimed by foz, batch 2/3","timestamp":"2026-07-24T00:54:24.605Z"},{"author":"foz","commit_hash":"9a9170e071f28ec6855e2b7e2ba3f8b68c37b65b","entry_id":"ent_0a071f073df8470f","notes":"[pi-run pr_mry89izg_a614a8a8 phase=summarizing]","timestamp":"2026-07-24T00:54:44.605Z"},{"author":"foz","commit_hash":"9a9170e071f28ec6855e2b7e2ba3f8b68c37b65b","entry_id":"ent_d2205685c37e4fe4","notes":"[pi-run pr_mry89izg_a614a8a8 done] → review","timestamp":"2026-07-24T01:02:57.919Z"}]},"owner":"foz","comments":[{"id":"cm_b8428064b3ea44d5","author":"foz","body":"[pi-run pr_mry89izg_a614a8a8 done] → review\n\ncompleted by foz (subagent run)\n\n.cards/backlog.jsonl | 37 ++--\n CHANGELOG.md | 34 ++++\n README.md | 29 ++-\n docs/README.md | 7 +-\n docs/architecture/index.md | 13 +-\n docs/design/fts-vs-like-disposition.md | 28 +--\n docs/events/core.md | 33 ++--\n docs/events/integration.md | 158 ++++++++++------\n docs/events/rollout.md | 13 +-\n docs/examples/software-delivery.md | 74 +++++---\n docs/reference/card-definitions.md | 14 +-\n docs/reference/implementation-status.md | 58 +++---\n docs/reference/workspace-and-boards.md | 13 +-\n docs/roadmap.md | 5 +-\n docs/spec/api-surface.md | 43 ++---\n docs/spec/data-model.md | 6 +-\n internal/config/config.go | 3 +\n internal/config/config_test.go | 23 +++\n internal/core/service.go | 52 +++++-\n internal/core/service_test.go | 313 ++++++++++++++++++++++++++++++++\n internal/core/validate.go | 41 ++++-\n internal/httpapi/feed_test.go | 53 ++++++\n internal/httpapi/httpapi_test.go | 78 ++++++++\n internal/httpapi/sse.go | 8 +-\n mkdocs.yml | 3 +\n 25 files changed, 912 insertions(+), 227 deletions(-)\n\nMy changes are the new file plus the two nav edits (the other modified docs were already dirty in the working tree before I started). Done.\n\n## Summary\n\n**What changed**\n- **Created `docs/reference/INTEGRATOR-REFERENCE.md`** — a single-page, code-verified integrator reference covering: data model, HTTP API endpoint table, MCP surface, events (incl. the SSE retention/replay guarantee), actor & identity model, git-defined workspace/schema, extensions, and the explicit non-goals boundary. Uses `[built]`/`[proposed]`/`[drift]` tags, and carries the two load-bearing corrections the card called for: the discriminator is `type_id` (not `card_kind`), and cards have **no** `board_id` (board membership is derived).\n- **Registered it in nav:** added a bullet in `docs/README.md` (Reference section) an","created_at":"2026-07-24T01:02:57.928416Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_75ddeda92f164c99","author":"local-dev","body":"Code review (2026-07-24): INTEGRATOR-REFERENCE.md corrected take-next owner registration (matches §5 + card_1c877e6). File still untracked; dedupe vs implementation-status.md remains follow-up before closing.","created_at":"2026-07-24T01:31:06.60411Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_81d2ffd9651e40a7","author":"local-dev","body":"Closed after 51de9dd: INTEGRATOR-REFERENCE.md committed and nav wired; take-next owner drift documented (fix tracked on card_1c877e6).","created_at":"2026-07-24T01:32:53.599237Z","edited_at":"0001-01-01T00:00:00Z"}],"version":15,"created_at":"2026-06-30T22:49:28.177619184Z","updated_at":"2026-07-24T01:32:53.599237Z","created_by":"jeremy","status_since":"2026-07-24T01:32:53.524776Z"},"type":"card"} {"data":{"id":"card_670dab61ea454d2dbb3fe39d270f8200","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Sprint 07-22 P1: foundation hygiene — MCP doc, DoD on tracker","status":"todo","fields":{"branch":"chore/sprint-0722-p1-hygiene","description":"Sprint 07-22 Phase 1 of docs/plans/2026-07-22-sprint-plan.md — enabling work only (no user demo).\n\n## Context (revised plan)\n\nDropped: backlog.md regen (30026459 already under done; board.sh exports JSONL only). Deferred: writeDefinitionAndReload until P5 stretch adds POST /v1/card-types on reload seam.\n\n## DO\n\n1. Document cards mcp no-reload: tools fixed at process start (serve.go → mcp.New); reconnect recovery; optional-default field behavior in docs/extensions/mcp.md.\n2. Record DoD rules (1)+(2) on sprint tracker card_4c5326d4 — NOT card_b3079e56.\n\n## Exit\n\n- MCP staleness doc merged\n- DoD on 4c5326d4\n- No writeDefinitionAndReload / buildDSN in P1\n","kind":"infra"},"links":[{"type_id":"parent","target":"card_4c5326d4395745aa98eb241d4361be31","note":"sprint 07-22 phase card","created_by":"claude","created_at":"2026-07-22T16:50:08.04158Z"}],"comments":[{"id":"cm_5298a956af4b4468","author":"claude","body":"DO 1 (regenerate backlog.md) was already satisfied by the 2026-07-22 board-sync commit that landed this sprint's cards — the overview now lists 30026459 under done. When picking this card up: verify with grep (exit criterion stands) and skip straight to DO 2.","created_at":"2026-07-22T16:52:42.229742Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_9898b41fbeb84272","author":"local-dev","body":"Revised plan: drop backlog.md regen (already done); defer writeDefinitionAndReload to P5 stretch. Pick up DO 1 MCP doc + DO 2 DoD on card_4c5326d4 only.","created_at":"2026-07-22T18:30:06.882316Z","edited_at":"0001-01-01T00:00:00Z"}],"version":5,"created_at":"2026-07-22T16:49:19.12274Z","updated_at":"2026-07-22T18:30:06.882316Z","created_by":"claude","status_since":"2026-07-22T16:49:19.12274Z"},"type":"card"} @@ -157,6 +161,7 @@ {"data":{"id":"card_a1bd49a30b984d40bfce52fe8233ad8a","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Fix idempotency: wrong actor + schema PK allows cross-actor overwrite","status":"done","fields":{"branch":"fix/code-review","description":"Two compounding bugs. (1) The idempotent() wrapper calls actorFromCtx() which reads context+default user, but withActor() hasn't run yet (idempotent is the OUTER wrapper), so lookup/record always use default_user regardless of X-Work-Cards-Actor header. Fix: use resolveActor() (reads header/env/default) in the idempotent wrapper. (2) idempotency_keys PK is `key` only; GetIdempotency filters key+actor but INSERT OR REPLACE overwrites another actor's row for the same key. Fix: PRIMARY KEY (key, actor). Add tests with X-Work-Cards-Actor != default user.","work_log":[{"author":"pi","commit_hash":"fix/idempotency-actor","entry_id":"ent_9cfa0d7abe3e4917","notes":"Fixed two compounding bugs: (1) wrapper order was idempotent(withActor(...)) — idempotent ran before withActor set the context, so actor was always default_user. Swapped to withActor(idempotent(...)). (2) idempotency_keys PK was just (key) — two actors with the same key collided. Changed to composite PK (key, actor). Regression test: TestIdempotencyActorScoping verifies two actors with the same key get distinct cards, and replay is scoped per actor. Verified end-to-end against live server.","timestamp":"2026-06-26"}]},"owner":"pi","tags":["bug"],"comments":[{"id":"cm_effe009db4f84097","author":"foz","body":"Verified. Routes: s.idempotent(s.withActor(...)) — idempotent is OUTER, runs before withActor sets the context. idempotent() calls s.actorFromCtx(r) which reads core.ActorFromCtx (context) → falls back to default_user, ignoring X-Work-Cards-Actor. So all idempotency records are scoped to default_user. Schema: idempotency_keys PK is `key` only (sqlite.go:103); GetIdempotency filters key+actor but PutIdempotency uses INSERT OR REPLACE on key → overwrites cross-actor. Existing test passes only because it uses 'local-dev' (the default user). Fix: (1) use resolveActor() in idempotent wrapper; (2) PRIMARY KEY (key, actor).","created_at":"2026-06-26T11:21:45.175806Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-06-26T11:21:00.429122Z","updated_at":"2026-06-26T13:24:42.134355Z","created_by":"foz","status_since":"2026-06-26T13:24:42.134355Z"},"type":"card"} {"data":{"id":"card_a21414e31d2b493db8f03fd21cafc0a7","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Hooks: parse extensions.yaml + declare hook table","status":"done","fields":{"branch":"feat/slice4","description":"Hooks: parse extensions.yaml + declare hook table","work_log":[{"author":"foz","commit_hash":"slice4","entry_id":"ent_ee64ff5ef4c747a5","notes":"DONE: config.LoadExtensions (yaml+json), HookFilter with board/type/card/to_status/from_status, validate at load.","timestamp":"2026-06-26T09:00:00Z"}]},"owner":"foz","tags":["feature"],"comments":[{"id":"cm_ae26d82e8302432c","author":"foz","body":"Closed: config.LoadExtensions (yaml+json), HookFilter (board/type/card/to_status/from_status), validate at load. extensions.json added to demo workspace.","created_at":"2026-06-26T09:25:42.468993Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-06-26T08:48:13.71503Z","updated_at":"2026-06-26T09:25:42.468993Z","created_by":"foz","status_since":"2026-06-26T08:58:03.090169Z"},"type":"card"} {"data":{"id":"card_a37517208e3447d38e92851c76c29a14","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"pi-cards: git board persistence + interim worklog activity (P4a)","status":"done","fields":{"branch":"feat/git-sync","description":"pi-cards series 7/8 — git board persistence + interim worklog activity (P4a). Spec §6.6, §6.7. Blocked by card 6 (execution).\n\nExecution lands activity on cards; this adds G5 and the worklog-on-turn option: board state rides in git via the CLI exporter, never hand-written, with opt-in auto-export cadences.\n\nScope:\n- `src/sync.ts`: `exportBoard()` — if `/scripts/board.sh` exists, shell out to `board.sh export`; else run `cards export --state-only --out /backlog.jsonl` directly; then `git add backlog.jsonl && git commit -m \"cards: board sync ( cards)\"`, guarded by `git diff --quiet` (commit only when the export changed something). Never push.\n- `/cards export` command; board UI `g` key wired to it.\n- Auto mode per config `autoExport`: `\"shutdown\"` → `session_shutdown` hook; `\"idle\"` → debounced `agent_settled` hook firing only after any `cards_*` mutation this session (track a dirty flag in `session.ts`); `\"off\"` default.\n- `worklogOnTurn:true` → debounced `turn_end` handler: while a card is claimed by this session and `git log --oneline` shows new commits since the last entry, append an interim `work_log` note via `cards_append_entry`. Default off; close-out entries from card 6 unaffected.\n\nImplementation notes:\n- All git ops via `pi.exec(\"git\", [...], {timeout})` in the workspace repo root; surface stderr verbatim on failure, never retry a commit.\n- `backlog.jsonl` is written only by the exporter (repo rule, §6.6) — no manual edits anywhere in the extension.\n- Shutdown hook must be fast and non-interactive: skip export if backend resolution failed this session.\n\nAcceptance:\n- In a git-initialized fixture repo: mutate a card, run `pi -e ./src/index.ts -p \"/cards export\"` → `git log --oneline` shows `cards: board sync ( cards)` and the exported `backlog.jsonl` re-imports into a fresh workspace successfully.\n- Second `/cards export` with no changes → no new commit (diff-quiet guard).\n- `autoExport:\"shutdown\"` config → session exit produces the commit; `worklogOnTurn:true` + a commit mid-run → interim work_log entry visible on the card.\n- `node --test` + typecheck green (git ops unit-tested against a temp repo).\n\nOut of scope: auto-push, PR creation, pre-commit hook installation (board.sh's job, user's choice); conflict resolution for diverged `backlog.jsonl`.","kind":"feature","work_log":[{"author":"pi","commit_hash":"4c2eac4","entry_id":"ent_672268274d10434f","notes":"Git sync complete on feat/git-sync: sync.ts (exportBoard w/ board.sh preference + diff-quiet guard + realpathSync fix), sync-register.ts (autoExport shutdown/idle hooks, worklogOnTurn interim entries), session dirty/activeWorkCard/lastSeenHead flags, /cards export + board g key. 61/61 tests green; live e2e /cards export committed snapshot in fresh fixture repo.","timestamp":"2026-07-18T19:38:46Z"}]},"owner":"pi","links":[{"type_id":"depends-on","target":"card_00581679d72843898be27b343c2d465c","created_by":"pi","created_at":"2026-07-18T15:10:46.548251Z"}],"comments":[{"id":"cm_5eb81cafd4ed4fcc","author":"pi","body":"DONE (pi). feat/git-sync merged @ main, 61/61 tests + tsc green. Live e2e: /cards export in a fresh fixture repo produced 'cards: board sync (10 cards)' commit with backlog.jsonl. macOS symlink gotcha found + fixed (realpathSync). autoExport + worklogOnTurn are opt-in (defaults off) and hook-tested via unit seams.","created_at":"2026-07-18T19:38:46.704332Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-18T15:10:27.532136Z","updated_at":"2026-07-18T19:38:46.74979Z","created_by":"pi","status_since":"2026-07-18T19:38:46.74979Z"},"type":"card"} +{"data":{"id":"card_a3adf5b556c74534beec7926f80360a7","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Docs visuals: 'one schema, five surfaces' diagram, refreshed screenshots, agent-loop demo","status":"backlog","fields":{"branch":"docs/refresh-visuals","description":"Three visuals the storyline needs and the docs don't have: (1) ONE diagram — definitions/ (JSON in git) + work-cards.db → one service layer → HTTP, CLI, MCP, web UI, TUI — as an SVG under docs/assets/ that renders in light and dark (mkdocs Material palette toggle); used on the site home and linked from the README. (2) Refreshed screenshots at one consistent size and theme: the welcome board right after cards init, the card detail with a work_log feed, the TUI board — replacing media/board.png (dev build with a stale 'Breaches' nav item) and the current mixed set under docs/assets/img/. (3) An agent-loop demo: a short terminal recording or an annotated four-step sequence (take-next → work → patch --if-match latest → comment) for the Agents section. Prefer SVG and PNG over GIF for size; if a cast is used, keep it under 30 seconds.\n\nAcceptance: the diagram appears on docs/index.md and is readable at 700px wide in both palettes; each screenshot's source command is recorded in a comment on this card so they can be regenerated; total added asset size < 1.5 MB.","kind":"design"},"links":[{"type_id":"parent","target":"card_65f8ae3848c046b092cdbbb2da68871d","created_by":"claude","created_at":"2026-09-06T18:27:53.610147Z"}],"version":2,"created_at":"2026-09-06T18:27:53.485779Z","updated_at":"2026-09-06T18:27:53.610147Z","created_by":"claude","status_since":"2026-09-06T18:27:53.485779Z"},"type":"card"} {"data":{"id":"card_a810025b1d964ef9b24b66e2bab53783","workspace_id":"demo","type_id":"api-task","schema_version":1,"title":"Security: escape/validate filter key paths in the filter DSL (SQL injection)","status":"done","fields":{"acceptance":"Filter key outside [A-Za-z0-9_] (after optional fields. prefix) → validation_failed on field 'filter' (HTTP 400), never reaches SQLite; legit fields. and bare-field filters unchanged; regression test bites without the fix. go test ./internal/sqlite green.","api_change":"fix","branch":"fix/filter-key-sql-injection","description":"Found by the Copilot review of PR #18 (https://github.com/somebox/cards/pull/18#issuecomment-4935628132), verified real.\n\nPROBLEM: internal/sqlite/filter.go columnExpr interpolated a client-supplied filter key straight into the '$.' JSON-path string literal with no escaping. The VALUE side is parameterized (?), but the PATH side was not. A filter key like fields.x') OR 1=1-- broke out of the literal — confirmed by a temporary test run producing 'SQL logic error: incomplete input' and 'near \";\": syntax error'. Pre-existing on main; the new $has operator (Phase 3) extended the same pattern. Not exploitable under YOLO/local-only trust, but the filter DSL is a public API surface and should land before --auth token (AUTH.md).\n\nFIX: validate the field-id portion of the key against ^[A-Za-z0-9_]+$ (mirrors core.sortFieldRE in sort.go) before interpolation; reject anything else as a filter validation error (surfaces as HTTP 400). Collapses the fields. and bare-key branches through one guard. Legit keys unchanged.\n\nTEST: three injection/invalid keys added to TestFilterDSLMalformedIsValidationError (break-out attempt, DROP TABLE attempt, dotted id). Proven non-vacuous — fails without the fix, passes with it. Full suite + vet green.","endpoint":"GET /v1/cards?filter= (and any list endpoint compiling the filter DSL)","verify":"go test ./internal/sqlite ./...; the added cases in TestFilterDSLMalformedIsValidationError"},"comments":[{"id":"cm_34775f0a6cc74c3b","author":"claude","body":"Post-merge review disposition 2026-07-12: fix is on main as ccc05a2 (fieldIDRE guard in internal/sqlite/filter.go:99 + injection cases in TestFilterDSLMalformedIsValidationError). Acceptance met per review; moving to review for human accept alongside the P0a pile.","created_at":"2026-07-12T00:10:23.55132Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_d53e9ad633a44854","author":"claude","body":"Human-accepted 2026-07-12. Verifying commit ccc05a2 on main (fieldIDRE guard + non-vacuous injection regression tests).","created_at":"2026-07-12T01:54:14.77389Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-10T14:18:28.04277Z","updated_at":"2026-07-12T01:54:14.812311Z","created_by":"claude","status_since":"2026-07-12T01:54:14.812311Z"},"type":"card"} {"data":{"id":"card_a9bfec52aa52414aacbdd4794a2126d1","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"DEMO blocker","status":"done","fields":{"branch":"b","description":"d"},"version":4,"created_at":"2026-07-04T23:42:34.463347808Z","updated_at":"2026-07-04T23:43:30.623447091Z","created_by":"local-dev","status_since":"2026-07-04T23:43:30.623447091Z"},"type":"card"} {"data":{"id":"card_a9cc1a1dbb404f62a47b77a985ea5df8","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Core/CLI: serverless CLI — auto-select backend (direct core+sqlite vs HTTP)","status":"done","fields":{"branch":"design/serverless-cli","description":"Let read/write CLI commands work without a running HTTP server, in addition to (not instead of) the server.\n\nAlready partly true: the transport-agnostic core (internal/core) is shared by all surfaces, and export/import/mcp already skip HTTP — they call sqlite.Open + core.NewService directly against the workspace DB. Extending list/get/create/patch/... to do the same is mostly wiring.\n\nWhy: zero-config one-shots (`cards list` in a .cards/ dir just works, matching the new git-style workspace resolution); lower latency; no port/URL bookkeeping.\n\nTwo catches that mean HTTP cannot simply be dropped: (1) the event bus is in-process — a CLI writing directly to the DB bypasses a running server bus, so SSE live updates and status_changed hooks would not fire; (2) SQLite is single-writer across processes — direct writes while `cards serve` holds the DB risk SQLITE_BUSY/lock contention (WAL allows concurrent readers).\n\nClean design (matches ARCHITECTURE notes): CLI auto-selects its backend — if CARDS_URL is set or a server is detected, use HTTP so events/hooks/SSE stay correct; otherwise open core+sqlite directly for a fast serverless one-shot. Cost: a small backend interface the CLI commands target, with HTTP and direct-service implementations behind it.","work_log":[{"author":"jeremy","commit_hash":"d880663","entry_id":"ent_218892b7304a4374","notes":"Implemented via a cli.Transport seam: in-process backend runs the real /v1 router (httptest) against the resolved workspace; HTTP backend used when CARDS_URL set. No CARDS_URL => serverless. Reuses full handler/service stack, zero duplication. Tested + e2e verified.","timestamp":"2026-06-30T15:00:00Z"},{"author":"foz","commit_hash":"9a9170e071f28ec6855e2b7e2ba3f8b68c37b65b","entry_id":"ent_22cd5d90cb714bd1","notes":"## Summary\n\n**Already implemented** (commit `d880663` and follow-ons). No code changes needed.\n\n### What’s in tree\n- **`cli.Transport`** seam (`internal/cli/client.go`): HTTP vs custom backends\n- **Serverless backend** (`cmd/cards/directcli.go`): in-process `/v1` router via `httptest` against the resolved workspace\n- **Selection** (`cmd/cards/main.go`): `CARDS_URL`/`--url` → HTTP; otherwise direct core+sqlite\n- Workspace resolution: `--workspace` → `$CARDS_WORKSPACE` → nearest `.cards/` → `~/.cards`\n- Docs: `docs/reference/cli.md`, architecture notes\n\n### Design choice vs card text\nCard text mentions “or a server is detected.” The landed design is **opt-in only** (`CARDS_URL`/`--url`) — no silent probe of `:8787`. Docs recommend pointing at a running server so bus/SSE/hooks stay correct; W","timestamp":"2026-07-23T02:35:17.147Z"}]},"owner":"foz","links":[{"type_id":"related","target":"card_b86c7fe97c124dc9b4073da79e124600","note":"Extends zero-config: serverless one-shots in a .cards/ dir.","created_by":"jeremy","created_at":"2026-06-30T10:40:03.108059785Z"}],"comments":[{"id":"cm_cf10cc8dbd874f60","author":"foz","body":"completed by foz (subagent run)\n\n## Summary\n\n**Already implemented** (commit `d880663` and follow-ons). No code changes needed.\n\n### What’s in tree\n- **`cli.Transport`** seam (`internal/cli/client.go`): HTTP vs custom backends\n- **Serverless backend** (`cmd/cards/directcli.go`): in-process `/v1` router via `httptest` against the resolved workspace\n- **Selection** (`cmd/cards/main.go`): `CARDS_URL`/`--url` → HTTP; otherwise direct core+sqlite\n- Workspace resolution: `--workspace` → `$CARDS_WORKSPACE` → nearest `.cards/` → `~/.cards`\n- Docs: `docs/reference/cli.md`, architecture notes\n\n### Design choice vs card text\nCard text mentions “or a server is detected.” The landed design is **opt-in only** (`CARDS_URL`/`--url`) — no silent probe of `:8787`. Docs recommend pointing at a running server so bus/SSE/hooks stay correct; W","created_at":"2026-07-23T02:35:17.172063Z","edited_at":"0001-01-01T00:00:00Z"}],"version":12,"created_at":"2026-06-30T10:39:55.443967566Z","updated_at":"2026-07-26T13:28:01.35913Z","created_by":"jeremy","status_since":"2026-07-26T13:28:01.35913Z"},"type":"card"} @@ -216,7 +221,7 @@ {"data":{"id":"card_e71a64fab0ba4e02b8b7468a2c12518d","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"UI/themes: remove labels template branch — make themes CSS-only (prereq for extraction)","status":"done","fields":{"branch":"feat/themes-css-only","description":"Prerequisite for extracting themes out of the embedded style.css (see docs/design/THEMES.md). The labels theme currently branches the shared card_body template ({{if eq $.Theme \"labels\"}} in card_modal.html) for its two-row detail header. A theme that touches markup cannot be a standalone CSS file, so the contract is not honestly CSS-only yet.\n\nDo: promote the labels detail header layout (icon | title+meta | close) to stable hooks in the SHARED template — emit icon/title/actions/meta with stable classes + data attributes for every theme. Let labels.css lay them out as the two-row attribute table; the default theme lays them out as today single meta line. Default theme appearance must not change. Then delete the {{if eq $.Theme}} branch so labels is pure CSS.\n\nDoD: no {{if eq $.Theme}} in card_modal.html; labels detail header reproduced entirely from labels.css + shared template hooks; default + journal detail views unchanged; go test ./internal/httpapi green."},"links":[{"type_id":"related","target":"card_440a2bed167f4a769d4058aae5c16ccd","note":"builds on theme contract","created_by":"local-dev","created_at":"2026-07-06T22:01:13.203488Z"},{"type_id":"related","target":"card_8b3e83d902a045279233468a3d04c698","note":"both extend the data-icon/type-theme hook; style_field is independent of where theme CSS lives","created_by":"local-dev","created_at":"2026-07-06T22:01:26.643855Z"}],"comments":[{"id":"cm_ac7b988a2bd642fc","author":"local-dev","body":"Design doc: docs/design/THEMES.md (exploration). Sequencing: (1) this card — kill the labels template branch so themes are CSS-only; (2) follow-up — move journal/labels blocks into definitions/themes/*.css + load+concat in httpapi.uiStylesheet, register names with resolveTheme; (3) defer --themes-dir overlay until requested.","created_at":"2026-07-06T22:01:42.612727Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_6e5dbe70df914a33","author":"local-dev","body":"Built + committed as 0eb5dee (UI sprint P3 / THEMES.md step 1). Templates are theme-blind — grep for theme conditionals returns 0, pinned by TestTemplatesAreThemeBlind. The labels detail header is reproduced ENTIRELY in CSS from shared hooks: .modal__type-icon cell, meta key/value items with data-meta ids, display:contents flattening the tail into the 5-cell attribute grid, editable status/owner styled as cells. Default theme anatomy unchanged (keys hidden, classic 'updated' inline; one addition: the subtle copy-id ⧉, generalized from the labels design). Fonts moved to a Go data manifest ({{with themeFonts .Theme}}) — step 2 (definitions/themes/.css+json loader) is now unblocked and should be filed as the follow-up card THEMES.md describes. Also TestStyleCSSBalanced guards the brace balance (mutation-proven), motivated by the dropped-brace incident that silently ate this very theme.","created_at":"2026-07-07T11:08:50.150243Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_7348926f8d794a9e","author":"local-dev","body":"Close-out verification (per maintainer request): refactoring is complete and the template layer is ready for the next phases. Checked: (1) TestTemplatesAreThemeBlind + TestStyleCSSBalanced green; (2) zero stale references to the deleted labels templates/classes anywhere in templates or CSS; (3) the labels theme CSS is ONE contiguous block (style.css lines ~1080-1424, no other theme's rules interleaved) — precisely the shape THEMES.md step 2 needs for extraction to definitions/themes/labels.css; the font manifest (themeFonts map) is the interim form of labels.json. Step 2 (workspace-loaded theme files + loader + name registration) is unblocked and worth filing as its own card. Moving to done.","created_at":"2026-07-07T14:55:43.477136Z","edited_at":"0001-01-01T00:00:00Z"}],"version":9,"created_at":"2026-07-06T21:59:51.69154Z","updated_at":"2026-07-07T14:55:43.517105Z","created_by":"local-dev","status_since":"2026-07-07T14:55:43.517105Z"},"type":"card"} {"data":{"id":"card_e79fb682c52f4f70999d536afb0358bd","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Core/Board: enforce board default_filter (parsed but never applied) + deterministic board selection","status":"done","fields":{"branch":"feat/workspace-concepts","description":"Board.default_filter was declared + documented (SPEC §9) but applyBoardScope never applied it, so board_id queries ignored it. Fold it in as a hard scope (AND-ed with any caller filter; can narrow, never widen). Also fixed boardForTypeID picking the governing board via randomized map iteration — now sorted by id, prefers a transition-enforcing board. Unblocks per-sub-app board isolation over a shared card type.","work_log":[{"author":"jeremy","commit_hash":"ddcae2d6debd1901a9d0bd1cf4ee5b58d1ef1b57","entry_id":"ent_94889ba35855413b","notes":"Enforced board default_filter as a hard query boundary and made governing-board selection deterministic.","timestamp":"2026-06-29T22:20:00Z"},{"author":"foz","commit_hash":"ddcae2d6debd1901a9d0bd1cf4ee5b58d1ef1b57","entry_id":"ent_28545ddcde2d4c23","notes":"Verified default_filter isolation with focused repeated tests. Dedicated tie-break coverage for boardForTypeID remains the review item.","timestamp":"2026-07-22T19:31:12.945Z"}]},"owner":"foz","links":[{"type_id":"related","target":"card_2578a31e63dc4e3995514e915c812a9d","note":"Same bug class: a declared filter parsed but never applied.","created_by":"jeremy","created_at":"2026-06-29T20:36:58.982599818Z"}],"comments":[{"id":"cm_99c2020f15664f23","author":"foz","body":"completed by foz (subagent run)\n\n.cards/backlog.jsonl | 16 +--\n README.md | 11 +-\n docs/concepts/index.md | 271 +++++++++++++++++++++++++++++------\n docs/examples/software-delivery.md | 8 +-\n docs/plans/2026-07-22-sprint-plan.md | 228 ++++++++++++++---------------\n docs/reference/card-definitions.md | 6 +-\n docs/spec/api-surface.md | 4 +-\n 7 files changed, 358 insertions(+), 186 deletions(-)\n\n**Summary:** This card is already implemented on `main` (commit `ddcae2d`).\n\n### What’s in place\n1. **`applyBoardScope`** (`internal/core/service.go`) ANDs the board’s `default_filter` with any caller filter (hard scope — can narrow, not widen).\n2. **`boardForTypeID`** (`internal/core/validate.go`) sorts board IDs and prefers a transition-enforcing board instead of map iteration order.\n3. **`TestBoardDefaultFilterScope`** covers isolation + non-widen semantics via the `hipri` fixture board.\n\n### Verification\n- `go test ./internal/core/ -count=1` — pass \n- `go test ./internal/core/ -count=20 -run TestBoardDefaultFilterScope` — pass \n- Commit is an ancestor of current `HEAD`\n\n### Left for review\n- No dedicated unit test for `boardForTypeID` tie-breaking (only the default_filter list test).","created_at":"2026-07-22T19:31:12.979229Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_a16d200a1def4774","author":"claude","body":"Review evidence corrected to ddcae2d. Default-filter behavior is proven; card remains in review because deterministic board-selection tie-breaking still lacks a dedicated regression test.","created_at":"2026-07-23T21:47:20.338146Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_a721cad6432c4144","author":"claude","body":"Follow-up completed in the pending review tree: TestTakeNext_DeterministicGoverningBoardSelection now pins lexicographic tie-breaking across two enforcing boards and passed 100 repeated runs. Together with the existing default_filter isolation test, the card acceptance is proven.","created_at":"2026-07-23T21:50:21.704829Z","edited_at":"0001-01-01T00:00:00Z"}],"version":12,"created_at":"2026-06-29T20:22:06.467617224Z","updated_at":"2026-07-23T21:50:21.728042Z","created_by":"jeremy","status_since":"2026-07-23T21:50:21.728042Z"},"type":"card"} {"data":{"id":"card_e897dc0712384369a4d869ab797b4bb3","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"UI: restyle select / enum controls (tags, enums) to match the design language","status":"done","fields":{"branch":"feat/ui","description":"Native select boxes for enums and the tags control look out of place against the print-shop design system. Style selects to match (mono, crisp borders, clear dropdown affordance) and give tag/enum pickers a consistent on-brand look — styled native select + datalist, or a small custom dropdown. Extends the tags-as-chips card."},"tags":["feature"],"comments":[{"id":"cm_208bb4fcf162491f","author":"jeremy","body":"Scope tied to DESIGN.md §Principle 3 (WYSIWYG): selects/enums must not change voice when a value becomes editable. Restyle .select with appearance:none + masked chevron (currentColor mask, consistent with the icon system), mono type matching the display text size. One rule set on .select — no per-field variants. Datalist/tags input inherits the same treatment.","created_at":"2026-07-02T12:07:25.374882671Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_df318053c1eb447f","author":"local-dev","body":"Select restyle done. One .select rule: appearance:none drops native chrome; masked chevron via --select-chevron token (currentColor-ish, remapped per theme — verified stroke #4d545b light / #9aa1a9 dark). background-color (not shorthand) so the chevron survives. padding-right --s-5 clearance. Applies everywhere: modal header status select (tighter, chrome-free), body enum/user selects, new-card form, board filters. Dark chevron + surface verified via computed-styles.","created_at":"2026-07-02T12:33:57.953163598Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_ae220415e8c94948","author":"jeremy","body":"Implemented and merged in main (48c90d6) as part of the design-system PR #2. .select restyled with appearance:none + masked-chevron token (one rule, all selects); .field__edit input/select/textarea match the view box model (field-sizing:content textareas); header status/owner selects matched to the chrome-free mono-xs meta voice (no size jump). Verified visually light+dark. Moving to review for acceptance.","created_at":"2026-07-02T13:56:43.205680248Z","edited_at":"0001-01-01T00:00:00Z"}],"version":9,"created_at":"2026-07-02T10:30:40.060531263Z","updated_at":"2026-07-04T22:01:47.619866536Z","created_by":"jeremy","status_since":"2026-07-04T22:01:47.619866536Z"},"type":"card"} -{"data":{"id":"card_ea7ea2a300144730b82f3b7266234205","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"CLI: cards run-extensions + cards do + cards extensions list","status":"todo","fields":{"branch":"feat/slice4","description":"CLI: cards run-extensions + cards do + cards extensions list","work_log":[{"author":"foz","commit_hash":"slice4","entry_id":"ent_ce90fb6ad86a48a5","notes":"DONE: cards run-extensions (supervisor), cards do --param, cards extensions list/show.","timestamp":"2026-06-26T09:00:00Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_297820e9084f4822","notes":"[pi-run pr_mrya4lfs_1acededf start] claimed by foz, batch 2/3","timestamp":"2026-07-24T01:46:33.737Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_6fec8f3e0772409d","notes":"[pi-run pr_mrya4lfs_1acededf phase=summarizing]","timestamp":"2026-07-24T01:46:53.737Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_b56a992ffcb04b15","notes":"[pi-run pr_mrya4lfs_1acededf failed] subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card","timestamp":"2026-07-24T02:06:33.781Z"}]},"tags":["feature"],"comments":[{"id":"cm_8afde88e7c814da8","author":"foz","body":"Closed: cards run-extensions (supervisor), cards do --param, cards extensions list/show. serve --run-extensions flag runs the supervisor in-process alongside the HTTP server.","created_at":"2026-06-26T09:25:42.6085Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_6e3c43c20df04a1c","author":"foz","body":"[pi-run pr_mrya4lfs_1acededf failed] subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card\n\nfailed: subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card\n\nbash /tmp/cards run-extensions --workspace /tmp/card…","created_at":"2026-07-24T02:06:33.788892Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_4d7e226cf5f04be4","author":"foz","body":"not started in this batch (stopped: ea7ea2a3 failed (subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card)); 2 card(s) completed by foz","created_at":"2026-07-24T02:06:33.801723Z","edited_at":"0001-01-01T00:00:00Z"}],"version":15,"created_at":"2026-06-26T08:48:13.7184Z","updated_at":"2026-07-24T02:06:33.801723Z","created_by":"foz","status_since":"2026-07-24T01:46:33.724989Z"},"type":"card"} +{"data":{"id":"card_ea7ea2a300144730b82f3b7266234205","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"CLI: cards run-extensions + cards do + cards extensions list","status":"done","fields":{"branch":"feat/slice4","description":"CLI: cards run-extensions + cards do + cards extensions list","work_log":[{"author":"foz","commit_hash":"slice4","entry_id":"ent_ce90fb6ad86a48a5","notes":"DONE: cards run-extensions (supervisor), cards do --param, cards extensions list/show.","timestamp":"2026-06-26T09:00:00Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_297820e9084f4822","notes":"[pi-run pr_mrya4lfs_1acededf start] claimed by foz, batch 2/3","timestamp":"2026-07-24T01:46:33.737Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_6fec8f3e0772409d","notes":"[pi-run pr_mrya4lfs_1acededf phase=summarizing]","timestamp":"2026-07-24T01:46:53.737Z"},{"author":"foz","commit_hash":"c3d85467bff2e266db2221519ed45e8f562598bb","entry_id":"ent_b56a992ffcb04b15","notes":"[pi-run pr_mrya4lfs_1acededf failed] subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card","timestamp":"2026-07-24T02:06:33.781Z"}]},"tags":["feature"],"comments":[{"id":"cm_8afde88e7c814da8","author":"foz","body":"Closed: cards run-extensions (supervisor), cards do --param, cards extensions list/show. serve --run-extensions flag runs the supervisor in-process alongside the HTTP server.","created_at":"2026-06-26T09:25:42.6085Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_6e3c43c20df04a1c","author":"foz","body":"[pi-run pr_mrya4lfs_1acededf failed] subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card\n\nfailed: subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card\n\nbash /tmp/cards run-extensions --workspace /tmp/card…","created_at":"2026-07-24T02:06:33.788892Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_4d7e226cf5f04be4","author":"foz","body":"not started in this batch (stopped: ea7ea2a3 failed (subagent timed out after 20m (work.timeoutMs) — raise work.timeoutMs in .pi/cards.json or work a single card)); 2 card(s) completed by foz","created_at":"2026-07-24T02:06:33.801723Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_7cac1a59c5fc4db7","author":"claude","body":"**What shipped:** `cards run-extensions`, `cards do`, and `cards extensions list` all exist (cmd/cards/main.go dispatch; `cards extensions list --workspace ./examples/demo-workspace` prints review-notify and review-bot). Verified 2026-09-06 during sprint planning; closing as already delivered.","created_at":"2026-09-06T18:25:24.60237Z","edited_at":"0001-01-01T00:00:00Z"}],"version":18,"created_at":"2026-06-26T08:48:13.7184Z","updated_at":"2026-09-06T18:25:32.253115Z","created_by":"foz","status_since":"2026-09-06T18:25:32.253115Z"},"type":"card"} {"data":{"id":"card_ec61b093d1a444dcb5c915518de5c67f","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 07-11 P3b: --watch definitions poller + definition_reload_failed in-browser banner + reload contract","status":"done","fields":{"branch":"feat/definitions-watch","description":"Phase 3 of docs/plans/sprint-2026-07-11.md. Audience: the definition author with a BROWSER open — the philosophy's headline daily-DX win: edit git-backed JSON, see it live, including seeing it FAIL live. Decides the reload-contract seam ONCE so P5 inherits answers, not contradictions.\n\nDO:\n1) Optional --watch flag wiring a definitions/ watcher into reloadableApp.reloadLocked(). Dependency-free poller modeled on scripts/dev-server.sh's fingerprint-hash loop — NO fsnotify (principle 10). Designed for testability from the start: injectable clock + synchronously-drivable scan function so debounce/coalescing/self-write tests assert deterministically — no real sleeps, no -race flake.\n2) Self-write suppression via a suppression token set by handleCreateBoard's write-then-reload path — NEVER sharing reloadableApp.mu with the poll loop, so the poller can never block the HTTP write path.\n3) Failure channel decided NOW, in the author's favor: a broken JSON edit keeps the last-good generation serving AND emits definition_reload_failed on the bus; the UI (ui.go already listens for definition_reloaded) shows a banner/toast with the structured error. stderr is additive, not primary — under --watch there is no HTTP response to carry the error and the author is looking at the browser.\n4) Write the reload-contract note covering: debounce policy, self-write suppression, supervisor-generation provenance (from P3a), failure surfacing, mutex serialization.\n\nDEMO CAVEAT: the SSE stream filters on card_type_ids only — default_filter is NOT applied. Do not script a demo that assumes board-scoped SSE.\n\nACCEPTANCE: saving a definition reloads live without dropping SSE; a burst of editor saves (atomic unlink+create, save-all) coalesces to exactly one reload; handleCreateBoard's own write does not double-fire; broken edit shows an in-browser banner within the debounce window while the board keeps serving last-good; banner clears on next successful reload; contract note written; tests run deterministically via injected clock; no new dependency."},"links":[{"type_id":"parent","target":"card_1b5289099221445090a54893e379106f","note":"sprint 07-11 phase card","created_by":"claude","created_at":"2026-07-11T21:07:50.947584Z"},{"type_id":"depends-on","target":"card_524c5758b3b34e4d814142130cab9eca","created_by":"claude","created_at":"2026-07-11T21:07:51.147113Z"},{"type_id":"depends-on","target":"card_8b25cef932d84aa7b90ede8a5921e946","created_by":"claude","created_at":"2026-07-11T21:07:51.161906Z"}],"comments":[{"id":"cm_d5da150f069346f5","author":"claude","body":"P3b done 2026-07-11: cards serve --watch fingerprint poller (no fsnotify); injectable clock + scanOnce tests; selfWriteGate for board-create; definition_reload_failed bus event + in-browser banner; docs/architecture/RELOAD.md. go test ./cmd/cards ./internal/httpapi green.","created_at":"2026-07-11T21:43:40.133951Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-11T21:07:50.747544Z","updated_at":"2026-07-11T21:43:40.14273Z","created_by":"claude","status_since":"2026-07-11T21:43:40.14273Z"},"type":"card"} {"data":{"id":"card_ed55ef06ab5945419a4089211843acd4","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Docs: refresh spec/events-history.md and events/index.md [built] markers","status":"backlog","fields":{"branch":"docs/events-normative-sync","description":"Code review 2026-07-24: spec/events-history.md still claims artifact_added/definition_reloaded are not emitted; events/index.md marks built envelope fields as [proposed]. Uncommitted events/core.md and integration.md are ahead of these normative/index pages."},"version":1,"created_at":"2026-07-24T01:31:09.50885Z","updated_at":"2026-07-24T01:31:09.50885Z","created_by":"local-dev","status_since":"2026-07-24T01:31:09.50885Z"},"type":"card"} {"data":{"id":"card_eecf0e0303a24b328b9993d547c4df65","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Sprint 07-11 P5c: reconcile-on-reload — service identity key + decision table; zero churn on board-create reload","status":"done","fields":{"branch":"feat/service-reconcile","description":"Phase 5 of docs/plans/sprint-2026-07-11.md, final step — its own design + implementation, DESIGNED BEFORE CODING: config.Result.Extensions is rebuilt per generation while the supervisor lives outside the reloadable seam.\n\nDEFINE:\n1) Service identity key: the extension ID; a change to command/args/env hashes as 'same service, changed declaration'.\n2) Decision table: added = start; removed = drain+stop; unchanged = leave alone; declaration-changed = drain+restart.\n3) Concurrency model: the reload swap holds reloadableApp.mu; the supervisor reconciles from a snapshot handed off AFTER the swap completes — never acquiring mu from the supervise loop, so no HTTP-handler deadlock.\n\nTESTS: board-creation reload leaves running services untouched (zero churn); edited declaration restarts exactly that service; removed declaration stops it.\n\nCLOSE-OUT: flip INTEGRATOR-REFERENCE.md section 7 (service kind) to [built] with the audit changelog updated.\n\nACCEPTANCE: routine board-create reload churns zero services; genuine declaration changes handled per the documented table; INTEGRATOR-REFERENCE section 7 flipped to [built]; go test ./... green and race-clean."},"links":[{"type_id":"parent","target":"card_1b5289099221445090a54893e379106f","note":"sprint 07-11 phase card","created_by":"claude","created_at":"2026-07-11T21:07:51.023443Z"},{"type_id":"depends-on","target":"card_23c7070a75114ba9ad14306803293f4f","created_by":"claude","created_at":"2026-07-11T21:07:51.241078Z"},{"type_id":"depends-on","target":"card_ec61b093d1a444dcb5c915518de5c67f","created_by":"claude","created_at":"2026-07-11T21:07:51.253274Z"}],"comments":[{"id":"cm_b45a5951cd604289","author":"claude","body":"P5c done 2026-07-11: ServiceDeclFingerprint + Reconcile decision table in RELOAD.md; board-create zero churn; INTEGRATOR §7 [built]. Tests: TestReconcile* + TestCreateBoardReloadServiceZeroChurn.","created_at":"2026-07-11T21:58:15.199517Z","edited_at":"0001-01-01T00:00:00Z"}],"version":7,"created_at":"2026-07-11T21:07:50.819017Z","updated_at":"2026-07-11T21:58:15.202989Z","created_by":"claude","status_since":"2026-07-11T21:58:15.202989Z"},"type":"card"} @@ -228,6 +233,7 @@ {"data":{"id":"card_f30a6618c41740b5afccc1e2471bb83e","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Events: condition events + monitors (WIP, time-in-status, empty lane, idle)","status":"done","fields":{"branch":"plan/integration","description":"Declarative board 'monitors' -> condition events on the bus. Instant (eval synchronously after the triggering mutation): wip_exceeded/cleared, lane_drained/refilled, card_blocked/unblocked. Temporal (status_timeout, card_idle): emitted by a monitor-evaluator goroutine beside the hook supervisor, driven by a deadline min-heap (sleeps until the earliest deadline, NO fixed tick; empty heap = no wakeups). Deadlines armed from a denormalized status_since column + a fired-marker keyed by (card,status,status_since); reconstructible from state, nothing persisted. Lazy/refcounted: deadlines scheduled only while a monitor has a live consumer (SSE filter, hook/webhook, or persist:true), dropped when the last consumer disconnects. Idempotent crossing-state tracking; core emits only, never acts. See docs/INTEGRATION.md."},"links":[{"type_id":"related","target":"card_bb0552e91e754aa186ebff55de3659c9","note":"part of the integration contract","created_by":"jeremy","created_at":"2026-06-30T17:09:09.830214451Z"},{"type_id":"related","target":"card_1767f1e8acc14713b20c97af8ee08324","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:16:12.33207809Z"},{"type_id":"related","target":"card_18a67d08a15d456bb012c85443a0bd53","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:16:12.347314016Z"},{"type_id":"related","target":"card_996194d019ed4b7f8f87c0ffe5ab20b8","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:16:12.35757103Z"},{"type_id":"related","target":"card_2f42b53c559b441dac0fb9b1a2f4e62f","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:16:12.377098641Z"},{"type_id":"related","target":"card_abdf1c7ebdfd4c00940ca970d3f80ed1","note":"sub-slice of this umbrella card","created_by":"local-dev","created_at":"2026-07-01T15:16:12.390800093Z"}],"comments":[{"id":"cm_f5793b1fa53b4f53","author":"jeremy","body":"Refined temporal design folded into INTEGRATION.md: deadline min-heap replaces the fixed tick (resolution is automatic, zero wakeups when idle); monitors are lazy/refcounted (run only while subscribed); condition events are ephemeral/derived (not replayed) with a GET /v1/breaches catch-up query split out to its own card.","created_at":"2026-06-30T17:42:30.229774622Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_240746e237424b7e","author":"jeremy","body":"picraft feedback (§2.2): status_timeout/wip_exceeded/lane_drained/card_blocked map exactly onto their colony-bar + escalation behaviors. Two asks: (a) keep condition events on the SAME bus as mutation events (already designed so); (b) support persist:true so escalations are auditable/replayable — picraft turns each into a durable system card. Add persist:true as an explicit acceptance criterion.","created_at":"2026-06-30T21:47:16.289669718Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_f7fb6104aa7b4323","author":"local-dev","body":"Split into small testable slices per docs/EVENTS.md §12 Step 3, ordered to de-risk the hardest part (the deadline scheduler) after the easier instant-condition machinery is proven: card_1767f1e8acc14713b20c97af8ee08324 (Signal path, one condition type), card_18a67d08a15d456bb012c85443a0bd53 (persist:true escalation), card_996194d019ed4b7f8f87c0ffe5ab20b8 (remaining instant conditions), card_2f42b53c559b441dac0fb9b1a2f4e62f (deadline min-heap scheduler, isolated), card_abdf1c7ebdfd4c00940ca970d3f80ed1 (wire temporal conditions to it). This card now tracks the umbrella; close when all five land.","created_at":"2026-07-01T15:16:12.317511029Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_39c978fd4655464e","author":"local-dev","body":"Closed: decomposed into slices per docs/EVENTS.md §12 Step 3 — card_1767… (3a signal path), card_18a67… (3b persist:true escalation), card_996194… (3c remaining instant conditions), card_2f42b… (3d deadline scheduler), card_abdf1… (3e wire status_timeout/card_idle). Implementation lives in the slices (still backlog); this umbrella is closed as a tracking item, NOT built.","created_at":"2026-07-01T19:44:55.108460928Z","edited_at":"0001-01-01T00:00:00Z"}],"version":15,"created_at":"2026-06-30T17:09:09.755534644Z","updated_at":"2026-07-01T19:45:13.825166985Z","created_by":"jeremy","status_since":"2026-07-01T19:45:13.825166985Z"},"type":"card"} {"data":{"id":"card_f4e2b74e935c45c49335d7c375dea684","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P4 — leaf components on Alpine (comments, entries, artifacts) + cardsAPI","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in aa62b36. cardsAPI (api.js): one fetch seam — actor header, structured-error parse, 409→STALE_MSG, 413, network-safe (never rejects); 7 node tests. Alpine components (components.js): commentComposer/commentRow (x-model, disabled-when-empty, in-place edit via x-show), entryEditor (defaults user→actor/date→today, data-raw prefill, number coercion, append/patch/delete), artifactZone (state machine via :data-state, zone-scoped drag, oversize pre-check). refreshCardSurface makes the detail page's previously-DEAD comment/entry/artifact controls work (context-aware reload). x-cloak added. wireComments/wireEntryEditors/wireArtifactUpload deleted.\n\nBrowser-verified: full comment/entry/artifact lifecycles, re-binding across ~6 modal swaps, forced 409/422 render inline correctly, zero console errors. During verification: accidentally clobbered card_fec1801913's evidence artifact with a test file — RESTORED from the content-addressed blob store (field re-pointed, API-verified). go test green (14 pkgs), node 13/13.","surface":"modal"},"owner":"jeremy","tags":["feature"],"version":5,"created_at":"2026-07-09T11:25:50.046017Z","updated_at":"2026-07-09T11:25:50.238122Z","created_by":"jeremy","status_since":"2026-07-09T11:25:50.238122Z"},"type":"card"} {"data":{"id":"card_f570b35bef334378ab25e084b6edc357","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"Design RFC: transitions-as-callbacks + trigger seam (board-shipped behavior) — v0.5 direction","status":"backlog","fields":{"branch":"main","description":"PLAIN: today the core both declares transition rules (data) and enforces them (code). This RFC sketches moving enforcement behind a Validator interface with a subprocess callback seam, so policy richness lives at the boundary instead of growing an in-process rules dialect in core.\n\nPlanted for NEXT sprint. First-pass sketch for docs/design/BOARD-BEHAVIOR.md (does not yet exist). GOVERNING DOCS (frozen 2026-07-10, see docs/NOTES.md freeze entry): docs/design/CORE-BOUNDARIES.md §3.3 (three-layer framing + force policy) and docs/design/AUTH.md §7 (identity invariant). OUT of sprint 07-10 (see sprint tracker card_3f225267).\n\nTHREE-LAYER FRAMING (from CORE-BOUNDARIES §3.3 — adopt in the RFC):\n- DECLARATIVE GRAPH (Board.Transitions in board JSON) = core DATA, introspectable/exportable — STAYS. Same family as columns and WIP limits.\n- ENFORCEMENT (service.go:768, :1487 refusal) = the seam this RFC refactors behind a Validator interface, with in-core evaluate as the builtin first chain link.\n- OBSERVATION (transition_rejected, events.go:282 TransitionRejectedDiff; constructed :291; emitted service.go:775) = stays wherever enforcement lives; continuity guaranteed.\nThe vision quote ('core should not have strong opinions about transitions') bans PROGRAMMABLE rules in core, not the declarative map.\n\nOVERLAY PRINCIPLE at top of the doc:\n'Core ships contracts and boring defaults. Policy richness lives at the boundary (auth adapter, validate callback, external hooks) — never as a growing in-process product dialect.'\n\nSHAPE — data + code:\n- Declared RULES (data): today's transitions: map, plus optional require_field_before etc. — read by a default builtin validator.\n- Optional per-board VALIDATOR (subprocess declared in board JSON) returning a narrow verdict.\n- Default = current EnforceTransitions behavior refactored behind the interface, so nothing regresses.\n\nRUNTIME, DECIDED: SUBPROCESS-FIRST, single implementation. Embedded JS is EXPLICIT NON-GOAL until someone shows a demo subprocess can't carry. No behavior.js implicit paths — the board JSON declares a command.\n\nBOARD DECLARATION (example):\n \"behavior\": { \"validate\": { \"command\": [\"./extensions/eng-validate\"], \"timeout_ms\": 200 } }\n\nVERDICT WIRE SHAPE (JSON stdout of the subprocess):\n { \"allow\": true, \"code\": \"transition_illegal\", \"message\": \"...\",\n \"field\": \"status\", \"valid_options\": [\"review\"] }\nReuses existing structured-error vocabulary; no partial card mutations; no nested Service calls; max stdout size (documented).\n\nFAIL-CLOSED MATRIX (writes only; reads never call this):\n- timeout → fail-closed\n- non-zero exit or unparseable JSON → fail-closed\n- allow:false without a code → default transition_illegal (transitions) / validation_error (field triggers)\n- subprocess missing / not executable → fail-closed with a CLEAR OPERATOR MESSAGE (config error, not a validation event)\n\nCONTINUITY OF OBSERVATION: builtin deny and callback deny share the existing transition_rejected event/code path (events.go:282 already defines this today; emitted at service.go:775). Add an optional diff.source = \"builtin\" | \"board_validate\" only if it earns its keep in debugging — no new public event surface in v1.\n\nEVENT COVERAGE, PHASED (avoids the workflow-engine siren):\n- v1: status transitions only (patch/create status) — replaces in-core enforce.\n- v1.1: declarative require_field_before_transition — ONE PATTERN ONLY ('status S requires field F set'), not a rules language. Compiles to the same Validator interface as the default builtin, so board subprocesses sit as chain peers, not special cases.\n- v2: on_comment_added / on_field_changed validators ONLY IF real demand.\n\nCHAIN ORDER: builtin declarative rules → board callback → commit. The callback does not reimplement column membership unless the operator opts into 'disable builtin.'\n\nFORCE POLICY (decided 2026-07-10, CORE-BOUNDARIES §3.3 revised — this card is its durable home):\n- Force is a PER-WRITE request field (force:true in PATCH body / req.Force / CLI --force), not a serve mode.\n- force=true SKIPS BOTH the builtin declarative rules AND the board-callback validator — matches today's req.Force behavior (service.go:768); the web UI's drag-and-force-confirm flow depends on it. YOLO escape hatch by default.\n- Force NEVER skips identity verification: under --auth token/proxy an unauthenticated forced write is 401 (AUTH.md §7). Attribution of a forced write is the resolved verified identity.\n- NO NEW EVENT TYPE: forced-ness rides status_changed with diff.forced=true. A separate transition_forced EventType was considered and rejected (SPEC + bus-filter + INTEGRATOR churn without consumer demand).\n- BOARD OPT-OUT: settings.allow_force:false makes the declarative graph a hard floor for that board (no force override there). Data, not code — same family as enforce_transitions. Small SPEC-DATA-MODEL addition.\n\nHOOKS / VALIDATORS COVENANT (the load-bearing distinction — PHILOSOPHY §5 boundary):\n\n| Seam | Timing | Power | Contract |\n|---------------------|-----------------------------|--------------------------|----------|\n| Hooks (today) | post-commit, at-most-once | observe / side effects | no veto |\n| Validators (proposed)| pre-commit, sync, bounded | verdict only | no email/HTTP/card writes from the sandbox; if a validator does that via its own subprocess, that's the operator's problem — core provides no client helpers |\n\nMIGRATION STORY: boards with today's enforce_transitions: true and no custom behavior → IDENTICAL outcomes; transition_rejected event still fires; no observable regression.\n\nOPEN TENSIONS (decided next sprint, not now):\n- Validator sync cost on every write: queue depth, panic isolation, dry-run escape hatch.\n- Definition-trust ≠ card-ACL: see AUTH.md implementation notes."},"links":[{"type_id":"parent","target":"card_3f225267e3724f9f8d802772731d3316","note":"planted for next sprint","created_by":"claude","created_at":"2026-07-10T06:26:48.651639Z"},{"type_id":"related","target":"card_c7a70b64617042229df715d5bae10b8d","note":"hooks/covenant lineage — next sprint starts from the boundary conversation","created_by":"claude","created_at":"2026-07-10T06:26:48.664413Z"}],"comments":[{"id":"cm_408ed8c3f89047e8","author":"foz","body":"**Quality review (batch B) — keep backlog**\n\n**Validated:** Governing docs exist: `docs/design/CORE-BOUNDARIES.md` and `docs/design/AUTH.md`. `BOARD-BEHAVIOR.md` correctly noted as not-yet-created (RFC target). `req.Force` skip + `EnforceTransitions` refusal confirmed in service.go. `transition_rejected` event + `TransitionRejectedDiff` confirmed.\n\n**Issues found:** Three line refs had drifted: enforcement refusal is service.go:768 (was :751) and :1487 (was :1456); `TransitionRejectedDiff` is events.go:282 (was :283), constructed :291, emitted at service.go:775.\n\n**Changes made:** Added plain-language lead. Corrected all line refs and made the observation-layer citation precise (struct / constructor / emit site). RFC content, force policy, covenant table, and phased event coverage preserved verbatim — this is a design contract, not implementation.\n\n**Scope verdict:** Keep — appropriately scoped as an RFC (no code); open tensions correctly deferred.\n\n**Arch verdict:** Aligned — subprocess-first validator behind a Validator interface, default = refactored current behavior (no regression), fail-closed matrix, no new event type. Strongly matches PHILOSOPHY §5 (hooks vs validators) and extensions-over-plugins.","created_at":"2026-07-19T17:36:51.754031Z","edited_at":"0001-01-01T00:00:00Z"}],"version":6,"created_at":"2026-07-10T06:26:16.89354Z","updated_at":"2026-07-19T17:36:51.754031Z","created_by":"claude","status_since":"2026-07-10T06:26:16.89354Z"},"type":"card"} +{"data":{"id":"card_f59debb8763f4217994f65fb4b4b2ba8","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"Using Cards: keep the working session, move the operations catalog to Reference","status":"backlog","fields":{"branch":"docs/refresh-using-cards","description":"docs/using-cards.md is 635 lines: §1 'A working session' (lines 23-50) and §2-4 (work in the repo, picking a setup, coordinating around git) are the narrative the Guide needs; §5 'The operations' (lines 178-635) is a 440-line catalog of every verb with CLI, HTTP and MCP side by side — reference material that belongs under Build on it, next to reference/cli.md and spec/api-surface.md, and that overlaps both. Split: the narrative stays as Guide → 'The workflow' (≤200 lines); the catalog becomes reference/operations.md, deduplicated against reference/cli.md (which keeps flags and the TUI section) and linked from each verb's spec entry. Lands after Sprint A so the patch/claim/release examples show --if-match latest and --md.\n\nAcceptance: using-cards.md ≤200 lines; no operation documented in two places with different examples; mkdocs build --strict passes; internal links from agents/*.md and get-started.md updated.","kind":"design"},"links":[{"type_id":"parent","target":"card_65f8ae3848c046b092cdbbb2da68871d","created_by":"claude","created_at":"2026-09-06T18:27:53.633912Z"},{"type_id":"depends-on","target":"card_069ec1d16a804b3286dfccb64d10d0e4","created_by":"claude","created_at":"2026-09-06T18:27:53.722262Z"}],"version":3,"created_at":"2026-09-06T18:27:53.512585Z","updated_at":"2026-09-06T18:27:53.722262Z","created_by":"claude","status_since":"2026-09-06T18:27:53.512585Z"},"type":"card"} {"data":{"id":"card_f638d7a2597141f0ab23f32ab78d9a36","workspace_id":"demo","type_id":"frontend-task","schema_version":1,"title":"Rebuild P1 — one field_control partial (canonical schema→control switch)","status":"done","fields":{"branch":"frontend-rebuild","description":"Done in b91a024. Three duplicated type→control switches (create form / modal click-to-edit / entry sub-form) collapsed into templates/field_control.html, ctx-parameterized, per-surface contracts preserved verbatim incl. the 3-way user divergence (unified in P5). Guard TestFieldControlIsTheOnlySwitch. PROVEN behavior-identical: side-by-side server diff of 9 routes (all 5 create types, modal, detail, 2 boards) — whitespace-only diffs, textarea content byte-identical.","surface":"modal"},"owner":"jeremy","tags":["feature"],"version":5,"created_at":"2026-07-09T08:40:28.007045Z","updated_at":"2026-07-09T08:40:28.428037Z","created_by":"jeremy","status_since":"2026-07-09T08:40:28.428037Z"},"type":"card"} {"data":{"id":"card_f64d3bf336c5422195c0ef3e7af70a82","workspace_id":"demo","type_id":"programming-task","schema_version":2,"title":"SSE: make feed-to-live recovery gap-free","status":"backlog","fields":{"branch":"fix/sse-replay-handoff","description":"Review found that the SSE handler replays at most 500 durable events and subscribes to the live bus only after replay, leaving a race where a write can land between the two. Documentation now describes SSE as bounded, best-effort and tells strict consumers to reconcile the feed. Make the server handoff gap-free without blocking writers: establish a high-water/subscription ordering, page replay beyond 500 or emit an explicit incomplete-replay signal, and deduplicate overlap by durable event id. Done when deterministic tests inject events during handoff, cover more than 500 missed events, and prove every durable id is delivered or explicitly delegated to feed recovery.","kind":"bug"},"version":1,"created_at":"2026-07-23T21:56:50.448103Z","updated_at":"2026-07-23T21:56:50.448103Z","created_by":"cursor-review","status_since":"2026-07-23T21:56:50.448103Z"},"type":"card"} {"data":{"id":"card_f6d2f5eaed994a1c8d470587e4370e58","workspace_id":"demo","type_id":"programming-task","schema_version":1,"title":"UI: create a new board","status":"done","fields":{"branch":"feat/ui","description":"A flow to create a board from the UI: name, columns (chosen from workspace.columns), card types, optional starting theme + transitions. Writes a new board definition and navigates to it. Entry points: the home boards grid and/or nav. Share the form with the board-settings editor card (create = settings on an empty board). Validate against workspace columns/types."},"tags":["feature"],"comments":[{"id":"cm_f89da46318ce467c","author":"local-dev","body":"Built + committed as f772951 (UI sprint P4 — the gated stretch phase, with its enabler 4b507da7 workspace reload). POST /v1/workspace/reload: reloadable composition seam in cmd/cards — atomic generation swap, load error → 422 + OLD definitions keep serving (never half-loaded, pinned by corrupt-then-fix test); store + event bus shared across generations (core.WithBus) so SSE/hooks survive; definition_reloaded fans out per board id and open boards refetch; 'cards reload' CLI verb. POST /v1/boards writes definitions/boards/.json (reviewable file, as if hand-written), validates through the REAL loader with rollback on rejection, then reloads. Nav '+' opens the create-board modal (columns/type badges/optional WIP). Browser-verified: created 'UI Review' from the nav, landed on the new board with it live in the nav, then removed the file + cards reload took it away. Board-settings EDITING deferred (0707a008) per plan.","created_at":"2026-07-07T14:16:12.133763Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_6cc3255f87ff40ab","author":"jeremy","body":"STATUS CHECK 2026-07-10 (frontend-rebuild):\n\nSHIPPED on this branch:\n- GET /ui/boards/new/modal + boardCreate Alpine component\n- POST /v1/boards from UI; nav `+` affordance; navigate to new board on success\n\nREMAINING polish (keep this card or split — prefer keep small & close soon):\n- No Idempotency-Key on boardCreate (double-click can host two boards)\n- Checkbox soup UX (columns/types) should reuse chip/multiselect control language\n- Home grid entry point is fine; board-settings editor (0707a008) still separate\n- Optional theme pick not in form yet (defer to theme/settings cards)\n\nRecommend: leave in review until idempotency + checkbox→chip polish lands, then done. Do not expand into full board settings.","created_at":"2026-07-09T23:25:58.666104Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_a01a91c9d2a54aed","author":"claude","body":"P0a disposition 2026-07-11: LEFT IN REVIEW (do-not-flip list). Core create-board UI shipped (f772951); unmet: server Idempotency-Key on board create (card_f20e87d5) + residual UX polish.","created_at":"2026-07-11T21:30:44.392793Z","edited_at":"0001-01-01T00:00:00Z"},{"id":"cm_5997f9901ece4d4a","author":"claude","body":"Human-accepted 2026-07-12. Core shipped f772951; idempotency gap closed by b23aff8 middleware + d16039c (card_f20e87d5 done). Residual checkbox→chip polish spun out as card_5ea0d5ef6e4c4b98a04b5293b3654c34.","created_at":"2026-07-12T01:54:14.981575Z","edited_at":"0001-01-01T00:00:00Z"}],"version":10,"created_at":"2026-07-02T13:29:42.675026434Z","updated_at":"2026-07-12T01:54:15.04121Z","created_by":"jeremy","status_since":"2026-07-12T01:54:15.04121Z"},"type":"card"} diff --git a/.claude/launch.json b/.claude/launch.json index 97a709e..953ca99 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -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 } ] } diff --git a/docs/plans/2026-09-06-sprint-plan.md b/docs/plans/2026-09-06-sprint-plan.md new file mode 100644 index 0000000..87486f6 --- /dev/null +++ b/docs/plans/2026-09-06-sprint-plan.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index 6632779..3e69a14 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,6 +93,7 @@ not_in_nav: | plans/2026-07-22-sprint-plan.md plans/2026-08-08-sprint-plan.md plans/2026-08-31-sprint-plan.md + plans/2026-09-06-sprint-plan.md nav: - Home: index.md