samagotchi 0.157.0 → 0.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/README.md +1 -1
  4. data/bin/chi +3 -1
  5. data/docs/architecture.md +7 -6
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +2 -1
  8. data/docs/cli.md +21 -17
  9. data/docs/configuration.md +7 -5
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +8 -7
  12. data/docs/hooks.md +44 -80
  13. data/docs/internals/bridge.md +1 -1
  14. data/docs/internals/bundles.md +47 -34
  15. data/docs/internals/engine.md +7 -5
  16. data/docs/memory.md +39 -9
  17. data/docs/plugins.md +13 -9
  18. data/docs/releasing.md +6 -5
  19. data/docs/sessions.md +5 -5
  20. data/docs/testing.md +4 -4
  21. data/lib/samagotchi/bridge/card_places.rb +5 -13
  22. data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
  23. data/lib/samagotchi/bridge/turn_drawings.rb +7 -1
  24. data/lib/samagotchi/bundle_command/check.rb +23 -10
  25. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  26. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  27. data/lib/samagotchi/bundle_command.rb +15 -43
  28. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  29. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  30. data/lib/samagotchi/bundles/system/memory_guide.md +6 -6
  31. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  32. data/lib/samagotchi/context_command.rb +7 -5
  33. data/lib/samagotchi/context_sources.rb +4 -2
  34. data/lib/samagotchi/engine.rb +1 -6
  35. data/lib/samagotchi/extension_load.rb +20 -41
  36. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  37. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  38. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  39. data/lib/samagotchi/hook_routes.rb +2 -2
  40. data/lib/samagotchi/hooks.rb +2 -3
  41. data/lib/samagotchi/memory_bundle/asset_installer.rb +39 -119
  42. data/lib/samagotchi/memory_bundle/builder.rb +10 -34
  43. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  44. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  45. data/lib/samagotchi/memory_bundle/installed_copy.rb +11 -18
  46. data/lib/samagotchi/memory_bundle/installed_manifest.rb +2 -16
  47. data/lib/samagotchi/memory_bundle/installer.rb +32 -14
  48. data/lib/samagotchi/memory_bundle/manifest.rb +37 -54
  49. data/lib/samagotchi/memory_bundle/provenance.rb +11 -51
  50. data/lib/samagotchi/memory_bundle/remerge.rb +7 -3
  51. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  52. data/lib/samagotchi/memory_bundle/shipped_update.rb +30 -35
  53. data/lib/samagotchi/memory_bundle/status.rb +12 -10
  54. data/lib/samagotchi/memory_bundle/system_bundle.rb +16 -5
  55. data/lib/samagotchi/memory_bundle/trash.rb +7 -12
  56. data/lib/samagotchi/memory_orphans.rb +4 -4
  57. data/lib/samagotchi/memory_paths.rb +102 -13
  58. data/lib/samagotchi/plugin/api.rb +1 -4
  59. data/lib/samagotchi/plugin/loader.rb +42 -5
  60. data/lib/samagotchi/project_command.rb +167 -0
  61. data/lib/samagotchi/project_init.rb +69 -0
  62. data/lib/samagotchi/project_scope.rb +6 -4
  63. data/lib/samagotchi/prompt_files.rb +10 -7
  64. data/lib/samagotchi/recap_place.rb +3 -4
  65. data/lib/samagotchi/run_options.rb +1 -0
  66. data/lib/samagotchi/session.rb +44 -18
  67. data/lib/samagotchi/session_access/contract.rb +1 -1
  68. data/lib/samagotchi/session_field.rb +5 -4
  69. data/lib/samagotchi/steer.rb +10 -0
  70. data/lib/samagotchi/system_prompt.rb +20 -11
  71. data/lib/samagotchi/terminal_ui/join_replay.rb +6 -1
  72. data/lib/samagotchi/testing/bundle.rb +5 -12
  73. data/lib/samagotchi/testing/install.rb +5 -5
  74. data/lib/samagotchi/testing/recorder.rb +1 -1
  75. data/lib/samagotchi/testing.rb +1 -1
  76. data/lib/samagotchi/tool_declarations.rb +1 -1
  77. data/lib/samagotchi/toolkit_binding.rb +1 -4
  78. data/lib/samagotchi/toolkit_command.rb +1 -4
  79. data/lib/samagotchi/tools/delegate.rb +12 -8
  80. data/lib/samagotchi/version.rb +1 -1
  81. data/lib/samagotchi/web/app.rb +35 -0
  82. data/lib/samagotchi/web/project_list.rb +3 -2
  83. data/lib/samagotchi/web/public/index.html +6 -1
  84. data/lib/samagotchi/web/public/project_picker.js +86 -4
  85. metadata +3 -2
  86. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
