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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -1
  3. data/README.md +4 -2
  4. data/docs/architecture.md +3 -3
  5. data/docs/bundles.md +38 -1
  6. data/docs/cli.md +76 -24
  7. data/docs/configuration.md +8 -6
  8. data/docs/guardrails.md +2 -1
  9. data/docs/hooks.md +2 -1
  10. data/docs/internals/background-tasks.md +1 -1
  11. data/docs/internals/bridge.md +8 -0
  12. data/docs/internals/bundles.md +42 -14
  13. data/docs/internals/objectives.md +269 -0
  14. data/docs/internals/web-frontend.md +4 -1
  15. data/docs/internals/web-push.md +2 -2
  16. data/docs/internals/working-set.md +5 -2
  17. data/docs/memory.md +47 -9
  18. data/docs/plugins.md +10 -2
  19. data/docs/releasing.md +1 -1
  20. data/docs/sessions.md +45 -2
  21. data/docs/system-one.md +17 -1
  22. data/docs/testing.md +5 -0
  23. data/lib/samagotchi/atomic_file.rb +3 -0
  24. data/lib/samagotchi/bootstrap_command.rb +5 -2
  25. data/lib/samagotchi/bridge/card_store.rb +52 -7
  26. data/lib/samagotchi/bridge.rb +1 -1
  27. data/lib/samagotchi/bundle_command.rb +91 -68
  28. data/lib/samagotchi/bundler_env.rb +24 -0
  29. data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
  30. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  31. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
  32. data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
  33. data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
  34. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  35. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  36. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
  37. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
  38. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
  39. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
  40. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  41. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  42. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  43. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  44. data/lib/samagotchi/chi_home.rb +4 -2
  45. data/lib/samagotchi/chi_shim.rb +2 -1
  46. data/lib/samagotchi/child_env.rb +10 -3
  47. data/lib/samagotchi/config.rb +7 -1
  48. data/lib/samagotchi/context_note.rb +17 -5
  49. data/lib/samagotchi/engine.rb +113 -6
  50. data/lib/samagotchi/engine_peers.rb +11 -0
  51. data/lib/samagotchi/git_env.rb +43 -0
  52. data/lib/samagotchi/guardrails/context.rb +2 -0
  53. data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
  54. data/lib/samagotchi/guardrails/targets.rb +4 -1
  55. data/lib/samagotchi/llm/chat_loop.rb +1 -1
  56. data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
  57. data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
  58. data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
  59. data/lib/samagotchi/memory_bundle/installer.rb +161 -52
  60. data/lib/samagotchi/memory_bundle/listing.rb +4 -3
  61. data/lib/samagotchi/memory_bundle/merger.rb +209 -8
  62. data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
  63. data/lib/samagotchi/memory_bundle/profile.rb +7 -3
  64. data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
  65. data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
  66. data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
  67. data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
  68. data/lib/samagotchi/memory_bundle/source.rb +3 -3
  69. data/lib/samagotchi/memory_bundle/status.rb +21 -3
  70. data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
  71. data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
  72. data/lib/samagotchi/memory_bundle/trash.rb +58 -2
  73. data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
  74. data/lib/samagotchi/note_command.rb +1 -1
  75. data/lib/samagotchi/note_delivery.rb +6 -2
  76. data/lib/samagotchi/objectives.rb +395 -0
  77. data/lib/samagotchi/objectives_audit.rb +115 -0
  78. data/lib/samagotchi/objectives_check.rb +365 -0
  79. data/lib/samagotchi/objectives_command.rb +192 -0
  80. data/lib/samagotchi/plugin/api.rb +2 -1
  81. data/lib/samagotchi/plugin/context.rb +21 -1
  82. data/lib/samagotchi/project_env.rb +3 -2
  83. data/lib/samagotchi/self_report.rb +2 -0
  84. data/lib/samagotchi/session_commands.rb +15 -0
  85. data/lib/samagotchi/session_continue.rb +3 -0
  86. data/lib/samagotchi/session_inbox.rb +14 -6
  87. data/lib/samagotchi/session_manager.rb +1 -1
  88. data/lib/samagotchi/steer.rb +24 -4
  89. data/lib/samagotchi/testing/boundary.rb +2 -0
  90. data/lib/samagotchi/testing/host.rb +1 -0
  91. data/lib/samagotchi/testing/minitest.rb +3 -0
  92. data/lib/samagotchi/testing/rspec.rb +3 -0
  93. data/lib/samagotchi/testing/sandbox.rb +3 -1
  94. data/lib/samagotchi/tool_activity.rb +17 -0
  95. data/lib/samagotchi/tool_declarations.rb +34 -0
  96. data/lib/samagotchi/tools/builtin_calls.rb +2 -1
  97. data/lib/samagotchi/tools/builtins.rb +4 -1
  98. data/lib/samagotchi/tools/delegate.rb +2 -1
  99. data/lib/samagotchi/tools/objectives_tool.rb +75 -0
  100. data/lib/samagotchi/tools/peers.rb +3 -0
  101. data/lib/samagotchi/tools/task_runtime.rb +13 -10
  102. data/lib/samagotchi/turn_flow.rb +3 -1
  103. data/lib/samagotchi/update_command.rb +76 -28
  104. data/lib/samagotchi/update_hint.rb +4 -1
  105. data/lib/samagotchi/version.rb +1 -1
  106. data/lib/samagotchi/web/auto_update.rb +8 -8
  107. data/lib/samagotchi/web/auto_update_record.rb +6 -4
  108. data/lib/samagotchi/web/message_display.rb +9 -0
  109. data/lib/samagotchi/web/participant_view.rb +2 -2
  110. data/lib/samagotchi/web/public/app.js +15 -3
  111. data/lib/samagotchi/web/public/block_open.js +13 -0
  112. data/lib/samagotchi/web/public/bubbles.js +4 -2
  113. data/lib/samagotchi/web/public/cards_place.js +30 -9
  114. data/lib/samagotchi/web/public/commands.js +4 -1
  115. data/lib/samagotchi/web/public/details_motion.js +2 -1
  116. data/lib/samagotchi/web/public/index.html +20 -14
  117. data/lib/samagotchi/web/public/objectives.css +80 -0
  118. data/lib/samagotchi/web/public/objectives_entry.js +22 -0
  119. data/lib/samagotchi/web/public/objectives_model.js +100 -0
  120. data/lib/samagotchi/web/public/objectives_moves.js +145 -0
  121. data/lib/samagotchi/web/public/objectives_strip.js +242 -0
  122. data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
  123. data/lib/samagotchi/web/public/stage_model.js +5 -6
  124. data/lib/samagotchi/web/public/stage_view.js +8 -7
  125. data/lib/samagotchi/web/public/stream_handlers.js +6 -1
  126. data/lib/samagotchi/web/public/turn_block.js +34 -19
  127. data/lib/samagotchi/web/public/turn_html.js +4 -1
  128. data/lib/samagotchi/web/public/turn_model.js +1 -1
  129. data/lib/samagotchi/web/public/update_sheet.js +8 -3
  130. data/lib/samagotchi/web/public/working_set_view.js +10 -7
  131. data/lib/samagotchi/web/push/notifier.rb +7 -9
  132. data/lib/samagotchi/web/session_hub.rb +2 -1
  133. data/lib/samagotchi/web/session_show.rb +12 -1
  134. data/lib/samagotchi/web/update_ops.rb +2 -1
  135. data/lib/samagotchi/worker.rb +3 -0
  136. data/lib/samagotchi/worker_inbound.rb +16 -8
  137. metadata +22 -1
