samagotchi 0.147.0 → 0.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +2 -2
  4. data/docs/architecture.md +1 -1
  5. data/docs/bundles.md +64 -17
  6. data/docs/cli.md +18 -0
  7. data/docs/configuration.md +7 -3
  8. data/docs/guardrails.md +27 -5
  9. data/docs/hooks.md +2 -1
  10. data/docs/internals/objectives.md +264 -0
  11. data/docs/internals/web-frontend.md +4 -1
  12. data/docs/plugins.md +4 -2
  13. data/docs/releasing.md +1 -1
  14. data/docs/sessions.md +44 -2
  15. data/docs/system-one.md +18 -2
  16. data/lib/samagotchi/answer_tail.rb +1 -1
  17. data/lib/samagotchi/bundles/coordinator/guardrails/merge.yml +21 -0
  18. data/lib/samagotchi/bundles/coordinator/manifest.yml +9 -5
  19. data/lib/samagotchi/bundles/coordinator/plugin.rb +5 -3
  20. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +44 -13
  21. data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
  22. data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
  23. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  24. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  25. data/lib/samagotchi/config.rb +12 -1
  26. data/lib/samagotchi/context_note.rb +17 -5
  27. data/lib/samagotchi/engine.rb +113 -6
  28. data/lib/samagotchi/engine_peers.rb +11 -0
  29. data/lib/samagotchi/guardrail_wiring.rb +11 -6
  30. data/lib/samagotchi/guardrails/approvals.rb +3 -1
  31. data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
  32. data/lib/samagotchi/guardrails/rules.rb +6 -1
  33. data/lib/samagotchi/guardrails/verdict.rb +21 -1
  34. data/lib/samagotchi/guardrails_command.rb +47 -3
  35. data/lib/samagotchi/llm/chat_loop.rb +1 -1
  36. data/lib/samagotchi/note_command.rb +1 -1
  37. data/lib/samagotchi/note_delivery.rb +6 -2
  38. data/lib/samagotchi/objectives.rb +395 -0
  39. data/lib/samagotchi/objectives_audit.rb +115 -0
  40. data/lib/samagotchi/objectives_check.rb +335 -0
  41. data/lib/samagotchi/objectives_command.rb +192 -0
  42. data/lib/samagotchi/plugin/api.rb +2 -1
  43. data/lib/samagotchi/plugin/context.rb +21 -1
  44. data/lib/samagotchi/run_options.rb +1 -0
  45. data/lib/samagotchi/send_command.rb +2 -1
  46. data/lib/samagotchi/session_chain.rb +91 -8
  47. data/lib/samagotchi/session_commands.rb +16 -1
  48. data/lib/samagotchi/session_continue.rb +6 -2
  49. data/lib/samagotchi/session_inbox.rb +14 -6
  50. data/lib/samagotchi/session_manager.rb +1 -1
  51. data/lib/samagotchi/steer.rb +24 -4
  52. data/lib/samagotchi/system_one/record.rb +17 -3
  53. data/lib/samagotchi/testing/boundary.rb +1 -0
  54. data/lib/samagotchi/testing/host.rb +1 -0
  55. data/lib/samagotchi/tool_activity.rb +17 -0
  56. data/lib/samagotchi/tool_declarations.rb +34 -0
  57. data/lib/samagotchi/tools/builtin_calls.rb +2 -1
  58. data/lib/samagotchi/tools/builtins.rb +4 -1
  59. data/lib/samagotchi/tools/objectives_tool.rb +75 -0
  60. data/lib/samagotchi/tools/peers.rb +3 -0
  61. data/lib/samagotchi/version.rb +1 -1
  62. data/lib/samagotchi/web/message_display.rb +9 -0
  63. data/lib/samagotchi/web/participant_view.rb +1 -1
  64. data/lib/samagotchi/web/public/app.js +10 -0
  65. data/lib/samagotchi/web/public/cards_place.js +20 -4
  66. data/lib/samagotchi/web/public/commands.js +4 -1
  67. data/lib/samagotchi/web/public/index.html +2 -0
  68. data/lib/samagotchi/web/public/objectives.css +80 -0
  69. data/lib/samagotchi/web/public/objectives_entry.js +22 -0
  70. data/lib/samagotchi/web/public/objectives_model.js +100 -0
  71. data/lib/samagotchi/web/public/objectives_moves.js +145 -0
  72. data/lib/samagotchi/web/public/objectives_strip.js +242 -0
  73. data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
  74. data/lib/samagotchi/web/public/stream_handlers.js +6 -1
  75. data/lib/samagotchi/web/public/turn_html.js +4 -1
  76. data/lib/samagotchi/web/session_show.rb +12 -1
  77. data/lib/samagotchi/worker.rb +3 -0
  78. data/lib/samagotchi/worker_inbound.rb +16 -8
  79. metadata +16 -1
@@ -0,0 +1,264 @@
1
+ # Session objectives
2
+
3
+ A session's objectives are a short list the user types: what this session is to achieve. The first version is a
4
+ try-out, so each piece can come out alone. There is no new message kind or store: the list is a note in the
5
+ conversation and the command writes a note file. Terminals show the default note bubble; the web draws it
6
+ ([The page](#the-page)). For the user's side, see
7
+ [sessions.md](../sessions.md#objectives) and [cli.md](../cli.md).
8
+
9
+ ## The object and the entry
10
+
11
+ `Objectives::List` holds `Objectives::Item`s (`id`, `text`, `status` active|done|dropped, optional `done_when`) and the
12
+ number the next id takes. That number only grows, so an id is never reused, not after a drop or a `set`. `done_when`
13
+ is a hint, never enforced.
14
+
15
+ Each edit is a whole new list in one note entry:
16
+
17
+ ```ruby
18
+ ContextNote.message(note_id:, text: list.text, source: "objectives", header: ContextNote::OBJECTIVES_HEADER,
19
+ objectives: list.to_payload)
20
+ ```
21
+
22
+ - `objectives` is one key in `ContextNote::KEYS`, so everything that copies a note field by field keeps it
23
+ (`ChatLoop#plain`). A note is also what `take_initial_prompt` and `TurnFlow#restore` already handle, which is why
24
+ this isn't a new kind.
25
+ - The payload is `{"next" => 3, "items" => [{"id", "text", "status", "done_when"}]}`, in string keys before a save and
26
+ after one (`List#to_payload`). `Objectives.from_payload` reads either style.
27
+ - It is never sent. The wire gets role and content only, and the frame instructs by itself:
28
+
29
+ ```
30
+ [OBJECTIVES, set by the user. What the user wants this session to achieve; replaces any earlier list. Keep work on it. Say when something falls outside it. Don't change it yourself.]
31
+ O1 [done] Two coffees, open the cart
32
+ O2 Stops before checkout (done when: no order is placed)
33
+ [END NOTE]
34
+ ```
35
+
36
+ A dropped item is left out of the text, and a list with nothing left says `(no objectives now)` so that it still
37
+ replaces the one before it. The frame has no clock, and there are no system prompt lines about it, so a session with
38
+ no list adds no note and no prompt line. It still carries the tool's bytes ([Moves](#moves)).
39
+
40
+ A note body isn't escaped when it is saved, and a system-role note reaches the model as it stands, so outside text
41
+ could carry a line that looks exactly like this frame. On the wire, `Steer.wire_text` leads such a line with `(quoted) `
42
+ (`Steer::FORGED_OBJECTIVES`). That covers every entry with a kind, on both loops: a context note, a peer's `send_note`,
43
+ `chi note`, a delegate report, a steer. A participant's message gets the wider `FORGED_OPENINGS`. The real note is
44
+ known by its `objectives` payload key, never by its text. The saved content stays raw, the rewrite is deterministic,
45
+ and text with no such line goes out byte for byte, so the prompt cache holds. The owner's typed prompt has no kind and
46
+ isn't touched. Tool output isn't covered: it goes out as a tool result, not as a system message.
47
+
48
+ A payload whose `next` isn't an Integer or whose `items` isn't an Array is not a list (`from_payload` answers nil), so
49
+ it can't wipe the fold or restart the ids.
50
+
51
+ ## The current list
52
+
53
+ `Objectives.build(conversation, pending:)` is a pure fold over the entries, shaped like `WorkingSet.build`: the newest
54
+ list note wins, and a pending payload wins over that, because a file not yet absorbed is newer than anything in the
55
+ conversation. `Objectives.pending_payload(session_dir)` finds the newest objectives file among the notes;
56
+ `Objectives.current` is the two together.
57
+
58
+ `build` also fills `List#moves`, the latest `Objectives::Move` per id ([Moves](#moves)). Moves are never in the frame or
59
+ the payload, so an edit and a continue carry none: a new link starts with no last move.
60
+
61
+ ## Editing
62
+
63
+ `/objectives` (`ObjectivesCommand`, registered `anytime`) reads `Objectives.current`. An edit goes out through
64
+ `NoteDelivery.deliver(objectives:)`, which means:
65
+
66
+ - One edit's read and write are one step (`Objectives.with_lock`, a file lock in the session's folder, across threads
67
+ and processes), and the worker's absorb holds it from its claim to the files' delete, so no edit reads between them.
68
+ Each note holds a whole list: without this, two edits that read the same list lost one.
69
+ - `done` refuses a dropped item, and `undo` puts an item back to active.
70
+ - A part with no text of its own refuses the line. An empty done-when is none, and of two done-when groups the last
71
+ counts. A leading `[done]`, `[dropped]` or `[active]` is stripped, so a text can't pass for a status. Any whitespace
72
+ collapses, Unicode line separators too, so each item stays one line of the frame.
73
+ - It never writes session.json and runs on any thread: it only adds a file to `notes/`.
74
+ - It is refused in a session a chi REPL owns (`--no-shared`), with the reason `chi note` gives
75
+ (`NoteDelivery::REPL_REFUSAL`). A show still answers there.
76
+ - A second edit stacks on an unabsorbed first, since the current list reads the pending file.
77
+ - `WorkerInbound#absorb_notes` adds only the newest list in a batch and deletes the others, so edits made during a
78
+ turn cost one entry. It runs between turns: an idle edit lands at the worker's next pass (the fallback tick, or just
79
+ before the next turn), and a mid-turn edit waits for the turn's end. A long coordinator turn is the known gap.
80
+
81
+ The attached TUI and the web composer send the line like any session command (the registry's listing tells them it
82
+ runs mid-turn too). So does `chi send -m "/objectives …"`. A participant's command is refused as any other is.
83
+
84
+ ## Moves
85
+
86
+ The `objectives` tool (`Tools::ObjectivesTool`, schema last in `TOOL_SCHEMAS`) has one action, `move`: `id`, `note`,
87
+ optional `evidence`. A call with no `action` is a move too. It is information only: nothing is written, and the call in
88
+ history is the record. A handler can't append an entry mid-turn safely, and a system entry would split a call from its
89
+ response, so there is no new entry kind. `Objectives.moves_in` reads the calls back with
90
+ `LLMContextStale.paired_runs`, as `WorkingSet` does: both hosts' forms, a `call_tool` of it as it ran. A run whose output
91
+ counts only if its output is the tool's answer that it recorded that id (`ObjectivesTool.recorded?`): a call with no list,
92
+ a refusal, a denial or an interrupt recorded nothing. A rollback that drops the turn drops its moves.
93
+
94
+ Mid-turn, `session.messages` doesn't hold the running turn. `/objectives` adds the loop's own conversation
95
+ (`Engine#live_conversation`, a `dup` read on the command's thread), because `Engine#plugin_messages`' running part (the
96
+ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows once its result is in.
97
+
98
+ - **The answer** says it recorded a move, that the list is unchanged and that the user edits it with `/objectives`; so
99
+ does the schema's description. Other actions get an `Error:` with the same sentence. A small model told to edit the
100
+ list has nothing to call, and this is the only place it is told so: no stable-prompt line.
101
+ - **No list**: `There are no objectives here…`, not an error, for a delegate (its fold is empty), a session with no list
102
+ and a kernel without a session (`Tools::Peers#objectives` is nil; `Engine::PeerView#objectives` reads the session).
103
+ - **Refused**: an unknown id, a dropped objective, an empty note. A `done` objective takes a move.
104
+ - **Declared**: on `full` (`all_core`) and on the coordinator kit (its bundle adds it); named in the not-loaded line
105
+ elsewhere, and so counted in `find_tools`' chi line, and among the deferred tools on a host that loads tools.
106
+ - **Cost**: one extra generation when the move is the step's only call, none when it comes in a batch with another call.
107
+ - **Guardrails**: no rule names it, so it passes with no ask, in `strict` too. `ReviewBoundary` denies it as anything
108
+ outside its kit. `PairBoundary` denies it as owner-voiced: a move stays in the owner's history and reads as the
109
+ session's own.
110
+ - **The row**: `recording a move` in the TUI's line with `id=`, `note=`, `evidence=`; the web's row is `objectives O2 note · evidence`
111
+ (`ToolActivity#tool_title`). No UI of its own.
112
+
113
+ `serves` on `ask_user_question` waits until moves read well. The model can't edit the list.
114
+
115
+ ## Proposals
116
+
117
+ `action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
118
+ (`Objectives::Proposal#refusal`) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
119
+ as `#context_choice`'s cards are kept: shown in `run_turn`'s ensure, it is a between-turns card (no `in_turn`), so it
120
+ writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a restart (`CardStore` takes
121
+ them only from a card shown in a turn). The same change twice in a turn is one card. A P3 finding stands: a card action
122
+ can't carry a plain reply, so an action here is a real edit.
123
+
124
+ Apply is `/objectives --card ID done O2`: the proposal's own line (`Proposal#line`) behind a prefix the command reads
125
+ (`ObjectivesCommand::CARD`), so it goes through the one route, with its refusals. On success the card with that id is
126
+ replaced by a one-line notice. Dismiss (`--card ID dismiss`) only folds it. Either is recorded under the lock
127
+ (`objectives_cards.json` in the session's folder), so a repeat, from a stale tab or a second tap, applies nothing: it
128
+ says the proposal was already applied and folds the card again. A reword carries what it was proposed against
129
+ (`--was`, `Objectives.fingerprint`) and is refused when the item has changed since. It keeps the item's done-when unless its text
130
+ brings its own. The page keeps a proposal card's buttons off until the folded card comes or the action fails
131
+ (`cards_place.js actionFailed`). A turn leaves at most `PROPOSALS_PER_TURN` (3) cards; past that the tool says so. A refused edit leaves the card, and its
132
+ `command_ran` shows the refusal. Nothing is recorded as a move (`recorded?` is positive); the call stays in history as
133
+ any call. An add's text can't split on ` | `. The body names the line, for a terminal, which shows no button.
134
+
135
+ ## The page
136
+
137
+ `Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
138
+ the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
139
+ `:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
140
+ once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
141
+ with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
142
+ `ParticipantView::DROPPED`.
143
+
144
+ Idle, an edit lands at once: the worker's anytime path wakes the loop (`@waker.wake`, one extra pass for any anytime
145
+ command), which absorbs the file. It no longer waits for the fallback tick.
146
+
147
+ - **The wiring** (`objectives_wiring.js`): the routes the strip and the moves post on, each following a woken worker
148
+ only while the page still shows that session.
149
+ - **The strip** (`objectives_strip.js`, `#objectivesStrip`; pure parts in `objectives_model.js`, CSS in
150
+ `objectives.css`). A factory with its own closure (the list; never `store.current`), seeded by `renderSessionView` and reset
151
+ by `forgetSession`. Its edit list is a `chip_popover.js` shell, so a phone gets the bottom sheet and the focus rules.
152
+ Every edit is a line on the command route with `card: true`: no echo, and `commandView` hides a good `/objectives`
153
+ answer that came with that mark, so a tap leaves no bubble, a refusal does. The strip redraws from the event, never
154
+ optimistically.
155
+ - **The entry** (`objectives_entry.js`, from `turn_html.js noteBubbleTemplate`): `Objectives.entry` puts the note's items
156
+ and, against the list before it, `changes` (`Objectives.changes`, display only) on the history item and on
157
+ `context_added`. A first list is pills; a later one a line, `You marked O1 done`. The raw text stays the tooltip.
158
+ - **Moves** (`objectives_moves.js`): a tool row's id is a chip (`moveRowText`), and a row that recorded a move carries
159
+ `data-move-id`. One observer on `#history` reads those back into a line after each finished `.turn-work`, so a reload,
160
+ a live end and a hand-off all draw it. The newest line's Not yet is `objectivesStrip.notYet`. A decision card's
161
+ `Concerns O2` gets the chip there too.
162
+ - **Not yet** is `POST /turn` with a client id of its own (`<tab>:objectives`), so the stream draws its bubble in every
163
+ tab, this one too: the page's own echo needs the composer. It steers a running turn or starts a turn, as a typed line.
164
+ - **Short version**: the same observer offers `/short` (decision-card) under the newest long answer (`foldable`), only with a list
165
+ and with the command in the session's listing, and not during a turn: `/short` itself refuses then, since a running
166
+ turn's "last answer" is interim text. A request that fails, or a worker that leaves with one still out (the bundle's service
167
+ stops after chi's 3 s wait for a command), ends the card saying so; a worker killed outright leaves its one-line `…` row.
168
+
169
+ ## Continue, rollback, fork
170
+
171
+ - **Continue**: `Objectives.carry`, in `SessionContinue`'s `before_spawn`, writes a second note file in the new link,
172
+ after the carry note and any moved context's note, so the list reads as the latest word. It is not in
173
+ `SessionChain.note_text` (capped; the last answer gets what the recap leaves). It reads the previous link fresh,
174
+ with its unabsorbed newest file. A failure is only logged (`continue_objectives_failed`), because it runs inside the
175
+ continue's rollback. A review link's list stays behind, as its last answer does: the next link's participants could
176
+ differ.
177
+ - **Rollback**: a note survives `TurnFlow#restore`, and an unabsorbed edit is on disk, untouched.
178
+ - **Fork, delegate**: a plugin fork copies what it is passed. A delegate starts with an empty fold, so the tool answers
179
+ that there are none.
180
+
181
+ ## The long-answer card
182
+
183
+ A bundle, not core: [decision-card](../bundles.md#the-decision-card-bundle), removed by uninstalling. It reads the list with
184
+ `ctx.objectives`, the plugin API over `Objectives.current`; `items` and `text` are the bundle's only view of the object.
185
+
186
+ `:after_turn` reads the answer's length and tail from `event[:answer]`, not from the stored message, which on the native
187
+ loop still holds its thought block, and starts a thread only when both pass, so the turn's end
188
+ is held for no request and the web's answer doesn't wait (no `event[:present]`). The thread reads the list and, with an active
189
+ objective, makes one `ctx.ask_model`. A Transcript drops system entries, so the list goes in `prompt:`. The objective a reply
190
+ names is display only: an id the list doesn't have, or has dropped, is left out.
191
+
192
+ Stale is a new turn: `:before_turn` takes the running `Ask` and cancels its request, so a one-slot server is free for that
193
+ turn, and `show` holds the same mutex, so a card shows before that start or not at all. The hook runs inside its turn, so the
194
+ card waits for `ctx.turn_running?` to go false: shown earlier, it gets `in_turn: true` and lands in that turn's collapsed block.
195
+ The request doesn't wait: it goes ahead of the turn-end warm-up, so on a one-slot server the warm-up queues behind it and is the
196
+ last thing in the slot, and the next turn keeps its prefilled prompt. A shown card is replaced by a one-line one with
197
+ the same id only at a turn of the owner's (a prompt, no author): a wake has no prompt, and a child's report isn't an answer. A
198
+ worker that restarts forgets a shown card, which then stays as it is: it has no buttons.
199
+
200
+ A card with no actions writes no `pending_card.json`, so it never reaches `Web::Push::Attention`.
201
+
202
+ ## Check this
203
+
204
+ `/objectives check O2` and the list's **Check this** (an objective with a move, the one route) run `ObjectivesCheck.run` on the
205
+ anytime command's own thread: one side request (`Engine#ask_objective_check`, on `ask_side_model`) reads the objective's latest
206
+ move against the session's record, and the answer is a verdict with reasons. Information only: no status changes, it is
207
+ never an approval, no coordinator path reads it, and nothing enters the conversation or the next request.
208
+
209
+ **Why not a `VerifyFresh` child.** The plan said "VerifyFresh-like, fit unchecked". It fits poorly:
210
+
211
+ - `VerifyFresh.start` starts from a saved unclear verdict and its `VerifyAnchor`, a quote of at most 200 characters that
212
+ `verify_anchor_source_session` holds against the parent's text. A move has neither, and a faked one would land in `session.verdicts`.
213
+ - A child is an agent on the session's model with the owner's tools: an unknown number of requests, approvals waiting
214
+ on the child, a place in `session.max_children` and in the session lists, and its spend outside the parent's line.
215
+ Keeping it read-only and out of the delegate paths (`ChildRing`, `ChildrenStatus`) needs a new session field.
216
+
217
+ The side request costs exactly one request, shows in the session's side line (kind `objectives`) and comes out with one
218
+ file. What it gives up: **it can't open a file or run anything.** It reads what the session saw, so a claim that nothing in
219
+ the record settles is `unclear`, and a file changed since isn't seen. If that turns out too weak, a read-only child is the
220
+ next step, and the file and view below can stay.
221
+
222
+ **Independence.** The request (`ObjectivesCheck.request`) has its own system text and no tools. Its record is what people and
223
+ notes said and what tools returned (`.record`, each entry cut at 1,500 characters, the newest 24,000 kept). It never holds the
224
+ agent's prose, the system prompt, the list's notes, a continue's carry note, which holds the agent's last answer
225
+ (`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
226
+ Note and evidence are capped at 1,000 characters each. Mid-turn it reads the loop's conversation (`Engine#live_conversation`), the running turn's runs too.
227
+
228
+ **Its frame.** The claim and the record sit between `<record-RANDOM>` tags made per request, so tool output printing a closing tag
229
+ stays inside; the request says that what's inside is data and its instructions aren't to be followed. No model is needed for
230
+ the frame to hold; a model may still be swayed by text inside it, which is why a verdict is information only.
231
+
232
+ **Its host.** The model is the session's, or `objectives.check_model`, whose host may be another one. The whole user text goes through
233
+ `Web::ParticipantView::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
234
+ the home folder `~`. That is by pattern, so a secret of another shape still goes.
235
+
236
+ **State.** `objective_checks.jsonl` beside the session: a `running` line, then one with `verdict` (confirmed, refuted, unclear,
237
+ `no_answer`), `reason`, `text`, `model` and `mark`: a digest of the item's line as sent and the move itself (`Move#ref`, its run's
238
+ id and its model entry's time, with its words). The start is under `Objectives.with_lock`, so a double tap or a second tab answers "already
239
+ being checked" and asks nothing. A running line stops counting once its worker's pid is gone, so a kill, a shutdown or a restart frees
240
+ the next check at once, and past `RUNNING_SECONDS` in any case. `Objectives.view` puts the last check on its item (`check`), only while `mark`
241
+ matches: a reword, a status change or a newer move, in the same words too, offers the check again. A running check carries `until`, because no event says
242
+ its worker died: the strip lets the row go at that time (`armExpiry`), and a reconnect's session read clears it sooner. `announce_objectives` carries the start and the end. A continue doesn't carry it.
243
+
244
+ **Shown as** a card with no actions (`source: objectives`, on the command's thread, so `anytime` and not in a turn's block, and it
245
+ writes no `pending_card.json`, so no push), the list's row, and `check:` under the move in `/objectives`. A model that fails is
246
+ `no_answer`: logged (`objectives_check_failed`), an info card saying so, and the next tap may try again.
247
+
248
+ **Limits.** Refused during a participant's turn, whose text stays out of a side request; one ending during their turn is still the owner's (`Engine#side_author`). A participant can't reach it: the
249
+ command route is the owner's, and `card` and `objectives_updated` are in `ParticipantView::DROPPED`. A move made in the running
250
+ turn isn't on the page until the turn's end (`announce_objectives`), so its tap waits for that; the typed line works at once.
251
+
252
+ ## System 1 in audit
253
+
254
+ `ObjectivesAudit`, off unless `objectives.system_one: audit`. See [system-one.md](../system-one.md#objectives-in-audit).
255
+
256
+ ## Not in this version
257
+
258
+ Stable-prompt lines aren't here, and terminals have no picture: they keep `/objectives`.
259
+
260
+ Taking it out: old notes stay readable framed bubbles and keep being sent, and the key is ignored. Old `objectives` calls
261
+ stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
262
+ page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
263
+ falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
264
+ the `check` key; the audit is `objectives_audit.rb` and three calls in `Engine`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
23
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
24
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
25
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep) and `worker_features.js` are new modules for that reason.
26
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js` and the `objectives_*.js` modules are new modules for that reason.
27
27
 
28
28
  ## Two pages, two models
29
29
 
@@ -54,6 +54,9 @@ change.
54
54
  | **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
55
55
  | **SSE** | see Streams | the server | owner page: events go into the DOM directly; participant page: an event marks the data stale |
56
56
 
57
+ The objectives' strip keeps the list in its own closure (`objectives_strip.js`, seeded by `renderSessionView`, reset by
58
+ `forgetSession`, updated by `objectives_updated`): [objectives.md](objectives.md#the-page).
59
+
57
60
  The start page's own choices (the model, the ⚙ panel, the 🧰 list) live in `start_page.js`'s closure, never in
58
61
  `store.current`. A fresh chain link's picker reads both.
59
62
 
data/docs/plugins.md CHANGED
@@ -850,10 +850,12 @@ full label, `plugin.rb (bundle my-bundle)`.
850
850
  | `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
851
851
  | `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
852
852
  | `ctx.prompt_files` | the files whose text is in the session's system prompt now: the AGENTS.md chain (the work tree top down to the cwd) and the appended files (`append_system_prompt:`, `--append-system-prompt`); absolute paths, in prompt order, `[]` before the first build |
853
+ | `ctx.objectives` | the session's [objectives](internals/objectives.md), read now, as a frozen value: `empty?`, `items` (each `id`, `text`, `status` `"active"`, `"done"` or `"dropped"`, `done_when`), `find(id)`, `text` (the lines the model is shown) and `moves` (the latest move per id; mid-turn, a worker's leaves the running turn's out). An edit not yet absorbed counts. Empty when the session has none. Use its methods, not its class. Needs chi newer than 0.148.0 |
853
854
  | `ctx.notify(text, level: :info, fallback_for: nil)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command). `fallback_for: :display` marks a line that repeats the answer's display (`event[:present]`), which a UI that renders the display leaves out (`fallback_for: :editor`: a file the viewer could open from a ref in its editor): see [Hooks](hooks.md#what-a-hook-can-do-the-runtime). The keyword needs chi 0.35.0 (it raises on an older chi) |
854
855
  | `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
855
856
  | `ctx.ask_user(question:, options:, header: nil, allow_freeform: false, effects: nil)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options or effects). `effects:` tags the options a parent agent's answer is held to (label => `:spend`, `:permit`, `:permit_wide` or `:deny`; untagged = `:answer`): tag every option that spends or permits, and never let text alone or a dismissal do either ([hooks.md](hooks.md#what-a-hook-can-do-the-runtime)). It needs a chi newer than 0.85.0 |
856
857
  | `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
858
+ | `ctx.turn_running?` | whether a turn runs now. An `:after_turn` hook runs inside its turn, so a card a thread shows before the turn is over belongs to that turn's block, which the web collapses: wait for this to go false. Needs chi newer than 0.148.0 |
857
859
  | `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. The model reads it with a header naming your bundle (`[Steer from the my-bundle plugin, mid-task. …]`); the session keeps the raw text. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
858
860
  | `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
859
861
  | `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
@@ -1377,8 +1379,8 @@ that calls it, or for a real external bundle that does. No method on the chance
1377
1379
  `Samagotchi::Tools::Execute.timeout_seconds`.
1378
1380
  5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
1379
1381
  6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
1380
- `Samagotchi::Log.debug`, `Samagotchi::Config.get` and
1381
- `Samagotchi::VERSION`.
1382
+ `Samagotchi::Log.debug`, `Samagotchi::Config.get`,
1383
+ `Samagotchi::VERSION` and `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes).
1382
1384
 
1383
1385
  `Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
1384
1386
  and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
data/docs/releasing.md CHANGED
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
13
13
  and `rake release:check` enforce it). The system bundle upgrades itself when
14
14
  chi starts.
15
15
  - **Every other shipped bundle** (btw, check-in, guardrails, known-names,
16
- loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
16
+ loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, and the profiles core and dev) has its own semver in its
17
17
  `manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
18
18
  `chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
19
19
  - a change to a bundle's files bumps that bundle's `version:`
data/docs/sessions.md CHANGED
@@ -192,6 +192,48 @@ OSA
192
192
  [ -n "$picked" ] && pbpaste | "$chi" note --source clipboard $(print -r -- "$picked" | cut -f1)
193
193
  ```
194
194
 
195
+ ## Objectives
196
+
197
+ `/objectives set A | B` gives a session a short list of what it is to achieve, and `/objectives` shows it. The list goes
198
+ into the conversation as a note under its own frame (`[OBJECTIVES, set by the user. …]`), which asks the model to keep
199
+ work on it, to say when something falls outside it and not to change it itself. A session with no list behaves as it
200
+ would without the feature; its request only carries the `objectives` tool below (declared on `full`, named in the
201
+ not-loaded line elsewhere). Each item has an id (`O1`, never reused), a status (active, done, dropped) and
202
+ optionally a done-when hint, which nothing enforces.
203
+
204
+ An edit waits for a moment between turns: it lands within seconds in an idle session, and at the turn's end during one.
205
+ Several edits made during a turn go in as one list. A continue copies the list to the next link, as a second note after
206
+ the carry note; a review link doesn't. `!rollback` keeps it. A chi REPL's session (`--no-shared`) refuses an edit, as it
207
+ refuses a note. See [cli.md](cli.md#session-objectives) for the commands and
208
+ [internals/objectives.md](internals/objectives.md) for the mechanism.
209
+
210
+ The model can record a **move** with the `objectives` tool: a line on what it did or found for an objective, with
211
+ optional evidence. A move shows as the tool's row in the turn, and `/objectives` shows each objective's latest one under
212
+ it. It is information only: only you change the list or a status, and it never approves anything. A move costs one
213
+ generation when it's the step's only call. On a kit that doesn't declare the tool, `/toolkit add objectives` declares it
214
+ (the model can also reach it with `call_tool`). A participant's turn is denied it.
215
+
216
+ The model can also **propose** a change with that tool (add, reword, done, drop). You see a card when the turn is over:
217
+ Apply makes the edit, once, Dismiss leaves the list, and ignoring it changes nothing. A turn leaves at most 3. A proposed
218
+ reword is refused when you changed that objective since. The model never changes the list by itself.
219
+
220
+ **Check this** (`/objectives check O2`, or the button in the web's list, for an objective with a move) asks for an independent read of
221
+ the latest move: one side request, on your request only, on the session's model or `objectives.check_model`. It sees the
222
+ claim, what people and notes said and what tools returned, not the agent's prose, and no file: what the record doesn't
223
+ settle comes back `unclear`. **That is up to 24,000 characters of raw tool output, which with a hosted `check_model` leaves the machine on one tap, even from a local session.** chi's redaction takes out known key shapes and `token=`-style values first, by pattern only, so another secret in a tool's output still goes. The verdict (confirmed, refuted, unclear) and its reasons come as a card and under the
224
+ objective. It is information only: it changes no status, approves nothing, and isn't in the conversation or the model's next request.
225
+ A second tap while one runs asks nothing. Its cost is in the session's side line. Mid-turn it runs beside the turn; on a one-slot
226
+ local server it answers after the running request. A participant can't reach it. See
227
+ [internals/objectives.md](internals/objectives.md#check-this).
228
+
229
+ **In the web** a session with a list has a strip above the conversation: its objectives and how many are done (a phone
230
+ shows one line, the one being worked on). A tap opens the list, where you add, mark done, drop, undo and tap a text to
231
+ reword it, with no message and no turn. Mid-turn the strip has the change and says it applies after this turn. Each list
232
+ in the conversation shows as pills, or as one line of what you changed. A turn's moves show as chips above its answer;
233
+ Not yet on one (or in the list, for an objective with a move) sends `Not yet on O2 (…).` as a normal message. With the
234
+ [decision-card](bundles.md#the-decision-card-bundle) bundle, the newest long answer offers its Short version in a card,
235
+ which enters no history. A session with no list shows none of this. A participant doesn't see the list.
236
+
195
237
  ## Notes a turn leaves for the model
196
238
 
197
239
  A turn that ends without an answer leaves one line for the model to read on its next turn, as a tail
@@ -304,8 +346,8 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
304
346
 
305
347
  `SessionManager.continue_session` makes the next link:
306
348
 
307
- - **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
308
- - **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
349
+ - **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript beyond the note's last answer, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
350
+ - **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Then `Its last answer:` and the previous link's last answer, read after the recap wait, so its exact shas, paths and next steps survive: a longer one keeps its last 4000 characters, after a `…` (a wrap-up closes on what comes next). There is none when the newest bubble was a step that asked for tools, when a later turn failed or was cut before it answered, and on a review link (a continue doesn't copy the grants, so the next link's participants could differ); `session.carry_last_answer: false` leaves it out of every note. No other transcript. The note never makes a continue fail: it stays within the 16 KiB a note may hold, the answer taking what the head and recap leave, and a session file that can't be read gives no answer. An idle new link holding only the note is kept, not discarded as empty.
309
351
  - **Its attached context:** the previous link's movable sources (a command source by default; a pushed one stays) are copied to the new link before its worker starts, with their last text and the removed and muted markers, and a second note (`attached context`, after the carry note) names what moved and what stayed. A push into the archived link is then refused with the new link's id. See [Movable context](context.md#movable-context).
310
352
  - **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
311
353
  - **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its finished delegates are archived with it, as any archive does.
data/docs/system-one.md CHANGED
@@ -3,7 +3,7 @@
3
3
  System 1 is the cheap judgment model chi asks small questions: approvals
4
4
  ([guardrails.md](guardrails.md#system-1-experimental)), toolkit suggestions
5
5
  ([configuration.md](configuration.md#toolkits)), context candidates
6
- ([context.md](context.md)) and bundles' own ([plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge)). Each use keeps its own mode, gates and log. What they share is
6
+ ([context.md](context.md)), the objectives' audit ([below](#objectives-in-audit)) and bundles' own ([plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge)). Each use keeps its own mode, gates and log. What they share is
7
7
  one host (`system_one.host`, `.model`, `.timeout_ms`) and one layer that saves every ask, so a later comparison or fine-tune has what
8
8
  was sent and what came of it. The runs don't leave the machine; a hosted System 1 host sees each state as it is asked.
9
9
 
@@ -20,6 +20,7 @@ one warn and never changes the answer. A use that needs the run's id at once pas
20
20
  | `approvals.allow` | `GuardrailWiring#system_one_judge` | gates, acted (the judgment line's decision), labels |
21
21
  | `toolkit.suggest` | `ToolkitSuggest.judge` | acted (the log's `did`), no labels |
22
22
  | `context.candidate` | `ContextJudge.ask` | labels |
23
+ | `objectives.incoming`, `objectives.outgoing` | `ObjectivesAudit` | none: nothing acts on them |
23
24
  | `<bundle>.<name>` | `SystemOne::Judgments` (`ctx.judge`, `ctx.judge_later`) | the bundle's: labels, acted |
24
25
 
25
26
  Not saved: a toolkit warm-up (it goes straight to the backend), `chi guardrails replay`,
@@ -33,7 +34,7 @@ same `Client` and `Recording`: the API is in [plugins.md](plugins.md#judgments-c
33
34
  [configuration.md](configuration.md#bundles-judgments).
34
35
 
35
36
  Only bundles' judgments have settings of their own (`JudgmentSettings`). Core's take no host, model or mode here, whatever
36
- `system_one.judgments` says: `Record` and `Similar` are model-blind (`Record#initialize` filters by rule only), so a model swapped
37
+ `system_one.judgments` says: `Record` and `Similar` are model-blind (`Record#initialize` filters by rule, and by repository for a rule in `guardrails.system_one.repo_rules`, never by model), so a model swapped
37
38
  for approvals alone would inherit a clean record and could auto-allow. Before any such swap, `Record` and `Similar` have to go by model.
38
39
 
39
40
  Core's uses keep their own threads (`ToolkitSuggest::Pending`, `ContextJudge`, the approval path) and contention; only the ask-and-save
@@ -41,6 +42,21 @@ seam is common. A bundle's `judge_later` runs on the Engine's `SideJudgments` un
41
42
  at shutdown covers it, and a run cut by that one-second limit is lost. Its asks stay out of the session's spend, as all of System 1's do, and no
42
43
  cost cap covers them on a hosted host.
43
44
 
45
+ ## Objectives in audit
46
+
47
+ `objectives.system_one: audit` asks whether a message may affect an objective: one yes/no (`affects`) per active objective, each ask
48
+ one request and one run. It is off by default, and asks only with a list that has an active objective, a `system_one.host` and saving on
49
+ (`system_one.runs`). **Mode audit only:** there is no `on`. Nothing reads an answer: no card, no wake, no push, nothing in a prompt, and no turn. A
50
+ per-message judge on an objective didn't work as a gate on the pod (AUC about .5 to .66), so these runs are there to look at, with `chi judgments report objectives.incoming`.
51
+
52
+ - `objectives.incoming`: a person's message of the owner's of 40 characters or more at its turn's start (`TurnOrigin#human?`: not a reminder's or a delegate's prompt), not a `/` or `!` line, and a note going
53
+ into the conversation (not the list's own, nor a continue's carry note: `Objectives.outside_note?`). A steer isn't asked about.
54
+ - `objectives.outgoing`: a finished answer of the owner's turn of 400 characters or more, not a cancelled or exhausted one.
55
+ - The state: `objective` (its text and done-when, no id or status) and `message` (2000 characters: a message's head, an answer's tail).
56
+ **That text goes to the System 1 host**, a hosted one too, and into the run. A participant's turn, a review session and a scratch session are never asked about.
57
+ - At most 6 objectives per message. Each direction has one ask running: what comes meanwhile is a run with `asked: false`, `skipped: busy`.
58
+ - The asks run on `SideJudgments`' threads beside the turn, so a turn doesn't wait; on a one-slot server shared with the session's model, a request can still queue behind or ahead of them. They share `/v1/systemone` with approvals: on a slow one-slot host an approval can wait behind up to 2 in-flight asks and time out to the user. A failed ask is a run with its `failure` and one log line (`system_one_failed`), and a `--no-shared` REPL's stderr shows that line as for any System 1 ask.
59
+
44
60
  ## The store
45
61
 
46
62
  `<state dir>/system_one/runs/<judgment>/YYYY-MM-DD.jsonl` (`SystemOne::RunsStore`): one file per UTC day,
@@ -46,7 +46,7 @@ module Samagotchi
46
46
  return false if ContextNote.note?(message) || Steer.steer?(message) || TurnNote.empty_answer(message)
47
47
  return false unless %w[assistant model].include?((message[:role] || message["role"]).to_s)
48
48
 
49
- !OutputFormatter.strip_markup((message[:content] || message["content"]).to_s).empty?
49
+ !OutputFormatter.strip_markup((message[:content] || message["content"]).to_s.scrub).empty?
50
50
  end
51
51
  end
52
52
  end
@@ -0,0 +1,21 @@
1
+ # The coordinator's merge approval (docs/bundles.md, The coordinator bundle).
2
+ # Here and not in the guardrails bundle: that one is in the core profile, so
3
+ # everyone would get a new ask and --non-interactive merges would start being
4
+ # denied. The skill and the rule upgrade together.
5
+ #
6
+ # Votes in every session on a machine with this bundle, any repo, children and
7
+ # background builds too, and a --non-interactive merge is denied: nobody to ask.
8
+ #
9
+ # Asks on every merge, whatever the branch. Same git-with-global-options prefix
10
+ # as the guardrails bundle's rules; merge-base, merge-tree, merge-file,
11
+ # mergetool and a merge whose first word is --abort, --continue or --quit don't
12
+ # match. git pull and gh pr merge are not covered. Text as data over-asks (a
13
+ # commit message, chi send -m, a heredoc).
14
+ rules:
15
+ - id: git-merge
16
+ tool: shell
17
+ command: '\bgit(?:\s+(?:-C\s+\S+|-c\s+\S+|--?[A-Za-z][\w-]*(?:=\S+)?))*\s+merge(?=[\s;&|)]|$)(?!\s+--(?:abort|continue|quit)\b)'
18
+ skip_read_only: true
19
+ verdict: ask
20
+ scopes: [once, rule]
21
+ reason: git merge joins a branch into the checked-out one; allowing the rule covers every later merge in this repo
@@ -1,18 +1,22 @@
1
1
  ---
2
2
  name: coordinator
3
- version: 0.6.1
3
+ version: 0.8.0
4
4
  scope: system
5
- description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, ask before each merge, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions) and the coding toolkit for build children (child + be concise)"
5
+ description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions, objectives) and the coding toolkit for build children (child + be concise)"
6
6
  trust_level: reviewed
7
7
  files:
8
8
  skill_coordinator.md:
9
- sha256: sha256:5f6b879de274ebc89b7811d3dda3abbecbeb696159e326647a1c7393c3598f57
10
- description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, ask the user before each merge; resume from a handoff_* memory"
9
+ sha256: sha256:1cac885facd28757aaa0197b28889246f845834c17101d59b5625d1b2b89679a
10
+ description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, merge under the git-merge approval or ask the user; resume from a handoff_* memory"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:0cd1e14cfc8afc6c8fd82548f390d20c812f336bdd357edaaa1b8ba1be32e0ef
13
+ sha256: sha256:84535a86b754f3a12cdb2f2591027ea71bbf9ffb74734b1050899b9a4fde07ac
14
14
  # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
15
15
  # coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
16
16
  # the next chi.
17
17
  requires_chi: ">= 0.107.0"
18
18
  # /coordinate review uses ctx.sessions.review (the next chi).
19
+ # No requires_chi_next for guardrails/merge.yml: its keys are long supported, and first-ever too-new rules would make a
20
+ # live older worker deny every call. A chi without `chi guardrails rule` gives a usage error, and the skill then asks.
21
+ # Nor for the objectives tool in the kit and the skill: on an older chi the kit's add names nothing (it declares only what
22
+ # is registered), and a call of it comes back as an unknown tool, no harm.
@@ -3,7 +3,8 @@
3
3
  # The coordinator bundle (docs/bundles.md, The coordinator bundle): chi as a
4
4
  # coordinator of parallel work. Its skill_coordinator memory holds the steps
5
5
  # (split the work, one worktree and a wait: false delegate per task, check
6
- # each report, ask before each merge); this plugin adds two commands.
6
+ # each report, merge under the git-merge rule's approval or ask); this plugin
7
+ # adds two commands, and guardrails/merge.yml that rule.
7
8
  #
8
9
  # /children [all] shows this session's children as a card, newest first:
9
10
  # state, branch, the last reply and whether it was reported (ChildrenStatus
@@ -41,7 +42,8 @@
41
42
  # itself (the core child-boundary guardrail), in every mode: nothing to set up.
42
43
  #
43
44
  # The coordinator toolkit is lean plus the tools that run children
44
- # (delegate, delegate_result, list_sessions). /coordinate puts the session
45
+ # (delegate, delegate_result, list_sessions) and objectives (the skill's
46
+ # moves). /coordinate puts the session
45
47
  # on it before it asks the model (ctx.use_toolkit), which chi allows only
46
48
  # before the session's first model reply and outside a turn: otherwise it
47
49
  # says to run /toolkit coordinator, unless the session declares delegate
@@ -83,7 +85,7 @@ class Plugin
83
85
  def initialize(_settings = {}); end
84
86
 
85
87
  TOOLKIT = "coordinator"
86
- TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions].freeze
88
+ TOOLKIT_TOOLS = %w[delegate delegate_result list_sessions objectives].freeze
87
89
  CODING_TOOLKIT = "coding"
88
90
  CODING_PROMPT = "- Be concise in your responses\n- Show file paths clearly when working with files"
89
91