samagotchi 0.156.0 → 0.158.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 +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
data/docs/sub-agent.md
CHANGED
|
@@ -81,8 +81,15 @@ With `--format json` stdout is one JSON object (see
|
|
|
81
81
|
when the wait is about a known turn, it includes `turn_id`. Use
|
|
82
82
|
`chi send --wait --format json --turn <turn_id> <session_id>` to wait for
|
|
83
83
|
that exact turn again after exit 4, or after exit 3 if someone else will
|
|
84
|
-
answer the question. A turn id can name the running turn, a queued turn
|
|
85
|
-
the last turn that ended
|
|
84
|
+
answer the question. A turn id can name the running turn, a queued turn,
|
|
85
|
+
the last turn that ended, or a message a running turn merged (waited for as
|
|
86
|
+
that turn); any other id is an error. A turn nobody typed (a
|
|
87
|
+
reminder, an attached context change, a finished task, a delegate report: a
|
|
88
|
+
wake turn) has an id its worker made, `wake-` and 16 hex digits, so
|
|
89
|
+
`chi send --wait` with no message names it too and `--turn` waits for it. A
|
|
90
|
+
message merged into a running turn (a steer) gets that turn's reply or end;
|
|
91
|
+
its `turn_id` stays the message's own, and `--turn` with it waits for that
|
|
92
|
+
turn again.
|
|
86
93
|
|
|
87
94
|
```json
|
|
88
95
|
{"status":"question","session_id":"297da360-…","question":{"id":"3f1c…","kind":"question",
|
|
@@ -198,6 +205,14 @@ answers from any local process.
|
|
|
198
205
|
- If the worker dies while a question waits, the question goes with it.
|
|
199
206
|
`chi answer` then exits 1 and says to send the task again
|
|
200
207
|
(`chi send --wait -m "…" ID`); a wait ends with `worker_gone` (exit 1).
|
|
208
|
+
- `worker_gone` follows one rule everywhere (`ReplyWait::GONE_AFTER`): no
|
|
209
|
+
worker has owned the session for 5 s while the wait runs (it died without
|
|
210
|
+
marking the session). A worker chi just started is awaited first (`chi send
|
|
211
|
+
--new`, a `delegate` child), so a slow boot isn't read as a death; a
|
|
212
|
+
delegate child whose worker never starts is reported `worker_gone` ("its
|
|
213
|
+
worker did not start") once the boot limit (30 s) passes. That await is
|
|
214
|
+
part of the `delegate` call's wait: the call's `timeout` bounds it (the
|
|
215
|
+
child then reads `running`) and the parent's Stop ends it.
|
|
201
216
|
- A follow-up (`chi send --wait -m "…" ID`) to a session waiting for an answer
|
|
202
217
|
isn't sent: it exits 1 with the `chi answer` command for the question. The
|
|
203
218
|
step-limit question is the exception: it waits between turns, so a message
|
data/docs/testing.md
CHANGED
|
@@ -75,6 +75,47 @@ spec/engine_turn_golden_spec.rb`, then read `git diff spec/fixtures/turn_goldens
|
|
|
75
75
|
to `tmp/turn_goldens/<golden>.actual` and names the `diff` to run. Changes to the turn path (engine, kernel loop, chat loop,
|
|
76
76
|
tool runner, gate, guardrail wiring, system prompt, tool declarations, registry) pick it in `script/affected-specs`.
|
|
77
77
|
|
|
78
|
+
The model-setting characterizations (`spec/engine_effective_model_characterization_spec.rb`,
|
|
79
|
+
`spec/engine_session_model_settings_characterization_spec.rb`, `spec/web/app_session_settings_characterization_spec.rb`
|
|
80
|
+
over `spec/fixtures/effective_model/`, and `spec/model_setting_precedence_characterization_spec.rb`'s ladders over
|
|
81
|
+
`spec/fixtures/model_setting_precedence/`) pin the settings on the code as it was before the `ModelLookup` refactor
|
|
82
|
+
(`UPDATE_GOLDENS=1` regenerates them after an intended change only). They are retired after their refactor: the
|
|
83
|
+
next round folds the first three into one wiring example each in `effective_model_spec.rb`,
|
|
84
|
+
`session_model_settings_spec.rb` and `session_setting_spec.rb`, and the ladders the round after into the setting
|
|
85
|
+
modules' and `model_lookup_spec.rb`.
|
|
86
|
+
|
|
87
|
+
The session file goldens (`spec/session_file_goldens_spec.rb`, `spec/fixtures/session_goldens/`) pin the bytes `Session#save`
|
|
88
|
+
writes: every field away from its default, a new session, a newer chi's file, an old chi's file in a git repo, every key
|
|
89
|
+
present as `null`, the plain writers' raw values, a running turn's checkpoint and the scrub (the `*_in.json` files are
|
|
90
|
+
hand-written inputs, never regenerated); and the file each path that makes or rewrites a session writes (a fork, a
|
|
91
|
+
continue, a delegate child, a plain spawn, a review, a Verify fresh child, a resume, a respawn, an archive), with
|
|
92
|
+
`carry_matrix.json`, what each keeps of its source. They are pinned before Session's fields became one table and are kept
|
|
93
|
+
as the file format's pins, not retired: a diff is a change to every session file on disk, so only a commit that means to
|
|
94
|
+
change the format regenerates them (`UPDATE_GOLDENS=1 bundle exec rspec spec/session_file_goldens_spec.rb`).
|
|
95
|
+
|
|
96
|
+
`UPDATE_GOLDENS=1` is the characterization specs' variable (the prompt and turn goldens above use `UPDATE_PROMPTS`):
|
|
97
|
+
the model-setting ones above, the session file goldens, claude-peer's goldens and the bundle-state corpus below. Regenerate only for an intended
|
|
98
|
+
change, then read the diff.
|
|
99
|
+
|
|
100
|
+
The bundle-state corpus (`spec/samagotchi/memory_bundle/file_state_corpus/`, `spec/fixtures/bundle_states/`) pins what
|
|
101
|
+
every bundle file state does under every command that reads or writes it. Each case in `memory_cases.yml` (a memory:
|
|
102
|
+
owned, edited, behind, missing, dropped, the user's own, another bundle's too…) and `asset_cases.yml` (a rule, the
|
|
103
|
+
plugin, a script, an older install's class-file hook) is written directly as a starting tree, in the 0.155 and the 0.149 record shape, and run
|
|
104
|
+
through install, `--force`, `--dry-run`, upgrade and its flags, chi update's plan, Remerge, the system bundle's restore,
|
|
105
|
+
uninstall and status. A cell holds only what a user or the next command sees (the bytes, the trash, base and checksum,
|
|
106
|
+
the mark, the summary's word, status's label, the warnings), never the Installer's result strings. `expect/*.yml` are
|
|
107
|
+
generated (`UPDATE_GOLDENS=1 bundle exec rspec spec/samagotchi/memory_bundle/file_state_corpus/`, which also writes
|
|
108
|
+
`tmp/bundle_states/review.<file>.txt`, every cell in words); a mismatch names the case, command and shape and writes
|
|
109
|
+
`tmp/bundle_states/<file>.yml.actual` to diff. A regeneration asserts nothing, so run the folder again without
|
|
110
|
+
`UPDATE_GOLDENS`: that run checks the new cells against `cases_spec.rb`, which holds what every regeneration keeps. No
|
|
111
|
+
user bytes (neither the bundle's text nor what the record vouches for, its checksum first) are changed or removed without
|
|
112
|
+
a copy in the trash first; the two record shapes give one answer except on a marked case (an allowlist entry needs a
|
|
113
|
+
note); and for every asset, chi update's plan lists as replaced exactly the files the upgrade copies to the trash. To stay
|
|
114
|
+
under 3 s, `upgrade_spec.rb` runs the 0.149 shape for marked cases only (`MARKED_ONLY_149`), where the shapes can differ. A regenerated cell is
|
|
115
|
+
reviewed: a refactor that turns the corpus red stops and asks, and only a commit that means to change a cell
|
|
116
|
+
regenerates, its changed cells read one by one; a cell that looks wrong today carries a `divergent:` tag and a note in
|
|
117
|
+
its case.
|
|
118
|
+
|
|
78
119
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
|
|
79
120
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
80
121
|
`question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
@@ -124,7 +165,15 @@ Each level owns its own share; a detail is asserted once, at the lowest level th
|
|
|
124
165
|
- **e2e** is for real-browser behaviour (layout, focus, reload, two tabs), one happy path per Ruby↔web mechanism and
|
|
125
166
|
a few journeys. Before adding a test, extend an existing journey's step; a variant goes to node, a Ruby↔web shape
|
|
126
167
|
variant to a shared fixture (`spec/fixtures/composer_routes.json`). A journey names its stages with `test.step`
|
|
127
|
-
(the report names the one that failed: web.spec.js's notifications).
|
|
168
|
+
(the report names the one that failed: web.spec.js's notifications). A turn test ends with
|
|
169
|
+
`expectSameAfterReload` (spec/e2e/support/history_shape.js): the history's shape live, then after a reload, the
|
|
170
|
+
same (live is the truth). Its allowlist is `history_shape.js ACCEPTED`, each entry with its reason; a test names
|
|
171
|
+
the entries its scenario has (only deliberate ones are left). A drawn turn's variants are node's: the shared
|
|
172
|
+
scenarios in `spec/shared/turn_drawings/` (written from `spec/support/turn_drawing_scenarios.rb`;
|
|
173
|
+
`UPDATE_TURN_DRAWINGS=1 bundle exec rspec spec/bridge/turn_drawings_spec.rb spec/web/session_show_drawings_spec.rb`
|
|
174
|
+
rewrites their lines and show after an intended change) feed `turn_drawing_parity.test.js`, which draws each one
|
|
175
|
+
live (the real stream handlers and stage) and from its drawing and compares the shapes with no allow list. The
|
|
176
|
+
`lan` project's chi web is on the LAN only
|
|
128
177
|
when the machine has a private IPv4: without one its LAN tests skip and the participant review journey runs on
|
|
129
178
|
loopback.
|
|
130
179
|
|
|
@@ -254,6 +303,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
|
|
|
254
303
|
| `lock_holder.rb` | *helper*: `spawn_lock_holder(session_dir)`, a stand-in worker process holding a session's owner lock, which says so on a pipe |
|
|
255
304
|
| `integration_server.rb` | *helper*: the live model server `:integration` examples use, from `SAMAGOTCHI_INTEGRATION_*` ([Integration specs](#integration-specs)) |
|
|
256
305
|
| `mcp_bundle.rb` | The shipped mcp bundle's path and the fake MCP server its specs start (`MCP_FAKE_COMMAND`, no RubyGems) |
|
|
306
|
+
| `model_setting_ladders.rb` | shared context "model setting ladders": per setting a config, env and CLI with every layer set, peeled top down (`peel`), against `spec/fixtures/model_setting_precedence/` |
|
|
257
307
|
| `no_console.rb` | Loaded first (`ruby -r`) by spawned bin/chi children: `IO.console` is nil, so output never wraps at a terminal's width |
|
|
258
308
|
| `recording_surface.rb` | `RecordingSurface`: a `TerminalUI::Surface` that records commits and slots instead of drawing |
|
|
259
309
|
| `remote_backend.rb` | `RemoteBackend`: `SessionAccess::Remote` against chi web's Rack app in this process (no socket, LAN token gate) |
|
|
@@ -271,7 +321,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
|
|
|
271
321
|
|
|
272
322
|
## Bundle specs: the test kit
|
|
273
323
|
|
|
274
|
-
A shipped bundle's plugin
|
|
324
|
+
A shipped bundle's plugin is tested through the bundle test kit (`samagotchi/testing`,
|
|
275
325
|
[plugins.md](plugins.md#testing-a-bundle-experimental)): `shipped_bundle("check-in", settings: …)` loads it through
|
|
276
326
|
chi's own loaders, registries, `ctx` and guardrail Gate, with the session, the hook helpers and `ctx.sessions`
|
|
277
327
|
recorded. No fake `chi` or `ctx`: what the kit can't express is a kit change (with a kit spec), not a local fake.
|
|
@@ -290,13 +340,13 @@ The kit's own specs, in `spec/samagotchi/testing/`, keep it and the bundles hone
|
|
|
290
340
|
- `spec/samagotchi/testing/contract_spec.rb`: the kit sets every `Plugin::Host` and `Hooks::Runtime` member,
|
|
291
341
|
`Testing::Sessions` has `Plugin::Sessions`' methods and parameters, and each `ctx` helper lands in its recorder. A
|
|
292
342
|
member added to the Host fails it until the kit has it.
|
|
293
|
-
- `spec/samagotchi/testing/shipped_bundles_spec.rb`: each shipped bundle with a plugin
|
|
343
|
+
- `spec/samagotchi/testing/shipped_bundles_spec.rb`: each shipped bundle with a plugin registers the same
|
|
294
344
|
commands, tools, hooks, inits, toolkits and sources loaded by the kit as installed and loaded by chi
|
|
295
345
|
(`Testing::Install`).
|
|
296
346
|
- `spec/samagotchi/bundle_command/check_spec.rb`: `chi bundle check` passes every shipped bundle with no errors (one
|
|
297
347
|
example per bundle), so a shipped bundle that wouldn't install or load cleanly for a user fails here too. Run it
|
|
298
348
|
by hand on one with `ruby -Ilib bin/chi bundle check lib/samagotchi/bundles/NAME`.
|
|
299
|
-
- `spec/samagotchi/testing/boundary_spec.rb`, the boundary lint: each bundle's Ruby (plugin,
|
|
349
|
+
- `spec/samagotchi/testing/boundary_spec.rb`, the boundary lint: each bundle's Ruby (plugin, scripts, specs)
|
|
300
350
|
reaches chi only through `Samagotchi::Testing::Boundary::ALLOWED` (the "Stable for bundles" list) and that
|
|
301
351
|
bundle's `Boundary::EXCEPTIONS` entry, each with why. A new internal reference fails it, and so does an exception
|
|
302
352
|
no longer used. Fix the reference (the kit or a stable API), or add the exception with its why.
|
|
@@ -334,7 +384,8 @@ files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*
|
|
|
334
384
|
`lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
|
|
335
385
|
docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
|
|
336
386
|
file's layer (any `foo/*.rb` → `spec/foo_golden_spec.rb` when there is one; `turn_flow.rb` and `turn_note.rb`, whose
|
|
337
|
-
words the attached TUI prints, pick its `event_renderer` and `attached_loop` specs by a rule
|
|
387
|
+
words the attached TUI prints, pick its `event_renderer` and `attached_loop` specs by a rule; `memory_bundle/*.rb`,
|
|
388
|
+
`bundle_command.rb`, `memory_paths.rb`, `model_overlay.rb` and the corpus's fixtures pick the bundle-state corpus). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
|
|
338
389
|
effective-model, request, prompt-snapshot and full system-prompt goldens because they hash the request's prompt and
|
|
339
390
|
tools. Then come the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
340
391
|
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
@@ -138,7 +138,8 @@ module Samagotchi
|
|
|
138
138
|
end
|
|
139
139
|
# Answered here, or no longer open (answered elsewhere, or a newer
|
|
140
140
|
# question): what comes next either way.
|
|
141
|
-
wait_for_reply(id,
|
|
141
|
+
wait_for_reply(id, target: ReplyWait::Target.answer(start),
|
|
142
|
+
timeout: options[:timeout])
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
# The labels to send. A number is an option's place (1-based); an
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "time"
|
|
6
6
|
require_relative "atomic_file"
|
|
7
|
+
require_relative "session_paths"
|
|
7
8
|
|
|
8
9
|
require_relative "session"
|
|
9
10
|
|
|
@@ -46,7 +47,7 @@ module Samagotchi
|
|
|
46
47
|
|
|
47
48
|
# @return [Boolean] whether it was archived (and is not now)
|
|
48
49
|
def self.unarchive(session_id, state_dir:)
|
|
49
|
-
return false unless archived?(
|
|
50
|
+
return false unless archived?(SessionPaths.session_dir(session_id, state_dir: state_dir))
|
|
50
51
|
|
|
51
52
|
write(session_id, { "unarchived_at" => Time.now.iso8601(3) }, state_dir: state_dir)
|
|
52
53
|
end
|
|
@@ -63,9 +64,9 @@ module Samagotchi
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def self.write(session_id, record, state_dir:)
|
|
66
|
-
return false unless File.exist?(
|
|
67
|
+
return false unless File.exist?(SessionPaths.session_file(session_id, state_dir: state_dir))
|
|
67
68
|
|
|
68
|
-
dir =
|
|
69
|
+
dir = SessionPaths.session_dir(session_id, state_dir: state_dir)
|
|
69
70
|
FileUtils.mkdir_p(dir)
|
|
70
71
|
path = File.join(dir, FILE)
|
|
71
72
|
AtomicFile.write(path, JSON.generate(record))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "rbconfig"
|
|
4
4
|
require_relative "config"
|
|
5
5
|
require_relative "model_profile"
|
|
6
|
+
require_relative "host_address"
|
|
6
7
|
require_relative "host_registry"
|
|
7
8
|
require_relative "host_keys"
|
|
8
9
|
require_relative "chi_home"
|
|
@@ -342,7 +343,7 @@ module Samagotchi
|
|
|
342
343
|
# The "loading the model?" hint fits a local server (a llama.cpp loading
|
|
343
344
|
# weights); a remote provider's slow answer is the network, not a load.
|
|
344
345
|
def loading_hint?(result)
|
|
345
|
-
!
|
|
346
|
+
!HostAddress.remote_address?(result.candidate.scheme, result.candidate.host)
|
|
346
347
|
end
|
|
347
348
|
|
|
348
349
|
# 0 written, 1 written but not as planned, nil with @exit on a refusal.
|
|
@@ -489,7 +490,7 @@ module Samagotchi
|
|
|
489
490
|
when :skipped then "skipped (#{result.error})"
|
|
490
491
|
else "failed (#{result.error})"
|
|
491
492
|
end
|
|
492
|
-
@stdout.puts("system bundle: #{text}")
|
|
493
|
+
@stdout.puts("system bundle: #{[text, *result.warnings].join("; ")}")
|
|
493
494
|
result.status != :failed
|
|
494
495
|
end
|
|
495
496
|
|
|
@@ -14,9 +14,10 @@ module Samagotchi
|
|
|
14
14
|
# an entry says how many it came after (+past_idless+, #idless_after), and
|
|
15
15
|
# which one that is when it has an id (+after_opening+, #after_opening).
|
|
16
16
|
#
|
|
17
|
-
# The same place as a stamp (#opening_of): the id of
|
|
18
|
-
# comes after (a prompt's turn_id, a line's
|
|
19
|
-
# finds by its bubble, not by counting.
|
|
17
|
+
# The same place as a stamp (#opening_of, Steer.opening_stamp): the id of
|
|
18
|
+
# the opening the entry comes after (a prompt's turn_id, a line's
|
|
19
|
+
# opening_id), which the page finds by its bubble, not by counting.
|
|
20
|
+
# Pure: no Engine, no file.
|
|
20
21
|
class CardPlaces
|
|
21
22
|
# @param turn_log [Array<String>] the turn ids, oldest first
|
|
22
23
|
# @param messages [Array<Hash>] the messages the page draws
|
|
@@ -114,10 +115,7 @@ module Samagotchi
|
|
|
114
115
|
position >= 0 ? @last_at_or_before[position] : nil
|
|
115
116
|
end
|
|
116
117
|
|
|
117
|
-
def stamp_of(message)
|
|
118
|
-
id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
|
|
119
|
-
id.is_a?(String) && !id.empty? ? id : nil
|
|
120
|
-
end
|
|
118
|
+
def stamp_of(message) = Steer.opening_stamp(message)
|
|
121
119
|
|
|
122
120
|
def idless_run(opening)
|
|
123
121
|
@ids.drop(opening + 1).take_while(&:nil?).size
|
|
@@ -147,6 +147,9 @@ module Samagotchi
|
|
|
147
147
|
stamp = places&.opening_of(entry)
|
|
148
148
|
out[:opening_id] = stamp if stamp
|
|
149
149
|
out[:current] = current
|
|
150
|
+
# Whose row it is (its turn's id, +of_turn+): the page leaves a drawn
|
|
151
|
+
# turn's own rows and cards to its drawing (Bridge::TurnDrawings).
|
|
152
|
+
out[:of_turn] = entry[:turn_id] if shown[:in_turn] && entry[:turn_id]
|
|
150
153
|
out[:during] = true if entry[:during]
|
|
151
154
|
out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
|
|
152
155
|
out[:label] = "from #{entry[:command]}" if entry[:command]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../events"
|
|
4
4
|
require_relative "../kernel_loop"
|
|
5
|
+
require_relative "../log"
|
|
5
6
|
require_relative "../steer"
|
|
6
7
|
require_relative "../tool_output_cut"
|
|
7
8
|
require_relative "../tool_row_fields"
|
|
@@ -23,10 +24,17 @@ module Samagotchi
|
|
|
23
24
|
# its head and tail: a reload's cap, so a joined row is the row a
|
|
24
25
|
# reload draws
|
|
25
26
|
# @param clock [#call] monotonic seconds: a tool call's duration
|
|
27
|
+
# @param on_turn_end [#call, nil] (turn, end_event): the turn's final
|
|
28
|
+
# fold (a deep copy, #current_turn's shape without elapsed_ms) and its
|
|
29
|
+
# end event, at the end event, before the turn is cleared (the Bridge's
|
|
30
|
+
# TurnDrawings#append). It runs under this accumulator's mutex and the
|
|
31
|
+
# SessionObserver's lock: it must never call back in here. An error is
|
|
32
|
+
# logged and the fold goes on.
|
|
26
33
|
def initialize(max_output_chars: ToolOutputCut::WEB_PREVIEW_CHARS,
|
|
27
|
-
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
34
|
+
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, on_turn_end: nil)
|
|
28
35
|
@max_output_chars = max_output_chars
|
|
29
36
|
@clock = clock
|
|
37
|
+
@on_turn_end = on_turn_end
|
|
30
38
|
@mutex = Mutex.new
|
|
31
39
|
@turn = nil
|
|
32
40
|
# When each running tool call started, by [iteration, call_index].
|
|
@@ -47,13 +55,14 @@ module Samagotchi
|
|
|
47
55
|
|
|
48
56
|
# @return [Hash, nil] a copy of the turn in progress: prompt, turn_id, origin,
|
|
49
57
|
# continue, at (its wall time), ordered parts (generation / thinking / text / tool /
|
|
50
|
-
# input / steer / reminder / notice), pending_question and the last event_seq folded
|
|
58
|
+
# input / steer / reminder / notice / card), pending_question and the last event_seq folded
|
|
51
59
|
# in. A finished tool part keeps the live row's action and duration_ms
|
|
52
60
|
# too, a running one the time it has run so far (elapsed_ms), so a
|
|
53
61
|
# join times its row from its start. A notice part holds the event
|
|
54
62
|
# of one of the turn's rows (TurnNotice: a hook's notice, an
|
|
55
63
|
# empty-answer retry, a question and its answer), which a UI replays
|
|
56
|
-
# through its live handler.
|
|
64
|
+
# through its live handler. A card part is a card the turn showed in
|
|
65
|
+
# its step (in_turn), its latest version in the place of its first.
|
|
57
66
|
def current_turn
|
|
58
67
|
@mutex.synchronize do
|
|
59
68
|
next nil unless @turn
|
|
@@ -132,10 +141,13 @@ module Samagotchi
|
|
|
132
141
|
# the live activity: action, tool, params, status), a merged_input
|
|
133
142
|
# (the one synthetic type) per merged prompt, a steer-only
|
|
134
143
|
# pending_input_merged per steer, a reminder_injected, and a notice
|
|
135
|
-
# part's own event.
|
|
144
|
+
# part's own event. Card parts only with +cards+ (a finished turn's
|
|
145
|
+
# drawing, chi web's unfold of it): a running turn's join gets its cards
|
|
146
|
+
# from CardStore's snapshot.
|
|
136
147
|
# @param started_at [String, nil] the turn's start, on turn_started
|
|
148
|
+
# @param cards [Boolean] replay card parts as their card events
|
|
137
149
|
# @return [Array<Hash>] symbol keys and types; [] with no turn
|
|
138
|
-
def self.replay_events(turn, started_at: nil)
|
|
150
|
+
def self.replay_events(turn, started_at: nil, cards: false)
|
|
139
151
|
return [] unless turn
|
|
140
152
|
|
|
141
153
|
started = { type: :turn_started, prompt: turn[:prompt], origin: turn[:origin], continue: !!turn[:continue] }
|
|
@@ -171,18 +183,73 @@ module Samagotchi
|
|
|
171
183
|
when "notice"
|
|
172
184
|
close_text.call
|
|
173
185
|
notice = part[:event]
|
|
174
|
-
|
|
186
|
+
# A damaged drawing's type may be anything: only a name replays.
|
|
187
|
+
if notice.is_a?(Hash) && (notice[:type].is_a?(String) || notice[:type].is_a?(Symbol))
|
|
188
|
+
events << notice.merge(type: notice[:type].to_sym)
|
|
189
|
+
end
|
|
175
190
|
when "steer"
|
|
176
191
|
close_text.call
|
|
177
192
|
events << { type: :pending_input_merged, count: 0, content: nil, steers: [{ source: part[:source], text: part[:text] }] }
|
|
193
|
+
when "card"
|
|
194
|
+
next unless cards && part[:event].is_a?(Hash)
|
|
195
|
+
|
|
196
|
+
close_text.call
|
|
197
|
+
events << part[:event].merge(type: :card, in_turn: true)
|
|
178
198
|
end
|
|
179
199
|
end
|
|
180
200
|
events
|
|
181
201
|
end
|
|
182
202
|
|
|
203
|
+
# The page's unfold of a session view (chi web's show sends it as
|
|
204
|
+
# `replay`; turn_events.js snapshotEvents was its JS twin): the turn in
|
|
205
|
+
# progress as #replay_events has it, its turn_started with what the
|
|
206
|
+
# page also reads (its wall time `at`, the run's `cut_after`, and every
|
|
207
|
+
# author key, a pair turn's participant_mode too), then a turn_enqueued
|
|
208
|
+
# per prompt queued behind it and a command_queued per command waiting
|
|
209
|
+
# for its end (spec/shared/turn_snapshot.json pins both sides). The TUI
|
|
210
|
+
# keeps #replay_events.
|
|
211
|
+
# @param current_turn [Hash, nil] #current_turn's shape, symbol keys
|
|
212
|
+
# (a snapshot's: .from_wire)
|
|
213
|
+
# @param queued [Array<Hash>] #queued's entries
|
|
214
|
+
# @param queued_commands [Array<Hash>] #queued_commands' entries
|
|
215
|
+
# @return [Array<Hash>] symbol keys and types
|
|
216
|
+
def self.join_events(current_turn:, queued: [], queued_commands: [], started_at: nil)
|
|
217
|
+
events = replay_events(current_turn, started_at: started_at)
|
|
218
|
+
if (started = events.first)
|
|
219
|
+
started[:at] = current_turn[:at] if current_turn[:at].is_a?(String)
|
|
220
|
+
started[:cut_after] = current_turn[:cut_after] unless current_turn[:cut_after].nil?
|
|
221
|
+
Steer::AUTHOR_KEYS.each { |key| started.delete(key) }
|
|
222
|
+
Steer::AUTHOR_KEYS.each { |key| started[key] = current_turn[key] if current_turn[key] } if current_turn[:author]
|
|
223
|
+
end
|
|
224
|
+
Array(queued).each do |entry|
|
|
225
|
+
enqueued = { type: :turn_enqueued, enqueued_id: entry[:enqueued_id], client_id: entry[:client_id], prompt: entry[:prompt] }
|
|
226
|
+
enqueued[:delivery] = entry[:delivery] if entry[:delivery]
|
|
227
|
+
enqueued[:author] = entry[:author] if entry[:author]
|
|
228
|
+
events << with_images(enqueued, entry[:images])
|
|
229
|
+
end
|
|
230
|
+
Array(queued_commands).each do |entry|
|
|
231
|
+
command = { type: :command_queued, command_id: entry[:command_id], client_id: entry[:client_id], line: entry[:line],
|
|
232
|
+
waits: "turn_end" }
|
|
233
|
+
command[:author] = entry[:author] if entry[:author]
|
|
234
|
+
events << command
|
|
235
|
+
end
|
|
236
|
+
events
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# A Bridge snapshot's part (JSON: string keys) as the accumulator's own
|
|
240
|
+
# shapes: symbol keys all the way down, values as they are.
|
|
241
|
+
def self.from_wire(value)
|
|
242
|
+
case value
|
|
243
|
+
when Hash then value.to_h { |key, item| [key.is_a?(String) ? key.to_sym : key, from_wire(item)] }
|
|
244
|
+
when Array then value.map { |item| from_wire(item) }
|
|
245
|
+
else value
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
183
249
|
def self.tool_replay_events(part)
|
|
184
250
|
call = { iteration: part[:iteration], call_index: part[:call_index], tool: part[:tool] }
|
|
185
251
|
call[:label] = part[:label] if part[:label]
|
|
252
|
+
call[:tool_id] = part[:tool_id] if part[:tool_id]
|
|
186
253
|
# Its row fields (ToolRowFields), each in its place: title and, for
|
|
187
254
|
# an alias's call, the name the model used, also in the activity.
|
|
188
255
|
title = part.slice(*ToolRowFields::ACTIVITY_KEYS).compact
|
|
@@ -296,6 +363,7 @@ module Samagotchi
|
|
|
296
363
|
|
|
297
364
|
fold_turn_event(event)
|
|
298
365
|
if Events::TURN_END.include?(type)
|
|
366
|
+
turn_ended(event)
|
|
299
367
|
@turn = nil
|
|
300
368
|
@merged_origins = []
|
|
301
369
|
@working_set = nil
|
|
@@ -326,6 +394,8 @@ module Samagotchi
|
|
|
326
394
|
part = { kind: "tool", iteration: event[:iteration], call_index: event[:call_index],
|
|
327
395
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
328
396
|
part[:label] = event[:label] if event[:label]
|
|
397
|
+
# The id its output is saved under (ToolIds; not a row field).
|
|
398
|
+
part[:tool_id] = event[:tool_id] if event[:tool_id]
|
|
329
399
|
ToolRowFields::KEYS.each { |key| part[key] = event[key] if event[key] }
|
|
330
400
|
parts << part
|
|
331
401
|
@tool_started_at[[event[:iteration], event[:call_index]]] = @clock.call
|
|
@@ -342,16 +412,26 @@ module Samagotchi
|
|
|
342
412
|
# Only System 1's auto-allow is drawn on the row (the web's label).
|
|
343
413
|
guardrail = event.dig(:activity, :guardrail)
|
|
344
414
|
tool[:guardrail] = guardrail if guardrail.is_a?(Hash) && guardrail[:decided_by] == "system one"
|
|
345
|
-
|
|
415
|
+
# The event's web preview (ToolRunner: the reload's cut) when it
|
|
416
|
+
# has one; an older worker's event is cut here the same way.
|
|
417
|
+
tool[:output] = if event[:preview] then event[:preview].to_s.dup
|
|
418
|
+
elsif capped then ToolOutputCut.preview(output, cap: @max_output_chars, tool: event[:tool])
|
|
419
|
+
else output.dup
|
|
420
|
+
end
|
|
346
421
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
347
422
|
tool[:images] = event[:images] if event[:images]
|
|
348
423
|
tool[:diff] = event[:diff] if event[:diff]
|
|
349
424
|
action = event.dig(:activity, :action)
|
|
350
425
|
tool[:action] = action.to_s if action
|
|
351
|
-
#
|
|
352
|
-
#
|
|
426
|
+
# The event's (ToolRunner's: the figure every UI shows); without
|
|
427
|
+
# one (an older worker's event), as the live row times it: from
|
|
428
|
+
# tool_call_started, less an approval wait.
|
|
353
429
|
started_at = @tool_started_at.delete([event[:iteration], event[:call_index]])
|
|
354
|
-
|
|
430
|
+
if event[:duration_ms].is_a?(Integer)
|
|
431
|
+
tool[:duration_ms] = event[:duration_ms]
|
|
432
|
+
elsif started_at
|
|
433
|
+
tool[:duration_ms] = [((@clock.call - started_at) * 1000) - event[:waited_ms].to_f, 0].max.round
|
|
434
|
+
end
|
|
355
435
|
when :pending_input_merged
|
|
356
436
|
# A steer-only merge (count 0) has no user part: the origins stay
|
|
357
437
|
# for the user lines' own merge.
|
|
@@ -366,6 +446,8 @@ module Samagotchi
|
|
|
366
446
|
end
|
|
367
447
|
when :working_set_updated
|
|
368
448
|
@working_set = { items: event[:items], at: event[:at] }
|
|
449
|
+
when :card
|
|
450
|
+
fold_card(parts, event) if event[:in_turn]
|
|
369
451
|
when :reminder_injected
|
|
370
452
|
parts << { kind: "reminder", reminders: Array(event[:reminders]).map(&:dup) }
|
|
371
453
|
when :question_requested
|
|
@@ -382,6 +464,27 @@ module Samagotchi
|
|
|
382
464
|
parts << { kind: "notice", event: TurnNotice.slice(event) } if TurnNotice.notice?(event)
|
|
383
465
|
end
|
|
384
466
|
|
|
467
|
+
# The card fields a UI draws (CardStore keeps the same), a copy.
|
|
468
|
+
CARD_FIELDS = %i[id source title body level actions].freeze
|
|
469
|
+
private_constant :CARD_FIELDS
|
|
470
|
+
|
|
471
|
+
# A card the turn shows in its step; one with an earlier card's id
|
|
472
|
+
# replaces that part where it was.
|
|
473
|
+
def fold_card(parts, event)
|
|
474
|
+
card = { kind: "card", id: event[:id], event: Marshal.load(Marshal.dump(event.slice(*CARD_FIELDS))) }
|
|
475
|
+
index = parts.index { |part| part[:kind] == "card" && part[:id] == card[:id] }
|
|
476
|
+
index ? parts[index] = card : parts << card
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# The turn's final fold to the hook (#initialize's on_turn_end).
|
|
480
|
+
def turn_ended(event)
|
|
481
|
+
return unless @on_turn_end
|
|
482
|
+
|
|
483
|
+
@on_turn_end.call(Marshal.load(Marshal.dump(@turn)), event)
|
|
484
|
+
rescue StandardError => e
|
|
485
|
+
Log.warn(:bridge, "turn_end_hook_failed", error: "#{e.class}: #{e.message}")
|
|
486
|
+
end
|
|
487
|
+
|
|
385
488
|
def append_text(iteration, kind, text)
|
|
386
489
|
return if text.nil? || text.empty?
|
|
387
490
|
|