samagotchi 0.157.0 → 0.159.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 +47 -1
- data/README.md +1 -1
- data/bin/chi +5 -1
- data/docs/architecture.md +14 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +2 -1
- data/docs/cli.md +76 -17
- data/docs/configuration.md +16 -5
- data/docs/context.md +12 -4
- data/docs/guardrails.md +11 -8
- data/docs/hooks.md +47 -80
- data/docs/internals/bridge.md +2 -1
- data/docs/internals/bundles.md +51 -36
- data/docs/internals/engine.md +7 -5
- data/docs/memory.md +43 -9
- data/docs/plugins.md +13 -9
- data/docs/releasing.md +6 -5
- data/docs/scheduling.md +228 -0
- data/docs/sessions.md +30 -6
- data/docs/testing.md +4 -4
- data/lib/samagotchi/bridge/card_places.rb +5 -13
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
- data/lib/samagotchi/bridge.rb +26 -2
- data/lib/samagotchi/bundle_command/check.rb +23 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command.rb +53 -44
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/client_id.rb +13 -3
- data/lib/samagotchi/config.rb +12 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_move.rb +74 -0
- data/lib/samagotchi/context_sources.rb +4 -2
- data/lib/samagotchi/engine.rb +1 -6
- data/lib/samagotchi/extension_load.rb +20 -41
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/pair_boundary.rb +1 -1
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/memory_bundle/asset_installer.rb +112 -211
- data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
- data/lib/samagotchi/memory_bundle/builder.rb +10 -34
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +51 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
- data/lib/samagotchi/memory_bundle/installer.rb +32 -14
- data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
- data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
- data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +41 -41
- data/lib/samagotchi/memory_bundle/status.rb +30 -9
- data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
- data/lib/samagotchi/memory_bundle/trash.rb +7 -12
- data/lib/samagotchi/memory_bundle/uninstaller.rb +10 -4
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/plugin/api.rb +1 -4
- data/lib/samagotchi/plugin/loader.rb +42 -5
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/recap_place.rb +3 -4
- data/lib/samagotchi/run_options.rb +2 -0
- data/lib/samagotchi/schedule_command.rb +252 -0
- data/lib/samagotchi/schedules.rb +948 -0
- data/lib/samagotchi/session.rb +44 -18
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +20 -0
- data/lib/samagotchi/session_field.rb +5 -4
- data/lib/samagotchi/session_manager/spawn.rb +25 -11
- data/lib/samagotchi/session_manager/worker_command.rb +13 -13
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
- data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -5
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/tool_declarations.rb +44 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/builtin_calls.rb +2 -0
- data/lib/samagotchi/tools/builtins.rb +17 -0
- data/lib/samagotchi/tools/cancel_schedule.rb +43 -0
- data/lib/samagotchi/tools/delegate.rb +12 -8
- data/lib/samagotchi/tools/list_schedules.rb +43 -0
- data/lib/samagotchi/tools/schedule_task.rb +39 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -0
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/chains.js +32 -8
- data/lib/samagotchi/web/public/index.html +10 -2
- data/lib/samagotchi/web/public/info_bar.js +4 -3
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/turn_events.js +4 -0
- data/lib/samagotchi/web/session_hub.rb +20 -0
- data/lib/samagotchi/worker.rb +24 -6
- data/lib/samagotchi/worker_boot.rb +3 -4
- data/lib/samagotchi/worker_inbound.rb +23 -0
- data/lib/samagotchi/worker_link.rb +5 -0
- data/lib/samagotchi/worker_wakes.rb +9 -1
- metadata +10 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
data/docs/memory.md
CHANGED
|
@@ -3,12 +3,41 @@
|
|
|
3
3
|
Samagotchi stores memories in two scopes:
|
|
4
4
|
|
|
5
5
|
- Project scope: `$XDG_CONFIG_HOME/samagotchi/memories/projects/<name>_<hash>/`, one folder per
|
|
6
|
-
|
|
7
|
-
subdirectories
|
|
6
|
+
project (named and hashed by its root): a git repository, shared by all its worktrees and
|
|
7
|
+
subdirectories, or a folder marked by `chi project init` and its subfolders; outside both, one
|
|
8
|
+
per working directory
|
|
8
9
|
- System scope: `$XDG_CONFIG_HOME/samagotchi/memories`
|
|
9
10
|
|
|
10
11
|
`$XDG_CONFIG_HOME` falls back to `~/.config` when unset.
|
|
11
12
|
|
|
13
|
+
## Projects without git
|
|
14
|
+
|
|
15
|
+
A folder in no git repository becomes a project with `chi project init [DIR]` (see
|
|
16
|
+
[cli.md](cli.md#commands)): it writes `DIR/.chi/project.yml` (one comment line; the file, not a bare
|
|
17
|
+
`.chi/` dir, is the marker), and DIR and every subfolder share one project memories folder, group their
|
|
18
|
+
sessions (`chi sessions list`, the web's project picker) and take `chi context --project`. Nothing
|
|
19
|
+
chi-specific has to be committed anywhere: a repository never needs the file. It can also be created
|
|
20
|
+
from chi web's project picker (a folder inside a git repository opens that repository's project, and
|
|
21
|
+
the folder is left as is: it is created when missing, nothing is marked).
|
|
22
|
+
|
|
23
|
+
- **Git wins.** chi looks up a folder's `.git` first; only when there is none does it look for the
|
|
24
|
+
nearest marker above it, so a marker inside a repository is ignored (`chi project` says so) and a
|
|
25
|
+
marked folder inside another is a project of its own, as nested repositories are. A dotfiles repository
|
|
26
|
+
at `~` puts every folder under home in that repository, so markers there never apply.
|
|
27
|
+
- **Not at home.** A marker in `$HOME`, in a folder above it, or in `/` is ignored (`chi project init`
|
|
28
|
+
refuses them), so a marker in `~` never makes every folder under home one project.
|
|
29
|
+
- **The key.** The marked root is the folder's real path (as git's root is), so a folder chi already ran
|
|
30
|
+
in at its top keeps its memories after `init`, and a later `git init` in the same folder keeps the same
|
|
31
|
+
key. Memories saved from a *subfolder* before `init` stay in that subfolder's own folder: move them by
|
|
32
|
+
hand if you want them in the project's.
|
|
33
|
+
- **Restart what runs there.** A session running in the folder when it is marked keeps the old memories
|
|
34
|
+
folder and AGENTS.md chain in its prompt until `chi sessions restart ID` (init names them); a running
|
|
35
|
+
`chi web` groups the folder's older sessions under the project after its restart.
|
|
36
|
+
- **Git-only features stay off in a marked project**: guardrails' outside-the-repo rule, the participant
|
|
37
|
+
boundaries, a plugin's `ctx.repo_root` (and the bundles using it: nested AGENTS.md, slow-commands,
|
|
38
|
+
known-names), `chi home doctor`'s project checks and delegate's work-tree check measure from a git
|
|
39
|
+
checkout only.
|
|
40
|
+
|
|
12
41
|
Tool behavior:
|
|
13
42
|
|
|
14
43
|
- `memory_read`: `scope` is optional.
|
|
@@ -28,7 +57,7 @@ memories copied to a machine (or a path) where the repository lives elsewhere si
|
|
|
28
57
|
longer reads. Rename the folder to the new key, or move memories with a bundle zip (see
|
|
29
58
|
[What a bundle owns](#what-a-bundle-owns)).
|
|
30
59
|
|
|
31
|
-
Inside a git repository, when this project's folder is missing or holds no `*.md` while the projects
|
|
60
|
+
Inside a project (a git repository or a marked folder), when this project's folder is missing or holds no `*.md` while the projects
|
|
32
61
|
dir has a folder of the same basename with another hash, chi says so, naming both folders and the
|
|
33
62
|
`mv` that fixes it (a name match can't prove it's the same project, so nothing is renamed for you):
|
|
34
63
|
|
|
@@ -430,14 +459,14 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
430
459
|
repo can have a `README.md` without it becoming a memory. A listed file is
|
|
431
460
|
installed whatever its name (`chi bundle build` of your own `security.md`
|
|
432
461
|
lists it). Since chi 0.75.0. Older chi installs every top-level `*.md`
|
|
433
|
-
whatever `requires_chi` says (it gates
|
|
462
|
+
whatever `requires_chi` says (it gates rules and the plugin, not
|
|
434
463
|
memories), so a bundle for older chi too names its readme
|
|
435
464
|
`README.markdown` (or puts docs in a subdirectory).
|
|
436
465
|
`chi bundle upgrade` moves a README an older install made a memory to the
|
|
437
466
|
trash (unless you edited it, like any file a bundle stops shipping).
|
|
438
467
|
- **A symlink is never installed.** A memory that is a symlink is skipped
|
|
439
|
-
with `notes.md is a symlink (not installed)`, and so is a
|
|
440
|
-
|
|
468
|
+
with `notes.md is a symlink (not installed)`, and so is a guardrail rule
|
|
469
|
+
that is one or lives in a linked dir (`guardrails/x.yml is a symlink (not
|
|
441
470
|
installed)`); a plugin or script that is one fails the install. A tar, zip
|
|
442
471
|
or git repo keeps links, and copying through one would install whatever
|
|
443
472
|
file it points at (`~/.ssh/…`, an API key) as a memory the model reads.
|
|
@@ -497,7 +526,7 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
497
526
|
does an upgrade's removal of a file the new version no longer ships. A
|
|
498
527
|
file you edited still needs `--force`: chi tells by comparing it with what
|
|
499
528
|
the install recorded (the record's checksum, else its base copy; a file with
|
|
500
|
-
neither counts as edited). A
|
|
529
|
+
neither counts as edited). A rule, plugin or script (or an older install's hook) you
|
|
501
530
|
edited isn't asked about: it is copied to the same dir (`Copied to the trash
|
|
502
531
|
first: scripts/x (…)`) and the bundle's own copy goes. `chi bundle trash` lists the trash
|
|
503
532
|
(oldest first, with file count, size, and age); `--empty` deletes all
|
|
@@ -561,7 +590,12 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
561
590
|
- **`chi bundle status`** says per file what is `modified` (the uninstall's
|
|
562
591
|
rule above: edited, or with nothing recorded to tell), `missing` or
|
|
563
592
|
`unreadable`, and that a file stays behind and why. Its `issues=` counts files
|
|
564
|
-
behind, missing or unreadable (and a plugin
|
|
593
|
+
behind, missing or unreadable (and a plugin that won't load, or class-file hooks an older chi installed, which no
|
|
594
|
+
longer load: `Hooks (N): not loaded`), not edits. `chi bundle status NAME`
|
|
595
|
+
lists the bundle's guardrail rules and scripts too (`Rules (N):`, `Scripts (N):`),
|
|
596
|
+
each `[ok]`, `[missing]` or `[modified]`: edited, so the next install or upgrade
|
|
597
|
+
replaces it, its copy kept in the trash. An edited or missing rule doesn't load
|
|
598
|
+
and counts in `issues=`, as a missing script does. A
|
|
565
599
|
file an older chi marked, which no retry has looked at, says `may be behind`, and so
|
|
566
600
|
do the counts in `chi bundle list`, `chi self` and `chi update`'s last line.
|
|
567
601
|
`chi bundle status NAME` also has a `Manifest:` line: `[kept]` when chi reads
|
|
@@ -569,7 +603,7 @@ memory still marked as behind an earlier upgrade keeps its mark either way.
|
|
|
569
603
|
what the install recorded, with why (installed by an older chi, the file edited
|
|
570
604
|
or gone) and the command that fixes it (`chi update` keeps it where it can,
|
|
571
605
|
[CLI: Updating](cli.md#updating)).
|
|
572
|
-
- **A dev install** (`chi bundle install --dev PATH`) links the
|
|
606
|
+
- **A dev install** (`chi bundle install --dev PATH`) links the rules,
|
|
573
607
|
plugin and scripts to `PATH` but still copies the memories at install, so
|
|
574
608
|
an edit to one of them in `PATH` isn't seen until the next install.
|
|
575
609
|
- **The rest of the manifest** (`plugin:`, `requires_chi`, `session_settings:`,
|
data/docs/plugins.md
CHANGED
|
@@ -17,7 +17,7 @@ loop, the event shapes, the kit, the pitfalls.
|
|
|
17
17
|
my-bundle/
|
|
18
18
|
manifest.yml
|
|
19
19
|
plugin.rb
|
|
20
|
-
identity.md # optional, like any bundle's memories
|
|
20
|
+
identity.md # optional, like any bundle's memories and guardrails/
|
|
21
21
|
README.md # never a memory (nor CHANGELOG, LICENSE, ...): see memory.md#what-a-bundle-owns
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -30,7 +30,7 @@ plugin:
|
|
|
30
30
|
sha256: sha256:6a1a7022… # shasum -a 256 plugin.rb
|
|
31
31
|
# required: true # optional: a guardrail plugin; if it fails to load, every tool call is
|
|
32
32
|
# denied (see chi.on's on_error: :fail_closed; chi 0.157.0 or newer)
|
|
33
|
-
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's
|
|
33
|
+
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's rules check it too
|
|
34
34
|
# every key below has a chi floor: check warns when requires_chi admits a chi that never read it
|
|
35
35
|
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
36
36
|
# to the chi being released (docs/releasing.md)
|
|
@@ -473,7 +473,8 @@ end
|
|
|
473
473
|
|
|
474
474
|
### `chi.on(event, priority: 100, on_error: :log) { |event, ctx| … }`
|
|
475
475
|
|
|
476
|
-
This is a bundle hook
|
|
476
|
+
This is a bundle hook: the one way a bundle hooks an event (class-file
|
|
477
|
+
`hooks/*.rb` hooks are retired, [hooks.md](hooks.md#class-file-hooks-are-retired)). See
|
|
477
478
|
[hooks.md](hooks.md#hook-events) for the events; on `:after_turn`,
|
|
478
479
|
`event[:present]` sets how the answer is shown in the web
|
|
479
480
|
([Presenting the answer](hooks.md#presenting-the-answer-display-only)) and `event[:refs]`
|
|
@@ -1546,7 +1547,7 @@ bundle.shutdown
|
|
|
1546
1547
|
```
|
|
1547
1548
|
|
|
1548
1549
|
- **Real seams.** The plugin loads through `Plugin::Loader.register_file` (the real `chi` API and a real `ctx`),
|
|
1549
|
-
|
|
1550
|
+
into registries that already hold chi's own
|
|
1550
1551
|
commands and tools: a name chi has fails the load as it would in chi. `tool_call` runs a real
|
|
1551
1552
|
`Guardrails::Gate` (context and targets computed for the call; `verdict:` starts from one with a vote in it).
|
|
1552
1553
|
Only the edges are recorded: the session (`Testing::Host`), the hook helpers (`Testing::Runtime`),
|
|
@@ -1600,7 +1601,10 @@ bundle.shutdown
|
|
|
1600
1601
|
go on; `bundle.lenient { … }` collects them in `bundle.errors` instead.
|
|
1601
1602
|
- **Sandbox.** A test runs with temp config, state, data and cache homes (config.yml, memories, `ctx.data_dir`, the
|
|
1602
1603
|
debug log), `HOME`, `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, no git config of yours, no Keychain and none of your
|
|
1603
|
-
`SAMAGOTCHI_*` settings; `sandbox.config(hash)` writes its config.yml.
|
|
1604
|
+
`SAMAGOTCHI_*` settings; `sandbox.config(hash)` writes its config.yml. A `Testing::Bundle.new` or `Bundle.load` with
|
|
1605
|
+
no `sandbox:` and no current sandbox enters its own, snapshots `ENV` when it is built, and restores that copy at
|
|
1606
|
+
`bundle.shutdown`. Build it before changing `ENV` in a test, not lazily inside a block that sets `ENV` for one example (your own
|
|
1607
|
+
helper, or ClimateControl); or scope the sandbox to the example with the `:bundle` tag or `Sandbox.with { … }`.
|
|
1604
1608
|
- **RSpec:** `require "samagotchi/testing/rspec"`; example groups tagged `:bundle` (a spec under a bundle's own
|
|
1605
1609
|
`spec/` is tagged by itself) get `load_bundle(dir)`, `shipped_bundle(name)` and `sandbox`, each example in its
|
|
1606
1610
|
own sandbox, its bundles shut down after it (chi's shipped bundles keep theirs in `<bundle>/spec/`,
|
|
@@ -1634,14 +1638,14 @@ When a session's worker exits (an idle exit, `/exit`, a crash, TERM), or a
|
|
|
1634
1638
|
## Loading, and when it fails
|
|
1635
1639
|
|
|
1636
1640
|
Plugins load when a session starts (`Engine.new`, in a worker or a
|
|
1637
|
-
`--non-interactive` one-shot), after the
|
|
1641
|
+
`--non-interactive` one-shot), after the config hooks. The steps are:
|
|
1638
1642
|
|
|
1639
1643
|
1. The file must match the sha256 recorded at install (not checked for a bundle installed with `--dev`).
|
|
1640
1644
|
2. chi must meet `requires_chi`. A bundle whose `requires_chi` this chi doesn't
|
|
1641
1645
|
meet is refused at install, so this step is reached only by one installed
|
|
1642
1646
|
with `--force` or by a chi that has since been downgraded.
|
|
1643
1647
|
3. The file is `module_eval`'d into a new module in the bundle's namespace
|
|
1644
|
-
(`Samagotchi::Bundles::<bundle
|
|
1648
|
+
(`Samagotchi::Bundles::<bundle>`, `Plugin::Loader.namespace_for`).
|
|
1645
1649
|
4. The class is built, and `register(chi)` runs.
|
|
1646
1650
|
|
|
1647
1651
|
What `register` adds takes effect only when it returns. A plugin that raises
|
|
@@ -1663,8 +1667,8 @@ starts again (`chi sessions stop`, or the idle exit).
|
|
|
1663
1667
|
|
|
1664
1668
|
## Trust
|
|
1665
1669
|
|
|
1666
|
-
A plugin is Ruby code that runs inside chi with your permissions
|
|
1667
|
-
|
|
1670
|
+
A plugin is Ruby code that runs inside chi with your permissions.
|
|
1671
|
+
**Installing a bundle means trusting it**, as you would a
|
|
1668
1672
|
gem.
|
|
1669
1673
|
|
|
1670
1674
|
- The sha256 is an integrity check, not proof of who wrote the file. A file
|
data/docs/releasing.md
CHANGED
|
@@ -31,10 +31,10 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
31
31
|
profile's (next point);
|
|
32
32
|
- a new `ctx.*` / `chi.*` call a plugin may make gets its release's version in `Plugin::Api::SINCE` once
|
|
33
33
|
released (`chi bundle check`'s `requires-chi-floor` reads it); an unreleased one waits there as an `# Unreleased` comment, and
|
|
34
|
-
`release:
|
|
34
|
+
`release:bump` moves each call it names up with the new version (`release:check` fails while a comment is left);
|
|
35
35
|
- a new manifest key or shape gets its release's version in `Manifest::SINCE` the same way (check's
|
|
36
36
|
`requires-chi-floor` reads it for the manifest too), waits as an `# Unreleased` comment while unreleased,
|
|
37
|
-
and `release:
|
|
37
|
+
and `release:bump` moves it up with the new version the same way;
|
|
38
38
|
- a new shipped bundle joins a profile (`includes:` in `core` or `dev`; a
|
|
39
39
|
spec checks every bundle is in exactly one), and that profile's version
|
|
40
40
|
goes up: `chi update` then installs only the new bundle for those who
|
|
@@ -69,14 +69,14 @@ as the GitHub release body.
|
|
|
69
69
|
|
|
70
70
|
| Task | What it does |
|
|
71
71
|
| --- | --- |
|
|
72
|
-
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `
|
|
72
|
+
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `plugin:`, `scripts:`). |
|
|
73
73
|
| `rake bundles:check` | Sha lines match; every top-level `*.md` of a bundle is listed in its `files:` (an install takes only the listed ones); a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`, and a profile (`core`, `dev`) whose `requires_chi` is below a member's. The release workflow and `release:check` run it in full. |
|
|
74
74
|
| `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
|
|
75
75
|
| `rake bundles:specs` | The shipped bundles' own specs (`lib/samagotchi/bundles/*/spec`) on the bundle test kit alone (no `spec_helper`), as an external bundle's run. CI runs it as its own step. |
|
|
76
76
|
| `rake lint` / `rake lint:fix` | RuboCop over the repo (must report no offenses; CI runs it) / its safe autocorrect (`rubocop -a`). |
|
|
77
77
|
| `rake gem:validate`, `gem:build`, `gem:install`, `gem:clean` | Check the gemspec; build the gem into `pkg/`; build and install it locally; remove the built gems. |
|
|
78
78
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
79
|
-
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line. No commit. |
|
|
79
|
+
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped), then a profile left below a member's `requires_chi` raised to it, with a patch bump of its version (none when it moved since the last tag) and named in the CHANGELOG's "Update with `chi update`" line, and each entry an `# Unreleased` comment in `Plugin::Api::SINCE` or `Manifest::SINCE` names moved into the table at X.Y.Z (the comment dropped). No commit. |
|
|
80
80
|
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, no `# Unreleased` comment left in `Plugin::Api::SINCE` or `Manifest::SINCE`, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
81
81
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
82
82
|
|
|
@@ -96,7 +96,8 @@ The agent does each step and stops where the user has to say yes.
|
|
|
96
96
|
names the bundles whose `requires_chi` it raised (their `requires_chi_next`
|
|
97
97
|
markers are gone) and the profiles it raised with them (a patch bump of
|
|
98
98
|
each one's version, none for a profile already bumped since the last tag),
|
|
99
|
-
which it names in the CHANGELOG's "Update with `chi update`" line
|
|
99
|
+
which it names in the CHANGELOG's "Update with `chi update`" line, and the
|
|
100
|
+
SINCE entries it moved out of their `# Unreleased` comments.
|
|
100
101
|
5. **Check.** Commit first (the check wants a clean tree), then run it:
|
|
101
102
|
`git commit -am "Release X.Y.Z"` and `bundle exec rake release:check`.
|
|
102
103
|
Fix anything it finds in new commits and run it again.
|
data/docs/scheduling.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Schedules (`chi schedule`)
|
|
2
|
+
|
|
3
|
+
A **schedule** is a prompt with a clock. It lives in one file on this machine, not in a
|
|
4
|
+
worker's memory, so it outlives every session, every restart and every crash:
|
|
5
|
+
`~/.local/state/samagotchi/schedules.json` (next to `sessions/`, written with
|
|
6
|
+
`AtomicFile`, every change a read-modify-write under `schedules.lock`, so cron and
|
|
7
|
+
chi web's hub can never lose an update to each other). A **tick** — cron, systemd,
|
|
8
|
+
launchd, `chi schedule tick`, or chi web's hub when `schedule.auto_tick` is on —
|
|
9
|
+
walks the entries whose `next_due` has passed and runs the ones that are not busy.
|
|
10
|
+
|
|
11
|
+
Each entry carries `id`, an optional `title`, `anchor` (the session it was made
|
|
12
|
+
against, never rewritten: the chain's root and the id you remember), `head` (the
|
|
13
|
+
chain's current link, re-anchored after every firing: the link a chain continue
|
|
14
|
+
made, or the session a `same` turn was queued into), `mode`,
|
|
15
|
+
`prompt`, `recurrence`, `catch_up`, `keep_links`, `max_failures`, `max_skips`,
|
|
16
|
+
`next_due` (UTC ISO), `last_fired`, `last_result`, `failures`, `skips` and
|
|
17
|
+
`enabled`.
|
|
18
|
+
|
|
19
|
+
<a id="reminder-vs-schedule"></a>## Reminder or schedule?
|
|
20
|
+
|
|
21
|
+
`register_reminder` and `chi schedule` are separate on purpose; which one you want
|
|
22
|
+
depends on whether the worker has to be running.
|
|
23
|
+
|
|
24
|
+
| | `register_reminder` | `chi schedule` |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Where it lives | the worker's `Engine` (`ReminderStore`), in memory | `<state dir>/schedules.json`, durable |
|
|
27
|
+
| Who fires it | `IdleReminders`, inside a live idle worker | `chi schedule tick`, run by cron/systemd (or the hub when opted in) |
|
|
28
|
+
| Needs a worker | yes — and a reminder *holds* the worker up | no — it wakes one when none runs |
|
|
29
|
+
| Survives a stop, a crash, an archive | only the short asked-restart hand-off (`ReminderCarry`, 2 min) | yes, until cancelled |
|
|
30
|
+
| Longest interval | 1440 minutes | unlimited (`every`, `weekday`, `date`) |
|
|
31
|
+
| What it produces | a turn in the same session | a new chain link, or a queued turn in the same session |
|
|
32
|
+
|
|
33
|
+
A schedule turn carries `client_id: "system:schedule:<id>"`, so `ClientId.origin`
|
|
34
|
+
says `schedule` and the terminal and the web label it as such rather than as
|
|
35
|
+
something you typed. Like `register_reminder`, the schedule tools are owner-voiced:
|
|
36
|
+
a participant's review or pair turn is denied them ([guardrails.md](guardrails.md)).
|
|
37
|
+
|
|
38
|
+
## The two modes
|
|
39
|
+
|
|
40
|
+
- **chain** (the default) runs the prompt as the **next link of the session's chain**:
|
|
41
|
+
the head is archived, a new link is created with the carry note
|
|
42
|
+
([sessions.md, Session chains](sessions.md#session-chains)), and `head` moves to it.
|
|
43
|
+
This is the "same routine, fresh context" case: a daily coordinator that starts each
|
|
44
|
+
day with the recap of the last one instead of a 40-link transcript.
|
|
45
|
+
- **same** queues the prompt as a turn **in the session itself**
|
|
46
|
+
(`SessionManager.deliver_turn`, `Delivery::QUEUE`), waking a worker when none runs.
|
|
47
|
+
It prunes nothing, and it never archives anything. Like chain mode, it follows
|
|
48
|
+
`head` through the chain: once the session is continued — by a chain schedule or
|
|
49
|
+
by a `chi send --continues` — the turn goes to the new link, not to the archived
|
|
50
|
+
anchor the schedule was made against.
|
|
51
|
+
|
|
52
|
+
A `scratch` session is refused at `add`: it is deleted when its terminal leaves.
|
|
53
|
+
A prompt is checked at `add` too — non-empty and at most 16 KiB
|
|
54
|
+
(`SessionInbox::NOTE_MAX_BYTES`); a blank one would archive a link and start an
|
|
55
|
+
idle session for nothing.
|
|
56
|
+
|
|
57
|
+
## Recurrence
|
|
58
|
+
|
|
59
|
+
One kind per schedule:
|
|
60
|
+
|
|
61
|
+
| Kind | Written as | How the next due is computed |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `every` | `--every 45m` / `2h` / `1d` | interval arithmetic from the firing — `next_due = fired_at + interval` |
|
|
64
|
+
| `weekday` | `--weekday 1 --time 09:00` (1=Mon … 7=Sun) | the next such wall-clock moment in the machine's local zone, strictly after now (today counts if the time is still ahead), stored as UTC |
|
|
65
|
+
| `date` | `--date 2026-11-20` or `--date 2026-11-20T09:00` | one local moment; after it fires the entry is disabled |
|
|
66
|
+
|
|
67
|
+
**The DST caveat.** `every` is interval math in UTC, so an `every 1d` schedule
|
|
68
|
+
drifts by an hour across a DST fold: fired Monday 09:00, its next due is Tuesday
|
|
69
|
+
09:00 UTC, which is a different hour on the wall clock after the change. For a
|
|
70
|
+
schedule at a *time of day*, use `--weekday` (or `--date`): those build the moment
|
|
71
|
+
with `Time.local` for the candidate date, so `Mon 09:00` stays 09:00 local across
|
|
72
|
+
the fold. If a local moment does not exist (a spring-forward gap), it fires at the
|
|
73
|
+
next valid minute. Schedules use the machine's local clock; there are no named
|
|
74
|
+
time zones.
|
|
75
|
+
|
|
76
|
+
`catch_up` says what an overdue entry does when the tick arrives after its moment:
|
|
77
|
+
|
|
78
|
+
- `once` (default): fire once now, then reschedule normally.
|
|
79
|
+
- `none`: fire only when the tick is near the moment. "Missed" — `last_result:
|
|
80
|
+
"missed"` and the entry moves to its next occurrence — is a **whole cycle**, not
|
|
81
|
+
a late tick: it happens only when the occurrence *after* the one it missed is
|
|
82
|
+
already past, which is what the file looks like when the machine was asleep
|
|
83
|
+
across the interval. A `Mon 09:00` schedule ticked at 09:20 fires; the same
|
|
84
|
+
entry first seen the following Monday is missed and moves to the Monday after.
|
|
85
|
+
A `date` one-off has no next occurrence to compare with, so it is never missed:
|
|
86
|
+
it fires late, once.
|
|
87
|
+
|
|
88
|
+
## The busy check
|
|
89
|
+
|
|
90
|
+
A cron tick cannot read a worker's memory, so the tick has two paths.
|
|
91
|
+
|
|
92
|
+
**No live worker** (the usual case for a weekly schedule): the files say what can
|
|
93
|
+
be known — the saved status is not `running` (and not inside the starting grace),
|
|
94
|
+
`input/` is empty, no step-limit question is open. Nothing else can be busy.
|
|
95
|
+
|
|
96
|
+
**A live worker**: ask it, over the new Bridge route
|
|
97
|
+
`GET /session/:id/activity` (`Worker#activity_snapshot`, the same signals
|
|
98
|
+
`WorkerIdleExit#hold` reads): `{turn_running, input_pending, awaiting_continue,
|
|
99
|
+
open_streams, reminders, tasks_holding}`. It is in `Bridge::FEATURES`, so a worker
|
|
100
|
+
from an older chi has no such route, and any non-200 from it is read as "older":
|
|
101
|
+
the tick says to restart that worker.
|
|
102
|
+
|
|
103
|
+
- `turn_running`, `input_pending` or `awaiting_continue` ⇒ busy, in both modes.
|
|
104
|
+
- `open_streams` (a terminal attached, or a web tab open on the session) counts
|
|
105
|
+
**only for chain mode**: a chain firing archives the session and stops its
|
|
106
|
+
worker, which would cut off the person looking at it. A queued turn into an open
|
|
107
|
+
session is fine, so `same` mode ignores streams.
|
|
108
|
+
- **An older worker is itself a busy signal for chain mode.** Its 404 must not fall
|
|
109
|
+
back to the file checks: those cannot see an open tab, so the firing would
|
|
110
|
+
archive under someone's terminal. The tick skips it with "its worker is too old
|
|
111
|
+
to ask: `chi sessions restart ID`" — chi's usual rule for an older worker:
|
|
112
|
+
refuse with a restart, never silently degrade. `same` mode, which only queues a
|
|
113
|
+
turn, may use the file checks.
|
|
114
|
+
|
|
115
|
+
A `chi send --wait` or a parent's `delegate` wait is **not** busy: `ReplyWait::Poll`
|
|
116
|
+
reads the session files and `GET /session/:id/state`, never `open_stream`, so a
|
|
117
|
+
coordinator waiting on its own child does not block that child's chain.
|
|
118
|
+
|
|
119
|
+
Schedules never hold a worker up: unlike reminders, they are not in
|
|
120
|
+
`WorkerIdleExit#hold` (FR14) — the whole point is that the worker may be gone and
|
|
121
|
+
the tick wakes it.
|
|
122
|
+
|
|
123
|
+
## Skip, not failure
|
|
124
|
+
|
|
125
|
+
A firing that cannot run now is a **skip**: nothing happens, `skips += 1`,
|
|
126
|
+
`last_result: "skipped"`, and `next_due` goes back to the occurrence this tick tried
|
|
127
|
+
(the tick advances the clock before acting and keeps that moment as `due_was`, so the
|
|
128
|
+
next tick tries it again — a weekly schedule skipped on its Monday is retried at that
|
|
129
|
+
Monday, not a week later). A failure restores it the same way. A `date` entry is the
|
|
130
|
+
exception: advancing it spends it, so it comes back at that same moment.
|
|
131
|
+
Skips
|
|
132
|
+
are the refusals that mean "not this moment": `ArchiveRefused` with `busy`, `queued`
|
|
133
|
+
or `offer`, or `busy_child`/`queued_child`/`offer_child` (a chain's own running
|
|
134
|
+
delegate refuses the archive, since `archivable!` walks the whole descendant tree),
|
|
135
|
+
`SessionContinue::Refused(:continued)` (a human continued it while we were
|
|
136
|
+
resolving) and `:open_children`.
|
|
137
|
+
|
|
138
|
+
Anything else is a **failure** (`failures += 1`): a gone or archived target,
|
|
139
|
+
`SessionContinue::Refused(:folder_gone)`, `SessionManager::DeleteRefused`,
|
|
140
|
+
`ModelProfile::MissingModel`, `OwnedByTUI`, `Session::NotFound`, and anything a
|
|
141
|
+
spawn throws (`Errno::ENOSPC` included — `run_locked` re-raises it after undoing
|
|
142
|
+
the move, so it is never mistaken for a skip). A `DeleteRefused(:worker_running)`
|
|
143
|
+
hit while pruning stops the pruning for that firing, is logged, and is not a
|
|
144
|
+
schedule failure.
|
|
145
|
+
|
|
146
|
+
`skips >= max_skips` (default 12) or `failures >= max_failures` (default 3)
|
|
147
|
+
disables the entry and `chi schedule list` says why. **Any successful firing resets
|
|
148
|
+
both counters**, so twelve skips spread over months do not kill a healthy weekly
|
|
149
|
+
schedule. `list` prints the skip budget as a time window — 12 skips at a 5 minute
|
|
150
|
+
tick is about an hour of patience, which is what the count really measures.
|
|
151
|
+
|
|
152
|
+
A chain holds at most `schedule.max_per_session` enabled schedules (default 10) and
|
|
153
|
+
the machine `schedule.max_total` (default 50); `schedule_task` and `chi schedule add`
|
|
154
|
+
refuse past them, naming the key. A schedule the chain already has — the same prompt
|
|
155
|
+
and recurrence, or the same title — is refused too, naming its id, so a routine that
|
|
156
|
+
tells each of its links to "make sure the Monday audit is scheduled" cannot multiply
|
|
157
|
+
itself. `schedule_task`, `list_schedules` and `cancel_schedule` see only schedules of
|
|
158
|
+
the calling session's chain (`all: true` steps outside it); `chi schedule list` is
|
|
159
|
+
always the machine-wide view. An id, prefix or title that matches more than one entry
|
|
160
|
+
is refused with exit 2, naming the matches.
|
|
161
|
+
|
|
162
|
+
The tick's lock covers the file only, never the action: the clock advances before
|
|
163
|
+
the work starts — for `every` and `weekday` to the next occurrence, for a `date`
|
|
164
|
+
one-off to none — so a tick killed mid-continue cannot fire the same entry twice.
|
|
165
|
+
`firing_since` is what `chi schedule show` prints; the advanced clock is the guard.
|
|
166
|
+
And a `chi schedule add` from a terminal never waits on an archive or a recap wait. Chain mode targets `last:<head>`, so `SessionContinue.run`'s
|
|
167
|
+
own retry loop follows a continue a human made between our resolve and its lock
|
|
168
|
+
instead of racing it.
|
|
169
|
+
|
|
170
|
+
## Keeping the chain short
|
|
171
|
+
|
|
172
|
+
`keep_links` (default `schedule.default_keep_links`, `0` keeps all) is how many
|
|
173
|
+
chain links stay behind the head. After a successful chain firing — and after the
|
|
174
|
+
result is recorded under the lock, so a prune that raises midway cannot lose the
|
|
175
|
+
new `head` — the walk goes backwards from `head` and every link older than the
|
|
176
|
+
window is deleted **together with its delegated children** (`descendant_ids`): the
|
|
177
|
+
whole subtree only when every session in it is archived and no worker owns any of
|
|
178
|
+
it, deepest child first and the link last, oldest link first. A subtree with a live
|
|
179
|
+
worker or an unarchived descendant is left whole and retried at the next firing. Pruning a link alone would leave its finished
|
|
180
|
+
delegates archived forever with a dangling `parent_id`: archiving a session archives
|
|
181
|
+
its descendants, and archived sessions are outside the retention sweep
|
|
182
|
+
([sessions.md](sessions.md)), so the delegate pile is the bulk of the archived
|
|
183
|
+
count. The `anchor` is never pruned: it is the chain's root, the id the user and
|
|
184
|
+
the coordinator remember, and `last:<anchor>` must keep resolving.
|
|
185
|
+
|
|
186
|
+
The oldest kept link still has `continues` naming a link that is gone. The walks
|
|
187
|
+
follow `continues` forward, so nothing breaks; the web's `↩ #N of M` chip counts
|
|
188
|
+
from the surviving root (pruning renumbers the links) and says "earlier links
|
|
189
|
+
pruned" instead of linking to a deleted session.
|
|
190
|
+
|
|
191
|
+
If a link to prune still has a worker (`DeleteRefused(:worker_running)`), pruning
|
|
192
|
+
stops there for this firing and is retried at the next one.
|
|
193
|
+
|
|
194
|
+
## Running the tick
|
|
195
|
+
|
|
196
|
+
`chi schedule tick` is quiet and safe to run often; it is this machine only
|
|
197
|
+
(`chi --remote studio schedule list` exits 2), so point cron at the chi of this
|
|
198
|
+
machine (cron runs with a thin PATH, so name the binary rather than a shell
|
|
199
|
+
function):
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
# crontab -e on the machine
|
|
203
|
+
*/5 * * * * /Users/you/.local/share/mise/installs/ruby/global/bin/chi schedule tick >> /Users/you/.local/state/samagotchi/schedule-tick.log 2>&1
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Run it from systemd or launchd instead if you prefer a timer: the command is the
|
|
207
|
+
same, `chi schedule tick`, and the lock makes overlapping runs harmless — the
|
|
208
|
+
second one exits 0 with "no schedules due (another tick holds the lock)".
|
|
209
|
+
`--dry-run` prints what would fire and changes nothing; `--format json` is the seam
|
|
210
|
+
for scripts and for a future web UI.
|
|
211
|
+
|
|
212
|
+
A tick fires **one entry per chain**: of the due entries of one chain, the one due
|
|
213
|
+
earliest (`next_due`, then id) fires and the rest are skipped — "the chain just
|
|
214
|
+
fired; retry at the next tick" — with `skips += 1` and `next_due` left at the
|
|
215
|
+
occurrence they tried, so the chain's entries take turns instead of the first one
|
|
216
|
+
added winning every tick. A second chain
|
|
217
|
+
firing in the same pass would continue the link the first one just made and archive
|
|
218
|
+
it before its prompt ever ran: that link has no worker lock yet and the tick's
|
|
219
|
+
listing predates it, so the busy check cannot see it. `same` entries are unaffected:
|
|
220
|
+
they queue a turn, they do not archive.
|
|
221
|
+
|
|
222
|
+
`schedule.auto_tick: true` makes chi web's hub tick on
|
|
223
|
+
`schedule.tick_interval_minutes` (default 5) as well, sharing the same lock, so
|
|
224
|
+
cron and the hub can coexist without firing twice. It is off by default: cron or
|
|
225
|
+
systemd is the documented driver.
|
|
226
|
+
|
|
227
|
+
See [cli.md](cli.md) for the subcommands and flags, and
|
|
228
|
+
[configuration.md](configuration.md) for the `schedule.*` settings.
|