samagotchi 0.86.0 → 0.88.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/bin/chi +39 -2
  4. data/docs/architecture.md +5 -3
  5. data/docs/cli.md +18 -5
  6. data/docs/configuration.md +1 -1
  7. data/docs/guardrails.md +53 -3
  8. data/docs/hooks.md +1 -1
  9. data/docs/plugins.md +1 -1
  10. data/docs/sessions.md +19 -7
  11. data/docs/sub-agent.md +15 -3
  12. data/lib/samagotchi/answer_command.rb +6 -6
  13. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -3
  14. data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
  15. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  16. data/lib/samagotchi/child_reports.rb +100 -7
  17. data/lib/samagotchi/cli/parent_wait.rb +3 -1
  18. data/lib/samagotchi/guardrails/parent_policy.rb +94 -5
  19. data/lib/samagotchi/parent_answers_command.rb +80 -0
  20. data/lib/samagotchi/parent_report.rb +53 -34
  21. data/lib/samagotchi/question_desk.rb +3 -2
  22. data/lib/samagotchi/relay.rb +34 -2
  23. data/lib/samagotchi/relay_desk.rb +13 -0
  24. data/lib/samagotchi/send_command.rb +75 -10
  25. data/lib/samagotchi/session.rb +26 -2
  26. data/lib/samagotchi/session_commands.rb +35 -9
  27. data/lib/samagotchi/session_setup.rb +10 -8
  28. data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -8
  29. data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -5
  30. data/lib/samagotchi/terminal_ui/question_prompt.rb +2 -1
  31. data/lib/samagotchi/terminal_ui.rb +12 -6
  32. data/lib/samagotchi/tools/delegate.rb +4 -1
  33. data/lib/samagotchi/tools/delegate_relay.rb +69 -15
  34. data/lib/samagotchi/tools/delegate_wait.rb +14 -8
  35. data/lib/samagotchi/version.rb +1 -1
  36. data/lib/samagotchi/web/public/notify.js +6 -2
  37. data/lib/samagotchi/web/public/question_card.js +2 -0
  38. data/lib/samagotchi/web/public/question_cards.js +25 -18
  39. data/lib/samagotchi/worker.rb +60 -10
  40. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bd5a2287b8113de9eb91acc1a735e6e132f339dd1569567e7cec6e1bea6637a
4
- data.tar.gz: 2cfb2f0a9cc14bb226e227fcced1ce2efb16268707953423bd1db991070e36d5
3
+ metadata.gz: f395ab65a9e00861ae1dda52090efbbe363946df8098702f14c41cdc665f7bb3
4
+ data.tar.gz: 9f22902c72237fd51481b78f1cc91197fdc474e7f5b816e0a4429d354eb5c0aa
5
5
  SHA512:
6
- metadata.gz: 3d5c8b88e0d4ab6fbebdb80d1705ae2904d6b5ac59b463099c9101f22161a912cbe56fe93da7cdcef672f62cc4162d212325c884f66a49aa27385a9aa71ba160
7
- data.tar.gz: 3a54cc7c7629d902d1857e85b2f6fa43d2fb36805bf85ac733bfa4fba614c7a9488080dd40633c40e94ccae0749fdcce4f05c3fc7888b6e65931cd5cd492477d
6
+ metadata.gz: 2db614ca70a821eb84905eef2d8a8fead4ac4c95b9521cb32417e034fd9f6c923dd97974f25aa2a401d3131fa479ac1911bc8e8f41aa1f383b4e34f925c51078
7
+ data.tar.gz: 0a2625e7ce2e7022628f5a1e14c6277eeab21caf21ae4968e908cfe68735be5c39dea2f638137f96b63a0d0ed0796f60bc7c1d3a4e71db87dc453716012957cc
data/CHANGELOG.md CHANGED
@@ -8,6 +8,35 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.88.0] - 2026-10-19
12
+
13
+ ### Added
14
+
15
+ - A `delegate` child's hook question (the cost cap's Continue/Stop, a bundle's `ctx.ask_user`) now reaches its parent,
16
+ in the background too: reported to the parent's model when it may pick something other than a deny under the
17
+ child's `parent_answers` (so under the default `soft` a background parent may Continue its child's cost cap), else
18
+ relayed to the parent's user as a card, as approvals are.
19
+
20
+ ### Fixed
21
+
22
+ - A session file whose `parent_answers` isn't `hard`, `standard` or `soft` (a hand edit) now gets one warning when
23
+ its worker loads it, instead of being read as `hard` silently.
24
+
25
+ ## [0.87.0] - 2026-10-19
26
+
27
+ ### Added
28
+
29
+ - A session's own `parent_answers`, tighten-only: `chi --parent-answers LEVEL` and `chi send --new --parent-answers
30
+ LEVEL` save it in the new session (kept by respawns, forks and continues; a `delegate` child starts with its
31
+ parent's), and what a parent agent may answer there is the stricter of it and config.yml's
32
+ `guardrails.parent_answers`, in `chi answer`, the worker and the guidance. `/parent-answers` shows and sets it;
33
+ anyone may tighten it, only you loosen it, at the REPL, an attached terminal or the web.
34
+
35
+ ### Fixed
36
+
37
+ - The known-names hook treats `#` URL fragments as token boundaries, allowing exact protected names in URLs while
38
+ still rejecting misspellings before a fragment.
39
+
11
40
  ## [0.86.0] - 2026-10-18
12
41
 
13
42
  ### Added
@@ -3466,7 +3495,9 @@ and long-lived sessions.
3466
3495
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
3467
3496
  hotkey panel that send selected text or the clipboard to your sessions.
3468
3497
 
3469
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.86.0...HEAD
3498
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.88.0...HEAD
3499
+ [0.88.0]: https://github.com/dm1try/samagotchi/compare/v0.87.0...v0.88.0
3500
+ [0.87.0]: https://github.com/dm1try/samagotchi/compare/v0.86.0...v0.87.0
3470
3501
  [0.86.0]: https://github.com/dm1try/samagotchi/compare/v0.85.0...v0.86.0
