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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -1
- data/README.md +2 -2
- data/docs/architecture.md +1 -1
- data/docs/bundles.md +64 -17
- data/docs/cli.md +18 -0
- data/docs/configuration.md +7 -3
- data/docs/guardrails.md +27 -5
- data/docs/hooks.md +2 -1
- data/docs/internals/objectives.md +264 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/plugins.md +4 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +44 -2
- data/docs/system-one.md +18 -2
- data/lib/samagotchi/answer_tail.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/guardrails/merge.yml +21 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +9 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +5 -3
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +44 -13
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +12 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/guardrail_wiring.rb +11 -6
- data/lib/samagotchi/guardrails/approvals.rb +3 -1
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/rules.rb +6 -1
- data/lib/samagotchi/guardrails/verdict.rb +21 -1
- data/lib/samagotchi/guardrails_command.rb +47 -3
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +335 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/send_command.rb +2 -1
- data/lib/samagotchi/session_chain.rb +91 -8
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_continue.rb +6 -2
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/system_one/record.rb +17 -3
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +10 -0
- data/lib/samagotchi/web/public/cards_place.js +20 -4
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- 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)
|
|
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
|
|
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.`
|
|
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
|
|
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.
|
|
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,
|
|
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:
|
|
10
|
-
description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report,
|
|
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:
|
|
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,
|
|
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)
|
|
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
|
|