data/docs/sessions.md CHANGED
@@ -192,6 +192,49 @@ OSA
192
192
  [ -n "$picked" ] && pbpaste | "$chi" note --source clipboard $(print -r -- "$picked" | cut -f1)
193
193
  ```
194
194
 
195
+ ## Objectives
196
+
197
+ `/objectives set A | B` gives a session a short list of what it is to achieve, and `/objectives` shows it. The list goes
198
+ into the conversation as a note under its own frame (`[OBJECTIVES, set by the user. …]`), which asks the model to keep
199
+ work on it, to say when something falls outside it and not to change it itself. A session with no list behaves as it
200
+ would without the feature; its request only carries the `objectives` tool below (declared on `full`, named in the
201
+ not-loaded line elsewhere). Each item has an id (`O1`, never reused), a status (active, done, dropped) and
202
+ optionally a done-when hint, which nothing enforces.
203
+
204
+ An edit waits for a moment between turns: it lands within seconds in an idle session, and at the turn's end during one.
205
+ Several edits made during a turn go in as one list. A continue copies the list to the next link, as a second note after
206
+ the carry note; a review link doesn't. `!rollback` keeps it. A chi REPL's session (`--no-shared`) refuses an edit, as it
207
+ refuses a note. See [cli.md](cli.md#session-objectives) for the commands and
208
+ [internals/objectives.md](internals/objectives.md) for the mechanism.
209
+
210
+ The model can record a **move** with the `objectives` tool: a line on what it did or found for an objective, with
211
+ optional evidence. A move shows as the tool's row in the turn, and `/objectives` shows each objective's latest one under
212
+ it. It is information only: only you change the list or a status, and it never approves anything. A move costs one
213
+ generation when it's the step's only call. On a kit that doesn't declare the tool, `/toolkit add objectives` declares it
214
+ (the model can also reach it with `call_tool`). A participant's turn is denied it.
215
+
216
+ The model can also **propose** a change with that tool (add, reword, done, drop). You see a card when the turn is over:
217
+ Apply makes the edit, once, Dismiss leaves the list, and ignoring it changes nothing. A turn leaves at most 3. A proposed
218
+ reword is refused when you changed that objective since. The model never changes the list by itself.
219
+
220
+ **Check this** (`/objectives check O2`, or the button in the web's list, for an objective with a move) asks for an independent read of
221
+ the latest move: one side request, on your request only, on the session's model or `objectives.check_model`. It sees the
222
+ claim, what people and notes said, what tools returned and the agent's visible answers (never its thinking), and no file. The agent's
223
+ words settle what it said or asked, never that its work was done: "I ran the tests" with no run behind it isn't `confirmed`. What the record doesn't
224
+ settle comes back `unclear`. **That is up to 24,000 characters of raw tool output, messages and the agent's visible answers, which with a hosted `check_model` leaves the machine on one tap, even from a local session.** chi's redaction takes out known key shapes and `token=`-style values first, by pattern only, so another secret in them still goes. The verdict (confirmed, refuted, unclear) and its reasons come as a card and under the
225
+ objective. It is information only: it changes no status, approves nothing, and isn't in the conversation or the model's next request.
226
+ A second tap while one runs asks nothing. Its cost is in the session's side line. Mid-turn it runs beside the turn; on a one-slot
227
+ local server it answers after the running request. A participant can't reach it. See
228
+ [internals/objectives.md](internals/objectives.md#check-this).
229
+
230
+ **In the web** a session with a list has a strip above the conversation: its objectives and how many are done (a phone
231
+ shows one line, the one being worked on). A tap opens the list, where you add, mark done, drop, undo and tap a text to
232
+ reword it, with no message and no turn. Mid-turn the strip has the change and says it applies after this turn. Each list
233
+ in the conversation shows as pills, or as one line of what you changed. A turn's moves show as chips above its answer;
234
+ Not yet on one (or in the list, for an objective with a move) sends `Not yet on O2 (…).` as a normal message. With the
235
+ [decision-card](bundles.md#the-decision-card-bundle) bundle, the newest long answer offers its Short version in a card,
236
+ which enters no history. A session with no list shows none of this. A participant doesn't see the list.
237
+
195
238
  ## Notes a turn leaves for the model
196
239
 
197
240
  A turn that ends without an answer leaves one line for the model to read on its next turn, as a tail
@@ -429,8 +472,8 @@ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while
429
472
  included, asks you first; a read-only mention (`find . -name mise.toml`) doesn't. `.envrc` and mise's local files
430
473
  (`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
