samagotchi 0.154.1 → 0.155.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +1 -1
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/anthropic.md +4 -4
  9. data/docs/internals/attached-context.md +8 -6
  10. data/docs/internals/block-renderers.md +8 -1
  11. data/docs/internals/engine.md +220 -99
  12. data/docs/internals/inline-widgets.md +136 -0
  13. data/docs/internals/objectives.md +8 -6
  14. data/docs/internals/participants.md +5 -5
  15. data/docs/internals/prompt-caching.md +9 -5
  16. data/docs/internals/web-frontend.md +1 -1
  17. data/docs/internals/working-set.md +2 -1
  18. data/docs/plugins.md +2 -0
  19. data/docs/releasing.md +1 -1
  20. data/docs/sessions.md +2 -2
  21. data/docs/testing.md +1 -1
  22. data/lib/samagotchi/block_renderers.rb +5 -0
  23. data/lib/samagotchi/bundle_command/check.rb +1 -1
  24. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  25. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  26. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  27. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -125
  29. data/lib/samagotchi/engine/guardrails.rb +4 -2
  30. data/lib/samagotchi/engine/messages.rb +9 -6
  31. data/lib/samagotchi/engine/objectives.rb +22 -90
  32. data/lib/samagotchi/engine/participants.rb +15 -10
  33. data/lib/samagotchi/engine/plugins.rb +3 -1
  34. data/lib/samagotchi/engine/questions.rb +1 -1
  35. data/lib/samagotchi/engine/side_requests.rb +5 -2
  36. data/lib/samagotchi/engine/toolkit.rb +33 -190
  37. data/lib/samagotchi/engine/turn_end.rb +1 -1
  38. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  39. data/lib/samagotchi/engine/verify.rb +7 -143
  40. data/lib/samagotchi/engine.rb +162 -99
  41. data/lib/samagotchi/engine_peers.rb +1 -1
  42. data/lib/samagotchi/event_fanout.rb +18 -12
  43. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  44. data/lib/samagotchi/guardrails/gate.rb +41 -40
  45. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  46. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  47. data/lib/samagotchi/guardrails/targets.rb +6 -4
  48. data/lib/samagotchi/guardrails_command.rb +1 -1
  49. data/lib/samagotchi/inline_widgets.rb +325 -0
  50. data/lib/samagotchi/kernel_loop.rb +190 -213
  51. data/lib/samagotchi/llm/backend.rb +3 -1
  52. data/lib/samagotchi/llm/chat_loop.rb +63 -41
  53. data/lib/samagotchi/llm/native_backend.rb +3 -2
  54. data/lib/samagotchi/llm_context_forget.rb +5 -3
  55. data/lib/samagotchi/model_binding.rb +3 -3
  56. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  57. data/lib/samagotchi/question_desk.rb +1 -1
  58. data/lib/samagotchi/revise_anchor.rb +1 -1
  59. data/lib/samagotchi/system_prompt.rb +37 -30
  60. data/lib/samagotchi/testing/boundary.rb +7 -0
  61. data/lib/samagotchi/testing/bundle.rb +7 -5
  62. data/lib/samagotchi/testing/install.rb +3 -2
  63. data/lib/samagotchi/tool_activity.rb +8 -6
  64. data/lib/samagotchi/tool_declarations.rb +4 -4
  65. data/lib/samagotchi/tool_response.rb +4 -3
  66. data/lib/samagotchi/tool_runner.rb +91 -100
  67. data/lib/samagotchi/toolkit_binding.rb +40 -14
  68. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  69. data/lib/samagotchi/tools/registry.rb +43 -40
  70. data/lib/samagotchi/tools/tool_index.rb +135 -88
  71. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  72. data/lib/samagotchi/turn_context.rb +67 -0
  73. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  74. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  75. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  76. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  77. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  78. data/lib/samagotchi/turn_state.rb +46 -4
  79. data/lib/samagotchi/version.rb +1 -1
  80. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  81. data/lib/samagotchi/web/message_display.rb +1 -1
  82. data/lib/samagotchi/web/public/app.js +5 -1
  83. data/lib/samagotchi/web/public/block_host.js +74 -199
  84. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  85. data/lib/samagotchi/web/public/block_source.js +7 -4
  86. data/lib/samagotchi/web/public/block_watch.js +256 -0
  87. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  88. data/lib/samagotchi/web/public/index.html +28 -9
  89. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  90. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  91. data/lib/samagotchi/web/public/participant.js +1 -1
  92. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  93. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  94. data/lib/samagotchi/web/public/text_roots.js +1 -1
  95. data/lib/samagotchi/web/public/verify_target.js +1 -1
  96. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  97. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  98. data/lib/samagotchi/web/public/widgets.css +70 -0
  99. metadata +17 -1
