samagotchi 0.148.0 → 0.150.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 +62 -1
- data/README.md +4 -2
- data/docs/architecture.md +3 -3
- data/docs/bundles.md +38 -1
- data/docs/cli.md +76 -24
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +269 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +10 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +45 -2
- data/docs/system-one.md +17 -1
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -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/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- 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 +178 -20
- 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/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +365 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +2 -0
- data/lib/samagotchi/testing/host.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/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -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/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +2 -2
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +30 -9
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- 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/stream_handlers.js +6 -1
- 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 +12 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +22 -1
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,19 @@ 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 |
|
|
42
51
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
43
52
|
| `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
|
|
53
|
+
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` and `Remerge` at an equal version (`ShippedUpdate::Row`'s `:would_merge`, `:merged`; `SystemBundle::Result`'s `:merged`). `SystemBundle` also puts back a memory missing at an equal version, that file alone, with no Installer (`verify_files_present`, `Provenance#record_restored`). `ensure!` echoes a merge in one line, in the row's words. That reaches a terminal launch; a worker has only its log (`Log.configure(stderr: false)`), so every merge leaves a note on the file's record (`Installer#note_merges`, a failed run's too, and `Remerge#note_merges`; `Provenance#note_merged`: version, time, trash dir). `chi bundle status` shows it, and `chi update` reads the notes before the run (`SystemBundle.merge_notes`, `ShippedUpdate::Row#earlier`), names them in the row and clears after it those exact ones and the notes of the files that run merged (a worker's meanwhile, on another file, stays), unless it was a dry run, the row failed or has no place for them (a skipped system row), or the run is unattended (`ChildEnv::UNATTENDED_UPDATE`, which `Web::UpdateOps` sets for `update.auto`, an env marker so a hand-over's exec keeps it and an older chi ignores it; `--json` alone says nothing, as a person or script may use it). `chi bundle upgrade` clears the notes of what it printed, and `--force` those of the files it replaced. `--json`'s `merged` is this run's, `earlier` the notes'. `ensure!` says what an upgrade left behind and nothing of a file already behind, which `Result#behind` still has for the row. A plan or sync that can't read a file is that bundle's failed row, in `Trash.failure_message`'s words (`ShippedUpdate.plan_row`, `SystemBundle.sync`). `ShippedUpdate.external` is the rest: what `chi update` only checks |
|
|
45
54
|
| `InstalledCopy`, `BundleCommand::InstalledCheck` | `chi update`'s re-check of the bundles chi didn't ship ([docs/cli.md](../cli.md#updating)). `InstalledCopy` lays a record and its installed files out as a source folder (manifest.yml from `Manifest.write`, the files' own shas, memories from `bases/`); `InstalledCheck` forks per bundle (a plugin's `exit` or hang is that bundle's row; stdio to /dev/null, so `--json` stays parseable; `TIMEOUT`, then KILL of its group) and runs `Check.new(dir, installed: true)` there, which leaves out what only suits a source tree. Its `boundary` finding is narrower there than in a source tree: `Testing::Boundary.gone?` calls a reference gone only when its constant doesn't resolve or the object has no method the name names, so a bundle reaching `Samagotchi::Log` gets no row. lib loads lazily, so when a name doesn't resolve `gone?` requires every file of this chi's library in that same child and asks again (about 0.2 s, only for a bundle that reaches into chi at all); anything it can't read — a `const_get`, a require of a file that is there — is taken as still there. The fork is the point: `Sandbox` swaps ENV and the plugin's code is arbitrary. `UpdateCommand#external_rows` makes the rows (`level`, never `failed`) and holds the check back while a newer gem is yet to take over (`@not_checked`), since a fork is this chi's code |
|
|
46
|
-
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files
|
|
55
|
+
| `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
56
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
|
48
57
|
| `EntrySize` | one memory's size in tokens: `memory.entry_warn_tokens`' crossing note after a memory write, `chi self`'s `big memories` row |
|
|
49
58
|
| `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 +95,44 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
86
95
|
`#source_change` (the summary's first line, `Source changes: old → new`). A source `SourceIdentity` can't place
|
|
87
96
|
(an older record's relative path) never refuses.
|
|
88
97
|
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`)
|
|
98
|
+
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
|
|
99
|
+
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
|
|
100
|
+
differs copied to the trash first (`Trash#copy`, said by `#trash_copy_line`; a profile's in `InstallResult#trash_copies`). A
|
|
91
101
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
92
102
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
|
93
103
|
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
|
|
104
|
+
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
105
|
file that is a symlink or resolves outside the bundle dir (`AssetInstaller#linked?`, a linked `hooks/` too) is
|
|
96
106
|
skipped with a warning (a hook or rule) or raises `InstallError` (the plugin, a script).
|
|
97
107
|
5. An upgrade's `prune_dropped_files`: unchanged → trash; edited → kept (`kept_pruned`); owned by another bundle → kept.
|
|
98
108
|
6. `verify_memory_checksums` (strict), `warn_missing_needs`.
|
|
99
|
-
7. `write_provenance` (manifest, not a dry run): bases come from
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
7. `write_provenance` (manifest, not a dry run): an upgrade's bases come from each file's `Merger::Outcome`
|
|
110
|
+
(`Installer#next_base`): the file on disk, which has the bundle's bytes; the incoming file for a merge, since the merged
|
|
111
|
+
file holds the user's edit and taking it would make current == base, so the next upgrade would fast-forward over the
|
|
112
|
+
edit (the edited signal stays current != base, which `chi bundle status` and uninstall's refusal read); and where the user's edit was
|
|
113
|
+
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
|
|
114
|
+
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
|
|
115
|
+
entry and its recorded checksum): taking the user's file would let the next upgrade fast-forward over it. A plain
|
|
116
|
+
install carries the `conflict` mark of a file it skipped (`held_conflict_marks`), a newer source moving `version`
|
|
117
|
+
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
118
|
owned and this version doesn't ship, while it is on disk (`dropped_owned_files`), so the next upgrade prunes it.
|
|
103
119
|
8. `detect_placeholders`.
|
|
104
120
|
|
|
121
|
+
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
|
|
122
|
+
`InstallError` in `Trash.failure_message`'s words, as it is an `UninstallError` in `Uninstaller#run`. Each save comes before
|
|
123
|
+
its removal, so no edit is lost, and the record isn't written, so a retry is the same upgrade. Not atomic: files the
|
|
124
|
+
failed run already placed stay.
|
|
125
|
+
|
|
126
|
+
Which files a plan asks `Merger::Upgrade` about is the caller's, and the lists differ, each less the files the bundle
|
|
127
|
+
doesn't own: the Installer's `MemoryFiles.select`; `ShippedUpdate.forecast`' every shipped `*.md`, so `chi update`'s plan
|
|
128
|
+
can name a file dropped from `files:` or now a symlink, which the upgrade doesn't merge: edited, it stays (`kept_pruned`, or `kept_shared` when another bundle has it too); `SystemBundle`'s dry
|
|
129
|
+
run the manifest's `files:`, so a manifest with none plans nothing (the shipped one lists its files).
|
|
130
|
+
|
|
105
131
|
Results are `@results` (file → `{status:, reason:}`), summarised by `#summary`. An upgrade with conflicts keeps an
|
|
106
132
|
owned source dir alive (`#source_dir`) until `#cleanup_source!`, because the conflict agent reads the incoming
|
|
107
|
-
files; `
|
|
133
|
+
files; `BundleCommand#settle_conflicts` then runs `Provenance#resolve_conflicts`, which moves a base forward and keeps
|
|
134
|
+
the entry's description, only for a file the agent step changed that holds no markers (`Merger.markers?`), and whose incoming
|
|
135
|
+
copy is still there. Any other keeps its old base and mark, and the command exits `CONFLICTS_KEPT`.
|
|
108
136
|
|
|
109
137
|
## Builder#run, in order
|
|
110
138
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Session objectives
|
|
2
|
+
|
|
3
|
+
A session's objectives are a short list the user types: what this session is to achieve. The first version is a
|
|
4
|
+
try-out, so each piece can come out alone. There is no new message kind or store: the list is a note in the
|
|
5
|
+
conversation and the command writes a note file. Terminals show the default note bubble; the web draws it
|
|
6
|
+
([The page](#the-page)). For the user's side, see
|
|
7
|
+
[sessions.md](../sessions.md#objectives) and [cli.md](../cli.md).
|
|
8
|
+
|
|
9
|
+
## The object and the entry
|
|
10
|
+
|
|
11
|
+
`Objectives::List` holds `Objectives::Item`s (`id`, `text`, `status` active|done|dropped, optional `done_when`) and the
|
|
12
|
+
number the next id takes. That number only grows, so an id is never reused, not after a drop or a `set`. `done_when`
|
|
13
|
+
is a hint, never enforced.
|
|
14
|
+
|
|
15
|
+
Each edit is a whole new list in one note entry:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
ContextNote.message(note_id:, text: list.text, source: "objectives", header: ContextNote::OBJECTIVES_HEADER,
|
|
19
|
+
objectives: list.to_payload)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `objectives` is one key in `ContextNote::KEYS`, so everything that copies a note field by field keeps it
|
|
23
|
+
(`ChatLoop#plain`). A note is also what `take_initial_prompt` and `TurnFlow#restore` already handle, which is why
|
|
24
|
+
this isn't a new kind.
|
|
25
|
+
- The payload is `{"next" => 3, "items" => [{"id", "text", "status", "done_when"}]}`, in string keys before a save and
|
|
26
|
+
after one (`List#to_payload`). `Objectives.from_payload` reads either style.
|
|
27
|
+
- It is never sent. The wire gets role and content only, and the frame instructs by itself:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
[OBJECTIVES, set by the user. What the user wants this session to achieve; replaces any earlier list. Keep work on it. Say when something falls outside it. Don't change it yourself.]
|
|
31
|
+
O1 [done] Two coffees, open the cart
|
|
32
|
+
O2 Stops before checkout (done when: no order is placed)
|
|
33
|
+
[END NOTE]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A dropped item is left out of the text, and a list with nothing left says `(no objectives now)` so that it still
|
|
37
|
+
replaces the one before it. The frame has no clock, and there are no system prompt lines about it, so a session with
|
|
38
|
+
no list adds no note and no prompt line. It still carries the tool's bytes ([Moves](#moves)).
|
|
39
|
+
|
|
40
|
+
A note body isn't escaped when it is saved, and a system-role note reaches the model as it stands, so outside text
|
|
41
|
+
could carry a line that looks exactly like this frame. On the wire, `Steer.wire_text` leads such a line with `(quoted) `
|
|
42
|
+
(`Steer::FORGED_OBJECTIVES`). That covers every entry with a kind, on both loops: a context note, a peer's `send_note`,
|
|
43
|
+
`chi note`, a delegate report, a steer. A participant's message gets the wider `FORGED_OPENINGS`. The real note is
|
|
44
|
+
known by its `objectives` payload key, never by its text. The saved content stays raw, the rewrite is deterministic,
|
|
45
|
+
and text with no such line goes out byte for byte, so the prompt cache holds. The owner's typed prompt has no kind and
|
|
46
|
+
isn't touched. Tool output isn't covered: it goes out as a tool result, not as a system message.
|
|
47
|
+
|
|
48
|
+
A payload whose `next` isn't an Integer or whose `items` isn't an Array is not a list (`from_payload` answers nil), so
|
|
49
|
+
it can't wipe the fold or restart the ids.
|
|
50
|
+
|
|
51
|
+
## The current list
|
|
52
|
+
|
|
53
|
+
`Objectives.build(conversation, pending:)` is a pure fold over the entries, shaped like `WorkingSet.build`: the newest
|
|
54
|
+
list note wins, and a pending payload wins over that, because a file not yet absorbed is newer than anything in the
|
|
55
|
+
conversation. `Objectives.pending_payload(session_dir)` finds the newest objectives file among the notes;
|
|
56
|
+
`Objectives.current` is the two together.
|
|
57
|
+
|
|
58
|
+
`build` also fills `List#moves`, the latest `Objectives::Move` per id ([Moves](#moves)). Moves are never in the frame or
|
|
59
|
+
the payload, so an edit and a continue carry none: a new link starts with no last move.
|
|
60
|
+
|
|
61
|
+
## Editing
|
|
62
|
+
|
|
63
|
+
`/objectives` (`ObjectivesCommand`, registered `anytime`) reads `Objectives.current`. An edit goes out through
|
|
64
|
+
`NoteDelivery.deliver(objectives:)`, which means:
|
|
65
|
+
|
|
66
|
+
- One edit's read and write are one step (`Objectives.with_lock`, a file lock in the session's folder, across threads
|
|
67
|
+
and processes), and the worker's absorb holds it from its claim to the files' delete, so no edit reads between them.
|
|
68
|
+
Each note holds a whole list: without this, two edits that read the same list lost one.
|
|
69
|
+
- `done` refuses a dropped item, and `undo` puts an item back to active.
|
|
70
|
+
- A part with no text of its own refuses the line. An empty done-when is none, and of two done-when groups the last
|
|
71
|
+
counts. A leading `[done]`, `[dropped]` or `[active]` is stripped, so a text can't pass for a status. Any whitespace
|
|
72
|
+
collapses, Unicode line separators too, so each item stays one line of the frame.
|
|
73
|
+
- It never writes session.json and runs on any thread: it only adds a file to `notes/`.
|
|
74
|
+
- It is refused in a session a chi REPL owns (`--no-shared`), with the reason `chi note` gives
|
|
75
|
+
(`NoteDelivery::REPL_REFUSAL`). A show still answers there.
|
|
76
|
+
- A second edit stacks on an unabsorbed first, since the current list reads the pending file.
|
|
77
|
+
- `WorkerInbound#absorb_notes` adds only the newest list in a batch and deletes the others, so edits made during a
|
|
78
|
+
turn cost one entry. It runs between turns: an idle edit lands at the worker's next pass (the fallback tick, or just
|
|
79
|
+
before the next turn), and a mid-turn edit waits for the turn's end. A long coordinator turn is the known gap.
|
|
80
|
+
|
|
81
|
+
The attached TUI and the web composer send the line like any session command (the registry's listing tells them it
|
|
82
|
+
runs mid-turn too). So does `chi send -m "/objectives …"`. A participant's command is refused as any other is.
|
|
83
|
+
|
|
84
|
+
## Moves
|
|
85
|
+
|
|
86
|
+
The `objectives` tool (`Tools::ObjectivesTool`, schema last in `TOOL_SCHEMAS`) has one action, `move`: `id`, `note`,
|
|
87
|
+
optional `evidence`. A call with no `action` is a move too. It is information only: nothing is written, and the call in
|
|
88
|
+
history is the record. A handler can't append an entry mid-turn safely, and a system entry would split a call from its
|
|
89
|
+
response, so there is no new entry kind. `Objectives.moves_in` reads the calls back with
|
|
90
|
+
`LLMContextStale.paired_runs`, as `WorkingSet` does: both hosts' forms, a `call_tool` of it as it ran. A run whose output
|
|
91
|
+
counts only if its output is the tool's answer that it recorded that id (`ObjectivesTool.recorded?`): a call with no list,
|
|
92
|
+
a refusal, a denial or an interrupt recorded nothing. A rollback that drops the turn drops its moves.
|
|
93
|
+
|
|
94
|
+
Mid-turn, `session.messages` doesn't hold the running turn. `/objectives` adds the loop's own conversation
|
|
95
|
+
(`Engine#live_conversation`, a `dup` read on the command's thread), because `Engine#plugin_messages`' running part (the
|
|
96
|
+
Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows once its result is in.
|
|
97
|
+
|
|
98
|
+
- **The answer** says it recorded a move, that the list is unchanged and that the user edits it with `/objectives`; so
|
|
99
|
+
does the schema's description. Other actions get an `Error:` with the same sentence. A small model told to edit the
|
|
100
|
+
list has nothing to call, and this is the only place it is told so: no stable-prompt line.
|
|
101
|
+
- **No list**: `There are no objectives here…`, not an error, for a delegate (its fold is empty), a session with no list
|
|
102
|
+
and a kernel without a session (`Tools::Peers#objectives` is nil; `Engine::PeerView#objectives` reads the session).
|
|
103
|
+
- **Refused**: an unknown id, a dropped objective, an empty note. A `done` objective takes a move.
|
|
104
|
+
- **Declared**: on `full` (`all_core`) and on the coordinator kit (its bundle adds it); named in the not-loaded line
|
|
105
|
+
elsewhere, and so counted in `find_tools`' chi line, and among the deferred tools on a host that loads tools.
|
|
106
|
+
- **Cost**: one extra generation when the move is the step's only call, none when it comes in a batch with another call.
|
|
107
|
+
- **Guardrails**: no rule names it, so it passes with no ask, in `strict` too. `ReviewBoundary` denies it as anything
|
|
108
|
+
outside its kit. `PairBoundary` denies it as owner-voiced: a move stays in the owner's history and reads as the
|
|
109
|
+
session's own.
|
|
110
|
+
- **The row**: `recording a move` in the TUI's line with `id=`, `note=`, `evidence=`; the web's row is `objectives O2 note · evidence`
|
|
111
|
+
(`ToolActivity#tool_title`). No UI of its own.
|
|
112
|
+
|
|
113
|
+
`serves` on `ask_user_question` waits until moves read well. The model can't edit the list.
|
|
114
|
+
|
|
115
|
+
## Proposals
|
|
116
|
+
|
|
117
|
+
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
|
+
(`Objectives::Proposal#refusal`) and hands it to `Engine#propose_objective`, which keeps it until the turn is over,
|
|
119
|
+
as `#context_choice`'s cards are kept: shown in `run_turn`'s ensure, it is a between-turns card (no `in_turn`), so it
|
|
120
|
+
writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a restart (`CardStore` takes
|
|
121
|
+
them only from a card shown in a turn). The same change twice in a turn is one card. A P3 finding stands: a card action
|
|
122
|
+
can't carry a plain reply, so an action here is a real edit.
|
|
123
|
+
|
|
124
|
+
Apply is `/objectives --card ID done O2`: the proposal's own line (`Proposal#line`) behind a prefix the command reads
|
|
125
|
+
(`ObjectivesCommand::CARD`), so it goes through the one route, with its refusals. On success the card with that id is
|
|
126
|
+
replaced by a one-line notice. Dismiss (`--card ID dismiss`) only folds it. Either is recorded under the lock
|
|
127
|
+
(`objectives_cards.json` in the session's folder), so a repeat, from a stale tab or a second tap, applies nothing: it
|
|
128
|
+
says the proposal was already applied and folds the card again. A reword carries what it was proposed against
|
|
129
|
+
(`--was`, `Objectives.fingerprint`) and is refused when the item has changed since. It keeps the item's done-when unless its text
|
|
130
|
+
brings its own. The page keeps a proposal card's buttons off until the folded card comes or the action fails
|
|
131
|
+
(`cards_place.js actionFailed`). A turn leaves at most `PROPOSALS_PER_TURN` (3) cards; past that the tool says so. A refused edit leaves the card, and its
|
|
132
|
+
`command_ran` shows the refusal. Nothing is recorded as a move (`recorded?` is positive); the call stays in history as
|
|
133
|
+
any call. An add's text can't split on ` | `. The body names the line, for a terminal, which shows no button.
|
|
134
|
+
|
|
135
|
+
## The page
|
|
136
|
+
|
|
137
|
+
`Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
|
|
138
|
+
the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
|
|
139
|
+
`:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
|
|
140
|
+
once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
|
|
141
|
+
with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
|
|
142
|
+
`ParticipantView::DROPPED`.
|
|
143
|
+
|
|
144
|
+
Idle, an edit lands at once: the worker's anytime path wakes the loop (`@waker.wake`, one extra pass for any anytime
|
|
145
|
+
command), which absorbs the file. It no longer waits for the fallback tick.
|
|
146
|
+
|
|
147
|
+
- **The wiring** (`objectives_wiring.js`): the routes the strip and the moves post on, each following a woken worker
|
|
148
|
+
only while the page still shows that session.
|
|
149
|
+
- **The strip** (`objectives_strip.js`, `#objectivesStrip`; pure parts in `objectives_model.js`, CSS in
|
|
150
|
+
`objectives.css`). A factory with its own closure (the list; never `store.current`), seeded by `renderSessionView` and reset
|
|
151
|
+
by `forgetSession`. Its edit list is a `chip_popover.js` shell, so a phone gets the bottom sheet and the focus rules.
|
|
152
|
+
Every edit is a line on the command route with `card: true`: no echo, and `commandView` hides a good `/objectives`
|
|
153
|
+
answer that came with that mark, so a tap leaves no bubble, a refusal does. The strip redraws from the event, never
|
|
154
|
+
optimistically.
|
|
155
|
+
- **The entry** (`objectives_entry.js`, from `turn_html.js noteBubbleTemplate`): `Objectives.entry` puts the note's items
|
|
156
|
+
and, against the list before it, `changes` (`Objectives.changes`, display only) on the history item and on
|
|
157
|
+
`context_added`. A first list is pills; a later one a line, `You marked O1 done`. The raw text stays the tooltip.
|
|
158
|
+
- **Moves** (`objectives_moves.js`): a tool row's id is a chip (`moveRowText`), and a row that recorded a move carries
|
|
159
|
+
`data-move-id`. One observer on `#history` reads those back into a line after each finished `.turn-work`, so a reload,
|
|
160
|
+
a live end and a hand-off all draw it. The newest line's Not yet is `objectivesStrip.notYet`. A decision card's
|
|
161
|
+
`Concerns O2` gets the chip there too.
|
|
162
|
+
- **Not yet** is `POST /turn` with a client id of its own (`<tab>:objectives`), so the stream draws its bubble in every
|
|
163
|
+
tab, this one too: the page's own echo needs the composer. It steers a running turn or starts a turn, as a typed line.
|
|
164
|
+
- **Short version**: the same observer offers `/short` (decision-card) under the newest long answer (`foldable`), only with a list
|
|
165
|
+
and with the command in the session's listing, and not during a turn: `/short` itself refuses then, since a running
|
|
166
|
+
turn's "last answer" is interim text. A request that fails, or a worker that leaves with one still out (the bundle's service
|
|
167
|
+
stops after chi's 3 s wait for a command), ends the card saying so; a worker killed outright leaves its one-line `…` row.
|
|
168
|
+
|
|
169
|
+
## Continue, rollback, fork
|
|
170
|
+
|
|
171
|
+
- **Continue**: `Objectives.carry`, in `SessionContinue`'s `before_spawn`, writes a second note file in the new link,
|
|
172
|
+
after the carry note and any moved context's note, so the list reads as the latest word. It is not in
|
|
173
|
+
`SessionChain.note_text` (capped; the last answer gets what the recap leaves). It reads the previous link fresh,
|
|
174
|
+
with its unabsorbed newest file. A failure is only logged (`continue_objectives_failed`), because it runs inside the
|
|
175
|
+
continue's rollback. A review link's list stays behind, as its last answer does: the next link's participants could
|
|
176
|
+
differ.
|
|
177
|
+
- **Rollback**: a note survives `TurnFlow#restore`, and an unabsorbed edit is on disk, untouched.
|
|
178
|
+
- **Fork, delegate**: a plugin fork copies what it is passed. A delegate starts with an empty fold, so the tool answers
|
|
179
|
+
that there are none.
|
|
180
|
+
|
|
181
|
+
## The long-answer card
|
|
182
|
+
|
|
183
|
+
A bundle, not core: [decision-card](../bundles.md#the-decision-card-bundle), removed by uninstalling. It reads the list with
|
|
184
|
+
`ctx.objectives`, the plugin API over `Objectives.current`; `items` and `text` are the bundle's only view of the object.
|
|
185
|
+
|
|
186
|
+
`:after_turn` reads the answer's length and tail from `event[:answer]`, not from the stored message, which on the native
|
|
187
|
+
loop still holds its thought block, and starts a thread only when both pass, so the turn's end
|
|
188
|
+
is held for no request and the web's answer doesn't wait (no `event[:present]`). The thread reads the list and, with an active
|
|
189
|
+
objective, makes one `ctx.ask_model`. A Transcript drops system entries, so the list goes in `prompt:`. The objective a reply
|
|
190
|
+
names is display only: an id the list doesn't have, or has dropped, is left out.
|
|
191
|
+
|
|
192
|
+
Stale is a new turn: `:before_turn` takes the running `Ask` and cancels its request, so a one-slot server is free for that
|
|
193
|
+
turn, and `show` holds the same mutex, so a card shows before that start or not at all. The hook runs inside its turn, so the
|
|
194
|
+
card waits for `ctx.turn_running?` to go false: shown earlier, it gets `in_turn: true` and lands in that turn's collapsed block.
|
|
195
|
+
The request doesn't wait: it goes ahead of the turn-end warm-up, so on a one-slot server the warm-up queues behind it and is the
|
|
196
|
+
last thing in the slot, and the next turn keeps its prefilled prompt. A shown card is replaced by a one-line one with
|
|
197
|
+
the same id only at a turn of the owner's (a prompt, no author): a wake has no prompt, and a child's report isn't an answer. A
|
|
198
|
+
worker that restarts forgets a shown card, which then stays as it is: it has no buttons.
|
|
199
|
+
|
|
200
|
+
A card with no actions writes no `pending_card.json`, so it never reaches `Web::Push::Attention`.
|
|
201
|
+
|
|
202
|
+
## Check this
|
|
203
|
+
|
|
204
|
+
`/objectives check O2` and the list's **Check this** (an objective with a move, the one route) run `ObjectivesCheck.run` on the
|
|
205
|
+
anytime command's own thread: one side request (`Engine#ask_objective_check`, on `ask_side_model`) reads the objective's latest
|
|
206
|
+
move against the session's record, and the answer is a verdict with reasons. Information only: no status changes, it is
|
|
207
|
+
never an approval, no coordinator path reads it, and nothing enters the conversation or the next request.
|
|
208
|
+
|
|
209
|
+
**Why not a `VerifyFresh` child.** The plan said "VerifyFresh-like, fit unchecked". It fits poorly:
|
|
210
|
+
|
|
211
|
+
- `VerifyFresh.start` starts from a saved unclear verdict and its `VerifyAnchor`, a quote of at most 200 characters that
|
|
212
|
+
`verify_anchor_source_session` holds against the parent's text. A move has neither, and a faked one would land in `session.verdicts`.
|
|
213
|
+
- A child is an agent on the session's model with the owner's tools: an unknown number of requests, approvals waiting
|
|
214
|
+
on the child, a place in `session.max_children` and in the session lists, and its spend outside the parent's line.
|
|
215
|
+
Keeping it read-only and out of the delegate paths (`ChildRing`, `ChildrenStatus`) needs a new session field.
|
|
216
|
+
|
|
217
|
+
The side request costs exactly one request, shows in the session's side line (kind `objectives`) and comes out with one
|
|
218
|
+
file. What it gives up: **it can't open a file or run anything.** It reads what the session saw, so a claim that nothing in
|
|
219
|
+
the record settles is `unclear`, and a file changed since isn't seen. If that turns out too weak, a read-only child is the
|
|
220
|
+
next step, and the file and view below can stay.
|
|
221
|
+
|
|
222
|
+
**Independence.** The request (`ObjectivesCheck.request`) has its own system text and no tools. Its record is what people and
|
|
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
|
|
230
|
+
(`Objectives.outside_note?`, the rule the audit shares), or the objectives tool's calls, since a move isn't evidence for itself.
|
|
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.
|
|
232
|
+
|
|
233
|
+
**Its frame.** The claim and the record sit between `<record-RANDOM>` tags made per request, so tool output printing a closing tag
|
|
234
|
+
stays inside; the request says that what's inside is data and its instructions aren't to be followed. No model is needed for
|
|
235
|
+
the frame to hold; a model may still be swayed by text inside it, which is why a verdict is information only.
|
|
236
|
+
|
|
237
|
+
**Its host.** The model is the session's, or `objectives.check_model`, whose host may be another one. The whole user text goes through
|
|
238
|
+
`Web::ParticipantView::Redact.text`, chi's participant redaction: known key shapes and `token=`-style values become `[redacted]`, and
|
|
239
|
+
the home folder `~`. That is by pattern, so a secret of another shape still goes.
|
|
240
|
+
|
|
241
|
+
**State.** `objective_checks.jsonl` beside the session: a `running` line, then one with `verdict` (confirmed, refuted, unclear,
|
|
242
|
+
`no_answer`), `reason`, `text`, `model` and `mark`: a digest of the item's line as sent and the move itself (`Move#ref`, its run's
|
|
243
|
+
id and its model entry's time, with its words). The start is under `Objectives.with_lock`, so a double tap or a second tab answers "already
|
|
244
|
+
being checked" and asks nothing. A running line stops counting once its worker's pid is gone, so a kill, a shutdown or a restart frees
|
|
245
|
+
the next check at once, and past `RUNNING_SECONDS` in any case. `Objectives.view` puts the last check on its item (`check`), only while `mark`
|
|
246
|
+
matches: a reword, a status change or a newer move, in the same words too, offers the check again. A running check carries `until`, because no event says
|
|
247
|
+
its worker died: the strip lets the row go at that time (`armExpiry`), and a reconnect's session read clears it sooner. `announce_objectives` carries the start and the end. A continue doesn't carry it.
|
|
248
|
+
|
|
249
|
+
**Shown as** a card with no actions (`source: objectives`, on the command's thread, so `anytime` and not in a turn's block, and it
|
|
250
|
+
writes no `pending_card.json`, so no push), the list's row, and `check:` under the move in `/objectives`. A model that fails is
|
|
251
|
+
`no_answer`: logged (`objectives_check_failed`), an info card saying so, and the next tap may try again.
|
|
252
|
+
|
|
253
|
+
**Limits.** Refused during a participant's turn, whose text stays out of a side request; one ending during their turn is still the owner's (`Engine#side_author`). A participant can't reach it: the
|
|
254
|
+
command route is the owner's, and `card` and `objectives_updated` are in `ParticipantView::DROPPED`. A move made in the running
|
|
255
|
+
turn isn't on the page until the turn's end (`announce_objectives`), so its tap waits for that; the typed line works at once.
|
|
256
|
+
|
|
257
|
+
## System 1 in audit
|
|
258
|
+
|
|
259
|
+
`ObjectivesAudit`, off unless `objectives.system_one: audit`. See [system-one.md](../system-one.md#objectives-in-audit).
|
|
260
|
+
|
|
261
|
+
## Not in this version
|
|
262
|
+
|
|
263
|
+
Stable-prompt lines aren't here, and terminals have no picture: they keep `/objectives`.
|
|
264
|
+
|
|
265
|
+
Taking it out: old notes stay readable framed bubbles and keep being sent, and the key is ignored. Old `objectives` calls
|
|
266
|
+
stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
|
|
267
|
+
page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
|
|
268
|
+
falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
|
|
269
|
+
the `check` key; the audit is `objectives_audit.rb` and three calls in `Engine`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
|
|
@@ -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)
|
|
26
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `block_open.js` and the `objectives_*.js` modules are new modules for that reason.
|
|
27
27
|
|
|
28
28
|
## Two pages, two models
|
|
29
29
|
|
|
@@ -54,6 +54,9 @@ change.
|
|
|
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
|
|
|
57
|
+
The objectives' strip keeps the list in its own closure (`objectives_strip.js`, seeded by `renderSessionView`, reset by
|
|
58
|
+
`forgetSession`, updated by `objectives_updated`): [objectives.md](objectives.md#the-page).
|
|
59
|
+
|
|
57
60
|
The start page's own choices (the model, the ⚙ panel, the 🧰 list) live in `start_page.js`'s closure, never in
|
|
58
61
|
`store.current`. A fresh chain link's picker reads both.
|
|
59
62
|
|
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,12 +511,48 @@ 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.
|
data/docs/plugins.md
CHANGED
|
@@ -850,10 +850,12 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
850
850
|
| `ctx.messages` | the conversation, as a frozen copy, without the system prompt. While a turn runs, a session worker's (attached, web) adds that turn so far: its prompt, the model's text and the lines merged into it (no tool calls or thinking); the REPL's is the conversation before that turn. An entry may carry `at`, when it happened (docs/sessions.md, Message times) |
|
|
851
851
|
| `ctx.messages_partial?` | whether `ctx.messages` leaves out a running turn (the REPL mid-turn), so a plugin can say what its answer is about |
|
|
852
852
|
| `ctx.prompt_files` | the files whose text is in the session's system prompt now: the AGENTS.md chain (the work tree top down to the cwd) and the appended files (`append_system_prompt:`, `--append-system-prompt`); absolute paths, in prompt order, `[]` before the first build |
|
|
853
|
+
| `ctx.objectives` | the session's [objectives](internals/objectives.md), read now, as a frozen value: `empty?`, `items` (each `id`, `text`, `status` `"active"`, `"done"` or `"dropped"`, `done_when`), `find(id)`, `text` (the lines the model is shown) and `moves` (the latest move per id; mid-turn, a worker's leaves the running turn's out). An edit not yet absorbed counts. Empty when the session has none. Use its methods, not its class. Needs chi newer than 0.148.0 |
|
|
853
854
|
| `ctx.notify(text, level: :info, fallback_for: nil)` | one line to the user, labelled by the bundle (`my-bundle> …`). Every UI shows it, during a turn (a tool, a hook) or between turns (a command). `fallback_for: :display` marks a line that repeats the answer's display (`event[:present]`), which a UI that renders the display leaves out (`fallback_for: :editor`: a file the viewer could open from a ref in its editor): see [Hooks](hooks.md#what-a-hook-can-do-the-runtime). The keyword needs chi 0.35.0 (it raises on an older chi) |
|
|
854
855
|
| `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
|
|
855
856
|
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false, effects: nil)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options or effects). `effects:` tags the options a parent agent's answer is held to (label => `:spend`, `:permit`, `:permit_wide` or `:deny`; untagged = `:answer`): tag every option that spends or permits, and never let text alone or a dismissal do either ([hooks.md](hooks.md#what-a-hook-can-do-the-runtime)). It needs a chi newer than 0.85.0 |
|
|
856
857
|
| `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
|
|
858
|
+
| `ctx.turn_running?` | whether a turn runs now. An `:after_turn` hook runs inside its turn, so a card a thread shows before the turn is over belongs to that turn's block, which the web collapses: wait for this to go false. Needs chi newer than 0.148.0 |
|
|
857
859
|
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. The model reads it with a header naming your bundle (`[Steer from the my-bundle plugin, mid-task. …]`); the session keeps the raw text. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
858
860
|
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
859
861
|
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
@@ -940,6 +942,10 @@ worker gone), then moves out of the block, after the turn's end line, so it
|
|
|
940
942
|
stays in sight; a reload puts it in the same place. An `:info` card of a
|
|
941
943
|
completed turn stays in its step.
|
|
942
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.
|
|
948
|
+
|
|
943
949
|
An [anytime command](#anytime-true)'s cards show as it shows them, after its
|
|
944
950
|
line, in every UI, whether a turn runs or not.
|
|
945
951
|
|
|
@@ -1377,8 +1383,10 @@ that calls it, or for a real external bundle that does. No method on the chance
|
|
|
1377
1383
|
`Samagotchi::Tools::Execute.timeout_seconds`.
|
|
1378
1384
|
5. `Samagotchi::Tools::TaskRuntime::TASKS_DIR` and `EXIT_CODE_FILENAME`.
|
|
1379
1385
|
6. `Samagotchi::AtomicFile.write`, `Samagotchi::Log.warn` /
|
|
1380
|
-
`Samagotchi::Log.debug`, `Samagotchi::Config.get
|
|
1381
|
-
`Samagotchi::VERSION
|
|
1386
|
+
`Samagotchi::Log.debug`, `Samagotchi::Config.get`,
|
|
1387
|
+
`Samagotchi::VERSION`, `Samagotchi::CancellationController` (`new`, `cancel!`: what `cancel:` takes) and
|
|
1388
|
+
`Samagotchi::GitEnv::UNSET` (a spawn env: a bundle's own git, or gh, asks about the folder it names, not the repo
|
|
1389
|
+
a hook's or `rebase --exec`'s environment names).
|
|
1382
1390
|
|
|
1383
1391
|
`Samagotchi::Testing::Boundary::ALLOWED` is the checked copy of this list,
|
|
1384
1392
|
and `Boundary::EXCEPTIONS` the per-bundle list of what each shipped bundle
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|