samagotchi 0.90.0 → 0.92.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 +79 -1
- data/README.md +5 -2
- data/bin/chi +4 -4
- data/docs/architecture.md +17 -13
- data/docs/bundle-authoring.md +6 -11
- data/docs/cli.md +7 -4
- data/docs/configuration.md +8 -4
- data/docs/context.md +5 -3
- data/docs/hooks.md +3 -3
- data/docs/internals/background-tasks.md +27 -2
- data/docs/internals/bundles.md +4 -4
- data/docs/internals/engine.md +21 -8
- data/docs/internals/prompt-caching.md +4 -0
- data/docs/internals/tool-guardrails.md +10 -2
- data/docs/memory.md +6 -0
- data/docs/plugins.md +30 -13
- data/docs/releasing.md +3 -0
- data/docs/sessions.md +4 -4
- data/lib/samagotchi/bridge.rb +11 -7
- data/lib/samagotchi/bundle_command/check.rb +43 -12
- data/lib/samagotchi/bundle_command/scaffold.rb +12 -0
- data/lib/samagotchi/bundle_command/test_run.rb +12 -0
- data/lib/samagotchi/bundle_command/try/command.rb +164 -0
- data/lib/samagotchi/bundle_command/try/render.rb +80 -0
- data/lib/samagotchi/bundle_command/try/run.rb +35 -17
- data/lib/samagotchi/bundle_command/try/sandbox.rb +113 -34
- data/lib/samagotchi/bundle_command/try/trace.rb +26 -15
- data/lib/samagotchi/bundle_command.rb +11 -213
- data/lib/samagotchi/bundle_session_settings.rb +82 -45
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -5
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +57 -12
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +5 -0
- data/lib/samagotchi/child_env.rb +50 -0
- data/lib/samagotchi/client_id.rb +3 -0
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_note.rb +13 -6
- data/lib/samagotchi/engine.rb +31 -15
- data/lib/samagotchi/hooks/registry.rb +2 -0
- data/lib/samagotchi/kernel_loop.rb +14 -3
- data/lib/samagotchi/last_turn.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm/errors.rb +4 -4
- data/lib/samagotchi/memory_bundle/builder.rb +3 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +7 -2
- data/lib/samagotchi/memory_bundle/installer.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +42 -8
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/parent_report.rb +12 -0
- data/lib/samagotchi/reply_wait.rb +6 -2
- data/lib/samagotchi/sandbox_layout.rb +66 -0
- data/lib/samagotchi/send_command.rb +3 -3
- data/lib/samagotchi/session_manager.rb +12 -20
- data/lib/samagotchi/task_finish_note.rb +90 -0
- data/lib/samagotchi/task_watch.rb +228 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +19 -4
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -2
- data/lib/samagotchi/testing/bundle.rb +32 -19
- data/lib/samagotchi/testing/install.rb +26 -17
- data/lib/samagotchi/testing/sandbox.rb +45 -37
- data/lib/samagotchi/testing/waiting.rb +5 -7
- data/lib/samagotchi/tool_declarations.rb +16 -0
- data/lib/samagotchi/tool_response.rb +28 -6
- data/lib/samagotchi/tools/builtins.rb +5 -1
- data/lib/samagotchi/tools/task_record.rb +9 -0
- data/lib/samagotchi/tools/task_runtime.rb +2 -2
- data/lib/samagotchi/tools/task_wait.rb +10 -3
- data/lib/samagotchi/turn_checkpoint.rb +23 -10
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +24 -14
- data/lib/samagotchi/web/public/cards_place.js +4 -1
- data/lib/samagotchi/web/public/images.js +6 -0
- data/lib/samagotchi/web/public/model_pick.js +10 -0
- data/lib/samagotchi/web/public/notify.js +4 -4
- data/lib/samagotchi/web/public/toasts.js +17 -9
- data/lib/samagotchi/web/public/turn_events.js +13 -1
- data/lib/samagotchi/web/public/unqueue.js +20 -4
- data/lib/samagotchi/worker.rb +104 -42
- data/lib/samagotchi/worker_idle_exit.rb +26 -3
- data/lib/samagotchi/worker_inbound.rb +18 -4
- data/lib/samagotchi/worker_task_wakes.rb +160 -0
- data/lib/samagotchi/worker_wakes.rb +11 -4
- metadata +8 -1
data/docs/internals/engine.md
CHANGED
|
@@ -97,11 +97,14 @@ StandardError in run_turn:
|
|
|
97
97
|
Interrupt (Ctrl-C; the REPL only: a worker traps INT, see the signal row below):
|
|
98
98
|
→ if turn.ended: re-raised only
|
|
99
99
|
→ turn.controller.cancel!(:ctrl_c)
|
|
100
|
-
→ end_turn(turn, "canceled"): ctrl_c_messages keeps turn.messages
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
100
|
+
→ end_turn(turn, "canceled"): ctrl_c_messages keeps the loop's conversation, else turn.messages
|
|
101
|
+
# (it came before the loop), with TurnNote.cancelled(:ctrl_c) at the tail.
|
|
102
|
+
# Both loops attach their conversation to the Interrupt as to a failure
|
|
103
|
+
# (LLM::FailedTurn.attach), so the turn's finished iterations stay, as after a Stop,
|
|
104
|
+
# and the cancel note names the tasks the turn started. One mid-tool is answered:
|
|
105
|
+
# ToolResponse.run_batch gives every call left an interrupted result (the native
|
|
106
|
+
# loop's batch entry joins them after the finished ones), so no call is kept without
|
|
107
|
+
# its result. A half-streamed reply is not kept (a Stop keeps it, [interrupted]).
|
|
105
108
|
→ :turn_canceled {cancellation_reason: :ctrl_c, duration_ms}
|
|
106
109
|
→ re-raised (the caller decides whether to exit)
|
|
107
110
|
|
|
@@ -191,7 +194,8 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
|
|
|
191
194
|
```
|
|
192
195
|
|
|
193
196
|
`turn.conversation` is the loop's own copy, not the Engine's `turn.messages`. A failure attaches it to the error
|
|
194
|
-
(`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it;
|
|
197
|
+
(`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; an Interrupt (Ctrl-C) too, for
|
|
198
|
+
`Engine#ctrl_c_messages` (see Error paths).
|
|
195
199
|
|
|
196
200
|
### ChatLoop (the chat APIs)
|
|
197
201
|
|
|
@@ -468,7 +472,9 @@ TUI. Errors in the sink or an observer are caught; they never break the turn.
|
|
|
468
472
|
|
|
469
473
|
`Engine#announce` puts an event that belongs to no turn (`ANNOUNCEABLE_EVENTS`: `turn_enqueued`, `turn_dequeued`
|
|
470
474
|
(a queued message taken back before it ran, `{enqueued_id, client_id}`: the remover's), `card`,
|
|
471
|
-
`hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only.
|
|
475
|
+
`hook_notice`, `guardrail_warning`, `plugin_init_started`, and more) to the observers only. `:side_usage`
|
|
476
|
+
(`{kind, side}`: a side request's usage, the recap's, `/btw`'s or `ask_model`'s, and the session's side sums) goes
|
|
477
|
+
to the observers the same way from `Engine#note_side_usage` (any thread), without being announceable.
|
|
472
478
|
|
|
473
479
|
Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
474
480
|
|
|
@@ -491,6 +497,8 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
491
497
|
| `:tool_dispatch_started` / `:tool_dispatch_completed` | around a batch | `{iteration, call_count}` |
|
|
492
498
|
| `:tool_call_started` | before a call runs | Engine event includes `call` (see ToolRunner); the Bridge's SSE frame omits it |
|
|
493
499
|
| `:tool_call_completed` | after a call ran | see ToolRunner |
|
|
500
|
+
| `:question_requested` / `:question_answered` / `:question_cancelled` | `QuestionDesk`: a question is asked (an approval, `ask_user_question`, a hook's or plugin's `ask_user`, the step-limit Continue), answered, or withdrawn | `{pending_question}` / `{id, answer}` / `{id, reason}` |
|
|
501
|
+
| `:plugin_init_wait` | a turn waits for plugin init tasks that provide tools (`PluginTasks`) | `{tasks: [{bundle, id, label}]}` |
|
|
494
502
|
| `:used_memories_updated` | a memory was read, and after the loop | `{used_memory_names, read_names?}` |
|
|
495
503
|
| `:hook_notice` | a hook's or plugin's notify during a turn | `{hook, text, level}` (announced with `between_turns: true` outside a turn) |
|
|
496
504
|
| `:guardrail_warning` | a guardrails line during a turn | `{message}` |
|
|
@@ -681,7 +689,7 @@ prompt starts with what the server has cached).
|
|
|
681
689
|
and config.yml hooks survive.
|
|
682
690
|
3. **Each loop works on its own copy of the conversation.** The Engine's `turn.messages` is what it sent. After
|
|
683
691
|
a normal end or a Stop it keeps `result.conversation`; after a failure, the copy attached with
|
|
684
|
-
`FailedTurn.attach
|
|
692
|
+
`FailedTurn.attach` (a Ctrl-C's too); `turn.messages` only when it ended before the loop.
|
|
685
693
|
4. **Events are the contract.** UIs read events, not internal state. `on_event` is per turn; observers are
|
|
686
694
|
persistent.
|
|
687
695
|
5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
|
|
@@ -749,3 +757,8 @@ Paths are under `lib/samagotchi/`.
|
|
|
749
757
|
| `turn_checkpoint.rb` | `TurnCheckpoint`: a worker's running turn saved at the loop's iteration boundaries (`Session#turn_checkpoint`), so a killed worker loses at most `INTERVAL` of it |
|
|
750
758
|
| `context_status.rb` | `ContextStatus`: one turn's context tracking (the status line's value, the `:context_status` event, the model's guidance line) ([context-telemetry.md](context-telemetry.md)) |
|
|
751
759
|
| `session_manager.rb` | Background session processes (workers) and their IPC |
|
|
760
|
+
| `worker_wakes.rb` | `WorkerWakes`: a worker's wake budget for the turns nobody typed (`session.max_wakes`) |
|
|
761
|
+
| `task_watch.rb`, `task_finish_note.rb` | `TaskWatch`: the background tasks a worker's session started that the model hasn't seen finish; `TaskFinishNote`: the note one leaves when it does ([background-tasks.md](background-tasks.md#finish-wake)) |
|
|
762
|
+
| `side_usage.rb` | `SideUsage`: one side request's usage (a recap, `ctx.ask_model`), folded into the session's side sums (`:side_usage`) |
|
|
763
|
+
| `bridge/client_event.rb` | `Bridge::ClientEvent.project`: what an SSE client gets for an engine event ([architecture.md](../architecture.md)) |
|
|
764
|
+
| `guardrails/parent_policy.rb` | `Guardrails::ParentPolicy`: which options of a question a parent agent may pick (`guardrails.parent_answers`) |
|
|
@@ -83,6 +83,10 @@ right-hand column happens.
|
|
|
83
83
|
finishes booting between load and the first turn changes search answers only, so two sessions on the same
|
|
84
84
|
toolkit with the same MCP cache send the same bytes. Whether find_tools/call_tool are declared at all reads the
|
|
85
85
|
same two things (`Registry#index_needed?`), never live groups.
|
|
86
|
+
- A worker's `execute` and `task_create` descriptions carry the finish wake's sentence ("if you end your turn before
|
|
87
|
+
it finishes, chi tells you when it does"; `Engine.new(task_finish_notes:)`, set once as the worker builds its
|
|
88
|
+
Engine); the `--no-shared` REPL's, and a worker's under `session.task_wakes: off`, don't. A session that moves
|
|
89
|
+
between the two, or whose worker restarts after `task_wakes` changed, misses the cache once.
|
|
86
90
|
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
87
91
|
breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
|
|
88
92
|
#2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
|
|
@@ -27,6 +27,14 @@ Telemetry uses existing context estimation settings:
|
|
|
27
27
|
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` (`context.window_tokens`)
|
|
28
28
|
- `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN` (`context.chars_per_token`)
|
|
29
29
|
|
|
30
|
+
Line ranges (`read` and `edit`; env only, no config key):
|
|
31
|
+
|
|
32
|
+
- `SAMAGOTCHI_READ_END_OPTIONAL` / `SAMAGOTCHI_EDIT_END_OPTIONAL` (default `true`): a `start_line` without
|
|
33
|
+
`end_line` means "to the end of the file"; `false` makes it an error.
|
|
34
|
+
- `SAMAGOTCHI_READ_ALLOW_OOR_END` / `SAMAGOTCHI_EDIT_ALLOW_OOR_END` (default `true`): an `end_line` past the end is
|
|
35
|
+
clamped to the last line and the result says so; `false` makes it `Error: range out of bounds`. A `start_line`
|
|
36
|
+
past the end is always an error.
|
|
37
|
+
|
|
30
38
|
## Execute Tool Output Guardrails
|
|
31
39
|
|
|
32
40
|
The `execute` tool applies the same guardrail model to command output:
|
|
@@ -64,8 +72,8 @@ built-in name; a tool with none gets `:head`):
|
|
|
64
72
|
everything else the cap leaves, so a larger `max_tool_output_chars` keeps a longer tail. Both are snapped to whole
|
|
65
73
|
lines when that keeps at least half of each. execute's stdout and stderr blocks are cut each on its own (stdout gets
|
|
66
74
|
about two thirds of the room when both are long, a block that needs less gives the rest to the other); the `exit:`
|
|
67
|
-
line is always kept. The whole stays within the cap; a cap
|
|
68
|
-
characters gets the `:head` cut, and so does one whose gap lines leave no room for
|
|
75
|
+
line is always kept. The whole stays within the cap; a cap under three times `ToolOutputCut::MIN_TAIL` (200), so
|
|
76
|
+
under 600 characters, gets the `:head` cut, and so does one whose gap lines leave no room for a 200-char tail (checked with the
|
|
69
77
|
file's planned path before anything is written): nothing is saved then, and the event has no `output_file:`.
|
|
70
78
|
|
|
71
79
|
The gap line says what was left out and where the whole output is:
|
data/docs/memory.md
CHANGED
|
@@ -308,6 +308,12 @@ for a file another bundle installed.
|
|
|
308
308
|
with neither and no terminal it keeps your edits and says so. Kept edits
|
|
309
309
|
exit 2; `chi bundle diff NAME FILE` shows the base, and `--force` takes
|
|
310
310
|
the bundle's version.
|
|
311
|
+
- **A dev install** (`chi bundle install --dev PATH`) links the hooks, rules,
|
|
312
|
+
plugin and scripts to `PATH` but still copies the memories at install, so
|
|
313
|
+
an edit to one of them in `PATH` isn't seen until the next install.
|
|
314
|
+
- **The rest of the manifest** (`plugin:`, `requires_chi`, `session_settings:`,
|
|
315
|
+
`context_providers:`, `requires_chi_next`) is in
|
|
316
|
+
[Plugins](plugins.md) and [internals/bundles.md](internals/bundles.md).
|
|
311
317
|
|
|
312
318
|
## Bundle profiles: core and dev
|
|
313
319
|
|
data/docs/plugins.md
CHANGED
|
@@ -31,7 +31,8 @@ plugin:
|
|
|
31
31
|
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
|
|
32
32
|
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
33
33
|
# to the chi being released (docs/releasing.md)
|
|
34
|
-
session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3)
|
|
34
|
+
session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3);
|
|
35
|
+
# {cap: number} types them: number (or off), boolean, string or any
|
|
35
36
|
needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
|
|
36
37
|
```
|
|
37
38
|
|
|
@@ -766,10 +767,13 @@ A bundle's settings are config.yml's, for every session. A setting a session may
|
|
|
766
767
|
say) is one the manifest lists in `session_settings:`; `chi --set BUNDLE.KEY=VALUE` and `chi send --new --set …`
|
|
767
768
|
then give it to a new session, saved with it (a respawn, a fork and a continued link keep it; a delegate child
|
|
768
769
|
doesn't). chi refuses a bundle that isn't installed (or whose plugin doesn't load) and a key the manifest doesn't
|
|
769
|
-
list, so a typo isn't saved.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
770
|
+
list, so a typo isn't saved. A manifest that types a key (`session_settings: {cap: number}`: `number`, which
|
|
771
|
+
takes off too, `boolean`, `string`, or `any`) gets only a value of that type: `--set my-bundle.cap=five` is refused,
|
|
772
|
+
exit 2, as a key it doesn't list is (a mapping needs a chi newer than 0.91.0). Values are typed simply: an integer (`3`, `-1`) or a decimal (`2.5`, `.5`, `1e1`)
|
|
773
|
+
is a number, `true`, `yes` and `on` are true, `false`, `no` and `off` are false (any case, as config.yml's YAML reads
|
|
774
|
+
them), and anything else stays the text as given (`010`, `0x10`, `3:30`, `y`, `null`; YAML would read the first
|
|
775
|
+
three as numbers and `null` as nothing); a key typed `string` keeps its text as given (`yes`, `5`, `1.10`); `POST /api/sessions` `bundle_settings` types its strings the same way and
|
|
776
|
+
keeps JSON numbers and booleans. `ctx.settings` is the config's section with the session's values laid over it (one level: a session's
|
|
773
777
|
`cap` replaces the config's `cap`). Read such a key from `ctx.settings` where it is used, not in
|
|
774
778
|
`initialize(settings)`: that runs once at load, before any session, and gets the config only.
|
|
775
779
|
|
|
@@ -1195,7 +1199,8 @@ bundle.shutdown
|
|
|
1195
1199
|
parts (`reported:`, `estimated:`, `side:`, `known:`; `total` is reported + estimated) or a callable giving either,
|
|
1196
1200
|
called at each read (a counting lambda shows whether the bundle read it).
|
|
1197
1201
|
- **`session_settings:`** (on `Bundle.load` and `bundle.set`) are the session's own `--set` values for the bundle,
|
|
1198
|
-
keys its manifest lists in `session_settings:` (another
|
|
1202
|
+
keys its manifest lists in `session_settings:` (another key, or a value of another type than the manifest gives
|
|
1203
|
+
it, raises `ArgumentError`, as chi refuses it). `ctx.settings`
|
|
1199
1204
|
lays them over `settings:` through chi's own `Context#settings`; the plugin's `initialize(settings)` gets
|
|
1200
1205
|
`settings:` only. `bundle.set(session_settings:)` moves the session to a new id (`test-session-2`, or the
|
|
1201
1206
|
`session_id:` given beside it): chi never changes them within a session, and builds `ctx.settings` once per id.
|
|
@@ -1218,7 +1223,8 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
|
|
|
1218
1223
|
- **From the command line:** `chi bundle test [PATH]` runs a bundle's `test/**/*_test.rb` (or `spec/`) on the
|
|
1219
1224
|
Ruby and lib chi runs on, and `chi bundle check [PATH] [--fix]` runs `Install` (and the manifest, sha and
|
|
1220
1225
|
`requires_chi` checks) in a sandbox ([cli.md](cli.md#writing-a-bundle-new-test-check)). `chi bundle new NAME`
|
|
1221
|
-
writes a test to start from.
|
|
1226
|
+
writes a test to start from. `chi bundle try PATH -p TEXT` runs it against a real model for one turn
|
|
1227
|
+
([cli.md](cli.md#trying-a-bundle)), and `chi bundle install --dev PATH` links it into your own sessions.
|
|
1222
1228
|
|
|
1223
1229
|
## The btw bundle
|
|
1224
1230
|
|
|
@@ -1615,7 +1621,10 @@ bundles:
|
|
|
1615
1621
|
a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
|
|
1616
1622
|
- **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
|
|
1617
1623
|
cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
|
|
1618
|
-
delegate child, else `default:`.
|
|
1624
|
+
delegate child, else `default:`. A cap is a number (`3`, `.5`, `1e1`; in config.yml and `/cap`, `"$2"` too, which `--set` refuses). `--set` refuses
|
|
1625
|
+
another value (`cost-cap.cap=five`: exit 2); one in config.yml (or set before this check) that doesn't read
|
|
1626
|
+
(`default: five`) gets one notice, `cost-cap: default: "five" isn't a number or off; no cap`, and a `cap:` or
|
|
1627
|
+
`child:` that doesn't read gives way to the next key (`…; using default: ($5.00)`), never a silent no cap. Delegates and continued links each have their own cap (a continued link keeps
|
|
1619
1628
|
the `--set` cap, not a `/cap` or Continue); there is no family cap.
|
|
1620
1629
|
- **A host it can't watch:** a model that reports no cost and has no configured price (a local server, a free
|
|
1621
1630
|
model that reports 0) reads unknown: one notice, `the $3.00 cap can't be enforced: <model> reports no cost and has
|
|
@@ -1626,7 +1635,7 @@ bundles:
|
|
|
1626
1635
|
|
|
1627
1636
|
- `/cap`: `cost-cap: $1.20 spent of $3.00 (reported $1.10, estimated $0.10, side $0.02)`, or `cost-cap: no cap
|
|
1628
1637
|
($1.20 spent)`, with `(can't be enforced: no cost reported or priced)` when it can't be.
|
|
1629
|
-
- `/cap 5` or `/cap
|
|
1638
|
+
- `/cap 5`, `/cap $5` or `/cap .5`: the cap (and what Continue adds) becomes $5.00 for this session. A cap at or under the
|
|
1630
1639
|
spend is allowed: the next generation asks.
|
|
1631
1640
|
- `/cap off` (or `/cap 0`): no cap for this session, over the config's `default:`.
|
|
1632
1641
|
|
|
@@ -2254,7 +2263,7 @@ crash, TERM), chi shuts the session's Engine down:
|
|
|
2254
2263
|
Plugins load when a session starts (`Engine.new`, in the REPL or a
|
|
2255
2264
|
worker), after the bundle hooks. The steps are:
|
|
2256
2265
|
|
|
2257
|
-
1. The file must match the sha256 recorded at install.
|
|
2266
|
+
1. The file must match the sha256 recorded at install (not checked for a bundle installed with `--dev`).
|
|
2258
2267
|
2. chi must meet `requires_chi`.
|
|
2259
2268
|
3. The file is `module_eval`'d into a new module in the bundle's namespace
|
|
2260
2269
|
(`Samagotchi::Bundles::<bundle>`).
|
|
@@ -2280,9 +2289,11 @@ a bundle's hooks. **Installing a bundle means trusting it**, as you would a
|
|
|
2280
2289
|
gem.
|
|
2281
2290
|
|
|
2282
2291
|
- The sha256 is an integrity check, not proof of who wrote the file. A file
|
|
2283
|
-
edited after install is not loaded until you reinstall the bundle.
|
|
2284
|
-
|
|
2285
|
-
start.
|
|
2292
|
+
edited after install is not loaded until you reinstall the bundle. A
|
|
2293
|
+
`--dev` install skips that check for its own bundle: its files are the
|
|
2294
|
+
source directory's, and an edit loads at the next start.
|
|
2295
|
+
- Install only copies the file (`--dev` links it). The code first runs at the
|
|
2296
|
+
next session start.
|
|
2286
2297
|
- Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
|
|
2287
2298
|
chi's own tools; path and command rules see what `targets:` says.
|
|
2288
2299
|
|
|
@@ -2301,6 +2312,12 @@ gem.
|
|
|
2301
2312
|
`requires_chi`, its `scripts/` and `context_providers:`) into the built
|
|
2302
2313
|
bundle.
|
|
2303
2314
|
- `chi bundle uninstall <name>`: removes the plugin with the bundle.
|
|
2315
|
+
- `chi bundle install --dev <dir>`: links the bundle's plugin, hooks, rules and scripts to `<dir>` instead of
|
|
2316
|
+
copying them, with no sha256 check, so an edit applies at the next worker start
|
|
2317
|
+
([cli.md](cli.md#writing-a-bundle-new-test-check)).
|
|
2318
|
+
- `chi bundle new`, `test` and `check`: start a bundle, run its tests on chi's Ruby, check it as an install takes
|
|
2319
|
+
it ([cli.md](cli.md#writing-a-bundle-new-test-check)); `chi bundle try PATH -p TEXT`: run it for one turn in a
|
|
2320
|
+
sandbox and trace what it did ([cli.md](cli.md#trying-a-bundle)). The path: [bundle-authoring.md](bundle-authoring.md).
|
|
2304
2321
|
|
|
2305
2322
|
## Not yet
|
|
2306
2323
|
|
data/docs/releasing.md
CHANGED
|
@@ -61,6 +61,9 @@ as the GitHub release body.
|
|
|
61
61
|
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
|
|
62
62
|
| `rake bundles:check` | Sha lines match; every top-level `*.md` of a bundle is listed in its `files:` (an install takes only the listed ones); a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`, and a profile (`core`, `dev`) whose `requires_chi` is below a member's. The release workflow and `release:check` run it in full. |
|
|
63
63
|
| `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
|
|
64
|
+
| `rake bundles:specs` | The shipped bundles' own specs (`lib/samagotchi/bundles/*/spec`) on the bundle test kit alone (no `spec_helper`), as an external bundle's run. CI runs it as its own step. |
|
|
65
|
+
| `rake lint` / `rake lint:fix` | RuboCop over the repo (must report no offenses; CI runs it) / its safe autocorrect (`rubocop -a`). |
|
|
66
|
+
| `rake gem:validate`, `gem:build`, `gem:install`, `gem:clean` | Check the gemspec; build the gem into `pkg/`; build and install it locally; remove the built gems. |
|
|
64
67
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
65
68
|
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped). No commit. |
|
|
66
69
|
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
data/docs/sessions.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
4
4
|
|
|
5
|
-
**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`). 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
|
|
5
|
+
**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`). 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`.
|
|
6
6
|
|
|
7
7
|
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
17
17
|
|
|
18
18
|
A session is deleted if **expired by age OR overflow by count** (unless `keep_status` or the live-owner guard: a session a worker or `chi` still has open is never pruned). A session's `status` is its turn state (`idle`/`running`), not whether a worker is alive. Orphan dirs without a `*.json` are never deleted, except skeleton-only ones (see *Empty sessions*). Deletion removes both `*.json` and sidecar dir atomically. Set both `DAYS=0` and `MAX=0` to retain forever.
|
|
19
19
|
|
|
20
|
-
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up. The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, reminders, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
20
|
+
**Worker idle exit:** a background worker (plain `chi`, Web UI sessions) exits after `SAMAGOTCHI_SESSION_IDLE_EXIT_MINUTES` (`session.idle_exit_minutes`, default `30`, `0`=never) with no turn running or queued, no client on its stream (an open web tab or attached terminal keeps it), no reminder registered, no background task it watches for a [finish wake](internals/background-tasks.md#finish-wake) still running or waiting for its wake turn (with `session.task_wakes: wake`; not one whose turn you stopped) and no continue offer (the step-limit question) waiting for an answer. It removes `bridge.json` and frees `owner.lock`; the next prompt or `--attach` wakes a new worker (`WorkerIdleExit`, `SessionManager.run_session_loop`). A client can also ask the worker to exit right away (Bridge `POST /session/:id/exit`; `/exit` in an attached terminal does, `/detach` doesn't): the same rules apply without the timeout (even with `0`), and the asker's own stream doesn't count. The reply names what keeps it up (for background tasks, which ones and how to stop them). The session's status stays as it was, not `stopped`. With `"restart": true` (a worker whose sidecar lists the `restart` feature) the worker hands the session to a new worker on the newest chi installed: it leaves, then starts its successor once its lock is free, keeping even an empty session and writing no recap. Clients' streams don't hold a restart (a web tab and an attached terminal reconnect to the new worker); what would be lost does: a turn running or queued, a continue offer, a question or approval waiting, reminders, a `/btw` (or another anytime command) still running, a delegate's approval relay open or settled in the last 10 minutes, and background tasks this conversation started that still run (`WorkerIdleExit#hold_for_restart`; 409 `held` with `reason`). What lives only in the worker's memory goes: the event history a late client replays, the kept `/btw` answers, a plugin's in-memory state.
|
|
21
21
|
|
|
22
22
|
**Empty sessions:** a session nothing happened in is deleted when it is left, so `chi` → `/exit` leaves no trace. Empty means: no messages, no turn tried (a failed turn leaves a `last_prompt` and keeps it), no memory used, no pending question, nothing in `input/`, `notes/` or `images/`, nothing in its dir beyond the worker's skeleton, mode `assist`, and the default model. A session on another model (`/model`, `--model`) counts as prepared and stays (`SessionManager.discardable?`). The worker checks as it idle-exits or leaves on a client's request and deletes it once its lock is free, checking again first. The attached `/exit` then says `Detached; the session is empty: it will be discarded if nothing arrives before its worker stops.` (a note or prompt landing first keeps it). The REPL (`--no-shared`) does the same at `/exit` or Ctrl-D. The retention sweep catches those killed first: empty sessions over an hour old with no live owner, whatever `DAYS`/`MAX`, and skeleton-only dirs with no `*.json`. `session.keep_empty: true` (env `SAMAGOTCHI_SESSION_KEEP_EMPTY`) turns all of it off.
|
|
23
23
|
|
|
@@ -143,7 +143,7 @@ system message marked `kind: turn_note` (the UIs don't show it; they showed the
|
|
|
143
143
|
the user, who may send it again.]` in the REPL and attached mode, where the prompt is given back;
|
|
144
144
|
`… The user's last message was not answered.]` after `-p`, where the prompt stays in the session;
|
|
145
145
|
`… The wake turn for the change in attached context pr-7 was not answered; chi starts no other wake turn until
|
|
146
|
-
the user writes.]` after a wake turn chi started (a context source's change, a delegate's report), not the user
|
|
146
|
+
the user writes.]` after a wake turn chi started (a context source's change, a delegate's report, a background task that finished), not the user
|
|
147
147
|
(for a delegate's report it adds `chi keeps the report and brings it again with the user's next message.`);
|
|
148
148
|
- cancelled (Ctrl-C, the web's stop): `[SYSTEM: the previous turn was cancelled (ctrl-c) after 12s; the reply
|
|
149
149
|
above ends where it was cut off.]` (an answer or only narration had streamed), or `…; no answer had been shown.]` when only thinking had streamed;
|
|
@@ -211,7 +211,7 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
211
211
|
- Exit codes with `--wait`: 0 answered; 3 the turn waits for an answer from you (a question, a hook's question, a guardrail approval, or the step-limit question: see [chi as a sub-agent](sub-agent.md#the-step-limit)), and the session keeps waiting: stderr gets the whole question (`waiting for an answer (approval): …`, its text, the numbered options, whether free text is allowed) and how to answer it (`chi answer ID --question QID --option N`, or `chi --attach ID`, the web); 4 `--timeout S` passed with the turn still running (it goes on; wait again); 1 the turn ended without a reply (canceled, failed or empty), the worker failed or vanished, or the session was stopped; 130 on Ctrl-C, which leaves the turn running (`still running: chi --attach ID`). There is no default timeout.
|
|
212
212
|
- `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `removed` (a `--queue --wait` message taken back before it ran), `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` (`worker_stopped`: the turn waited for died with its worker and the next worker settled it) and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
|
|
213
213
|
- `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
|
|
214
|
-
- `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer is a usage error (exit 2), as is `--set` without `--new`.
|
|
214
|
+
- `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer, or a value of another type than the manifest gives the key (`cost-cap.cap=five`), is a usage error (exit 2), as is `--set` without `--new`.
|
|
215
215
|
- `--parent-answers LEVEL` (with `--new`) gives the new session its own `guardrails.parent_answers` (`hard`, `standard`, `soft`), tighten-only: config.yml's applies when it is stricter, with `chi send: warning: --parent-answers soft is looser than config.yml's …`. See *Parent answers* below.
|
|
216
216
|
- `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit, bundle settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
|
|
217
217
|
- The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -84,8 +84,8 @@ module Samagotchi
|
|
|
84
84
|
# @param on_exit_request [#call, nil] a client asks the worker to exit
|
|
85
85
|
# now: takes the client_id and delete: (the exit is to delete the
|
|
86
86
|
# session, /exit --delete), returns nil when the worker will leave or
|
|
87
|
-
#
|
|
88
|
-
# with the event log held. Without one, POST /exit answers 501
|
|
87
|
+
# what keeps it up (a WorkerIdleExit::Hold: #reason, #tasks, #hint);
|
|
88
|
+
# called with the event log held. Without one, POST /exit answers 501
|
|
89
89
|
# @param exit_discards [#call, nil] after an exit the worker agreed to:
|
|
90
90
|
# whether it will delete the session as empty (the 200 says
|
|
91
91
|
# +discard+); without one the reply leaves the field out
|
|
@@ -805,7 +805,8 @@ module Samagotchi
|
|
|
805
805
|
# event log held, so no POST /turn lands between its check and its
|
|
806
806
|
# answer; it leaves from its loop after this reply.
|
|
807
807
|
# 200 {status: "exiting", discard?: the session is empty and goes} ({status: "restarting"} for a restart), or 409
|
|
808
|
-
# {status: "held", reason:} naming what keeps it up
|
|
808
|
+
# {status: "held", reason:} naming what keeps it up (with tasks: [{id:,
|
|
809
|
+
# command:}] and a hint: how to stop them, for background_tasks). A past +deadline+
|
|
809
810
|
# (see #handle_post_turn), checked with the event log held: 408
|
|
810
811
|
# deadline_passed, the worker not asked (its client said it didn't stop).
|
|
811
812
|
# Returns [headers, status, body].
|
|
@@ -822,15 +823,15 @@ module Samagotchi
|
|
|
822
823
|
deadline = fetched(parsed, "deadline")
|
|
823
824
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
824
825
|
|
|
825
|
-
|
|
826
|
+
hold = @engine.synchronize_events do
|
|
826
827
|
raise IntakeClosed if stopped?
|
|
827
828
|
next :expired if expired?("exit_expired", deadline, sid: session_id, client_id: client_id)
|
|
828
829
|
|
|
829
830
|
@on_exit_request.call(client_id, **options)
|
|
830
831
|
end
|
|
831
|
-
return deadline_passed("exit request") if
|
|
832
|
+
return deadline_passed("exit request") if hold == :expired
|
|
832
833
|
|
|
833
|
-
if
|
|
834
|
+
if hold.nil?
|
|
834
835
|
return [{}, 200, { status: "restarting", session_id: @session_id }] if restart
|
|
835
836
|
|
|
836
837
|
body = { status: "exiting", session_id: @session_id }
|
|
@@ -838,7 +839,10 @@ module Samagotchi
|
|
|
838
839
|
return [{}, 200, body]
|
|
839
840
|
end
|
|
840
841
|
|
|
841
|
-
|
|
842
|
+
# A hold may name what it waits for (tasks: the background tasks a
|
|
843
|
+
# finish wake watches).
|
|
844
|
+
[{}, 409, { status: "held", reason: hold.reason.to_s, tasks: hold.tasks, hint: hold.hint,
|
|
845
|
+
session_id: @session_id }.compact]
|
|
842
846
|
end
|
|
843
847
|
|
|
844
848
|
# Queue a turn for this session through its file IPC (the worker polls
|
|
@@ -8,6 +8,7 @@ require_relative "../memory_bundle/shas"
|
|
|
8
8
|
require_relative "../memory_bundle/memory_files"
|
|
9
9
|
require_relative "../plugin/api"
|
|
10
10
|
require_relative "../version"
|
|
11
|
+
require_relative "../cli/flags"
|
|
11
12
|
|
|
12
13
|
module Samagotchi
|
|
13
14
|
class BundleCommand
|
|
@@ -23,6 +24,22 @@ module Samagotchi
|
|
|
23
24
|
# sha256 lines (MemoryBundle::Shas). #run returns 0 without errors, 1
|
|
24
25
|
# with any.
|
|
25
26
|
class Check
|
|
27
|
+
HELP = <<~TEXT
|
|
28
|
+
Usage: chi bundle check [PATH] [--fix] [--set KEY=VALUE]
|
|
29
|
+
|
|
30
|
+
Check the bundle at PATH (default: .) as an install would take it, one finding per line:
|
|
31
|
+
error|warn|info CODE file[:line]: message (fix: hint)
|
|
32
|
+
It installs and loads the bundle in a sandbox (temp HOME, config and state), not in yours, but its plugin and
|
|
33
|
+
hooks run in this process as written: run it on code you'd install.
|
|
34
|
+
--fix rewrite stale and placeholder sha256 lines in manifest.yml
|
|
35
|
+
--set KEY=VALUE a setting for the load (config.yml bundles: <name>: KEY), repeatable
|
|
36
|
+
Exit status: 0 no errors, 1 errors, 2 usage.
|
|
37
|
+
TEXT
|
|
38
|
+
FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
39
|
+
f.switch "--fix"
|
|
40
|
+
f.value "--set", key: :set, repeat: true
|
|
41
|
+
end
|
|
42
|
+
|
|
26
43
|
Finding = Data.define(:level, :code, :file, :line, :message, :fix) do
|
|
27
44
|
def error? = level == :error
|
|
28
45
|
|
|
@@ -234,13 +251,26 @@ module Samagotchi
|
|
|
234
251
|
# A shipped bundle installed from any chi's copy is that bundle.
|
|
235
252
|
return if shipped?
|
|
236
253
|
|
|
237
|
-
record =
|
|
238
|
-
return unless record
|
|
254
|
+
record = installed_record(name)
|
|
255
|
+
return unless record
|
|
256
|
+
return unreadable_clash(name, record.error) if record.error
|
|
257
|
+
return if File.expand_path(record.source.to_s) == @dir
|
|
239
258
|
|
|
240
259
|
add(:warn, :clash, "manifest.yml", "a bundle named #{name} is installed from #{record.source}",
|
|
241
260
|
line: manifest_line("name"), fix: "rename the bundle, or chi bundle upgrade #{display(@dir)} to replace it")
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The installed same-named bundle's record; an unreadable one as
|
|
264
|
+
# InstalledBundle.unreadable.
|
|
265
|
+
def installed_record(name)
|
|
266
|
+
MemoryBundle::Provenance.new(name: name).record
|
|
267
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
268
|
+
MemoryBundle::InstalledBundle.unreadable(name, "#{e.class}: #{e.message.lines.first&.strip}")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def unreadable_clash(name, error)
|
|
272
|
+
add(:warn, :clash, "manifest.yml", "a bundle named #{name} is installed, but its record can't be read (#{error})",
|
|
273
|
+
line: manifest_line("name"), fix: "rename the bundle, or chi bundle install --force #{display(@dir)} to replace it")
|
|
244
274
|
end
|
|
245
275
|
|
|
246
276
|
# An unfilled `sha256: sha256:` isn't YAML: the Installer can't read
|
|
@@ -303,19 +333,20 @@ module Samagotchi
|
|
|
303
333
|
|
|
304
334
|
def report_registered(manifest, registered)
|
|
305
335
|
plugin = manifest.plugin ? manifest.plugin[:file] : "manifest.yml"
|
|
306
|
-
registered
|
|
307
|
-
registered
|
|
308
|
-
registered
|
|
309
|
-
hooks.each do |
|
|
310
|
-
file = manifest.hooks.key?(hook) ? "hooks/#{hook}" : plugin
|
|
311
|
-
next add(:info, :registered, file, "hook #{event} (priority #{priority})") if Hooks::EVENTS.include?(event)
|
|
336
|
+
registered.commands.each { |command| add(:info, :registered, plugin, "command #{command.name}") }
|
|
337
|
+
registered.tools.each { |name| add(:info, :registered, plugin, "tool #{name}") }
|
|
338
|
+
registered.hooks.each do |event, hooks|
|
|
339
|
+
hooks.each do |hook|
|
|
340
|
+
file = manifest.hooks.key?(hook.hook) ? "hooks/#{hook.hook}" : plugin
|
|
341
|
+
next add(:info, :registered, file, "hook #{event} (priority #{hook.priority})") if Hooks::EVENTS.include?(event)
|
|
312
342
|
|
|
313
343
|
add(:warn, :"hook-event", file, "hook on #{event.inspect}, which chi never fires",
|
|
314
344
|
fix: "one of #{Hooks::EVENTS.join(", ")}")
|
|
315
345
|
end
|
|
316
346
|
end
|
|
317
|
-
{
|
|
318
|
-
registered
|
|
347
|
+
{ "init" => registered.inits, "toolkit" => registered.toolkits, "tool source" => registered.tool_sources,
|
|
348
|
+
"peer source" => registered.peer_sources }.each do |what, names|
|
|
349
|
+
names.each { |name| add(:info, :registered, plugin, "#{what} #{name}") }
|
|
319
350
|
end
|
|
320
351
|
end
|
|
321
352
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "../memory_bundle/shas"
|
|
5
5
|
require_relative "../version"
|
|
6
|
+
require_relative "../cli/flags"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
class BundleCommand
|
|
@@ -12,6 +13,17 @@ module Samagotchi
|
|
|
12
13
|
# Minitest test on the kit that passes as written, and a README. The
|
|
13
14
|
# test is the pattern to copy (docs/bundle-authoring.md).
|
|
14
15
|
class Scaffold
|
|
16
|
+
HELP = <<~TEXT
|
|
17
|
+
Usage: chi bundle new NAME [--dir DIR]
|
|
18
|
+
|
|
19
|
+
Start a bundle in DIR/NAME (default ./NAME; refused when it exists): manifest.yml (shas filled,
|
|
20
|
+
requires_chi this chi), plugin.rb (a /NAME command and an after_tool_call hook), test/NAME_test.rb
|
|
21
|
+
(passes as written) and README.md. Then: chi bundle test, chi bundle check, chi bundle install.
|
|
22
|
+
TEXT
|
|
23
|
+
FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A--/) do |f|
|
|
24
|
+
f.value "--dir"
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
# A name that is also a slash command (Plugin::Api::COMMAND_NAME).
|
|
16
28
|
NAME = /\A[a-z][a-z0-9_-]{0,31}\z/
|
|
17
29
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbconfig"
|
|
4
|
+
require_relative "../cli/flags"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
class BundleCommand
|
|
@@ -15,6 +16,17 @@ module Samagotchi
|
|
|
15
16
|
# (a checkout or a vendored install: its gems in vendor/bundle), there.
|
|
16
17
|
# The tests' exit status is #run's.
|
|
17
18
|
class TestRun
|
|
19
|
+
HELP = <<~TEXT
|
|
20
|
+
Usage: chi bundle test [PATH] [-- ARGS]
|
|
21
|
+
|
|
22
|
+
Run the tests of the bundle at PATH (default: .) on the Ruby and the lib chi runs on, outside any Bundler env:
|
|
23
|
+
test/**/*_test.rb on Minitest (require "samagotchi/testing/minitest"), or else spec/ on RSpec with the kit
|
|
24
|
+
(-r samagotchi/testing/rspec). ARGS go to the test runner (-- -n /name/, -- -e "a name").
|
|
25
|
+
The tests run the bundle's code as written, with your HOME (the kit's sandbox is per test): run code you'd install.
|
|
26
|
+
Exit status: the tests'; 1 no tests; 2 usage, or minitest / rspec doesn't load.
|
|
27
|
+
TEXT
|
|
28
|
+
FLAGS = CLI::Flags.new(help: %w[--help], flag_pattern: /\A-/)
|
|
29
|
+
|
|
18
30
|
LIB_DIR = File.expand_path("../..", __dir__)
|
|
19
31
|
|
|
20
32
|
def initialize(dir, args: [], stdout: $stdout, stderr: $stderr)
|