3471
3502
  [0.85.0]: https://github.com/dm1try/samagotchi/compare/v0.84.0...v0.85.0
3472
3503
  [0.84.0]: https://github.com/dm1try/samagotchi/compare/v0.83.0...v0.84.0
data/bin/chi CHANGED
@@ -82,7 +82,7 @@ ARGV.each do |arg|
82
82
  base = arg.split("=", 2).first
83
83
  # If dashed version is a known flag (registry or hardcoded), reject underscore variant
84
84
  dashed = base.tr("_", "-")
85
- known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
85
+ known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
86
86
  if known.include?(dashed) || known.include?(base)
87
87
  warn "Unknown option: #{arg} (did you mean #{dashed}?)"
88
88
  exit 1
@@ -130,6 +130,11 @@ parser = OptionParser.new do |opts|
130
130
  "saved in the session: --set cost-cap.cap=3)") do |v|
131
131
  (options[:sets] ||= []) << v
132
132
  end
133
+ opts.on("--parent-answers LEVEL", "What a parent agent may answer on this session's questions: " \
134
+ "hard|standard|soft, only stricter than config.yml's guardrails.parent_answers (saved in the " \
135
+ "session; only you loosen it, with /parent-answers in the terminal or the web)") do |v|
136
+ options[:parent_answers] = v
137
+ end
133
138
  opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
134
139
  opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
135
140
  "config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
@@ -234,6 +239,22 @@ if options[:sets] && web
234
239
  warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
235
240
  exit 2
236
241
  end
242
+ # --parent-answers: a level in any case (no abbreviations), else a usage
243
+ # error, as chi send's.
244
+ if options[:parent_answers]
245
+ require "samagotchi/guardrails/parent_policy"
246
+ levels = Samagotchi::Guardrails::ParentPolicy::ORDER
247
+ word = options[:parent_answers].strip.downcase
248
+ unless levels.include?(word)
249
+ warn "Error: --parent-answers takes #{levels.join(", ")}, not #{options[:parent_answers]}"
250
+ exit 2
251
+ end
252
+ options[:parent_answers] = word
253
+ end
254
+ if options[:parent_answers] && web
255
+ warn "Error: --parent-answers is a new session's (chi --parent-answers, chi send --new --parent-answers), not chi web's"
256
+ exit 2
257
+ end
237
258
 
238
259
  # A scratch session is new, runs here and goes when it ends.
239
260
  if scratch
@@ -395,6 +416,20 @@ if options[:sets]
395
416
  options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--set"]
396
417
  end
397
418
 
419
+ # --parent-answers LEVEL: a new session's own guardrails.parent_answers. An
420
+ # existing session's (--attach/--resume) is ignored with the flags' line,
421
+ # never sent as a /parent-answers command: that would loosen it from a
422
+ # command line, which is the user's to do in a UI.
423
+ if options[:parent_answers]
424
+ if options[:attach] || options[:resume]
425
+ options[:toolkit_flags] = (options[:toolkit_flags] || []) + ["--parent-answers"]
426
+ else
427
+ require "samagotchi/guardrails/parent_policy"
428
+ note = Samagotchi::Guardrails::ParentPolicy.capped_note(options[:parent_answers])
429
+ warn "Warning: --parent-answers #{note}" if note
430
+ end
431
+ end
432
+
398
433
  chi_update_hint.call
399
434
  require "samagotchi/launch_mode"
400
435
  launch, launch_note = if scratch
@@ -432,7 +467,8 @@ if launch == :attached
432
467
  appended_prompt_files: options[:appended_prompt_files] || [],
433
468
  toolkit: options[:toolkit_value],
434
469
  toolkit_flags: options[:toolkit_flags] || [],
435
- bundle_settings: options[:bundle_settings] || {})
470
+ bundle_settings: options[:bundle_settings] || {},
471
+ parent_answers: options[:parent_answers])
436
472
  rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
437
473
  warn "Error: #{e.message}"
438
474
  exit 1
@@ -455,6 +491,7 @@ begin
455
491
  toolkit: options[:toolkit_value],
456
492
  toolkit_flags: options[:toolkit_flags] || [],
457
493
  bundle_settings: options[:bundle_settings] || {},
494
+ parent_answers: options[:parent_answers],
458
495
  non_interactive: options[:non_interactive],
459
496
  model_name: options[:model],
460
497
  scratch: scratch
data/docs/architecture.md CHANGED
@@ -296,8 +296,9 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
296
296
  (`chi answer`, a parent agent) the answer is checked by `Guardrails::ParentPolicy` in
297
297
  `QuestionDesk#answer`: the question carries each option's effect (`effects`, set by its asker:
298
298
  `Approval.payload`, `ContinueOffer`, `Relay.card`; derived for an untagged one) and `user_only`,
299
- and an option beyond the worker's `guardrails.parent_answers` is `403 parent_answer_refused`
300
- (with its `reason`), the question still open. `chi answer` makes the same check first.
299
+ and an option beyond the worker's `guardrails.parent_answers` (tightened by the session's own
300
+ `parent_answers`: `ParentPolicy.for_session`) is `403 parent_answer_refused` (with its `reason`),
301
+ the question still open. `chi answer` makes the same check first, from the session file.
301
302
  - `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
302
303
  `200`, or `409` when it is no longer pending.
303
304
  - `POST /session/:id/tasks/stop` — `{task_id}`: stop a background task as the user
@@ -321,7 +322,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
321
322
  pending question's `relayed_to` mark (`QuestionDesk#annotate`, `:question_relay`); `answered`
322
323
  makes the child ask its parent's Bridge for the answer (`RelayVerifier`) and take it only for
323
324
  this child and the question pending now. `200`, `409` no longer pending, `403` a parent agent's
