WS-27 S1–S4: Tasks becomes a real slice of Projects — one card, one detail surface, one selection grammar; plus the one-store decision and the fences that found three latent defects - #419
Merged
Conversation
…s as a pop-up Owner ruling: Projects is canonical, Tasks conforms. DESIGN_SYSTEM §6 has specified the house layout as an optional w-[380px] desktop side panel (bottom sheet on mobile) all along; /tasks was the only app opening a record as an overlay. Desktop list/board views now dock the detail as a third column — page.tsx renders <aside className="w-[380px] …"><ItemDetail /></aside> beside ItemList, driven by the store's selectedItemId, mirroring how projects/page.tsx docks TaskPanel. Present only while something is selected, closes from its own ✕. TaskFocusModal keeps two lives: the phone branch (store-driven, full-screen, the same move Projects makes with fixed inset-0), and an explicit maximise from the docked pane (controlled by itemId/onClose), which is the mitigation for the max-w-3xl reading width the 380px column cannot give. Inbox, Engage, Calendar and the Assistant carry no pane and keep raising the store-driven overlay. Two dead exports came back rather than being deleted: ItemDetail() as the pane's entry (restoring the INBOX → ClarifyPanel branch, now with the pane's ✕ so an inbox selection on a task view is dismissable) and the "Open full page" button, which now has an unfocused mount to live on. openFocus stays the app-wide "open this task" verb every list component calls; on the docked surface the page drops its focus half so a row selects into the pane — dropped, not ignored, or a stale id would pop the overlay open on the next view switch. Also: sm:rounded-2xl → sm:rounded-lg on the modal sheet (§4 radius; a fixed 16px corner ignores Graphite's 0.125rem and Material's 1rem). No other rounded-xl site under src/app/tasks/ was swept. R4: task_manager_app.md status header records the change; workbench/AGENTS.md gains the tasks entry with the openFocus rule. R7, honestly: nothing in this tree tests layout, panel counts or mobile branches, so the composition is ADVISORY — held by review and the theme-switch pass, which could not be run here (no browser; Playwright cannot install). Verified: npx tsc --noEmit → 0 · npx vitest run → 73 files, 1586 tests, 0 · npx vitest run src/lib/theme/ → 353 tests, 0 · eslint on the three files → 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Owner ruling, not re-litigated: "the Tasks app is only a slice of the Projects app" — Projects is canonical, Tasks conforms where neither is clearly better, and a GTD-only need diverges only with its reason next to the code. Six items, frontend only, no migration, no API change: 1. Column chrome — `rounded-xl` + `bg-secondary/30` becomes /projects' `rounded-lg border border-border bg-card`, refusal overlay `rounded-lg`, and the gutter moves from a per-card `mb-2` to the column's `space-y-1` (cards and drop gaps are siblings now, because `space-y-*` only reaches direct children). 2. Completed — TaskCardShell had `completed` since WS-27ad and nothing under app/tasks/ had ever passed it, so a done task was dimmed and struck through on one board and drawn as live work on the other, from one component, with every test green. 3. Cursor ring — the board's wrapper div re-drawing `ring-2 ring-ring` is gone; the card takes `atCursor` and the shell draws the ring on the card's own radius. `useFlash`'s attach moved onto the card too. 4. Avatars — TaskCard's private Avatar/AvatarStack deleted for @/components/TaskMeta's shared pair. 5. Checkbox — out of the card and into a left-gutter sibling (/projects' `flex items-start gap-1.5` + stopPropagation), always present, never mode-gated: `selectMode` no longer changes what a click on a card means, and TaskCard has no `selectMode` prop. The drag grip is DROPPED, not relocated — the whole card is `draggable`, so it was never a handle, /projects draws none, and the shell's hover lift is the affordance both boards already use. 6. Title — clamped to `line-clamp-2` in TaskCardShell so BOTH apps change together; a caller-supplied `truncate`/`line-clamp-*` is stripped rather than merged, because nowrap vs -webkit-box on one element resolves by CSS source order. Fences (R7), all in src/lib/sharedTaskUi.test.ts and all mutation- measured red: every TaskCardShell caller passes `completed` and `atCursor` (brace-aware tag scan — a lazy regex stops at the first arrow function and passes for the wrong reason); no board re-implements the card cursor ring; both boards' columns share one radius. AvatarStack and TaskCardTitle join the SEAM table, which gains an `except` map with an argument per file plus a staleness check. Correction to the audit's premise: `rounded-xl` is NOT unthemed in this tree. globals.css derives the whole `--radius-*` scale from `--radius` inside @theme and `--radius-xl` IS `var(--radius)`, so a tree-wide ratchet would have baselined 274 correctly-themed occurrences. The fence is written to the true rule — the two boards agree — and the false claim in AGENTS.md rule 6 / DESIGN_SYSTEM §4 is reported, not edited from here. R4: project-docs/specs/project_management_app.md status header + §9.2, including striking WS-27ad's "Tasks' modal select-mode is KEPT" on the card side. Not verified: how any of it looks. No browser is runnable here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Owner decision: ONE store. GTD tasks stay private via the personal-project mechanism migration 147 already built (pm_projects.personal_owner + the per-member pm_task_personal overlay); project tasks are ordinary rows in ordinary projects. Not two databases -- "local" is a visibility property, not a storage location. The argument that settles it is already in 147's header: a single disposition column on a shared task cannot express delegation (one person is doing it, the other is waiting on it), and two separate stores cannot either, because the task would exist twice and the copies disagree the moment either moves. What this decision creates is the real work, and it was mis-specified. 7.5 said the move is "a copy rather than a translation". True of the seven overlay columns pm_task_personal mirrors; false of the rest. gtd_items carries 31 columns plus gtd_waiting, and fifteen have no pm_tasks home. Executed on that sentence, WS-27h would delete the founder priority matrix, all of timeboxing and the entire Waiting-For view -- and report success, because a migration that drops an unmapped column looks exactly like one with nothing to map. 7.5.1 now names the destination of every column, decided by one question: whose fact is it? A task fact is true for everyone on the row (pm_tasks); a member fact can legitimately differ between two people on the same task (pm_task_personal). The test: if Ana and Ben are both on this task, can their answers differ? The priority matrix, timeboxing and deep_work all fail that test as shared columns, so they go per-member -- the same argument 147 makes for disposition, applied consistently rather than only where first noticed. Two sequencing constraints recorded, both load-bearing: the provider columns cannot drop before WS-27g retires the arm that fills them (or SourceBadge loses its data while the integration is live), and horizon_id belongs to WS-21, which is DO-NOT-DISPATCH -- this ticket may not decide it either way. R7: tests/unit/test_gtd_retirement_plan.py reads the migrations and the spec and fails when a legacy column has no named destination. It earned its place on the first run by catching one I had missed -- gtd_waiting.item_id -- which is exactly the failure mode the section exists to prevent. It also pins the blocked markers and the NEW markers, since losing either turns a schema change back into "just a data move", and guards its own parser against matching nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…-management-a1f6zd
S1 measured it: globals.css's @theme derives sm/md/lg/xl/2xl/3xl from --radius, and --radius-xl IS var(--radius), i.e. identical to rounded-lg. The rule I added to AGENTS.md this session claimed rounded-xl was a fixed 12px ignoring Graphite and Material. False. Acting on it would have baselined ~274 correctly-themed occurrences across ~70 files as debt -- a fence against a non-violation, which is worse than no fence. DESIGN_SYSTEM 4 was not wrong, only incomplete: it listed sm/md/lg and forbade rounded-[14px], which reads as "the others are unthemed". Now spelled out, with the note that an agent acted on that reading. What survives from the rule: consistency between surfaces. Two boards at different radii look like two products even when both are themed -- which is the real finding S1 fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…-task-management-a1f6zd
Owner ruling 2026-08-10 — "Projects is canonical, Tasks conforms" — reversing
WS-27ad's kept modal select-mode. The reason it recorded ("a permanent checkbox
would take the drag-grip gutter or make one gesture mean two things") was never
structural: /projects put the box OUTSIDE the card as a sibling in the row,
/tasks put it INSIDE over the grip, so the collision was one /tasks built for
itself.
The list surfaces (ItemList, TaskListGrouped, FlatList) + the store's selection
slice. The board card and WaitingForView are sibling/unowned slices and still
draw their box behind `selectMode`.
- `selectMode` is no longer a mode: a derived mirror of `selectedIds.size > 0`,
maintained in one helper, meaning only "the bulk bar is up". `setSelectMode`
and the "Select" button are gone; the three list surfaces never read it.
- The checkbox is unconditional, in its own gutter beside the grip. The row's
click still opens the task, and drag-reorder is no longer disabled while
something is selected.
- Shift-sweep ungated on both lists (it needed select mode; /projects never did).
- The bulk bar moved to the TOP onto /projects' chrome (`border-b border-border
bg-muted`) and onto Button/Badge — the hand-rolled outline buttons were
AGENTS.md rule 3, and the conformance regexes only see solid fills.
- Select-all added (`selectAllVisible`) over the FILTERED set, plus
`pruneSelection` so a selection cannot outlive the filter it was made in.
- Bulk power stays archive/restore/delete and says why in code: `/items/bulk` +
`/items/bulk-archive` is the whole gateway surface and `gtd_items` has no
tags column.
Fence (R7): app/tasks/lib/selectionParity.test.ts — store invariant, no
setSelectMode, no surface reading selectMode or drawing a conditional checkbox,
both apps' bar on one chrome string. Mutation-verified.
R4: project_management_app.md status header + the WS-27ad selection bullet
(struck) + an S3 as-built entry. DOX: workbench/AGENTS.md gains the /tasks entry.
No browser was run: the phone-viewport and four-theme pass is owed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Three findings where Projects, not Tasks, carried the defect. The standing ruling is "Projects is canonical, Tasks conforms"; these are the exceptions, and convergence runs both ways when the evidence says so. Spec §11.21. 1. The active token. MyWork's selected context pill was `bg-accent text-accent-foreground`; the measured house token is `bg-primary/10 text-primary` (AGENTS.md rule 6). Two sites swapped, plus the `aria-pressed` the toggles never carried. The lasting deliverable is the fence: conformance gains a SIXTH rule matching the pair, ratcheted per file, with `lib/statusAccent.ts` excepted with its argument (its violet chip is a hue, not a state). FilterBar 2 / SearchPalette 1 / people/page 1 are baselined and can only go down. 2. The fourth task card is gone. MyWork's `Row` bypassed TaskCardShell, TaskMeta and StatusChip — and MyWork is the personal task list INSIDE Projects, so it sits opposite /tasks in the owner's comparison and looked like neither app. Rebuilt on the shared shell/title/chip row, with the GTD disposition as a StatusChip whose hue goes through `statusAccent` (`accentForDisposition`), pinned by a test to agree with the name-keyword route wherever that route has an opinion (rule 5). Capture field and triage buttons become Input/Button primitives. 3. Two empty states where there was one. "Nothing to show. Clear a filter, or this project has no statuses yet" named both causes and asked the reader to guess. Now: "No tasks match your filters." + Clear filters when `isFiltered(filters)` — the same predicate the toolbar reads, not a second one — and otherwise "This project has no statuses yet." on the status axis or "No tasks here yet.". The box is promoted to `src/components/EmptyState.tsx`; the decision and copy are pure and unit-tested in `projects/lib/emptyState.ts`. Fences (R7): conformance rule 6 · sharedTaskUi seam entry + consumer row for EmptyState · emptyState.test.ts (including "the filtered copy never mentions statuses, the empty copy never mentions filters", and that every icon it names is mapped in every pack — which caught a FilterX mapped in none) · accent.test.ts disposition agreement · `filters`/`onClearFilters` are REQUIRED props, so tsc catches an unwired call site. All mutation-checked. Frontend only; no migration, no API change. R4: spec status header + §11.21 updated in this change. NOT verified in a browser — Playwright cannot install here, so the four-theme sweep on /projects and /tasks is owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…task-management-a1f6zd # Conflicts: # project-docs/specs/project_management_app.md # workbench/AGENTS.md
…-management-a1f6zd # Conflicts: # project-docs/specs/project_management_app.md # workbench/control_plane/AGENTS.md # workbench/control_plane/src/lib/sharedTaskUi.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Driven by the owner's reframing:
Ruling applied throughout: Projects is canonical, Tasks conforms — except where Tasks is demonstrably better, which S4 handles in the other direction.
No migrations, no backend changes. Frontend, specs and tests only. (167 shipped in #418 and is already applied.)
The three divergences the owner named, from looking at the deployed app
Cards. Tasks' board adopts Projects' column shape, surface and gutter. The shared shell's
completedandatCursorprops are finally passed by their Tasks caller — done tasks were never dimmed or struck through, because the shell supported it and nobody wired it. The privateAvatarStackcopy is deleted for the shared one. Titles clamp inTaskCardShellso both apps move together rather than forking.Detail surface.
/tasksdocks aw-[380px]pane instead of opening a pop-up. This was a bug against an existing rule, not a design difference:DESIGN_SYSTEM.mdalready specifies a side panel, and Tasks was the only overlay in the product (email uses a reading pane, notes a route, projects an aside). The modal survives as the phone branch and as an explicit maximise, because Tasks' detail is genuinely denser than Projects' and 380px cannot give it themax-w-3xlreading width. Two pieces of dead code — a side-panel entry imported nowhere and a maximise button that could never render — came back to life rather than being deleted.Selection. The checkbox is permanent and sits in its own gutter beside the row.
selectModeis now a derived mirror of "something is selected" and gates only the bulk bar. The bar moved to the top on Projects' chrome and primitives; select-all exists.The previous slice had deliberately kept Tasks' modal select-mode, reasoning a permanent checkbox would steal the drag-grip gutter. That reason did not survive: Projects puts its checkbox outside the card as a sibling with
stopPropagation, so there are two targets and the card surface stays the open affordance. Tasks put it inside, overlapping the grip — it created the collision and then documented it as a constraint. The comment now says what is true.And the other direction (S4) — three findings where Projects carried the defect: the
bg-accentactive token (house norm isbg-primary/10 text-primary), a fourth bespoke task row inMyWorkthat bypassed the shared card vocabulary entirely, and no distinct filtered-empty state (Projects conflated no data with filtered to nothing and asked the user to guess).The architecture decision
Owner question: one database with per-user separation, or two apps? One store, and the mechanism was already built and unused —
pm_projects.personal_owner(a personal project, granted to one email) plus the per-memberpm_task_personaloverlay. "Local" is a visibility property, not a storage location.The argument that settles it is already in migration 147's header: a single
dispositioncolumn on a shared task cannot express delegation — one person doing it, another waiting on it — and two separate stores cannot either, because the task would exist twice and the copies disagree the moment either moves.§7.5.1 is new and is the substance here. WS-27h said the move is "a copy rather than a translation." True of the seven overlay columns; false of the rest.
gtd_itemscarries 31 columns plusgtd_waiting, and fifteen have nopm_taskshome — the founder priority matrix, all of timeboxing,deep_work, the email-capture badge, and the entire Waiting-For view. Executed on that sentence the migration would have been a feature deletion reporting success. Every column now has a named destination, decided by one question: if Ana and Ben are both on this task, can their answers legitimately differ? Two sequencing constraints recorded — the provider columns cannot drop before WS-27g, andhorizon_idbelongs to WS-21, which is DO-NOT-DISPATCH.Three latent defects the fences found — none of which was the ticket
A rule I wrote in the previous PR was wrong.
rounded-xlis not a fixed 12px:--radius-xl: var(--radius), identical torounded-lg, and the whole named scale is derived. Acting on my claim would have baselined ~274 correctly-themed occurrences across ~70 files as debt — a fence against a non-violation, worse than no fence. Corrected inAGENTS.mdwith the correction visible, andDESIGN_SYSTEM.mdspelled out so the misreading cannot recur.Two source files contained literal NUL bytes. ripgrep stops at the first one — so every grep-based fence in this repo, and every
grepa human or agent ran, silently saw only the first third ofprojects/TaskBoard.tsx, and stayed silent even when the file was named explicitly. The fences passed because they never looked.sourceHygiene.test.tsnow fails on any NUL undersrc/; it found a second offender on its first run and a third in its own docstring, where the warning had pasted the very byte it warns about.Local
mainwas ~50 commits stale, so three agents cut branches backwards; one noticed only because half the WS-27 files vanished. Reset.Also:
sharedTaskUi's fence caught a genuine cross-slice gap — S4 built on the shared shell without declaringatCursor, which is indistinguishable from forgetting it. Now stated with its reason.Verification
tscclean · 1629 tests / 76 files (from 1586) · theme suite 356 · productionnext buildcompiles and prerenders all 39 pages · backend189 passedon the touched suites. Every new guard was mutation-measured red and reverted byte-identical.Owed, and not claimed
No visual check was performed, by any slice. Playwright cannot install in the build environment, so neither the phone-viewport pass nor the Fluent → Material → Graphite sweep happened. Each agent compensated with what it could inspect — class strings against the theme tokens, icon names against the registry, a production build — and each said plainly that it did not look at the running app.
This matters more than usual because there is still no structural or layout test anywhere in this tree. Nothing asserts panel composition, shell adoption, or that two apps draw a card the same way. The docked pane in particular is held by review alone.
Known, deliberate, and worth seeing before judging:
WaitingForViewstill gates its checkbox on the old mode, so its first pick must come from select-all until it gets the same treatment. The shared card title is stilltext-[13px]against the house scale — the one contradiction left for the owner rather than settled by whichever slice touched the file last.Generated by Claude Code