Commit Graph

789 Commits

Author SHA1 Message Date
claude da2d11dab6 plan: the claim unit and its four bands, measured (V-565)
Measures what each claimant on an utterance reports across the 91-case RU
fixture, then argues an ordinal band set from that distribution.

The classifier's cosine is flat against correctness: 62% correct below its
median and 62% above, over a spread only 0.083 wide, with every case above
the 0.55 gate. Its top-two margin is p50 0.009 and never reaches 0.03. So a
calibrated float is not cheaply available and the ledger's assumption holds.

Stage 0 is 20/20 on the cases it claims and asserts 1.0 for all of them. The
LLM router emits two values, and the lower one is a self-veto with a reason
flattened into a number.
2026-08-06 00:46:50 +04:00
claude b6305f1b6e Merge: an unrecognized act says so and lists nothing (#205) 2026-08-05 23:43:22 +04:00
claude 5bd1406c7a an unrecognized act says so and lists nothing (V-556)
Reciting the allowlist answered a question he did not ask. She says the command
is not one she knows, once, and parks nothing.
2026-08-05 23:43:22 +04:00
claude d988154063 Merge: an act with nothing on the other end says so (#204) 2026-08-05 23:34:33 +04:00
claude 1b76fa8205 an act with nothing on the other end says so (V-556)
askClarify parked "Что сделать?" whatever was on the other end. With an empty
allowlist that question has no answer: she asks, fails, asks again and gives up,
three turns spent on a request she could have declined in the first one.

Empty allowlist now names the gap and parks nothing. A non-empty one still asks,
and names what she can run, capped at six, so the question is answerable.
2026-08-05 23:34:25 +04:00
claude e87088afb8 Merge V-515: workpc is the client machine (#203) 2026-08-05 23:22:57 +04:00
claude 1b8d2c60d3 workpc is the client machine the voice loop was waiting for (V-515)
Three durable stores said no client machine existed. That was written
when the workstation was only a model host. It is where he sits most of
the day and it has the microphone.

The verdict is unchanged and so is the seam. What changes is the size of
the remaining work: deploying two daemons and asking mavend to listen on
TCP, not acquiring hardware. Note that deploying them does not by itself
prove a wake word — mavwaked gates on energy and has no keyword model
(V-487).
2026-08-05 23:22:57 +04:00
claude 12667fd3b8 Merge V-555: the self prompt asks for the present tense (#202) 2026-08-05 23:06:48 +04:00
claude e4fd6140a9 the self prompt asks for the present tense (V-555)
Measured on the box: "глаголы в прошедшем времени с окончанием -ла",
copied from the query prompt where it fixes her gender, was read by the
resident model as an instruction to use the past tense throughout. She
answered "я вела заметки" and "если ты разрешил, я управляла домом",
which makes a live capability sound finished.

The gender rule stays, without the example.
2026-08-05 23:06:48 +04:00
claude 2ba5d0a60e Merge V-555 tail: she does not look herself up (#201) 2026-08-05 23:05:09 +04:00
claude c6b11a6d1d she does not look herself up (V-555)
Two defects found probing the new source on the box.

"кто ты" was answered from one of his notes. The self source sat below
memory and notes, which match by proximity and have no idea the subject
is her. It belongs above all three: a question about her has no answer
in his data either.

And PhraseQuery opens every answer with "вот что я нашла: ", which is
deliberate — it marks the answer as a lookup. Her own description is the
one subject she did not look up, so this is PhraseSelf instead, same
read-only discipline and its own opener. The Stub reads the description
out as it stands, which needs no fallback: it is already her voice.
2026-08-05 23:05:09 +04:00
claude 45622eff3d Merge V-555: a question about herself has an answer (#200) 2026-08-05 23:00:03 +04:00
claude 5815f0b8f3 a question about herself has an answer (V-555)
"что ты умеешь" reached the personal boundary, which claimed it as his
and said "не знаю — не нашла у тебя такой записи" about her own
description. Letting it past would be no better: SearXNG answers about
somebody else's assistant.

A self query source above the boundary, reading one frozen description.
It is NOT a note — notes are his, and a note about her would come back
for "что я записал", would be fed to the digestion worker as something
he said, and would be recalled by proximity for questions that are not
about her.

The description names only what this box does. Everything that depends
on config — the house, the LAN, the feeds, the list, weather, telegram —
is named as depending on what he allowed, and a test pins that split:
inventing a capability here is the same defect as inventing a fact.

topicSelf is scored like every other topic, with a narrow keyword floor
for the no-embedder case. "что ты умеешь" moved off topicOther, where it
had been sitting so an attention question had something to lose to — a
phrasing on two sides never clears the margin. TestONNXTopics 38/38 ->
43/43 on held-out utterances.
2026-08-05 22:59:56 +04:00
claude f68d49d9e2 Merge V-554 tail: a device's history is not a scan request (#199) 2026-08-05 22:45:10 +04:00
claude 36bc603f52 a device's history is not a scan request (V-554)
Found verifying the three fixes on the box: "кто изобрёл телефон" ran a
LAN scan and answered "нашла 3 устройства". The network seed set opens
with "кто в сети сейчас" and names devices throughout, so a "кто ..."
question about any device noun landed there.

Three topicOther seeds, same shape as the V-553 fix. TestONNXTopics
34/34 -> 38/38 on held-out utterances, and a real scan is still a scan.
2026-08-05 22:45:10 +04:00
claude 59214b4fdd Merge V-554: three defects that made an ordinary conversation go wrong (#198) 2026-08-05 22:40:38 +04:00
claude e94c868160 a chat prompt says which turn to answer (V-554)
Prior turns were joined with newlines and nothing else, so the model got
four unlabelled lines and no way to tell which one was the question. It
answered an earlier one: asked "как дела" after a question about the
telephone, she carried on about the telephone. Four turns live for
fifteen minutes, so the line she answered was often minutes old.

One user message still, because the template constraint that forced the
flattening is real. The turns are labelled as his own earlier words and
the current utterance is named as the one to answer. With no history
the message is the utterance alone, unchanged.
2026-08-05 22:40:28 +04:00
claude de4c47459a the personal boundary lets a narrative world question through (V-554)
"расскажи про Байкал" was refused as his by 0.0052. Every world seed
opened with an interrogative, so a world question phrased as an order
landed nearer "я тебе рассказывал об этом?" — the same verb about his
own words. Four narrative seeds on the world side.

TestONNXPersonalBoundary 25/25 -> 29/29 on held-out utterances, and the
control "я рассказывал тебе про байкал?" is still his. TestONNXTopics
unchanged at 34/34.
2026-08-05 22:36:10 +04:00
claude 27bb9119fb clarify steps aside when the next turn is its own request (V-554)
A parked question consumed whatever came next. One act she could not
fulfil ate three turns: "выключи свет в спальне" asked "Что сделать?",
and "кто изобрёл телефон" was scored as an answer to it, then "как
дела" after that. Nothing tested whether the words could be an answer.

The test is two offline token checks that already existed for other
callers: a question shape, or a capture verb. It fires only where the
answer filled nothing, so an answer that closes the gap still lands
whatever shape it has, and the retry budget is untouched — the count
was never the problem.
2026-08-05 22:34:01 +04:00
claude 0ab5dc1482 Merge the personal boundary day-word seeds (#197) 2026-08-05 21:57:32 +04:00
claude 35ae1f41da the personal boundary reads the day-word frame too (V-553)
The topic seeds let "какой сегодня праздник" and "что интересного
произошло сегодня в мире" past the weather source, and the personal
boundary refused them one source further down: "не знаю — не нашла у
тебя такой записи" about a public holiday.

Same defect, same mechanism, one layer lower. "что у меня сегодня" is a
personal seed and worldSeeds had nothing in that frame. Two seeds fix it.

TestONNXPersonalBoundary 22/22 -> 25/25, nothing regressed.
2026-08-05 21:57:32 +04:00
claude a9db82b04c Merge the day-word topic seeds (#196) 2026-08-05 21:54:38 +04:00
claude 278eeeffdf the world question that names a day is not weather and not his (V-553)
Two recognisers claimed world questions naming a day, both by the same
mechanism and neither by its keyword floor.

topics: weather was the only topic whose seeds carry a day word, four of
eight. So every "какой сегодня X" landed nearest it. "какой сегодня
курс доллара" cleared the margin by 0.0220 and "какой сегодня
праздник" by 0.0398, against 0.0883 for a real weather question, and the
gate asked "для какого города?" about the dollar.

The margin was not the knob: 0.0398 is not a coin flip, and raising the
bar far enough would take real weather with it. topicOther was missing
the negative class. Six seeds, four naming a day and two carrying the
"какой сегодня X" frame itself — a frame both topics use has to sit on
both sides, or the side that owns it wins every noun it has never seen.

personal boundary: the same shape one layer down. "что у меня сегодня"
and "когда моя встреча" put "when does a thing happen" on the
personal side and no world seed answered it, so "во сколько закат
сегодня" was refused as his. Three world seeds, each carrying сегодня,
which is the half of the frame that does the pulling — without it they
caught nothing.

Measured, both opt-in against the ONNX embedder homesrv runs:
  TestONNXTopics           27/27 -> 34/34 (7 new cases, none regressed)
  TestONNXPersonalBoundary 19/19 -> 22/22 (3 new cases, none regressed)

The control matters as much as the fix: "во сколько у меня встреча" is
the same frame about something that IS his, and it holds at +0.0842,
unchanged from before the seeds moved.
2026-08-05 21:54:28 +04:00
claude b23596f54f Merge the calendar narrowing (#195) 2026-08-05 21:41:00 +04:00
claude 6e3bb3be97 each agenda grammar is tested against its own example (V-552)
Replaces a test whose name promised more than its body checked: it
looped the grammars asserting Pattern != nil, which regexp.MustCompile
already guarantees at init. Asserting the grammars pass IsAgendaQuestion
would be true by construction, since the first arm is that same loop.

A hand-written example per grammar name catches what neither does: a
grammar edited until it no longer matches the case its comment gives,
and a new grammar nobody wrote an example for.
2026-08-05 21:40:49 +04:00
claude aa7ef33bbf the calendar answers his day, not any day (V-552)
queryCalendar matched on a day word and stepped aside only on weather
wording. Every world question naming a day was claimed by it and answered
with an empty schedule: "какой сегодня курс доллара" replied "на
05.08.2026 ничего нет", which reads as an answer about a subject she
never looked at. All four probe utterances have an answer in search, and
search sits below the calendar.

V-474 fixed one instance of the class. Sunset, holidays, exchange rates
and world news are the same class and weather wording does not cover them.

router.IsAgendaQuestion is the narrowing. Its first arm reuses
AgendaQueryGrammars, so the rule that routes a question to the query
chain and the rule that lets the calendar answer it cannot drift. The
second reads a scheduled-thing noun, wider than the grammars because
"какие встречи завтра" carries no possessive. The third claims a
question that names no subject of its own.

A continuation is exempt: "а завтра?" cannot name an agenda, and this
is the only date-aware source there is.
2026-08-05 21:40:15 +04:00
claude f2851b3729 Merge the routing re-measurement (#194)
V-320 items 2 and 3. Cascade + resident model is 75.8% full / 80.2%
intent-only at p50 1.19s on the 91-case fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:31:31 +04:00
claude d49067f7dd eval: score and time the resident model as router (V-320)
Item 2 was blocked because the resident llama-server binds --port 0 inside the
container, so no host process can reach it. Cleared by taking the first of the
three ways out the task listed: a second llama-server on the same gguf, on a
fixed host port.

Cascade + resident model scores 75.8% full and 80.2% intent-only at p50 1.19s
and p95 1.65s, on the fixture as it now stands at 91 cases. That is a new
baseline rather than a movement: 14 cases were added since the 77-case number
in CLAUDE.md.

The model alone scores 37.4% full against 61.5% intent-only. The gap is slots,
not routing. Every reminder case leaves the time to the daemon, which is what
the contract asks of it, and the cascade fills them.

Item 3: the ~6s figure recorded in the task was one sample through the whole
of POST /api/chat, not the router, and is not comparable.

Item 4 is still not run. Killing the resident llama-server needs a permission
this session does not have, and it now has a second half anyway, since with the
workstation up only killing both proves the classifier answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:31:31 +04:00
claude b528a8f5c9 Merge the bare-hour fix (#193)
V-551. "завтра в семь" booked the reminder for the current clock. dateparser
needs the colon, so the script gives it one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:17:30 +04:00
claude 44320ee496 dates: a bare hour after a day word is an hour, not the current clock (V-551)
At 21:12 "напомни мне завтра в семь позвонить маме" confirmed a reminder for
21:12 tomorrow. The hour was dropped and the wall clock carried onto the named
day. She did not ask; she named a time nobody gave her, on a path that fires.
A bare "напомни в семь" declines correctly, so adding "завтра" turned a decline
into an invented answer.

dateparser only reads a bare hour when it carries a qualifier or a colon.
"завтра в 7" keeps the current clock and "завтра в 7 часов" is read as seven
hours from now, which moves the day as well. English "at 7" fails identically,
so this is not a Russian defect and both prepositions are rewritten.

The script now gives it the colon: "в 7", "в 7 часов" and "at 7" become
"в 07:00" beside the existing утра/вечера rewrites. A duration is untouched,
because "через 2 часа" has no preposition to match, and so are "в 7:30",
"в 30 минут" and "в 2026 году".

The stub parser has always read the token after the day word, so the floor was
right and the production parser was not. No test on the stub could have caught
this. The four new cases are in TestPythonDateParser, which runs where
dateparser is installed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:17:21 +04:00
claude 337a777d2e Merge the reach measurement with the resident model (#192)
V-517. The model alone reaches Praxis 0/12, so the V-516 stage-0 grammars are
the only path there. Cascade+llm is 28/30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:03:23 +04:00
claude 576dfd8b4c eval: the resident model never reaches Praxis either (V-517)
V-405 measured reach with the classifier only, and the LLM router is the
deployed default, so 16/30 was the floor rather than the shipped behaviour.
TestReachWithLLMRouter scores the same 30 cases with the model, gated on
MAVEN_LLM_URL like TestLLMRouterBaseline.

The open question was whether the model writes a literal Praxis capability
into the fn slot and reaches a service the classifier structurally cannot. It
does not. Praxis is 0/12 with the model alone, exactly what the classifier
alone scores, and all twelve fail the same way: local, empty fn. Nothing in the
router prompt names a Praxis capability, so there is no string for it to write.

So V-516's stage-0 grammars are the only path to Praxis, not a determinism
argument. Through the cascade the model scores 28/30 with praxis 11/12, one
point above the classifier baseline. Hexis is 10/10 either way.

Overreach is 1 in both configurations, under the 4 the harness asserts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 21:03:05 +04:00
claude ed9db8dc44 Merge the history side fix (#191)
V-456. A question about what she recorded is answered as her turn, not his.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:54:20 +04:00
claude a8710c859b history: answer the side of the question that was asked (V-456)
"что ты записала сегодня?" was recognised as a history question and then
answered with "ты говорил: …". The rows are right — a tapped fact is one act
seen from two sides — but the sentence hands the question back instead of
answering it.

historyAsks returns which side was asked and queryHistory phrases from it,
including the nothing-found reply. His side is tested first, because "отмечать"
is on both verb lists and "что я отметил" is not a question about her.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:54:11 +04:00
claude 1bacbb7952 Merge the wipe (#190)
V-494 part 1. Store.Wipe drops every table and rebuilds from the migrations;
mavend -wipe is a dry run and -confirm-wipe deletes. QA isolation and
onboarding are the remaining two thirds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:51:55 +04:00
claude 82d9a3324e wipe: one command empties the box and leaves it standing (V-494)
There was no documented way to repair a poisoned box. Two invented facts
written during QA disabled world answering for every later turn (V-470), and
revert voids the SQL row while leaving the vector behind (V-493). This is the
operation that undoes both.

Store.Wipe drops every table sqlite_master reports and rebuilds from schema.sql
plus the migrations, rather than deleting from a hand-written list. A list has
to be edited whenever a table is added, and the once it is not, the wipe leaves
personal data behind while reporting success. It vacuums afterwards, because
free pages still hold readable text.

mavend -wipe prints every table and its row count and exits. That alone is a
dry run and answers what a QA session actually asks: what is on this box. It
deletes only with -confirm-wipe. Two flags, because the destructive reading of
one flag is the reading a mistyped command gets.

Nothing outside the database moves. Config, models, passkeys.json and the
encryption key are files.

QA isolation and onboarding are the other two thirds of V-494 and are not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:51:46 +04:00
claude 03d48ab789 Merge the intake form on /tasks (#189)
V-511. Confirming a candidate asks for a definition of done, resolves a
blocked-on name against nexus, and books a reminder when a date is set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:46:44 +04:00
claude 570b204571 board: /tasks confirms a candidate into an open task (V-511)
The candidate row is now an intake form, not a button. Confirming asks for a
definition of done and refuses without one, takes an optional blocked-on name,
and carries the date and the importance through.

The blocked-on is a name in the form and a canonical nexus id in the store.
promoteCandidate resolves it over the new ipc.ResolveEntity seam and stops the
confirmation on an ambiguous or unplaceable name rather than picking.

A date set here books a reminder for 09:00 that morning. That is the only
unprompted delivery the persona allows, because the owner set the date himself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 20:46:35 +04:00
claude cb350efb19 a surface can ask Nexus for the id behind a name (V-511)
blocked_on stores a canonical entity id, so the form that fills it needs a way
to turn "Kate" into one. ipc.ResolveEntity is that seam: the store adapter
refuses it, because identity is not the store's to answer, and the daemon
overrides it with the Nexus client the voice path already holds.

Three outcomes are kept apart, because a caller deciding whether to store an
id has to tell them apart. No nexus block is ErrNotImplemented. A miss is
ErrNoEntity. Several matches come back Ambiguous with the names, and the
caller asks — picking one is how a task ends up blocked on the wrong person
with nobody able to see it happened.

An outage stays the transport error. "There is no such person" and "Nexus is
down" must not read the same.
2026-08-05 20:42:28 +04:00
claude 43b0c32154 Merge the task edit path (#188) 2026-08-05 20:39:29 +04:00
claude b95a0278a4 /tasks edits a task in place (V-509)
The open list carries the text, the date and the importance as an inline form
with a save button. The status is not in it: that ladder is one-way and has
its own two buttons.

The step-up gate was re-argued rather than inherited, which is what the task
asked for, and edit stays ungated. It rewrites a line on a list he reads
himself, the same blast radius drop already has here, and the store refuses
the two edits that would cost something. A collision is named ("another open
task already says this"), not merged.

A weight outside the three rungs keeps its own option in the select, or
saving an unrelated edit would silently reset it to normal.
2026-08-05 20:39:22 +04:00
claude a6b17ada8b a live task can be edited, a resolved one cannot (V-509)
SetTaskStatus was the only mutation on a task row, so a typo in a dictated
task was permanent and a deadline could not move. EditTask rewrites the three
fields capture set — text, due date and weight — and nothing else. Status
stays the one-way ladder SetTaskStatus owns.

Two things the task asked to settle.

A text edit re-normalises the dedupe key and can collide with another live
row. That is ErrTaskDuplicate, a refusal rather than a merge: two live rows
carry two provenances, two capture times and possibly two external
identities, and merging picks a winner for all three with nobody asked. The
surface names the row that holds the text.

A resolved task is refused outright (ErrTaskResolved). Its text is the record
of what was finished, and rewriting it rewrites history.

due nil clears the date, because clearing has to be sayable — an absent date
and "remove the date" cannot be one argument.
2026-08-05 20:39:11 +04:00
claude 92949e886f Merge the Vikunja MCP preload note (#187) 2026-08-05 20:29:47 +04:00
claude 6b2667b7af CLAUDE.md: load the Vikunja MCP schemas in one call (V-445)
The four schemas are deferred, so a session that looks them up on first use
spends four round trips on tools it always needs. One ToolSearch line at the
start covers them.

Also records the update_task quirk: a call carrying a description resets done
to false, so closing a task with a write-up takes two calls.
2026-08-05 20:29:47 +04:00
claude 494a7721e0 Merge the CLAUDE.md pronoun fix (#186) 2026-08-05 20:26:22 +04:00
claude 46b58f0278 CLAUDE.md names the owner instead of saying "he" (V-550)
The third person here leaked into answers addressed to him, where it reads as
talking about the person reading the reply. Six lines now say "the owner".

"you" is not available in this file: CLAUDE.md addresses the agent, so "you"
there means the agent.

One "him" stays, in the persona block. That line states that Maven must never
say "он"/"его" about the owner, which is a fact about required Russian output
rather than a reference.
2026-08-05 20:26:22 +04:00
claude a88c984d16 Merge the definition of done and the blocker (#185) 2026-08-05 20:17:38 +04:00
claude 496559c9dd tasks carry a definition of done and a blocker (V-510)
Migration #22 adds done_when and blocked_on to tasks, both NOT NULL DEFAULT
''. "He has not written one" and "there is nothing to write" are the same
state here, so no caller has to tell NULL from empty.

blocked_on is a canonical Nexus entity id, never a name. It names a person
and identity lives in Nexus, so free text here would be a second answer to a
question Nexus already owns. The caller resolves before it writes.

Both columns round-trip through ipc.TaskAPI: on ipc.Task, settable at intake
through CaptureTaskReq, and writable afterwards through the new
SetTaskFields, which is deliberately not one-way — he may sharpen a
criterion, and a blocker clears when the person answers.

SetTaskStatus now refuses candidate → open when done_when is empty
(ErrTaskNoDoneWhen, mapped across the wire), the same refusal
ParseTaskCapture makes for a capture marker with nothing after it: confirming
work whose finish line nobody wrote is how a board fills with rows that can
never leave it. Dropping such a candidate stays legal, and the /tasks confirm
button now says what is missing instead of surfacing a not-found.

One caller skips the gate. CaptureTask promoting a candidate he stated out
loud would otherwise be denied intake rather than asked for a criterion, and
a direct open capture never carried one either. The gate belongs to the
deliberate promotion on /tasks, where V-511 puts a form.
2026-08-05 20:17:30 +04:00
claude d21b4a65da Merge the board status change and the stall counts (#184) 2026-08-05 19:54:21 +04:00
claude be62660be9 /tasks counts stall shapes, and assesses none of them (V-512)
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
2026-08-05 19:54:21 +04:00