Address #132 review: one verb list, self-reporting tools, rune-safe log
Build image / build-and-push (push) Successful in 8s

- changeClaim is built from one changeVerbs list; the opener is just
  done/fixed/undone so an informational "Updated totals:" can't trip it.
- public_link (get reads) and undo_change (nothing left to revert) are
  self-reporting: their success no longer counts as a change by name; the
  adapter says whether they changed something (noteChange / didChange).
- whenMissing covers the object and plant tools too (move/update/delete
  object, clear/remove plantings by object, update/delete plant).
- The step summary cuts on a rune boundary.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-23 02:49:55 -04:00
co-authored by Claude Fable 5
parent d0ca56b79b
commit d4eb62a2ba
3 changed files with 80 additions and 23 deletions
+32 -7
View File
@@ -277,6 +277,10 @@ type adapter struct {
today string
mu sync.Mutex
// changed is set by the tools whose success does not itself mean a change
// — public_link (get reads) and undo_change (nothing left to revert) — when
// they did change something, for Run's honesty check.
changed bool
// reverts is every change set undo_change produced this turn. A revert is
// its own change set (it points back at the one it undid, and the target is
// marked undone), so it never joins the turn's scope — which leaves a turn
@@ -285,6 +289,20 @@ type adapter struct {
reverts []int64
}
// noteChange records that a self-reporting tool changed something.
func (a *adapter) noteChange() {
a.mu.Lock()
a.changed = true
a.mu.Unlock()
}
// didChange reports whether a self-reporting tool changed something this turn.
func (a *adapter) didChange() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.changed
}
// lastRevert is the newest change set undo_change produced this turn, if any.
func (a *adapter) lastRevert() *int64 {
a.mu.Lock()
@@ -393,8 +411,9 @@ func (a *adapter) moveObject(ctx context.Context, args struct {
YCM float64 `json:"yCm" description:"new center y in garden cm"`
Version int64 `json:"version" description:"the object's current version (from describe_garden)"`
}) (any, error) {
return a.svc.UpdateObject(ctx, a.actor, args.ObjectID,
o, err := a.svc.UpdateObject(ctx, a.actor, args.ObjectID,
service.ObjectPatch{XCM: &args.XCM, YCM: &args.YCM}, args.Version)
return o, whenMissing(err, "object", args.ObjectID, "describe_garden")
}
func (a *adapter) placePlanting(ctx context.Context, args struct {
@@ -510,11 +529,12 @@ func (a *adapter) updatePlant(ctx context.Context, args struct {
Vendor *string `json:"vendor" description:"optional vendor name"`
Notes *string `json:"notes" description:"optional free-text notes"`
}) (any, error) {
return a.svc.UpdatePlant(ctx, a.actor, args.PlantID, service.PlantPatch{
p, err := a.svc.UpdatePlant(ctx, a.actor, args.PlantID, service.PlantPatch{
Name: args.Name, Category: args.Category, SpacingCM: args.SpacingCM, Color: args.Color,
SetDays: args.DaysToMaturity != nil, DaysToMaturity: args.DaysToMaturity,
SourceURL: args.SourceURL, Vendor: args.Vendor, Notes: args.Notes,
}, args.Version)
return p, whenMissing(err, "plant of the user's own", args.PlantID, "find_plant")
}
func (a *adapter) addJournalEntry(ctx context.Context, args struct {
@@ -542,7 +562,7 @@ func (a *adapter) clearObject(ctx context.Context, args struct {
}
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID, service.ClearOptions{RemovedAt: on})
if err != nil {
return nil, err
return nil, whenMissing(err, "object", args.ObjectID, "describe_garden")
}
return map[string]int{"cleared": n}, nil
}
@@ -563,7 +583,7 @@ func (a *adapter) removePlantings(ctx context.Context, args struct {
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID,
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: on})
if err != nil {
return nil, err
return nil, whenMissing(err, "object", args.ObjectID, "describe_garden")
}
return map[string]int{"removed": n}, nil
}
@@ -727,6 +747,7 @@ func (a *adapter) undoChange(ctx context.Context, args struct {
}
a.mu.Lock()
a.reverts = append(a.reverts, cs.ID)
a.changed = true
a.mu.Unlock()
return res, nil
}
@@ -795,17 +816,18 @@ func (a *adapter) updateObject(ctx context.Context, args struct {
RotationDeg *float64 `json:"rotationDeg" description:"optional new rotation in degrees"`
Plantable *bool `json:"plantable" description:"optional: whether the object can hold plants"`
}) (any, error) {
return a.svc.UpdateObject(ctx, a.actor, args.ObjectID, service.ObjectPatch{
o, err := a.svc.UpdateObject(ctx, a.actor, args.ObjectID, service.ObjectPatch{
Name: args.Name, WidthCM: args.WidthCM, HeightCM: args.HeightCM,
RotationDeg: args.RotationDeg, Plantable: args.Plantable,
}, args.Version)
return o, whenMissing(err, "object", args.ObjectID, "describe_garden")
}
func (a *adapter) deleteObject(ctx context.Context, args struct {
ObjectID int64 `json:"objectId" description:"object to delete (with its plantings)"`
}) (any, error) {
if err := a.svc.DeleteObject(ctx, a.actor, args.ObjectID); err != nil {
return nil, err
return nil, whenMissing(err, "object", args.ObjectID, "describe_garden")
}
return map[string]any{"deleted": args.ObjectID}, nil
}
@@ -974,7 +996,7 @@ func (a *adapter) deletePlant(ctx context.Context, args struct {
return nil, fmt.Errorf("%w: the plant is still used — by plantings (past seasons count) or a seed lot — so it stays; tell the user rather than removing those", domain.ErrPlantInUse)
}
if err != nil {
return nil, err
return nil, whenMissing(err, "plant of the user's own", args.PlantID, "find_plant")
}
return map[string]any{"deleted": args.PlantID}, nil
}
@@ -1164,5 +1186,8 @@ func (a *adapter) publicLink(ctx context.Context, args struct {
if err != nil {
return nil, err
}
if action != "get" {
a.noteChange()
}
return a.linkOf(link), nil
}