samagotchi 0.151.0 → 0.152.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/docs/bundle-authoring.md +4 -4
  4. data/docs/cli.md +32 -7
  5. data/docs/guardrails.md +4 -0
  6. data/docs/internals/block-renderers.md +3 -1
  7. data/docs/internals/bundles.md +48 -14
  8. data/docs/internals/objectives.md +6 -4
  9. data/docs/memory.md +6 -1
  10. data/docs/plugins.md +51 -14
  11. data/docs/releasing.md +7 -2
  12. data/docs/sessions.md +1 -1
  13. data/lib/samagotchi/block_renderers.rb +9 -8
  14. data/lib/samagotchi/bundle_command/check.rb +43 -3
  15. data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
  16. data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
  17. data/lib/samagotchi/bundle_command.rb +88 -10
  18. data/lib/samagotchi/bundle_needs.rb +4 -6
  19. data/lib/samagotchi/bundle_settings_check.rb +14 -13
  20. data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
  21. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  22. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  23. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  24. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  25. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  26. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  27. data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
  28. data/lib/samagotchi/child_env.rb +15 -0
  29. data/lib/samagotchi/context_providers.rb +42 -10
  30. data/lib/samagotchi/engine.rb +27 -4
  31. data/lib/samagotchi/extension_load.rb +4 -3
  32. data/lib/samagotchi/guardrail_wiring.rb +28 -19
  33. data/lib/samagotchi/guardrails/approval.rb +2 -2
  34. data/lib/samagotchi/guardrails/rules.rb +1 -0
  35. data/lib/samagotchi/guardrails/verdict.rb +20 -0
  36. data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
  37. data/lib/samagotchi/memory_bundle/asset_installer.rb +11 -8
  38. data/lib/samagotchi/memory_bundle/builder.rb +36 -30
  39. data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
  40. data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
  41. data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
  42. data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -46
  43. data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -15
  44. data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
  45. data/lib/samagotchi/memory_bundle/installer.rb +90 -16
  46. data/lib/samagotchi/memory_bundle/manifest.rb +146 -6
  47. data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
  48. data/lib/samagotchi/memory_bundle/merger.rb +7 -8
  49. data/lib/samagotchi/memory_bundle/profile.rb +3 -2
  50. data/lib/samagotchi/memory_bundle/provenance.rb +217 -18
  51. data/lib/samagotchi/memory_bundle/shas.rb +4 -3
  52. data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
  53. data/lib/samagotchi/memory_bundle/status.rb +11 -10
  54. data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
  55. data/lib/samagotchi/plugin/loader.rb +1 -1
  56. data/lib/samagotchi/session_config.rb +5 -4
  57. data/lib/samagotchi/system_one/record.rb +10 -2
  58. data/lib/samagotchi/tools/delegate.rb +20 -2
  59. data/lib/samagotchi/update_command.rb +13 -2
  60. data/lib/samagotchi/version.rb +1 -1
  61. data/lib/samagotchi/web/app.rb +2 -1
  62. data/lib/samagotchi/web/session_access_routes.rb +2 -1
  63. data/lib/samagotchi/web/session_hub.rb +6 -0
  64. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24e1f2b0d0bd36a11e7fc2de449630696a6d7e3431c24b6def08a70dd3d27487
4
- data.tar.gz: 42ae4e62a4474d254ee294a887a856fde273d99f7a7d6b5adc961f08eaf2fb7d
3
+ metadata.gz: 6d208a5aecb9745a21107ab97399c7ec4901c24fd781e3ba4ec1658a69764ded
4
+ data.tar.gz: abc21f8bfe39f0bdc614b70b755dd3cd195d016dd305b6d3ba28b018d0b75d54
5
5
  SHA512:
6
- metadata.gz: fde71cbc5d64e63d34a0c85cae516c8f0f37aecda9d1b099803f023b00e31a95653374c3f8725fa82e4dcfd72aefd65b1de4d36f3a6823f0a55f6028a3a25aa4
7
- data.tar.gz: 5ead8e63f46e4b8fdb10ea3b19706c8733a0ff1e30d5f129fc3dd4e1595c92d13d6a0da424335963bcc1683e3309eb73958470951b02607b7255ba960b201405
6
+ metadata.gz: 1105443bb7097fd93f0ab4bb1f0f9926fbbc9c89f9d57584d12ae3ac560b726498c607c1e897416e8a17f0e150076fc7de2b2b19b0919b49ad5b40645dd3883a
7
+ data.tar.gz: ba009b4669d67eb24ccd15cf669f84431a1738153b721c13c18633d6b05f32cdd4bfd01d25530b18ef5ab39cbf84e0b19688691f6751884c92e0a8d412935771
data/CHANGELOG.md CHANGED
@@ -5,8 +5,35 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  versions follow [Semantic Versioning](https://semver.org/); before 1.0, config
6
6
  and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
+
8
9
  ## [Unreleased]
9
10
 
11
+ ## [0.152.0] - 2026-11-06
12
+
13
+ ### Added
14
+
15
+ - `chi bundle install` and `chi bundle check` name a manifest key this chi doesn't read (`unknown-key`); `chi bundle check` also warns (`requires-chi-floor`) when a manifest uses a key newer than its `requires_chi` admits. [docs/plugins.md](docs/plugins.md)
16
+ - `chi bundle status NAME` has a `Manifest:` line saying where the bundle's manifest is read from, and why.
17
+
18
+ ### Changed
19
+
20
+ - An installed bundle keeps its `manifest.yml` and chi reads it live, so a newer chi reads keys the installing chi didn't know, with no reinstall. Bundles installed earlier get theirs on `chi update` (shipped bundles, or a folder or archive that is still there; the system bundle at the next start); for one from git, `chi update` names the `chi bundle upgrade NAME` to run. [docs/internals/bundles.md](docs/internals/bundles.md)
21
+ - `chi bundle install` and `chi bundle upgrade` refuse a bundle whose `requires_chi` this chi doesn't meet, and write nothing; `--force` installs it anyway. [docs/plugins.md](docs/plugins.md)
22
+ - `chi bundle upgrade NAME` upgrades a bundle from a folder, archive or git URL from the source its record keeps: the name alone is enough.
23
+ - "Stable for bundles" now covers the manifest schema. [docs/plugins.md](docs/plugins.md)
24
+
25
+ ### Fixed
26
+
27
+ - `chi bundle diff NAME FILE` shows the bundle's new text for a file that stays behind, so the sections the "stays behind" line names can be read.
28
+ - `chi bundle try` no longer passes the user's dropped settings (guardrails, memories, hooks, log…) to its sandboxed chi through the environment.
29
+ - A bundle's `context_providers:` are skipped when this chi is older than the bundle's `requires_chi`.
30
+ - Reinstalling a bundle from the same source says what it did: `Up to date:` for unchanged files, and a last line naming what was rewritten and which of your memories were kept.
31
+ - A `files:` entry with no checksum no longer warns "Checksum mismatch" on every install.
32
+ - Objectives proposal cards no longer appear after a canceled or failed turn.
33
+ - A `delegate` child in a linked worktree is no longer told to re-delegate to the main checkout.
34
+
35
+ Update with `chi update` (agents-md 0.1.2, btw 0.1.4, loop-guard 0.3.10, mcp 0.7.6, skills 0.1.9, slow-commands 0.2.5: their `requires_chi` now names the chi that reads their settings).
36
+
10
37
  ## [0.151.0] - 2026-11-05
11
38
 
12
39
  ### Added
@@ -5690,7 +5717,8 @@ and long-lived sessions.
5690
5717
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5691
5718
  hotkey panel that send selected text or the clipboard to your sessions.
5692
5719
 
5693
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.151.0...HEAD
5720
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.152.0...HEAD
5721
+ [0.152.0]: https://github.com/dm1try/samagotchi/compare/v0.151.0...v0.152.0
5694
5722
  [0.151.0]: https://github.com/dm1try/samagotchi/compare/v0.150.0...v0.151.0
5695
5723
  [0.150.0]: https://github.com/dm1try/samagotchi/compare/v0.149.0...v0.150.0
5696
5724
  [0.149.0]: https://github.com/dm1try/samagotchi/compare/v0.148.0...v0.149.0
@@ -129,10 +129,10 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
129
129
  - **Shas.** Every file the manifest names has a sha256 line, and an edit makes it stale: the install then warns
130
130
  of a checksum mismatch, and a line with no sha installs unchecked. `chi bundle check --fix` rewrites them; run it
131
131
  after the last edit.
132
- - **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
133
- newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
134
- later counts too (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)`
135
- 0.72.0, `chi.toolkit(groups:)` 0.121.0), when the call is written on `ctx` / `chi` itself (not through a variable).
132
+ - **requires_chi.** Use the chi the user runs (`chi --version`): a bundle whose `requires_chi` that chi doesn't
133
+ meet is now **refused at install** (`--force` is the only way past it), not just unloaded. `check` warns when the plugin calls something newer than the floor (`ctx.http` 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0),
134
+ or a keyword added later (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)` 0.72.0, `chi.toolkit(groups:)` 0.121.0), written on `ctx` / `chi` itself rather than through a variable,
135
+ or when the manifest itself uses a key newer than the floor (`block_renderers:` 0.151.0, `settings:` 0.143.0, `session_settings:` 0.84.0, `context_providers:` 0.32.0): an older chi ignores that key.
136
136
  - **A typo in an API call.** `check` warns (`api`) on a call or keyword this chi lacks, on any path; for a newer chi on purpose, guard it with `ctx.respond_to?(:name)`.