@@ -39,7 +39,7 @@ module Samagotchi
39
39
  List installed bundles and the ones shipped with chi that aren't installed.
40
40
 
41
41
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
42
- Build local memories and installed hooks (and plugin) into a shareable bundle (dir or zip).
42
+ Build local memories and an installed plugin, rules and scripts into a shareable bundle (dir or zip).
43
43
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
44
44
  FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
45
45
  a named memory brings its model overlays (<name>.<model-key>.md).
@@ -75,7 +75,7 @@ module Samagotchi
75
75
  It also installs a bundle whose requires_chi this chi doesn't meet: without --force that install is
76
76
  refused, and the bundle is not written anywhere.
77
77
  install --dev PATH
78
- Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
78
+ Install the bundle in the local directory PATH as a link to it: its plugin, rules and scripts
79
79
  are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
80
80
  their sha256 isn't checked, for this bundle only. Its memories are copied; re-install with --force to
81
81
  pick up memory edits. Upgrade refuses it; uninstall removes the link, never PATH's files.
@@ -90,7 +90,7 @@ module Samagotchi
90
90
  Usage: chi bundle build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
91
91
 
92
92
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
93
- Build local memories and installed hooks into a shareable bundle (dir or zip).
93
+ Build local memories and an installed plugin, rules and scripts into a shareable bundle (dir or zip).
94
94
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
95
95
  FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
96
96
  a named memory brings its model overlays (<name>.<model-key>.md).
@@ -200,7 +200,7 @@ module Samagotchi
200
200
 
201
201
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, dev: dev)
202
202
  run_installer(installer, failed: "Install") do |manifest|
203
- print_hooks_and_plugin(manifest)
203
+ print_plugin(manifest)
204
204
  print_provenance(bundle_name, manifest)
205
205
  end
206
206
  end
@@ -236,7 +236,7 @@ module Samagotchi
236
236
 
237
237
  unless Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).installed?
238
238
  @stderr.puts "Bundle '#{bundle_name}' not installed — falling back to install"
239
- # (no Hooks/Plugin lines here)
239
+ # (no Plugin line here)
240
240
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, dry_run: dry_run)
241
241
  return run_installer(installer, failed: "Install") do |manifest|
242
242
  next dry_run_done if dry_run
@@ -246,7 +246,7 @@ module Samagotchi
246
246
  end
247
247
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, upgrade: true, dry_run: dry_run)
248
248
  run_installer(installer, failed: "Upgrade") do |manifest|
249
- print_hooks_and_plugin(manifest, plugin: !dry_run)
249
+ print_plugin(manifest, plugin: !dry_run)
250
250
  next dry_run_done if dry_run
251
251
 
252
252
  # Its summary said so, to someone reading.
@@ -304,7 +304,7 @@ module Samagotchi
304
304
  # Runs +installer+, prints its summary, then yields the manifest.
305
305
  # Ensures the extracted source dir is cleaned up even if an exception
306
306
  # occurs between installer.run and the block (e.g. summary,
307
- # print_hooks_and_plugin). The block (upgrade_conflicts) still sees
307
+ # print_plugin). The block (upgrade_conflicts) still sees
308
308
  # the files because cleanup happens after the block returns or raises.
309
309
  # @return [Integer] the block's exit status, or 1 after "<failed> failed: …"
310
310
  def run_installer(installer, failed:)
@@ -318,8 +318,7 @@ module Samagotchi
318
318
  installer.cleanup_source!
319
319
  end
320
320
 
321
- def print_hooks_and_plugin(manifest, plugin: true)
322
- @stdout.puts "Hooks: #{manifest.hooks.size} hook(s) (#{manifest.hooks.keys.join(", ")})" if manifest&.hooks&.any?
321
+ def print_plugin(manifest, plugin: true)
323
322
  return unless plugin && manifest&.plugin
324
323
 
325
324
  # Installed with --force past its requires_chi: the Engine won't load it.
@@ -425,8 +424,6 @@ module Samagotchi
425
424
  puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
426
425
  @stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
427
426
  @stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
