samagotchi 0.151.0 → 0.153.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 +46 -1
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +84 -0
- data/docs/cli.md +35 -10
- data/docs/configuration.md +19 -14
- data/docs/guardrails.md +11 -5
- data/docs/hooks.md +4 -3
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +3 -0
- data/docs/internals/block-renderers.md +3 -1
- data/docs/internals/bundles.md +48 -14
- data/docs/internals/engine.md +9 -5
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +7 -3
- data/docs/internals/web-frontend.md +20 -17
- data/docs/memory.md +6 -1
- data/docs/plugins.md +80 -29
- data/docs/releasing.md +8 -3
- data/docs/sessions.md +2 -2
- data/docs/testing.md +2 -2
- data/lib/samagotchi/block_renderers.rb +9 -8
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +37 -9
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +11 -8
- data/lib/samagotchi/memory_bundle/builder.rb +36 -30
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -46
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -15
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +90 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +146 -6
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +217 -18
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/worker.rb +6 -0
- metadata +11 -1
data/docs/plugins.md
CHANGED
|
@@ -29,6 +29,7 @@ plugin:
|
|
|
29
29
|
file: plugin.rb
|
|
30
30
|
sha256: sha256:6a1a7022… # shasum -a 256 plugin.rb
|
|
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
|
+
# every key below has a chi floor: check warns when requires_chi admits a chi that never read it
|
|
32
33
|
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
33
34
|
# to the chi being released (docs/releasing.md)
|
|
34
35
|
session_settings: [cap] # optional: the settings a session may set (chi --set my-bundle.cap=3);
|
|
@@ -243,8 +244,13 @@ Web only: the terminals type the command (`/claude validate <text>`).
|
|
|
243
244
|
|
|
244
245
|
### `chi.tool(name, description, params:, schema:, label:, preview:, targets:, refs:) { |args, ctx| … }`
|
|
245
246
|
|
|
246
|
-
This adds a tool the model can call.
|
|
247
|
-
|
|
247
|
+
This adds a tool the model can call. A session's toolkit decides whether it
|
|
248
|
+
is declared, as for a built-in ([Toolkits](configuration.md#toolkits)): `full`
|
|
249
|
+
(and any kit on `base: full`) declares it, in the system prompt and in the
|
|
250
|
+
chat path's `tools:`, after chi's own tools; a kit that doesn't list it (`lean`,
|
|
251
|
+
`child`) leaves it **reachable**, found with `find_tools` as `chi/<name>` and
|
|
252
|
+
run by `call_tool` as the tool itself ([the chi group](#chitool_sourcecall-acts_as-label-wait-prefix));
|
|
253
|
+
`add:` declares it there, `remove:` takes it away.
|
|
248
254
|
|
|
249
255
|
- `name`: a–z first, then a–z, 0–9 and `_`, up to 48 characters.
|
|
250
256
|
- `params`: `{ name => property }`, where a property is a JSON Schema
|
|
@@ -461,6 +467,12 @@ end
|
|
|
461
467
|
tool raises `ArgumentError` at once, as `chi.tool` does, and nothing is
|
|
462
468
|
staged.
|
|
463
469
|
- Inside `register`, use `chi.tool`: `replace_tools` raises there.
|
|
470
|
+
- Whether the session needs `find_tools` and `call_tool` is set by the tools
|
|
471
|
+
registered when its worker started. A tool a set adds on a kit that
|
|
472
|
+
leaves it out is reachable when the kit declares them anyway (`lean` does,
|
|
473
|
+
for its left-out built-ins); on a kit that declares neither it stays out
|
|
474
|
+
of reach until the user declares it (`/toolkit add NAME`), with one warn
|
|
475
|
+
notice by your bundle's name.
|
|
464
476
|
|
|
465
477
|
### `chi.on(event, priority: 100) { |event, ctx| … }`
|
|
466
478
|
|
|
@@ -683,11 +695,12 @@ two tools in all, whatever the number of sources: `find_tools(query,
|
|
|
683
695
|
group?)` (a keyword search that answers each match's `inputSchema`) and
|
|
684
696
|
`call_tool(tool, args)` (calls one as `<group>/<tool>`; its row label is the
|
|
685
697
|
first source's `label`). The index has one group of its own, **`chi`**: the
|
|
686
|
-
built-ins the session's toolkit leaves out but
|
|
687
|
-
([toolkits](configuration.md#toolkits)), each with its
|
|
698
|
+
built-ins and plugin tools (`chi.tool`) the session's toolkit leaves out but
|
|
699
|
+
doesn't remove ([toolkits](configuration.md#toolkits)), each with its
|
|
700
|
+
registered schema; never `find_tools` and `call_tool` themselves.
|
|
688
701
|
The two tools are registered once, after the plugins load, and declared per
|
|
689
|
-
toolkit when it reaches a source group or has a reachable
|
|
690
|
-
with no source declares neither). Nothing a
|
|
702
|
+
toolkit when it reaches a source group or has a reachable tool, as the tools
|
|
703
|
+
stood then (`full` with no source declares neither). Nothing a
|
|
691
704
|
source publishes later changes the request's tools, only what a search
|
|
692
705
|
answers, so the prompt cache keeps: the search tool's description lists the
|
|
693
706
|
source groups as they were at load, and a `chi` line first, per toolkit.
|
|
@@ -702,24 +715,25 @@ session's toolkit. A source's
|
|
|
702
715
|
group named `chi`, or named like an earlier group, is left out of search,
|
|
703
716
|
lookup and the description, with one warning per name.
|
|
704
717
|
|
|
705
|
-
**A
|
|
718
|
+
**A chi tool through `call_tool` runs as that tool.** `call_tool(tool:
|
|
706
719
|
"chi/delegate", args: {…})` (or a bare `delegate` while it's reachable) is
|
|
707
720
|
turned into the `delegate` call itself before the guardrails, so rules on
|
|
708
721
|
`delegate`, approvals (keyed `delegate:…`, the same as a direct call's),
|
|
709
722
|
`before_tool_call`/`after_tool_call` hooks and the row see `delegate`, with
|
|
710
723
|
`call[:via] == "call_tool"`; the row reads "delegate (via call_tool)", live
|
|
711
|
-
and after a reload.
|
|
712
|
-
`
|
|
713
|
-
|
|
714
|
-
|
|
724
|
+
and after a reload. A plugin tool's the same: `chi/web_search` runs as
|
|
725
|
+
`web_search`, with its `label:`, `preview:` and `targets:`. `tool:
|
|
726
|
+
call_tool` rules see MCP calls and misses only. A `chi/` name the toolkit
|
|
727
|
+
removes or declares, or any chi tool once `call_tool` itself is removed, is
|
|
728
|
+
refused by its name before the guardrails (no hook, no card).
|
|
715
729
|
|
|
716
730
|
**On a Messages host the model calls them by name too.** With `api:
|
|
717
731
|
anthropic` (not Fireworks' dialect), every tool `call_tool` reaches goes in
|
|
718
732
|
the request's tools deferred (`defer_loading`: its name only, until
|
|
719
|
-
something loads it): a built-in by its name, a source's tool by its
|
|
733
|
+
something loads it): a built-in or plugin tool by its name, a source's tool by its
|
|
720
734
|
`acts_as` (`mcp_fs_read`; one with a refusal, an acts_as two tools share, or
|
|
721
735
|
a name another tool has stays out). A direct call of one runs whether
|
|
722
|
-
anything loaded it or not: a built-in as itself (as if declared), a
|
|
736
|
+
anything loaded it or not: a built-in or plugin tool as itself (as if declared), a
|
|
723
737
|
source's tool as the `call_tool` call it stands for, so its rules,
|
|
724
738
|
approvals, hooks and row are `call_tool`'s. Elsewhere such a call is
|
|
725
739
|
refused with the `call_tool` route, as before.
|
|
@@ -864,9 +878,9 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
864
878
|
| `ctx.http` | an HTTP client for calling services (`get`, `post_json`, `request`): see [Calling services](#calling-services-ctxhttp) |
|
|
865
879
|
| `ctx.sessions` | fork, send to, read, list and stop other sessions: see [Other sessions](#other-sessions-ctxsessions) |
|
|
866
880
|
| `ctx.toolkit` | the name of the session's toolkit (`full` for a session from before toolkits); nil with no session |
|
|
867
|
-
| `ctx.declared_tools` | the tool names the session's next turn declares (its toolkit resolved, with its own adds and removes): ask this, not the name, whether a tool is there |
|
|
881
|
+
| `ctx.declared_tools` | the tool names the session's next turn declares (its toolkit resolved, with its own adds and removes): ask this, not the name, whether a tool is there. Your bundle's own `chi.tool` tools are in it only on a kit that declares them (`full`, an `add:`); elsewhere they are reachable, not declared (the testing kit answers every tool, as on full, unless `bundle.set(declared_tools: […])`) |
|
|
868
882
|
| `ctx.use_toolkit(name)` | put the session on toolkit `name` (yours from `chi.toolkit`, or chi's): saved, applied at the next turn's start and announced (`toolkit> toolkit: NAME from the next turn's start`, the web's tools chip follows). Only before the session's first model reply and outside a turn (the declared tools start every request, and a plugin doesn't ask first); otherwise it returns false and changes nothing: tell the user to run `/toolkit NAME`. An unknown name raises ArgumentError. Needs chi 0.66.0 |
|
|
869
|
-
| `ctx.load_tools(names)` | load tools the session reaches but doesn't declare, so the model calls them by name: built-ins (`"delegate"`, `"chi/delegate"`), MCP tools (`"mcp_fs_read"`, `"fs/read"`). Returns the names it staged (the wire names); a declared tool, one loaded before, a removed or unknown one is skipped (an unknown one logged), so calling it every turn is fine. From a `before_turn` hook it loads for that turn; anywhere else (mid-turn too) from the next turn's start. Call it from a hook: before the session's first turn its toolkit isn't applied yet and nothing resolves. **A hint, not access**: the tools keep their guardrails and approvals, and only a host that takes tool changes (`api: anthropic`, not Fireworks) gets one, as a system message after the user's (`[tools loaded: delegate (your-bundle)]` in the session, a notice line live); elsewhere nothing is sent or saved, and the tools stay reachable with `call_tool`. A hook may switch the model after you call it, so the return value can't promise delivery. Raises ArgumentError for anything but an Array. Needs chi 0.99.0 |
|
|
883
|
+
| `ctx.load_tools(names)` | load tools the session reaches but doesn't declare, so the model calls them by name: built-ins and plugin tools (`"delegate"`, `"chi/delegate"`, `"chi/web_search"`), MCP tools (`"mcp_fs_read"`, `"fs/read"`). Returns the names it staged (the wire names); a declared tool, one loaded before, a removed or unknown one is skipped (an unknown one logged), so calling it every turn is fine. From a `before_turn` hook it loads for that turn; anywhere else (mid-turn too) from the next turn's start. Call it from a hook: before the session's first turn its toolkit isn't applied yet and nothing resolves. **A hint, not access**: the tools keep their guardrails and approvals, and only a host that takes tool changes (`api: anthropic`, not Fireworks) gets one, as a system message after the user's (`[tools loaded: delegate (your-bundle)]` in the session, a notice line live); elsewhere nothing is sent or saved, and the tools stay reachable with `call_tool`. A hook may switch the model after you call it, so the return value can't promise delivery. Raises ArgumentError for anything but an Array. Needs chi 0.99.0 |
|
|
870
884
|
| `ctx.context` | attach outside text to this session and list it: see [Attached context](#attached-context-ctxcontext) |
|
|
871
885
|
| `ctx.cache_confirm(line, changed:)` | inside a command: nil to apply a prompt-cache-restarting change now, or a confirm to return as `[text, confirm]`: see [Commands that ask](#commands-that-ask-ctxcache_confirm). Needs chi 0.132.0 |
|
|
872
886
|
|
|
@@ -1351,6 +1365,10 @@ context_providers:
|
|
|
1351
1365
|
and becomes the installed bundle's folder each time it runs, so an
|
|
1352
1366
|
upgrade moves nothing. `{ruby}` becomes the Ruby chi runs on (a `ruby` on
|
|
1353
1367
|
the PATH may be another one, such as macOS's 2.6).
|
|
1368
|
+
- A bundle whose `requires_chi` this chi doesn't meet provides nothing:
|
|
1369
|
+
`.resolve` and `.any?` skip it (logged once per process per bundle and
|
|
1370
|
+
version), and a source already attached to one — before a chi downgrade,
|
|
1371
|
+
say — is refused by name, as a changed script is.
|
|
1354
1372
|
- The command follows [the contract](context.md#the-command-contract).
|
|
1355
1373
|
|
|
1356
1374
|
## Block renderers
|
|
@@ -1390,9 +1408,9 @@ block_renderers:
|
|
|
1390
1408
|
|
|
1391
1409
|
## Stable for bundles
|
|
1392
1410
|
|
|
1393
|
-
Bundles normally use only `chi.*` and `ctx`; the constants and
|
|
1394
|
-
are the rest of the surface they may use. A change to any of them gets
|
|
1395
|
-
CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
1411
|
+
Bundles normally use only `chi.*` and `ctx`; the constants, helpers and manifest
|
|
1412
|
+
keys below are the rest of the surface they may use. A change to any of them gets
|
|
1413
|
+
a CHANGELOG line and bumps the shipped bundles that use it. Anything else under
|
|
1396
1414
|
`Samagotchi::` is internal.
|
|
1397
1415
|
|
|
1398
1416
|
**New API ships with its first caller:** a `chi.*` or `ctx.*` method arrives in the same change as a shipped bundle
|
|
@@ -1424,8 +1442,23 @@ that calls it, or for a real external bundle that does. No method on the chance
|
|
|
1424
1442
|
`Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
|
|
1425
1443
|
`Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
|
|
1426
1444
|
a hook's or `rebase --exec`'s environment names).
|
|
1427
|
-
|
|
1428
|
-
|
|
1445
|
+
7. **The manifest schema**: the manifest's keys and shapes, as [A bundle with a plugin](#a-bundle-with-a-plugin)
|
|
1446
|
+
and [bundles.md](bundles.md) document them — `name`, `version`, `scope`, `description`, `files`, `hooks`,
|
|
1447
|
+
`trust_level`, `plugin`, `requires_chi`, `requires_chi_next` (shipped bundles only: the release tooling
|
|
1448
|
+
reads it, chi doesn't), `needs`, `scripts`, `context_providers`, `block_renderers`, `includes`,
|
|
1449
|
+
`session_settings`, `settings`. Those are `Manifest::KEYS`; `Manifest::SINCE` is the chi release each
|
|
1450
|
+
key or shape first worked in, and the docs don't copy it — the code is its truth.
|
|
1451
|
+
- A key keeps its meaning. A new key gets a `SINCE` entry and a CHANGELOG line, and an older chi says
|
|
1452
|
+
it ignores it (`unknown-key`).
|
|
1453
|
+
- A later chi reads every key an earlier chi installed: the install keeps the bundle's `manifest.yml`
|
|
1454
|
+
([Trust](#trust)). One installed before chi kept manifests reads what its install recorded until it is
|
|
1455
|
+
upgraded.
|
|
1456
|
+
- A bundle declares its floor with `requires_chi`, and an install below it is refused (`--force` aside).
|
|
1457
|
+
There is no `api_version:`.
|
|
1458
|
+
- `block_renderers:` and the frame's `chi.defineRenderer` ([Block renderers](#block-renderers)) are part
|
|
1459
|
+
of it. Neither is Ruby, so the scan below doesn't see them.
|
|
1460
|
+
|
|
1461
|
+
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of items 1-6,
|
|
1429
1462
|
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
|
1430
1463
|
(and its specs) reaches beyond it, each with why: the APIs to design before
|
|
1431
1464
|
that bundle could leave chi's repo. `spec/samagotchi/testing/boundary_spec.rb`
|
|
@@ -1433,9 +1466,6 @@ fails on a reference in neither (docs/testing.md). In `chi update`'s re-check of
|
|
|
1433
1466
|
an installed bundle the same scan warns only for a name this chi no longer has
|
|
1434
1467
|
(`Boundary.gone?`): a reference that still resolves gets no row.
|
|
1435
1468
|
|
|
1436
|
-
A bundle's `block_renderers:` and the frame's `chi.defineRenderer` ([Block renderers](#block-renderers)) are stable
|
|
1437
|
-
too. They aren't Ruby, so the scan above doesn't see them.
|
|
1438
|
-
|
|
1439
1469
|
**Used by shipped bundles today, but internal** (may change without notice; a
|
|
1440
1470
|
bundle that needs one should ask for an API instead):
|
|
1441
1471
|
|
|
@@ -1480,6 +1510,7 @@ bundle.judgments.runs("progress").last["asked"] # the runs as chi saves
|
|
|
1480
1510
|
bundle.sessions.on(:children) { |all:| [] } # ctx.sessions, scripted and recorded
|
|
1481
1511
|
bundle.http.stub(:post, "http://127.0.0.1:9000/classify", json: { label: "safe" }) # ctx.http, never the network
|
|
1482
1512
|
bundle.set(running: false) # the session's values: cwd:, messages:, cancelled: …
|
|
1513
|
+
bundle.set(declared_tools: %w[read]) # ctx.declared_tools (default: every tool, as on full)
|
|
1483
1514
|
bundle.set(cost: 2.5) # ctx.cost: reported USD, or { reported:, estimated:, side:, known: }
|
|
1484
1515
|
bundle.set(usage: { tool_calls: 40, active_ms: 600_000 }) # ctx.usage: { turns:, tool_calls:, tool_calls_by_tool:, steps:, active_ms:, tool_ms_by_tool: }
|
|
1485
1516
|
bundle.set(turn_author: "p:p_0123abcd") # a participant's turn: ctx.turn_author, and author: on every event
|
|
@@ -1551,7 +1582,8 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
|
|
|
1551
1582
|
`Samagotchi::Testing::Case` (the same helpers).
|
|
1552
1583
|
- **Installed, not loaded from source:** `Samagotchi::Testing::Install.run(dir, settings: {})` (or
|
|
1553
1584
|
`bundle.install`) installs the bundle into the sandbox as `chi bundle install` does (sha256 and `requires_chi`
|
|
1554
|
-
checked
|
|
1585
|
+
checked: a bundle whose `requires_chi` this chi doesn't meet raises `InstallError` unless `force: true`)
|
|
1586
|
+
and loads the installed copy with chi's own loaders. It returns `warnings` (the Installer's),
|
|
1555
1587
|
`plugin_failures` / `hook_failures` (each with the `location` in your source, `plugin.rb:12`) and `registered`
|
|
1556
1588
|
(commands, tools, hooks per event, inits, toolkits, tool and peer sources). A name chi ships raises
|
|
1557
1589
|
`MemoryBundle::Installer::InstallError` unless `force: true`.
|
|
@@ -1579,7 +1611,9 @@ Plugins load when a session starts (`Engine.new`, in the REPL or a
|
|
|
1579
1611
|
worker), after the bundle hooks. The steps are:
|
|
1580
1612
|
|
|
1581
1613
|
1. The file must match the sha256 recorded at install (not checked for a bundle installed with `--dev`).
|
|
1582
|
-
2. chi must meet `requires_chi`.
|
|
1614
|
+
2. chi must meet `requires_chi`. A bundle whose `requires_chi` this chi doesn't
|
|
1615
|
+
meet is refused at install, so this step is reached only by one installed
|
|
1616
|
+
with `--force` or by a chi that has since been downgraded.
|
|
1583
1617
|
3. The file is `module_eval`'d into a new module in the bundle's namespace
|
|
1584
1618
|
(`Samagotchi::Bundles::<bundle>`).
|
|
1585
1619
|
4. The class is built, and `register(chi)` runs.
|
|
@@ -1609,6 +1643,11 @@ gem.
|
|
|
1609
1643
|
source directory's, and an edit loads at the next start.
|
|
1610
1644
|
- Install only copies the file (`--dev` links it). The code first runs at the
|
|
1611
1645
|
next session start.
|
|
1646
|
+
- The bundle's `manifest.yml` is kept at install and checked the same way: chi
|
|
1647
|
+
reads hook events, `context_providers:`, `block_renderers:`, settings and
|
|
1648
|
+
`requires_chi` from it only while it has the sha256 recorded at install. An
|
|
1649
|
+
edited one isn't read (chi falls back to what the install recorded). `--dev`
|
|
1650
|
+
links it, unchecked.
|
|
1612
1651
|
- Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
|
|
1613
1652
|
chi's own tools; path and command rules see what `targets:` says.
|
|
1614
1653
|
|
|
@@ -1616,18 +1655,30 @@ gem.
|
|
|
1616
1655
|
|
|
1617
1656
|
- `chi bundle install <dir>`: copies the plugin to
|
|
1618
1657
|
`<memories>/.bundles/<name>/plugin/`. It records the plugin's sha256 and
|
|
1619
|
-
`requires_chi`, and warns about a wrong declared sha256
|
|
1620
|
-
`requires_chi
|
|
1658
|
+
`requires_chi`, and warns about a wrong declared sha256. A bundle whose
|
|
1659
|
+
`requires_chi` this chi doesn't meet is **refused**: `Install failed: NAME
|
|
1660
|
+
can't be installed: it requires chi >= X (this is chi Y)`, exit 1, nothing
|
|
1661
|
+
written. `--force` installs it anyway, with the warnings that its plugin,
|
|
1662
|
+
hooks and rules won't load. It keeps the bundle's `manifest.yml` beside the record, and
|
|
1663
|
+
chi reads the manifest from there at load, so for a bundle installed by this
|
|
1664
|
+
chi or later, a key this chi didn't know at install works once a newer chi
|
|
1665
|
+
reads it (one installed before reads what its install recorded until
|
|
1666
|
+
`chi bundle upgrade <name>`).
|
|
1667
|
+
- `chi bundle upgrade <name>`: upgrades an installed bundle by name from the
|
|
1668
|
+
source its record keeps (its folder, archive or git URL, `#ref` included),
|
|
1669
|
+
so a bundle from elsewhere needs no path retyped; `upgrade <source>` takes a
|
|
1670
|
+
new one and says `Source changes: old → new`. A dev install is refused: it
|
|
1671
|
+
already follows its source.
|
|
1621
1672
|
- `chi bundle status [<name>]`: shows `plugin=plugin.rb` in the list. For
|
|
1622
1673
|
one bundle it shows `Plugin: plugin.rb [ok|modified|missing]` and a
|
|
1623
1674
|
`requires_chi` failure.
|
|
1624
1675
|
- `chi bundle diff <name> [plugin.rb]`: shows the installed base and the
|
|
1625
|
-
file on disk.
|
|
1676
|
+
file on disk; for a memory file that stays behind, also the bundle's new text.
|
|
1626
1677
|
- `chi bundle build --name <name>`: puts the installed plugin (and
|
|
1627
1678
|
`requires_chi`, its `scripts/` and `context_providers:`) into the built
|
|
1628
1679
|
bundle.
|
|
1629
1680
|
- `chi bundle uninstall <name>`: removes the plugin with the bundle.
|
|
1630
|
-
- `chi bundle install --dev <dir>`: links the bundle's plugin, hooks, rules and
|
|
1681
|
+
- `chi bundle install --dev <dir>`: links the bundle's plugin, hooks, rules, scripts and `manifest.yml` to `<dir>` instead of
|
|
1631
1682
|
copying them, with no sha256 check, so an edit applies at the next worker start
|
|
1632
1683
|
([cli.md](cli.md#writing-a-bundle-new-test-check)).
|
|
1633
1684
|
- `chi bundle new`, `test` and `check`: start a bundle, run its tests on chi's Ruby, check it as an install takes
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, web-search, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
@@ -21,7 +21,9 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
21
21
|
- a bundle that uses something new in chi marks it in the same change with
|
|
22
22
|
`requires_chi_next: true` (or a short reason) in its `manifest.yml`, and
|
|
23
23
|
leaves `requires_chi` alone: raising it to a chi that isn't released yet
|
|
24
|
-
would make chi on main refuse its own bundle
|
|
24
|
+
would make chi on main refuse its own bundle outright — an unmet
|
|
25
|
+
`requires_chi` now stops the install, not just the load (`--force` is the
|
|
26
|
+
only way past it). `release:bump` raises a
|
|
25
27
|
marked bundle's `requires_chi` to `">= X.Y.Z"` and drops the marker;
|
|
26
28
|
`release:check` fails while a marker is left. chi itself ignores the key,
|
|
27
29
|
and `bundles:check` notes it (and fails a marker that isn't `true` or a
|
|
@@ -30,6 +32,9 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
30
32
|
- a new `ctx.*` / `chi.*` call a plugin may make gets its release's version in `Plugin::Api::SINCE` once
|
|
31
33
|
released (`chi bundle check`'s `requires-chi-floor` reads it); an unreleased one waits there as an `# Unreleased` comment, and
|
|
32
34
|
`release:check` fails while that comment is left: move each call up with the new version;
|
|
35
|
+
- a new manifest key or shape gets its release's version in `Manifest::SINCE` the same way (check's
|
|
36
|
+
`requires-chi-floor` reads it for the manifest too), waits as an `# Unreleased` comment while unreleased,
|
|
37
|
+
and `release:check` fails while that comment is left;
|
|
33
38
|
- a new shipped bundle joins a profile (`includes:` in `core` or `dev`; a
|
|
34
39
|
spec checks every bundle is in exactly one), and that profile's version
|
|
35
40
|
goes up: `chi update` then installs only the new bundle for those who
|
|
@@ -72,7 +77,7 @@ as the GitHub release body.
|
|
|
72
77
|
| `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. |
|
|
73
78
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
74
79
|
| `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), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line. No commit. |
|
|
75
|
-
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, no `# Unreleased` comment left in `Plugin::Api::SINCE`, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), 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). |
|
|
80
|
+
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, no `# Unreleased` comment left in `Plugin::Api::SINCE` or `Manifest::SINCE`, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), 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). |
|
|
76
81
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
77
82
|
|
|
78
83
|
## Runbook
|
data/docs/sessions.md
CHANGED
|
@@ -102,7 +102,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
102
102
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
103
103
|
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new sessions start in that folder; the header chip says `<project> · all`, and `all` opens the same place as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a repo it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a repo). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat, and the grip on the chat's top edge — the same thin handle as the composer's, its twin — puts them away and brings them back: a drag up (or a click, or Enter/Space on it) hides them, a drag down (or a click) shows them again, and the choice is remembered. A hidden strip leaves its status in the top bar (`N sessions · ● 1 live · ● 1 waiting`, from the cards), and a click on that pill opens All sessions, without showing the strip first; there the pill shows whatever the strip's state, pressed, and a click on it goes back to the chat (as Esc does). In a window under 900 px wide, where the strip scrolls sideways, the grip stays under it, outside the scroller; a window under 700 px tall hides the strip by itself, so a question or approval card in the stage keeps room, and the grip shows it until the window is tall again. A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too; just `▸ 2 delegates` while the rows are open: they carry the marks then) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`; its header is the top bar's right end: a search over preview, id and status, the count, an `archived` toggle and Select (on a phone a row of its own under the bar). Esc or the pressed pill returns; where the sessions are stored is a faint line under the list. A card is its title on one line (an untitled session reads `Untitled` over its first message), a line under it (its recap, else its first message) and its meta row, with one mark per state and no status dot: a waiting card has its warn outline and its `question` (or `approval`…) chip, an errored one an `error` badge, a session a worker runs the `live` badge (not on a waiting card: waiting means a worker), and idle, completed and stopped ones are unmarked; a family's and a chain's rows the same. There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 · 7 delegates` (`3 / 14` while a search narrows it), a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new session. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar's id copies the full session id on click ("Session id copied"), the id every CLI command takes (`chi --attach ID`, `chi send ID`). A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its ⚙ chip, after the model, holds the session's thinking level, reasoning history and LLM context: `⚙` alone while the session follows the model for all three, else `⚙` and what the session set, in the accent colour (`⚙ think low · ctx stale,forget`; a phone shows ⚙ with a dot; the tooltip gives each value and its source). Its panel is the start page's ⚙ panel for this session: a Thinking row (`/thinking`), on a chat host a Reasoning row (the reasoning history, `/reasoning-history`), and a Context row with its Apply and budget fields (`/llm-context`); a tap runs that row's command at once in the worker (one reply in the conversation, as a typed one) and changes that field alone, the Context row's `default` resets the strategy, apply rule and budget (`/llm-context reset`), and a tap while a turn runs is queued for after it (the panel's note says so). `GET /api/sessions/:id`'s `session.thinking`, `session.reasoning_history` (null on a native host) and `session.llm_context` carry the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. After ⚙, the 🧰 chip names the session's toolkit: `🧰` alone on the configured default (`toolkit.default`, `session.toolkit_default`), `🧰 lean` on another (see [Toolkits](configuration.md#toolkits); `/toolkit` changes it). A `/thinking` or `/reasoning-history` change typed in the composer that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the ⚙ panel sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`); after a `/model` switch the info bar's tooltip adds a `per model:` row per run of turns on one model above those totals (`splash:qwen3-35b · 6 turns · 96.0k in / 9.0k out · ~$0.02`, a switch's prefill under its row: `└ switch prefill 58.0k uncached prompt ≈$0.01`; the timing's `model_segments`, replaced by each `generation_completed`'s; the session cards stay as they were), and right after the switch its ctx % is the new model's window's; and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new session starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`; `toolkits`: `{default, suggest, kits: [{name, description?}]}`, the start page's 🧰 list: the configured `toolkit.default`, `suggest` `on` only when a new session's first message may get a suggested kit (`toolkit.suggest.mode: on`, a System 1 host, two described kits; else `audit` or `off`), and the built-in and config kits a new session can start on, without `review`, `child`, `undecided`, a kit `groups:` refuses or one that doesn't resolve (a base that is a bundle kit), the default always listed; left out when it can't be read), and `POST /api/sessions` takes `model` (blank means the default). Beside it, ⚙ opens the new session's rarely used settings (thinking and LLM context, following each `GET /api/models` row's `thinking` and `llm_context`, and the review files; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), and 🧰 its toolkit: **Auto** (with `toolkit.suggest.mode: on`: the first message may get a suggested kit, else the default) or a kit from the payload's `toolkits`, sent as `POST /api/sessions`'s `toolkit` (see [configuration.md](configuration.md#toolkits)); a refused kit (`400 toolkit_refused`) is said in the list, opened again. What is set shows on the row as an accent chip (`think high`, `tools: lean`; a dot on ⚙ or 🧰 on a phone); each is for the next new session only, never remembered. The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
104
104
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
105
|
-
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, with Stop turn in its status row while it runs, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model, three groups with hairlines between them, on one line above a phone's width (the folder gives way first): the setup (id, status, model, the ⚙ and 🧰 chips, notes, the memory or working-set chip), the relations (chain links, the parent as `↑ <id>`, delegates, participants; only when there is one) and the telemetry (folder, ctx, speed, the session's time, worker badge)), `settings_chip.js` (the session bar's ⚙ chip and its panel over `settings_panel.js`: a tap runs that row's command, one field), `toolkit_chip.js` (the session bar's 🧰 chip: the kit against `toolkit.default`), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker, its ⚙ panel's and 🧰 list's choices for the next new session only and their override chips; a fresh chain link's picker and its `/model`), `start_settings.js` (the start page's ⚙ panel: thinking and LLM context as rows of segments, applied at once, the Apply and budget disclosure, the review files' view; the override chips' words and ⚙'s dot), `settings_panel.js` (the panel's rows as lit-html templates, shared: the segments, the disclosure, a hidden row's sent-anyway line, the review line; the caller passes its rows and applies the picks), `start_toolkit.js` (the start page's 🧰 list: Auto with suggest on, the kits, the default marked; a pick closes it; its override chip and dot), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
105
|
+
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, with Stop turn in its status row while it runs, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model, three groups with hairlines between them, on one line above a phone's width (the folder gives way first): the setup (id, status, model, the ⚙ and 🧰 chips, notes, the memory or working-set chip), the relations (chain links, the parent as `↑ <id>`, delegates, participants; only when there is one) and the telemetry (folder, ctx, speed, the session's time, worker badge)), `settings_chip.js` (the session bar's ⚙ chip and its panel over `settings_panel.js`: a tap runs that row's command, one field), `toolkit_chip.js` (the session bar's 🧰 chip: the kit against `toolkit.default`), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts (the restarting set is `version_notices.js`'s) as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `strip_hidden.js` (the strip hidden or shown: the saved choice, the short window's own hide, the grip and the body's class), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker, its ⚙ panel's and 🧰 list's choices for the next new session only and their override chips; a fresh chain link's picker and its `/model`), `start_settings.js` (the start page's ⚙ panel: thinking and LLM context as rows of segments, applied at once, the Apply and budget disclosure, the review files' view; the override chips' words and ⚙'s dot), `settings_panel.js` (the panel's rows as lit-html templates, shared: the segments, the disclosure, a hidden row's sent-anyway line, the review line; the caller passes its rows and applies the picks), `start_toolkit.js` (the start page's 🧰 list: Auto with suggest on, the kits, the default marked; a pick closes it; its override chip and dot), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
106
106
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
107
107
|
- The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn isn't the message box's: it sits in the running turn's stage, at the right edge of its status row beside the fold caret (collapsed stage or not), while the turn runs, and goes when the turn ends A turn the page knows runs but has nothing of to draw (a reload whose session payload carries no current turn) shows that status row alone, `running` and Stop turn. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only. The fold keeps the height the grip set (`chi_composer_floor`): the box opens again at the height it had, and only a double-click on the grip forgets it. That height never takes more than half of the window the box is drawn in: a floor saved in a bigger window is held to that half in a smaller one (a rotated phone, a resized window, another device on the same origin), and the saved floor itself stays, so the height comes back when the room does. A click on the expanded grip hides the box as well: it folds 250 ms after the press, so a second click inside that window is read as a double-click and resets the height instead (a drag, Enter or Space act at once, and a drag after a click takes the wait back). The start page's grip only resizes.
|
|
108
108
|
|
|
@@ -495,7 +495,7 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
495
495
|
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-output-limits.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `↑ <parent id>` in its info bar, its tooltip `delegated by session …`; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
|
496
496
|
|
|
497
497
|
- `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one. The wait (`wait: true`, a later `delegate_result`) and the delegate report are for the reply of the child's turn the follow-up went to: its running turn, which it merges into, or the turn of its own it runs when the child is idle (`turn` in `delegates.json`, set when the child's worker names `turn_ids`), not a newer reply of a turn queued behind it. A follow-up merged as a steer into a turn the child started at that moment (a wake, a reminder, another message) gets that turn's reply or question. A child on its first turn, or with an older worker, gives its next reply, as before.
|
|
498
|
-
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's. With no `cwd:` and a task that names another checkout of the same repository (a path in the task belonging to a linked worktree, read from the files with `Guardrails::GitInfo.checkouts`, no git process), the result carries `Warning: this child runs in <folder>, but its task names <checkout>. If it should work there, stop it (chi sessions stop <id>) and call delegate again without session, with cwd: <checkout>.` One line however many are named; nothing for a path of the child's own checkout, a folder that isn't one,
|
|
498
|
+
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's. With no `cwd:` and a task that names another checkout of the same repository (a path in the task belonging to a linked worktree, read from the files with `Guardrails::GitInfo.checkouts`, no git process), the result carries `Warning: this child runs in <folder>, but its task names <checkout>. If it should work there, stop it (chi sessions stop <id>) and call delegate again without session, with cwd: <checkout>.` One line however many are named; nothing for a path of the child's own checkout, a folder that isn't one, a path with spaces, or the main checkout when the child runs in a linked worktree of it (the work belongs in the worktree, so naming a main-checkout file is not a forgotten `cwd:`). The child starts as it would anyway, and with `wait: true` (the default) the line comes after the child ran, so it only helps with `wait: false`. A `Warning:` for an unknown model id comes first, then this one. `Log.info(:worker, "delegate_other_checkout", parent:)` records each line.
|
|
499
499
|
- `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
|
|
500
500
|
- A wait also ends early when a person's message waits for the parent's turn's next step (the `task_wait` rule: `Engine#input_waiting?`, `SessionInbox.steer_waiting?`, never in a participant's turn): `ReplyWait`'s `message_waiting:` probe gives `:message_waiting`, checked after every outcome the child can arrive at (a reply, a question, an end, a gone worker) and a cancel, before the deadline, and the result says `no reply yet: a message from the user is waiting; the child keeps running. Read the message first.` with the same last line as a timeout's. The child is never touched, the cursor doesn't move (the reply still comes, as a report or at the next `delegate_result`), a relayed approval's outcome line stays in the result, and a reserved report reads as for a timeout. A delegate child's report doesn't end a wait. This holds for `delegate`'s own wait too (both go through `DelegateWait`).
|
|
501
501
|
- A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
|
data/docs/testing.md
CHANGED
|
@@ -27,7 +27,7 @@ It also checks `spec/e2e/` for two things: `no-unused-vars`, and the Playwright
|
|
|
27
27
|
Wait for the state (a locator's `expect`, `data-phase`, an animation's end through `waitForFunction`); time the page
|
|
28
28
|
keeps (a pointer's dwell, a hold) runs on Playwright's `page.clock` (`install`, then `pauseAt` and `runFor`: the
|
|
29
29
|
stage's hand-off dwell in stage.spec.js); a pause that is the behaviour outside the page (a slow post held in a
|
|
30
|
-
route) carries `// eslint-disable-line no-restricted-syntax -- why`. And in the
|
|
30
|
+
route) carries `// eslint-disable-line no-restricted-syntax -- why`. And in the seven
|
|
31
31
|
modules that hold the owner page's session state, one more: no write into `current`, `store.current` or `state()`
|
|
32
32
|
in place (`samagotchi/session-writes`; the store is the write path, see internals/web-frontend.md, The session store).
|
|
33
33
|
|
|
@@ -56,7 +56,7 @@ exception's text changes between Ruby versions: write the string yourself).
|
|
|
56
56
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_block.js`,
|
|
57
57
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
58
58
|
`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`,
|
|
59
|
-
`chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `start_settings.js`, `settings_panel.js`, `start_toolkit.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`,
|
|
59
|
+
`chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `start_settings.js`, `settings_panel.js`, `start_toolkit.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`,
|
|
60
60
|
`vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
61
61
|
never shipped; lit reads `document` as it loads). The shim's `markupOf(template)`,
|
|
62
62
|
`htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
|
|
@@ -76,10 +76,10 @@ module Samagotchi
|
|
|
76
76
|
def for_installed
|
|
77
77
|
taken = {}
|
|
78
78
|
MemoryBundle::Provenance.each_installed do |bundle, record|
|
|
79
|
-
|
|
80
|
-
next if
|
|
79
|
+
manifest = record.manifest
|
|
80
|
+
next if manifest.requires_chi_failure(VERSION)
|
|
81
81
|
|
|
82
|
-
renderers(bundle,
|
|
82
|
+
renderers(bundle, manifest).each do |renderer|
|
|
83
83
|
taken[renderer.lang] ||= Installed.new(lang: renderer.lang, bundle: bundle, files: renderer.load)
|
|
84
84
|
end
|
|
85
85
|
end
|
|
@@ -104,11 +104,12 @@ module Samagotchi
|
|
|
104
104
|
raise Invalid, "bundle #{bundle}'s scripts/#{file} differs from the installed one (reinstall the bundle)"
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
# Its parsed renderers; a list that doesn't parse is none (logged).
|
|
108
|
+
def renderers(bundle, manifest)
|
|
109
|
+
if (problem = manifest.problems["block_renderers"])
|
|
110
|
+
Log.warn(:web, "block_renderers_unreadable", bundle: bundle, msg: problem)
|
|
111
|
+
end
|
|
112
|
+
manifest.block_renderers
|
|
112
113
|
end
|
|
113
114
|
private_class_method :renderers
|
|
114
115
|
end
|
|
@@ -84,6 +84,7 @@ module Samagotchi
|
|
|
84
84
|
if manifest
|
|
85
85
|
missing = check_files(manifest)
|
|
86
86
|
check_source_tree(manifest, missing) unless @installed
|
|
87
|
+
check_keys(manifest)
|
|
87
88
|
check_requires_chi(manifest)
|
|
88
89
|
check_trust(manifest) unless @installed
|
|
89
90
|
check_clash(manifest)
|
|
@@ -147,6 +148,44 @@ module Samagotchi
|
|
|
147
148
|
index && (index + 1)
|
|
148
149
|
end
|
|
149
150
|
|
|
151
|
+
# ── the manifest's own keys ───────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
# A key this chi's parser never reads is one it ignores, and a key whose
|
|
154
|
+
# floor is above what requires_chi admits is one an older chi would
|
|
155
|
+
# ignore too. The floor is the author's fix, so an installed bundle gets
|
|
156
|
+
# only the unknown-key line, as check_floor's call floor does today.
|
|
157
|
+
def check_keys(manifest)
|
|
158
|
+
manifest.unknown_keys.each do |key|
|
|
159
|
+
add(:warn, :"unknown-key", "manifest.yml", MemoryBundle::Manifest.unknown_key_line(key), line: manifest_line(key),
|
|
160
|
+
fix: "update chi, or remove #{key}: if it's a typo")
|
|
161
|
+
end
|
|
162
|
+
check_key_floor(manifest) unless @installed
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The manifest uses a key the oldest chi requires_chi admits never read.
|
|
166
|
+
# No plugin needed: the key is the bundle's own claim.
|
|
167
|
+
def check_key_floor(manifest)
|
|
168
|
+
key, version = manifest.floor_keys.max_by { |_, floor| Gem::Version.new(floor) }
|
|
169
|
+
return if key.nil?
|
|
170
|
+
|
|
171
|
+
bound = lower_bound(manifest.requires_chi)
|
|
172
|
+
return if bound && Gem::Version.new(version) <= bound
|
|
173
|
+
|
|
174
|
+
add(:warn, :"requires-chi-floor", "manifest.yml",
|
|
175
|
+
"#{manifest.requires_chi ? "requires_chi #{manifest.requires_chi.inspect} admits chi #{bound}," : "no requires_chi,"} " \
|
|
176
|
+
"but the manifest uses #{key_label(key)}, which needs chi #{version}",
|
|
177
|
+
line: manifest_line(key.split(".").first), fix: "requires_chi: \">= #{version}\"")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# How to name a SINCE key in a message: a shape is not a key on its own.
|
|
181
|
+
def key_label(key)
|
|
182
|
+
case key
|
|
183
|
+
when "files.description" then "a files: entry given as a mapping"
|
|
184
|
+
when "session_settings.types", "settings.types" then "#{key.split(".").first}: as a mapping of keys to types"
|
|
185
|
+
else "#{key}:"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
150
189
|
# ── files ─────────────────────────────────────────────────────────
|
|
151
190
|
|
|
152
191
|
# @return [Boolean] whether a listed file is missing
|
|
@@ -340,15 +379,16 @@ module Samagotchi
|
|
|
340
379
|
return if manifest.settings.nil? || @settings.empty?
|
|
341
380
|
|
|
342
381
|
require_relative "../bundle_settings_check"
|
|
343
|
-
|
|
344
|
-
session_settings: manifest.session_settings)
|
|
345
|
-
BundleSettingsCheck.record_warnings(record, @settings).each { |line| add(:warn, :settings, "manifest.yml", line, line: manifest_line("settings")) }
|
|
382
|
+
BundleSettingsCheck.record_warnings(manifest, @settings).each { |line| add(:warn, :settings, "manifest.yml", line, line: manifest_line("settings")) }
|
|
346
383
|
end
|
|
347
384
|
|
|
348
385
|
def install_warnings(warnings)
|
|
349
386
|
stale_reported = @findings.any? { |f| f.code == :sha }
|
|
350
387
|
warnings.each do |warning|
|
|
351
388
|
next if stale_reported && warning.start_with?("Checksum mismatch")
|
|
389
|
+
# An unknown manifest key is check_keys' row, with the fix the install
|
|
390
|
+
# can't offer; the same condition twice would be two rows.
|
|
391
|
+
next if warning.include?("doesn't know the manifest key")
|
|
352
392
|
next if @too_new && warning.include?("won't load:")
|
|
353
393
|
# This machine's PATH, said at install; nothing a chi update changed.
|
|
354
394
|
next if @installed && warning.start_with?("needs ")
|
|
@@ -58,13 +58,17 @@ module Samagotchi
|
|
|
58
58
|
private_class_method :in_child
|
|
59
59
|
|
|
60
60
|
# What the child sends: an edited file, which the copy's own shas can't
|
|
61
|
-
# tell,
|
|
62
|
-
#
|
|
61
|
+
# tell, a manifest key that doesn't parse (the copy holds the mirror's
|
|
62
|
+
# value or none: InstalledManifest#key_problems), and Check's warnings
|
|
63
|
+
# and errors, naming the installed bundle's folder rather than the copy's.
|
|
63
64
|
def answer_of(bundle)
|
|
64
65
|
edited = MemoryBundle::ShippedUpdate.edited_executables(bundle.name, bundle).map do |file|
|
|
65
66
|
Check::Finding.new(level: :warn, code: :edited, file: file, line: nil, fix: "chi bundle upgrade #{bundle.name}, or reinstall it",
|
|
66
67
|
message: "changed since it was installed, so chi doesn't load it")
|
|
67
68
|
end
|
|
69
|
+
edited += bundle.manifest.key_problems.values.map do |message|
|
|
70
|
+
Check::Finding.new(level: :error, code: :manifest, file: "manifest.yml", line: nil, message: message, fix: nil)
|
|
71
|
+
end
|
|
68
72
|
edited = edited.map { |finding| { level: finding.level, text: finding.to_s } }
|
|
69
73
|
installed_dir = MemoryBundle::Provenance.new(name: bundle.name).bundle_dir
|
|
70
74
|
edited + MemoryBundle::InstalledCopy.with(bundle) do |dir|
|
|
@@ -101,6 +101,9 @@ module Samagotchi
|
|
|
101
101
|
raise
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# The environment to spawn a child with: what the sandbox keeps, with
|
|
105
|
+
# every key it dropped present as nil (ChildEnv.spawn_env), so the
|
|
106
|
+
# child doesn't inherit it from this process.
|
|
104
107
|
def child_env = @child_env.dup
|
|
105
108
|
|
|
106
109
|
# Installs the tried bundle, then the shipped guardrails and
|
|
@@ -219,7 +222,12 @@ module Samagotchi
|
|
|
219
222
|
# The user's environment (their hosts and keys) minus what no child
|
|
220
223
|
# chi takes from its spawner (ChildEnv), the worker markers, this
|
|
221
224
|
# chi's log and test settings, and the env forms of the config
|
|
222
|
-
# sections the sandbox drops.
|
|
225
|
+
# sections the sandbox drops. ChildEnv.spawn_env makes every dropped
|
|
226
|
+
# key an unset in the child: a Hash handed to Open3 or Process.spawn
|
|
227
|
+
# is merged onto this process's environment, so deleting a key from
|
|
228
|
+
# it alone still passes it on. The Bundler changes come from ENV, not
|
|
229
|
+
# from this copy: read from the copy, a key dropped above would look
|
|
230
|
+
# like one Bundler added and come back with its value.
|
|
223
231
|
def build_child_env
|
|
224
232
|
env = ChildEnv.scrub(ENV.to_h)
|
|
225
233
|
env.delete("SAMAGOTCHI_LOG_FILE")
|
|
@@ -235,8 +243,8 @@ module Samagotchi
|
|
|
235
243
|
"SAMAGOTCHI_KEYCHAIN" => "on",
|
|
236
244
|
**@layout.env
|
|
237
245
|
)
|
|
238
|
-
env.merge!(ChildEnv.unbundled
|
|
239
|
-
env
|
|
246
|
+
env.merge!(ChildEnv.unbundled)
|
|
247
|
+
ChildEnv.spawn_env(env)
|
|
240
248
|
end
|
|
241
249
|
end
|
|
242
250
|
end
|