Pansy v2 tracking epic — grow log & garden assistant #58

Closed
opened 2026-07-21 04:20:55 +00:00 by steve · 2 comments
Owner

Where v2 is going

v1 (#20) built the planner: draw beds, place plops, share gardens. v2 turns it into something you use through a season — it knows where your seeds came from and how many are left, it remembers what you observed in June, and you can talk to it.

The north star, in one sentence: a self-hostable garden planner and assist tool you can hold a conversation with.

Three concrete things that means:

  1. "German Red Garlic" links back to the Johnny's page it came from, and pansy knows how much is left.
  2. You take notes all season, on any bed or plant, and they're still there next year.
  3. You say "change the garlic garden bed to instead be cucumbers this year" and it happens — via majordomo/executus against Ollama Cloud, on a configurable model.

Scope posture: this is a personal tool. Multi-tenant concerns — spend caps, per-user rate limiting, quota — are explicitly not v2 problems. Build for one household and don't pay the complexity tax for a hosting story that doesn't exist yet.

All three land. Verified live 2026-07-21.

Typing "change the garlic bed to cucumbers this year" into the editor clears the bed and replants it as one undoable change set. Seed lots track what's left; the journal remembers the season.

Phases

Phase 0 — Foundations (land before anything is built on top)

Phase 1 — Grow log

Phase 2 — Assistant

Already shipped toward this: #46 garden copy — which doubles as the scenario primitive for #54 (copy last year, edit it, keep both).

Follow-ups found by actually using it

None of these had issues; they were found in one live session after the assistant came up, and are listed here so the epic reflects what really shipped.

  • #72 Undo reported "nothing left to undo" after a successful undo. The revert response carries the change set just inserted, which has no per-op tally — an empty Counts was being read as "nothing happened". Fixed both sides; a null change set is the no-op signal.
  • #73 A client disconnect mid-turn orphaned real changes with no change set — the one guarantee the whole design rests on. commitScope now detaches from cancellation for every caller, including the auto-scope path every REST mutation uses. Plus an SSE keep-alive so a quiet stream isn't cut by a proxy.
  • #74 The undo outcome note hard-coded text-right, fighting the chat's left-aligned layout.

The lesson worth keeping from #72/#73: a passing test can be asserting your assumptions rather than reality (the undo fixture populated Counts, which the real API doesn't), and fixing one path is not fixing the rule — the cancellation fix landed on the failure path, then the success path, then finally in commitScope where it covers everyone.

Decisions already made

Question Decision
Seed inventory shape Purchase lots, not quantity columns — inventory belongs to a purchase, not a variety
Agent autonomy Acts freely, no confirm-first — conditional on undo existing, which is why #48 blocks #56
Undo model Git-like: change sets group revisions; revert is a new change set, so undo is undoable
Chat location Panel in the editor — watching the canvas change is the confirmation
Units fix First and alone
Build tag Dropped. majordomo is a real dependency, resolved from Gitea as a pseudo-version — no replace directive, which would break the Docker build. internal/agent/doc.go rewritten, not left stale
Seasons No seasons tableplanted_at/removed_at already say when things were in the ground
Plant catalog Stays flat — no species→variety hierarchy
Spend caps / rate limiting Not in v2. Personal tool; runs still get a step cap and timeout, but that's loop safety, not cost control
Journal entries in revision history No. They're append-shaped and individually versioned; undoing a note is deleting it
Imperial entry Compound 2' 7" parsing in the existing field (#59), not two fields and not finer decimals
Editor rail One rail, tabs inside it (Inspector / Journal / History / Assistant). Selecting an object switches to Inspector automatically, so the rail is never something you operate before you can edit
History write durability Detached from cancellation, always — by the time a commit runs, the data it describes is already written, so cancelling can only lose the record

OLLAMA_CLOUD_API_KEY and PANSY_AGENT_MODEL (default ollama-cloud/glm-5.2:cloud) are live in Komodo and confirmed working — GET /api/v1/capabilities returns {"agent": true}. Model strings pass verbatim to majordomo.Parse, so a comma-separated spec gives failover for free. Startup logs which condition failed when the assistant is off, since "the key is on the stack but not in the container" looks identical to "no key".

Dependency graph

graph TD
  I47["#47 units ✅"] --> I59["#59 ft-and-in entry ✅"]
  I48["#48 revisions ✅"] --> I49["#49 history UI ✅"]
  I48 --> I56["#56 agent runtime ✅"]
  I50["#50 seed lots ✅"] --> I51["#51 seed shelf UI ✅"]
  I52["#52 journal ✅"] --> I53["#53 journal UI ✅"]
  I54["#54 season view ✅"]
  I55["#55 plant tools ✅"] -.-> I56
  I56 --> I57["#57 chat panel ✅"]
  I49 -.rail layout.-> I53
  I49 -.rail layout.-> I57
  I50 -.lot quantities.-> I55
  I52 -.journal tool.-> I55

Deliberately parked

Post-v2 (revisit only if pansy stops being a personal tool): agent spend caps, per-user rate limiting, quotas — executus/budget is the precedent whenever that day comes.

Other: photos on journal entries (needs a storage decision) · crop-rotation warnings (easy now #54 has landed) · voice input · fractional-inch entry (2' 6 1/2") · companion-planting-aware fills · reverting a garden deletion · automatic inventory decrement · cross-garden variety notes · MCP surface · polygon beds · real-time sync · executus audit/ as a run-level sink (change_sets.agent_run_id is populated and logged, so it's a join, not a retrofit).

Still open

Two things, neither of them code:

  1. #59's mobile keyboard wants checking on an actual phone. Imperial dimension fields are inputMode="text" now, because ' and " are on no numeric keypad. A bare number still means feet so the fast path survives, but it's a real trade and it was decided rather than observed.
  2. The assistant has only been driven by me, in one session. Every acceptance criterion is covered by tests through majordomo's fake provider, and the flagship interaction works live — but a season of real use will find prompt problems a scripted model can't.

Everything tracked above is merged and deployed.

## Where v2 is going v1 ([#20](https://gitea.stevedudenhoeffer.com/steve/pansy/issues/20)) built the planner: draw beds, place plops, share gardens. v2 turns it into something you *use through a season* — it knows where your seeds came from and how many are left, it remembers what you observed in June, and you can talk to it. The north star, in one sentence: **a self-hostable garden planner and assist tool you can hold a conversation with.** Three concrete things that means: 1. "German Red Garlic" links back to the Johnny's page it came from, and pansy knows how much is left. 2. You take notes all season, on any bed or plant, and they're still there next year. 3. You say *"change the garlic garden bed to instead be cucumbers this year"* and it happens — via majordomo/executus against Ollama Cloud, on a configurable model. **Scope posture: this is a personal tool.** Multi-tenant concerns — spend caps, per-user rate limiting, quota — are explicitly not v2 problems. Build for one household and don't pay the complexity tax for a hosting story that doesn't exist yet. > ### ✅ All three land. Verified live 2026-07-21. > Typing *"change the garlic bed to cucumbers this year"* into the editor clears the bed and replants it as **one undoable change set**. Seed lots track what's left; the journal remembers the season. ## Phases **Phase 0 — Foundations** *(land before anything is built on top)* - [x] #47 Units correctness: exact imperial, garden grid at dimension scale, no silently-invisible grid → #60 - [x] #59 Feet-and-inches entry: type `2' 7"` instead of `2.6` → #62 - [x] #48 Revision history: change sets + revisions + revert — **the undo substrate** → #61 - [x] #49 History panel + undo in the editor *(owns the rail-layout decision)* → #63 **Phase 1 — Grow log** - [x] #50 Seed provenance + inventory: source links on plants, `seed_lots` → #64 - [x] #51 Seed shelf UI: source links, lots, and what's left → #68 - [x] #52 Grow journal: time-stamped notes on gardens, beds and plantings → #65 - [x] #53 Journal UI: write and read season notes where you're already looking → #67 - [x] #54 Season view: filter the editor to a year → #66 **Phase 2 — Assistant** - [x] #55 Agent tools: plant lookup + creation *(closed the gap blocking the demo task)* → #69 - [x] #56 Agent runtime: majordomo in-process, Ollama Cloud config, chat endpoint → #70 - [x] #57 Chat panel in the garden editor → #71 Already shipped toward this: **#46** garden copy — which doubles as the scenario primitive for #54 (copy last year, edit it, keep both). ## Follow-ups found by actually using it None of these had issues; they were found in one live session after the assistant came up, and are listed here so the epic reflects what really shipped. - [x] **#72** Undo reported *"nothing left to undo"* after a successful undo. The revert response carries the change set just inserted, which has no per-op tally — an empty `Counts` was being read as "nothing happened". Fixed both sides; a `null` change set is the no-op signal. - [x] **#73** **A client disconnect mid-turn orphaned real changes with no change set** — the one guarantee the whole design rests on. `commitScope` now detaches from cancellation for *every* caller, including the auto-scope path every REST mutation uses. Plus an SSE keep-alive so a quiet stream isn't cut by a proxy. - [x] **#74** The undo outcome note hard-coded `text-right`, fighting the chat's left-aligned layout. **The lesson worth keeping from #72/#73:** a passing test can be asserting your assumptions rather than reality (the undo fixture populated `Counts`, which the real API doesn't), and fixing one path is not fixing the rule — the cancellation fix landed on the failure path, then the success path, then finally in `commitScope` where it covers everyone. ## Decisions already made | Question | Decision | |---|---| | Seed inventory shape | **Purchase lots**, not quantity columns — inventory belongs to a purchase, not a variety | | Agent autonomy | **Acts freely**, no confirm-first — conditional on undo existing, which is why #48 blocks #56 | | Undo model | **Git-like**: change sets group revisions; revert is a new change set, so undo is undoable | | Chat location | **Panel in the editor** — watching the canvas change is the confirmation | | Units fix | **First and alone** | | Build tag | **Dropped.** majordomo is a real dependency, resolved from Gitea as a pseudo-version — **no `replace` directive**, which would break the Docker build. `internal/agent/doc.go` rewritten, not left stale | | Seasons | **No `seasons` table** — `planted_at`/`removed_at` already say when things were in the ground | | Plant catalog | **Stays flat** — no species→variety hierarchy | | Spend caps / rate limiting | **Not in v2.** Personal tool; runs still get a step cap and timeout, but that's loop safety, not cost control | | Journal entries in revision history | **No.** They're append-shaped and individually versioned; undoing a note is deleting it | | Imperial entry | **Compound `2' 7"` parsing** in the existing field (#59), not two fields and not finer decimals | | Editor rail | **One rail, tabs inside it** (Inspector / Journal / History / Assistant). Selecting an object switches to Inspector automatically, so the rail is never something you operate before you can edit | | History write durability | **Detached from cancellation, always** — by the time a commit runs, the data it describes is already written, so cancelling can only lose the record | `OLLAMA_CLOUD_API_KEY` and `PANSY_AGENT_MODEL` (default `ollama-cloud/glm-5.2:cloud`) are live in Komodo and confirmed working — `GET /api/v1/capabilities` returns `{"agent": true}`. Model strings pass verbatim to `majordomo.Parse`, so a comma-separated spec gives failover for free. Startup logs which condition failed when the assistant is off, since "the key is on the stack but not in the container" looks identical to "no key". ## Dependency graph ```mermaid graph TD I47["#47 units ✅"] --> I59["#59 ft-and-in entry ✅"] I48["#48 revisions ✅"] --> I49["#49 history UI ✅"] I48 --> I56["#56 agent runtime ✅"] I50["#50 seed lots ✅"] --> I51["#51 seed shelf UI ✅"] I52["#52 journal ✅"] --> I53["#53 journal UI ✅"] I54["#54 season view ✅"] I55["#55 plant tools ✅"] -.-> I56 I56 --> I57["#57 chat panel ✅"] I49 -.rail layout.-> I53 I49 -.rail layout.-> I57 I50 -.lot quantities.-> I55 I52 -.journal tool.-> I55 ``` ## Deliberately parked **Post-v2** *(revisit only if pansy stops being a personal tool)*: agent spend caps, per-user rate limiting, quotas — `executus/budget` is the precedent whenever that day comes. **Other**: photos on journal entries (needs a storage decision) · crop-rotation warnings (easy now #54 has landed) · voice input · fractional-inch entry (`2' 6 1/2"`) · companion-planting-aware fills · reverting a garden deletion · automatic inventory decrement · cross-garden variety notes · MCP surface · polygon beds · real-time sync · `executus` `audit/` as a run-level sink (`change_sets.agent_run_id` is populated and logged, so it's a join, not a retrofit). ## Still open Two things, neither of them code: 1. **#59's mobile keyboard wants checking on an actual phone.** Imperial dimension fields are `inputMode="text"` now, because `'` and `"` are on no numeric keypad. A bare number still means feet so the fast path survives, but it's a real trade and it was decided rather than observed. 2. **The assistant has only been driven by me, in one session.** Every acceptance criterion is covered by tests through majordomo's fake provider, and the flagship interaction works live — but a season of real use will find prompt problems a scripted model can't. Everything tracked above is merged and deployed.
steve added the epic label 2026-07-21 04:20:55 +00:00
Author
Owner

All twelve issues merged and deployed

Issue PR
Phase 0 #47 units correctness · #59 ft/in entry · #48 revision history · #49 history panel #60 #62 #61 #63
Phase 1 #50 seed lots · #51 seed shelf UI · #52 journal · #53 journal UI · #54 season view #64 #68 #65 #67 #66
Phase 2 #55 agent plant tools · #56 agent runtime · #57 chat panel #69 #70 #71

Every PR was reviewed by Gadfly, findings addressed or declined with reasoning, pipeline green before merge.

⚠️ One thing needs you: the assistant is configured but not reaching the container

GET /api/v1/capabilities on the live instance returns {"agent": false}.

That's the code working correctly — it declines to offer a chat tab that would 404 — but it means Agent.Ready() is false. The binary is definitely current: /gardens/:id/journal/counts, /gardens/:id/years and /seed-lots all answer 401 (route exists, needs auth) from the same build, while the agent routes answer 404 (route not registered).

Ready() needs three things: PANSY_AGENT_ENABLED not explicitly false, a non-empty OLLAMA_CLOUD_API_KEY, and a non-empty PANSY_AGENT_MODEL (which defaults, so it can't be the culprit). The likeliest cause is that the variable is set on the Komodo stack but not passed through to the service's environment: — those are different things, and the symptom is identical to never having set it.

#71 added a startup log line for exactly this, so the container's first few lines now say which condition failed:

api: garden assistant disabled enabled=… hasApiKey=false model=ollama-cloud/glm-5.2:cloud
  hint="needs OLLAMA_CLOUD_API_KEY set in the container's environment (not just the stack's)"

Once the key lands, the Assistant tab appears in the editor rail with no further deploy needed beyond a restart.

Verified live

Against the sandbox copy (garden 2), then cleaned up:

  • Units — entering a 1 ft garden grid stores exactly 30.48. The real Home garden's legacy 3 cm grid now draws, coarsened to every 5th line with the badge grid every 0′ 6″ — previously it was invisible while snapping stayed silently active, which is the bug #47 existed for.
  • Undo — a REST PATCH with nobody opening a change set landed in history via auto-scope; revert restored the position; revert-of-revert reapplied it; both show in the list with correct revertsId/revertedById. Reverting a stale change set answered 409 naming the entity and left the row untouched.
  • Journal, seed lots, season view — entry written and counted, lot recorded reporting 100 remaining / 0 used, ?year=2026 filtering correctly.

What I'd watch next

  • #59's mobile keyboard. Imperial dimension fields are inputMode="text" now, because ' and " aren't on a numeric keypad. Bare numbers still mean feet so the fast path survives, but it wants a real phone.
  • The assistant against a real model. Every acceptance criterion is covered by tests driven through majordomo's fake provider — one change set per turn, viewer refusal, step cap, no-key startup — but a live model will find prompt problems that a scripted one can't.
  • executus is still not a dependency. change_sets.agent_run_id is populated and logged, so wiring its audit/ sink later is a join, not a retrofit.
## All twelve issues merged and deployed | | Issue | PR | |---|---|---| | **Phase 0** | #47 units correctness · #59 ft/in entry · #48 revision history · #49 history panel | #60 #62 #61 #63 | | **Phase 1** | #50 seed lots · #51 seed shelf UI · #52 journal · #53 journal UI · #54 season view | #64 #68 #65 #67 #66 | | **Phase 2** | #55 agent plant tools · #56 agent runtime · #57 chat panel | #69 #70 #71 | Every PR was reviewed by Gadfly, findings addressed or declined with reasoning, pipeline green before merge. ## ⚠️ One thing needs you: the assistant is configured but not reaching the container `GET /api/v1/capabilities` on the live instance returns **`{"agent": false}`**. That's the code working correctly — it declines to offer a chat tab that would 404 — but it means `Agent.Ready()` is false. The binary is definitely current: `/gardens/:id/journal/counts`, `/gardens/:id/years` and `/seed-lots` all answer `401` (route exists, needs auth) from the same build, while the agent routes answer `404` (route not registered). `Ready()` needs three things: `PANSY_AGENT_ENABLED` not explicitly false, a non-empty `OLLAMA_CLOUD_API_KEY`, and a non-empty `PANSY_AGENT_MODEL` (which defaults, so it can't be the culprit). The likeliest cause is that **the variable is set on the Komodo stack but not passed through to the service's `environment:`** — those are different things, and the symptom is identical to never having set it. #71 added a startup log line for exactly this, so the container's first few lines now say which condition failed: ``` api: garden assistant disabled enabled=… hasApiKey=false model=ollama-cloud/glm-5.2:cloud hint="needs OLLAMA_CLOUD_API_KEY set in the container's environment (not just the stack's)" ``` Once the key lands, the Assistant tab appears in the editor rail with no further deploy needed beyond a restart. ## Verified live Against the sandbox copy (garden 2), then cleaned up: - **Units** — entering a 1 ft garden grid stores exactly `30.48`. The real Home garden's legacy 3 cm grid now *draws*, coarsened to every 5th line with the badge `grid every 0′ 6″` — previously it was invisible while snapping stayed silently active, which is the bug #47 existed for. - **Undo** — a REST `PATCH` with nobody opening a change set landed in history via auto-scope; revert restored the position; revert-of-revert reapplied it; both show in the list with correct `revertsId`/`revertedById`. Reverting a stale change set answered **409** naming the entity and left the row untouched. - **Journal, seed lots, season view** — entry written and counted, lot recorded reporting `100 remaining / 0 used`, `?year=2026` filtering correctly. ## What I'd watch next - **#59's mobile keyboard.** Imperial dimension fields are `inputMode="text"` now, because `'` and `"` aren't on a numeric keypad. Bare numbers still mean feet so the fast path survives, but it wants a real phone. - **The assistant against a real model.** Every acceptance criterion is covered by tests driven through majordomo's fake provider — one change set per turn, viewer refusal, step cap, no-key startup — but a live model will find prompt problems that a scripted one can't. - **`executus`** is still not a dependency. `change_sets.agent_run_id` is populated and logged, so wiring its `audit/` sink later is a join, not a retrofit.
Author
Owner

Closing — v2 is complete and was verified live (2026-07-21): "change the garlic bed to cucumbers this year" lands as one undoable change set, seed lots track what's left, the journal remembers the season. Everything tracked above is merged and deployed.

The two remaining items are non-code and don't need this epic held open:

  1. #59's imperial keyboard on a real phone — effectively absorbed: the whole editor got a mobile-first overhaul in #96, and #121 just reclaimed the editor's mobile chrome.
  2. The assistant driven by real season use — that's ongoing use, not a deliverable.

Both are recorded outside the tracker (memory + v3 notes); reopen if either turns into actual work.

Closing — v2 is complete and was verified live (2026-07-21): "change the garlic bed to cucumbers this year" lands as one undoable change set, seed lots track what's left, the journal remembers the season. Everything tracked above is merged and deployed. The two remaining items are non-code and don't need this epic held open: 1. **#59's imperial keyboard on a real phone** — effectively absorbed: the whole editor got a mobile-first overhaul in #96, and #121 just reclaimed the editor's mobile chrome. 2. **The assistant driven by real season use** — that's ongoing use, not a deliverable. Both are recorded outside the tracker (memory + v3 notes); reopen if either turns into actual work.
steve closed this issue 2026-07-23 01:15:31 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#58