diff --git a/docs/plans/15-board-surface.md b/docs/plans/15-board-surface.md new file mode 100644 index 0000000..5b49d30 --- /dev/null +++ b/docs/plans/15-board-surface.md @@ -0,0 +1,119 @@ +# Plan: The work board surface + +**The decision Vikunja #431 asked for. Written 04-08-2026.** + +**Verdict: build it, in a smaller shape than the task imagined.** The board is worth +moving out of the file. The intake form belongs on the `/tasks` page, not on the voice +path. The argument is not built, now or later. + +## Why it is worth building + +The reason is the one the task gives and it holds: company rules forbid pointing Claude +at work repos, and Maven is the one assistant on the box that work material may reach. +No telemetry, no cloud model, no third-party account. That is not a preference here, it +is the whole permission. + +The build is also small, because most of it landed already: + +| Piece | Where | State | +|---|---|---| +| task rows, dedupe, status lifecycle | `internal/store/migrations.go:149` and migration #15 | done | +| capture from speech, urgency stripped | `router.ParseTaskCapture`, `router.TaskCaptureGrammar` | done | +| recite the list on request | `router.IsTaskListQuery` | done | +| a page to read and change the board | `/tasks` in `cmd/mavweb` | done | +| counting a shape without judging it | `internal/memory/behavior.go` | done, as precedent | +| a proposal he reads when he chooses | `/routines`, the proposed-routine queue | done, as precedent | + +`tasks` already carries `status` (candidate, open, done, dropped), `due_ts`, `weight`, +`source`, `evidence`, `ext_id` and `resolved_by`. Three things are missing. It has no +definition of done and no blocked-on. There is no way to edit a task after capture: +`SetTaskStatus` moves the status and nothing writes text, date or weight again. And +there is no grouping he controls, because order is computed by `tasks.Rank` alone. + +## Where the form lives, and why not voice + +The task asks the form to refuse a capture with no definition of done. That refusal +cannot live on the voice path, for two reasons. + +**The parked-state mechanism is binary.** `resolveConfirm` in `cmd/mavend/confirm.go` +answers yes or no against a slot with a 90-second life. Filling four fields over four +turns is slot filling, which is a different mechanism and a new one. Nothing in the +daemon does it today. + +**The definition of done is the worst possible field to dictate.** It is the one string +that has to be exact, because its whole purpose is to be unarguable later. Whisper +transcribing a sentence of Russian work vocabulary is where exactness goes to die, and +the capture path already had to strip a question mark that whisper invented. + +So: voice captures a line and recites the list. The page is where a line becomes an +item with a definition of done, a blocked-on and a date. A captured line lands as +`candidate` and stays there until it is filled in, which is what `candidate` was for. + +The refusal the task wants survives, moved: the page will not promote a candidate to +`open` without a definition of done, the same way `ParseTaskCapture` will not file a +marker with nothing after it. And the field must close on either outcome, so "it already +works" counts as complete. A definition of done that only one result satisfies is a wish. + +## Does the stage-0 trick stretch + +The task asks this before any shape is committed to. It was checked. The answer is +partly. + +`TaskCaptureGrammar` matches every utterance and lets `ParseTaskCapture` decide inside +`Build`, keeping the intent at `note` and leaving the frozen seven-intent contract alone. +That trick stretches to **recite** and to **status change**: both are a marker plus a +referent, both are a lookup, and a status change is a small closed verb set over a list +he can see. It does not stretch to **intake**, because intake is not one utterance, and +it does not need to, because intake moved to the page. + +One cost to name. Each such grammar matches everything and runs its parser on every +turn, ahead of the resident model. Two more of them is fine. A dozen would make stage 0 +a second router with no evaluation behind it, and at that point the frozen enum is the +smaller problem. + +## What is not built: the argument + +Not now and not later behind a flag. The task is right about why, and +`internal/memory/behavior.go` already argued it for habits: a 1.7B asked whether evidence +proves anything will agree fluently and launder a guess into a decision. A wrong claim +about his work, stated confidently, is the most expensive kind of wrong Maven can be. + +The line is the same line behaviour memory drew. She may **count**: + +- no state change in eleven days +- blocked on a person, with no date +- four of nine waiting on two people + +Those are queries over rows. She may not assess whether a build proves anything, whether +a blocker is real, or whether a task should be dropped. + +## Persona + +A progress tracker is a nag by default, and "not a nag" is hard. The line is already +drawn twice in the codebase and it is drawn the same way here: + +- A date he set becomes a reminder. He set it, so it is not her raising it. +- A stall becomes a proposal he reads when he chooses, on a page, like `/routines`. +- Ask what is on the board and she recites. She never opens with it. + +The day plan is the place to watch. `tickLoop.dayPlan` reads calendar events, pending +reminders and checklist facts, and it does not read tasks. Adding the board to the +morning nudge is exactly the move that turns this into a nag, so the board goes on the +page and into the answer when asked, and not into the unprompted morning message. + +## The build, as tasks + +1. Two columns on `tasks`: definition of done, and blocked-on. Blocked-on resolves + through Nexus like any other person reference, because identity lives in Nexus. +2. An edit path. Today a task is write-once except for its status, so the form has + nothing to save into. +3. `/tasks` grows the form: promote candidate to open only with a definition of done, + set a date, set blocked-on. A date set here writes a reminder. +4. A status-change grammar at stage 0, following `TaskCaptureGrammar`. +5. Counted stall shapes on `/tasks`, phrased as counts. No assessment. + +Note for whoever picks up 3: `/tasks` accepts its POST without the step-up gate, while +`/routines` and `/tools` require a passkey. That was deliberate for capture. Adding an +edit path is the moment to re-argue it, not to inherit it silently. + +Each is separable and each is worth stopping after.