431
474
  `.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
432
- - **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
433
- `BUNDLE_BIN_PATH` or `BUNDLER_VERSION` from the project's mise env: it runs without `RUBYLIB` and Bundler's
475
+ - **What tools still drop**: every `execute`, task, `!cmd`, context source and MCP server takes no `RUBYOPT`, `RUBYLIB`, or what `bundle exec` sets
476
+ (`BUNDLE_GEMFILE`, `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP`: `BundlerEnv`) from the project's mise env: it runs without `RUBYLIB` and Bundler's
434
477
  variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
435
478
  - **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
436
479
  a dir holding only a `chi` script that boots the home (rewritten at each worker start when the home changed; it
data/docs/system-one.md CHANGED
@@ -3,7 +3,7 @@
3
3
  System 1 is the cheap judgment model chi asks small questions: approvals
4
4
  ([guardrails.md](guardrails.md#system-1-experimental)), toolkit suggestions
5
5
  ([configuration.md](configuration.md#toolkits)), context candidates
6
- ([context.md](context.md)) and bundles' own ([plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge)). Each use keeps its own mode, gates and log. What they share is
6
+ ([context.md](context.md)), the objectives' audit ([below](#objectives-in-audit)) and bundles' own ([plugins.md](plugins.md#judgments-chijudgment-and-ctxjudge)). Each use keeps its own mode, gates and log. What they share is
7
7
  one host (`system_one.host`, `.model`, `.timeout_ms`) and one layer that saves every ask, so a later comparison or fine-tune has what
8
8
  was sent and what came of it. The runs don't leave the machine; a hosted System 1 host sees each state as it is asked.
9
9
 
@@ -20,6 +20,7 @@ one warn and never changes the answer. A use that needs the run's id at once pas
20
20
  | `approvals.allow` | `GuardrailWiring#system_one_judge` | gates, acted (the judgment line's decision), labels |
