Commit Graph

20 Commits

Author SHA1 Message Date
claude 0e82cb442f the unplaceable word rides a typed error, not the message (V-634)
Recovering it by cutting on quotes in err.Error() meant the reply depended on
the wording of an error string. UnknownTargetError carries the word and
errors.Is still holds.
2026-08-06 20:06:25 +04:00
claude d94ed2e630 an act with a target the system cannot have does not run (V-634)
V-633 gave tools spoken aliases, so a Russian act reaches a tool. It resolves
the verb only: the rest of the sentence became argv. "перезагрузи роутер" ran
as systemctl restart роутер, which is a real tool, a real word and a target
that cannot exist on this box. She then reported systemctl's own confusion as
if she had tried something sensible, and on a destructive row she spent a
confirm turn on it first.

The executor now refuses, ahead of the confirm gate, and names the word it
could not place. The check is the script and not a word list: a unit, a
container, a host and a path are ASCII here, so a Cyrillic argv element means
the alias match swallowed the verb and handed on the next word.

Process rows only. An MCP argument is not a target — a task title is Russian
and always was — and a house row drops the spoken args already.

It does not try to guess the right target. Identity is Nexus's, and a target
Nexus resolves reaches Hexis through handleHexisAct before this executor is
asked.
2026-08-06 20:05:34 +04:00
claude e7ecce2859 a Russian act reaches a tool, and the seeds stop disagreeing (V-633)
Three tangled defects, fixed together because each one hid the others.

DefaultActMatcher matched an exact English prefix and internal/tool.Matcher
delegated straight to it, so no Russian utterance could reach a tool: 55 of the
69 lines in models/seeds/act.txt routed to IntentAct and fell to proposeGap.
Tools now carry spoken aliases from deploy/mavend.json, matched as exact leading
tokens, longest phrase first. Config data, not a stem pattern in code. The
comment claiming "the production matcher is fuzzy" was false and is gone.

Seven lines were exact duplicates inside models/seeds/query.txt, each one a
second identical vector double-weighting its region.

"как дела у сервера" carried both a query and a system label. It leaves
system.txt, because replySystem's stats arm answers "системная статистика пока
не подключена." and always did. The mode inventory records that shape as
act.tool.hoststats rather than a system mode.

Fixture unchanged at 69/91, and it cannot see any of this: no host-stat case and
no Russian act in it. TestActMatcherAliases is the coverage.
docs/evals/2026-08-06-russian-acts-reach-tools.md has the numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
2026-08-06 18:09:11 +04:00
claude f4a021d3da a tool row with no cmd no longer execs the utterance (V-581)
An enabled row whose Cmd is empty built argv from the spoken args alone. So
args[0] became the program name, and free text picked the binary. A proposal is
drafted with no cmd. /tools can enable one before anybody fills it in, so
reaching this took no compromise. Exec now refuses such a row with ErrNotEnabled
before it builds argv. TestExecEmptyCmdRefuses pins it.