428
- hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
429
- @stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
430
427
  uninstaller.warnings.each { |w| @stdout.puts w }
431
428
  0
432
429
  rescue Samagotchi::MemoryBundle::Uninstaller::UninstallError => e
@@ -519,22 +516,14 @@ module Samagotchi
519
516
  @stdout.puts " #{k}: #{label}"
520
517
  end
521
518
  @stdout.puts " #{manifest_line(st.provenance)}"
522
- # Hooks
523
519
  manifest = st.provenance.manifest
524
- hooks = manifest.hooks
525
520
  trust = manifest.trust_level || "experimental"
526
521
  commit = st.provenance.source_commit
527
- @stdout.puts " trust_level: #{trust}" if hooks.any? || trust
522
+ @stdout.puts " trust_level: #{trust}"
528
523
  @stdout.puts " source_commit: #{commit}" if commit
529
- if hooks.any?
530
- @stdout.puts " Hooks (#{hooks.size}):"
531
- hooks.each do |hk, hook|
532
- hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk)
533
- exists = File.exist?(hook_path) ? "ok" : "missing"
534
- @stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
535
- end
536
- @stdout.puts " requires_chi: #{manifest.requires_chi}" if st.hooks_requires_failure
537
- @stdout.puts " not loaded: #{st.hooks_requires_failure}" if st.hooks_requires_failure
524
+ unless st.retired_hooks.empty?
525
+ @stdout.puts " Hooks (#{st.retired_hooks.size}): not loaded: #{Samagotchi::MemoryBundle::Manifest::HooksRetired::WORDS}"
526
+ st.retired_hooks.each { |file, state| @stdout.puts " #{file} [#{state}]" }
538
527
  end
539
528
  if (plugin = st.plugin)
540
529
  note = plugin[:state] == "modified" ? " (edited after install: not loaded; reinstall the bundle)" : ""
@@ -555,11 +544,10 @@ module Samagotchi
555
544
 
556
545
  st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
557
546
  mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
558
- hooks_count = st.provenance.manifest.hooks.size
559
- hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
547
+ hook_info = st.retired_hooks.empty? ? "" : " hooks=#{st.retired_hooks.size}"
560
548
  plugin = st.plugin
561
549
  mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
562
- mods += 1 if st.hooks_requires_failure
550
+ mods += 1 unless st.retired_hooks.empty?
563
551
  mods += 1 if st.scope_error
564
552
  plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
565
553
  includes = st.provenance.includes
@@ -606,15 +594,12 @@ module Samagotchi
606
594
  rescue ArgumentError => e
607
595
  @stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
608
596
  end
609
- hooks = record.manifest.hooks
610
597
  plugin_path = prov.plugin_path(record)
611
598
  plugin_file = plugin_path && File.basename(plugin_path)
612
599
  if file_arg
613
- # Try file first, then hook
600
+ # A memory first, then the plugin (an older chi's hook is no file of the bundle's)
614
601
  if record.owns?(file_arg)
615
602
  diff_memory(prov, target_dir, file_arg)
616
- elsif hooks.key?(file_arg)
617
- diff_hook(prov, file_arg, hooks[file_arg])
618
603
  elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
619
604
  diff_plugin(prov, record, plugin_path)
620
605
  else
@@ -627,10 +612,6 @@ module Samagotchi
627
612
  diff_memory(prov, target_dir, key)
628
613
  @stdout.puts ""
629
614
  end
630
- hooks.each do |key, hook|
631
- diff_hook(prov, key, hook)
632
- @stdout.puts ""
633
- end
634
615
  if plugin_file
635
616
  diff_plugin(prov, record, plugin_path)
636
617
  @stdout.puts ""
@@ -666,11 +647,6 @@ module Samagotchi
666
647
  end
667
648
  end
668
649
 
669
- def diff_hook(prov, key, hook)
670
- print_base_and_current("hooks/#{key}", prov.base_path(key), File.join(prov.hooks_dir, key))
671
- @stdout.puts hook_metadata_line(hook)
672
- end
673
-
674
650
  def diff_plugin(prov, record, plugin_path)
675
651
  file = File.basename(plugin_path)
676
652
  print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
@@ -733,10 +709,6 @@ module Samagotchi
733
709
  "includes=#{here.join(",")}#{" left out=#{gone.join(",")}" unless gone.empty?}"
734
710
  end
735
711
 
