samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
data/docs/internals/bridge.md
CHANGED
|
@@ -15,7 +15,8 @@ rescues its own errors: an observer must never break the running turn.
|
|
|
15
15
|
|-------|------------------|-------|
|
|
16
16
|
| `RingBuffer` | `ring_buffer.rb` | The last `ring_capacity` (256) raw events by `event_seq`, in memory, for a reconnect's replay. The capture observer feeds it. |
|
|
17
17
|
| `TurnAccumulator` | `turn_accumulator.rb` | The turn in progress, folded from events. |
|
|
18
|
-
| `CardStore` | `card_store.rb` | The last cards and notices, saved in `cards.json
|
|
18
|
+
| `CardStore` | `card_store.rb` | The last cards and notices, saved in `cards.json`, each anchored to its turn. |
|
|
19
|
+
| `CardPlaces` | `card_places.rb` | Pure: where `CardStore`'s entries sit in the messages the page draws (`turns_since`). |
|
|
19
20
|
| `PendingCard` | `pending_card.rb` | `pending_card.json` `{id:, bundle:}`: the running turn's open card with actions (check-in's Nudge), written and removed with that card, so chi web's hub, which watches files and not workers, can notify. |
|
|
20
21
|
|
|
21
22
|
Per connection there is an `SSEWriter` (with a `BoundedQueue`); `ClientEvent` and `EventId` are pure helpers. The
|
|
@@ -49,17 +50,60 @@ The cards a plugin or the Engine shows (`Engine#show_card`), hook notices, load
|
|
|
49
50
|
the "asking again" and steer-cut rows, ✂ rows and questions (with how they were answered), for a UI that joins later:
|
|
50
51
|
`snapshot[:cards]` and `GET /tail`'s `cards`.
|
|
51
52
|
|
|
52
|
-
- **
|
|
53
|
-
`
|
|
54
|
-
|
|
53
|
+
- **Anchored to its turn.** The store keeps a **turn log**, every id it saw at `:turn_started`, oldest first. An entry
|
|
54
|
+
holds `turn_id`: the running turn's, else the log's last (nil before any); a load warning, which the Engine announces
|
|
55
|
+
inside the first turn, takes the one before it. `list(messages:)` computes `turns_since` from the messages the page draws
|
|
56
|
+
(`CardPlaces`, the snapshot's own list; a stopped session's file for `.saved`): the turn openings (`Steer.turn_opening?`, as
|
|
57
|
+
the page's `TURN_PROMPT` and the recap count) after the last one whose id is in the log at or before the entry's, else from
|
|
58
|
+
the first one whose id is later in the log, else 0. The log only orders; the messages count. So a turn with no opening (a
|
|
59
|
+
reminder, a Continue, a failed or rolled-back one, a prompt stopped before the model) moves nothing, a wake's note does, and
|
|
60
|
+
an opening with no id (`/continue no`'s reason, Qwen's recovery line) counts as on the page. Nothing orders such an opening
|
|
61
|
+
against an entry, so the store reads the session's messages (its `messages:` lambda) when an entry is shown between turns, and
|
|
62
|
+
at the turn's end for what a running turn showed that isn't its own: `past_idless` is how many of them, right after the opening
|
|
63
|
+
it counts from, the entry already came after, and they don't count for it. A `!rollback` usually only shortens that run, and the count
|
|
64
|
+
is capped at the run still there, so a stale number doesn't eat a later turn. It can also move an entry's counting point back to a
|
|
65
|
+
run the entry never recorded (T1, `/continue no, too slow`, a card, a held T2 stopped, a second card, `!rollback`: the second card,
|
|
66
|
+
anchored to T2, now counts from T1 and reads above the reason and the first card). That rare inversion is accepted. A turn's own rows don't read it: they count such a line, which sits below them. `current` is a card of the running turn. A card that
|
|
67
|
+
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
|
|
68
|
+
between-turns notice. A card with an earlier card's id replaces it in place and keeps its anchor. A `list` with no messages
|
|
69
|
+
(`?cards=1`'s bodies, `chi bundle try`'s trace) has no `turns_since`.
|
|
70
|
+
- **A turn that leaves nothing** is read, not told: an in-turn entry not `current` whose turn has no opening in the messages
|
|
71
|
+
(a `!rollback`, a failed turn with nothing kept, a prompt stopped before the model) lists as a between-turns entry at its
|
|
72
|
+
place, a card without its actions and a hook's notice without its step; its other rows, having no step to sit in, aren't
|
|
73
|
+
listed. Nothing in the entry changes. A kept Continue or reminder turn never has an opening, so its entries are `continued`
|
|
74
|
+
and stay rows of the turn before; a failed one loses the mark at its end. The page draws a failed prompt at the history's
|
|
75
|
+
end, so an entry pushed idle after it lists `after_failed` (the store's saved `failed_turn`) and goes below it, until the
|
|
76
|
+
next turn starts.
|
|
77
|
+
- **A command's card.** `SessionCommands#run` wraps its dispatch in `Engine#running_command`, so a card shown on that thread
|
|
78
|
+
carries `command:` (the event's field, `show_card` adds it). The store keeps the first show's (a replacement keeps it, even
|
|
79
|
+
none) and lists `label: "from /short"`, never `command`. The page draws it after a reload, where the command's line is gone.
|
|
55
80
|
- **Caps.** `CAPACITY` (20) each for notices, ✂ rows (`LLM_CONTEXT_TYPES`) and cards, so one burst cannot push out
|
|
56
81
|
another kind. An open question (an approval too) is never pushed out.
|
|
57
82
|
- **Saved.** With a `path` (`cards.json` in the session's folder) the store rewrites the file after each event in
|
|
58
|
-
`SAVED_ON`, and a later worker's store starts from it, so a turn's cards outlive the worker; `CardStore.saved(dir)`
|
|
59
|
-
what chi web reads for a session no worker runs.
|
|
60
|
-
and an
|
|
61
|
-
`
|
|
62
|
-
|
|
83
|
+
`SAVED_ON`, and a later worker's store starts from it, so a turn's cards outlive the worker; `CardStore.saved(dir, messages:)`
|
|
84
|
+
is what chi web reads for a session no worker runs. The file is `{version: 2, turn_log:, entries:}`. The log is pruned at
|
|
85
|
+
a turn's start and end and at an idle show (a running turn's rows reuse the start's, as finding it walks the messages) to start at the opening the earliest anchor counts from (all of it while an entry has none, or for a store with no
|
|
86
|
+
`messages:`), so a restart reads an entry as the live store did; an old card therefore keeps it whole. `seed` drops an entry
|
|
87
|
+
whose `turn_id`, `base_since` or `past_idless` isn't an id or a non-negative integer. Load warnings
|
|
88
|
+
are the worker's own (each start announces them) and an unanswered question waits for no one: neither is saved or seeded. An
|
|
89
|
+
entry from an earlier worker lists with `earlier: true`; one shown during a turn comes back **without its actions** (their
|
|
90
|
+
turn no longer runs), while a between-turns card keeps its buttons.
|
|
91
|
+
- **Version 1.** A file from before the log (top-level `turns`, each entry its own, maybe `running`) keeps its places, drift
|
|
92
|
+
included: `seed` turns each count into `base_since`, new openings add to it, and such an entry never reads as having left
|
|
93
|
+
nothing (no id to check). The next save writes version 2 with `base_since` kept. `SessionManager.cards_empty?` reads both.
|
|
94
|
+
- **An older chi on a version 2 file.** Its `seed` wants an integer `turns`, finds none, and starts with no entries. A chi web not
|
|
95
|
+
yet restarted only reads: it lists no cards for that session until restarted, and the file stays whole. An older **writer** does
|
|
96
|
+
damage: at its first saved event, `:turn_started`, it writes `{"version":1,"turns":0,"running":true,"entries":[]}` over the file
|
|
97
|
+
(`{"version":1,"turns":1,"entries":[]}` after that first turn), so that session's cards go, and with them the answered approvals
|
|
98
|
+
`chi guardrails replay` reads. A plain upgrade doesn't reach this: a worker already running seeded before and keeps its own version 1
|
|
99
|
+
file, and a spawn runs the newest chi installed (`SessionManager.worker_command`), unless that activation raises LoadError: the boot
|
|
100
|
+
line then falls back to the spawner's or the home's pinned version, which can be older. Otherwise it takes an older process starting
|
|
101
|
+
on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a REPL or `bin/chi`
|
|
102
|
+
from an older gem or checkout on the same state dir. Nothing on this side stops
|
|
103
|
+
it: an older chi can't be changed, and having it read the file would mean writing fake `turns` counters into every file.
|
|
104
|
+
- **A killed turn needs nothing special.** `TurnCheckpoint` keeps a running turn's prompt in the session file, with its id from
|
|
105
|
+
the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A REPL keeps no running turn in the
|
|
106
|
+
session, so a kill takes the prompt along and there's nothing to count.
|
|
63
107
|
|
|
64
108
|
## SSEWriter
|
|
65
109
|
|
data/docs/internals/bundles.md
CHANGED
|
@@ -13,9 +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>/bases/ each file
|
|
16
|
+
<name>/bases/ each file's base for the next upgrade's 3-way merge: as installed, or the
|
|
17
|
+
bundle's own text after a merge
|
|
18
|
+
<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
|
|
17
21
|
<name>/hooks/ guardrails/ plugin/ scripts/
|
|
18
|
-
.trash/<name>-<stamp>/ files an uninstall or upgrade took away
|
|
22
|
+
.trash/<name>-<stamp>/ files an uninstall or upgrade took away, and copies of what an install
|
|
23
|
+
overwrote (Trash), never deleted
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
## Components
|
|
@@ -35,15 +40,20 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
35
40
|
| `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) |
|
|
36
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) |
|
|
37
42
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
38
|
-
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
39
|
-
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
|
40
|
-
| `
|
|
43
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `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) |
|
|
41
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 |
|
|
51
|
+
| `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)) |
|
|
42
52
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
43
53
|
| `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 |
|
|
44
|
-
| `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
|
|
54
|
+
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
45
55
|
| `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
|
|
46
|
-
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files
|
|
56
|
+
| `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`) |
|
|
47
57
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
48
58
|
| `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
|
|
49
59
|
| `IndexSize` | one scope's memory index as the prompt carries it (tokens, lines, bytes): `memory.index_warn_tokens`' crossing note after a memory write, `chi self`'s summary, the prompt's figures (`SystemPrompt`) |
|
|
@@ -86,25 +96,44 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
86
96
|
`#source_change` (the summary's first line, `Source changes: old → new`). A source `SourceIdentity` can't place
|
|
87
97
|
(an older record's relative path) never refuses.
|
|
88
98
|
3. `install_memories`, per memory (`MemoryFiles.select`: a non-empty `files:` list, else every top-level `*.md` but
|
|
89
|
-
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
|
|
90
|
-
classifies (`preview_memory`), a plain install writes new files and skips existing ones (`skip_existing`)
|
|
99
|
+
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns; a symlink is skipped with a warning): an upgrade 3-way merges the files the bundle owns (`merge_memory`: a clean text merge replaces the file through `Merger.write_merged`, and a real conflict, no git or no base leaves it untouched, `note_conflict`, in `#behind`; a fast-forward over an entry this chi marked `conflict`, a file put back by hand, copies it to the trash first: `save_marked_file`), a dry run only
|
|
100
|
+
classifies (`preview_memory`, `would_merge` for a merge, git running on temp copies; both through `Merger::Upgrade`), a plain install writes new files and skips existing ones (`skip_existing`), and `--force` overwrites, a file that
|
|
101
|
+
differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
|
|
91
102
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
92
103
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
93
104
|
4. `AssetInstaller#run`: hooks (manifest-listed, else every `hooks/*.rb`), prune dropped hooks, hook checksums,
|
|
94
|
-
guardrails, plugin, scripts; each set replaces the previous version's. requires_chi warnings for hooks/rules. A
|
|
105
|
+
guardrails, plugin, scripts; each set replaces the previous version's, an edited file copied to the trash first. requires_chi warnings for hooks/rules. A
|
|
95
106
|
file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`, a linked `hooks/` too) is
|
|
96
107
|
skipped with a warning (a hook or rule) or raises `InstallError` (the plugin, a script).
|
|
97
108
|
5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
98
109
|
6. `verify_memory_checksums` (strict), `warn_missing_needs`.
|
|
99
|
-
7. `write_provenance` (manifest, not a dry run): bases come from
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
7. `write_provenance` (manifest, not a dry run): an upgrade's bases come from each file's `Merger::Outcome`
|
|
111
|
+
(`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
|
|
112
|
+
file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
|
|
113
|
+
edit (the edited signal stays current != base, which `chi bundle status` and uninstall's refusal read); and where the user's edit was
|
|
114
|
+
kept (keep, conflict, whose `FileStanding::Behind` goes in `marks:`; a file the bundle dropped, kept_pruned, and a re-install's skipped edit take the same path by their
|
|
115
|
+
status and reason): those keep the old base, so the next upgrade still sees the edit. One with no base stays without one (a nil in `Provenance#write`'s `files:`, which keeps the
|
|
116
|
+
entry and its recorded checksum): taking the user's file would let the next upgrade fast-forward over it. A plain
|
|
117
|
+
install carries the `conflict` mark of a file it skipped (`held_conflict_marks`), a newer source moving `version`
|
|
118
|
+
included; it goes once the file has the incoming bytes. A plain install also keeps the record (the old base) of a file an earlier install
|
|
102
119
|
owned and this version doesn't ship, while it is on disk (`dropped_owned_files`), so the next upgrade prunes it.
|
|
103
120
|
8. `detect_placeholders`.
|
|
104
121
|
|
|
122
|
+
A file system error in steps 3 to 5 (`Installer#replace_files`; a `.trash` it can't write, an edited file it can't read) is an
|
|
123
|
+
`InstallError` in `Trash.failure_message`'s words, as it is an `UninstallError` in `Uninstaller#run`. Each save comes before
|
|
124
|
+
its removal, so no edit is lost, and the record isn't written, so a retry is the same upgrade. Not atomic: files the
|
|
125
|
+
failed run already placed stay.
|
|
126
|
+
|
|
127
|
+
Which files a plan asks `Merger::Upgrade` about is the caller's, and the lists differ, each less the files the bundle
|
|
128
|
+
doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' every shipped `*.md`, so `chi update`'s plan
|
|
129
|
+
can name a file dropped from `files:` or now a symlink, which the upgrade doesn't merge: edited, it stays (`kept_pruned`, or `kept_shared` when another bundle has it too); `SystemBundle`'s dry
|
|
130
|
+
run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
|
|
131
|
+
|
|
105
132
|
Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
|
|
106
133
|
owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
|
|
107
|
-
files; `
|
|
134
|
+
files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
|
|
135
|
+
the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
|
|
136
|
+
copy is still there. Any other keeps its old base and mark, and the command exits `CONFLICTS_KEPT`.
|
|
108
137
|
|
|
109
138
|
## Builder#run, in order
|
|
110
139
|
|
|
@@ -220,8 +220,13 @@ the record settles is `unclear`, and a file changed since isn't seen. If that tu
|
|
|
220
220
|
next step, and the file and view below can stay.
|
|
221
221
|
|
|
222
222
|
**Independence.** The request (`ObjectivesCheck.request`) has its own system text and no tools. Its record is what people and
|
|
223
|
-
notes said
|
|
224
|
-
|
|
223
|
+
notes said, what tools returned, and the agent's visible answers labelled `[agent's own words]` (`.record`, each entry cut at 1,500 characters, the
|
|
224
|
+
newest 24,000 kept). Without those answers a claim about what the agent said or asked could only come back `unclear`. So the request says they settle
|
|
225
|
+
what was said or asked and are never proof of work, which needs a tool's output or a person's word. That rule is words to a model, not a mechanism: a
|
|
226
|
+
probe on Qwen3.8 Flash Next held it 3 of 3 (only the agent's "all pass": `unclear`), and a verdict is information only either way.
|
|
227
|
+
The record never holds the agent's thinking (`own_words`: a chat entry's `thinking` and `anthropic_content` aren't read; a native
|
|
228
|
+
entry's content is the raw response, so its inline thoughts, a thought cut off unclosed, and its call markup come out), the system prompt, the list's
|
|
229
|
+
notes, a continue's carry note, an earlier session's answer that would read as outside news
|
|
225
230
|
(`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
|
|
226
231
|
Note and evidence are capped at 1,000 characters each. Mid-turn it reads the loop's conversation (`Engine#live_conversation`), the running turn's runs too.
|
|
227
232
|
|
|
@@ -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` 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/internals/web-push.md
CHANGED
|
@@ -181,9 +181,9 @@ from saying it twice across restarts). The key's `chi:` prefix keeps sw.js from
|
|
|
181
181
|
|
|
182
182
|
| `kind` | key | title / body |
|
|
183
183
|
|---|---|---|
|
|
184
|
-
| `installed` (`update.restart: never`) | `chi:installed:<X>` | "chi X is installed" / "chi web and its sessions switch over when you restart them." +
|
|
184
|
+
| `installed` (`update.restart: never`) | `chi:installed:<X>` | "chi X is installed" / "chi web and its sessions switch over when you restart them." + the edited files (`FileStanding.counts_sentence`) |
|
|
185
185
|
| `failed` | `chi:update_failed:<X>` | "chi X didn't install" (or "installed with a problem" when the gem row was fine) / the first failed row's status, "Open chi web for details." |
|
|
186
|
-
| `updated` | `chi:updated:<X>` | "chi updated to X" / "chi web and 3 sessions restarted; 1 restarts when idle." +
|
|
186
|
+
| `updated` | `chi:updated:<X>` | "chi updated to X" / "chi web and 3 sessions restarted; 1 restarts when idle." + the edited files |
|
|
187
187
|
| `restart_failed` | `chi:restart_failed:<X>` | "chi X is installed; chi web didn't restart" / why (a failed `--version` preflight, no wrapper), "Restart chi web in its terminal." |
|
|
188
188
|
|
|
189
189
|
`updated` is sent by the **restarted** chi web (its AutoUpdate finds the run at `phase: web`): the exec ends the
|
|
@@ -242,7 +242,10 @@ session's set (load, resync) and when the list opens (a file changed while the s
|
|
|
242
242
|
passes `working_set_updated` to it, and does nothing when the flag is off. A new snapshot redraws:
|
|
243
243
|
- the session bar's chip and the open list;
|
|
244
244
|
- the footprints, in place (`refreshFootprints`, the same node; a `data-ws-sig` of the items' states skips an
|
|
245
|
-
unchanged one)
|
|
245
|
+
unchanged one). A footprint is a `<span>` in its block's summary line, `.turn-work > summary > .tw-sum`, before the
|
|
246
|
+
steps' `.turn-steps`: drawn in the reload's template (`turnHistoryNodes`), prepended at the hand-off
|
|
247
|
+
(`stage_view.js handOff`, while the stage still hides the summary). A click there must not toggle the block:
|
|
248
|
+
`details_motion.js` skips `.ws-foot`, and `footClick` cancels the click;
|
|
246
249
|
- the rail (`onServer`, the stage's `scheduleRefresh`).
|
|
247
250
|
|
|
248
251
|
A state is the item's state now, not when its turn ran. A state change never refolds and never moves a line: the
|
|
@@ -287,7 +290,7 @@ leaves nothing behind. The rail adds a dashed `+ name · next turn` line per pen
|
|
|
287
290
|
(`working_set_rail.js addLabels`, inside the rail's budget), and `read again next turn` on a held line's title.
|
|
288
291
|
|
|
289
292
|
**Looks** (design spec §3, §5): a stale line is faded (`--ws-stale-opacity`: `.55` on the light palette, `.4` on the
|
|
290
|
-
dark one, where a lighter grey reads as plain secondary text; a footprint label `.55` on both
|
|
293
|
+
dark one, where a lighter grey reads as plain secondary text; a footprint label `.55` on both); a forgotten one is struck in `--text-faint`
|
|
291
294
|
at `opacity: .5`. A group shows `✂n` for its forgotten members. It is struck only when all its members are
|
|
292
295
|
forgotten, and faded when all are stale or forgotten (`groupState`). The list's ops text says `stale` or `forgotten`
|
|
293
296
|
in words (colour is never alone) and shows `~1.2k` tokens per row (`tokensText`). A footprint adds `n stale` (plain; a file the turn's
|
data/docs/memory.md
CHANGED
|
@@ -187,7 +187,7 @@ Working habits for this model:
|
|
|
187
187
|
`model_notes_<name>.md` you already have and never owns it, so your note
|
|
188
188
|
is what loads, the two never stack, and upgrades leave it alone.
|
|
189
189
|
`chi bundle install model-notes` says `Skipped model_notes_deepseek.md
|
|
190
|
-
(already exists;
|
|
190
|
+
(already exists; --force takes the bundle's version, your file copied to the trash first)`; through the `dev` profile
|
|
191
191
|
(`chi bundle install dev`, `chi bundle upgrade dev`, `chi update`) it is
|
|
192
192
|
`model-notes: kept your own model_notes_deepseek.md (not the bundle's; to
|
|
193
193
|
take the bundle's, remove yours with chi memory rm model_notes_deepseek
|
|
@@ -197,14 +197,14 @@ Working habits for this model:
|
|
|
197
197
|
(`chi memory rm model_notes_deepseek --scope system`, or `memory_write`
|
|
198
198
|
with `remove: true`, moves it to the trash) and run
|
|
199
199
|
`chi bundle install model-notes` again: the install writes the missing
|
|
200
|
-
file and owns it from then on.
|
|
201
|
-
|
|
200
|
+
file and owns it from then on. `--force` overwrites every file of the
|
|
201
|
+
bundle instead, each one that differs copied to the bundle trash first.
|
|
202
202
|
- **Other notes stack.** A note of another name for the same models
|
|
203
203
|
(`model_notes_my_deepseek`), or a project-scope `model_notes_deepseek`,
|
|
204
204
|
loads beside the bundle's (notes are told apart by their file, not their
|
|
205
205
|
name), and the total may pass the 3,000-character warning.
|
|
206
|
-
- **Your edits to an installed note**
|
|
207
|
-
|
|
206
|
+
- **Your edits to an installed note** survive an upgrade: it merges them
|
|
207
|
+
with the new version, or leaves your file as it is on a real conflict.
|
|
208
208
|
- Without a model note the system prompt is what it was.
|
|
209
209
|
|
|
210
210
|
|
|
@@ -487,7 +487,9 @@ for a file another bundle installed.
|
|
|
487
487
|
and the output names them and the dir:
|
|
488
488
|
`Moved to the trash: notes.md (…/.trash/team-notes-20261003-120000)`. So
|
|
489
489
|
does an upgrade's removal of a file the new version no longer ships. A
|
|
490
|
-
file you edited still needs `--force`.
|
|
490
|
+
file you edited still needs `--force`. A hook, rule, plugin or script you
|
|
491
|
+
edited isn't asked about: it is copied to the same dir (`Copied to the trash
|
|
492
|
+
first: scripts/x (…)`) and the bundle's own copy goes. `chi bundle trash` lists the trash
|
|
491
493
|
(oldest first, with file count, size, and age); `--empty` deletes all
|
|
492
494
|
folders, `--older-than DAYS` keeps only recent ones, and `--dry-run`
|
|
493
495
|
previews. Hooks, rules and the plugin live in the bundle's own dir and
|
|
@@ -509,17 +511,53 @@ for a file another bundle installed.
|
|
|
509
511
|
`.tgz` or `.tar` gives that archive, any other path a directory; default
|
|
510
512
|
`<name>.zip`).
|
|
511
513
|
- **An upgrade that meets your edits.** `chi bundle upgrade` updates a file
|
|
512
|
-
you didn't edit
|
|
514
|
+
you didn't edit. One you edited that the bundle also changed takes a text
|
|
515
|
+
merge (with `git`, which chi doesn't otherwise need): when it comes out clean
|
|
516
|
+
the file holds both, the old one is in the bundle trash, and the output says
|
|
517
|
+
`merged 0.8.0 into your X` (a dry run, `would merge 0.8.0 into your X`). Entries both sides added under a
|
|
518
|
+
`Changelog` heading are kept, yours first. A clean merge is text, not
|
|
519
|
+
meaning: two sentences can still disagree, so read what you care about (`chi
|
|
520
|
+
bundle trash` lists the old copy).
|
|
521
|
+
- **A real conflict** leaves your file untouched, never with markers in it, and
|
|
522
|
+
marks it. So does no `git`, a file with no recorded base, or a file marked by
|
|
523
|
+
an older chi (below). The output says `X stays behind 0.8.0:` with why and what
|
|
524
|
+
it missed, as `chi update`, `chi bundle status` and `chi bundle list` (`1 file
|
|
525
|
+
behind`) do from then on, until the file has the bundle's bytes; the bundle's text
|
|
526
|
+
waits in `.bundles/<name>/incoming/` until it settles. The upgrade then asks on a
|
|
513
527
|
terminal whether to launch a chi session that merges the two (`/exit`
|
|
514
528
|
when done). `--agent` launches it without asking, `--no-agent` never does;
|
|
515
|
-
with neither and no terminal
|
|
529
|
+
with neither and no terminal the file just stays behind. Only a file
|
|
530
|
+
the session changed, with no conflict markers left, counts as resolved: any
|
|
531
|
+
other stays a conflict (`Nothing changed in X: still behind.`). A file marked
|
|
532
|
+
this way is retried at the same version: the system bundle's at each chi start
|
|
533
|
+
and on `chi update`, another shipped bundle's on `chi update`, and any other
|
|
534
|
+
bundle's on `chi bundle upgrade`.
|
|
535
|
+
- **A file marked by an older chi** merges normally when its recorded base is a
|
|
536
|
+
version chi shipped, which chi checks against the samagotchi gems still
|
|
537
|
+
installed. Otherwise that base may be your own text, so the file merges only if
|
|
538
|
+
the bundle takes out or rewords no line it still holds. If it does, chi can't
|
|
539
|
+
tell whether the file holds edits of yours and says that; the file
|
|
540
|
+
stays behind until you resolve it by hand, with `--agent` or with `--force`, and
|
|
541
|
+
the retry can't fix that. A file left behind makes the upgrade
|
|
516
542
|
exit 2; `chi bundle diff NAME FILE` shows the base, and `--force` takes
|
|
517
|
-
the bundle's version
|
|
543
|
+
the bundle's version, copying a file that differs to the bundle trash first
|
|
544
|
+
(the output says `Copied to the trash first: …`).
|
|
545
|
+
- **Every merge is noted on the file's record.** The system bundle's upgrade and
|
|
546
|
+
retry run at each start, a worker's only output is its log, and no one may read
|
|
547
|
+
an automatic update. So `chi bundle status` shows the merge with the old copy's
|
|
548
|
+
folder while the note exists. A `chi update` row or `chi bundle upgrade` output
|
|
549
|
+
printed to you ends it for what it printed (an automatic update leaves it), and
|
|
550
|
+
`--force` replacing the file ends it too. No session is told at start.
|
|
551
|
+
- **`chi bundle status`** says per file what is `modified`, `missing` or
|
|
552
|
+
`unreadable`, and that a file stays behind and why. Its `issues=` counts files
|
|
553
|
+
behind, missing or unreadable (and a plugin or hooks that won't load), not edits. A
|
|
554
|
+
file an older chi marked, which no retry has looked at, says `may be behind`, and so
|
|
555
|
+
do the counts in `chi bundle list`, `chi self` and `chi update`'s last line.
|
|
518
556
|
- **A dev install** (`chi bundle install --dev PATH`) links the hooks, rules,
|
|
519
557
|
plugin and scripts to `PATH` but still copies the memories at install, so
|
|
520
558
|
an edit to one of them in `PATH` isn't seen until the next install.
|
|
521
559
|
- **The rest of the manifest** (`plugin:`, `requires_chi`, `session_settings:`,
|
|
522
|
-
`context_providers:`, `requires_chi_next`) is in
|
|
560
|
+
`context_providers:`, `block_renderers:`, `requires_chi_next`) is in
|
|
523
561
|
[Plugins](plugins.md) and [internals/bundles.md](internals/bundles.md).
|
|
524
562
|
|
|
525
563
|
## Bundle profiles: core and dev
|
data/docs/plugins.md
CHANGED
|
@@ -942,6 +942,12 @@ worker gone), then moves out of the block, after the turn's end line, so it
|
|
|
942
942
|
stays in sight; a reload puts it in the same place. An `:info` card of a
|
|
943
943
|
completed turn stays in its step.
|
|
944
944
|
|
|
945
|
+
Between turns, a card goes above the first turn prompt after it, live and
|
|
946
|
+
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 shown before it, and before those
|
|
948
|
+
shown after. After a reload a card a command showed names it (`from /short`),
|
|
949
|
+
its line being gone.
|
|
950
|
+
|
|
945
951
|
An [anytime command](#anytime-true)'s cards show as it shows them, after its
|
|
946
952
|
line, in every UI, whether a turn runs or not.
|
|
947
953
|
|
|
@@ -957,8 +963,8 @@ then a new turn) starts from them. Load warnings and a question still
|
|
|
957
963
|
waiting when the worker went are not saved.
|
|
958
964
|
|
|
959
965
|
The event is `{type: :card, id:, source:, title:, body:, level:, actions:,
|
|
960
|
-
in_turn:}` (`source` is the bundle),
|
|
961
|
-
title.
|
|
966
|
+
in_turn:}` (`source` is the bundle), plus `command:` when a session command
|
|
967
|
+
showed it (`"/short"`), logged as `card` with its source, id and title.
|
|
962
968
|
|
|
963
969
|
## Side answers: `ctx.ask_model`
|
|
964
970
|
|
|
@@ -1347,6 +1353,41 @@ context_providers:
|
|
|
1347
1353
|
the PATH may be another one, such as macOS's 2.6).
|
|
1348
1354
|
- The command follows [the contract](context.md#the-command-contract).
|
|
1349
1355
|
|
|
1356
|
+
## Block renderers
|
|
1357
|
+
|
|
1358
|
+
A bundle can draw a fenced code block as a picture in chi web's answers: the model writes ```` ```mermaid ````, the
|
|
1359
|
+
page shows the drawing. No tool, no plugin code in the web server: chi web runs no plugins, and a reloaded page must
|
|
1360
|
+
draw after the worker has gone, so JS draws and Ruby only routes and serves.
|
|
1361
|
+
|
|
1362
|
+
```yaml
|
|
1363
|
+
# manifest.yml
|
|
1364
|
+
scripts:
|
|
1365
|
+
draw.js: sha256:… # scripts/draw.js in the bundle
|
|
1366
|
+
block_renderers:
|
|
1367
|
+
- lang: mermaid # a fence's first word, downcased: [a-z0-9_+-], at most 32
|
|
1368
|
+
load: [draw.js] # scripts: keys, loaded into the frame in this order
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
- `load:` names files under `scripts:`, so they install and check as any script does (`chi bundle check`,
|
|
1372
|
+
`rake bundles:sha`), and one changed after the install isn't served until the bundle is reinstalled; a `--dev`
|
|
1373
|
+
install's links aren't checked. A bundle with no memories installs fine, but `chi bundle build` still needs one:
|
|
1374
|
+
it refuses with "No memories to build".
|
|
1375
|
+
- The first installed bundle (by name) with a language draws it. A bundle whose `requires_chi` this chi doesn't meet
|
|
1376
|
+
draws nothing.
|
|
1377
|
+
- **What it needs of the page:** `web.markdown` on (a default-off setting, and the commonmarker gem). With it off a
|
|
1378
|
+
fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
|
|
1379
|
+
the model: say so in the bundle's README.
|
|
1380
|
+
- **Where it draws:** the owner's page only, in answers and closed steps. Participants' pages, cards, question
|
|
1381
|
+
replies, the asking slot, delegate reports and the doc viewer show the source, and so does the terminal.
|
|
1382
|
+
- **The script's side:** the last file calls `chi.defineRenderer({ init, render })`. `init()` (optional, may be async)
|
|
1383
|
+
runs once; `render(source, theme)` returns the SVG as a string, or a promise of one. Throw to keep the block as
|
|
1384
|
+
source. `theme` is `{name, bg, panel, text, muted, border, accent, font}`, from the page's tokens. The script runs in
|
|
1385
|
+
a sandboxed frame with no cookie and no access to the page, and the CSP closes its network, but for WebRTC (what the
|
|
1386
|
+
frame holds, the diagrams it rendered, could leave that way). The SVG is shown as an `<img>`, so its scripts, links
|
|
1387
|
+
and remote images don't run: it has to stand alone (no `foreignObject`, every namespace prefix bound) and stay under 50,000 px a
|
|
1388
|
+
side and 100M px. Over that, or if the image doesn't decode, the block stays source. The
|
|
1389
|
+
layers and the residual risk are in [internals/block-renderers.md](internals/block-renderers.md).
|
|
1390
|
+
|
|
1350
1391
|
## Stable for bundles
|
|
1351
1392
|
|
|
1352
1393
|
Bundles normally use only `chi.*` and `ctx`; the constants and helpers below
|
|
@@ -1380,7 +1421,9 @@ that calls it, or for a real external bundle that does. No method on the chance
|
|
|
1380
1421
|
5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
|
|
1381
1422
|
6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
|
|
1382
1423
|
`Samagotchi::Log.debug`, `Samagotchi::Config.get`,
|
|
1383
|
-
`Samagotchi::VERSION
|
|
1424
|
+
`Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
|
|
1425
|
+
`Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
|
|
1426
|
+
a hook's or `rebase --exec`'s environment names).
|
|
1384
1427
|
|
|
1385
1428
|
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
|
|
1386
1429
|
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
|
@@ -1390,6 +1433,9 @@ fails on a reference in neither (docs/testing.md). In `chi update`'s re-check of
|
|
|
1390
1433
|
an installed bundle the same scan warns only for a name this chi no longer has
|
|
1391
1434
|
(`Boundary.gone?`): a reference that still resolves gets no row.
|
|
1392
1435
|
|
|
1436
|
+
A bundle's `block_renderers:` and the frame's `chi.defineRenderer` ([Block renderers](#block-renderers)) are stable
|
|
1437
|
+
too. They aren't Ruby, so the scan above doesn't see them.
|
|
1438
|
+
|
|
1393
1439
|
**Used by shipped bundles today, but internal** (may change without notice; a
|
|
1394
1440
|
bundle that needs one should ask for an API instead):
|
|
1395
1441
|
|
data/docs/sessions.md
CHANGED
|
@@ -219,8 +219,9 @@ reword is refused when you changed that objective since. The model never changes
|
|
|
219
219
|
|
|
220
220
|
**Check this** (`/objectives check O2`, or the button in the web's list, for an objective with a move) asks for an independent read of
|
|
221
221
|
the latest move: one side request, on your request only, on the session's model or `objectives.check_model`. It sees the
|
|
222
|
-
claim, what people and notes said
|
|
223
|
-
settle
|
|
222
|
+
claim, what people and notes said, what tools returned and the agent's visible answers (never its thinking), and no file. The agent's
|
|
223
|
+
words settle what it said or asked, never that its work was done: "I ran the tests" with no run behind it isn't `confirmed`. What the record doesn't
|
|
224
|
+
settle comes back `unclear`. **That is up to 24,000 characters of raw tool output, messages and the agent's visible answers, which with a hosted `check_model` leaves the machine on one tap, even from a local session.** chi's redaction takes out known key shapes and `token=`-style values first, by pattern only, so another secret in them still goes. The verdict (confirmed, refuted, unclear) and its reasons come as a card and under the
|
|
224
225
|
objective. It is information only: it changes no status, approves nothing, and isn't in the conversation or the model's next request.
|
|
225
226
|
A second tap while one runs asks nothing. Its cost is in the session's side line. Mid-turn it runs beside the turn; on a one-slot
|
|
226
227
|
local server it answers after the running request. A participant can't reach it. See
|
|
@@ -471,8 +472,8 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
|
|
|
471
472
|
included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
|
|
472
473
|
(`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
|
|
473
474
|
`.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
|
|
474
|
-
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, `
|
|
475
|
-
`
|
|
475
|
+
- **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, or what `bundle exec` sets
|
|
476
|
+
(`BUNDLE_GEMFILE`, `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP`: `BundlerEnv`) from the project's mise env: it runs without `RUBYLIB` and Bundler's
|
|
476
477
|
variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
|
|
477
478
|
- **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
|
|
478
479
|
a dir holding only a `chi` script that boots the home (rewritten at each worker start when the home changed; it
|
data/docs/testing.md
CHANGED
|
@@ -40,6 +40,11 @@ that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT
|
|
|
40
40
|
|
|
41
41
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
42
42
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
43
|
+
It also clears what tells git which repo to act on (`GIT_DIR`, `GIT_WORK_TREE`, the rest of
|
|
44
|
+
`git rev-parse --local-env-vars`: `GitEnv`), first thing in `spec_helper.rb` and as the bundle test kit's
|
|
45
|
+
adapters load, so a run from `git rebase --exec`, `bisect run` or a hook can't reach the real repo through a spec's git.
|
|
46
|
+
`npm run e2e` clears the same list as `spec/e2e/support/env.js` loads, asked of git. chi's own git calls on a folder chi
|
|
47
|
+
chose spawn with `GitEnv::UNSET`, in any run.
|
|
43
48
|
`SAMAGOTCHI_TAILSCALE_CLI` points at a file that does not exist (`spec_helper.rb`), so no spec reaches a real `tailscale`; a
|
|
44
49
|
`chi web --tunnel tailscale` spec writes a fake one and sets the variable.
|
|
45
50
|
|
|
@@ -18,6 +18,8 @@ module Samagotchi
|
|
|
18
18
|
# @param content [String] written as-is, byte for byte
|
|
19
19
|
# @param perm [Integer, nil] the new file's mode; nil = a fresh file's
|
|
20
20
|
# (0666 less the umask), like File.write
|
|
21
|
+
# @yield just before the rename, when given: a last check of what is being
|
|
22
|
+
# replaced; raising leaves +path+ as it was
|
|
21
23
|
# @return [String] +path+
|
|
22
24
|
def write(path, content, perm: nil)
|
|
23
25
|
tmp = "#{path}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
|
|
@@ -25,6 +27,7 @@ module Samagotchi
|
|
|
25
27
|
io.write(content)
|
|
26
28
|
end
|
|
27
29
|
File.chmod(perm, tmp) if perm
|
|
30
|
+
yield if block_given?
|
|
28
31
|
File.rename(tmp, path)
|
|
29
32
|
path
|
|
30
33
|
ensure
|