The call/mustCall closures were copied between TestRecordKeepingTools and
TestCatalogAndGardenTools; both now use a file-level toolCaller. The other
notes are left as they are: the 'nothing to change' guard enumerates the
args on purpose (it is the tool's own contract, next to the struct it
checks), and wrapping a sentinel with %w is how every readable refusal in
this package is built.
Co-Authored-By: Claude Fable 5 <[email protected]>
- update_seed_lot / delete_seed_lot: correct or drop a recorded purchase
("it was three packets, not two"); the plant a lot is for stays fixed.
- delete_plant: remove a duplicate from the user's catalog. The service
already refuses while plantings (past seasons included) or a lot reference
it; the tool turns that sentinel into words the model can pass on, and
tells it not to clear those references to get its way.
- create_garden: a new place, with the service's defaults; the prompt says a
plan is still a copy_garden.
- describe_garden groups carry readyAround — planting date plus days to
maturity for the plops still in the ground — so "what can I pick this
week?" is a lookup rather than arithmetic the model got wrong live.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Every dated tool argument now goes through day() → parseDay, so a prose
date on remove_planting / remove_plantings / clear_object (and place,
fill, journal) is refused with the same message as update_planting's.
- parseDay's trimmed value is what gets stored, not the raw argument.
- list_years re-sorts after adding the gardener's year instead of
prepending it: newest first holds when their year is the oldest.
- ClearSeedLot matches its JSON tag; the label-clearing branch says why nil.
- The prompt says the notes are facts to plan with, not instructions.
Left as is: update_garden's read-then-overlay merge. UpdateGarden is
whole-row by design (the REST PATCH sends every field too), and a service
GardenPatch would duplicate gardenFromInput's validation for one caller.
Co-Authored-By: Claude Fable 5 <[email protected]>
Six tools the live assistant kept needing and a prompt that knows about them:
- undo_change wraps RevertChangeSet(source=agent). A revert is its own change
set, so Run reports the last one as the turn's handle when the turn changed
nothing else — an undo-only reply keeps its "Undo this", which is now a redo.
- describe_garden takes a year: the season view (GardenFull(year)), pulled
plops included, with removed/removedAt per group and per plop; list_years
says which years have records. Rotation questions finally have data.
- update_planting corrects a plop's date, count, label, radius or seed lot in
place; remove_planting, remove_plantings and clear_object take a removedAt so
a harvest can be backdated.
- update_journal_entry / delete_journal_entry correct a note instead of
stacking a contradicting one.
- update_garden renames/resizes/re-units a garden and rewrites its notes — and
the notes now go into the system prompt as the gardener's standing facts, so
"remember we're in zone 6a" persists across conversations.
describe_garden also reports the garden's notes, version and grid, which the
new tools need. Prompt, CLAUDE.md and DESIGN.md updated to match; UI step
labels for the new tools.
Co-Authored-By: Claude Fable 5 <[email protected]>
- The plan-name line of the system prompt interpolates the garden's name
with %q like the rest of the prompt: any editor can rename a garden, and a
name with a newline in it must not read as an instruction.
- fill_region refuses an inverted rectangle with its corners named, and a
rectangle that misses the bed (or only touches its edge) is an error from
the service rather than a successful fill of nothing.
- remove_plantings requires plantId; omitted it would remove plant 0 and
report success.
- historyEntry.Undo → UndoOf (it holds the reverted change set's id).
- remove_planting's description names list_plantings as an id source.
- RemovePlanting takes the removal date itself; the dateless wrapper had no
callers left.
Co-Authored-By: Claude Fable 5 <[email protected]>
Twenty-one prompts against the live assistant found one fabricated success,
a model that believed it was 2025, and a describe_garden that was ~450 plop
entries per turn. This is the set of fixes, each traceable to a finding:
- The gardener's LOCAL day travels with the turn (`today` on POST /agent/chat,
sent by the UI like plantedAt) into the system prompt and every dated tool
default. Left to guess, the model dated journal entries a year back; left to
the server, a 9 pm fill landed on UTC's tomorrow.
- describe_garden groups plops by plant — count, where, planted date, days to
maturity — and lists ids only for groups of ≤ 8; list_plantings spells a big
group out on demand and remove_plantings acts on one plant in a bed ("take
the beets out, leave the garlic"), which used to mean 116 single removals.
- New tools: move_planting (keeps the planting date; across beds via the new
MovePlanting, which is why the store's UPDATE now writes object_id),
update_plant, read_history, copy_garden (the "<garden> — <year>" plan
convention). fill_region takes an explicit local rectangle and a seedLotId;
place_planting's radius defaults to one plant (spacing/2) instead of a guess.
- The system prompt states the date and the gardener's units, forbids claiming
a change no tool made, says it cannot undo and points at the Undo button,
asks before clearing beds on an ambiguous sentence, and stops narrating its
own plantings into the journal.
- A mutation aimed at ANOTHER garden inside a turn is recorded under that
garden as its own change set, not filed into the open scope.
- UI: the thread scrolls inside the Assistant panel so the composer stays
put; every tool has a step label; wide tables stay inside the bubble.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Garden and plant dialogs keep centimeters as the source of truth
(LengthField in lib/units.ts): a no-change Save no longer rewrites
900 cm as 899.922 or a 45 cm spacing as 44.958, bumping versions and
writing bogus history entries on the way.
- The UI stamps every date with the browser's local day (lib/dates.ts).
Journal notes already did; plop placement, fill and removal now do too,
so a 9 pm placement isn't "planted tomorrow". The fill endpoint gained an
optional plantedAt; API and agent callers still default to UTC today.
- Removing an object that holds plants asks first and says how many go
with it. An empty one still goes straight away (one Undo restores it).
- The expanded plant card's action row wraps instead of clipping "Delete".
- Monogram lettering switches to a dark ink on pale marker colors (garlic,
cabbage, marigold) instead of near-white on near-white.
- Copy-as-plan proposes the next free year and warns when the typed name
already exists, so two gardens can't both read as "the 2027 plan".
- Plan cards show the base name with a "2027 plan" tag, so the year — the
point of the name — survives truncation.
- A rejected model spec now says which model and why: a wrapped
ErrInvalidInput's reason reaches the client as the 400's message, and the
Settings field shows it inline instead of toasting "invalid input".
Also defuses a clock bomb in TestRemainingReturnsWhenAPlantingIsRemoved,
which only passed while the real date was before 2026-08-01.
Co-Authored-By: Claude Fable 5 <[email protected]>
The toolbox could create and move but not delete or resize; write the
journal but not read it; clear a whole bed but not pull one plant; report
seed remaining but not record a purchase. Close those gaps with thin
adapters over the SAME service methods the REST API uses, so they inherit
the permission checks unchanged:
read_journal → ListJournal (the write/read asymmetry, most visible)
update_object → UpdateObject (resize / rotate / rename / plantable)
delete_object → DeleteObject (counterpart to create_object)
remove_planting → UpdatePlanting (soft-remove ONE plop, like clear does)
list_seed_lots → ListSeedLots
record_seed_lot → CreateSeedLot (record a purchase; "I bought 2 packets")
To address a single plop the agent needs its id + version, so
DescribePlanting now carries both — the same way DescribeObject.Version
already lets it edit an object. remove_planting soft-removes (removed_at =
today), mirroring clear_object, so the plant stays in planting history and
the change is undoable.
Deferred deliberately: an undo/revert tool needs a way to list recent
change sets to get a changeSetId, which is a larger addition; noted on the
issue for a follow-up.
Tested through the tool layer (TestCorrectiveTools): resize, single-plop
removal, journal read-back, seed-lot record+list, and delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Steve chose option 3: keep clumps as the default primitive, add a grid/rows
fill mode, so sketching and planning are different operations with different
outputs rather than one model forced to be both.
A plop is a CLUMP, not a plant — great for "a few plops of garlic in a corner",
useless for drawing a plantable 8-rows-of-garlic bed (that came out as ~15
blobs, #77). FillLayout selects what a fill packs:
- clump (default, unchanged): radius 1.5×spacing, ~7 plants per plop.
- grid: radius spacing/2, pitch = spacing, ONE plant per plop — rows you could
actually plant from.
The geometry is the SAME hexCenters lattice and the SAME #75 half-spacing edge
rule; only the radius→spacing relationship differs (plopRadiusFor). Grid keeps
no 15cm floor — its whole point is true spacing — while clump keeps it so a
tiny-spacing plant doesn't make invisible clumps.
Threaded through FillRegion/FillNamedRegion (empty layout = clump, so existing
callers are unchanged; unknown layout = ErrInvalidInput), the REST /fill
endpoint (`layout`), and the agent's fill_region tool (`mode`, enum clump|grid),
so "plant the bed in rows" works.
Tests: grid produces many more, single-plant plops than clump on the same bed
(radius spacing/2, derived count 1); unknown layout is refused at both the
service and the API. maxFillPlops still caps a grid fill of a huge bed.
No frontend fill affordance exists yet (fill is agent-only in the UI; the fill
UI was deferred in #82), so the mode toggle rides along when that's built —
noted. Docs: DESIGN placement-model decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