736
- def hook_metadata_line(hook)
737
- "--- metadata: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} sha256=#{hook.sha256}"
738
- end
739
-
740
712
  def build(rest)
741
713
  parsed = parse_flags(BUILD_FLAGS, rest, "build", help: BUILD_HELP)
742
714
  return parsed if parsed.is_a?(Integer)
@@ -76,7 +76,7 @@ guardrails:
76
76
  disable: [guardrails:git-rebase]
77
77
  ```
78
78
 
79
- - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
79
+ - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
80
80
 
81
81
  ```yaml
82
82
  bundles:
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.157.0
3
+ version: 0.158.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
9
- config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
10
- memory_guide.md: sha256:fe4dbb25ce4c9f9b51507a6130fa7bf992a2cc41af2453830eacd27c09a65d1e
8
+ self_map.md: sha256:750822b3488b302c6e32638e6224f270d8e06b50ea85cd6e0bb5a6ff12eed1d8
9
+ config_modification_protocol.md: sha256:44cf7bdac8e526805bff36b76b54fefecd41db30ca525c2ed661d78fe9155dd8
10
+ memory_guide.md: sha256:91e8fd248a155afad6b825097d9ee0a0215ba0b79092c8ab25501c0fc19dedfc
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -15,7 +15,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
15
15
  | `system` | `$XDG_CONFIG_HOME/samagotchi/memories/`, default `~/.config/samagotchi/memories/` (`MemoryPaths.system_dir`) | User-wide preferences, identity, cross-project knowledge |
16
16
  | `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
17
17
 
18
- - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). Outside a repository the root is the working directory. A separate clone is a different project.
18
+ - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). A separate clone is a different project. Outside a repository, a folder marked by `chi project init` (its `.chi/project.yml`) is the project for it and every subfolder; outside both, the root is the working directory.
19
19
  - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
20
20
 
21
21
  ## Tools you have
@@ -101,11 +101,11 @@ Writing or updating a toolkit memory is free like writing a skill; say so in the
101
101
 
102
102
  ## Memory Bundles — shareable packs
103
103
 
104
- Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `hooks/*.rb` (bundle hooks, `docs/hooks.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`). They are shareable and installable.
104
+ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`; class-file `hooks/*.rb` are retired: chi refuses a source with them, `docs/hooks.md`). They are shareable and installable.
105
105
 
106
- - Manifest: `files:` (memory → `sha256:`), `hooks:` (file → sha256/event/on_error/priority), `plugin: {file: plugin.rb, sha256: sha256:…}`, `requires_chi: ">= 0.1.30"` (gem-style), `needs:` (outside commands looked up on `PATH`, advisory; `docs/memory.md` "Bundles that need outside commands").
107
- - Integrity: each file's sha256 is recorded at install; a hook/plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
108
- - Installing = trusting its Ruby code (hooks, plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
106
+ - Manifest: `files:` (memory → `sha256:`), `plugin: {file: plugin.rb, sha256: sha256:…}`, `requires_chi: ">= 0.1.30"` (gem-style), `needs:` (outside commands looked up on `PATH`, advisory; `docs/memory.md` "Bundles that need outside commands").
107
+ - Integrity: each file's sha256 is recorded at install; a plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
108
+ - Installing = trusting its Ruby code (the plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
109
109
  - A bundle without `*.md` (btw, mcp, loop-guard) adds no line to the prompt's memory index.
110
110
  - Profiles are shipped bundles with only `includes:` (bundle names): `core` (loop-guard, check-in, guardrails, slow-commands; `chi bootstrap` installs it) and `dev` (known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer). `chi bundle install core` installs the ones not installed and records them; a bundle the user uninstalled stays out on a later install, upgrade or `chi update`, which install only bundles new to the profile. `chi bundle uninstall core` removes its recorded bundles, then core (`docs/memory.md` "Bundle profiles").
111
111
 
@@ -119,7 +119,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
119
119
  | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
120
120
  | `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
121
121
  | `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
122
- | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
122
+ | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and an installed bundle's plugin, rules and scripts. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
123
123
 
124
124
  **Scope resolution for install/build:**
125
125
  - CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<basename>_<hash>` of the project root (`MemoryPaths.scope_dir`).
@@ -51,7 +51,7 @@
51
51
  normal session: it shows in `chi sessions list` with `↳ <parent>`, and the user can steer it
52
52
  with `chi --attach <id>`. While I wait for a child, its tool approvals go to my user as my own
53
53
  approvals (I see only how each was answered); see `docs/sessions.md` "Delegating".
54
- - Bundles extend me: memories, `hooks/*.rb`, `guardrails/*.yml`, and a `plugin.rb` that adds slash
54
+ - Bundles extend me: memories, `guardrails/*.yml`, and a `plugin.rb` that adds slash
55
55
  commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
56
56
  cards, side answers (`ask_model`), System 1 judgments (`chi.judgment`, `ctx.judge`), HTTP calls (`ctx.http`),
57
57
  child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
@@ -409,17 +409,19 @@ module Samagotchi
409
409
  end
410
410
 
411
411
  # A project's sources: local only (SessionAccess::Contract), this
412
- # machine's checkout.
412
+ # machine's checkout or marked folder.
413
413
  def project_target
414
- unless MemoryPaths.in_repo?(@cwd)
415
- error_line("#{command_name}: --project needs a git repository, and #{@cwd} is in none")
414
+ found = MemoryPaths.lookup(@cwd)
415
+ unless found.project?
416
+ error_line("#{command_name}: --project needs a project (a git repository, or a folder marked with " \
417
+ "`chi project init`), and #{@cwd} is in none")
416
418
  return nil
417
419
  end
418
420
 
419
- root = MemoryPaths.project_root(@cwd)
421
+ root = found.root
420
422
  # Its sources run in the root, or in this checkout when the root is a
421
423
  # git dir (ContextSources.project_cwd).
422
- Target.new(session_id: nil, project_root: root, cwd: MemoryPaths.work_tree_root(@cwd) || @cwd,
424
+ Target.new(session_id: nil, project_root: root, cwd: found.top || @cwd,
423
425
  label: "project #{File.basename(root)}")
424
426
  end
425
427
 
@@ -658,13 +658,15 @@ module Samagotchi
658
658
  end
659
659
 
660
660
  # Where a project's source runs: the project root when it is a checkout
661
- # (its .git is there), else +fallback+ (the session's folder): a bare
661
+ # (its .git is there) or a marked folder (MemoryPaths::MARKER), else
662
+ # +fallback+ (the session's folder): a bare
662
663
  # repo, proj/.bare or --separate-git-dir makes the root the git dir
663
664
  # itself (MemoryPaths.project_root), and a command run there sees no
664
665
  # files.
665
666
  def project_cwd(project_root, fallback)
666
667
  root = project_root.to_s
667
- !root.empty? && File.exist?(File.join(root, ".git")) ? root : fallback
668
+ checkout = !root.empty? && [".git", MemoryPaths::MARKER].any? { |name| File.exist?(File.join(root, name)) }
669
+ checkout ? root : fallback
668
670
  end
669
671
 
670
672
  # A deleted session's sources, snapshots, subscriptions and markers.
@@ -357,12 +357,7 @@ module Samagotchi
357
357
  # toolkit, the tool index, peer sources), then the plugins themselves
358
358
  # unless +load+ is false (see #initialize for when).
359
359
  def build_tools_and_plugins(load:, task_finish_notes:)
360
- # The routes are built already (#initialize), so a bundle's hook file
361
- # that raises at fire time is told the same way a chi.on handler is:
362
- # once per hook in this worker. The guard covers a hook that raises
363
- # before the routes are there (nothing in the Engine's path does).
364
- @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures,
365
- hook_failed: ->(hook, failed, effect: nil) { @hook_routes&.plugin_hook_failed(hook, failed, effect: effect) })
360
+ @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures)
366
361
  @hooks = @extension_load.hooks
367
362
  # The tools this session offers (the prompts' declarations and the
368
363
  # kernel's dispatch): the built-ins, per Engine. execute.description
@@ -7,20 +7,13 @@ require_relative "plugin/loader"
7
7
 
8
8
  module Samagotchi
9
9
  # What an Engine loads from outside chi as it starts: config.yml's hooks,
10
- # installed bundles' hooks and plugins, and config.yml `bundles:` (their
11
- # settings). It holds what plugins showed while they loaded (notices,
10
+ # installed bundles' plugins, and config.yml `bundles:` (their settings). It holds what plugins showed while they loaded (notices,
12
11
  # cards): no UI is there yet, so the Engine announces them on the first
13
12
  # turn (Engine#announce_load_events!).
14
13
  class ExtensionLoad
15
14
  # @param hook_failures [Guardrails::LoadFailures] where hooks that fail to load go
16
- # @param hook_failed [#call, nil] (label, "Class: message", effect:) when
17
- # an installed bundle's hook file raises at fire time: the Engine's
18
- # HookRoutes, so the user is told once per hook. nil where there is no
19
- # UI to tell (the test kit's sandbox): a hook that raises then says
20
- # nothing, and raises nothing.
21
- def initialize(hook_failures:, hook_failed: nil)
15
+ def initialize(hook_failures:)
22
16
  @hook_failures = hook_failures
23
- @hook_failed = hook_failed
24
17
  @loading = false
25
18
  @held_events = []
26
19
  end
@@ -40,12 +33,13 @@ module Samagotchi
40
33
  end
41
34
 
42
35
  # The hooks: config.yml's (Hooks::Loader; an empty Registry without a
43
- # hooks config), then the installed bundles' added to them.
36
+ # hooks config). An installed record that still has class-file hooks (a
37
+ # 0.157 install) adds a not-required failure each, which the Engine
38
+ # announces at the first turn: they don't load (#tell_retired_hooks).
44
39
  # @return [Hooks::Registry]
45
40
  def hooks
46
- registry = hooks_from_config
47
- add_bundle_hooks(registry)
48
- registry
41
+ tell_retired_hooks
42
+ hooks_from_config
49
43
  end
50
44
 
51
45
  # Load the installed bundles' plugins into +registries+; one that fails
@@ -62,8 +56,7 @@ module Samagotchi
62
56
  @loading = false
63
57
  end
64
58
 
65
- # config.yml `bundles:` (ConfigFile.bundle_settings), read once: the
66
- # bundle hooks and the plugins both want it.
59
+ # config.yml `bundles:` (ConfigFile.bundle_settings), read once.
67
60
  def bundle_settings
68
61
  @bundle_settings ||= Samagotchi::ConfigFile.bundle_settings
69
62
  end
@@ -78,36 +71,22 @@ module Samagotchi
78
71
  Hooks::Registry.new
79
72
  end
80
73
 
81
- def add_bundle_hooks(registry)
74
+ # One failure per record whose hooks: map isn't empty; an empty
75
+ # hooks/ dir is not one, nor a manifest.json that doesn't read (the
76
+ # plugin loader and the rules report that: a record holding only hooks/
77
+ # with an unreadable manifest.json is silent at start, as its hooks
78
+ # wouldn't load anyway; `chi bundle status` lists it unreadable).
79
+ def tell_retired_hooks
82
80
  require_relative "memory_bundle/provenance"
83
- settings = bundle_settings
84
81
  MemoryBundle::Provenance.each_installed(holding: :hooks) do |bundle_name, bundle|
85
- if bundle.error?
86
- Log.warn(:hooks, "bundle_manifest_invalid", echo: "[samagotchi:hooks] bundle '#{bundle_name}': #{bundle.error}; its hooks are not loaded",
87
- bundle: bundle_name)
88
- @hook_failures.add("hooks (bundle #{bundle_name})", bundle.error, required: false)
89
- next
90
- end
91
- bundle_dir = File.join(MemoryBundle::Provenance.bundles_dir, bundle_name)
92
- hooks_dir = File.join(bundle_dir, "hooks")
93
- manifest = bundle.manifest
94
- if manifest.experimental?
95
- Log.info(:hooks, "experimental_bundle", echo: "[hooks] Bundle '#{bundle_name}' is experimental — its hooks may change or misbehave.", bundle: bundle_name)
96
- end
97
- begin
98
- Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: manifest.hooks, registry: registry,
99
- failures: @hook_failures, settings: settings[bundle_name.to_s] || {},
100
- manifest: manifest, hook_failed: @hook_failed)
101
- rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
102
- raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
82
+ next if bundle.error?
103
83
 
104
- Log.error(:hooks, "bundle_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' failed to load hooks: #{e.class}: #{e.message}", bundle: bundle_name, error: e.class.name)
105
- end
84
+ Log.warn(:hooks, "bundle_hooks_retired", bundle: bundle_name, hooks: bundle.hooks.size)
85
+ @hook_failures.add("hooks (bundle #{bundle_name})", MemoryBundle::Manifest::HooksRetired::WORDS, required: false)
106
86
  end
107
- rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
108
- raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
109
-
110
- Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to load bundle hooks: #{e.class}: #{e.message}", error: e.class.name)
87
+ rescue StandardError => e
88
+ Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to read the installed bundles: #{e.class}: #{e.message}",
89
+ error: e.class.name)
111
90
  end
112
91
  end
113
92
  end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Samagotchi
4
4
  module Guardrails
5
- # What failed to load at Engine start: hooks, bundle hook checksums,
6
- # rule files. A required one makes the gate deny every tool call (a
5
+ # What failed to load at Engine start: config hooks, a required
6
+ # plugin, rule files, an older chi's class-file bundle hooks (which no
7
+ # longer load). A required one makes the gate deny every tool call (a
7
8
  # guardrail that silently vanished is the failure this guards against);
8
9
  # the rest are only announced.
9
10
  class LoadFailures
@@ -153,6 +153,17 @@ module Samagotchi
153
153
  (low == low_dir || low.start_with?(File.join(low_dir, ""))) && folds_case?(dir)
154
154
  end
155
155
 
156
+ # Whether +root+ is the home folder or a folder above it (symlinks
157
+ # resolved on both; false for a blank one). ReviewPaths (a participant
158
+ # turn's project) and MemoryPaths (where a chi project marker is
159
+ # ignored) ask this.
160
+ # @param root [String, nil] @param home [String, nil]
161
+ def home_root?(root, home)
162
+ return false if home.to_s.empty? || root.to_s.empty?
163
+
164
+ under?(real(home.to_s), real(root.to_s))
165
+ end
166
+
156
167
  # Whether +path+ is in +dir+, not +dir+ itself (.under?'s terms).
157
168
  def strictly_under?(path, dir, fold: true)
158
169
  return true if path.start_with?(File.join(dir, ""))
@@ -59,11 +59,7 @@ module Samagotchi
59
59
  # review turn reads nothing there (#refusal: HOME_ROOT); a pair turn
60
60
  # asks the owner before every write (PairBoundary).
61
61
  # @param root [String] @param home [String, nil]
62
- def self.home_root?(root, home)
63
- return false if home.to_s.empty? || root.to_s.empty?
64
-
65
- PathResolve.under?(PathResolve.real(home.to_s), PathResolve.real(root.to_s))
66
- end
62
+ def self.home_root?(root, home) = PathResolve.home_root?(root, home)
67
63
 
68
64
  # @param root [String, nil] the session's project root (the work tree
69
65
  # top of its folder); nil or blank refuses every path
@@ -21,7 +21,7 @@ module Samagotchi
21
21
  # How much of a failed hook's error #plugin_hook_failed shows.
22
22
  HOOK_FAILURE_CHARS = 300
23
23
  # What the notice says the hook now does, by the policy Hooks.wrap gave it
24
- # (a chi.on handler is always :log; a bundle's hook file any of the three).
24
+ # (a chi.on handler :log, or :deny for an on_error: :fail_closed one).
25
25
  HOOK_FAILURE_EFFECTS = {
26
26
  log: "it's skipped, and only the log has its next failures",
27
27
  skip: "it's skipped, and its next failures are silent",
@@ -98,7 +98,7 @@ module Samagotchi
98
98
  nil
99
99
  end
100
100
 
101
- # A bundle hook file or a chi.on handler raised (Hooks.wrap; the log has
101
+ # A chi.on handler raised (Hooks.wrap; the log has
102
102
  # every firing of a :log one). Its first failure on this Engine, so in
103
103
  # this worker, tells the user, as a plugin that failed to load does; then
104
104
  # only the log. One line per label — file, bundle and event — so a
@@ -2,12 +2,11 @@
2
2
 
3
3
  require_relative "hooks/registry"
4
4
  require_relative "hooks/loader"
5
- require_relative "hooks/bundle_loader"
6
5
  require_relative "hooks/stream_watch"
7
6
 
8
7
  module Samagotchi
9
- # Thin wrapper that exposes the Hooks::Registry, Hooks::Loader and
10
- # Hooks::BundleLoader (bundle-owned hook plugins).
8
+ # Thin wrapper that exposes the Hooks::Registry and Hooks::Loader
9
+ # (config.yml's hooks; a bundle's hook through its plugin, Plugin::Api#on).
11
10
  #
12
11
  # The Engine holds an instance of Hooks::Registry and provides:
13
12
  # - #register_hook(name, &block) — register a turn-scoped hook