Step 5 of the board build. internal/tasks/stall.go counts three shapes —
overdue, sitting longer than StallDays, waiting for confirmation — and states
nothing about what any of them means. That is the line
internal/memory/behavior.go already drew for habits, and the reason is the
same: a 1.7B asked to judge will agree fluently and launder a guess into a
decision. A test asserts the wording carries no assessment.
Sitting is measured from created_ts, the only clock a live row carries: the
store stamps resolved_ts and nothing else. So "no state change in eleven days"
is exactly "captured eleven days ago and still live", which is narrower than
the plan's wording and is the claim the data supports. A candidate is never
counted as overdue, because its due date is Maven's reading of a mail rather
than a deadline he set.
Not a nag. No tick rule reads the counts; they go on /tasks and into the list
reply when he asks, and tickLoop.dayPlan still does not read tasks at all. The
empty case renders as nothing: "ничего не залежалось" appended to every list
read is a nag with a friendly face.
Three say entries, so the page and the spoken list cannot word it differently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
The other half of the stage-0 rule. actionAct intercepts task_status ahead of
both ecosystem clients, because the board is Maven's own store and reaching a
capability registry would answer a question about his task list with a gap.
Three answers besides the move, and none of them guesses. No match says so.
More than one match asks which, since closing the wrong task marks work he
never finished as done. No task named asks which too, because the router claims
the turn without the referent and the list lives here.
Matching is normalised containment either direction, over the same
store.NormalizeTaskText key capture dedupes on — he shortens what he said as
often as he pads it. Deliberately not fuzzy: a ranked best guess always returns
exactly one answer, and the one thing this has to be able to say is that it is
not sure.
A candidate he says is done takes both legal store moves. The store refuses
candidate → done, and saying it out loud IS the confirmation the candidate was
waiting for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
The clarify store was keyed per reach in V-466. The dialogue session was not:
five call sites read and wrote the constant voiceDialogueID, so anaphora,
history and the ordinal candidate list were one slot for the whole daemon.
The candidate list is the half that cost something. She recites tasks at the
mic, he types "первую сделал" on /chat, and it closes the second task he heard
out loud on a surface that never showed him a list. Now every one of those
sites reads dialogueIDOf(ctx), which handleText and the voice path already set.
resolveCandidate also wrote resolved_by "tap:voice" for every pick, including a
typed one. It takes the turn's source now. A row that lies about where it came
from is worse than no row.
Anaphora across surfaces was the other reading — one continuous conversation
with her, any surface. Rejected: a phone open while he talks is the case this
box hits, and two clients sharing one slot trample each other.
attentionq.go, repair.go and internal/router/complaint.go carry the last
hand-written Russian patterns of the V-522 sweep, and they live on task/467.
internal/lexicon, internal/morph and cmd/mavend/topics.go live here. One of
the two had to move.
Four conflicts, and one of them is a real collision rather than a mechanical
one. Both branches wrote the narrative stage 0 rule. This side had
NarrativeQueryGrammars, plural, with the rest-of-day rule beside it and the
verb alternation built from the lexicon; task/467 had NarrativeQueryGrammar,
singular, which extracts the topic into Slots.Text, refuses a bare "расскажи",
and excludes the shapes that are chat ("расскажи о себе", "историю на ночь").
Resolved by keeping this side's container and this side's lexicon-built
pattern, and taking every behaviour only the other side had: the topic slot,
the empty-topic refusal, chatNarrativeTopics, and its wiring position after
TaskCaptureGrammar so "запиши" still beats "расскажи".
The rest: queryFeeds keeps task/467's conditional claim (V-474 supersedes the
unconditional one), rank.go keeps Spoken and drops pluralTasksRU because
say.CountWord is the one copy of Russian count agreement, and vendor/ was
re-vendored — the merged modules.txt claimed replaces for nexus and praxis
that neither go.mod has.
Routing fixture 58/82, unchanged from both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read before routing and only when a list is bound: with nothing offered,
"второй" is an ordinary word and keeps routing. No verb reads it back
rather than guessing what to do with it.
The daemon tests that compared against one literal ask the entry instead: IsAck
names the line she could have said without pinning the wording. The eval scores
every ack variant on the persona checks the nudges already pass.
A task extracted from mail deduped on the live-norm index only, so once he
finished it the row left the live set and the next poll of the same immutable
message re-extracted it as a fresh candidate. mavmaild is a read-only reader
and marks nothing read, so that repeats forever. Derived rows now carry an
ext_id built from the message uid and the extracted span, unique across every
status, while voice keeps live-only norm dedupe because saying an errand again
is the recurrence signal. A derived source can no longer capture straight to
open, and saying a task out loud that Maven had only proposed promotes the
candidate instead of answering that it is already in the list.
SetTaskStatus was classified AuthRead. Resolving a task is not additive, it
erases work off his list, so it is a write, and the row now records the caller
that moved it. ListTasks was unbounded. The list-query matcher claimed any
utterance with "что мне делать", including "с чем мне помочь", and the urgency
stripper matched inside words.
Found in review of #60.
Ordering is computed, not generated. Asking a 1.7B which of his tasks
matters most produces a fluent opinion with no basis in anything, and a
confidently wrong priority is worse than none — same posture as the
behaviour profile in internal/memory, which counts instead of summarising.
internal/tasks is a pure package (no ipc, no store, no cgo) holding the
score, the order and the Russian rendering, so the spoken list and the
/tasks page cannot drift. Four signals, all of them things he stated:
deadline (overdue > today > tomorrow > this week), stated urgency, age
with a cap so nothing rots at the bottom, and confirmed work always
ahead of mail-derived candidates. A task with no due date and no weight
scores nothing and carries no reason string — inventing a "потому что"
about a priority he never set is the failure mode this avoids.
Capture now picks up urgency he says out loud ("добавь в задачи срочно
оплатить интернет"), stripping the marker from the task text, and the web
add form offers the same three rungs. Ranking is a read: it sorts and
renders, never writes, schedules or announces.
A task is not a fact and not a note. A fact is a claim about the world that a
correction supersedes; a note is something to recall by meaning. A task is work
with a lifecycle, and the read that matters is "everything outstanding right
now" — which over an append-only log would mean replaying history on every
question. So: a tasks table, migration #14, statuses candidate/open/done/dropped
that each move forward exactly once.
Dedupe is on normalised text among LIVE rows only, via a partial unique index.
That is the property the mail side needs: an extractor may call CaptureTask for
every message it reads, as often as it likes, without growing the list — while a
weekly errand is still capturable again once the last one is done.
Three ways in, one seam. ipc.CaptureTaskReq is it: the voice path
(router.ParseTaskCapture on an explicit marker — "добавь в задачи …", never
"надо бы поспать"), the /tasks form, and the email reader from #246 when it
exists. Mail-derived items set Source "email:<account>", Status "candidate" and
Evidence to whatever makes the row reviewable; a candidate is inert until he
confirms it on /tasks, and Maven names it as unconfirmed when she recites the
list rather than putting words in his mouth.
No new intent — the router enum is a contract with the relabelling prompt, so
capture rides the note intent and the list rides a query source, both matched
deterministically like the calendar and plan matchers already are.
Nothing here speaks. No tick rule reads tasks; the list is answered when asked
about, which is why /tasks POST is not step-up gated the way /tools and
/routines are — a task write moves no boundary.
Vikunja #130