@@ -0,0 +1,136 @@
1
+ # Inline widgets: chi-ui blocks the page draws
2
+
3
+ A model can write a ```` ```chi-ui ```` fence in an answer: YAML, `type:` first, a **picks** (rows of options to choose
4
+ from, which fill the reply) or a **walk** (a scenario stepped through). Core draws it, not a bundle: the server parses
5
+ the YAML once and the page draws from the parsed data. Where it doesn't draw (the terminal, `web.markdown` off, a
6
+ refused block) the YAML reads as text. The server half (the parse) and the page half (the drawers) are below.
7
+
8
+ What tells the model the blocks exist is the shipped inline-widgets bundle
9
+ ([bundles.md](../bundles.md#the-inline-widgets-bundle)): two prompt sections, `picks` and `walk`, each with an example
10
+ its spec holds to a strict parse. Without it nothing here changes, and blocks already in a history still draw.
11
+
12
+ ## The path
13
+
14
+ 1. `Web::MarkdownRenderer#fences` walks the `code_block` nodes, as for block renderers' `data-lang`
15
+ ([block-renderers.md](block-renderers.md), The path), keeping each one's language and its literal
16
+ (`string_content`, the source between the fences, also inside a list item or a blockquote) as a `Fence`.
17
+ 2. After sanitize's walk, `mark_languages` pairs them with the `<pre>`s by position and, for a `chi-ui` one, calls
18
+ `InlineWidgets.parse(literal)`. The block gets `data-widget` (the JSON of the data with its `type`) or
19
+ `data-widget-error` (the one-line reason), and `data-widget-lenient` when the lenient pass was needed. A count
20
+ that differs sets nothing, as for `data-lang`.
21
+ 3. Every HTML path goes through `render`, so answers, cards, delegate reports and `/api/render` all carry the
22
+ attributes; which of them draw is the page's choice (answer texts only).
23
+ 4. `chi-ui` is `BlockRenderers::RESERVED`: a bundle's `block_renderers:` can't claim it, so the page never has two
24
+ drawers for one block.
25
+
26
+ **Safety.** The JSON is an attribute value: Nokogiri escapes its quotes, and `<`, `>` and `&` in it are `\u` escapes
27
+ (`JSON_MARKUP`), so the HTML holds no model markup even as attribute text. The page draws its strings as text.
28
+
29
+ ## The parse (`Samagotchi::InlineWidgets`)
30
+
31
+ Pure Ruby, no web, no Engine. `parse(source)` returns a `Result`: `ok?`, `type`, `data` (string keys, JSON-ready),
32
+ `lenient?`, `error`, and `widget_json`.
33
+
34
+ - **Strings stay strings.** The YAML is never loaded: `Tree` reads Psych's node tree and takes each scalar's own text.
35
+ YAML 1.1 would make `- no` false (a real eval block has one), `id: 1.10` the float 1.1, and a date a `Date` that
36
+ `safe_load` refuses. A plain empty value, `~` or `null` is absent.
37
+ - **Plain data only.** An anchor, an alias or a tag (`!ruby/object`, `!!str`) is refused, as is a second document, a
38
+ key that isn't a scalar, a root that isn't a mapping, text that isn't valid UTF-8, and nesting past 8 levels
39
+ (`MAX_DEPTH`; the deepest legal block nests 6). A stack overflow is rescued too, as the backstop: it is no
40
+ `StandardError`, so it would escape `render` and break every render of the history.
41
+ - **Strict first, then the lenient pass.** Only a block Psych can't parse gets `Lenient.quote` and a second parse.
42
+ The pass is eval 0013's (`~/.claude/plans/evals/0013-inline-widgets/scripts/validate.rb`): the free-text values
43
+ (`title text why does sees behind when recommended`) and the list items that aren't mappings go in double quotes
44
+ unless they are one clean scalar already. It mends the traps models fell into: a colon in a value
45
+ (`does: Asked: ...`), a colon in an option (`- B: keep 1 h`), a value starting with a quoted word
46
+ (`text: "x" is called`). It is line by line, so a YAML error's line still points at the source. It can't mend a
47
+ comma in a flow list (`options: [A allow, B deny project, allow system]` is three options, so the recommended one
48
+ isn't one of them: eval round 1's failures), and a block that parses strictly into the wrong shape isn't retried.
49
+ - **Folds.** A block that parses strictly can still have an option or a free-text value that YAML read as a mapping
50
+ of one pair of scalars (`- fix now: drop the command`). Where text is wanted (an option, `title`, `text`, `why`,
51
+ `does`, `sees`, `behind`, `when`, `recommended`), that mapping is taken as the text written: the source slice from
52
+ the key to the value's end when both sit on one line, else `key: value`. The Result is then `lenient?`. A mapping
53
+ of several pairs, or one holding a list or a mapping, stays an error, and so does one where an id, an actor, a
54
+ `next` or a `goto` goes. This is a mend, not a second lenient pass (`Tree#folds`, `Schema#text`).
55
+ - **Normalised.** Ids are strings, unknown keys are dropped, optional values are absent rather than null, and text is
56
+ stripped. The page never sees model junk.
57
+
58
+ ### The schemas
59
+
60
+ | | Required | Optional | Rules |
61
+ |---|---|---|---|
62
+ | picks | `rows`: rows of `id`, `text`, `options` (2-4 texts) | `title`; on a row `recommended`, `why` | ids unique; a row's options unique; `recommended` is one of the options |
63
+ | walk | `title`, `actors` (2-5 texts), `steps`: steps of `id`, `actor`, `does` or `sees` | on a step `behind`, `next`, `cases` (each `when` + `goto`) | ids unique, none of them `end`; actors unique; `actor` is one of the actors; `next` names a step or is `end`, and not the step itself; every `goto` names a step |
64
+
65
+ Eval round 1's `branches:` (the same shape as `cases:`) is read as `cases`.
66
+
67
+ **`next: end`** ends the walk at that step: a branch that ends early, such as a success step written before the
68
+ error step it must not run into. The normalised JSON keeps `"next": "end"`, and the page shows the step as the last
69
+ one (its "end" tag, then "Start over"). A `goto` can't be `end`. In YAML:
70
+
71
+ ```yaml
72
+ - id: s4
73
+ actor: user
74
+ sees: the saved form
75
+ next: end
76
+ ```
77
+
78
+ ### The caps
79
+
80
+ The source is at most 16 KB, checked before the parse. After it: a picks has at most 12 rows, a walk at most 30 steps
81
+ and 2-5 unique actors, and every string is at most 300 characters. An error repeats at most 40 characters of a bad
82
+ `type`, and never a tag.
83
+
84
+ ## The page (`inline_widgets.js`)
85
+
86
+ `createWidgetDrawer` is a core drawer of `block_watch.js` (its header has the contract), wired in `app.js` ahead of the
87
+ bundles' frame drawer through `createBlockHost`'s `drawers`. So a `chi-ui` block gets the watcher's paths: a reload, a
88
+ session switch, a turn's end, Revise's clones, the stage's hand-off to `#history`.
89
+
90
+ - **What it draws.** A `<pre>` with `data-widget` gets a light-DOM `<figure class="block-drawn widget">` right before it,
91
+ holding a `.widget-host` and the shared `.block-toggle` (block_source.js `setSourceShown`, the same icon; the
92
+ figure's `data-toggle-picture` makes its other label "Show the widget"). The `<pre>` stays, hidden: the DOM is the state, copy.js gives it its `.code-wrap`
93
+ copy button (it copies the YAML, which is the answer's text), and the toggle shows it. The toggle is in the light DOM
94
+ because a click in a shadow root reaches the page's delegated listeners retargeted at the host.
95
+ - **Shadow DOM.** The host has an open shadow root holding the widget, with `widgets.css` adopted
96
+ (`adoptedStyleSheets`): the page fetches it once into a constructable `CSSStyleSheet` (`loadWidgetSheet`), and `draw`
97
+ returns a Promise that places the widget once it is there, so no widget flashes unstyled and the view held
98
+ (`holdingView`) measures the styled one. A failed fetch draws unstyled and the next draw tries again. Its words are then out of the bubble's `textContent`, which the shown-texts dedupe (`seedSeenFromDom`), copy's
99
+ fallback and an annotation's quote read, as with a renderer's `<img>`-only figure. chi's `:root` tokens reach it as
100
+ inherited custom properties, so light and dark follow the page. Playwright's locators pierce an open shadow root;
101
+ `page.evaluate`'s DOM calls don't. Whatever asks `document.activeElement` whether a field has the focus looks
102
+ through shadow roots (`composer_fold.js editable`, `stage_watch.js`), or typing in a note opens a folded composer.
103
+ - **A clone redraws.** `cloneNode` doesn't copy a shadow root: a Revise clone (`revise_switch.js viewOf`) arrives with
104
+ the figure and an empty host, which `needsDraw` sees, and `draw` fills it again with a fresh widget and its own
105
+ listeners. A widget's position and picks live in the widget, so a clone or a rewrite starts over (accepted for v1).
106
+ - **A refused block** (`data-widget-error`) stays source under `failBlock`'s muted `.block-note` ("Couldn't draw this
107
+ chi-ui block: …", an attribute CSS shows, a click copies it). Bad JSON or an unknown `type` fails the same way, from
108
+ the watcher. A `chi-ui` `<pre>` with neither attribute (an older server) is left alone.
109
+ - **walk** (`widget_walk.js`): the swimlane (one column per actor, one row per step; the current, seen and a hovered
110
+ case's target marked; a click jumps), the step card (actor, `does`, "You see", the dashed "Behind it"), a fork's
111
+ "Which case?" buttons, Back / Next / Start over, the path taken, and ← → while the widget has the focus
112
+ (a move that removes or hides the focused button leaves the focus on the widget; a click on the current step is no
113
+ move). `walkRoutes`
114
+ resolves where a step goes: its `next` (`end`: nowhere), else the step below; a fork's `cases` as written, a case whose `when` is no
115
+ condition (round 1's `always`) reading as the usual path, alone it is the step's next. At a phone's width the
116
+ swimlane scrolls sideways inside the widget; the page doesn't.
117
+ - **picks** (`widget_picks.js`): one row per item, its options as chips (`aria-pressed`), the recommended one marked
118
+ and picked to start with, a "+ note" per row, and "Fill reply".
119
+
120
+ **The only way out is composer text.** Fill reply builds `id: option (note) · …` from the picked rows (a row with no
121
+ pick is left out) and hands it to app.js's `fillComposer`, after a typed draft on a new line (`withDraft`; the draft
122
+ is kept). It never sends: the owner reads the text and sends it or not. A walk has no way out at all. The model's
123
+ strings reach the page through `textContent` and attributes only, never `innerHTML`, so a step reading
124
+ `<img src=x onerror=…>` shows as those characters.
125
+
126
+ **What doesn't draw.** The participant page (`participant.js` wires no block host), `web.markdown` off (no
127
+ `data-lang`), a live answer while it streams (its YAML shows until the turn's end re-renders it as markdown), and
128
+ anything outside `TEXTS`: cards, question replies, the asking slot, delegate reports. The terminal shows the YAML.
129
+
130
+ ## The fixtures
131
+
132
+ `spec/fixtures/inline_widgets/` holds eval 0013's 78 real blocks (47 from round 2, 31 from round 1), one file each,
133
+ and `manifest.yml`, the outcome (ok, lenient or error) the eval's `check()` gave each when they were copied, but for
134
+ r2-deepseek-picks-review.2: its options are one-pair mappings, an error in the eval, and a fold (lenient) here.
135
+ `spec/inline_widgets_spec.rb` checks every one gets it: all 47 of round 2's draw, and round 1's errors are the
136
+ comma splits.
@@ -116,9 +116,9 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
116
116
 
117
117
  `action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
118
118
  (`Objectives::Proposal#refusal`: its line through `Objectives::Edit`, the edit the card's Apply runs, so the tool refuses
119
- what Apply would) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
120
- as `#context_choice`'s cards are kept. `run_turn`'s ensure shows them only for a turn that completed: a canceled or
121
- failed one drops them (`Engine#discard_objective_proposals`), so its Apply card never outlives a turn whose call a
119
+ what Apply would) and hands it to `Engine#propose_objective` (`TurnStages::Objectives#propose`), which keeps it until the turn is over,
120
+ as `TurnStages::ContextJudge`'s cards are kept. `run_turn`'s ensure (`TurnStages::Objectives#finally`) shows them only for a turn that completed: a canceled or
121
+ failed one drops them, so its Apply card never outlives a turn whose call a
122
122
  rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
123
123
  `in_turn`), so it writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a
