samagotchi 0.150.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/docs/architecture.md +2 -2
  4. data/docs/bundle-authoring.md +5 -5
  5. data/docs/cli.md +32 -7
  6. data/docs/configuration.md +1 -1
  7. data/docs/guardrails.md +4 -0
  8. data/docs/internals/block-renderers.md +98 -0
  9. data/docs/internals/bridge.md +53 -17
  10. data/docs/internals/bundles.md +49 -14
  11. data/docs/internals/objectives.md +6 -4
  12. data/docs/internals/web-frontend.md +2 -2
  13. data/docs/memory.md +7 -2
  14. data/docs/plugins.md +91 -14
  15. data/docs/releasing.md +7 -2
  16. data/docs/sessions.md +1 -1
  17. data/lib/samagotchi/block_renderers.rb +116 -0
  18. data/lib/samagotchi/bridge/card_places.rb +93 -0
  19. data/lib/samagotchi/bridge/card_store.rb +219 -122
  20. data/lib/samagotchi/bridge.rb +12 -4
  21. data/lib/samagotchi/bundle_command/check.rb +43 -3
  22. data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
  23. data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
  24. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  25. data/lib/samagotchi/bundle_command.rb +88 -10
  26. data/lib/samagotchi/bundle_needs.rb +4 -6
  27. data/lib/samagotchi/bundle_settings_check.rb +14 -13
  28. data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
  29. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
  31. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  33. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  35. data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
  36. data/lib/samagotchi/child_env.rb +15 -0
  37. data/lib/samagotchi/context_providers.rb +42 -10
  38. data/lib/samagotchi/engine.rb +47 -6
  39. data/lib/samagotchi/extension_load.rb +4 -3
  40. data/lib/samagotchi/guardrail_wiring.rb +28 -19
  41. data/lib/samagotchi/guardrails/approval.rb +2 -2
  42. data/lib/samagotchi/guardrails/rules.rb +1 -0
  43. data/lib/samagotchi/guardrails/verdict.rb +20 -0
  44. data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
  45. data/lib/samagotchi/memory_bundle/asset_installer.rb +18 -9
  46. data/lib/samagotchi/memory_bundle/builder.rb +39 -31
  47. data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
  48. data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
  49. data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
  50. data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -43
  51. data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -14
  52. data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
  53. data/lib/samagotchi/memory_bundle/installer.rb +91 -16
  54. data/lib/samagotchi/memory_bundle/manifest.rb +166 -5
  55. data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
  56. data/lib/samagotchi/memory_bundle/merger.rb +7 -8
  57. data/lib/samagotchi/memory_bundle/profile.rb +3 -2
  58. data/lib/samagotchi/memory_bundle/provenance.rb +220 -19
  59. data/lib/samagotchi/memory_bundle/shas.rb +4 -3
  60. data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
  61. data/lib/samagotchi/memory_bundle/status.rb +11 -10
  62. data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
  63. data/lib/samagotchi/plugin/loader.rb +1 -1
  64. data/lib/samagotchi/session_chain.rb +2 -7
  65. data/lib/samagotchi/session_commands.rb +15 -9
  66. data/lib/samagotchi/session_config.rb +5 -4
  67. data/lib/samagotchi/session_manager.rb +10 -7
  68. data/lib/samagotchi/steer.rb +17 -0
  69. data/lib/samagotchi/system_one/record.rb +10 -2
  70. data/lib/samagotchi/terminal_ui.rb +4 -4
  71. data/lib/samagotchi/tools/delegate.rb +20 -2
  72. data/lib/samagotchi/turn_checkpoint.rb +11 -5
  73. data/lib/samagotchi/turn_flow.rb +2 -3
  74. data/lib/samagotchi/update_command.rb +13 -2
  75. data/lib/samagotchi/version.rb +1 -1
  76. data/lib/samagotchi/web/app.rb +24 -13
  77. data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
  78. data/lib/samagotchi/web/markdown_renderer.rb +39 -6
  79. data/lib/samagotchi/web/message_display.rb +3 -3
  80. data/lib/samagotchi/web/participant_view.rb +1 -1
  81. data/lib/samagotchi/web/public/app.js +2 -0
  82. data/lib/samagotchi/web/public/block_frame.js +202 -0
  83. data/lib/samagotchi/web/public/block_host.js +502 -0
  84. data/lib/samagotchi/web/public/block_renderers.css +20 -0
  85. data/lib/samagotchi/web/public/block_source.js +37 -0
  86. data/lib/samagotchi/web/public/bubbles.js +9 -5
  87. data/lib/samagotchi/web/public/card.js +17 -10
  88. data/lib/samagotchi/web/public/cards_place.js +2 -2
  89. data/lib/samagotchi/web/public/claim_link.js +4 -2
  90. data/lib/samagotchi/web/public/history_reveal.js +4 -1
  91. data/lib/samagotchi/web/public/index.html +2 -0
  92. data/lib/samagotchi/web/public/markdown_place.js +2 -3
  93. data/lib/samagotchi/web/public/text_roots.js +6 -0
  94. data/lib/samagotchi/web/public/turn_html.js +4 -1
  95. data/lib/samagotchi/web/session_access_routes.rb +2 -1
  96. data/lib/samagotchi/web/session_hub.rb +6 -0
  97. data/lib/samagotchi/web/session_show.rb +1 -1
  98. data/lib/samagotchi/worker.rb +4 -2
  99. metadata +13 -1
@@ -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,31 +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 |
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)) |
51
58
  | `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
52
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 |
53
- | `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 |
54
- | `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 |
55
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`) |
56
63
  | `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
57
64
  | `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
@@ -69,7 +76,16 @@ and a normal install from replacing each other (this is also why `chi bundle upg
69
76
 
70
77
  Readers of the installed state: `Hooks::BundleLoader` (Engine), `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
71
78
  `ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
72
- 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.
73
89
  The checks live in `Hooks::BundleLoader.load` and `Plugin::Loader.load_bundle`; the loading itself is
74
90
  `BundleLoader.load_hook_file` (one hook file, into a given `namespace:`) and `Plugin::Loader.register_file` (one
75
91
  plugin file: build, `register`, `commit!`), which raise instead of logging: the bundle test kit
@@ -80,12 +96,23 @@ spec's shape). A load failure carries `LoadFailures::Failure#location` (the firs
80
96
  bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`,
81
97
  `hooks/`) back to the source dir.
82
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
+
83
110
  ## Installer#run, in order
84
111
 
85
112
  The order matters: warnings print in it, and a plugin/script the manifest names but the bundle lacks raises
86
113
  `InstallError` after the memories are already copied.
87
114
 
88
- 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").
89
116
  2. `resolve_target`: scope = CLI > manifest > `system`; then `check_source` against the existing record
90
117
  (`SourceIdentity`, source.rb): a name differing from an installed one only in case raises (`force` too); a
91
118
  shipped name (any case) from a source that isn't a dir in the Installer's `shipped_dir:` (default `SHIPPED_DIR`;
@@ -128,7 +155,13 @@ doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' eve
128
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
129
156
  run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
130
157
 
131
- 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
132
165
  owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
133
166
  files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
134
167
  the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
@@ -139,5 +172,7 @@ copy is still there. Any other keeps its old base and mark, and the command exit
139
172
  `select_memories` (all but `index.md`/hidden; with `--files` the named ones + their overlays, else leaving out files
140
173
  another installed bundle owns) → `checksum_memories` → `installed_parts` (`Builder::InstalledParts`: the same-name
141
174
  installed bundle's hooks, rules, plugin, scripts, context providers, else the scope dir's `hooks/`/`guardrails/`) →
142
- `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
143
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
@@ -23,7 +23,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
23
23
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
24
24
  until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
25
25
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
26
- `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js` and the `objectives_*.js` modules are new modules for that reason.
26
+ `session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js` and the `objectives_*.js` modules are new modules for that reason. The last three `block_*` draw a bundle's fenced blocks: [block-renderers.md](block-renderers.md).
27
27
 
28
28
  ## Two pages, two models
29
29
 
@@ -50,7 +50,7 @@ change.
50
50
  | **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
51
51
  | **`store.sessions`** (the hub's projection) | every card | `store.update({sessions})` from app.js: `openSessionEvents` (`applySessionEvent`), `refresh` (the list fallback), `addCreatedSession`, `applyView` (re-sorted), `replaceSelectedCard` (`setLiveStatus` through `setStatus`, `readTurnEnd`, `setUsedMemories`), the stream's close (`withoutWorker`); `session_actions.js` (`setSessions`) | the store: `lists` and `infoBar`. **Always a new array**; an entry is replaced, never changed in place. `followSelectedSession` copies the open one's owner and worker (`fieldsFromCard`) and, when not streaming, its status (`setStatus(…, "hub")`) into `store.current` |
52
52
  | **Factory closures**: the `create*` factories (popovers, panels, views) | each component's own state: open-for, busy, error, its own fetched list | the factory | an `onChange` hook back into app.js, or a getter the factory reads at draw time |
53
- | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
53
+ | **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's or the recap's `turns_since`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
54
54
  | **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
55
55
  | **SSE** | see Streams | the server | owner page: events go into the DOM directly; participant page: an event marks the data stale |
56
56
 
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,11 +553,16 @@ 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.
559
564
  - **The rest of the manifest** (`plugin:`, `requires_chi`, `session_settings:`,
560
- `context_providers:`, `requires_chi_next`) is in
565
+ `context_providers:`, `block_renderers:`, `requires_chi_next`) is in
561
566
  [Plugins](plugins.md) and [internals/bundles.md](internals/bundles.md).
562
567
 
563
568
  ## Bundle profiles: core and dev
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);
@@ -944,7 +945,9 @@ completed turn stays in its step.
944
945
 
945
946
  Between turns, a card goes above the first turn prompt after it, live and
946
947
  after a reload. A failed turn whose prompt went back to its sender isn't a
947
- turn: its prompt line comes after the cards.
948
+ turn: its prompt line comes after the cards shown before it, and before those
949
+ shown after. After a reload a card a command showed names it (`from /short`),
950
+ its line being gone.
948
951
 
949
952
  An [anytime command](#anytime-true)'s cards show as it shows them, after its
950
953
  line, in every UI, whether a turn runs or not.
@@ -961,8 +964,8 @@ then a new turn) starts from them. Load warnings and a question still
961
964
  waiting when the worker went are not saved.
962
965
 
963
966
  The event is `{type: :card, id:, source:, title:, body:, level:, actions:,
964
- in_turn:}` (`source` is the bundle), logged as `card` with its source, id and
965
- title.
967
+ in_turn:}` (`source` is the bundle), plus `command:` when a session command
968
+ showed it (`"/short"`), logged as `card` with its source, id and title.
966
969
 
967
970
  ## Side answers: `ctx.ask_model`
968
971
 
@@ -1349,13 +1352,52 @@ context_providers:
1349
1352
  and becomes the installed bundle's folder each time it runs, so an
1350
1353
  upgrade moves nothing. `{ruby}` becomes the Ruby chi runs on (a `ruby` on
1351
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.
1352
1359
  - The command follows [the contract](context.md#the-command-contract).
1353
1360
 
1361
+ ## Block renderers
1362
+
1363
+ A bundle can draw a fenced code block as a picture in chi web's answers: the model writes ```` ```mermaid ````, the
1364
+ page shows the drawing. No tool, no plugin code in the web server: chi web runs no plugins, and a reloaded page must
1365
+ draw after the worker has gone, so JS draws and Ruby only routes and serves.
1366
+
1367
+ ```yaml
1368
+ # manifest.yml
1369
+ scripts:
1370
+ draw.js: sha256:… # scripts/draw.js in the bundle
1371
+ block_renderers:
1372
+ - lang: mermaid # a fence's first word, downcased: [a-z0-9_+-], at most 32
1373
+ load: [draw.js] # scripts: keys, loaded into the frame in this order
1374
+ ```
1375
+
1376
+ - `load:` names files under `scripts:`, so they install and check as any script does (`chi bundle check`,
1377
+ `rake bundles:sha`), and one changed after the install isn't served until the bundle is reinstalled; a `--dev`
1378
+ install's links aren't checked. A bundle with no memories installs fine, but `chi bundle build` still needs one:
1379
+ it refuses with "No memories to build".
1380
+ - The first installed bundle (by name) with a language draws it. A bundle whose `requires_chi` this chi doesn't meet
1381
+ draws nothing.
1382
+ - **What it needs of the page:** `web.markdown` on (a default-off setting, and the commonmarker gem). With it off a
1383
+ fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
1384
+ the model: say so in the bundle's README.
1385
+ - **Where it draws:** the owner's page only, in answers and closed steps. Participants' pages, cards, question
1386
+ replies, the asking slot, delegate reports and the doc viewer show the source, and so does the terminal.
1387
+ - **The script's side:** the last file calls `chi.defineRenderer({ init, render })`. `init()` (optional, may be async)
1388
+ runs once; `render(source, theme)` returns the SVG as a string, or a promise of one. Throw to keep the block as
1389
+ source. `theme` is `{name, bg, panel, text, muted, border, accent, font}`, from the page's tokens. The script runs in
1390
+ a sandboxed frame with no cookie and no access to the page, and the CSP closes its network, but for WebRTC (what the
1391
+ frame holds, the diagrams it rendered, could leave that way). The SVG is shown as an `<img>`, so its scripts, links
1392
+ and remote images don't run: it has to stand alone (no `foreignObject`, every namespace prefix bound) and stay under 50,000 px a
1393
+ side and 100M px. Over that, or if the image doesn't decode, the block stays source. The
1394
+ layers and the residual risk are in [internals/block-renderers.md](internals/block-renderers.md).
1395
+
1354
1396
  ## Stable for bundles
1355
1397
 
1356
- Bundles normally use only `chi.*` and `ctx`; the constants and helpers below
1357
- are the rest of the surface they may use. A change to any of them gets a
1358
- 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
1359
1401
  `Samagotchi::` is internal.
1360
1402
 
1361
1403
  **New API ships with its first caller:** a `chi.*` or `ctx.*` method arrives in the same change as a shipped bundle
@@ -1387,8 +1429,23 @@ that calls it, or for a real external bundle that does. No method on the chance
1387
1429
  `Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
1388
1430
  `Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
1389
1431
  a hook's or `rebase --exec`'s environment names).
1390
-
1391
- `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,
1392
1449
  and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
1393
1450
  (and its specs) reaches beyond it, each with why: the APIs to design before
1394
1451
  that bundle could leave chi's repo. `spec/samagotchi/testing/boundary_spec.rb`
@@ -1511,7 +1568,8 @@ docs/testing.md). **Minitest:** `require "samagotchi/testing/minitest"` and subc
1511
1568
  `Samagotchi::Testing::Case` (the same helpers).
1512
1569
  - **Installed, not loaded from source:** `Samagotchi::Testing::Install.run(dir, settings: {})` (or
1513
1570
  `bundle.install`) installs the bundle into the sandbox as `chi bundle install` does (sha256 and `requires_chi`
1514
- 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),
1515
1573
  `plugin_failures` / `hook_failures` (each with the `location` in your source, `plugin.rb:12`) and `registered`
1516
1574
  (commands, tools, hooks per event, inits, toolkits, tool and peer sources). A name chi ships raises
1517
1575
  `MemoryBundle::Installer::InstallError` unless `force: true`.
@@ -1539,7 +1597,9 @@ Plugins load when a session starts (`Engine.new`, in the REPL or a
1539
1597
  worker), after the bundle hooks. The steps are:
1540
1598
 
1541
1599
  1. The file must match the sha256 recorded at install (not checked for a bundle installed with `--dev`).
1542
- 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.
1543
1603
  3. The file is `module_eval`'d into a new module in the bundle's namespace
1544
1604
  (`Samagotchi::Bundles::<bundle>`).
1545
1605
  4. The class is built, and `register(chi)` runs.
@@ -1569,6 +1629,11 @@ gem.
1569
1629
  source directory's, and an edit loads at the next start.
1570
1630
  - Install only copies the file (`--dev` links it). The code first runs at the
1571
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.
1572
1637
  - Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
1573
1638
  chi's own tools; path and command rules see what `targets:` says.
1574
1639
 
@@ -1576,18 +1641,30 @@ gem.
1576
1641
 
1577
1642
  - `chi bundle install <dir>`: copies the plugin to
1578
1643
  `<memories>/.bundles/<name>/plugin/`. It records the plugin's sha256 and
1579
- `requires_chi`, and warns about a wrong declared sha256 or an unmet
1580
- `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.
1581
1658
  - `chi bundle status [<name>]`: shows `plugin=plugin.rb` in the list. For
1582
1659
  one bundle it shows `Plugin: plugin.rb [ok|modified|missing]` and a
1583
1660
  `requires_chi` failure.
1584
1661
  - `chi bundle diff <name> [plugin.rb]`: shows the installed base and the
1585
- file on disk.
1662
+ file on disk; for a memory file that stays behind, also the bundle's new text.
1586
1663
  - `chi bundle build --name <name>`: puts the installed plugin (and
1587
1664
  `requires_chi`, its `scripts/` and `context_providers:`) into the built
1588
1665
  bundle.
1589
1666
  - `chi bundle uninstall <name>`: removes the plugin with the bundle.
1590
- - `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
1591
1668
  copying them, with no sha256 check, so an edit applies at the next worker start
1592
1669
  ([cli.md](cli.md#writing-a-bundle-new-test-check)).
1593
1670
  - `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
@@ -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. `release:bump` raises a
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
@@ -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, or a path with spaces. 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.
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`.