324
- pick beyond the child's own `guardrails.parent_answers` (`parent_answer_refused`), `422 relay_unverified` (the question
325
+ pick beyond the child's own `guardrails.parent_answers` and session `parent_answers` (`parent_answer_refused`; the
326
+ relay card in the parent was held to the parent's), `422 relay_unverified` (the question
325
327
  stays open).
326
328
  - `POST /session/:id/relay/status` — on a parent's worker, `{relay_id}`: what its `RelayDesk`
327
329
  (memory only) holds for that relay (`child_id`, `child_question_id`, `state`, `answer`, `by`);
data/docs/cli.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
20
20
  - `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
21
21
  - `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending — to an idle session whose last turn already ended, that ended turn's reply or end comes back; `--wait --turn T ID` with no message waits for exactly turn T, whether running, queued or last ended; `--format json` includes its `turn_id` when known, for waiting again after exit 3 or 4; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
22
- - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers` (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
22
+ - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
23
23
  - `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
24
24
  - `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
25
25
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
@@ -237,6 +237,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
237
237
  | `--model NAME` | Use this model for the run (overrides the configured default and a resumed session's model). |
238
238
  | `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
239
239
  | `--toolkit NAME`, `--tool NAME`, `--no-tool NAME` | A new session's toolkit, the tools its prompts declare: `full` (every built-in), `lean`, `child`, one of config's `toolkit.kits` or a bundle's (without `--toolkit`: config's `toolkit.default`), with tools declared on top of it (`--tool`) or left out (`--no-tool`); both repeatable, a comma list too. Saved in the session as given; a respawned worker, a fork and a continue keep it. Ignored with `--attach`/`--resume` (one line, as `--memory`); `/toolkit` changes it. Not on `chi web` (config's `toolkit.default` covers its sessions). See [Toolkits](configuration.md#toolkits). |
240
+ | `--parent-answers LEVEL` | A new session's own `guardrails.parent_answers` (`hard`, `standard` or `soft`): what a parent agent may answer on its questions. It may only tighten config.yml's: the stricter of the two applies, and a looser one is saved as given with a warning (`--parent-answers soft is looser than config.yml's guardrails.parent_answers: standard; standard applies`). Saved in the session; a respawned worker, a fork and a continue keep it, a `delegate` child takes its parent's. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. Only you loosen it: `/parent-answers` in the terminal or the web. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
240
241
  | `--set BUNDLE.KEY=VALUE` | A bundle setting for a new session (repeatable; a later one of the same key wins), laid over config.yml's `bundles: <bundle>:` in what the bundle's plugin reads (`ctx.settings`): `--set cost-cap.cap=3`. The value is typed simply: an integer (`3`, `-1`) or a decimal (`2.5`) is a number, `true` is true, `false` and `off` are false, anything else stays the text as given (`010`, `0x10`, `3:30`, `No`, `null`). For cost-cap, `0`, a negative number or `off` is no cap. Only a key the installed bundle's manifest lists in `session_settings:` is taken: a bundle that isn't installed (or whose plugin doesn't load), a key it doesn't list (a record installed before the bundle listed it says to run `chi update` or `chi bundle upgrade NAME`), or another shape (`--set foo`, `--set a.b.c=1`) is an error naming what it takes, exit 2, nothing started. Saved in the session; a respawned worker, a fork and a continue keep it, a delegate child doesn't. Ignored with `--attach`/`--resume` (one line, as `--toolkit`); refused with `chi web`. See [Per-session settings](plugins.md#per-session-settings). |
241
242
  | `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
242
243
  | `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
@@ -490,9 +491,9 @@ file and index line stay as they are; only this session doesn't see it.
490
491
  - Names are checked before the launch, warnings only: `Warning: --memory 'x'
491
492
  not found`, `Warning: --mute 'x' matches no memory`, `Warning: 'x' is both
492
493
  --memory and --mute; muted`.
493
- - `--attach ID` or `--resume ID` with either flag: the session's prompt is
494
- already built, so the flags are ignored with one line,
495
- `(--mute applies to a new session; <id>'s prompt is already built)`.
494
+ - `--attach ID` or `--resume ID` with either flag: the session keeps its
495
+ own, so the flags are ignored with one line,
496
+ `(--mute applies to a new session; <id> keeps its own)`.
496
497
  - The status row shows `mem: <used and preloaded>` and `muted: <names>`; the
497
498
  Web UI's info-bar tooltip shows `memories: … · preloaded: … · muted: …`.
498
499
  - The `read` tool on `memories/<name>.md` is not refused (a guardrails rule
@@ -598,6 +599,18 @@ running turn keeps its tools. `/toolkit` alone answers mid-turn too; a change
598
599
  sent while a turn runs waits for the turn's end. The web shows a session that
599
600
  isn't on `full` as the `tools` chip. See [Toolkits](configuration.md#toolkits).
600
601
 
602
+ `/parent-answers` shows what a parent agent may answer on this session's
603
+ questions: the level that applies, the session's own and config.yml's
604
+ (`parent answers: standard (this session: standard; config.yml
605
+ guardrails.parent_answers: soft)`). `/parent-answers hard|standard|soft` sets the
606
+ session's own, `/parent-answers default` drops it; it is saved at once and
607
+ holds from the next answer. It never goes looser than config.yml's (the reply
608
+ says when config.yml's applies instead). Anyone may tighten it; only you loosen
609
+ it, at the REPL, an attached terminal or the web: `chi send`, a piped or marked
610
+ attach and `chi -p --non-interactive` are refused. `/parent-answers` alone answers
611
+ mid-turn too; a change waits for the turn's end. No chip: its reply is where the
612
+ web shows it. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
613
+
601
614
  A `/thinking`, `/reasoning-history` or `/toolkit` change that restarts the prompt cache
602
615
  asks first: the reply says what it costs, changes nothing, and names the line
603
616
  that applies it (`/thinking low --yes`). `--yes` (or `-y`) applies it at once.
@@ -1187,7 +1200,7 @@ Behavior notes:
1187
1200
  model that it wasn't continued (or your reason); `!rollback` still erases it.
1188
1201
  - `--non-interactive` (`chi -p`) has nobody to ask: the turn ends there. A `chi send --wait` on a session nobody can
1189
1202
  answer (an older worker) reports `limit` (exit 1) with how to continue it: `chi send ID -m '/continue yes'`
1190
- (not under `guardrails.parent_answers: hard`: a parent agent's `/continue yes`, sent with `chi send` or typed into a
1203
+ (not under `guardrails.parent_answers: hard`, config.yml's or the session's own: a parent agent's `/continue yes`, sent with `chi send` or typed into a
1191
1204
  piped `chi --attach`, is held to it as `chi answer --option Continue` is, and refused there).
1192
1205
  - The check-in bundle's card closes as the turn ends, before the step-limit question opens, so the two never stand
1193
1206
  together; with check-in's default `after: 50` and the limit of 300 a long turn shows check-in cards (at 50, 100, …)
@@ -1633,7 +1633,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1633
1633
  | `guardrails.enabled` | `true` | | `false`: no rules, and hooks' asks are dropped (a deny still applies). config.yml only: a worker unsets any `SAMAGOTCHI_GUARDRAILS_*` it inherits. See [Guardrails](guardrails.md). |
1634
1634
  | `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
1635
1635
  | `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
1636
- | `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
1636
+ | `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
1637
1637
  | `log.file` | state dir | yes | See "Debug Log File". |
1638
1638
  | `log.disable` | `false` | yes | No file logging. |
1639
1639
  | `log.level` | `info` | yes | `debug`, `info`, `warn`, `error`. |
data/docs/guardrails.md CHANGED
@@ -103,7 +103,12 @@ The file tools can't write the store.
103
103
  ### Answers from a parent agent
104
104
 
105
105
  A chi parent's `delegate` children don't need any of this for approvals: they
106
- go to the user on the parent's own card (the approval relay). This section is
106
+ go to the user on the parent's own card (the approval relay), as do the hook
107
+ questions (the cost cap's Continue/Stop, a bundle's `ctx.ask_user`) where the
108
+ parent may pick nothing but a deny under the child's `parent_answers`; the
109
+ others come back to the parent's model with the block below. A dismissal
110
+ always passes (the hook decides what no answer means): the strategy is a
111
+ convention for an honest parent, not a barrier. This section is
107
112
  the path for other agents, and for a chi parent that can't host the relay (a
108
113
  `--no-shared` REPL, `--non-interactive`): there the model gets the question
109
114
  block and the rules below. With the relay, the same rules still hold for a
@@ -150,6 +155,47 @@ approval's block lists the Allow once command next to the deny; under
150
155
  `standard` and `hard` it says allowing is up to the user. An invalid value
151
156
  warns and is `hard`.
152
157
 
158
+ #### A session's own level
159
+
160
+ A session may carry its own `parent_answers`, set when it is created (`chi
161
+ --parent-answers LEVEL`, `chi send --new --parent-answers LEVEL`) and kept by
162
+ its respawns, forks and continues; a `delegate` child starts with its parent's,
163
+ as it is then. It can only tighten: config.yml's value is the ceiling, and the
164
+ stricter of the two applies, in `chi answer`'s check (from the session file),
165
+ the worker's (from its session) and in what the question block offers. A refusal
166
+ then names it: `a parent agent may only stop this turn here (parent_answers:
167
+ hard (this session))`. A looser one is saved as given with a warning
168
+ (`soft is looser than config.yml's guardrails.parent_answers: standard;
169
+ standard applies`) and takes effect only if config.yml is loosened later. A
170
+ value that isn't a level reads as `hard`.
171
+
172
+ `/parent-answers` shows the level that applies, the session's own and
173
+ config.yml's; `/parent-answers LEVEL` sets the session's own, `/parent-answers
174
+ default` drops it (config.yml's applies). Anyone may tighten it, a parent too
175
+ (`chi send ID -m "/parent-answers hard"`). Loosening it (toward `soft`, or
176
+ `default`) is yours alone: the line must come from you at the REPL, an attached
177
+ terminal or the web composer. chi refuses it (`loosening parent_answers is the
178
+ user's: …`) from `chi send` (a parent's or yours: the worker can't tell them
179
+ apart), from a piped or marked `chi --attach` or REPL, from `chi -p
180
+ --non-interactive`, from a delegate's follow-up and from a plugin; `chi answer`
181
+ has no command route. `--parent-answers` on `--attach`/`--resume` is ignored,
182
+ never sent as the command.
183
+
184
+ config.yml is read each time an answer is checked; a session's own value stays
185
+ as saved and binds whenever it is the stricter of the two. A session saved as
186
+ `standard` under a `hard` config.yml follows `hard`, and `standard` again once
187
+ config.yml is loosened. A `delegate` child keeps the value it started with,
188
+ whatever its parent changes later.
189
+
190
+ A parent agent on a real terminal (a PTY: tmux, a kitty window) with no agent
191
+ marker set looks like you, and can loosen it; so can one that edits a session
192
+ file (`<id>.json`, read again by the next worker that wakes for it). That is
193
+ not a security boundary either: config.yml still caps the result.
194
+
195
+ A setting form waits for a running turn to end, as `/toolkit`'s does: a
196
+ tightening typed while a child's question is pending applies after that turn,
197
+ and a parent may answer the question meanwhile under the level before it.
198
+
153
199
  `soft` suits a capable parent model: it can let a child run one more command
154
200
  it asked about, still with every guardrail rule in force. For a weak or
155
201
  unknown parent model, set `standard` or `hard` (and see
@@ -177,7 +223,7 @@ A plugin's or a hook's question (`ask_user`) tags its options the same way
177
223
  (`effects:`, see [Plugins](plugins.md)); an untagged one is a plain `answer`.
178
224
 
179
225
  The guardrails settings (`enabled`, `mode`, `small_models`, `parent_answers`) are
180
- read from config.yml only: they have no environment variable, and a worker
226
+ read from config.yml only (a session's own `parent_answers` can only tighten it): they have no environment variable, and a worker
181
227
  chi starts unsets every `SAMAGOTCHI_GUARDRAILS_*` it would inherit. What a
182
228
  parent's environment does choose is the config dir: `XDG_CONFIG_HOME` picks
183
229
  which config.yml chi reads, and a worker the parent starts or wakes (`chi
@@ -204,7 +250,11 @@ person at a terminal with no marker keeps every scope.
204
250
  This guards an honest but eager parent, not a hostile one: it is not a
205
251
  security boundary. The worker's Bridge and the web's answer route on localhost
206
252
  take an answer from any local process, which can leave the marker out (or
207
- post through the web) and get every scope. chi's instructions for parents ([chi as a
253
+ post through the web) and get every scope. In the same way a parent can
254
+ loosen a session's own level by posting `/parent-answers` with a forged
255
+ `tui:<pid>` or `web:<tab>` client id to the Bridge or the web's command route,
256
+ by editing the session file, or by running `chi --attach` on a PTY with the
257
+ markers cleared. chi's instructions for parents ([chi as a
208
258
  sub-agent](sub-agent.md)) tell them to use `chi answer` only.
209
259
 
210
260
  ## Protected paths
data/docs/hooks.md CHANGED
@@ -117,7 +117,7 @@ end
117
117
 
118
118
  **Tag the options that spend or permit.** A question can be answered by a
119
119
  parent agent (`chi answer`, a piped `chi --attach`), and
120
- `guardrails.parent_answers` decides which options it may pick by their
120
+ `guardrails.parent_answers` (or the session's own, stricter one) decides which options it may pick by their
121
121
  `effects:` (label => effect): `:spend` (more time or money: a Continue),
122
122
  `:permit` (one call), `:permit_wide` (more than one), `:deny` (a Stop or a
123
123
  Deny); an option left out is a plain `:answer`, which a parent may always
data/docs/plugins.md CHANGED
@@ -1611,7 +1611,7 @@ bundles:
1611
1611
  pass the spend, saved for the session) and **Stop** (the turn stops, `canceled` with `stopped_by: cost-cap`, and a
1612
1612
  card shows the spend, reported, estimated and side). With no one to ask (`--non-interactive`) it stops. A parent
1613
1613
  agent answering it (`chi answer`) may Continue under `guardrails.parent_answers: soft` or `standard`, and only Stop
1614
- under `hard` (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
1614
+ under `hard` (config.yml's, or the session's own when stricter) (Continue is tagged `:spend`; see [Guardrails](guardrails.md#answers-from-a-parent-agent)). `/cap N` is
1615
1615
  a command, not an answer: a parent that sends it (`chi send ID -m '/cap 10'`) raises the cap under any strategy.
1616
1616
  - **The cap of a session:** the one `/cap` or a Continue set for it, else its own `cap:` (`chi --set
1617
1617
  cost-cap.cap=3`, `chi send --new --set cost-cap.cap=3`; `--set cost-cap.cap=off`, `0` or a negative number for none), else `child:` for a
data/docs/sessions.md CHANGED
@@ -212,7 +212,8 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
212
212
  - `--format json` (with `--wait`) prints one JSON object on stdout however the wait ends, for a script or a parent agent (see [chi as a sub-agent](sub-agent.md)): `{"status":"answered","session_id":…,"text":…}`; `{"status":"question",…,"question":{"id","kind","header","text","options","multi_select","allow_freeform","approval"},"answer_with":"chi answer ID --question QID --option N"}`, where `kind` is `question` (the model's), `hook` or `approval` (with the tool, command, cwd, rule, reason and offered scopes); `{"status":"running",…,"detail":…}` (exit 4, and on Ctrl-C); and `failed`, `canceled`, `removed` (a `--queue --wait` message taken back before it ran), `no_answer`, `error`, `worker_gone` or `stopped` with a `detail` line (exit 1); `failed` adds `error_kind` and `retryable`, `canceled` adds `cancel_reason` (`worker_stopped`: the turn waited for died with its worker and the next worker settled it) and `stopped_by`, when known. A failure before the wait (no such session, a busy one) is `error` too. A usage error (exit 2) prints no JSON.
213
213
  - `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
214
214
  - `--set BUNDLE.KEY=VALUE` (with `--new`, repeatable) gives the new session a bundle setting of its own, as `chi --set` does (see *Bundle settings* below): `chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap. A bundle or key the installed bundles don't offer is a usage error (exit 2), as is `--set` without `--new`.
215
- - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit and bundle settings, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool` and `--set` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
215
+ - `--parent-answers LEVEL` (with `--new`) gives the new session its own `guardrails.parent_answers` (`hard`, `standard`, `soft`), tighten-only: config.yml's applies when it is stricter, with `chi send: warning: --parent-answers soft is looser than config.yml's …`. See *Parent answers* below.
216
+ - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit, bundle settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
216
217
  - The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
217
218
  - A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
218
219
 
@@ -246,6 +247,17 @@ declares every tool (full). The file stores only the name: a kit's tools and pro
246
247
  `/toolkit` changes it, saved at once, from the next turn's start. See
247
248
  [Toolkits](configuration.md#toolkits).
248
249
 
250
+ ## Parent answers
251
+
252
+ A session file's `parent_answers` (`hard`, `standard`, `soft`, or `null` for none) is the session's own
253
+ `guardrails.parent_answers`: what a parent agent (`chi answer`, a relayed answer, a piped or marked attach) may
254
+ answer on its questions. It may only tighten config.yml's: the stricter of the two applies, in the worker's check and
255
+ in `chi answer`'s, and in what `chi send --wait`, `delegate_result` and the delegate reports tell a parent it may pick.
256
+ It is set at creation (`chi --parent-answers`, `chi send --new --parent-answers`); a respawned worker, a continue and a
257
+ fork keep it, and a `delegate` child starts with its parent's as it is then (a later change on the parent doesn't reach
258
+ it). A value that isn't a level reads as `hard`. `/parent-answers` changes it; loosening it is yours alone (see
259
+ [Guardrails](guardrails.md#answers-from-a-parent-agent)).
260
+
249
261
  ## Bundle settings
250
262
 
251
263
  A session file's `bundle_settings` (`{"cost-cap": {"cap": 3}}`, `{}` for none and for a session from before them)
@@ -265,14 +277,14 @@ The `delegate` tool hands a task to a **child session**: an ordinary chi session
265
277
  - `delegate_result(session:, timeout:)`: waits for a child's next reply: the one named, or the newest running child (`timeout` as for `delegate`: 0 checks once and returns at once). The reply comes back once; a later call waits for the next one. After `wait: false` it isn't needed: the report brings the reply.
266
278
  - A wait that timed out or was canceled still gets its delegate report: neither moves the parent's cursor, so the child's reply comes as a report when its turn ends. The result says so (`chi brings its reply here by itself as a delegate report …`; with `queue`, that it joins the next step or turn) and that `delegate_result` waits again only if the turn needs the reply now. A parent that gets no reports (`session.delegate_reports: off`, a `--no-shared` REPL, `--non-interactive`) is told `delegate_result <id> waits again` instead, and `wait: false`'s result says `delegate_result waits for its reply`.
267
279
  - What a wait returns, `session: <id>` then `status:` and the text, with the status words `chi send --wait --format json` uses: `answered` with the reply after a `---` line; `question` as soon as the child opens a question (the model's own, or a hook's), with the question's text, options and the `chi answer` command for it (nothing is answered for it: `chi answer`, attach or use the web, then wait again); `failed`, `canceled` or `no_answer` when the child's turn ended with nothing to say (its session shows what happened); `error` (its worker crashed) or `stopped` (`chi sessions stop`) at once; `worker_gone` when the child has had no worker for 15 seconds (it crashed without saying so, or idle-exited): `delegate` with `session:` starts it again; `running` on timeout, or when the parent's own turn is canceled (the child keeps running; the next turn can wait again). A reply saved before the child's worker idle-exited is returned at once.
268
- - **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `guardrails.parent_answers` (`hard`: Stop only; see [chi as a sub-agent](sub-agent.md#the-step-limit)).
269
- - **A child at its cost cap** (the [cost-cap bundle](plugins.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap": a parent waiting on it (`delegate`, `delegate_result`) gets it back as `status: question` with the `chi answer` commands, `--option Continue` or `--option Stop` (under `guardrails.parent_answers: hard` a parent's Continue is refused: Stop only). A **background** child (`wait: false`) paused at its cap does not wake its parent: delegate reports skip hook questions (they are the user's, on the child's card) and the approval relay carries approvals only, so the child waits on its card, with the web's bell. The parent sees it at its next `delegate_result`.
270
- - **A child's approvals go to the parent's user** (the approval relay). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
280
+ - **A child's step-limit question goes back to the parent model**, not to its user: `delegate_result` returns `status: question` with the `chi answer` commands for Continue and Stop, and says the model may instead send a narrower follow-up (`delegate session:`, which drops the question) or report back. A parent's `chi answer` follows `guardrails.parent_answers`, or the child's own when stricter (`hard`: Stop only; see [chi as a sub-agent](sub-agent.md#the-step-limit)).
281
+ - **A child at its cost cap** (the [cost-cap bundle](plugins.md#the-cost-cap-bundle), the child's own cap: `child:` in its settings, or `/cap`) asks a `hook` question, "Cost cap", `Continue` or `Stop`. Like every hook's question on a `delegate` child, it goes where it can be decided, under the child's `guardrails.parent_answers` (config.yml's, or the child's own when stricter): when the parent may pick an option other than a deny (Stop) there, `standard` and `soft` here, it comes back to the parent's model as `status: question` with the `chi answer` commands; when it may not, `hard` here (a parent's Continue is refused), it goes to the parent's user as a card, as approvals do (the relay below), and the model sees one line: `hook question relayed to your user: Cost cap → Stop` (or `dismissed`, which the hook reads as it likes, cost-cap as Stop; `refused by the child …`, `answered on the child`, `still open (your turn was stopped)`). Free text alone doesn't count as an option the parent may pick. A **background** child (`wait: false`) paused at its cap rings its parent as a question does (delegate reports, below): the parent's model gets it as a report when it may answer; otherwise the parent's worker relays it to its user at its next step boundary, or in a wake turn's first boundary when idle (before the model is called), and the report the model gets is the outcome line. While that card is open the parent's turn waits on it; a Stop closes it and leaves the question for your next input (or a `delegate_result`): no wake turn offers that question again meanwhile, while other children's reports and attached-context wakes go on. Under the default `soft` that means a background parent's model now hears its child's cost cap, and may Continue it.
282
+ - **A child's approvals go to the parent's user** (the approval relay), and so do the hook questions its parent can't usefully answer (above: a card headed `delegate ab12 asks: Cost cap`, the child's options and kind, no approval facts). While the parent waits (`delegate`, `delegate_result`), a guardrail approval the child opens reopens as the parent session's own approval: the same card in the parent's web page and attached terminal, with the same facts and diff, a line naming the delegate and its task (`delegate ab12 · fix the flaky spec…`, linked to its session) and the header `Approve delegate ab12's tool call?`. The options are the child's, by index; "for the session" says it is the delegate's session (the session scope keys on the child; repo and rule approvals are one shared store). The user answers once there; the parent's model never sees the approval, only one line per relayed approval in the tool's result, before the reply: `approval relayed to your user: execute: git push origin main → allowed once` (or `denied ("why")`, `dismissed (denied)`, `answered on the child`, `not delivered (the child's worker is gone)`, `refused by the child …`, `still open (your turn was stopped)`). The time the card is open doesn't count against `timeout`. While the parent waits on one child, its other running children's approvals are relayed too, oldest first, one card at a time (`+1 more delegate waiting`); each outcome comes in that child's next result.
271
283
  - The child's own card stays answerable (the first answer wins: answered there, the parent's card closes as "answered in ab12"). While relayed it says `Waiting for approval in parent ab12: answering here works too`; `chi sessions list` shows the child as `waiting (in parent ab12)` (`--format json`: `relayed_to`), and the web's bell rings for the parent's card, not the child's.
272
- - How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_answers` by the parent and again by the child.
284
+ - How the child trusts it: the parent's POST that says "answered" (Bridge `POST /session/<child>/relay`) carries no answer. The child asks its parent session's live Bridge (`POST /session/<parent>/relay/status`), which reports only what the parent's question flow took from a UI (`RelayDesk`, in memory only), and takes it only for this child, the question pending now, answered. A parent agent's answer on the relay card (a piped or marked `chi --attach PARENT`, `chi answer`) is held to `guardrails.parent_answers` (each session's own, when stricter) by the parent and again by the child.
273
285
  - A Stop of the parent's turn closes its card; the child's approval stays open there. A child's worker that goes closes the card (`… worker is gone`); a parent's worker that goes clears the child's "waiting in parent" line within 2 s. A later `delegate_result` relays it again.
274
- - No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or isn't waiting (`wait: false` until the next `delegate_result`): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions and hooks' questions come back to the model as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
275
- - **Delegate reports: a child's reply comes back by itself.** When a child ends a turn that was its parent's (its task, a follow-up, a continue the parent answered, a reminder: not a turn its user typed into it directly), it rings the parent: a small file in the parent's `children/` dir, written after the child's turn is saved. So does a question the parent's model decides (the child's own `ask_user_question`, the step-limit continue; not an approval or a hook's question, which the user answers on the child's card), the turn after such a question whoever starts it, and the child's worker crashing. The parent reads the child as `delegate_result` would and hands its model a **report**: the same text (`session:`, `status:`, the reply), under its own header (`[Delegate report, delivered by chi …; not your user's message. Tell your user what it found …]`), as an input message with `source: "delegate_report"`.
286
+ - No relay where the parent can't host one (a `--no-shared` REPL, `--non-interactive`) or, for an approval, isn't waiting (`wait: false` until the next `delegate_result`; a hook's question it may only deny is relayed from a delegate report instead, above): the approval waits on the child, with the web's bell, and the result's question block says to deny it or leave it for the user. The model's own questions, and the hooks' questions the parent may answer, come back to the model as before. A hook's question already answered on one of the parent's relay cards (a refused answer: it waits for the user on the child) is not relayed again; one whose card closed (a Stop, the child gone) is, and an approval always is, as before. A grandchild's (a plugin's `ctx.sessions.fork` can make one) is relayed hop by hop: `delegate ab12 → cd34 asks:`.
287
+ - **Delegate reports: a child's reply comes back by itself.** When a child ends a turn that was its parent's (its task, a follow-up, a continue the parent answered, a reminder: not a turn its user typed into it directly), it rings the parent: a small file in the parent's `children/` dir, written after the child's turn is saved. So does a question the parent decides (the child's own `ask_user_question`, the step-limit continue, a hook's question, relayed to the parent's user first when the parent may only deny it; not an approval, which the relay or the user on the child's card answers, nor a card the child relays from its own child), the turn after such a question whoever starts it, and the child's worker crashing. The parent reads the child as `delegate_result` would and hands its model a **report**: the same text (`session:`, `status:`, the reply), under its own header (`[Delegate report, delivered by chi …; not your user's message. Tell your user what it found …]`), as an input message with `source: "delegate_report"`.
276
288
  - Parent mid-turn: the report joins the turn at its next iteration boundary (a "delegate report" bubble in the web, `delegate report> ab12cd34 answered: …` in the attached terminal).
277
289
  - Parent idle: it runs a turn for the report (a *wake turn*, `last_turn.origin: "delegate_report"`) within the worker's 5 s tick, or wakes a worker for it when the parent's has idle-exited. Several reports that wait together are one turn and one message. A stopped, archived or scratch parent, or one open in a `--no-shared` REPL, is not woken: the rings wait on disk and join the next turn it runs (the user's message first). An archived parent that a later link continues wakes that link instead, which takes the rings its moved delegates left there (see *Session chains*). A worker just started (`chi send` to a stopped parent) runs no wake turn in its first 2 s, so the message it was started for goes first.
278
290
  - The parent's cursor per child (the newest reply it was given and the child as it was then) is in its `delegates.json`, shared with `delegate_result` and `wait: true`: a reply the model already had is never reported again, also after the parent's worker restarts. A ring is deleted only when the turn that took its report stays in the conversation (completed, canceled, or failed but kept: one that got to tool steps, or a reminder or continue turn, which keep what they read); a turn rolled back (a failed one that got nowhere, a canceled continue turn) leaves it for the next one (and the report isn't handed back to the composer). So the model reads each report once, unless a later `!rollback` erases a kept turn (what it read goes with it).
data/docs/sub-agent.md CHANGED
@@ -13,6 +13,15 @@ worker keeps the turn paused. The parent answers with `chi answer`, from what it
13
13
  knows or after asking its own user, and chi goes on from there. The model gets
14
14
  the answer as its tool result, as if you had answered in the web.
15
15
 
16
+ A chi parent's own `delegate` children work the same way, with one shortcut:
17
+ their approvals, and the hook questions the parent may only deny under the
18
+ child's `guardrails.parent_answers`, go to the parent's user as a card in the
19
+ parent's UIs instead (the relay, [Sessions](sessions.md#delegating)). When an
20
+ agent runs that parent with `chi send --wait`, the relay card is the parent's
21
+ own question: exit 3 with its text (`delegate ab12 … asks:`), options and
22
+ `answer_with`; the JSON has no `relay` key. Its `chi answer` is held to the
23
+ parent's setting, then to the child's.
24
+
16
25
  The other direction, chi sending a part of its work to a Claude Code session
17
26
  you already work with (to validate, as context, or with a question), is the
18
27
  [claude-peer bundle](plugins.md#the-claude-peer-bundle): Claude pairs with the
@@ -99,7 +108,8 @@ question and starts a new turn. It can't be dismissed.
99
108
  A parent may Continue by default: Continue grants no permission (every tool
100
109
  call still meets the guardrails), it only spends time and tokens. With
101
110
  `guardrails.parent_answers: hard` in chi's config.yml (config.yml only, no
102
- environment variable) a parent may only Stop: `chi answer` and the worker
111
+ environment variable; a session's own `parent_answers` may tighten it, see
112
+ [Guardrails](guardrails.md#a-sessions-own-level)) a parent may only Stop: `chi answer` and the worker
103
113
  refuse a Continue (exit 1, `a parent agent may only stop this turn here`), the
104
114
  question block's command is the Stop, and the user continues it from the web or
105
115
  the terminal. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
@@ -126,7 +136,8 @@ the relay (a `--no-shared` REPL, `--non-interactive`).
126
136
  An approval is a guardrail rule with `verdict: ask`: its user said "ask me". A
127
137
  parent may always deny it (`--option Deny --text "why"`, `--text` alone, or
128
138
  `--dismiss`). By default (`guardrails.parent_answers: soft` in chi's
129
- config.yml, which has no environment variable) it may also give "Allow once";
139
+ config.yml, which has no environment variable, unless the session's own
140
+ `parent_answers` is stricter) it may also give "Allow once";
130
141
  the wider scopes never go, and neither does any allow on chi's own config,
131
142
  hooks or guardrail rules: `chi answer` refuses those with exit 1 and a line
132
143
  like `only Allow once (guardrails.parent_answers: soft) can be given here: deny
@@ -212,7 +223,8 @@ Start a task: `chi send --new --wait --format json --timeout 500 -m "<task>"`
212
223
  JSON. If you won't answer, add `--dismiss` instead: chi does nothing it asked
213
224
  about, finishes with what it found, and the reply comes back (exit 0).
214
225
  - `question.kind: approval`: chi wants to run something its user's guardrails
215
- flagged. What you may give follows chi's `guardrails.parent_answers`, and the
226
+ flagged. What you may give follows chi's `guardrails.parent_answers` (or the
227
+ session's own, stricter `parent_answers`), and the
216
228
  question says it: when its `allowed_options` (and the text block's `allow:`
217
229
  line) list "Allow once" (`soft`, the default), you may allow that one call if
218
230
  your task clearly needs it; never a wider scope (this session, a repo, a
@@ -122,7 +122,7 @@ module Samagotchi
122
122
  return failure(not_dismissable(pending)) if options[:dismiss] && ParentReport.continue?(pending)
123
123
 
124
124
  selected = selection(pending, options) or return @selection_status
125
- refusal = parent_refusal(pending, selected)
125
+ refusal = parent_refusal(pending, selected, session)
126
126
  return failure(refusal) if refusal
127
127
 
128
128
  posted = post(id, qid, selected, options)
@@ -160,13 +160,13 @@ module Samagotchi
160
160
  end
161
161
 
162
162
  # Why a parent may not give this answer, or nil (Guardrails::ParentPolicy,
163
- # with this process's config.yml and the question's own tags). A deny,
164
- # text alone or a dismissal always passes. The worker checks it again
165
- # with its own config.
166
- def parent_refusal(pending, selected)
163
+ # with this process's config.yml tightened by the session file's own
164
+ # parent_answers, and the question's own tags). A deny, text alone or a
165
+ # dismissal always passes. The worker checks it again with its own.
166
+ def parent_refusal(pending, selected, session)
167
167
  offered = Array(pending.options).map(&:to_s)
168
168
  indices = selected.map { |label| offered.index(label) }
169
- strategy = Guardrails::ParentPolicy.current
169
+ strategy = Guardrails::ParentPolicy.for_session(session)
170
170
  reason = Guardrails::ParentPolicy.refusal(pending, indices, strategy: strategy)
171
171
  reason && Guardrails::ParentPolicy.message(reason, pending: pending, strategy: strategy)
172
172
  end
@@ -22,9 +22,9 @@ class KnownNames
22
22
  DERIVATIONS = %w[home user git repo].freeze
23
23
  DEFAULT_MIN_LENGTH = 6
24
24
  # Splits a command or path into tokens; each token's [-_.] parts are
25
- # candidates too. "~" splits: in "~name" (that user's home) the name
26
- # after it is what is checked.
27
- TOKEN_SPLIT = %r{[/\s"'=:,;|&()<>@~]+}
25
+ # candidates too. "#" and "~" split: in "~name" (that user's home) the
26
+ # name after it is what is checked.
27
+ TOKEN_SPLIT = %r{[/\s"'=:,;|&()<>@~#]+}
28
28
  PART_SPLIT = /[-_.]+/
29
29
  # A candidate with a shell glob character is not checked: `workato*`
30
30
  # is a pattern over the name, not a misspelling of it.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: known-names
3
- version: 0.1.5
3
+ version: 0.1.6
4
4
  scope: system
5
5
  description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
6
6
  trust_level: reviewed
@@ -8,7 +8,7 @@ files:
8
8
  known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
9
9
  hooks:
10
10
  known_names.rb:
11
- sha256: sha256:acff04fbe284b44644ca7140d574c1c0ebd2d999ede1243cd3fe0f432bfbb6d2
11
+ sha256: sha256:7743adea25655d019d308793e7616043fc29e3d664e1572b50d0b27bf3a9238f
12
12
  event: before_tool_call
13
13
  on_error: log
14
14
  priority: 50
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.86.0
3
+ version: 0.88.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files: