samagotchi 0.148.0 → 0.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -1
- data/README.md +4 -2
- data/docs/architecture.md +3 -3
- data/docs/bundles.md +38 -1
- data/docs/cli.md +76 -24
- data/docs/configuration.md +8 -6
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +2 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +269 -0
- data/docs/internals/web-frontend.md +4 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +10 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +45 -2
- data/docs/system-one.md +17 -1
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -4
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +9 -0
- data/lib/samagotchi/bundles/decision-card/manifest.yml +13 -0
- data/lib/samagotchi/bundles/decision-card/plugin.rb +280 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/config.rb +7 -1
- data/lib/samagotchi/context_note.rb +17 -5
- data/lib/samagotchi/engine.rb +113 -6
- data/lib/samagotchi/engine_peers.rb +11 -0
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/pair_boundary.rb +3 -2
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +6 -2
- data/lib/samagotchi/objectives.rb +395 -0
- data/lib/samagotchi/objectives_audit.rb +115 -0
- data/lib/samagotchi/objectives_check.rb +365 -0
- data/lib/samagotchi/objectives_command.rb +192 -0
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +21 -1
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_commands.rb +15 -0
- data/lib/samagotchi/session_continue.rb +3 -0
- data/lib/samagotchi/session_inbox.rb +14 -6
- data/lib/samagotchi/session_manager.rb +1 -1
- data/lib/samagotchi/steer.rb +24 -4
- data/lib/samagotchi/testing/boundary.rb +2 -0
- data/lib/samagotchi/testing/host.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tool_activity.rb +17 -0
- data/lib/samagotchi/tool_declarations.rb +34 -0
- data/lib/samagotchi/tools/builtin_calls.rb +2 -1
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/objectives_tool.rb +75 -0
- data/lib/samagotchi/tools/peers.rb +3 -0
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/message_display.rb +9 -0
- data/lib/samagotchi/web/participant_view.rb +2 -2
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +30 -9
- data/lib/samagotchi/web/public/commands.js +4 -1
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/objectives.css +80 -0
- data/lib/samagotchi/web/public/objectives_entry.js +22 -0
- data/lib/samagotchi/web/public/objectives_model.js +100 -0
- data/lib/samagotchi/web/public/objectives_moves.js +145 -0
- data/lib/samagotchi/web/public/objectives_strip.js +242 -0
- data/lib/samagotchi/web/public/objectives_wiring.js +62 -0
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +12 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +3 -0
- data/lib/samagotchi/worker_inbound.rb +16 -8
- metadata +22 -1
data/docs/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`, `
|
|
433
|
-
`
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
329
|
+
# @return [Integer] 0 all resolved, CONFLICTS_KEPT kept some edits
|
|
325
330
|
def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
|
|
326
|
-
|
|
327
|
-
|
|
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 "
|
|
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:
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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[:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
588
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|