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:
"German Red Garlic" links back to the Johnny's page it came from, and pansy knows how much is left.
You take notes all season, on any bed or plant, and they're still there next year.
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)
#47 Units correctness: exact imperial, garden grid at dimension scale, no silently-invisible grid → #60
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.
#73A 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 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".
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 · executusaudit/ 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:
#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.
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
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.
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:
#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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
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.
Phases
Phase 0 — Foundations (land before anything is built on top)
2' 7"instead of2.6→ Feet-and-inches entry: type 2' 7" instead of 2.6 (#59) (#62)Phase 1 — Grow log
seed_lots→ Seed provenance + inventory: source links and seed lots (#50) (#64)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.
Countswas being read as "nothing happened". Fixed both sides; anullchange set is the no-op signal.commitScopenow 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.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 incommitScopewhere it covers everyone.Decisions already made
replacedirective, which would break the Docker build.internal/agent/doc.gorewritten, not left staleseasonstable —planted_at/removed_atalready say when things were in the ground2' 7"parsing in the existing field (#59), not two fields and not finer decimalsOLLAMA_CLOUD_API_KEYandPANSY_AGENT_MODEL(defaultollama-cloud/glm-5.2:cloud) are live in Komodo and confirmed working —GET /api/v1/capabilitiesreturns{"agent": true}. Model strings pass verbatim tomajordomo.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
Deliberately parked
Post-v2 (revisit only if pansy stops being a personal tool): agent spend caps, per-user rate limiting, quotas —
executus/budgetis 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 ·executusaudit/as a run-level sink (change_sets.agent_run_idis populated and logged, so it's a join, not a retrofit).Still open
Two things, neither of them code:
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.Everything tracked above is merged and deployed.
All twelve issues merged and deployed
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/capabilitieson 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/yearsand/seed-lotsall answer401(route exists, needs auth) from the same build, while the agent routes answer404(route not registered).Ready()needs three things:PANSY_AGENT_ENABLEDnot explicitly false, a non-emptyOLLAMA_CLOUD_API_KEY, and a non-emptyPANSY_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'senvironment:— 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:
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:
30.48. The real Home garden's legacy 3 cm grid now draws, coarsened to every 5th line with the badgegrid every 0′ 6″— previously it was invisible while snapping stayed silently active, which is the bug #47 existed for.PATCHwith 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 correctrevertsId/revertedById. Reverting a stale change set answered 409 naming the entity and left the row untouched.100 remaining / 0 used,?year=2026filtering correctly.What I'd watch next
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.executusis still not a dependency.change_sets.agent_run_idis populated and logged, so wiring itsaudit/sink later is a join, not a retrofit.steve referenced this issue2026-07-21 22:10:54 +00:00
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:
Both are recorded outside the tracker (memory + v3 notes); reopen if either turns into actual work.