Files
Maven/docs/plans/15-board-surface.md
T
claude fd3d063e02 docs: decide the board surface — build the board, not the argument (V-431)
The decision the task asked for. Build it, in a smaller shape than the
task imagined, because most of it is already there: the tasks table, the
capture parse, the recite matcher and the /tasks page all landed under
#130, #129 and #128.

Three findings changed the shape.

The intake form cannot live on the voice path. resolveConfirm is a
binary yes/no slot with a 90-second life, so filling four fields is a
mechanism nobody has written, and the definition of done is the worst
possible field to dictate through whisper. It moves to the page. Voice
captures a line and recites the list; the page turns a candidate into an
open item.

The stage-0 trick stretches to recite and to status change, both of
which are a marker plus a lookup. It does not stretch to intake, and it
does not have to.

A task is write-once except for its status. SetTaskStatus is the only
mutation, so the form has nothing to save into until an edit path
exists. That is now step 2 of four, and it was not in the task text.

The argument stays unbuilt. Same line internal/memory/behavior.go
already drew for habits: she counts a stall and never assesses one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 06:00:56 +04:00

120 lines
6.2 KiB
Markdown

# 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.