Three smaller reads in the same sweep. CapabilityOf parsed a Home Assistant
entity id by hand where smarthome.DomainOf already does it. The fallback for an
id with no dot is unchanged. GroupByDomain built its map key twice per row. The
zenmoney and Home Assistant HTTP clients read an error body before checking the
status that discards it. The status check moved ahead of the read in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:11:53 +04:00
claude bee3ef80b4 Merge pull request 'Capability model: homelab.docker.restart instead of flat tool-to-enabled' (#146) from task/452-capability-model-homelab-docker-restart into master 2026-08-04 18:28:17 +02:00
claude 0ade0ec734 tool, mavend: Hexis owns the tier of a Hexis capability (V-523)
read_only was the whole decision on the Hexis act path, which flattened three
answers into two. A capability that wipes the thing it names got the same
single spoken "да" as one that restarts a service, and requires_confirmation —
which the Hexis contract calls server-derived and never settable by a caller —
was read by nobody. docs/ecosystem.md §17.3 says confirmation follows risk.

RiskOfCapability reads Hexis's risk, read_only and requires_confirmation and
returns one of the three tiers internal/tool already had. It takes plain values
rather than a Capability, so internal/tool keeps no dependency on the Hexis
client. RiskOf keeps deriving, because a shell row the owner ticked on /tools
has no upstream to ask.

Every disagreement between the three fields goes up, never down: safe and
mutating is a contradiction and takes the confirm, an unrecognised tier takes
the confirm, and requires_confirmation may only raise. Same default as an
unrecognised dispatch shape — argue your way down, never up.

The irreversible refusal was a Go literal in two places and is now one deck
entry, act_needs_authed_surface. It lost four words to the persona ceiling.
2026-08-04 18:01:28 +04:00
claude 8a21478f36 mavweb: group the allowlist by capability domain (V-452) 2026-08-04 04:55:13 +04:00
claude 958d2a2fc8 tool: read a row as a dotted capability id (V-452)
scope.domain.action, the shape Hexis has always spoken, derived from the row
rather than stored — a derivation is one place to argue with, a column is
whatever the last person to enable the tool typed. The name stays the primary
key and nothing about lookup or execution changes: this is a way to read the
allowlist, not a second allowlist.

MatchCapability widens one way, so house.lock covers every action on the
locks and nothing narrower can claim a wider pattern.
2026-08-04 04:55:13 +04:00
claude 7db139b83e tool, mavend: cover the tiers end to end (V-449) 2026-08-04 04:50:56 +04:00
claude 0987dabfc4 tool: risk tiers decide the confirm, not one boolean (V-449)
The Destructive column was a mechanism with no policy behind it: nothing said
which acts are destructive, whether a confirmed act stays confirmed, or what a
new tool domain inherits, so each domain answered for itself.

Three tiers, derived from the row rather than stored, so the answer can be
argued with in one place instead of being whatever the last person to tick the
checkbox believed. Safe runs. Destructive costs a confirm turn, every time —
a confirmation binds one capability, one target and one argument list, and it
dies with the parked turn. Irreversible is refused: a confirm turn there would
be theatre, because the STT, the router and the fuzzy allowlist match are all
guesses and a spoken "да" checks none of them. She names the gap; the row
stays enabled.

An unrecognised dispatch shape inherits destructive, not safe. A domain argues
its way down to running freely, never up to being gated.
2026-08-04 04:50:56 +04:00
claude 93987f2dfc docs: tier the tree by lifetime, so staleness shows in the path (V-446)
Seventeen markdown files at the repo root, twelve of them dated one-shot
reports sitting next to CLAUDE.md. That is why stale docs read as
current: nothing in the path said which was which.

Root now keeps CLAUDE.md and AGENTS.md. Living docs move under docs/
and carry a Last verified line. Dated measurements move to docs/evals/
ISO-prefixed, and are never edited after the day, so a newer number is
a new file. The senior review moves to docs/archive/.

Every reference was rewritten across markdown, Go comments, the Makefile
and the recall fixture. The touched Go packages still build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 03:28:49 +04:00
kami 76a251a20d Merge branch 'fix/g08' into fix/integrated
# Conflicts:
#	internal/store/migrations.go
2026-08-01 14:38:39 +04:00
kami 52f56947bb tool: separate a tool that is off from a backend that is down
An enabled MCP or smarthome row with no backend returned ErrNotEnabled,
and actionAct reads ErrNotEnabled as "this is unknown, draft a
proposal". So a tool Kami had already approved, whose server happened to
be restarting, produced a second proposal row and an answer saying the
tool needs approval. The right answer is that the server is down.
ErrNotConnected carries that, and the act path maps it, ErrNoServer and
ErrToolGone to replies that say which of the three happened.

Found in review of #71.
2026-08-01 14:11:57 +04:00
kami d62ba093f5 smarthome: keep the devices under the cap, and stop asserting what the house did not do
States sorted every entity by id and cut at MaxEntities. Entity ids sort
by domain prefix, so binary_sensor came first and forty slots went to
connectivity and update-available rows: propose found nothing
controllable, and homeSummary, reading the same list, said everything
was off with the lights on. The cap stays, because a tool name the 1.7B
half-remembers is a wrong act. What changes is which forty. Controllable
domains are taken first and round-robin, so every switch and light is in
before any sensor.

CallService reported done for a call that changed nothing. Home
Assistant answers a service call with the states it changed, and a
removed entity or an offline integration gets 200 and an empty array.
That is the one place Maven asserts something about the physical world,
so an empty array is now ErrUnknownEntity.

The confirm turn on a house row was a column, not an invariant. The
proposal is destructive, but /tools writes the checkbox through on
enable, so unticking it once made an unlock row that ran on first
hearing. Exec now demands the second turn for any smarthome row whatever
the column says, and lock is out of the default domain set so a bare
block does not propose an unlock for every door.

wireSmartHome enumerated the house synchronously, inside wireVoice,
before the socket was serving and inside the unlock handler. A box that
black-holes the connection held the daemon's start for the per-call
timeout. The first propose moved onto the ticker goroutine.

Smaller: an unreachable lamp is counted apart from an off one, a
truncated on-list says how many it left out, refresh has a floor of a
minute, and the http url is documented as a deliberate wg-only choice.
Found in review of #80.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
2026-08-01 14:06:38 +04:00
kami dc4c5b7841 Read and control the house through Home Assistant (#256)
A `smarthome` block points Maven at a Home Assistant instance. She reads its
entity states to answer "что включено дома?", and every controllable device
becomes a PROPOSED row in the existing act allowlist — cmd
["smarthome",<entity_id>,<service>], scope smarthome:<domain> — so nothing new
had to be invented for the mutating half. ProposeTool/EnableTool/DisableTool,
tool.Matcher and the confirm turn are untouched; one branch in Executor.Exec
routes such a row to the client instead of exec, and "smarthome" is never run as
a binary. This is the same trick overnight/mcp-tools used for #251, on purpose.

Discovery only ever PROPOSES, and every control row is destructive=true: there
is no read-only way to turn the heating off, so flipping something in his flat
always costs a confirm turn and always had to be enabled by hand on /tools,
behind step-up.

The entity and the service come from the row he enabled, never from the
utterance — Exec drops the spoken tail for a house row. A router that misheard
can pick the wrong lamp; it cannot compose a target of its own. The service is
checked against the domain's table on the way out too, so a hand-edited cmd
column cannot reach an arbitrary Home Assistant service. set_brightness and
set_temperature are deliberately absent: a spoken number the router got wrong is
a wrong act on real hardware, and on/off is the whole of what a voice turn can
defend.

The read side is a query source ("home", before calendar and the recall passes)
so "что нового дома?" is not answered from an old note. Its matcher needs a
house marker plus an ask plus a device word and bails out on weather wording,
because "какая температура на улице?" belongs to the weather source.

Off unless configured: the block is dark without "enabled": true, and
applyDefaults normalises a disabled block to nil so "off" stays in one place.
deploy/mavend.json carries it disabled, with the token as ${HA_TOKEN}.

NOT shipped, and not faked: MQTT / Zigbee2MQTT (plan steps 2 and 5) and the
sensor-to-fact and presence-probe pipelines. There is no broker and no Home
Assistant anywhere on this network — 8123 and 1883 are closed on every host in
192.168.1.0/24 — the module tree is vendored so a paho dependency cannot be
added offline, and Home Assistant already fronts Zigbee2MQTT where it exists.
Writing a sensor pipeline with no sensor to test it against would be a guess.

Vikunja #256
2026-08-01 06:27:39 +04:00
kami 8d5e357b57 Expose discovered MCP tools through the act allowlist (#251)
Second half of the MCP client: the tools the manager discovers become rows in
the existing act allowlist instead of a parallel capability system.

An MCP tool is encoded in the columns that already exist — cmd
["mcp",<server>,<tool>], scope mcp:<server> — so no migration, and
ProposeTool/EnableTool/DisableTool, tool.Matcher and the confirm turn need no
changes. One branch in Executor.Exec routes such a row to the manager instead
of exec, and "mcp" is never run as a binary.

Discovery only ever PROPOSES. destructive comes from the inverse of the MCP
readOnlyHint, so a tool that does not promise to be read-only inherits the
confirm turn, and enabling stays on /tools behind step-up.

Voice args are positional and MCP args are named, so CallPositional binds only
what it can defend: no required properties runs bare, and a read-only tool with
exactly one required string or number gets the tail. Everything else refuses
with ErrNeedsArgs rather than guessing. The read-only condition was learned
against the live Vikunja server: update_task requires only task_id and takes
the rest as optional, so one guessed argument blanked the fields it did not
mention. A partially-filled write destroys what it omits, so a mutating tool
never receives a guessed argument.

Also: a read-only mcp_servers IPC method and an "MCP servers" card on /tools
showing transport, target and state, with the trust level of a local target
spelled out. There is deliberately no call-a-tool IPC method and no run button,
so mutation keeps exactly one path.

Vikunja #251
2026-08-01 04:36:40 +04:00
kami e0d0244fa9 Fold SPEC/maven/ROADMAP into DESIGN.md and drop the stale session logs
15 root markdown files, ~4,900 lines against ~33,000 lines of Go, with at least
three pairs contradicting each other. When five documents describe the
architecture, the code becomes the only trustworthy one — which defeats the
point of having them. That drift is why the resident-model question had four
incompatible answers.

SPEC.md, maven.md and ROADMAP.md are deduped into DESIGN.md rather than
concatenated, with a "Superseded" section carrying eight retired decisions and
what replaced each: classifier-owns-the-route (the cascade is still the live
path, but as a stopgap, not a design to extend), faster-whisper/vosk/silero,
the small-model phrasing claim, sqlcipher, the Kotlin/Spring sketches,
obsidian->chroma, script deployment, and FloorEnrollment. Superseded material
is kept and marked rather than deleted, so it cannot read as current.

SESSION-05/06-07-2026.md and PLANS.md are removed outright — git history holds
them, and both were verified tracked before deletion.

Go doc comments citing the deleted files are repointed to the equivalent
DESIGN.md sections. Several asserted designs that were already retired, so the
claims are corrected and not just relinked: stt.go named faster-whisper as
production (it is whisper.cpp), tts.go named silero (it is piper), intent.go
still described the classifier as owning the route, and stale vosk/chroma
vocabulary is replaced. ECOSYSTEM-SPEC.md references are deliberately
untouched — that is a different document, and a naive grep for SPEC.md matches
it.

Root markdown drops from 4,880 to ~3,700 lines. The review's ~1,500 target is
not reachable while keeping the files it also said to keep — those alone are
2,553 lines — so trimming further needs a separate decision on
MAVEN_ECOSYSTEM_ARCHITECTURE.md and PROGRESS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:39:56 +04:00
kami 6b80fd0c0f tools: add scope column for capability model
Add a 'scope' TEXT column (default 'homelab') to the tools table so tools
can be namespaced by scope (e.g. "homelab:restart", "datacenter:reboot").
Backward-compat: bare name defaults to "homelab" scope.

Changes:
- Migration #1: ALTER TABLE tools ADD COLUMN scope
- store.Tool: add Scope field, update all SQL and scanTool()
- ipc.Tool DTO and request types: add Scope field
- CoreAPI interface: pass scope in ProposeTool/EnableTool
- storeAPI adapters: forward scope
- cmd/mavend/voice: pass scope (empty → homelab)
- cmd/mavweb/tools: show scope column in UI tables, hidden fields
- All tests updated for scope field
- Migration test made dynamic (startVer = len(migrations))
2026-07-05 11:40:15 +04:00
kami 359ae81d1f piper: pipe cleanup on early return, write error; misc error hygiene
piper_handler: close stdin/stdout pipes on Start() failure and on
WriteString error instead of leaking fds. Propagate WriteString error.

worker/client: log SetDeadline errors instead of discarding them.

voice/session: pushAudio marshals params inline and returns the marshal
error instead of swallowing it via mustParams (removed).

tool/matcher: log ListTools errors instead of silently returning an
empty allowlist that refuses every act.

config: applyDefaults now sets RouterThreshold and ToolTimeout defaults
so consumers self-contained defaults are belt-and-suspenders.
2026-07-03 12:11:24 +02:00
kami 612583d59a initial commit 2026-07-03 00:32:48 +02:00