Projects: beyond-parity queue WS-27u–z + Tasks↔Projects UI continuity - #408
Merged
Conversation
…lit unread
Spec §9.1 WS-27v (plane_pm_research_2026-08.md §3.2, P-2 + P-20). Shape
re-derived in this repo's idiom behind the AGPL wall; the one deliberate
divergence from Plane is load-bearing: delivery stays gated per recipient by
resolve_visibility_for, never by membership — a watcher row is an intent to
hear, not a right to see.
Migration 165: pm_task_watchers(task_id, watcher, organization_id),
UNIQUE(task_id, watcher), watcher folded and human (R10 + WS-27j rule 2 as
CHECKs, mirroring pm_notifications), tenant filled and verified by 161's
pm_organization_from_parent trigger. Seeds task creators as watchers so the
old assignees-plus-author audience survives the switch.
Gateway:
* watchers.py — ensure_watchers (the ONE idempotent subscribe helper),
PUT/DELETE /projects/tasks/{id}/watch, GET .../watchers; R5 throughout
(invisible task is 404, never 403), identity from the session only (R3).
* Auto-subscribe from all four triggers: commenting, editing (only when
something actually changed), being assigned (added set only, agents
fenced), being @mentioned (delivered mentions only).
* task_audience becomes watchers ∪ assignees; the actor-exclusion and the
per-recipient visibility gate re-assert unchanged over it.
* Mention diffing: editing a comment or a description notifies only the
NEWLY added mentions (new_mentions, set-differenced on the folded
address). Description edits fan out to the diffed mentions and nobody
else.
* The unread count splits into {total, mentions} in one FILTERed query so
the two numbers can never describe different row sets.
UI: bell badge = total with a distinct @ marker and panel count for unread
mentions (tokens only); TaskPanel gains a watch/unwatch toggle (Button
primitive, optimistic, hidden until the state is read); unreadSplit/afterRead
pure and tested, tolerating the pre-split payload during a deploy window.
Tests: shared fake learns pm_task_watchers and mirrors the audience UNION
arm-by-arm off the statement text; test_projects_watchers.py proves the four
triggers idempotent, watch/unwatch + R5, audience minus actor gated by the
recipient's visibility (including a watcher who LOST visibility), the
edit-twice mention diff, and pins migration 165 as text (found by content,
R1). 910 projects/tenancy tests green; vitest + tsc green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…, activity labels, coalescing, semantic sorts, picker exclusions, human IDs) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…t provenance A captured task is real from birth: POST /projects/intake writes an ordinary pm_tasks row plus a pm_intake wrapper in ONE transaction, parked in a triage-category status (provisioned per root on first use, never is_default). * Migration 164: pm_intake (task_id UNIQUE, status CHECK pending|accepted| declined|duplicate|snoozed, snoozed_until, duplicate_of_task_id, source, source_ref, organization_id per D-MT-3 with 161's fill/refuse trigger on BOTH task FKs), and `triage` joins the pm_task_statuses.category CHECK via migration 150's find-by-shape DROP/ADD idiom. * ONE default-list exclusion predicate — core.triage_exclusion_clause beside the visibility helpers. Board/list (/tasks), calendar/timeline (/calendar) and search all route through it and accept include_triage; a structural test pins that no surface can silently drop the flag (§11.16 extended) and that the clause has exactly one copy. * Rulings: accept flips the task's status IN PLACE through apply_status_transition (never copies); decline archives; duplicate sets duplicate_of_task_id and archives; snooze hides until snoozed_until and reappears by being read — no worker, no cron. All four write pm_activities via record_activity; the wrapper is never deleted (pinned behaviourally and structurally). Terminal wrappers refuse a second ruling (422). * Visibility: queue and rulings ride task_visibility_clause / load_visible_task — R5 404s (never 403), R10 case-folded identity, grants identical to the tasks the queue wraps. * UI: TriageRail (pending items + the four rulings: accept→lane picker, duplicate→search-backed task picker, snooze→date) with pure logic in lib/intake.ts; page.tsx wiring kept to one component block. * Tests: fake mirrors pm_intake, the queue join and the exclusion predicate (applied only when the statement carries it); the category-vocabulary pins now read the LAST migration to constrain the CHECK, not the file that created it. Out of scope per the ticket: routing rules / auto-accept (workflows nodes, D6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…dd, drop feedback, keyboard cursor Four interaction upgrades to the Projects board, list and calendar (spec §9.1 WS-27y; P-11, P-13, P-17, P-18), keeping the house shape: pure logic in lib/*.ts with vitest coverage, thin components on top. 1. Sub-grouping: the board takes a second axis drawn as swimlanes (lib/swimlanes.ts — lane order and membership reuse groupTasks, so two-owner tasks land in both lanes and status lanes keep configured order). Per-lane collapse + show-empty travel with the saved view via toConfig/fromConfig (round-trip extended and tested). NOTE: the gateway's normalise_view_config still strips the new keys — see the follow-up called out in the code and ticket report. 2. Group-context quick-add: every list group, board column, lane cell and calendar day carries an inline title-only add pre-filled with that group's value. lib/quickAdd.ts owns the surface-agnostic (axis, key) → payload mapping — create-body fragment plus the assignees follow-up PUT — for WS-27x's spreadsheet row to reuse. 3. Drop feedback: dragging is always offered; a refused target overlays dropRefusal's reason (many-valued axis, grant boundary) instead of silently disabling. A lane-cell drop patches BOTH axes in one write (buildCellDropPatch skips axes already satisfied). After any drop or quick-add the card scrolls into view and flashes (flash.module.css, theme tokens only, reduced-motion aware). 4. Keyboard cursor: ArrowUp/Down walk list and board rows, Shift+Arrow extends the WS-27n selection additively through selection.range, Enter opens the panel; BulkBar feeds unchanged. Transitions pure in lib/cursor.ts. Verification: npx vitest run (1172 passed, incl. theme conformance), npx tsc --noEmit clean, eslint identical to baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Conflicts resolved as unions: search.py carries both the triage exclusion (WS-27u) and the picker exclusion (WS-27w) in one WHERE; the fake's search mirror keeps both statement-keyed arms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Conflicts resolved by taking WS-27y's restructured board/list (swimlanes, quick-add, cursor) and grafting WS-27w's taskRef into the new card helper and the list's # column. Dropped WS-27w's now-duplicate row block — WS-27y applies sortForView inside sections. Also closes the gap WS-27y flagged: normalise_view_config now carries sub_group_by / collapsed_lanes / show_empty_lanes with the client's own value rules, so lane state survives the server round-trip (3 new tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Two pieces, one ticket, because the column set IS the contract:
- shown_fields joins the saved-view config. lib/shownFields.ts is the ONE
vocabulary (core keys + custom.<key> by shape) that both the table's
columns and the chip gate read; toConfig/fromConfig round-trip it (set
semantics, default omitted, explicit [] kept), and the gateway's
normalise_view_config mirrors the discipline exactly — unknown/non-string
dropped, absent stays absent, tests beside WS-27y's lane-state ones.
- Chip gating: card.visibleChips is the VISIBILITY layer over taskCard.ts's
fact layer — CHIP_FIELD maps chip kind → field key, a hidden field earns
no chip, and board, list, calendar and timeline all draw through it.
- TableView: one row per task, columns = shown fields (+ Title), inline
editors driving the EXISTING write paths (PATCH for status/dates/priority/
custom fields, PUT for assignees — no table-only write path). Header
clicks map onto the gateway's TASK_SORTS keys (asc → desc → the view's
own order); status stays the WS-27w semantic sort because the server
does the sorting. Sub-tasks nest under an on-page parent via
table.treeRows (orphans surface flat, collapse local, cycle-safe).
- Quick-add rows per group bottom, reusing WS-27y's QuickAdd +
quickAddPrefill + useFlash unchanged.
- tableCursor.ts: the 2-D cell cursor — arrows move a visible ring, Enter
edits (or opens the panel where a cell has no editor), Esc cancels;
entry/clamping semantics deliberately match lib/cursor.ts, not a fork.
- Wired as a third-ish layout ("table" beside board/list/calendar/
timeline), persisted with views like the others; FilterBar grows the
shown-fields picker and counts a non-default set as saveable.
DESIGN_SYSTEM: tokens only, Icon/Button/Input primitives, theme suite green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…ep, automation-flagged activities Migration 166 gives root pm_projects archive_after_months / close_after_months (NULL = off, CHECK > 0) and an IANA timezone (NOT NULL DEFAULT 'UTC') so "a month untouched" counts from the project's own midnight. The columns ride the ordinary project PATCH/read path, root-only (422 on a child — the root's policy governs its subtree, like statuses), validated the way workflows validates schedule timezones. automation.run_lifecycle_sweep applies every enabled root's policy once, idempotently: closed-category tasks untouched past the archive window are archived (WS-27w's guard held by construction — only closed lanes are candidates), stale open tasks close to the project's cancelled-else-done lane through apply_status_transition, triage-category tasks (WS-27u) are exempt. Every write goes through the ordinary service as system:workflow:<id> and carries the new meta.automation flag, which record_activity/record_field_change/apply_status_transition now accept and the TaskPanel timeline renders distinctly (Bot marker). Automated description edits still coalesce; human and automated runs never fold into each other. Engine wiring mirrors _pm_task_updater: a config-free pm_lifecycle node type (graph, handler, timeout, catalog metadata) reaching the sweep through a closure-imported NodeServices seam — the scheduled workflow supplies nothing but the trigger and the node; all policy lives in the DB columns (D6). Fakes learn the new columns and the bound `< :param` cutoff shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
… feedback across Tasks and Projects The Tasks app and the Projects app must feel like one product, so the Plane-inspired interaction upgrades that landed in Projects (WS-27y — patterns only, no Plane code) now reach /tasks, through ONE shared implementation of each piece. 1. Promotion: the keyboard cursor moves to src/lib/cursor.ts (tests with it), the QuickAdd component and useFlash + flash.module.css to src/components/. app/projects keeps thin re-export shims (lib/cursor.ts, components/QuickAdd.tsx, components/useFlash.ts) so no Projects component changes an import. The shared cursor carries its own copy of the eight-line sweep range (selection.range stays the Projects selection model's API — a shared lib must not import app code). 2. Shared chip vocabulary on Tasks cards: TaskCard's hand-rolled due/duration/attachments/subtasks chips are replaced by the one TaskMeta renderer over taskMeta() descriptors, adapted from GtdItem by the new app/tasks/lib/cardMeta.ts (tested — GtdItem knows a subtask COUNT, not done/total, so the adapter adds a count-only chip in the shared slot instead of a "0/5" lie). GTD-only badges (context, deep, energy, source, stage, priority) stay: one grammar for the shared facts, not an erased identity. 3. Keyboard cursor in the Tasks grouped list and board: ArrowUp/Down walk the rows with the Projects ring classes, Enter opens the focus modal, the cursor scrolls its row into view. Plain cursor + Enter only — /tasks has no anchor/range selection model, so the shared cursor's shift-sweep stays dormant rather than half-forking taskStore. 4. Group-context quick-add: every board column and every grouped-list section carries the shared QuickAdd, prefilled by the new app/tasks/lib/quickAdd.ts (tested) and created through the new taskStore.quickAddNext — born a clarified NEXT action IN that stage / @context / energy / depth group, flashing where it lands. The computed lenses (priority, mode) offer no box: no create payload can promise the landing, and an add that files itself into a sibling group reads as a lie. 5. Drop feedback on the Tasks board: dragging over a target the drop would refuse (same column under a field sort; select mode) overlays the reason — app/tasks/lib/dropRules.ts (tested), naming the sort from the toolbar's own SORT_LABEL map (moved to lib/ordering.ts) — instead of the old silent snap-back. Every successful drop (board and list) scrolls + flashes the moved card via the shared useFlash. And the Projects list gains the Tasks header grammar: collapsible sections (chevron + local Set, cursor skips folded rows) and a count pill, so the two grouped lists read identically. Verification: npx vitest run (1278 passed, incl. theme conformance), npx tsc --noEmit clean, eslint clean on every touched file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…n steps Spec §9.1 gains the build record (migration numbers 164/165/166, the no-archive-endpoint discovery, the pm_lifecycle owner step, continuity gaps). Work plan WS-27 row and HANDOVER top box updated to the restarted branch's state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
…u-z build news Conflict was work_plan's WS-26/WS-27 rows only - git followed the ai-company-brain -> project-docs rename (D26) automatically for the rest. WS-26 takes main's truth (D5 merged #403, mig 163); WS-27 keeps this branch's u-z paragraph with the state cell corrected to 'a-t merged, u-z on PR #408'. Migrations 164/165/166 do not collide with main's 163. Fences green: tenancy boundary, purge cascade map, tenant coverage, projects migration.
vjvarada
pushed a commit
that referenced
this pull request
Aug 10, 2026
…e mint dropped An audit re-derived all 31 Plane findings from the CODE rather than from the write-ups. Two classes of defect, both fixed here. **Stale facts.** The board row said u-z were "on PR #408" (they are on main), that ab/ac were "deferred by owner priority" (both built), that ae was "minted not dispatched" (both halves built), and listed migrations only to 166 — omitting 167 and 168, which under R1 is exactly the omission that produces renumber collisions. Sixteen places across the spec still claimed "on branch, NOT merged" for work that has since merged, several naming branches a reader would now chase and not find. **Two structural holes the board could not show.** 1. Slices S1-S6 belong to WS-27 and were never listed on it. Six built slices, one of which (S3) reversed a recorded WS-27ad decision by owner ruling, were invisible to anyone reading the dispatch board. 2. Minting WS-27u-z from the Plane research silently lost P-8, P-22, the inbox half of P-20, and the whole of research §4 item 14 — and the MECHANISM matters more than the four items. The research numbered its findings P-1..P-31 and its frontend list 1-14 independently, and §8's verdict table (the one minting read) maps only P-numbers. So item 14 was never eligible to be minted, and no `P-\d+` grep over the two documents could ever reveal its absence. That is why the loss stayed invisible through three readings of the same research. Minted as §9.3: WS-27ah (the four dropped items), WS-27ai (the descoped notifications inbox — whose only trace was the word "part" in a parenthetical), WS-27aj (two adoption triggers naming tickets that do not exist; one half is an owner decision because it would touch D12's two-axis visibility model). Recorded with it: `clickup_id` is globally unique, so two organisations cannot import the same ClickUp workspace, and nothing on the board said so. Cheap to fix now, a retrofit after the second tenant exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
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.
Builds the entire beyond-parity queue minted from the Plane research (⚠️ All interaction designs are re-derived in this repo's idiom — nothing is copied from AGPL-licensed Plane (see the AGPL wall in
specs/project_management_app.md§9.1) plus the owner-directed UI-continuity pass between the Tasks and Projects apps. Six tickets, built by six parallel agents in isolated worktrees and integrated in two waves.specs/plane_pm_research_2026-08.md).What's in here
WS-27u — intake/triage (migration 164)
pm_intakewrapper table (status pending/accepted/declined/duplicate/snoozed, provenance permanent) +triagestatus category.core.pybeside the visibility clause: triage-parked tasks appear on no board/list/calendar/timeline/search surface unlessinclude_triageis passed; the parameter-coverage structural test now enforces it can't be dropped per-surface.POST /projects/intake(task+wrapper in one transaction) and the four rulings; accept flips status in place, decline/duplicate archive with provenance, snooze reappears pastsnoozed_until. All rulings writepm_activities. TriageRail UI with lane/duplicate/date pickers.WS-27v — watchers & mentions (migration 165)
pm_task_watchers(casefolded, agents fenced; seeds task authors so the old audience survives the switch).resolve_visibility_for.{total, mentions}; the bell renders mentions distinctly.WS-27w — read-path/history hardening (no migration)
archived_athad no writer) — addedPOST …/archive+/unarchivewith the 422 guard naming the category.record_field_changesingle door: FK-valued history carries{field, old_id, new_id, old_label, new_label}, enforced by an AST structural test over call sites.(created_at, id)tiebreaker on everyTASK_SORTSentry.GET /projects/search?exclude_relatives_of=filters self/ancestors/descendants/already-related so pickers can't offer what writes will 422. Human task refs on cards/panel with copy-deep-link.WS-27x — spreadsheet layout + shown-fields contract
shown_fieldsjoins the saved-view config (client round-trip + servernormalise_view_config, old views stay byte-identical). Chip rendering on every surface gates on it.TableView: columns = shown fields, inline cell editors over the existing PATCH/PUT paths, header sorts verified againstTASK_SORTSby a cross-language test, indented sub-task expansion, bottom quick-add rows, arrow-key cell cursor (Enter edits, Esc cancels).WS-27y — board/list interaction upgrades
BulkBarunchanged.WS-27z — lifecycle policy (migration 166, default off)
pm_projectsgainsarchive_after_months/close_after_months/timezone; root's policy governs the subtree, child writes 422.run_lifecycle_sweep: archives closed-category tasks and closes stale open ones, cutoffs at midnight in the project's own timezone, triage exempt, idempotent, all writes through the ordinary service.automation: trueonrecord_activityfor workflow/sweep writes, rendered distinctly in the timeline. New config-freepm_lifecycleworkflow-engine node mirroringpm_task's wiring. Activation (authoring the scheduled workflow, enabling per-project) is an owner step — documented in HANDOVER §1.Continuity backport (owner directive)
src/lib/cursor.ts,src/components/QuickAdd.tsx,src/components/useFlash.ts— Projects re-exports shims, both apps run one implementation.taskMeta()/<TaskMeta>) alongside their GTD badges; Tasks board/list gain the keyboard cursor, group-prefilled quick-add, and drop-refusal/flash grammar; Projects list headers gain Tasks' collapse + count-pill affordances.Verification
uv run python -m pytest tests/unit/ -q→ 5789 passed (49 skipped are the documented needs-live-Postgres checks);ruff --select F821,F601,F602,F502,F7,B006clean.npx vitest run→ 1278 passed incl. the theme-conformance suite (no new colour debt);tsc --noEmitclean.test_tenant_coverage,test_tenancy_boundary) pass with the new tables auto-scoped.Not in this PR / owner-gated
schema_migrationscheck first; 160–162 from WS-27 parity backlog closed · WS-29 · merged with main's MT-1b — and the homonym it would have applied #399 may also still be pending there).pm_lifecyclescheduled workflow; enabling lifecycle policy on any project (all default off)./workflowsnodes per D6, deferred to email capture.🤖 Generated with Claude Code
https://claude.ai/code/session_01VmFScimSbeyHcLdut7RT4W
Generated by Claude Code