21
21
  | `toolkit.suggest` | `ToolkitSuggest.judge` | acted (the log's `did`), no labels |
22
22
  | `context.candidate` | `ContextJudge.ask` | labels |
23
+ | `objectives.incoming`, `objectives.outgoing` | `ObjectivesAudit` | none: nothing acts on them |
23
24
  | `<bundle>.<name>` | `SystemOne::Judgments` (`ctx.judge`, `ctx.judge_later`) | the bundle's: labels, acted |
24
25
 
25
26
  Not saved: a toolkit warm-up (it goes straight to the backend), `chi guardrails replay`,
@@ -41,6 +42,21 @@ seam is common. A bundle's `judge_later` runs on the Engine's `SideJudgments` un
41
42
  at shutdown covers it, and a run cut by that one-second limit is lost. Its asks stay out of the session's spend, as all of System 1's do, and no
42
43
  cost cap covers them on a hosted host.
43
44
 
45
+ ## Objectives in audit
46
+
47
+ `objectives.system_one: audit` asks whether a message may affect an objective: one yes/no (`affects`) per active objective, each ask
48
+ one request and one run. It is off by default, and asks only with a list that has an active objective, a `system_one.host` and saving on
49
+ (`system_one.runs`). **Mode audit only:** there is no `on`. Nothing reads an answer: no card, no wake, no push, nothing in a prompt, and no turn. A
50
+ per-message judge on an objective didn't work as a gate on the pod (AUC about .5 to .66), so these runs are there to look at, with `chi judgments report objectives.incoming`.
51
+
52
+ - `objectives.incoming`: a person's message of the owner's of 40 characters or more at its turn's start (`TurnOrigin#human?`: not a reminder's or a delegate's prompt), not a `/` or `!` line, and a note going
53
+ into the conversation (not the list's own, nor a continue's carry note: `Objectives.outside_note?`). A steer isn't asked about.
54
+ - `objectives.outgoing`: a finished answer of the owner's turn of 400 characters or more, not a cancelled or exhausted one.
55
+ - The state: `objective` (its text and done-when, no id or status) and `message` (2000 characters: a message's head, an answer's tail).
56
+ **That text goes to the System 1 host**, a hosted one too, and into the run. A participant's turn, a review session and a scratch session are never asked about.
57
+ - At most 6 objectives per message. Each direction has one ask running: what comes meanwhile is a run with `asked: false`, `skipped: busy`.
58
+ - The asks run on `SideJudgments`' threads beside the turn, so a turn doesn't wait; on a one-slot server shared with the session's model, a request can still queue behind or ahead of them. They share `/v1/systemone` with approvals: on a slow one-slot host an approval can wait behind up to 2 in-flight asks and time out to the user. A failed ask is a run with its `failure` and one log line (`system_one_failed`), and a `--no-shared` REPL's stderr shows that line as for any System 1 ask.
59
+
44
60
  ## The store
45
61
 
46
62
  `<state dir>/system_one/runs/<judgment>/YYYY-MM-DD.jsonl` (`SystemOne::RunsStore`): one file per UTC day,
data/docs/testing.md CHANGED
@@ -40,6 +40,11 @@ that way locally: `npx playwright run-server --port 3333`, then `PW_TEST_CONNECT
40
40
 
41
41
  No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
42
42
  clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
43
+ It also clears what tells git which repo to act on (`GIT_DIR`, `GIT_WORK_TREE`, the rest of
44
+ `git rev-parse --local-env-vars`: `GitEnv`), first thing in `spec_helper.rb` and as the bundle test kit's
45
+ adapters load, so a run from `git rebase --exec`, `bisect run` or a hook can't reach the real repo through a spec's git.
46
+ `npm run e2e` clears the same list as `spec/e2e/support/env.js` loads, asked of git. chi's own git calls on a folder chi
47
+ chose spawn with `GitEnv::UNSET`, in any run.
43
48
  `SAMAGOTCHI_TAILSCALE_CLI` points at a file that does not exist (`spec_helper.rb`), so no spec reaches a real `tailscale`; a
44
49
  `chi web --tunnel tailscale` spec writes a fake one and sets the variable.
45
50
 
@@ -18,6 +18,8 @@ module Samagotchi
18
18
  # @param content [String] written as-is, byte for byte
19
19
  # @param perm [Integer, nil] the new file's mode; nil = a fresh file's
20
20
  # (0666 less the umask), like File.write
21
+ # @yield just before the rename, when given: a last check of what is being
22
+ # replaced; raising leaves +path+ as it was
21
23
  # @return [String] +path+
22
24
  def write(path, content, perm: nil)
23
25
  tmp = "#{path}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
@@ -25,6 +27,7 @@ module Samagotchi
25
27
  io.write(content)
26
28
  end
27
29
  File.chmod(perm, tmp) if perm
30
+ yield if block_given?
28
31
  File.rename(tmp, path)
29
32
  path
30
33
  ensure
@@ -477,11 +477,14 @@ module Samagotchi
477
477
 
478
478
  def system_bundle_line(result, dry_run)
479
479
  version = "v#{result.to || result.from}"
480
+ standing = MemoryBundle::FileStanding.clauses(MemoryBundle::SystemBundle::BUNDLE_NAME, nil, merged: Array(result.merged),
481
+ behind: Array(result.behind), dry_run: dry_run).join("; ")
480
482
  text = case result.status
481
483
  when :installed then dry_run ? "would install #{version}" : "#{version} installed"
482
- when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}"
484
+ when :updated then "#{result.from} → #{result.to} #{dry_run ? "would update" : "updated"}#{" (#{standing})" unless standing.empty?}"
483
485
  when :restored then "#{version} #{dry_run ? "would restore" : "restored"} missing files"
484
- when :up_to_date then "#{version} up to date"
486
+ when :merged then "#{version} #{standing}"
487
+ when :up_to_date then "#{version} #{standing.empty? ? "up to date" : standing}"
485
488
  when :newer_installed then "v#{result.from} left as is (newer than this chi's v#{result.to})"
486
489
  when :skipped then "skipped (#{result.error})"
487
490
  else "failed (#{result.error})"
@@ -28,6 +28,10 @@ module Samagotchi
28
28
  # isn't the turn's but comes while one runs (an anytime command's, such
29
29
  # as /btw) goes after that turn: its prompt is already in the history.
30
30
  # A failed turn leaves no prompt, so its cards stay before the next one.
31
+ # A !rollback (:turn_rolled_back) takes a kept turn's prompt out again,
32
+ # and its count with it. Of a turn that left nothing, its cards and hook
33
+ # notices stay as between-turns ones where it was, without their
34
+ # actions; its other rows (no step to sit in) go.
31
35
  #
32
36
  # Notices (hook notices, load warnings, "asking again" rows), ✂ rows
33
37
  # and cards (with questions) are capped apart, +capacity+ each, so a
@@ -50,6 +54,15 @@ module Samagotchi
50
54
  # to a turn no worker runs any more (check-in's Nudge would steer the
51
55
  # new worker's turn). A card shown between turns (github-pr's offer,
52
56
  # btw, coordinator) keeps its buttons.
57
+ #
58
+ # A turn its worker was killed in never ended here, yet the session keeps
59
+ # its prompt (TurnCheckpoint, written before :turn_started) and it ends
60
+ # canceled. So +killed_turn_stays+ (a worker's) saves that a turn runs, and
61
+ # a store started from such a file counts that turn. A turn that ended
62
+ # (a graceful stop's cancel too) saved that it no longer runs. Started
63
+ # from the same file again, the count is the same: nothing is written
64
+ # until the next event. A REPL keeps no running turn in the session: a kill
65
+ # takes the prompt along, so its store saves no such thing.
53
66
  class CardStore
54
67
  CAPACITY = 20
55
68
  NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
@@ -59,8 +72,8 @@ module Samagotchi
59
72
  FILE = "cards.json"
60
73
  # The events after which the file is written again: the ones that
61
74
  # change an entry or the turn count.
62
- SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
63
- question_cancelled question_relay turn_failed] + Events::TURN_KEPT).freeze
75
+ SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
76
+ question_cancelled question_relay turn_failed turn_rolled_back] + Events::TURN_KEPT).freeze
64
77
 
65
78
  # What a session's saved file lists, as a store started from it lists
66
79
  # (no turn running): the web's cards for a session no worker runs.
@@ -72,9 +85,12 @@ module Samagotchi
72
85
 
73
86
  # @param path [String, nil] the file the entries are saved in and
74
87
  # started from; nil keeps them in memory only
75
- def initialize(capacity: CAPACITY, path: nil)
88
+ # @param killed_turn_stays [Boolean] the session keeps a running turn's
89
+ # prompt on disk (a worker's TurnCheckpoint): save that a turn runs
90
+ def initialize(capacity: CAPACITY, path: nil, killed_turn_stays: false)
76
91
  @capacity = capacity
77
92
  @path = path
93
+ @killed_turn_stays = killed_turn_stays
78
94
  @mutex = Mutex.new
79
95
  @entries = []
80
96
  @turns_done = 0
@@ -122,21 +138,26 @@ module Samagotchi
122
138
  return if entries.empty? && !File.exist?(@path)
123
139
 
124
140
  FileUtils.mkdir_p(File.dirname(@path))
125
- AtomicFile.write(@path, JSON.generate({ "version" => 1, "turns" => @turns_done, "entries" => entries }))
141
+ saved = { "version" => 1, "turns" => @turns_done, "entries" => entries }
142
+ saved["running"] = true if @killed_turn_stays && @running
143
+ AtomicFile.write(@path, JSON.generate(saved))
126
144
  rescue StandardError
127
145
  nil # a card that can't be saved still shows live
128
146
  end
129
147
 
130
148
  # The entries an earlier worker saved (a missing or broken file:
131
149
  # none), their types as symbols again; a card that came during that
132
- # worker's last turn stays where it was, as after a failed turn.
150
+ # worker's last turn stays where it was, as after a failed turn, or
151
+ # after that turn when the worker was killed in it. A file from before
152
+ # "running" has none: no turn counted.
133
153
  def seed
134
154
  return unless @path && File.file?(@path)
135
155
 
136
156
  data = JSON.parse(File.read(@path), symbolize_names: true)
137
157
  return unless data.is_a?(Hash) && data[:turns].is_a?(Integer) && data[:entries].is_a?(Array)
138
158
 
139
- @turns_done = data[:turns]
159
+ killed_in_turn = data[:running] == true
160
+ @turns_done = data[:turns] + (killed_in_turn ? 1 : 0)
140
161
  @entries = data[:entries].filter_map do |entry|
141
162
  next unless entry.is_a?(Hash) && entry[:type].is_a?(String) && entry[:turns].is_a?(Integer)
142
163
 
@@ -144,7 +165,7 @@ module Samagotchi
144
165
  entry = entry.merge(actions: []) if entry[:type] == :card && entry[:in_turn] && entry.key?(:actions)
145
166
  entry unless entry[:type] == :guardrail_warning || open_question?(entry)
146
167
  end
147
- settle_during(after_turn: false)
168
+ settle_during(after_turn: killed_in_turn)
148
169
  rescue StandardError
149
170
  @turns_done = 0
150
171
  @entries = []
@@ -169,6 +190,8 @@ module Samagotchi
169
190
  kept = Events.kept_turn?(event)
170
191
  @turns_done += 1 if kept
171
192
  settle_during(after_turn: kept)
193
+ unstep_turn_left_nothing unless kept
194
+ when :turn_rolled_back then take_turn_back
172
195
  when :card then add_card(event)
173
196
  when :hook_notice then add_notice(event)
174
197
  # What failed to load (guardrails, plugins): before the first turn
@@ -210,6 +233,28 @@ module Samagotchi
210
233
  end
211
234
  end
212
235
 
236
+ # The last kept turn's prompt left the conversation. An entry from
237
+ # after it (a card shown before the !rollback) stays before the next.
238
+ def take_turn_back
239
+ @turns_done = [@turns_done - 1, 0].max
240
+ unstep_turn_left_nothing
241
+ @entries.each { |entry| entry[:turns] = [entry[:turns], @turns_done].min }
242
+ end
243
+
244
+ # The turn that would be the next one's place has no prompt in the
245
+ # history: what it showed isn't a row of a turn any more (a later
246
+ # turn would read it as its own, +current+).
247
+ def unstep_turn_left_nothing
248
+ @entries = @entries.filter_map do |entry|
249
+ next entry unless entry[:in_turn] && entry[:turns] == @turns_done
250
+
251
+ case entry[:type]
252
+ when :card then entry.merge(in_turn: false, **(entry.key?(:actions) ? { actions: [] } : {}))
253
+ when :hook_notice then entry.except(:iteration, :calls).merge(in_turn: false)
254
+ end
255
+ end
256
+ end
257
+
213
258
  def add_notice(event)
214
259
  notice = event.slice(:type, :hook, :text, :level, :fallback_for)
215
260
  if event[:between_turns] || !@running
@@ -136,7 +136,7 @@ module Samagotchi
136
136
  @accumulator = TurnAccumulator.new
137
137
  # Saved in the session's folder: a later worker, and the web without
138
138
  # one, show the cards where they were.
139
- @cards = CardStore.new(path: File.join(session_dir, CardStore::FILE))
139
+ @cards = CardStore.new(path: File.join(session_dir, CardStore::FILE), killed_turn_stays: true)
140
140
  @pending_card = PendingCard.new(session_dir)
141
141
  @heartbeat_interval = heartbeat_interval
142
142
  @epoch = SecureRandom.hex(4)
@@ -15,17 +15,19 @@ module Samagotchi
15
15
  install <source> [--scope system|project] [--force]
16
16
  Install a memory bundle from a directory, zip, tar archive, or git URL.
17
17
  Source is the path/URL to the bundle (dir/zip/tar.gz/git).
18
- --force overwrites existing entries; default skips them.
18
+ --force overwrites existing entries (one that differs is copied to the trash first); default skips them.
19
19
 
20
20
  upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
21
- Upgrade a bundle (3-way merge: auto-merge if not edited, conflict otherwise).
21
+ Upgrade a bundle (3-way: a file you didn't edit updates; one you edited takes a text
22
+ merge, the old file in the bundle trash; a real conflict leaves it as it is).
22
23
  --dry-run shows what would change. --agent launches interactive session on conflict.
23
24
 
24
25
  uninstall <bundle> [--scope system|project] [--force]
25
- Remove a bundle and its index entries (--force if locally edited).
26
+ Remove a bundle and its index entries (--force moves a locally edited file to the trash).
26
27
 
27
28
  status [<bundle>]
28
- Show provenance + modification status for bundles.
29
+ Show provenance and each file's status for bundles. issues= counts what
30
+ needs you: a file that stays behind, is missing or can't be read, not one you edited.
29
31
 
30
32
  diff <bundle> [file]
31
33
  Show diffs between base/current/incoming for a bundle.
@@ -66,7 +68,7 @@ module Samagotchi
66
68
  install <source> [--scope system|project] [--force]
67
69
  Install a memory bundle from a directory, zip, tar archive, or git URL,
68
70
  or a bundle shipped with chi by name (see: chi bundle list).
69
- --force overwrites existing entries; default skips them.
71
+ --force overwrites existing entries (one that differs is copied to the trash first); default skips them.
70
72
  install --dev PATH
71
73
  Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
72
74
  are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
@@ -231,6 +233,9 @@ module Samagotchi
231
233
  run_installer(installer, failed: "Upgrade") do |manifest|
232
234
  print_hooks_and_plugin(manifest, plugin: !dry_run)
233
235
  next dry_run_done if dry_run
236
+
237
+ # Its summary said so, to someone reading.
238
+ Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).clear_merge_notes([], files: installer.merged_files)
234
239
  next upgrade_conflicts(installer, bundle_name, expanded_source, agent) if installer.conflicts.any? && !force
235
240
 
236
241
  print_provenance(bundle_name, manifest)
@@ -321,42 +326,64 @@ module Samagotchi
321
326
  # (scripts already read it so): not CLI::Exit::USAGE by meaning.
322
327
  CONFLICTS_KEPT = 2
323
328
 
324
- # @return [Integer] 0 resolved, CONFLICTS_KEPT kept the edits
329
+ # @return [Integer] 0 all resolved, CONFLICTS_KEPT kept some edits
325
330
  def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
326
- @stdout.puts "\n#{installer.conflicts.size} conflict(s) need resolution."
327
- installer.conflicts.each { |k, _| @stdout.puts " conflict: #{k}" }
331
+ standing = Samagotchi::MemoryBundle::FileStanding
332
+ @stdout.puts "\n#{standing.stay_behind(installer.behind.size)}: #{installer.behind.map(&:file).join(", ")}"
328
333
  launch = if agent.nil? && @stdin.tty?
329
- @stdout.print "Conflicts detected — launch interactive agent to resolve? [y/N] "
334
+ @stdout.print "Launch an interactive agent to merge them? [y/N] "
330
335
  ans = begin; @stdin.gets; rescue StandardError => _e; nil; end
331
336
  ans && ans.strip.downcase.start_with?("y")
332
337
  elsif agent.nil?
333
- @stdout.puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
338
+ @stdout.puts "Non-interactive terminal: the rest is upgraded. #{standing::FORCE_TAKES}; --agent in a TTY merges."
334
339
  return CONFLICTS_KEPT
335
340
  else
336
341
  agent
337
342
  end
338
343
  unless launch
339
- @stdout.puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
344
+ @stdout.puts "The rest is upgraded. #{standing.diff_hint(bundle_name, "FILE")} shows the base; #{standing::FORCE_TAKES}."
340
345
  return CONFLICTS_KEPT
341
346
  end
342
347
 
343
348
  prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
344
- @stdout.puts "Launching interactive session for conflict resolution… (/exit when done)"
349
+ before = installer.conflicts.to_h { |key, info| [key, Samagotchi::MemoryBundle::Merger.digest(info[:current])] }
350
+ @stdout.puts "Launching an interactive session to merge them… (/exit when done)"
345
351
  require "samagotchi/terminal_ui"
346
352
  Samagotchi::TerminalUI.new(prompt: prompt).run
347
- # The installer already recorded the upgrade (conflicted files kept
348
- # their old base); the resolved files now start from the bundle's
349
- # version.
350
- Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
351
- @stdout.puts "Provenance updated after interactive resolution."
352
- @stdout.puts "Upgrade resolved interactively."
353
- 0
353
+ settle_conflicts(installer.conflicts, before, bundle_name)
354
354
  ensure
355
355
  # An owned source (git/zip/tar) was kept for the incoming files above;
356
356
  # the agent step is done with them now.
357
357
  installer.cleanup_source!
358
358
  end
359
359
 
360
+ # After the agent step. The installer already recorded the upgrade
361
+ # (conflicted files kept their old base); a file the step changed, with
362
+ # no conflict markers left, now starts from the bundle's version. Any
363
+ # other stays a conflict: moving its base would turn a visible miss into
364
+ # a kept edit the next upgrade says nothing about.
365
+ # @param before [Hash{String => String, nil}] each file's digest before the step
366
+ # @return [Integer] 0 all resolved, CONFLICTS_KEPT some still behind
367
+ def settle_conflicts(conflicts, before, bundle_name)
368
+ merger = Samagotchi::MemoryBundle::Merger
369
+ untouched, changed = conflicts.partition { |key, info| merger.digest(info[:current]) == before[key] }
370
+ marked, clean = changed.partition { |_, info| merger.markers?(info[:current]) }
371
+ settled = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(clean.to_h)
372
+ gone = clean.map(&:first) - settled
373
+ standing = Samagotchi::MemoryBundle::FileStanding
374
+ untouched.each { |key, _| @stdout.puts standing.still_behind(:untouched, key) }
375
+ marked.each { |key, _| @stdout.puts standing.still_behind(:markers, key) }
376
+ gone.each { |key| @stdout.puts standing.still_behind(:incoming_gone, key) }
377
+ if untouched.empty? && marked.empty? && gone.empty?
378
+ @stdout.puts "Provenance updated after interactive resolution."
379
+ @stdout.puts "Upgrade resolved interactively."
380
+ return 0
381
+ end
382
+
383
+ @stdout.puts "Resolved: #{settled.join(", ")}" unless settled.empty?
384
+ CONFLICTS_KEPT
385
+ end
386
+
360
387
  def uninstall(rest)
361
388
  parsed = parse_flags(UNINSTALL_FLAGS, rest, "uninstall", help: UNINSTALL_HELP)
362
389
  return parsed if parsed.is_a?(Integer)
@@ -377,6 +404,7 @@ module Samagotchi
377
404
  uninstaller.run
378
405
  @stdout.puts "Uninstalled bundle '#{bundle_name}'"
379
406
  puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
407
+ @stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
380
408
  @stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
381
409
  hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
382
410
  @stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
@@ -402,6 +430,7 @@ module Samagotchi
402
430
  result.skipped.each { |member, why| lines << "skipped #{member}: #{why}" }
403
431
  result.failed.each { |member, why| lines << "failed #{member}: #{why}" }
404
432
  lines.concat(result.kept_own_lines)
433
+ lines.concat(result.trash_copies)
405
434
  lines << "nothing new to install" if lines.empty?
406
435
  lines.each { |line| @stdout.puts " #{line}" }
407
436
  @stdout.puts "(dry-run: no changes written)" if dry_run
@@ -453,16 +482,21 @@ module Samagotchi
453
482
  else
454
483
  @stdout.puts "Target: #{st.target_dir}"
455
484
  end
485
+ standing = Samagotchi::MemoryBundle::FileStanding
456
486
  st.files.each do |k, info|
457
487
  next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
458
488
 
459
489
  mods = []
460
- mods << "conflict (kept your edits over v#{st.provenance.version}: chi bundle diff #{name} #{k})" if info[:conflict]
461
490
  mods << "modified" if info[:modified]
462
491
  mods << "missing" if info[:missing]
492
+ mods << "unreadable" if info[:unreadable]
463
493
  mods << "no-index" unless info[:index_present] || info[:overlay]
464
- label = mods.empty? ? "ok" : mods.join(",")
494
+ label = mods.join(",")
495
+ label = "ok" if mods.empty? && !info[:behind]
465
496
  label += " (model overlay)" if info[:overlay]
497
+ behind = info[:behind] && "#{info[:behind].state} (#{standing.diff_hint(name, k)})"
498
+ label = [label, behind].reject { |part| part.nil? || part.empty? }.join("; ")
499
+ label += "; #{info[:merge_note].line}#{" (old copy in #{info[:merge_note].trash})" if info[:merge_note].trash}" if info[:merge_note]
466
500
  @stdout.puts " #{k}: #{label}"
467
501
  end
468
502
  # Hooks
@@ -499,7 +533,7 @@ module Samagotchi
499
533
  next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
500
534
 
501
535
  st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
502
- mods = st.files.values.count { |v| v[:modified] || v[:missing] }
536
+ mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
503
537
  hooks_count = st.provenance.hooks.size
504
538
  hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
505
539
  plugin = st.plugin
@@ -543,77 +577,62 @@ module Samagotchi
543
577
  rescue ArgumentError => e
544
578
  @stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
545
579
  end
546
- files = record.files
547
580
  hooks = record.hooks
548
581
  plugin_path = prov.plugin_path(record)
549
582
  plugin_file = plugin_path && File.basename(plugin_path)
550
- show_plugin = lambda do
551
- @stdout.puts "=== plugin/#{plugin_file} ==="
552
- @stdout.puts "--- base (provenance) ---"
553
- base = prov.plugin_base_path(plugin_file)
554
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
555
- @stdout.puts "--- current (on-disk) ---"
556
- @stdout.puts File.exist?(plugin_path) ? File.read(plugin_path) : "(missing)"
557
- @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
558
- end
559
583
  if file_arg
560
584
  # Try file first, then hook
561
585
  if record.owns?(file_arg)
562
- base = prov.base_path(file_arg)
563
- cur = File.join(target_dir, file_arg)
564
- @stdout.puts "=== #{file_arg} ==="
565
- @stdout.puts "--- base (provenance) ---"
566
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
567
- @stdout.puts "--- current (on-disk) ---"
568
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
586
+ diff_memory(prov, target_dir, file_arg)
569
587
  elsif hooks.key?(file_arg)
570
- base = prov.base_path(file_arg)
571
- cur = File.join(prov.hooks_dir, file_arg)
572
- @stdout.puts "=== hooks/#{file_arg} ==="
573
- @stdout.puts "--- base (provenance) ---"
574
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
575
- @stdout.puts "--- current (on-disk) ---"
576
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
577
- # Show hook metadata
578
- @stdout.puts hook_metadata_line(hooks[file_arg])
588
+ diff_hook(prov, file_arg, hooks[file_arg])
579
589
  elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
580
- show_plugin.call
590
+ diff_plugin(prov, record, plugin_path)
581
591
  else
582
592
  @stdout.puts "=== #{file_arg} ==="
583
593
  @stdout.puts "(not found in bundle)"
584
594
  end
585
595
  @stdout.puts ""
586
596
  else
587
- files.each_key do |k|
588
- base = prov.base_path(k)
589
- cur = File.join(target_dir, k)
590
- @stdout.puts "=== #{k} ==="
591
- @stdout.puts "--- base (provenance) ---"
592
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
593
- @stdout.puts "--- current (on-disk) ---"
594
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
597
+ record.files.each_key do |key|
598
+ diff_memory(prov, target_dir, key)
595
599
  @stdout.puts ""
596
600
  end
597
- # Hooks diff
598
- hooks.each do |k, hook|
599
- base = prov.base_path(k)
600
- cur = File.join(prov.hooks_dir, k)
601
- @stdout.puts "=== hooks/#{k} ==="
602
- @stdout.puts "--- base (provenance) ---"
603
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
604
- @stdout.puts "--- current (on-disk) ---"
605
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
606
- @stdout.puts hook_metadata_line(hook)
601
+ hooks.each do |key, hook|
602
+ diff_hook(prov, key, hook)
607
603
  @stdout.puts ""
608
604
  end
609
605
  if plugin_file
610
- show_plugin.call
606
+ diff_plugin(prov, record, plugin_path)
611
607
  @stdout.puts ""
612
608
  end
613
609
  end
614
610
  0
615
611
  end
616
612
 
613
+ def diff_memory(prov, target_dir, key)
614
+ print_base_and_current(key, prov.base_path(key), File.join(target_dir, key))
615
+ end
616
+
617
+ def diff_hook(prov, key, hook)
618
+ print_base_and_current("hooks/#{key}", prov.base_path(key), File.join(prov.hooks_dir, key))
619
+ @stdout.puts hook_metadata_line(hook)
620
+ end
621
+
622
+ def diff_plugin(prov, record, plugin_path)
623
+ file = File.basename(plugin_path)
624
+ print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
625
+ @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
626
+ end
627
+
628
+ def print_base_and_current(heading, base, current)
629
+ @stdout.puts "=== #{heading} ==="
630
+ @stdout.puts "--- base (provenance) ---"
631
+ @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
632
+ @stdout.puts "--- current (on-disk) ---"
633
+ @stdout.puts File.exist?(current) ? File.read(current) : "(missing)"
634
+ end
635
+
617
636
  def list
618
637
  listing = Samagotchi::MemoryBundle::Listing
619
638
  shipped = listing.shipped
@@ -634,6 +653,10 @@ module Samagotchi
634
653
  line = " #{b.name.ljust(name_w)} #{"v#{b.version || "?"}".ljust(ver_w)} scope=#{b.scope || "?"} " \
635
654
  "#{b.includes ? profile_members(b.includes, installed) : "files=#{b.files}"} installed=#{b.installed_at || "?"}"
636
655
  line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
656
+ if b.behind&.any?
657
+ count = Samagotchi::MemoryBundle::FileStanding.behind_count(b.behind.size, unsure: b.behind.any?(&:unsure))
658
+ line += " (#{count}: chi bundle status #{b.name})"
659
+ end
637
660
  @stdout.puts line
638
661
  end
639
662
  @stdout.puts " (or all at once: chi update)" if installed.any?(&:upgrade)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # Bundler's variables, in one place. Every chi spawn that must not carry a
5
+ # `bundle exec` around it takes its names from here.
6
+ module BundlerEnv
7
+ # All Bundler's own and its settings (BUNDLE_PATH, BUNDLE_WITHOUT, ...), for
8
+ # what must take none: a pattern's start (ChiShim, ProjectEnv::RESTORE_NEVER).
9
+ PREFIX = "BUNDLE"
10
+ # What `bundle exec` sets for a child (Bundler::SharedHelpers#set_bundle_variables;
11
+ # bundler_env_spec asks it). Without them, and without Bundler's RUBYOPT, a
12
+ # child ruby is free of the bundle; BUNDLER_SETUP on its own isn't: RubyGems
13
+ # requires what it names at boot, and a Gemfile it then finds writes its lock
14
+ # to BUNDLE_LOCKFILE, the bundle's. A project's own settings (BUNDLE_PATH and
15
+ # the like) aren't here: its `bundle` keeps them. Nor is BUNDLER_ORIG_*, which
16
+ # also reaches a tool: a project's `bundle exec` there reads it, for
17
+ # Bundler.original_env and with_unbundled_env, whose children get the PATH and
18
+ # GEM_* chi's own `bundle exec` started from.
19
+ VARS = %w[BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLER_SETUP BUNDLER_VERSION].freeze
20
+ # With what else `bundle exec` steers a child ruby by: what a command that
21
+ # isn't the bundle's runs without (TaskRuntime, ProjectEnv, ChiHome.probe).
22
+ CHILD_RUBY = (%w[RUBYOPT RUBYLIB] + VARS).freeze
23
+ end
24
+ end
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: coordinator
3
- version: 0.7.0
3
+ version: 0.8.0
4
4
  scope: system
5
- description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions) and the coding toolkit for build children (child + be concise)"
5
+ description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions, objectives) and the coding toolkit for build children (child + be concise)"
6
6
  trust_level: reviewed
7
7
  files:
8
8
  skill_coordinator.md:
9
- sha256: sha256:50b6dc3c2eca885133af9c6518964e6fe862194f2fa4556d9ee5374dde42242a
9
+ sha256: sha256:1cac885facd28757aaa0197b28889246f845834c17101d59b5625d1b2b89679a
10
10
  description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, merge under the git-merge approval or ask the user; resume from a handoff_* memory"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:fa5d7f8b01e465d80012e141abcb2ee27cd5cec43e5a74b9721d435be92d3730
13
+ sha256: sha256:84535a86b754f3a12cdb2f2591027ea71bbf9ffb74734b1050899b9a4fde07ac
14
14
  # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
15
15
  # coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
16
16
  # the next chi.
@@ -18,3 +18,5 @@ requires_chi: ">= 0.107.0"
18
18
  # /coordinate review uses ctx.sessions.review (the next chi).
19
19
  # No requires_chi_next for guardrails/merge.yml: its keys are long supported, and first-ever too-new rules would make a
20
20
  # live older worker deny every call. A chi without `chi guardrails rule` gives a usage error, and the skill then asks.
21
+ # Nor for the objectives tool in the kit and the skill: on an older chi the kit's add names nothing (it declares only what
22
+ # is registered), and a call of it comes back as an unknown tool, no harm.