137
137
  - **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
138
138
  - **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
data/docs/cli.md CHANGED
@@ -36,7 +36,7 @@
36
36
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `big memories` the memories over `memory.entry_warn_tokens` ([Memory](memory.md#a-memorys-size)), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
37
37
  - `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, and check the bundles from elsewhere, in one table (see [Updating](#updating))
38
38
  - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
39
- - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
39
+ - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet; without it such a bundle is refused (exit 1), nothing written. A plain `install` of a bundle that is already installed from the same source rewrites its hooks, rules, plugin, scripts and record: the summary lists a file that already holds the bundle's bytes as `Up to date: …`, one you wrote as `Skipped: …`, and ends with `NAME was already installed from SOURCE: its hooks, rules, plugin and scripts now match it and its record was rewritten` (plus, when a memory was kept, `your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)`). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
40
40
 
41
41
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
42
42
  - `chi guardrails rule <id>` — print exactly `asks` when that rule is loaded, not disabled and votes `ask` in this `guardrails.mode` for every model; any other line says why not (no such rule, disabled, another mode, it denies, for some models only, rules that failed to load, `guardrails.enabled: false`). Exit 0 either way, 2 for a usage error. It reads config.yml and the installed bundles as a worker does (`GuardrailWiring#rules`) and takes the rule's id, never a command; the [coordinator skill](bundles.md#the-coordinator-bundle) reads it before it merges without asking
@@ -133,8 +133,10 @@ The loop for a bundle you're writing (the whole path: [bundle-authoring.md](bund
133
133
  chi's Ruby (minitest is one of chi's gems; rspec is optional). The tests run the bundle's code as written, with
134
134
  your environment (the kit sandboxes each test's HOME and XDG dirs, not the process).
135
135
  - `chi bundle install --dev PATH` installs the bundle in the local directory `PATH` (not a git URL, archive or name:
136
- exit 2) as a link to it: its plugin, hooks, rules and scripts are `PATH`'s own files, so an edit applies at the
137
- next worker or `chi web` restart with no re-install, and their sha256 isn't checked, for this bundle only (the
136
+ exit 2) as a link to it: its plugin, hooks, rules, scripts and `manifest.yml` are `PATH`'s own files, so an edit applies at the
137
+ next worker or `chi web` restart with no re-install (a manifest edit to `block_renderers:` shows in chi web's next
138
+ request; a new `scripts:`, `hooks:` or `plugin:` entry needs `chi bundle install --dev PATH` again, since the record
139
+ has no entry for it), and their sha256 isn't checked, for this bundle only (the
138
140
  record says `dev`; every other bundle is checked as before). Its memories are copied at install. `chi bundle
139
141
  status` shows `NAME vX dev → /abs/path`. `chi bundle upgrade NAME` refuses it (it follows its source already; to
140
142
  leave dev mode: `chi bundle uninstall NAME`, then `chi bundle install` without `--dev`), as does a plain install
@@ -147,9 +149,13 @@ The loop for a bundle you're writing (the whole path: [bundle-authoring.md](bund
147
149
  so run it (and `test`) on code you'd install. The codes, in order: `manifest` (a parse error, verbatim; the run
148
150
  stops), `missing-file`, `unlisted` (warn: a file an install leaves out, by `MemoryFiles`' rules, so a README
149
151
  isn't one), `sha` (a stale or placeholder sha, or a plugin / hook / script entry with no sha line; `--fix`
150
- rewrites the first two and prints `fixed sha FILE → sha256:…`), `requires-chi` (this chi doesn't satisfy it:
152
+ rewrites the first two and prints `fixed sha FILE → sha256:…`), `unknown-key` (warn: a manifest key this chi's
153
+ parser never reads, so it ignores it, one row per key at its line; the install keeps the manifest, so a newer
154
+ chi reads the key from it with no reinstall, and `chi update`'s re-check names it too), `requires-chi` (this chi doesn't satisfy it:
151
155
  error; missing on a bundle with code: warn), `requires-chi-floor` (warn: the plugin calls an API newer than the
152
- oldest chi `requires_chi` admits, from `Plugin::Api::SINCE`), `trust` (warn: `trust_level: reviewed|trusted` on a
156
+ oldest chi `requires_chi` admits, from `Plugin::Api::SINCE`, or the manifest itself uses a key newer than it, from
157
+ `Manifest::SINCE`; an installed bundle gets the unknown-key row but never the floor, as it's the author's fix),
158
+ `trust` (warn: `trust_level: reviewed|trusted` on a
153
159
  bundle chi doesn't ship), `clash` (warn: a name chi ships, or one installed from elsewhere; the sandboxed install
154
160
  then forces), `install` (each Installer warning, a `needs` one as warn, and each plugin or hook load failure at
155
161
  the source `file:line`; skipped while a placeholder sha is unfilled), `registered` (info: each command, tool,
@@ -313,7 +319,7 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
313
319
  file in the bundle trash and `merged 0.8.0 into your FILE` in the row. A real
314
320
  conflict, no `git`, or no recorded base keeps your file untouched, and the row
315
321
  says `FILE stays behind 0.8.0:` with why and what it missed (up to three headings
316
- and a count, or `2 places`). `chi bundle diff NAME FILE` shows it; `chi bundle
322
+ and a count, or `2 places`). `chi bundle diff NAME FILE` shows it (the base, your file, then the bundle's new text); `chi bundle
317
323
  upgrade NAME --force` takes the bundle's, with a copy of yours in the bundle
318
324
  trash first. A later run retries such a file at the same version: its row reads
319
325
  `0.8.0 merged 0.8.0 into your FILE` when it merges, and names it as behind when
@@ -367,6 +373,26 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
367
373
  update` again). A failed gem install checks against this chi, since it stays.
368
374
  - `--no-bundles` (`update.bundles: false`) leaves these out too: no check, no row
369
375
  but `bundles skipped`.
376
+ - **Bundles installed by an older chi** have no kept `manifest.yml` (it didn't
377
+ keep it), so chi reads what their install recorded, and a manifest
378
+ key that chi didn't know (`block_renderers:` before 0.151.0) is missing. Where
379
+ chi has the bundle's own manifest at the installed version, the update keeps it,
380
+ once: a shipped bundle from chi's shipped copy (`up to date (kept its manifest)`;
381
+ the system bundle at the next chi start), a bundle from a folder or a zip/tar
382
+ that is still there from it (a `kept its manifest` row; the archive is extracted
383
+ and the extract removed). Only when the source is what's installed: the same
384
+ name and version, the same manifest values as the install recorded (a key the
385
+ older chi didn't know aside), and the same memories, plugin, hooks, scripts and rules. A git source
386
+ isn't cloned: the row names the command,
387
+ `installed by an older chi, which didn't keep its manifest: chi bundle upgrade NAME reads it again from URL`.
388
+ A source that's gone or doesn't match gets its own row (why, and what to run: for
389
+ a gone one, `NAME's recorded source S is gone: chi bundle upgrade <path or URL of a copy>`). The
390
+ same goes for a record an older chi rewrote after a newer one kept the manifest
391
+ (`an older chi rewrote its record after the install, so its manifest.yml may be an
392
+ earlier version's`). `--dry-run` says `would keep its manifest` and writes nothing.
393
+ An edited `manifest.yml` isn't kept over: its row says so (`chi bundle upgrade
394
+ NAME`). These rows are warnings and fail nothing; `chi bundle status NAME` says the
395
+ same on its `Manifest:` line.
370
396
  - **Profiles** (`core`, `dev`): an installed one installs the bundles a new
371
397
  chi added to it (`updated (+ name)`), even at the same version, and retries
372
398
  one that failed before; one you uninstalled stays out. See [Bundle
@@ -907,7 +933,6 @@ the model's `objectives` tool); [Objectives](sessions.md#objectives) has what th
907
933
  - `/objectives done O1 [O2…]`, `/objectives drop O1 [O2…]`: mark items. An id it doesn't have refuses the whole line, and
908
934
  so does `done` on a dropped item. The model is shown a done item as `O1 [done] …` and no dropped one.
909
935
  - `/objectives undo O1 [O2…]`: back to active, after a mistaken `done` or `drop`.
910
-
911
936
  - `/objectives check O1`: an independent read of O1's latest move, one side request; the verdict comes as a card and as
912
937
  `check: …` under the move. Information only: it edits nothing, and it is refused for an objective with no move or a dropped one, and during a participant's turn. See [Objectives](sessions.md#objectives).
913
938
 
data/docs/guardrails.md CHANGED
@@ -1157,6 +1157,10 @@ the whole call:
1157
1157
 
1158
1158
  `decision` is what the judgment led to: `audit` (audit mode), `asked` (on
1159
1159
  mode, the user was asked), `auto` (on mode, System 1 answered for the user).
1160
+ When two rules ask about one call the line's `rule` is the one the card names
1161
+ and `other_rules` lists the rest (`Verdict#other_ask_rules`), so a second
1162
+ rule's record (`SystemOne::Record`) counts the ask too; a line with one rule
1163
+ has no `other_rules` at all.
1160
1164
  A judgment that never arrived is logged too, with `p` null, so the failures
1161
1165
  count as well. The label line, keyed by the judgment's id, is what the user
1162
1166
  did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
@@ -10,7 +10,9 @@ model. The TUI is untouched: it shows a fence as source.
10
10
  in document order and zips them with the fragment's `<pre>`s, setting `data-lang` after sanitize's walk. Only the first
11
11
  word, downcased, matching `BlockRenderers::LANG`. A count that differs sets nothing. **`web.markdown` off means no
12
12
  `<pre data-lang>`, so nothing draws.**
13
- 2. `BlockRenderers.for_installed` (manifest `block_renderers:`, read from provenance, so chi web needs no plugins) lists
13
+ 2. `BlockRenderers.for_installed` (manifest `block_renderers:`, read through `InstalledBundle#manifest`: the bundle's
14
+ kept `manifest.yml`, else its record's mirror, so chi web needs no plugins, and a bundle installed by a chi that
15
+ didn't know the key draws once a kept manifest is there) lists
14
16
  who draws what: `GET /api/block_renderers`, `:owner`. `GET /api/block_renderers/:bundle/:file` serves a file, `:owner`:
15
17
  the URL's names go through `BlockRenderers.script_path` (membership in a listed renderer's `load` before any path is
16
18
  made), then `Provenance.sha_matches?`; a changed file is 409 `renderer_changed`. ETag + `no-cache`, as static files.
@@ -13,11 +13,14 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
13
13
  *.md, index.md the bundle's memories, one managed index line each ("· from <bundle>")
14
14
  <system memories>/.bundles/ MemoryPaths.bundles_dir
15
15
  <name>/manifest.json the install record (Provenance#write, read as InstalledBundle)
16
+ <name>/manifest.yml the bundle's own manifest as installed (Provenance#write's manifest_from:), its sha
17
+ the record's manifest_sha; a dev install links it to the source (DEV_SHA)
16
18
  <name>/bases/ each file's base for the next upgrade's 3-way merge: as installed, or the
17
19
  bundle's own text after a merge
18
20
  <name>/incoming/ the bundle's text for each file marked conflict, until the file settles (Provenance:
19
- #write, #refresh_marks, gone with #resolve_conflicts); nothing globs it for memories
20
- (Builder takes the scope dir's top level), and an uninstall's rm_rf sweeps it
21
+ #write, #refresh_marks, gone with #resolve_conflicts; #prune_incoming takes a settled file
22
+ at any depth and the dirs it leaves empty; read by BundleCommand#print_incoming for `chi bundle diff`);
23
+ nothing globs it for memories (Builder takes the scope dir's top level), and an uninstall's rm_rf sweeps it
21
24
  <name>/hooks/ guardrails/ plugin/ scripts/
22
25
  .trash/<name>-<stamp>/ files an uninstall or upgrade took away, and copies of what an install
23
26
  overwrote (Trash), never deleted
@@ -27,32 +30,35 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
27
30
 
28
31
  | Piece | Role |
29
32
  |---|---|
30
- | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it) |
33
+ | `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash/new/test/check/try` (`install --dev`: the Installer's `dev:`, links and `Provenance::DEV_SHA`); runs the Installer, prints its summary, runs the conflict agent step; refuses a source already installed under another name (`installed_under_another_name?`: a git URL bundle recorded under its repo's name before the manifest named it). `upgrade` resolves its argument by `upgrade_source`: a git URL or an existing path, then a shipped name, then the bare name of an installed bundle — whose record's source it then uses (a git URL with its `#ref`, a dir or an archive that is still there), printed as `Upgrading NAME from SOURCE`; a dev record is refused with `Installer.dev_install_line`, a recorded source that is relative, empty or gone says so and asks for the path or URL of a copy (`Installer.source_gone_line`, the words `ManifestStanding` uses too) |
31
34
  | `BundleCommand::Check` (bundle_command/check.rb) | `chi bundle check [PATH] [--fix] [--set K=V]`: a bundle being written, checked as an install takes it (docs/cli.md lists the codes); parses the manifest from `Shas.parseable` (a placeholder sha is a `sha` finding, not a YAML error), reads the user's installed bundles only for `clash`, and installs and loads through `Testing::Install` in a `Testing::Sandbox` with `force: true`, the log's echoes swallowed |
32
- | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
35
+ | `BundleCommand::Try` (`bundle_command/try/`) | `chi bundle try PATH -p TEXT`: `Command` (its flags, help and checks) drives the rest and `Render` prints the trace; a filtered-config `Sandbox` (its folders a `SandboxLayout`, the layout `Testing::Sandbox` uses too) installs the tried bundle and shipped `guardrails` / `cost-cap` in a separate child, then `Run` starts one non-interactive chi process from its realpath work dir; `TraceReader` reads that session's analytics, cards (`Bridge::CardStore.saved`, as the web does) and session JSON by id (never `Session.list`), joining timing and tool args for text or experimental JSON output (a turn a `--timeout` interrupt cut keeps its calls like a Ctrl-C, the one cut short answered as interrupted, so its rows come from the saved calls too; `Run::Result#timed_out` becomes `timed_out`). `Sandbox#child_env` is a Hash, never an ENV mutation; it keeps ordinary user model settings and Keychain access while dropping the spawner's session/guardrail/worker markers (`ChildEnv`, the scrub a session's worker spawn uses too). Every dropped key is sent as nil, so the child really lacks it: a spawn env is merged onto the spawner's, so a key only deleted from the Hash is still inherited (`ChildEnv.spawn_env`). The child runs in its own process group: a `--timeout` sends it INT, then `ProcessGroup.stop` (the groups its processes started too); after any exit the group is signalled only while a straggler is still in it, never a reaped group. `Sandbox#stop_leftovers` (before the trace, and in `cleanup`) stops what left the child's tree, found by the sandbox's own records: a delegate's worker by its session's `owner.lock`, a task by its running `task.json` whose pid still leads its group. |
33
36
  | `BundleCommand::ApiCalls` (bundle_command/api_calls.rb) | what a plugin's source calls of the API, by Prism: `#findings` (a call or keyword the API's classes lack, `check`'s `api`), `#keywords` (the floor check's `call(keyword:)`). Its only list is `MEMBERS`, which object a member gives (`ctx.sessions` → `Plugin::Sessions`); a new such object joins it |
34
37
  | `BundleCommand::Scaffold` (bundle_command/scaffold.rb) | `chi bundle new NAME`: the starting files (block-form `plugin:` entry, `requires_chi` at this chi, a kit test that passes as written); the manifest's shas are filled through `Shas.refresh` |
35
38
  | `BundleCommand::TestRun` (bundle_command/test_run.rb) | `chi bundle test`: `RbConfig.ruby -I <chi's lib>` on `test/**/*_test.rb` (Minitest) or `spec/` (RSpec, as `rake bundles:specs`), under `Bundler.with_unbundled_env` when Bundler's runtime is loaded |
36
39
  | `Shas` (shas.rb) | a manifest's `sha256:` lines: `.refresh(dir[, text])` rewrites them line by line (the layout kept; a `files:` entry it can't place raises `Shas::Error`), `.stale` → `[Stale(file, old, new)]` (`old` `""` for a placeholder, nil for a plugin / hook / script entry with no sha line), `.parseable` (a placeholder `sha256: sha256:` isn't YAML). Plain Ruby: `rake bundles:sha` / `bundles:check` (`script/release/release_tools.rb`) and `chi bundle check --fix` share it |
40
+ | `FileDigest` (file_digest.rb) | the one digest of a file on disk: `.hex(path)` (its bytes, streamed; nil when the path isn't a file, a raise for one that can't be read), `.sha256(path)` = `"sha256:<hex>"`, nil with no file — the shape a record writes for a hook, rule, plugin or script. `Provenance.file_sha`, `Merger.digest`, `Status.checksum`, `InstalledBundle#source_sha`, `FileStanding.differs?` and the Installer's checksum verifiers all go through it; a digest of bytes already in memory (a merge's text, a memory's content that is also its base) stays with the code holding them |
37
41
  | `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
38
42
  | `SourceIdentity` (source.rb) | what a source is, for comparing two installs of one name: `shipped:<dir>` (any gem or checkout), `git:<url>` (no `#ref`, trailing `/` or `.git`), `path:<realpath>`; nil when it can't place one |
39
43
  | `PreparedSource` (source.rb) | one source normalized once (dir, owned, git commit): `BundleCommand` reads the bundle's name from its `manifest.yml` (the name for every kind of source, a git URL too) and hands it to the Installer (`prepared:`), so a source is cloned or extracted once per command |
40
44
  | `MemoryFiles` | which top-level `*.md` files of a bundle are its memories (`.select`: the `files:` list, else all but README/LICENSE and the like), `.unshipped` (listed, not shipped) and `.symlinks` (skipped: a symlink is never a memory) |
41
- | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check; settings: the keys config.yml's `bundles: <name>:` may have, the same shape, recorded as `settings` (nil when the manifest has none) and `setting_types` for `BundleSettingsCheck`, whose warnings the Engine holds for the first turn and `SessionConfig`'s refusals share the words of) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
45
+ | `Manifest` | parses/validates `manifest.yml` (`name:` is the bundle's name and a directory under `.bundles/`, so a path such as `../x`, `a/b`, `.` or `..` is refused: `name: … is not a bundle name`; YAML that doesn't parse or isn't a mapping is a `ValidationError` too; files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers, session_settings: the keys a session's `chi --set` may give, a list or a mapping of them to types (`SESSION_SETTING_TYPES`), recorded at install in `manifest.json` (`session_settings`, `session_setting_types`) for `SessionConfig` to check; settings: the keys config.yml's `bundles: <name>:` may have, the same shape, recorded as `settings` (nil when the manifest has none) and `setting_types` for `BundleSettingsCheck`, whose warnings the Engine holds for the first turn and `SessionConfig`'s refusals share the words of) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md). `KEYS` is every top-level key it reads, so a key outside it is one this chi ignores: `#unknown_keys`, in the order the manifest lists them, said in `Manifest.unknown_key_line`'s words by the Installer and `chi bundle check`. `SINCE` is the first release that read each key or shape (taken from the tags), and `#floor_keys` names the ones this manifest uses — including the shapes only a newer chi reads (a `files:` mapping, a hook's `events:`, a typed `session_settings:`/`settings:`) — for check's `requires-chi-floor` |
42
46
  | `Installer` | one install/upgrade/dry run of one bundle (below) |
43
47
  | `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps). A file it replaces or removes whose bytes are neither the recorded sha nor the incoming file's is copied to the Installer's `Trash` first (`Trash#copy_edit`, `#copy_edits` for a folder at any depth, as `hooks/x`, `guardrails/x`, `plugin/x`, `scripts/sub/x`; no sha recorded counts as edited; a dev link never is) |
44
48
  | `Merger` | 3-way classification per file: `.classify` → an `Outcome` (`status`: `install`, `fast_forward`, `keep`, `noop`, `conflict`, `merged`; `base`: where the file's next base comes from, `:on_disk`, `:recorded` or `:incoming`; a new outcome and its base go in its constants, and it needs a branch in `Installer#merge_memory`, a kind in `#next_base` if its base is new, and its status word in `#preview_memory`, `#verify_memory_checksums` and `#summary`). `.classify` never merges and answers `conflict` for two changed sides; `Merger::Upgrade` (provenance, target dir, source dir) is the one way to ask what an upgrade does to a file, nothing written: `#outcome` (`.classify`, then `TextMerge` where both sides changed, git once per call: `merged`, with its `TextMerge::Result` and the digest it read as the Outcome's `merge` and `current_sha`; `ALREADY_MERGED`, a `keep` with base `:incoming`, when the merge adds nothing; or `conflict` with that `merge`, whose `why` says why none was possible), `#classified`, `#forecast` (a `Forecast` of `merged` and `behind` in one pass), `#behind` (a `conflict` outcome as a `FileStanding::Behind`: why, and from `TextMerge.changed_sections` what it missed, nothing for an older mark, whose base may be the user's text), `#paths`, used by the Installer's merge and dry run, `ShippedUpdate.plan` and `SystemBundle.sync(dry_run:)`. `.write_merged` puts a merge in place: the digest checked just before the rename (`AtomicFile.write`'s block), through a symlink, the mode kept, the old file in the `Trash`; false when a live `memory_write` got in. A mark from an older chi (`InstalledBundle.legacy_mark?`: no `incoming_sha`, or `legacy_base`, which keeps that known once the digest is recorded, until the mark goes) may have the user's own text as its base, 0.149's doing, and a merge against it would undo that text with nothing to show, or a fast-forward replace all of it: `#outcome` makes a fast-forward that takes a line out of the file, and `.merge_outcome` a merge that does, a `conflict` (`FileStanding::LEGACY_BASE`), on the Installer's and `Remerge`'s path alike, unless `ShippedHistory` proves the base is a version chi shipped. The price, with no such gem left or for a bundle chi doesn't ship: a real base looks the same, so such a file whose bundle rewords or removes a line it still holds stays behind until it is resolved by hand, with `--agent` or with `--force`; no retry fixes it. `Installer#save_marked_file` serves a fast-forward that goes ahead over a mark: a file put back to its base under this chi's mark, or an older mark with a proved base or an update that only adds. `.markers?` (a file holding a merge's `<<<<<<<`/`>>>>>>>` lines; `=======` alone is a Markdown underline) |
45
49
  | `ShippedHistory` | `.shipped?(bundle, file, path)`: whether those bytes are that file of that bundle (by manifest name, since a shipped dir can be renamed) in an installed `samagotchi-<version>` gem. Reads `<gem dir>/gems/` by hand, for `Gem.path` (set by a chi home's boot string) and the home's `gem_dirs` from home.json (a checkout or another Ruby runs on other gems), never `Gem::Specification`. Size, then bytes, then the manifest. Anything it can't read is no match, which keeps the guard. Asked only when `Merger`'s guard would otherwise fire: with 54 gems on the Studio a miss costs about 4 ms, a hit less |
46
50
  | `TextMerge` | the text merge: `.merge(base:, ours:, theirs:)` → a `Result` (`status` `:clean` with `text`, `:conflict` with `hunks`, `:failed` with `why`). `git merge-file -p --diff3 --marker-size=20` on temp copies in a temp cwd, with every `GIT_*` that points at a repository or a config unset or at `/dev/null` (it can run inside another tool's hook). git isn't a runtime dependency: none on PATH, or macOS's `/usr/bin/git` with no developer tools behind it (`.apple_stub?`, by `xcode-select -p`; running it pops a dialog), is `:failed`. So is a git past `TIMEOUT` (10 s; its process group is killed: this runs under `SystemBundle`'s lock), and any doubt about the answer: `min(hunks, 127)` against the exit status, a status over 127, a NUL in a side, marker lines that don't pair up (a user's line equal to one), a line no input holds, or an empty answer to non-empty inputs. `Scan` reads the output: a hunk unions (ours, then the entries of theirs that ours lacks) only when the last heading before it, at level 1 or 2 and outside code fences, HTML comments and any hunk, is titled exactly `Changelog` or `Change log`, with nothing but list entries (an item and its indented lines) since that heading and no fence or comment open, and base, ours and theirs each hold only list entries, with every base entry still on both sides: both only added. Any other hunk is a real conflict, and markers never leave the module. An entry is an item and the lines indented deeper under it, nested items too, and the union compares whole entries. `.changed_sections(base, theirs, ours)`: the headings of the sections the bundle changed and the file hasn't got, and whether text under no heading did, which is what names a miss: a conflict's hunks can't, since the whole file stays and the one hunk may be a Changelog line beside a whole new section. `Hunk#heading` counts the places under no heading |
47
51
  | `FileStanding` | the one owner of what chi says of an edited bundle file, used by every surface: `Behind` (`#state`, `#line`: `x.md stays behind 0.8.0: no merge possible (why); missed ## A, ## B, ## C and 2 more`, `would stay` in a dry run), `MergeNote`, `.merged_line`, `.no_merge` (for `LEGACY_BASE` it says chi can't tell whether the file holds edits), `.clauses` (a report's clauses for one bundle), `.behind_count`, `.stay_behind`, `.counts_sentence` (the push), `.still_behind`, `.text` (UTF-8, whatever a name or heading was tagged), `FORCE_TAKES`, `FORCE_MOVES`, `IN_THE_TRASH`. Behind means marked and bytes other than the mark's `incoming_sha` (`.behind?`, read by `InstalledBundle#behind_files` and `Remerge` alike): a file brought level by hand isn't, a missing one is a missing file, and one that can't be read is. An older mark no retry has looked at reads `may be behind` (`Behind#unsure`, and the counts say so), and isn't behind at all when level with its recorded source on disk (`InstalledBundle#behind_file?`). A new surface reads these and adds no wording |
48
- | `Remerge` | the retry at the same version, memory files only, for a file marked `conflict`: `#run(dry_run:)` → a `Result` (`merged`, `settled`, `stamped`, `behind`, `warnings`, `trash_copy`, `trash_dir`). It merges what merges and settles those entries (`Provenance#resolve_conflicts`, the index line refreshed), leaving `version`, `installed_at` and the assets alone: the Installer would `rm_rf` guardrails, plugin and scripts under live sessions. Every file it leaves is in `behind`, with why: one not shipped now (`MemoryFiles.select`) keeps its mark, base and text; one whose `incoming_sha` isn't the shipped text (a checkout at one VERSION) is left alone, no git runs, and it reads as its mark says; a mark with no digest (older chi) is tried as well, and a real conflict then records it (`Provenance#refresh_marks`, which writes only when the mark would read differently, so a conflict at each start writes nothing). A mark whose digest matches is tried on every run, since the file or git may have changed. `noop` and `keep` settle quietly; `fast_forward` waits for the next upgrade, reading as behind until then; `install` is a missing file, which isn't behind, and a warning says so. A file it can't read or replace stays as it is, saying why. Called from `ShippedUpdate.plan_row`/`apply` (`chi update`) and `SystemBundle.sync`'s equal-version branch, under its lock (each chi start and `chi update`); a bundle chi doesn't ship retries through `chi bundle upgrade`. No marks: nothing is read beyond the record. One marked file costs one git run per Engine start, about 12 ms on the Studio (0.1 ms with none) |
49
- | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases (its `files:` is the full list: a key left out is dropped, a nil value keeps the entry with no base; `marks:` are the files left behind, each a `FileStanding::Behind`, and `incoming_dir:` the bundle text each speaks of, recorded as `incoming_sha` and saved at `#incoming_path`; an older mark that stays keeps `legacy_base`), `#resolve_conflicts` (base to the incoming file, mark and its fields (`MARK_FIELDS`) and saved text gone; a file whose incoming copy is gone keeps both), `#refresh_marks` (what a retry found: why, missed, the digest and `legacy_base`, the saved text), `#note_merged` and `#clear_merge_notes(notes)` (`MERGE_NOTE_FIELDS`, kept through `#write`; the saved text is best effort, after the manifest, so it can't leave a plugin under the old record's sha; any other failure writing the record still can, until a retry), `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
50
- | `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty). `BundleHook#events` is the manifest's `event:` or its `events:` list (written back as `event:` for one, as an older chi reads it); `Hooks::BundleLoader#load_hook_file` builds the hook once and registers it under each |
52
+ | `Remerge` | the retry at the same version, memory files only, for a file marked `conflict`: `#run(dry_run:)` → a `Result` (`merged`, `settled`, `stamped`, `behind`, `warnings`, `trash_copy`). It merges what merges and settles those entries (`Provenance#resolve_conflicts`, the index line refreshed), leaving `version`, `installed_at` and the assets alone: the Installer would `rm_rf` guardrails, plugin and scripts under live sessions. Every file it leaves is in `behind`, with why: one not shipped now (`MemoryFiles.select`) keeps its mark, base and text; one whose `incoming_sha` isn't the shipped text (a checkout at one VERSION) is left alone, no git runs, and it reads as its mark says; a mark with no digest (older chi) is tried as well, and a real conflict then records it (`Provenance#refresh_marks`, which writes only when the mark would read differently, so a conflict at each start writes nothing). A mark whose digest matches is tried on every run, since the file or git may have changed. `noop` and `keep` settle quietly; `fast_forward` waits for the next upgrade, reading as behind until then; `install` is a missing file, which isn't behind, and a warning says so. A file it can't read or replace stays as it is, saying why. Called from `ShippedUpdate.plan_row`/`apply` (`chi update`) and `SystemBundle.sync`'s equal-version branch, under its lock (each chi start and `chi update`); a bundle chi doesn't ship retries through `chi bundle upgrade`. No marks: nothing is read beyond the record. One marked file costs one git run per Engine start, about 12 ms on the Studio (0.1 ms with none) |
53
+ | `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases (its `files:` is the full list: a key left out is dropped, a nil value keeps the entry with no base; `marks:` are the files left behind, each a `FileStanding::Behind`, and `incoming_dir:` the bundle text each speaks of, recorded as `incoming_sha` and saved at `#incoming_path`; an older mark that stays keeps `legacy_base`), `#resolve_conflicts` (base to the incoming file, mark and its fields (`MARK_FIELDS`) and saved text gone; a file whose incoming copy is gone keeps both), `#refresh_marks` (what a retry found: why, missed, the digest and `legacy_base`, the saved text), `#note_merged` and `#clear_merge_notes(notes)` (`MERGE_NOTE_FIELDS`, kept through `#write`; the saved text is best effort, after the manifest, so it can't leave a plugin under the old record's sha; any other failure writing the record still can, until a retry), `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file). `#write(manifest_from: DIR)` keeps `DIR/manifest.yml` at `#kept_manifest_path` (written aside and renamed; for `dev:` a link to its realpath) and records `manifest_sha` (`FileDigest.sha256`, `DEV_SHA` for dev) and `installed_by` (this chi's VERSION); the Installer and `Profile.install` pass their source dir, and a write without it carries the existing record's two fields, the kept file left alone. The in-place edits (`#resolve_conflicts`, `#record_restored`, `#refresh_marks`, `#note_merged`, `#clear_merge_notes`) keep them. The record's mirror of the manifest keys (`requires_chi` … `setting_types`) is still written from the kwargs, exactly as before: an older chi sharing the memories dir (an old worker, chi web, gem or worktree) reads only the record, and without the mirror's `requires_chi` it would load a too-new bundle's plugin and hooks. A key a later manifest gains is never mirrored. Any chi without the kept manifest that rewrites a record drops `manifest_sha` and `installed_by` (it rebuilds the record from its own kwargs). `#adopt_manifest(dir, dry_run:)` heals such a record (`InstalledBundle#manifest_unkept?`: no `manifest_sha`, or its file gone): when `dir` is the source of what's installed (its manifest names the bundle at the recorded version; each mirrored key, normalised as `.mirror_fields` records it (the one shape `#write` uses too), equals the record's, a key the record lacks being fine only when `Manifest::SINCE` has it, since an older chi couldn't have recorded it; each recorded hook's events, on_error and priority are the manifest's; and every recorded plugin, hook, script, rule and unmarked memory sha is that of `dir`'s file; else the why names the key or file) it keeps `dir/manifest.yml` and records `manifest_sha`, nothing else (not `installed_at`, the mirror or `installed_by`); otherwise it returns why and writes nothing. Never for a dev record (its next `install --dev` links the file) |
54
+ | `InstalledManifest` (installed_manifest.rb) | the one answer to "this installed bundle's manifest" (`InstalledBundle#manifest`): requires_chi, trust_level, hook metadata, needs, the scripts' names, context providers, block renderers, session settings and settings with their types, file descriptions, a profile's includes. Read from the kept `manifest.yml` (`origin: :kept`) when the record's `manifest_sha` is set and the file still has it (`Provenance.sha_matches?`, so a dev link is never checked), else from the record's `mirror` (`origin: :record`), in these cases: no kept file and no `manifest_sha` (a record from before chi kept it; no problem); no kept file, or something other than a file in its place, while `manifest_sha` is set (`:missing`); a kept file and no `manifest_sha` (a chi without the kept manifest rewrote the record: `problems["manifest.yml"] = :stale`); a kept file with another sha, or one that isn't a YAML mapping (`:edited`); the sha matches (`:kept`). `#standing` is that symbol, `#key_problems` the rest. The kept file is read by `Manifest.tolerant` key by key in a fixed order (its comment): a key that doesn't parse under this chi takes the mirror's value if that parses, else empty, with its message in `problems` (a reader that logged it logs that); `block_renderers` is checked against the record's scripts when the kept `scripts:` fails, and one bad hook entry keeps the record's hooks whole. Hooks: each *recorded* hook with the kept manifest's events, on_error and priority and the record's sha (a found `hooks/*.rb` with no entry as recorded; a manifest hook the record lacks isn't installed, so isn't listed). `description` and `unknown_keys` come only from a kept file; a profile's `includes` is always the record's (its members, identity). The value is deep-frozen, since callers share it. One process-wide cache (a Hash under a Mutex, cleared at `CACHE_LIMIT`) keyed by the record value and the kept file's stat (mtime, size, inode, device; a dev link's target): chi web asks per request, a worker at start and per `ContextProviders.resolve`. `#requires_chi_failure(version = VERSION)` is the one requires_chi gate; `#experimental?`, `#needs?`, `#context_providers?`, `#block_renderers?` |
55
+ | `ManifestStanding` (manifest_standing.rb) | the one owner of the words for an installed bundle's `manifest.yml` as `InstalledManifest` read it: `.state` (`:kept`, `:legacy`, `:stale`, `:missing`, `:edited`), `.label` (`[kept]` / `[from the install record]`), `.line(bundle, refused:)` (the case and the command: `chi bundle upgrade NAME` (reads it again from a git URL, or a source that didn't match, with why), `chi update keeps it` while untried, the source is gone (`Installer.source_gone_line`), `chi bundle install --dev SOURCE` for a dev record; for a kept one, only the keys whose `Manifest::SINCE` floor is above the record's `installed_by`: newer than its installer), `.keep(bundle, source_dir:, dry_run:)` → `Outcome` (the record after, the line, `kept`): `Provenance#adopt_manifest` from `source_dir`, else from the recorded source (a dir; a zip/tar through `PreparedSource`, cleaned up; never a git clone). Read by `ShippedUpdate` (`Row#manifest`, the plan's dry `keep` and apply's), `UpdateCommand#external_rows` (a `kept its manifest` row, else a warn row, before the bundle's re-check, which then sees the kept file) and `chi bundle status NAME` (`BundleCommand#manifest_line`: `Manifest:`, the label, `.status_line`: for a record with a folder source (a bundle chi ships: this chi's shipped copy at the recorded version, which `chi update` reads), a dry `adopt_manifest` first, so the line says whether `chi update` keeps it or why not; an archive isn't extracted there, its line says `chi update tries to keep it`; a profile chi doesn't ship is never kept by `chi update`, so its line names `chi bundle upgrade NAME`) |
56
+ | `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty). `InstalledBundle`'s members are identity and state (name, version, scope, source, `installed_at`, `source_commit`, files and their marks, hooks, guardrails, scripts and the plugin with their shas, a profile's `includes`, `dev`, `dir`, the kept manifest's `manifest_sha` and `installed_by`, nil on a record from before it was kept); the manifest keys it records sit in one `mirror` Hash (`MIRROR_KEYS`, frozen: a key a later manifest gains is never mirrored) that only `InstalledManifest` reads, so manifest data is read through `#manifest`. `BundleHook#events` is the manifest's `event:` or its `events:` list (written back as `event:` for one, as an older chi reads it); `Hooks::BundleLoader#load_hook_file` builds the hook once and registers it under each |
51
57
  | `BlockRenderers` (`lib/samagotchi/block_renderers.rb`) | a manifest's `block_renderers:`: `.parse_list`, and what chi web asks without loading plugins: `.for_installed` (first bundle by name wins a language; a record with an error, an unmet `requires_chi` or a list that doesn't parse is skipped) and `.script_path` (`load` membership first, then `Provenance.sha_matches?`). `Manifest` checks each `load` is a `scripts:` key; the files are ordinary scripts everywhere else ([block-renderers.md](block-renderers.md)) |
52
58
  | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
53
59
  | `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had, with other bytes (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`); a dry run runs each member's Installer as a dry run too, so it names them as well |
54
- | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). `ShippedUpdate.external` is the rest: what `chi update` only checks |
55
- | `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
60
+ | `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). At an equal version both keep the shipped `manifest.yml` for a record an older chi wrote (`Provenance#adopt_manifest` from the shipped dir): `SystemBundle.sync` under its lock, not in a dry run (so at the first Engine start or `chi update` after the upgrade, once), and `ShippedUpdate.apply` for a row that ends `:up_to_date` or `:merged` (`#keep_manifest`, through `ManifestStanding.keep`: `Row#manifest` is its line, `would keep its manifest` from the plan's dry run, appended to the row's status); a bumped bundle gets it from its Installer. `ShippedUpdate.external` is the rest: what `chi update` only checks |
61
+ | `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`; its `files:`, `hooks:`, `plugin:` and `scripts:` are always what the record and the copy hold, so a memory the install left out, one with no base or a manifest hook the record lacks gives no `missing-file`, and every other key is the kept `manifest.yml`'s when `InstalledManifest` read it, so the check sees its unknown keys, else the mirror's; a kept key that didn't parse keeps the value `InstalledManifest` fell back to, since `InstalledCheck` already names its problem); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
56
62
  | `Uninstaller`, `Trash`, `Status`, `Listing` | remove (the bundle's own folder too: an edited file there is copied to the trash first, `Uninstaller#trash_copy_line`); keep removed files (`Trash#move`) and a copy of overwritten ones (`Trash#copy`, its `as:` keeping a subfolder); `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
57
63
  | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
58
64
  | `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
@@ -70,7 +76,16 @@ and a normal install from replacing each other (this is also why `chi bundle upg
70
76
 
71
77
  Readers of the installed state: `Hooks::BundleLoader` (Engine), `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
72
78
  `ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
73
- and checks the recorded sha256 (and `requires_chi`) before loading.
79
+ and checks the recorded sha256 (and `requires_chi`) before loading. `ContextProviders.resolve`/`.any?` skip a bundle
80
+ whose `requires_chi` this chi doesn't meet, logging `providers_too_new` once per process per bundle and version (a
81
+ Set under a Mutex, like `GuardrailWiring`'s kept-rules notice), and `.command_for` raises `Invalid` for a source
82
+ attached to one. They read shas and state off the record, and
83
+ manifest data (requires_chi, hook metadata, needs, providers, renderers, settings) through `InstalledBundle#manifest`
84
+ (`InstalledManifest`: the kept `manifest.yml` when its sha holds, else the record's mirror), as do `SessionConfig`, `BundleSettingsCheck`, `BlockRenderers`, `Status`, `BundleCommand`'s
85
+ status and diff, `Builder` and `InstalledCopy`. Their requires_chi checks are `InstalledManifest#requires_chi_failure`;
86
+ `Hooks::BundleLoader.load` is given the installed bundle's manifest and uses the same gate. `GuardrailWiring#rules_stamp` stats each bundle's
87
+ `manifest.yml` with its `manifest.json` and `guardrails/*`, so a changed requires_chi re-reads the rules. No reader
88
+ calls `Manifest.new` on a kept file: `Manifest.tolerant` through `InstalledManifest` is the one read.
74
89
  The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`; the loading itself is
75
90
  `BundleLoader.load_hook_file` (one hook file, into a given `namespace:`) and `Plugin::Loader.register_file` (one
76
91
  plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
@@ -81,12 +96,23 @@ spec's shape). A load failure carries `LoadFailures::Failure#location` (the firs
81
96
  bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`,
82
97
  `hooks/`) back to the source dir.
83
98
 
99
+ Records from older chi. A record written before the kept manifest (**legacy**: no `manifest.yml`, no
100
+ `manifest_sha`), or rewritten since by such a chi (**stale**: a kept file, no `manifest_sha`; its file may be an
101
+ earlier version's), or whose kept file is gone (**missing**) reads the record's mirror: exactly what that install
102
+ recorded, so nothing breaks, and a key the installing chi didn't know stays unread. The mirror stays written because
103
+ those older chis (an old worker, an old chi web, an old gem, another worktree or chi home) keep reading and writing
104
+ `.bundles/` with no time limit; without its `requires_chi` they would load a too-new bundle. The gap closes where chi
105
+ has the bundle's own manifest at the recorded version (`Provenance#adopt_manifest`): `SystemBundle.sync` and
106
+ `ShippedUpdate.apply` from the shipped dir, `chi update` from an external record's folder or archive
107
+ (`ManifestStanding.keep`). A git source is never cloned for it; `chi bundle upgrade NAME` reads it again.
108
+ `ManifestStanding` says which case a record is in, and the command, for `chi update` and `chi bundle status NAME`.
109
+
84
110
  ## Installer#run, in order
85
111
 
86
112
  The order matters: warnings print in it, and a plugin/script the manifest names but the bundle lacks raises
87
113
  `InstallError` after the memories are already copied.
88
114
 
89
- 1. `load_source`: normalize (or take the `PreparedSource` `BundleCommand` passed as `prepared:`), read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning).
115
+ 1. `load_source`: normalize (or take the `PreparedSource` `BundleCommand` passed as `prepared:`), read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning). Then `refuse_too_new`: a bundle whose `requires_chi` this chi doesn't meet raises `InstallError` unless `force` — nothing is written, dry run included (docs/plugins.md, "Loading, and when it fails").
90
116
  2. `resolve_target`: scope = CLI > manifest > `system`; then `check_source` against the existing record
91
117
  (`SourceIdentity`, source.rb): a name differing from an installed one only in case raises (`force` too); a
92
118
  shipped name (any case) from a source that isn't a dir in the Installer's `shipped_dir:` (default `SHIPPED_DIR`;
@@ -129,7 +155,13 @@ doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' eve
129
155
  can name a file dropped from `files:` or now a symlink, which the upgrade doesn't merge: edited, it stays (`kept_pruned`, or `kept_shared` when another bundle has it too); `SystemBundle`'s dry
130
156
  run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
131
157
 
132
- Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
158
+ Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`: a file that already holds the
159
+ bundle's bytes (`reason: "already up to date"`, a memory or a hook/rule/plugin/script) is listed as `Up to date:`,
160
+ and only the files really left out (`already exists`, a dry run's `would_skip`, a dry run upgrade's `noop`) as
161
+ `Skipped:`. A plain install over a bundle already installed from the same source ends with `NAME was already
162
+ installed from SOURCE: its hooks, rules, plugin and scripts now match it and its record was rewritten`, plus
163
+ `; your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)` when
164
+ a memory was left out as `already exists`. An upgrade with conflicts keeps an
133
165
  owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
134
166
  files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
135
167
  the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
@@ -140,5 +172,7 @@ copy is still there. Any other keeps its old base and mark, and the command exit
140
172
  `select_memories` (all but `index.md`/hidden; with `--files` the named ones + their overlays, else leaving out files
141
173
  another installed bundle owns) → `checksum_memories` → `installed_parts` (`Builder::InstalledParts`: the same-name
142
174
  installed bundle's hooks, rules, plugin, scripts, context providers, else the scope dir's `hooks/`/`guardrails/`) →
143
- `stage_files` + `write_manifest` in a temp dir → `write_out` (zip, tar, tgz, or a new/empty dir). An installed
175
+ `stage_files` + `write_manifest` (requires_chi, needs, settings… from the installed bundle's `InstalledManifest`;
176
+ `trust_level` and `description` the CLI's, else that manifest's; a kept manifest's unknown keys carried, each with a
177
+ `unknown_key_line` warning) in a temp dir → `write_out` (zip, tar, tgz, or a new/empty dir). An installed
144
178
  profile's name is refused (`refuse_profile!`): a build can't remake its includes.
@@ -116,10 +116,12 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
116
116
 
117
117
  `action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
118
118
  (`Objectives::Proposal#refusal`) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
119
- as `#context_choice`'s cards are kept: shown in `run_turn`'s ensure, it is a between-turns card (no `in_turn`), so it
120
- writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a restart (`CardStore` takes
121
- them only from a card shown in a turn). The same change twice in a turn is one card. A P3 finding stands: a card action
122
- can't carry a plain reply, so an action here is a real edit.
119
+ as `#context_choice`'s cards are kept. `run_turn`'s ensure shows them only for a turn that completed: a canceled or
120
+ failed one drops them (`Engine#discard_objective_proposals`), so its Apply card never outlives a turn whose call a
121
+ rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
122
+ `in_turn`), so it writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a
123
+ restart (`CardStore` takes them only from a card shown in a turn). The same change twice in a turn is one card. A P3
124
+ finding stands: a card action can't carry a plain reply, so an action here is a real edit.
123
125
 
124
126
  Apply is `/objectives --card ID done O2`: the proposal's own line (`Proposal#line`) behind a prefix the command reads
125
127
  (`ObjectivesCommand::CARD`), so it goes through the one route, with its refusals. On success the card with that id is
data/docs/memory.md CHANGED
@@ -539,7 +539,7 @@ for a file another bundle installed.
539
539
  tell whether the file holds edits of yours and says that; the file
540
540
  stays behind until you resolve it by hand, with `--agent` or with `--force`, and
541
541
  the retry can't fix that. A file left behind makes the upgrade
542
- exit 2; `chi bundle diff NAME FILE` shows the base, and `--force` takes
542
+ exit 2; `chi bundle diff NAME FILE` shows the base, your file and the bundle's new text, and `--force` takes
543
543
  the bundle's version, copying a file that differs to the bundle trash first
544
544
  (the output says `Copied to the trash first: …`).
545
545
  - **Every merge is noted on the file's record.** The system bundle's upgrade and
@@ -553,6 +553,11 @@ for a file another bundle installed.
553
553
  behind, missing or unreadable (and a plugin or hooks that won't load), not edits. A
554
554
  file an older chi marked, which no retry has looked at, says `may be behind`, and so
555
555
  do the counts in `chi bundle list`, `chi self` and `chi update`'s last line.
556
+ `chi bundle status NAME` also has a `Manifest:` line: `[kept]` when chi reads
557
+ the bundle's installed `manifest.yml`, `[from the install record]` when it reads
558
+ what the install recorded, with why (installed by an older chi, the file edited
559
+ or gone) and the command that fixes it (`chi update` keeps it where it can,
560
+ [CLI: Updating](cli.md#updating)).
556
561
  - **A dev install** (`chi bundle install --dev PATH`) links the hooks, rules,
557
562
  plugin and scripts to `PATH` but still copies the memories at install, so
558
563
  an edit to one of them in `PATH` isn't seen until the next install.
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);
@@ -1351,6 +1352,10 @@ context_providers:
1351
1352
  and becomes the installed bundle's folder each time it runs, so an
1352
1353
  upgrade moves nothing. `{ruby}` becomes the Ruby chi runs on (a `ruby` on
1353
1354
  the PATH may be another one, such as macOS's 2.6).
1355
+ - A bundle whose `requires_chi` this chi doesn't meet provides nothing:
1356
+ `.resolve` and `.any?` skip it (logged once per process per bundle and
1357
+ version), and a source already attached to one — before a chi downgrade,
1358
+ say — is refused by name, as a changed script is.
1354
1359
  - The command follows [the contract](context.md#the-command-contract).
1355
1360
 
1356
1361
  ## Block renderers
@@ -1390,9 +1395,9 @@ block_renderers:
1390
1395
 
1391
1396
  ## Stable for bundles
1392
1397
 
1393
- Bundles normally use only `chi.*` and `ctx`; the constants and helpers below
1394
- are the rest of the surface they may use. A change to any of them gets a
1395
- CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1398
+ Bundles normally use only `chi.*` and `ctx`; the constants, helpers and manifest
1399
+ keys below are the rest of the surface they may use. A change to any of them gets
1400
+ a CHANGELOG line and bumps the shipped bundles that use it. Anything else under
1396
1401
  `Samagotchi::` is internal.
1397
1402
 
1398
1403
  **New API ships with its first caller:** a `chi.*` or `ctx.*` method arrives in the same change as a shipped bundle
@@ -1424,8 +1429,23 @@ that calls it, or for a real external bundle that does. No method on the chance
1424
1429
  `Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
1425
1430
  `Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
1426
1431
  a hook's or `rebase --exec`'s environment names).
1427
-
1428
- `Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
1432
+ 7. **The manifest schema**: the manifest's keys and shapes, as [A bundle with a plugin](#a-bundle-with-a-plugin)
1433
+ and [bundles.md](bundles.md) document them — `name`, `version`, `scope`, `description`, `files`, `hooks`,
1434
+ `trust_level`, `plugin`, `requires_chi`, `requires_chi_next` (shipped bundles only: the release tooling
1435
+ reads it, chi doesn't), `needs`, `scripts`, `context_providers`, `block_renderers`, `includes`,
1436
+ `session_settings`, `settings`. Those are `Manifest::KEYS`; `Manifest::SINCE` is the chi release each
1437
+ key or shape first worked in, and the docs don't copy it — the code is its truth.
1438
+ - A key keeps its meaning. A new key gets a `SINCE` entry and a CHANGELOG line, and an older chi says
1439
+ it ignores it (`unknown-key`).
1440
+ - A later chi reads every key an earlier chi installed: the install keeps the bundle's `manifest.yml`
1441
+ ([Trust](#trust)). One installed before chi kept manifests reads what its install recorded until it is
1442
+ upgraded.
1443
+ - A bundle declares its floor with `requires_chi`, and an install below it is refused (`--force` aside).
1444
+ There is no `api_version:`.
1445
+ - `block_renderers:` and the frame's `chi.defineRenderer` ([Block renderers](#block-renderers)) are part
1446
+ of it. Neither is Ruby, so the scan below doesn't see them.
1447
+
1448
+ `Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of items 1-6,
1429
1449
  and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
1430
1450
  (and its specs) reaches beyond it, each with why: the APIs to design before
1431
1451
  that bundle could leave chi's repo. `spec/samagotchi/testing/boundary_spec.rb`
@@ -1433,9 +1453,6 @@ fails on a reference in neither (docs/testing.md). In `chi update`'s re-check of
1433
1453
  an installed bundle the same scan warns only for a name this chi no longer has
1434
1454
  (`Boundary.gone?`): a reference that still resolves gets no row.
1435
1455
 
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
1456
  **Used by shipped bundles today, but internal** (may change without notice; a
1440
1457
  bundle that needs one should ask for an API instead):
1441
1458
 
@@ -1551,7 +1568,8 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
1551
1568
  `Samagotchi::Testing::Case` (the same helpers).
1552
1569
  - **Installed, not loaded from source:** `Samagotchi::Testing::Install.run(dir, settings: {})` (or
1553
1570
  `bundle.install`) installs the bundle into the sandbox as `chi bundle install` does (sha256 and `requires_chi`
1554
- checked) and loads the installed copy with chi's own loaders. It returns `warnings` (the Installer's),
1571
+ checked: a bundle whose `requires_chi` this chi doesn't meet raises `InstallError` unless `force: true`)
1572
+ and loads the installed copy with chi's own loaders. It returns `warnings` (the Installer's),
1555
1573
  `plugin_failures` / `hook_failures` (each with the `location` in your source, `plugin.rb:12`) and `registered`
1556
1574
  (commands, tools, hooks per event, inits, toolkits, tool and peer sources). A name chi ships raises
1557
1575
  `MemoryBundle::Installer::InstallError` unless `force: true`.
@@ -1579,7 +1597,9 @@ Plugins load when a session starts (`Engine.new`, in the REPL or a
1579
1597
  worker), after the bundle hooks. The steps are:
1580
1598
 
1581
1599
  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`.
1600
+ 2. chi must meet `requires_chi`. A bundle whose `requires_chi` this chi doesn't
1601
+ meet is refused at install, so this step is reached only by one installed
1602
+ with `--force` or by a chi that has since been downgraded.
1583
1603
  3. The file is `module_eval`'d into a new module in the bundle's namespace
1584
1604
  (`Samagotchi::Bundles::<bundle>`).
1585
1605
  4. The class is built, and `register(chi)` runs.
@@ -1609,6 +1629,11 @@ gem.
1609
1629
  source directory's, and an edit loads at the next start.
1610
1630
  - Install only copies the file (`--dev` links it). The code first runs at the
1611
1631
  next session start.
1632
+ - The bundle's `manifest.yml` is kept at install and checked the same way: chi
1633
+ reads hook events, `context_providers:`, `block_renderers:`, settings and
1634
+ `requires_chi` from it only while it has the sha256 recorded at install. An
1635
+ edited one isn't read (chi falls back to what the install recorded). `--dev`
1636
+ links it, unchecked.
1612
1637
  - Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
1613
1638
  chi's own tools; path and command rules see what `targets:` says.
1614
1639
 
@@ -1616,18 +1641,30 @@ gem.
1616
1641
 
1617
1642
  - `chi bundle install <dir>`: copies the plugin to
1618
1643
  `<memories>/.bundles/<name>/plugin/`. It records the plugin's sha256 and
1619
- `requires_chi`, and warns about a wrong declared sha256 or an unmet
1620
- `requires_chi`.
1644
+ `requires_chi`, and warns about a wrong declared sha256. A bundle whose
1645
+ `requires_chi` this chi doesn't meet is **refused**: `Install failed: NAME
1646
+ can't be installed: it requires chi >= X (this is chi Y)`, exit 1, nothing
1647
+ written. `--force` installs it anyway, with the warnings that its plugin,
1648
+ hooks and rules won't load. It keeps the bundle's `manifest.yml` beside the record, and
1649
+ chi reads the manifest from there at load, so for a bundle installed by this
1650
+ chi or later, a key this chi didn't know at install works once a newer chi
1651
+ reads it (one installed before reads what its install recorded until
1652
+ `chi bundle upgrade <name>`).
1653
+ - `chi bundle upgrade <name>`: upgrades an installed bundle by name from the
1654
+ source its record keeps (its folder, archive or git URL, `#ref` included),
1655
+ so a bundle from elsewhere needs no path retyped; `upgrade <source>` takes a
1656
+ new one and says `Source changes: old → new`. A dev install is refused: it
1657
+ already follows its source.
1621
1658
  - `chi bundle status [<name>]`: shows `plugin=plugin.rb` in the list. For
1622
1659
  one bundle it shows `Plugin: plugin.rb [ok|modified|missing]` and a
1623
1660
  `requires_chi` failure.
1624
1661
  - `chi bundle diff <name> [plugin.rb]`: shows the installed base and the
1625
- file on disk.
1662
+ file on disk; for a memory file that stays behind, also the bundle's new text.
1626
1663
  - `chi bundle build --name <name>`: puts the installed plugin (and
1627
1664
  `requires_chi`, its `scripts/` and `context_providers:`) into the built
1628
1665
  bundle.
1629
1666
  - `chi bundle uninstall <name>`: removes the plugin with the bundle.
1630
- - `chi bundle install --dev <dir>`: links the bundle's plugin, hooks, rules and scripts to `<dir>` instead of
1667
+ - `chi bundle install --dev <dir>`: links the bundle's plugin, hooks, rules, scripts and `manifest.yml` to `<dir>` instead of
1631
1668
  copying them, with no sha256 check, so an edit applies at the next worker start
1632
1669
  ([cli.md](cli.md#writing-a-bundle-new-test-check)).
1633
1670
  - `chi bundle new`, `test` and `check`: start a bundle, run its tests on chi's Ruby, check it as an install takes