124
124
  restart (`CardStore` takes them only from a card shown in a turn). The same change twice in a turn is one card. A P3
@@ -139,7 +139,7 @@ any call. An add's text can't split on ` | `. The body names the line, for a ter
139
139
 
140
140
  `Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
141
141
  the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
142
- `:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
142
+ `:objectives_updated` carry it. `Engine#announce_objectives` (`TurnStages::Objectives#announce`) emits when the view differs from the last: after an edit,
143
143
  once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
144
144
  with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
145
145
  `ParticipantView::DROPPED`.
@@ -259,7 +259,9 @@ turn isn't on the page until the turn's end (`announce_objectives`), so its tap
259
259
 
260
260
  ## System 1 in audit
261
261
 
262
- `ObjectivesAudit`, off unless `objectives.system_one: audit`. See [system-one.md](../system-one.md#objectives-in-audit).
262
+ `ObjectivesAudit`, off unless `objectives.system_one: audit`. `TurnStages::Objectives` asks it: `incoming` at an owner's
263
+ turn's start (a person's prompt only), `outgoing` after the end event of a completed answer, `note` for a note added
264
+ between turns (docs/internals/engine.md, "Turn stages"). See [system-one.md](../system-one.md#objectives-in-audit).
263
265
 
264
266
  ## Not in this version
265
267
 
@@ -269,4 +271,4 @@ Taking it out: old notes stay readable framed bubbles and keep being sent, and t
269
271
  stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
270
272
  page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
271
273
  falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
272
- 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.
274
+ the `check` key; the audit is `objectives_audit.rb` and three calls in `TurnStages::Objectives`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
@@ -214,10 +214,10 @@ the list's `mode` (the panel's words) and the grant's stamp: `Grant#mode` / the
214
214
  turns) stays review until the owner invites them again.
215
215
 
216
216
  A turn's mode is `ParticipantMode.for_turn(session, entry)`: `:pair` only for a stamped entry in a pair session.
217
- The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` → `#turn_mode`, from the entry
217
+ The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` → `TurnStart#mode`, into the turn's `TurnContext#participant_mode`, from the entry
218
218
  `SessionParticipants#entry_for` reads), and everything in the turn reads that: the stamp on the opening message
219
219
  (`participant_mode`), the boundary in `GuardrailWiring#checks`, `ToolRunner#unreached_group_refusal` (through
220
- `KernelLoop#participant_mode`: a review turn's boundary words the refusal; a pair turn gets the owner's), the
220
+ the call's `TurnContext`: a review turn's boundary words the refusal; a pair turn gets the owner's), the
221
221
  mid-turn stop and the cap notice's words. The system prompt's line is the pair one once an active participant is
222
222
  stamped pair (`SessionParticipants#prompt_mode`); the participant page's `viewer.mode` is their own grant's
223
223
  (`ParticipantMode.live_for`).
@@ -323,7 +323,7 @@ and `ctx.turn_author` names it ([hooks](../hooks.md#participants-turns)).
323
323
  their $2.00 review cap: their comments are refused until you raise it (chi share cap ID p_… AMOUNT)." (a working
324
324
  session's: "Alex reached their $2.00 cap: their turns stop and are refused until you raise it (…)"). A review
325
325
  turn's spend counts from its end, so a review turn can finish past the cap. A pair turn stops mid-turn: before each
326
- generation (`KernelLoop#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
326
+ generation (`KernelLoop::Services#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
327
327
  (`Worker#participant_turn_stop`: `metrics.spend(author)` counts the running turn's finished generations) and, past
328
328
  the cap, stops the turn on the plugins' stop path (`HookRoutes#stop_turn`: "stopped the turn: Alex reached their
329
329
  $2.00 cap"), with the owner's notice once per cap. A generation's cost is known only when it ends, so a pair turn
@@ -469,7 +469,7 @@ fields: a field added to an event later reaches nobody until it is named there.
469
469
  its final assistant reply (`AnswerTail`: the turn's last assistant message with text) contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
470
470
  Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
471
471
  this matches the rendered text the page selects without treating different casing as the same claim. The quote may
472
- also be in the answer as the pages show it (`Engine#verify_quote_in?`, `Redact.text`: the session's
472
+ also be in the answer as the pages show it (`TurnStages::VerifyAnchors#quote_in?`, `Redact.text`: the session's
473
473
  folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
474
474
  page quotes it the same way (`owner_claim.js` `opts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
475
475
  sides alike), so a claim naming a path is taken on both. A fresh child
@@ -510,7 +510,7 @@ fields: a field added to an event later reaches nobody until it is named there.
510
510
  `pair` only for a grant stamped to act in a working session whose worker runs pair turns, else `review`). Every
511
511
  turn of a participant's child is theirs, whoever starts it (the owner typing into it, a continue, a reminder):
512
512
  `VerifyFreshOrigin#origin_for` sets the author in `Engine#begin_turn` and in the worker, so their boundary
513
- (`GuardrailWiring#turn_mode` reads the saved mode, never a lookup), their System 1 skip, their once-only asks to
513
+ (`GuardrailWiring#begin_turn` reads the saved mode, never a lookup), their System 1 skip, their once-only asks to
514
514
  the owner and the stamps on their messages follow. The child has no `participants.json`: its participant lookups
515
515
  (`Session#participants_source_id`) read the parent's, so a revoke, an expiry or the parent's archive refuses the
516
516
  child's next turn and stops a pair turn mid-turn, as in the parent; their audit lines go to the parent's
@@ -73,11 +73,15 @@ right-hand column happens.
73
73
  - The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
74
74
  and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
75
75
  config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
76
- beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request, so a mid-turn change
77
- would differ from the turn's prompt. Under `full` nothing is filtered and no line is added: the bytes are what
78
- they were before toolkits.
76
+ beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request from the turn's
77
+ `Tools::ToolSurface` (built once in `Engine#generate` from the Registry, under the layers the turn's prompt was
78
+ built with), so its requests declare what the turn's prompt does, and a mid-turn change shapes only the next
79
+ turn's surface. The native declarations, the not-loaded line and the kit prompt come from a surface too: each
80
+ `SystemPrompt` build reads the Registry once as `ToolSurface.for(registry, layers:)` (its `declared`, `left_out`
81
+ and `index_only`, its toolkit's prompt), so the three agree and match the turn's (engine.md, "ToolSurface"). Under `full` nothing is filtered and no line is added: the bytes are what they were before
82
+ toolkits.
79
83
  - A toolkit suggestion (`toolkit.suggest.mode: on`) is set before `apply_toolkit!` of the turn it was asked on
80
- (`Engine#settle_toolkit_suggestion` → `ToolkitBinding#suggest`), and no request goes out before that, so on a
84
+ (`TurnStages::ToolkitSuggestion#settle` → `ToolkitBinding#suggest`), and no request goes out before that, so on a
81
85
  session's first turn it costs nothing. Its **deferred switch** (a later prompt after `undecided`, within
82
86
  `toolkit.suggest.defer_turns` / `defer_tokens`) is engine-side, so `/toolkit`'s CacheConfirm doesn't apply: it
83
87
  changes the declared tools at the front of the request, and the server re-reads **system prompt + tools +
@@ -171,7 +175,7 @@ right-hand column happens.
171
175
  The next turn's prompt differs from the turn's last request at its tail: the previous answer's thinking is
172
176
  stripped (the models' specs require it). A hybrid model on llama.cpp can only resume from a restore point, so the
173
177
  next turn prefills from well before the end, more with every turn. `PromptWarmup` moves that work into the time
174
- the user reads: after a completed turn, `Engine#warm_up_next_turn` formats the next prompt up to where the new
178
+ the user reads: after a completed turn, `TurnStages::Warmup#after` formats the next prompt up to where the new
175
179
  user message starts (`KernelLoop#warmup_prompt`) and sends it in a thread (`Client#warm_up`: native
176
180
  `/completion`, `n_predict: 1`, not streamed, no retries, log purpose `warmup`, then a `model warmup` line with
177
181
  `cached=`, `prefilled=` and `prefill_ms=`).
@@ -24,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
24
24
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
25
25
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
26
26
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
27
- `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`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `card_stamps.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
+ `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`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `inline_widgets.js`, `widget_walk.js`, `widget_picks.js` and the `objectives_*.js` modules are new modules for that reason. The last four `block_*` draw fenced blocks (`block_watch.js` finds and watches them, `block_host.js` is its frame drawer for a bundle's renderers): [block-renderers.md](block-renderers.md). `inline_widgets.js` is its core drawer for `chi-ui` blocks, drawing `widget_walk.js`'s and `widget_picks.js`'s widgets in a shadow root styled by `widgets.css`: [inline-widgets.md](inline-widgets.md).
28
28
 
29
29
  ## Two pages, two models
30
30
 
@@ -102,7 +102,8 @@ when it has outputs and none is `sent`. An edit-only item, a failed run's file o
102
102
  pending: [Steering: add and unadd](#steering-add-and-unadd)) (`Engine#emit_working_set`, `EventFanout`'s):
103
103
  - after each `:generation_started`, in `build_stream_event_handler` (`EventFanout`), from the running turn's
104
104
  conversation (`Engine#live_conversation`, read on the turn's thread) under the turn's strategy
105
- (`@kernel.turn_settings.llm_context`). Both loops start a generation after `apply_llm_context!`, so the set is
105
+ (`TurnContext#settings`' `llm_context`; `EventFanout#working_set_llm_context`: the running turn's, the last turn's
106
+ until it has its own). Both loops start a generation after `apply_llm_context!`, so the set is
106
107
  what that request sends;
107
108
  - once in `Engine#end_turn`, after `replace_session_messages` and before the end event, from the kept messages (the
108
109
  turn-end apply has run by then);
data/docs/plugins.md CHANGED
@@ -1387,6 +1387,8 @@ block_renderers:
1387
1387
  it refuses with "No memories to build".
1388
1388
  - The first installed bundle (by name) with a language draws it. A bundle whose `requires_chi` this chi doesn't meet
1389
1389
  draws nothing.
1390
+ - `chi-ui` is reserved: chi draws it itself (inline widgets, [internals/inline-widgets.md](internals/inline-widgets.md)),
1391
+ so a manifest that lists it is refused.
1390
1392
  - **What it needs of the page:** `web.markdown` on (a default-off setting, and the commonmarker gem). With it off a
1391
1393
  fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
1392
1394
  the model: say so in the bundle's README.
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, decision-card, web-search, 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, inline-widgets, web-search, 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
@@ -6,7 +6,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
6
6
 
7
7
  **Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
8
8
 
9
- **Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`Engine#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
9
+ **Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`TurnStages::VerifyAnchors#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
10
10
 
11
11
  **Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
12
12
 
@@ -141,7 +141,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
141
141
  | `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
142
142
  | `scratch` | A `chi scratch` session, deleted when its terminal leaves. | creation |
143
143
  | `last_turn` | How the last turn ended (`LastTurn`), for a parent's wait and the web's notifications. | the Engine, at a turn's end |
144
- | `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `Engine#record_verdict`, `VerifyFresh` |
144
+ | `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `TurnStages::VerifyAnchors#record_verdict`, `VerifyFresh` |
145
145
  | `delegate` | Started by the `delegate` tool (a fork has a `parent_id` but isn't one). | `Tools::Delegate` via `spawn_session` |
146
146
  | `llm_context` | The session's own LLM context values (strategy, apply rule, budget), over the model's; `null` with none. | creation (`--llm-context`), `/llm-context`; copied by a fork and a continue, not a delegate child |
147
147
  | `continues` | The previous link of a chain; `null` otherwise. See [Session chains](#session-chains). | `SessionContinue` |
data/docs/testing.md CHANGED
@@ -260,7 +260,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
260
260
  | `reply_wait_looks.rb` | `ReplyWaitLooks`: counts a `ReplyWait`'s polls, so a step waits for the looks instead of a delay |
261
261
  | `stamps.rb` | *helper*: `next_stamp`, distinct ordered times for the lib's `now:` seams; the pinned history clock |
262
262
  | `task_dirs.rb` | `with_task_dir`: a tasks' folder of its own, every task stopped and its recorders joined after |
263
- | `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on) and `test_client` for Engine specs |
263
+ | `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on; its options are `KernelLoop::Services` fields), `attach_services` (swap some of a kernel's services), `run_context` and `test_client` for Engine specs |
264
264
  | `thinking_off.rb` | shared context "thinking off": `SAMAGOTCHI_THINKING_LEVEL=off` for the example |
265
265
  | `virtual_terminal.rb` | `VirtualTerminal`: just enough VT100 to check what a Screen leaves on the terminal |
266
266
  | `waiting.rb` | *helper*: `SpecWaiting` (`wait_for`, `wait_until`, `join!`, `pop!`, …), the test kit's helpers ([Waiting and time](#waiting-and-time)) |
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
+ autoload :InlineWidgets, File.expand_path("inline_widgets", __dir__)
4
5
  autoload :Log, File.expand_path("log", __dir__)
5
6
  autoload :VERSION, File.expand_path("version", __dir__)
6
7
  module MemoryBundle
@@ -34,6 +35,9 @@ module Samagotchi
34
35
  # A language a block can carry: a fence's first word, downcased
35
36
  # (MarkdownRenderer sets it as data-lang under this same pattern).
36
37
  LANG = /\A[a-z0-9_+-]{1,32}\z/
38
+ # Languages core draws itself: a bundle's renderer for one would be a
39
+ # second drawer for the same block.
40
+ RESERVED = [InlineWidgets::LANG].freeze
37
41
 
38
42
  module_function
39
43
 
@@ -60,6 +64,7 @@ module Samagotchi
60
64
  unless lang.is_a?(String) && lang.match?(LANG)
61
65
  raise Invalid, "block_renderers: lang #{lang.inspect} isn't a fence's language (lowercase letters, digits, _ + -; at most 32)"
62
66
  end
67
+ raise Invalid, "block_renderers: #{lang} is drawn by chi itself; a bundle can't render it" if RESERVED.include?(lang)
63
68
 
64
69
  load = data["load"]
65
70
  unless load.is_a?(Array) && !load.empty? && load.all? { |file| file.is_a?(String) && file.match?(MemoryBundle::Manifest::SCRIPT_FILE) }
@@ -421,7 +421,7 @@ module Samagotchi
421
421
  end
422
422
  end
423
423
  { "init" => registered.inits, "toolkit" => registered.toolkits, "tool source" => registered.tool_sources,
424
- "peer source" => registered.peer_sources }.each do |what, names|
424
+ "peer source" => registered.peer_sources, "prompt section" => registered.prompt_sections }.each do |what, names|
425
425
  names.each { |name| add(:info, :registered, plugin, "#{what} #{name}") }
426
426
  end
427
427
  end
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: dev
3
- version: 0.7.7
3
+ version: 0.7.8
4
4
  scope: system
5
5
  description: "Developer tools"
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.154.0"
7
+ requires_chi: ">= 0.155.0"
8
8
  includes:
9
9
  - known-names
10
10
  - mcp
@@ -18,3 +18,4 @@ includes:
18
18
  - ruby
19
19
  - model-notes
20
20
  - decision-card
21
+ - inline-widgets
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: inline-widgets
3
+ version: 0.1.0
4
+ scope: system
5
+ description: "Teaches the model the chi-ui blocks chi web draws: picks (rows of options that fill your reply) and walk (a scenario stepped through)"
6
+ trust_level: reviewed
7
+ plugin:
8
+ file: plugin.rb
9
+ sha256: sha256:24ad0c3f0ffb592826b4a7389111d76723e9f28ea4d94ab63abda7986d3311bb
10
+ # chi.prompt_section is 0.132.0's; the web draws chi-ui blocks only from the next release
11
+ requires_chi: ">= 0.155.0"
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The inline-widgets bundle (docs/bundles.md, The inline-widgets bundle): two
4
+ # prompt sections that teach the model the ```chi-ui blocks chi web draws
5
+ # (docs/internals/inline-widgets.md): picks, rows of options that fill the
6
+ # user's reply, and walk, a scenario stepped through. The drawing is core's; this
7
+ # bundle only says the blocks exist. Uninstalled, the prompt loses the sections
8
+ # and blocks already in a history still draw.
9
+ #
10
+ # The text is eval 0013's round-2 hint, split in two. It is static, so the
11
+ # prompt cache keeps: same text at every build. A delegate child's answer is
12
+ # read by its parent, not drawn for a user, and a one-shot (-p --non-interactive)
13
+ # prints to a terminal: neither gets a section.
14
+ class Plugin
15
+ PICKS = <<~TEXT
16
+ The web UI draws fenced ```chi-ui code blocks (YAML inside) as interactive widgets; the terminal shows their source. Use one whenever it helps the user more than prose: a widget replaces the long explanation, it doesn't sit beside one. Never use one for what markdown shows well (tables, lists, key-value). At most one widget per answer; keep the prose around it to a few sentences and don't repeat the widget's content.
17
+
18
+ picks: whenever your answer leaves the user two or more things to decide (review findings, a plan's open decisions, which of several fixes to do), even if they didn't ask you to list them. The user clicks one option per row, can add a note, and the UI fills their reply.
19
+ ```chi-ui
20
+ type: picks
21
+ title: What to do with the findings
22
+ rows:
23
+ - id: "1" # unique, short; the user's reply names it
24
+ text: Bridge drops the replay buffer on reset # the item, <= 80 chars
25
+ options: # 2-4 short options, one per line
26
+ - fix now
27
+ - skip
28
+ - later
29
+ recommended: fix now # exactly one of the options
30
+ why: loses events a slow tab still needs # optional, one short line
31
+ ```
32
+ Every row id unique; a row's options unique. Quote a value that starts with a quote or contains ": ".
33
+ TEXT
34
+
35
+ WALK = <<~TEXT
36
+ walk is the other ```chi-ui widget (the same rules: the web UI draws it, at most one widget per answer): a scenario the user steps through one step at a time: who does what, what the user sees, what happens behind it. Use it for "when does this happen / how does this work / what does the user go through" questions.
37
+ ```chi-ui
38
+ type: walk
39
+ title: A tab reconnects
40
+ actors: [user, web, worker] # 2-5 short ids, unique
41
+ steps:
42
+ - id: s1 # unique
43
+ actor: user # one of actors
44
+ does: reopens the laptop # the action, <= 80 chars
45
+ sees: the page shows "reconnecting" # optional: what the user sees at this step
46
+ behind: the tab sends GET /events with Last-Event-ID # optional: what happens out of sight
47
+ - id: s2
48
+ actor: worker
49
+ does: checks the event id
50
+ cases: # optional: the step forks. List EVERY case, the usual one too
51
+ - when: the id is still buffered
52
+ goto: s3
53
+ - when: the id is too old
54
+ goto: s4
55
+ - id: s3
56
+ actor: worker
57
+ does: replays the missed events
58
+ next: end # optional: where to go after this step (default: the step below); end ends the walk here
59
+ - id: s4
60
+ actor: web
61
+ does: reloads the whole session
62
+ sees: the page redraws from the top
63
+ ```
64
+ A step needs `does` or `sees` (or both). Steps run in order unless `next` or `cases` say otherwise. Every id unique; every goto names a step; a next names another step or is `end`. Quote a value that starts with a quote or contains ": ".
65
+ TEXT
66
+
67
+ def register(chi)
68
+ chi.prompt_section("picks") { |ctx| PICKS if drawn?(ctx) }
69
+ chi.prompt_section("walk") { |ctx| WALK if drawn?(ctx) }
70
+ end
71
+
72
+ private
73
+
74
+ # Whether the session's answers reach a web page that draws them.
75
+ def drawn?(ctx) = ctx.frontend != :one_shot && !ctx.delegate?
76
+ end
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.154.1
3
+ version: 0.155.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files: