samagotchi 0.157.0 → 0.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +1 -1
- data/bin/chi +3 -1
- data/docs/architecture.md +7 -6
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +2 -1
- data/docs/cli.md +21 -17
- data/docs/configuration.md +7 -5
- data/docs/context.md +5 -3
- data/docs/guardrails.md +8 -7
- data/docs/hooks.md +44 -80
- data/docs/internals/bridge.md +1 -1
- data/docs/internals/bundles.md +47 -34
- data/docs/internals/engine.md +7 -5
- data/docs/memory.md +39 -9
- data/docs/plugins.md +13 -9
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +5 -5
- data/docs/testing.md +4 -4
- data/lib/samagotchi/bridge/card_places.rb +5 -13
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
- data/lib/samagotchi/bundle_command/check.rb +23 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command.rb +15 -43
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +4 -2
- data/lib/samagotchi/engine.rb +1 -6
- data/lib/samagotchi/extension_load.rb +20 -41
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/memory_bundle/asset_installer.rb +39 -119
- data/lib/samagotchi/memory_bundle/builder.rb +10 -34
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
- data/lib/samagotchi/memory_bundle/installer.rb +32 -14
- data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
- data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
- data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +30 -35
- data/lib/samagotchi/memory_bundle/status.rb +12 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
- data/lib/samagotchi/memory_bundle/trash.rb +7 -12
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/plugin/api.rb +1 -4
- data/lib/samagotchi/plugin/loader.rb +42 -5
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/recap_place.rb +3 -4
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session.rb +44 -18
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_field.rb +5 -4
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -5
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +12 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -0
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- metadata +3 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
data/docs/hooks.md
CHANGED
|
@@ -146,7 +146,7 @@ they must never spend or permit. See
|
|
|
146
146
|
chi newer than 0.85.0 (an older one raises on the keyword).
|
|
147
147
|
|
|
148
148
|
`event[:hook]` is the label the notices carry: `plugin.rb (bundle
|
|
149
|
-
known-names)` for a plugin's `chi.on` hook
|
|
149
|
+
known-names)` for a plugin's `chi.on` hook,
|
|
150
150
|
`audit.rb (config)` for a config hook,
|
|
151
151
|
`turn hook` for one registered at runtime.
|
|
152
152
|
|
|
@@ -438,13 +438,13 @@ sources' refs in the reply that asks, and in the question's text and options).
|
|
|
438
438
|
|
|
439
439
|
## Settings
|
|
440
440
|
|
|
441
|
-
A hook class whose `initialize` takes an argument gets its settings: **one
|
|
441
|
+
A hook class (a config hook's; a plugin's class the same way, [plugins.md](plugins.md)) whose `initialize` takes an argument gets its settings: **one
|
|
442
442
|
positional Hash with string keys** (`def initialize(settings = {})`;
|
|
443
443
|
`initialize(**kw)` is not supported). A class whose `initialize` takes none
|
|
444
444
|
is built bare. Defaults belong in the hook.
|
|
445
445
|
|
|
446
446
|
```yaml
|
|
447
|
-
bundles: # per bundle, by name, for its
|
|
447
|
+
bundles: # per bundle, by name, for its plugin
|
|
448
448
|
known-names:
|
|
449
449
|
names: [jonathandoe]
|
|
450
450
|
mode: reject
|
|
@@ -477,22 +477,10 @@ once in the UI.
|
|
|
477
477
|
Hook failures never break the engine loop — each hook is wrapped in its own
|
|
478
478
|
try/catch.
|
|
479
479
|
|
|
480
|
-
A
|
|
481
|
-
worker, on the plugins' surface
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
```
|
|
485
|
-
plugins> guardrail guardrails.rb (bundle my-guard) before_tool_call hook failed (RuntimeError: boom):
|
|
486
|
-
it denies every tool call it guards, and its next failures are in each denial's reason
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
The label names the file, the bundle and the event, so one line per hook: a
|
|
490
|
-
hook on a stream can't nag. `on_error` still decides what happens at every
|
|
491
|
-
firing — `skip` logs nothing, `log` warns each time (a stream hook at most
|
|
492
|
-
once a minute), a fail_closed `before_tool_call` hook denies the call — the
|
|
493
|
-
notice is only added on top. A config hook (config.yml's own files) keeps its
|
|
494
|
-
behaviour: no notice, just the log. A UI that joins later reads the same lines
|
|
495
|
-
from `Engine#plugin_warning`.
|
|
480
|
+
A plugin's `chi.on` handler that **raises** tells you once per hook in a
|
|
481
|
+
worker, on the plugins' surface ([plugins.md](plugins.md)). A config hook
|
|
482
|
+
(config.yml's own files) keeps its behaviour: no notice, just the log. A UI
|
|
483
|
+
that joins later reads the same lines from `Engine#plugin_warning`.
|
|
496
484
|
|
|
497
485
|
In the [bundle test kit](plugins.md#testing-a-bundle-experimental) a hook that raises raises
|
|
498
486
|
`Testing::HookFailed` out of the call that fired it, under every `on_error`.
|
|
@@ -660,70 +648,46 @@ declared tool's call, with no `via:`; a direct `mcp_fs_read` fires as the `call_
|
|
|
660
648
|
stands for (`tool: "call_tool"`, `args: {"tool" => "fs/read", "args" => {…}}`), as if the model
|
|
661
649
|
had called `call_tool`.
|
|
662
650
|
|
|
663
|
-
## Bundle
|
|
664
|
-
|
|
665
|
-
Bundles can ship executable guardrails alongside memories. A bundle with hooks lives as a directory with a `hooks/` subdirectory (flat, basename-keyed):
|
|
666
|
-
|
|
667
|
-
```
|
|
668
|
-
my-bundle/
|
|
669
|
-
manifest.yml
|
|
670
|
-
identity.md
|
|
671
|
-
hooks/
|
|
672
|
-
guardrails.rb # class Guardrails; def call(event); ...; end; end
|
|
673
|
-
audit.rb
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
`manifest.yml` may carry an optional `hooks:` map (both `files:` and `hooks:` are optional; a bundle may carry only one):
|
|
651
|
+
## Bundle hooks
|
|
677
652
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
identity.md: sha256:abc...
|
|
684
|
-
hooks:
|
|
685
|
-
guardrails.rb:
|
|
686
|
-
sha256: 1234...
|
|
687
|
-
event: before_tool_call
|
|
688
|
-
on_error: fail_closed # skip (default) | log | fail_closed (before_tool_call only)
|
|
689
|
-
priority: 10
|
|
690
|
-
audit.rb:
|
|
691
|
-
sha256: 5678...
|
|
692
|
-
event: after_tool_call
|
|
693
|
-
on_error: log
|
|
694
|
-
priority: 100
|
|
695
|
-
trust_level: reviewed # reviewed | trusted | experimental (default)
|
|
696
|
-
requires_chi: ">= 0.12.0" # optional: a gem-style requirement, as for plugins
|
|
697
|
-
needs: [gh] # optional: outside commands the memories use (docs/memory.md#bundles-that-need-outside-commands)
|
|
698
|
-
```
|
|
653
|
+
A bundle hooks events from its `plugin.rb` with `chi.on` (next to its
|
|
654
|
+
commands and tools): see [Plugins](plugins.md). There `ctx.notify`,
|
|
655
|
+
`ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`…
|
|
656
|
+
do for the event the block runs for, and `chi.on(:before_tool_call,
|
|
657
|
+
on_error: :fail_closed)` makes a guard that fails closed.
|
|
699
658
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
- Hook key = basename (flat under `hooks/`). No subdirs in v1.
|
|
703
|
-
- `event` is required for auto-registration; a hook with no event is skipped.
|
|
704
|
-
- `events: [after_turn, question_asked]` (a list, in place of `event:`) registers the hook for each event it
|
|
705
|
-
names: **one** instance serves them all (its state is shared), and `#call` tells them apart by
|
|
706
|
-
`event[:type]`. `event:` and `events:` together, or an `events:` that isn't a non-empty list of event names,
|
|
707
|
-
is refused at install (`hooks: <file>: …`). `on_error: fail_closed` applies to its `before_tool_call`
|
|
708
|
-
registration only. Needs chi newer than 0.97.0 (an older chi installs the hook with no event and never loads
|
|
709
|
-
it): set `requires_chi`.
|
|
710
|
-
- `sha256` is integrity (not authenticity). No signing in v1. Install records the sha256 of the copied file; at `Engine.new` a hook whose file differs is not loaded (reinstall the bundle after editing one by hand).
|
|
711
|
-
- Hook code is the bundle author's source of truth: on upgrade, hooks are overwritten; if the installed file was locally modified, a warning is emitted (`was locally modified; overwriting`).
|
|
712
|
-
- `on_error` defaults to `skip` for every event, `:before_tool_call` included: a raising hook is silent and the call goes ahead. `log` warns. `fail_closed` (only on `:before_tool_call`; on any other event it acts as `skip`) denies the call the hook raised on. Choose `fail_closed` for a hook that is a guardrail, whose failure should stop tool calls rather than let them through.
|
|
713
|
-
- `requires_chi`: when this chi doesn't meet it, none of the bundle's hooks load (each is reported, `not loaded: it requires chi …`), as for a [plugin](plugins.md#loading-and-when-it-fails).
|
|
714
|
-
- A `fail_closed` `:before_tool_call` hook is required: if it is missing, fails to load, its sha256 differs or chi doesn't meet the bundle's `requires_chi`, chi denies every tool call until it is fixed.
|
|
659
|
+
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order. Bundle hooks are process-scoped: they survive the per-turn `clear_hooks` (only plain hooks are cleared).
|
|
660
|
+
- Settings: the plugin's class gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
715
661
|
- A bundle can also ship YAML rules in `guardrails/*.yml`; see [Guardrails](guardrails.md#the-guardrails-bundle).
|
|
716
|
-
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
717
|
-
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
718
|
-
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
|
|
719
662
|
- Shipped bundles (each but web-search in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a plugin's `before_tool_call` hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a plugin's `after_turn` and `question_asked` hook: links source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Bundles](bundles.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Bundles](bundles.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Bundles](bundles.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Bundles](bundles.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Bundles](bundles.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Bundles](bundles.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Bundles](bundles.md#the-cost-cap-bundle)), `chi bundle install work-cap` (a plugin: a per-session cap on tool calls and active minutes, `/workcap`, see [Bundles](bundles.md#the-work-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Bundles](bundles.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Bundles](bundles.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Bundles](bundles.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Bundles](bundles.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Bundles](bundles.md#the-claude-peer-bundle)), `chi bundle install model-notes` (memories: reviewed model notes, see [Bundles](bundles.md#the-model-notes-bundle)), `chi bundle install decision-card` (a plugin: a long answer's question as a card, see [Bundles](bundles.md#the-decision-card-bundle)), `chi bundle install inline-widgets` (a plugin: prompt sections that teach the `chi-ui` blocks chi web draws, see [Bundles](bundles.md#the-inline-widgets-bundle)) and `chi bundle install web-search` (a plugin: a `web_search` tool through your SearXNG, in no profile, see [Bundles](bundles.md#the-web-search-bundle)).
|
|
720
|
-
- Installing a bundle
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
663
|
+
- Installing a bundle runs its plugin at `Engine` startup. Only install bundles you trust, as you would a gem.
|
|
664
|
+
|
|
665
|
+
### Class-file hooks are retired
|
|
666
|
+
|
|
667
|
+
Bundles used to ship hooks as class files: `hooks/*.rb` with a `hooks:` map in
|
|
668
|
+
`manifest.yml` (`event:`, `on_error:`, `priority:`, `sha256:`). chi no longer
|
|
669
|
+
installs or loads them:
|
|
670
|
+
|
|
671
|
+
- Installing or upgrading a source that has a `hooks:` entry or a `hooks/*.rb`
|
|
672
|
+
file is refused (`hooks: class-file bundle hooks are retired: …`), its
|
|
673
|
+
`--dry-run` too; `chi bundle check` reports it as `hooks-retired`. An empty
|
|
674
|
+
`hooks: {}` is fine.
|
|
675
|
+
- A bundle an older chi installed with hooks keeps its memories, rules, plugin
|
|
676
|
+
and scripts. Its hooks don't load: the first turn says so (`hooks (bundle X)
|
|
677
|
+
failed to load (class-file bundle hooks are retired: …)`, not a required
|
|
678
|
+
failure, so tool calls still run), `chi bundle status X` lists them under
|
|
679
|
+
`Hooks (N): not loaded`, and `chi update` adds a `hooks-retired` row for a
|
|
680
|
+
bundle chi doesn't ship.
|
|
681
|
+
- The next install or upgrade from a source without hooks removes the old
|
|
682
|
+
`hooks/` (an edited hook, or a file the record doesn't know, is copied to
|
|
683
|
+
the trash first; a dev link never): `Removed (chi no longer loads
|
|
684
|
+
class-file bundle hooks): hooks/x.rb`, and the dry run and `chi update`'s
|
|
685
|
+
plan say so. `chi bundle uninstall` still removes them the same way.
|
|
686
|
+
- To port one, move the class's `#call` into a `chi.on(event)` block of the
|
|
687
|
+
bundle's `plugin.rb` (one block per event; `on_error: :fail_closed` for a
|
|
688
|
+
fail_closed guard, with `plugin: required: true` in the manifest), then drop
|
|
689
|
+
`hooks/` and `hooks:`. known-names 0.2.0, source-links 0.7.0 and chi-nah 0.2.0
|
|
690
|
+
are ported examples.
|
|
727
691
|
|
|
728
692
|
## The source-links bundle
|
|
729
693
|
|
|
@@ -807,7 +771,7 @@ source is skipped whole (its partial matches are discarded) with a warning,
|
|
|
807
771
|
and the others still report. An entry with neither `prefix:` nor `pattern:`,
|
|
808
772
|
or a pattern that does not compile, is skipped with a warning at load. The
|
|
809
773
|
hook is `on_error: log`: a bug in it warns and the turn is unaffected. As with
|
|
810
|
-
every
|
|
774
|
+
every plugin, a running worker picks it up after its next start.
|
|
811
775
|
|
|
812
776
|
### Placeholders, and the project's own repo
|
|
813
777
|
|
data/docs/internals/bridge.md
CHANGED
|
@@ -125,7 +125,7 @@ the "asking again" and steer-cut rows, ✂ rows and questions (with how they wer
|
|
|
125
125
|
comes mid-turn but isn't the turn's (an anytime command's, such as `/btw`) goes after that turn (`during`), and so does a
|
|
126
126
|
between-turns notice. A card with an earlier card's id replaces it in place and keeps its anchor. A `list` with no messages
|
|
127
127
|
(`?cards=1`'s bodies, `chi bundle try`'s trace) has no `turns_since` and no `opening_id`.
|
|
128
|
-
- **The stamp.** Next to `turns_since`, an entry lists `opening_id` (`CardPlaces#opening_of`): the id of the opening its count
|
|
128
|
+
- **The stamp.** Next to `turns_since`, an entry lists `opening_id` (`CardPlaces#opening_of`, one `Steer.opening_stamp` rule with the recap's): the id of the opening its count
|
|
129
129
|
reaches, the one it comes after: a prompt's `turn_id` (a wake note's too), or `/continue no`'s reason line's `opening_id`. It is
|
|
130
130
|
resolved at `list` time from the same anchor (an entry's own turn often has no bubble: a Continue, a failed or rolled-back turn),
|
|
131
131
|
so the stamp and the count always name one place, and the page finds it by the bubble (`card_stamps.js`: `data-turn-id`, or `data-opening-id` on a wake's note or a reason line), not by counting
|
data/docs/internals/bundles.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Memory bundles: install, upgrade and build
|
|
2
2
|
|
|
3
3
|
How `lib/samagotchi/memory_bundle/` turns a bundle (a dir, git URL, zip or tar holding `manifest.yml`, `*.md`
|
|
4
|
-
memories and optional `
|
|
4
|
+
memories and optional `guardrails/`, a plugin file and `scripts/`) into installed state, and back. The
|
|
5
5
|
user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memory.md#what-a-bundle-owns) and
|
|
6
6
|
[plugins.md](../plugins.md) and [bundles.md](../bundles.md).
|
|
7
7
|
|
|
@@ -21,7 +21,8 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
21
21
|
#write, #refresh_marks, gone with #resolve_conflicts; #prune_incoming takes a settled file
|
|
22
22
|
at any depth and the dirs it leaves empty; read by BundleCommand#print_incoming for `chi bundle diff`);
|
|
23
23
|
nothing globs it for memories (Builder takes the scope dir's top level), and an uninstall's rm_rf sweeps it
|
|
24
|
-
<name>/
|
|
24
|
+
<name>/guardrails/ plugin/ scripts/
|
|
25
|
+
<name>/hooks/ an older chi's class-file hooks (none loads; the next install or upgrade removes it)
|
|
25
26
|
.trash/<name>-<stamp>/ files an uninstall or upgrade took away, and copies of what an install
|
|
26
27
|
overwrote (Trash), never deleted
|
|
27
28
|
```
|
|
@@ -36,31 +37,31 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
36
37
|
| `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 |
|
|
37
38
|
| `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` |
|
|
38
39
|
| `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 |
|
|
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 /
|
|
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
|
|
40
|
+
| `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 / 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 |
|
|
41
|
+
| `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 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 |
|
|
41
42
|
| `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
|
|
42
43
|
| `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 |
|
|
43
44
|
| `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 |
|
|
44
45
|
| `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) |
|
|
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,
|
|
46
|
+
| `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, 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 typed `session_settings:`/`settings:`) — for check's `requires-chi-floor`. A `hooks:` entry is `Manifest::HooksRetired` (a `ValidationError`; `WORDS` is the one wording of the retirement, `.for_key`, `.for_files`, `.files_in` a source's `hooks/*.rb`) on a strict read and ignored by `Manifest.tolerant` |
|
|
46
47
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
47
|
-
| `AssetInstaller` | the parts that live in `.bundles/<name>/`:
|
|
48
|
+
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: guardrail rules, plugin, scripts (and `#retire_hooks`: an older chi's `hooks/` removed, below); 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_edits`, 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). The rule alone, read-only, is `Trash.edit?` / `Trash.edits`. A dry run reports each asset as the real run would (`would_install`, `would_update`, up to date): one result, `#result_for`, recorded as is or through `#planned` |
|
|
48
49
|
| `FileState` (file_state.rb) | one bundle memory as a command meets it, derived, never stored: `FileState.of(key, record:, provenance:, target_dir:, source_dir:, claimants:, shipped:)` reads the file (D), its base (B), the bundle's text (I) and the record's entry (R) once into `Digests` (`disk`, `base`, `incoming`, `recorded`, `marked_incoming`; `:unreadable` for a file it can't read) and names its `kind` and `mark` (`:none`, `:current`, `:legacy` = `InstalledBundle.legacy_mark?`). With a source (install, upgrade, remerge, restore, a plan): `:new`, `:user_same`, `:user_file` (not the bundle's), `:missing`, `:level` (D == I), `:pristine`, `:edited`, `:diverged`, `:pristine_no_base`, `:edited_no_base`, `:dropped_pristine`, `:dropped_edited`, `:dropped_missing` (`shipped: false`: the bundle no longer ships the key), `:unreadable`; with `source_dir: nil` (status, uninstall, diff) only `:pristine`, `:edited`, `:pristine_no_base`, `:edited_no_base`, `:missing`, `:unreadable`. Edited or not is always `#edited?` = `!Provenance.unchanged?` (the one edited rule: the checksum first, then the base); the base then says what changed, and a base the checksum contradicts (equal to an edited file) is no base to merge from (`:edited_no_base`). `#source_moved?` (a mark that speaks of other text than the bundle ships: Remerge's first question), `#shared_with` (the other bundles listing it, `Provenance.claimants` asked lazily, at most once: only a dropped file and an uninstall ask) |
|
|
49
50
|
| `FileActions` (file_actions.rb) | what each command that acts does in each state: one table per command (`INSTALL`, `FORCE` = install and upgrade `--force`, `UPGRADE`, `REMERGE`, `RESTORE`, `UNINSTALL`), a cell keyed `[kind, mark]`, `[kind, :any]` or `[kind, :shared]`, its value an `Action` (`do`: `:write_incoming`, `:merge`, `:decide`, `:keep`, `:skip`, `:move_to_trash`, `:refuse`, `:restore`; `save_first`; `next_base`: `:on_disk`, `:incoming`, `:recorded`, `:none`; `word`, the report bucket; `record`: `:own`, `:keep_entry`, `:mark`, `:hold_mark`, `:clear_mark`, `:drop`, `:not_owned`). `.for(command, state)`. A `:decide` cell (`[:diverged, :any]`, the legacy guard's `[:pristine, :legacy]`) isn't a function of kind and mark: `Merger::Upgrade#decision` answers it with an `Outcome`, which `.decided(outcome, command)` turns into the action. A dry run reads its command's row and doesn't do it (`Installer::DRY_WORDS` gives its word). A new kind or a new acting command adds a row in each table it reaches; `file_actions_spec` checks every cell against the bundle-state corpus |
|
|
50
51
|
| `Merger` | the upgrade's merge. `Merger::Upgrade` (provenance, target dir, source dir) is the one way to ask what an upgrade does to a file, nothing written: `#decision(key[, state])` → a `Decision` (the `FileState`, the UPGRADE action with a `:decide` cell answered, and the `Outcome`), `#outcome` its `Outcome` (`status`: `install`, `fast_forward`, `keep`, `noop`, `conflict`, `merged`; `base`: the next base, `Action#next_base`'s words), `#state`, `#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, `Remerge`, `ShippedUpdate.plan` and `SystemBundle.sync(dry_run:)`. A `:decide` cell runs `.merge_outcome` (`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; no base: `TextMerge::NO_BASE`, a base the checksum contradicts: `FileStanding::LEGACY_BASE`). `.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: the upgrade's `[:pristine, :legacy]` cell decides: `#decision` 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. A fast-forward that goes ahead over a mark is a `save_first` cell (`Installer#save_first`): 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) |
|
|
51
52
|
| `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 |
|
|
52
53
|
| `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 |
|
|
53
54
|
| `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 |
|
|
54
|
-
| `Remerge` | the retry at the same version, memory files only, for a file marked `conflict` (its `FileState` and `REMERGE` row; a `:decide` cell through `Merger::Upgrade#decision`): `#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),
|
|
55
|
-
| `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
|
|
56
|
-
| `InstalledManifest` (installed_manifest.rb) | the one answer to "this installed bundle's manifest" (`InstalledBundle#manifest`): requires_chi, trust_level,
|
|
55
|
+
| `Remerge` | the retry at the same version, memory files only, for a file marked `conflict` (its `FileState` and `REMERGE` row; a `:decide` cell through `Merger::Upgrade#decision`): `#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), checking a merged incoming file against the manifest's sha256, in a dry run too. It leaves `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) |
|
|
56
|
+
| `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. 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 (a recorded hook doesn't count: retired); 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) |
|
|
57
|
+
| `InstalledManifest` (installed_manifest.rb) | the one answer to "this installed bundle's manifest" (`InstalledBundle#manifest`): requires_chi, trust_level, 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 an older install's `hooks:` is ignored (no problem row). `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?` |
|
|
57
58
|
| `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`) |
|
|
58
|
-
| `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
|
|
59
|
+
| `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` is an older record's hook entry: `.parse` and `#sha256` (what the retire, uninstall and the trash selection read) |
|
|
59
60
|
| `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)) |
|
|
60
|
-
| `Builder` | `chi bundle build`: memories (+ the installed bundle's
|
|
61
|
+
| `Builder` | `chi bundle build`: memories (+ the installed bundle's rules, plugin, scripts; never an older record's hooks) → zip/tar/dir |
|
|
61
62
|
| `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 |
|
|
62
|
-
| `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`), checked against the manifest's `files:` sha as an installed one is (`FileDigest.mismatch`). A sync's `Result#warnings` reach `chi update`'s row (`UpdateCommand#system_bundle_row`) and `chi bootstrap`'s line (`BootstrapCommand#system_bundle_line`); `ShippedUpdate.apply` puts an update's `Checksum mismatch` warnings in `Row#note`. `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::Row#replaced` is what the upgrade copies to the trash (`ShippedUpdate.trash_bound`: `Trash.edits` / `Trash.edit?` against the shipped source over the
|
|
63
|
-
| `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:`, `
|
|
63
|
+
| `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`), checked against the manifest's `files:` sha as an installed one is (`FileDigest.mismatch`). A sync's `Result#warnings` reach `chi update`'s row (`UpdateCommand#system_bundle_row`) and `chi bootstrap`'s line (`BootstrapCommand#system_bundle_line`); `ShippedUpdate.apply` puts an update's `Checksum mismatch` warnings in `Row#note`. `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::Row#replaced` is what the upgrade copies to the trash (`ShippedUpdate.trash_bound`: `Trash.edits` / `Trash.edit?` against the shipped source over the rules, plugin and scripts folders and an older record's `hooks/` (which `#retire_hooks` removes whole), no recorded sha counting as edited), the same rule and files as `AssetInstaller`'s copies. `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
64
|
+
| `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:`, `plugin:` and `scripts:` are always what the record and the copy hold (no `hooks:`), so a memory the install left out or one with no base 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 |
|
|
64
65
|
| `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); `Provenance.unchanged?` (the record's checksum, else the base; neither counts as edited) is the one edited rule, read by `prune_dropped_files`, the uninstall's refusal and `chi bundle status`'s `modified`; `chi bundle status` (per file, `behind`, `merge_note`, `unreadable`; `issues=` counts behind, missing and unreadable, not edited); `chi bundle list` (`Installed#behind`) |
|
|
65
66
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
66
67
|
| `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
|
|
@@ -68,24 +69,23 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
68
69
|
| `Placeholder` | `{{placeholder}}` patterns left in an installed memory, one line per file from `Installer#detect_placeholders`; a hint, never a gate |
|
|
69
70
|
|
|
70
71
|
A dev install (`chi bundle install --dev PATH`, `Installer#initialize(dev: true)`) links instead of copying:
|
|
71
|
-
`AssetInstaller#place` symlinks each
|
|
72
|
+
`AssetInstaller#place` symlinks each rule file, plugin and script into `.bundles/<name>/` to its realpath in
|
|
72
73
|
the source (memories are still copied: their bases and the index are the install's), and the record gets `"dev": true`
|
|
73
74
|
(`InstalledBundle#dev`, `source` the realpath) with `Provenance::DEV_SHA` (`sha256:dev`, never a hex digest) in
|
|
74
|
-
place of every
|
|
75
|
+
place of every rule, plugin and script sha256. `Provenance.sha_matches?` / `.unchecked?` read it as "not
|
|
75
76
|
checked", which is how the readers below skip it for that bundle only. `Installer#refuse_dev_mismatch` keeps a dev
|
|
76
77
|
and a normal install from replacing each other (this is also why `chi bundle upgrade` refuses a dev bundle);
|
|
77
78
|
`Uninstaller` removes links with `rm_rf`/`rm_f`, which never follow them into the source.
|
|
78
79
|
|
|
79
|
-
Readers of the installed state: `
|
|
80
|
+
Readers of the installed state: `GuardrailWiring#bundle_rules`, `Plugin::Loader`,
|
|
80
81
|
`ContextProviders` (scripts), `BundleNeeds` (index markers). Each goes through `Provenance.each_installed` / `#record`
|
|
81
82
|
and checks the recorded sha256 (and `requires_chi`) before loading. `ContextProviders.resolve`/`.any?` skip a bundle
|
|
82
83
|
whose `requires_chi` this chi doesn't meet, logging `providers_too_new` once per process per bundle and version (a
|
|
83
84
|
Set under a Mutex, like `GuardrailWiring`'s kept-rules notice), and `.command_for` raises `Invalid` for a source
|
|
84
85
|
attached to one. They read shas and state off the record, and
|
|
85
|
-
manifest data (requires_chi,
|
|
86
|
+
manifest data (requires_chi, needs, providers, renderers, settings) through `InstalledBundle#manifest`
|
|
86
87
|
(`InstalledManifest`: the kept `manifest.yml` when its sha holds, else the record's mirror), as do `SessionConfig`, `BundleSettingsCheck`, `BlockRenderers`, `Status`, `BundleCommand`'s
|
|
87
|
-
status and diff, `Builder` and `InstalledCopy`. Their requires_chi checks are `InstalledManifest#requires_chi_failure
|
|
88
|
-
`Hooks::BundleLoader.load` is given the installed bundle's manifest and uses the same gate. `GuardrailWiring#rules_stamp` stats each bundle's
|
|
88
|
+
status and diff, `Builder` and `InstalledCopy`. Their requires_chi checks are `InstalledManifest#requires_chi_failure`. `GuardrailWiring#rules_stamp` stats each bundle's
|
|
89
89
|
`manifest.yml` with its `manifest.json` and `guardrails/*`, so a changed requires_chi re-reads the rules. No reader
|
|
90
90
|
calls `Manifest.new` on a kept file: `Manifest.tolerant` through `InstalledManifest` is the one read.
|
|
91
91
|
A guardrail plugin (`plugin:` `required: true`) is known from `InstalledManifest#plugin_required` before
|
|
@@ -94,15 +94,28 @@ A guardrail plugin (`plugin:` `required: true`) is known from `InstalledManifest
|
|
|
94
94
|
unparsable kept manifest.yml never drops it (an older chi's `PluginRef` ignores the key):
|
|
95
95
|
its failure goes to the hook failures as required (`ExtensionLoad#load_plugins` passes them as `guardrail_failures:`),
|
|
96
96
|
where the gate denies every tool call; a `chi.on(on_error: :fail_closed)` hook is wrapped `Hooks.wrap(policy: :deny)`.
|
|
97
|
-
The checks live in `
|
|
98
|
-
`
|
|
99
|
-
plugin file: build, `register`, `commit!`), which
|
|
97
|
+
The checks live in `Plugin::Loader.load_bundle` (its `.sha_mismatch`; `.namespace_for` is the bundle's
|
|
98
|
+
`Samagotchi::Bundles::<bundle>` module); the loading itself is `Plugin::Loader.register_file` (one
|
|
99
|
+
plugin file: build, `register`, `commit!`), which raises instead of logging: the bundle test kit
|
|
100
100
|
(`Samagotchi::Testing::Bundle`) loads a bundle's source through them, unchecked. `Samagotchi::Testing::Install` is the
|
|
101
|
-
checked path in a sandbox: the Installer, then `ExtensionLoad` (
|
|
101
|
+
checked path in a sandbox: the Installer, then `ExtensionLoad` (config hooks, then `load_plugins`), returning the
|
|
102
102
|
Installer's warnings, this bundle's load failures and what it registered (`Install.registered`, an `Install::Registered` value, also the parity
|
|
103
103
|
spec's shape). A load failure carries `LoadFailures::Failure#location` (the first backtrace frame inside the
|
|
104
|
-
bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin
|
|
105
|
-
|
|
104
|
+
bundle, or a SyntaxError's own line), which `Install` maps from the installed copy (`.bundles/<name>/plugin/`)
|
|
105
|
+
back to the source dir.
|
|
106
|
+
|
|
107
|
+
Class-file hooks (retired). A record an older chi installed may still have a `hooks` map and `hooks/*.rb`
|
|
108
|
+
(`InstalledBundle#hooks`). Nothing loads them: `ExtensionLoad#hooks` adds one not-required failure per such record
|
|
109
|
+
(`hooks (bundle X)`, `Manifest::HooksRetired::WORDS`), which the Engine announces at the first turn; `Status`'s
|
|
110
|
+
`retired_hooks` gives `chi bundle status` its `Hooks (N): not loaded` lines (and the list an issue), and
|
|
111
|
+
`BundleCommand::InstalledCheck` a `hooks-retired` row for `chi update` (an `InstalledCopy` has no hooks). A source with
|
|
112
|
+
hooks is refused before its manifest is read (`Installer#refuse_hooks`, then `Manifest.read`), strict or not, a dry
|
|
113
|
+
run too; `ShippedUpdate`'s plan skips one the same way. Over such a record, `AssetInstaller#retire_hooks` (after the
|
|
114
|
+
plugin and scripts, so a refused install leaves them) copies every edited or unknown file under `hooks/` to the trash
|
|
115
|
+
(`Trash#copy_edits` with the recorded shas; a dev link never), removes the dir and records each as `:removed` (a dry
|
|
116
|
+
run's `:would_remove`, through `#planned`) with the `AssetInstaller::RETIRED` note, which `#summary` lists on a line of
|
|
117
|
+
its own; `ShippedUpdate#trash_bound` names the same files (`Trash.edits` over `hooks/`). `Provenance#write` records
|
|
118
|
+
`"hooks" => {}` and deletes the old hook bases. An older chi reading such a record sees a bundle with no hooks.
|
|
106
119
|
|
|
107
120
|
Records from older chi. A record written before the kept manifest (**legacy**: no `manifest.yml`, no
|
|
108
121
|
`manifest_sha`), or rewritten since by such a chi (**stale**: a kept file, no `manifest_sha`; its file may be an
|
|
@@ -135,12 +148,12 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
135
148
|
differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
|
|
136
149
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
137
150
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
138
|
-
4. `AssetInstaller#run`:
|
|
139
|
-
|
|
140
|
-
file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked
|
|
141
|
-
|
|
151
|
+
4. `AssetInstaller#run`: guardrails, plugin, scripts, then `#retire_hooks` (an older record's `hooks/`); each set
|
|
152
|
+
replaces the previous version's, an edited file copied to the trash first. requires_chi warnings for the plugin and
|
|
153
|
+
rules. A file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`) is skipped with a
|
|
154
|
+
warning (a rule) or raises `InstallError` (the plugin, a script).
|
|
142
155
|
5. An upgrade's `prune_dropped_files`: each dropped file's `FileState` (`shipped: false`) and its `UPGRADE` / `FORCE` row: unchanged (`#edited?` false) → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
143
|
-
6. `verify_memory_checksums` (strict: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; `Remerge`
|
|
156
|
+
6. `verify_memory_checksums` (strict except a dev install, whose own tree may have stale shas; `chi bundle check` flags them: a memory whose action takes the bundle's text, `Action#takes_source?`: installed, forced, an upgrade's or a re-install's fast-forward, or merged into a local edit; it checks the source file the write or merge takes its bytes from, so a dry run checks what it would bring in; a `Remerge` merge checks its incoming file too), `warn_missing_needs`.
|
|
144
157
|
7. `write_provenance` (manifest, not a dry run): each owned file's base comes from its action's `next_base`
|
|
145
158
|
(`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
|
|
146
159
|
file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
|
|
@@ -165,11 +178,11 @@ run the manifest's `files:`, so a manifest with none plans nothing (the shipped
|
|
|
165
178
|
|
|
166
179
|
Results are `@results` (file → `Installer::FileResult`: its `word`, a memory's `FileState` and action, a `note`),
|
|
167
180
|
summarised by `#summary` by word: a file that already holds the bundle's bytes (`:up_to_date`, a memory or a
|
|
168
|
-
|
|
181
|
+
rule/plugin/script, in a dry run as in the real run) is listed as `Up to date:`, and only the files really left
|
|
169
182
|
out (`:kept_own`, a memory's dry-run `:would_skip`) as `Skipped:`. A memory's dry-run word is its action's through
|
|
170
|
-
`Installer::DRY_WORDS`. A dry run's `would_update` (a
|
|
183
|
+
`Installer::DRY_WORDS`. A dry run's `would_update` (a plugin or script an upgrade would change) is `Would update:`; an asset
|
|
171
184
|
row is never `would_skip`. A plain install over a bundle already installed from the same source ends with `NAME was already
|
|
172
|
-
installed from SOURCE: its
|
|
185
|
+
installed from SOURCE: its rules, plugin and scripts now match it and its record was rewritten`, plus
|
|
173
186
|
`; your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)` when
|
|
174
187
|
a memory was left out as the user's (`:kept_own`: an edited one, or one the bundle doesn't own; a never-edited one takes the new text and isn't named there). An upgrade with conflicts keeps an
|
|
175
188
|
owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
|
|
@@ -181,7 +194,7 @@ copy is still there. Any other keeps its old base and mark, and the command exit
|
|
|
181
194
|
|
|
182
195
|
`select_memories` (all but `index.md`/hidden; with `--files` the named ones + their overlays, else leaving out files
|
|
183
196
|
another installed bundle owns) → `checksum_memories` → `installed_parts` (`Builder::InstalledParts`: the same-name
|
|
184
|
-
installed bundle's
|
|
197
|
+
installed bundle's rules, plugin, scripts, context providers, else the scope dir's `guardrails/`) →
|
|
185
198
|
`stage_files` + `write_manifest` (requires_chi, needs, settings… from the installed bundle's `InstalledManifest`;
|
|
186
199
|
`trust_level` and `description` the CLI's, else that manifest's; a kept manifest's unknown keys carried, each with a
|
|
187
200
|
`unknown_key_line` warning) in a temp dir → `write_out` (zip, tar, tgz, or a new/empty dir). An installed
|
data/docs/internals/engine.md
CHANGED
|
@@ -590,7 +590,7 @@ cache is dropped at the turn's end, not its start, so one turn doesn't ask twice
|
|
|
590
590
|
|
|
591
591
|
## Hooks
|
|
592
592
|
|
|
593
|
-
Plugins and hook files register hooks on hook points (docs/hooks.md). Every fire puts the hook runtime on the
|
|
593
|
+
Plugins (`chi.on`) and config hook files register hooks on hook points (docs/hooks.md). Every fire puts the hook runtime on the
|
|
594
594
|
event: `event[:hook]` (the label) and five helpers, which call the Engine's `Hooks::Runtime` (built by `HookRoutes`,
|
|
595
595
|
`hook_routes.rb`). A plugin's `ctx` has the same ones. A question takes keywords only at that plugin-facing edge;
|
|
596
596
|
from there it is one `HookQuestion` value through `Engine#hook_ask_user` and `HookRoutes#ask_user` to
|
|
@@ -639,10 +639,12 @@ re-validates them ([architecture.md](../architecture.md), the `display_refs` par
|
|
|
639
639
|
`clear_all` leaves them.
|
|
640
640
|
- **Turn-scoped hooks** (`Engine#register_hook`) are cleared by `clear_hooks` in `release_turn`.
|
|
641
641
|
- Fire order: bundle hooks (by priority, bundle, hook name), then config hooks, then turn-scoped hooks.
|
|
642
|
-
- **A hook that raises is logged; it never breaks the turn.** A plugin's `chi.on` handler
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
642
|
+
- **A hook that raises is logged; it never breaks the turn.** A plugin's `chi.on` handler also tells the user once
|
|
643
|
+
per label (`Hooks.wrap(on_failure:)` → `HookRoutes#plugin_hook_failed`: a `:guardrail_warning` labelled
|
|
644
|
+
`plugins`, through `Plugin::Registries#hook_failed`). A config hook has none, nor the test kit's sandbox.
|
|
645
|
+
- `ExtensionLoad#hooks` loads config.yml's hooks only; an installed record that still has class-file bundle hooks
|
|
646
|
+
(an older chi's install) adds a not-required load failure instead, announced at the first turn
|
|
647
|
+
([hooks.md](../hooks.md#class-file-hooks-are-retired)).
|
|
646
648
|
- A notice goes to the running turn's sink and the observers; outside a turn it is announced with
|
|
647
649
|
`between_turns: true`; while the plugins load it is held. `EventRoutes#route` decides it, the same routes as a card's
|
|
648
650
|
(`Engine#show_card`); a guardrails line after load takes only the turn / between-turns routes (`#route_by_turn`).
|
data/docs/memory.md
CHANGED
|
@@ -3,12 +3,41 @@
|
|
|
3
3
|
Samagotchi stores memories in two scopes:
|
|
4
4
|
|
|
5
5
|
- Project scope: `$XDG_CONFIG_HOME/samagotchi/memories/projects/<name>_<hash>/`, one folder per
|
|
6
|
-
|
|
7
|
-
subdirectories
|
|
6
|
+
project (named and hashed by its root): a git repository, shared by all its worktrees and
|
|
7
|
+
subdirectories, or a folder marked by `chi project init` and its subfolders; outside both, one
|
|
8
|
+
per working directory
|
|
8
9
|
- System scope: `$XDG_CONFIG_HOME/samagotchi/memories`
|
|
9
10
|
|
|
10
11
|
`$XDG_CONFIG_HOME` falls back to `~/.config` when unset.
|
|
11
12
|
|
|
13
|
+
## Projects without git
|
|
14
|
+
|
|
15
|
+
A folder in no git repository becomes a project with `chi project init [DIR]` (see
|
|
16
|
+
[cli.md](cli.md#commands)): it writes `DIR/.chi/project.yml` (one comment line; the file, not a bare
|
|
17
|
+
`.chi/` dir, is the marker), and DIR and every subfolder share one project memories folder, group their
|
|
18
|
+
sessions (`chi sessions list`, the web's project picker) and take `chi context --project`. Nothing
|
|
19
|
+
chi-specific has to be committed anywhere: a repository never needs the file. It can also be created
|
|
20
|
+
from chi web's project picker (a folder inside a git repository opens that repository's project, and
|
|
21
|
+
the folder is left as is: it is created when missing, nothing is marked).
|
|
22
|
+
|
|
23
|
+
- **Git wins.** chi looks up a folder's `.git` first; only when there is none does it look for the
|
|
24
|
+
nearest marker above it, so a marker inside a repository is ignored (`chi project` says so) and a
|
|
25
|
+
marked folder inside another is a project of its own, as nested repositories are. A dotfiles repository
|
|
26
|
+
at `~` puts every folder under home in that repository, so markers there never apply.
|
|
27
|
+
- **Not at home.** A marker in `$HOME`, in a folder above it, or in `/` is ignored (`chi project init`
|
|
28
|
+
refuses them), so a marker in `~` never makes every folder under home one project.
|
|
29
|
+
- **The key.** The marked root is the folder's real path (as git's root is), so a folder chi already ran
|
|
30
|
+
in at its top keeps its memories after `init`, and a later `git init` in the same folder keeps the same
|
|
31
|
+
key. Memories saved from a *subfolder* before `init` stay in that subfolder's own folder: move them by
|
|
32
|
+
hand if you want them in the project's.
|
|
33
|
+
- **Restart what runs there.** A session running in the folder when it is marked keeps the old memories
|
|
34
|
+
folder and AGENTS.md chain in its prompt until `chi sessions restart ID` (init names them); a running
|
|
35
|
+
`chi web` groups the folder's older sessions under the project after its restart.
|
|
36
|
+
- **Git-only features stay off in a marked project**: guardrails' outside-the-repo rule, the participant
|
|
37
|
+
boundaries, a plugin's `ctx.repo_root` (and the bundles using it: nested AGENTS.md, slow-commands,
|
|
38
|
+
known-names), `chi home doctor`'s project checks and delegate's work-tree check measure from a git
|
|
39
|
+
checkout only.
|
|
40
|
+
|
|
12
41
|
Tool behavior:
|
|
13
42
|
|
|
14
43
|
- `memory_read`: `scope` is optional.
|
|
@@ -28,7 +57,7 @@ memories copied to a machine (or a path) where the repository lives elsewhere si
|
|
|
28
57
|
longer reads. Rename the folder to the new key, or move memories with a bundle zip (see
|
|
29
58
|
[What a bundle owns](#what-a-bundle-owns)).
|
|
30
59
|
|
|
31
|
-
Inside a git repository, when this project's folder is missing or holds no `*.md` while the projects
|
|
60
|
+
Inside a project (a git repository or a marked folder), when this project's folder is missing or holds no `*.md` while the projects
|
|
32
61
|
dir has a folder of the same basename with another hash, chi says so, naming both folders and the
|
|
33
62
|
`mv` that fixes it (a name match can't prove it's the same project, so nothing is renamed for you):
|
|
34
63
|
|
|
@@ -430,14 +459,14 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
430
459
|
repo can have a `README.md` without it becoming a memory. A listed file is
|
|
431
460
|
installed whatever its name (`chi bundle build` of your own `security.md`
|
|
432
461
|
lists it). Since chi 0.75.0. Older chi installs every top-level `*.md`
|
|
433
|
-
whatever `requires_chi` says (it gates
|
|
462
|
+
whatever `requires_chi` says (it gates rules and the plugin, not
|
|
434
463
|
memories), so a bundle for older chi too names its readme
|
|
435
464
|
`README.markdown` (or puts docs in a subdirectory).
|
|
436
465
|
`chi bundle upgrade` moves a README an older install made a memory to the
|
|
437
466
|
trash (unless you edited it, like any file a bundle stops shipping).
|
|
438
467
|
- **A symlink is never installed.** A memory that is a symlink is skipped
|
|
439
|
-
with `notes.md is a symlink (not installed)`, and so is a
|
|
440
|
-
|
|
468
|
+
with `notes.md is a symlink (not installed)`, and so is a guardrail rule
|
|
469
|
+
that is one or lives in a linked dir (`guardrails/x.yml is a symlink (not
|
|
441
470
|
installed)`); a plugin or script that is one fails the install. A tar, zip
|
|
442
471
|
or git repo keeps links, and copying through one would install whatever
|
|
443
472
|
file it points at (`~/.ssh/…`, an API key) as a memory the model reads.
|
|
@@ -497,7 +526,7 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
497
526
|
does an upgrade's removal of a file the new version no longer ships. A
|
|
498
527
|
file you edited still needs `--force`: chi tells by comparing it with what
|
|
499
528
|
the install recorded (the record's checksum, else its base copy; a file with
|
|
500
|
-
neither counts as edited). A
|
|
529
|
+
neither counts as edited). A rule, plugin or script (or an older install's hook) you
|
|
501
530
|
edited isn't asked about: it is copied to the same dir (`Copied to the trash
|
|
502
531
|
first: scripts/x (…)`) and the bundle's own copy goes. `chi bundle trash` lists the trash
|
|
503
532
|
(oldest first, with file count, size, and age); `--empty` deletes all
|
|
@@ -561,7 +590,8 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
561
590
|
- **`chi bundle status`** says per file what is `modified` (the uninstall's
|
|
562
591
|
rule above: edited, or with nothing recorded to tell), `missing` or
|
|
563
592
|
`unreadable`, and that a file stays behind and why. Its `issues=` counts files
|
|
564
|
-
behind, missing or unreadable (and a plugin
|
|
593
|
+
behind, missing or unreadable (and a plugin that won't load, or class-file hooks an older chi installed, which no
|
|
594
|
+
longer load: `Hooks (N): not loaded`), not edits. A
|
|
565
595
|
file an older chi marked, which no retry has looked at, says `may be behind`, and so
|
|
566
596
|
do the counts in `chi bundle list`, `chi self` and `chi update`'s last line.
|
|
567
597
|
`chi bundle status NAME` also has a `Manifest:` line: `[kept]` when chi reads
|
|
@@ -569,7 +599,7 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
569
599
|
what the install recorded, with why (installed by an older chi, the file edited
|
|
570
600
|
or gone) and the command that fixes it (`chi update` keeps it where it can,
|
|
571
601
|
[CLI: Updating](cli.md#updating)).
|
|
572
|
-
- **A dev install** (`chi bundle install --dev PATH`) links the
|
|
602
|
+
- **A dev install** (`chi bundle install --dev PATH`) links the rules,
|
|
573
603
|
plugin and scripts to `PATH` but still copies the memories at install, so
|
|
574
604
|
an edit to one of them in `PATH` isn't seen until the next install.
|
|
575
605
|
- **The rest of the manifest** (`plugin:`, `requires_chi`, `session_settings:`,
|