samagotchi 0.61.0 → 0.62.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/docs/architecture.md +3 -0
  4. data/docs/cli.md +22 -8
  5. data/docs/configuration.md +32 -19
  6. data/docs/context.md +8 -0
  7. data/docs/guardrails.md +2 -1
  8. data/docs/hooks.md +8 -0
  9. data/docs/internals/engine.md +24 -11
  10. data/docs/internals/prompt-caching.md +6 -1
  11. data/docs/plugins.md +42 -3
  12. data/docs/sessions.md +4 -4
  13. data/docs/testing.md +4 -1
  14. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  15. data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
  16. data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
  17. data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
  18. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
  19. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
  20. data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
  21. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  22. data/lib/samagotchi/engine.rb +9 -0
  23. data/lib/samagotchi/engine_peers.rb +1 -0
  24. data/lib/samagotchi/kernel_loop.rb +7 -16
  25. data/lib/samagotchi/llm/chat_loop.rb +23 -15
  26. data/lib/samagotchi/plugin/api.rb +36 -18
  27. data/lib/samagotchi/reasoning_history.rb +22 -4
  28. data/lib/samagotchi/row_refs.rb +126 -0
  29. data/lib/samagotchi/session_metrics.rb +24 -2
  30. data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
  31. data/lib/samagotchi/terminal_ui.rb +26 -3
  32. data/lib/samagotchi/tool_activity.rb +8 -4
  33. data/lib/samagotchi/tool_response.rb +5 -4
  34. data/lib/samagotchi/tool_row_fields.rb +4 -2
  35. data/lib/samagotchi/tool_runner.rb +16 -3
  36. data/lib/samagotchi/tools/builtins.rb +9 -2
  37. data/lib/samagotchi/tools/execute.rb +15 -3
  38. data/lib/samagotchi/tools/peers.rb +3 -2
  39. data/lib/samagotchi/tools/registry.rb +5 -3
  40. data/lib/samagotchi/version.rb +1 -1
  41. data/lib/samagotchi/web/app.rb +14 -1
  42. data/lib/samagotchi/web/message_parts.rb +17 -9
  43. data/lib/samagotchi/web/public/activity.js +4 -1
  44. data/lib/samagotchi/web/public/app.js +59 -15
  45. data/lib/samagotchi/web/public/chains.js +18 -20
  46. data/lib/samagotchi/web/public/context_chips.js +92 -64
  47. data/lib/samagotchi/web/public/families_view.js +51 -39
  48. data/lib/samagotchi/web/public/index.html +61 -10
  49. data/lib/samagotchi/web/public/keep_focus.js +24 -0
  50. data/lib/samagotchi/web/public/lists_view.js +13 -13
  51. data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
  52. data/lib/samagotchi/web/public/question_card.js +11 -0
  53. data/lib/samagotchi/web/public/question_cards.js +60 -5
  54. data/lib/samagotchi/web/public/ref_menu.js +18 -5
  55. data/lib/samagotchi/web/public/refs.js +113 -15
  56. data/lib/samagotchi/web/public/scroll.js +42 -0
  57. data/lib/samagotchi/web/public/stage_model.js +22 -3
  58. data/lib/samagotchi/web/public/stage_view.js +299 -70
  59. data/lib/samagotchi/web/public/timing.js +1 -1
  60. data/lib/samagotchi/web/public/turn_html.js +5 -4
  61. data/lib/samagotchi/web/public/turn_model.js +49 -0
  62. data/lib/samagotchi/web/public/turn_view.js +104 -10
  63. data/lib/samagotchi/worker.rb +3 -2
  64. data/lib/samagotchi/worker_inbound.rb +5 -3
  65. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3f828171b95ef34cef1b89706f21a34822ea73dae63e55b2219767961241f7c
4
- data.tar.gz: b12760887f0613c4e087f824f653e8ccc20e0602de55ef69fd8aed69a897a33c
3
+ metadata.gz: 68aeb876a7c4c04d7f2f5de4290a1c6089df3acd181bce7380ccb6bde9ef9de8
4
+ data.tar.gz: ed308c360b3942d9f3d08233762396493a018585103e50661dd17ea4bb0d123c
5
5
  SHA512:
6
- metadata.gz: e68486688dfe56739e1d05a68549d050f3cb73c369536c5fa097d832baabcfb42ba41917580ad9d0a4707ab3933550247177e92b0184dee2493b467be5773bad
7
- data.tar.gz: 630eca4f31585d00938ad89b77275d25969384301b43c71415e20fac95be9fb22dfa60695f59afd1fe612e4cad8de94ead8ab949464c6ab7f21eb18fb8667357
6
+ metadata.gz: 8a3ecca6a15d1c2d0e4acf5531491116139b3423ca0e6fe2b4ad558f5c99b332b50672a700437f9d37c1397d9ed92e527b843e6577b11198906ff75ab16b4ecd
7
+ data.tar.gz: 949538fdd97549e09bd215353cbdf9dccbe42a21806b965ff828db5b708521791e939abd2d2cb93892c23660abbfabd3106bb492412eb505f52cc0cd742d6d99
data/CHANGELOG.md CHANGED
@@ -8,6 +8,63 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.62.0] - 2026-10-13
12
+
13
+ ### Added
14
+
15
+ - Plugins: `chi.tool(…, refs: ->(args) { [...] })` gives a tool call's web row its refs (a ticket, a PR, a file
16
+ under the session's folder) with their URL actions: the row's title opens the first, its `▾` menu offers the
17
+ rest, on a phone too. Saved with the result, so a reload shows them. See docs/plugins.md, "Refs on a tool's
18
+ row".
19
+
20
+ ### Changed
21
+
22
+ - Web: the stage view no longer jumps: it keeps its height while a turn runs, its chip stays put when the answer
23
+ arrives, and the finished turn slides away instead of collapsing. The stage floats above the composer (its rising at
24
+ a turn's start moves nothing, and the history behind it glides instead of jumping); content changes inside it (the
25
+ first words of a reply, an answered card, a short answer), so a card you answer stays where it is; it grows with a
26
+ short ease when a card or the answer needs room; its chip is its footer (the steps open right above it); and when
27
+ the turn moves into the history the stage slides away under the composer, then the turn appears there.
28
+ - Web: on a phone the top bar no longer gains a row mid-turn when the hidden strip's pill grows ("· 1 waiting"), which
29
+ pushed the whole session down; the pill drops its session count while it says live or waiting.
30
+ - Web: while a question or approval waits in the stage view, the stage shows the whole text of the reply that asked
31
+ it, not only its last sentence.
32
+ - Web: the model's question or its own approval, once answered, keeps the reply that asked it (a delegate's relayed
33
+ approval, a hook's question and the step limit don't): two muted lines under its `question → answer` line, the
34
+ whole text when the card is open, live and after a reload.
35
+ - Web: a finished turn's steps block shows its outline when its steps said something: open, one line per step that
36
+ wrote text (a mid-turn note reads without a click), and each run of tool-only steps folded into one `· N steps`
37
+ line that shows them. Live (both views) and after a reload; a turn whose steps said nothing collapses as before.
38
+ - Web: question and approval cards answer from the keyboard. Enter in the response box or on an option submits
39
+ (never Deny or Dismiss), and a new card takes the focus when the message box is empty.
40
+ - github-pr 0.5.0: a `path:line` it links in an answer is also a file ref, so the web's ref menu offers the file in
41
+ this checkout (editor, show in turn, copy) and a "PR diff" (or "File at head") action. Needs chi 0.61.0; run
42
+ `chi update`.
43
+ - source-links 0.5.0: each ref it links in an answer gets an "Open in <source>" action in the web's ref menu; a
44
+ source's new `kind:` key names its refs' kind (default `issue`). Needs chi 0.61.0; run `chi update`.
45
+ - Chat hosts (`api: openai`) keep the running turn's reasoning across a mid-turn steer under `reasoning_history:
46
+ turn`, as native hosts do: a person's line, a plugin's nudge (check-in, agents-md) or a step-limit Continue's text no
47
+ longer drops the reasoning of the replies before it, so the request after it extends the one before and the
48
+ server's prompt cache holds. A `!cmd` run while the step-limit question stands still starts past itself. On a stock
49
+ Qwen template (Splash) the benefit needs `chat_template_kwargs.preserve_thinking: true`; see
50
+ [docs/configuration.md](docs/configuration.md#reasoning-history).
51
+
52
+ ### Fixed
53
+
54
+ - A turn run in the REPL (`--no-shared`) keeps the answer's display and refs (source-links' and github-pr's links,
55
+ a hook's `present`), so the web shows them when it opens the session; before, the save dropped them.
56
+ - A command a signal to chi stopped (a worker's TERM, HUP or INT mid-`execute`) says "stopped by a signal to chi",
57
+ not "stopped by the user"; one it kept from starting says "not run, a signal to chi stopped the turn".
58
+ slow-commands 0.2.1 doesn't count either as slow.
59
+ - A turn whose worker was killed (SIGKILL, a crash) keeps its tokens, speed and timings up to its last checkpoint:
60
+ the session's stats count it as a canceled turn (`worker_stopped`); before, they lost it.
61
+ - Web: hovering a ref with a ▾ at the very end of a line no longer pushes it onto the next line.
62
+ - Web: a failed `+ URL` attach keeps the "why" you typed (it was emptied), and a context chip with the keyboard
63
+ focus keeps it when the chips refresh.
64
+
65
+ Update with `chi update`: the system bundle 0.62.0, github-pr 0.5.0, source-links 0.5.0 and slow-commands 0.2.1.
66
+ Then restart `chi web`.
67
+
11
68
  ## [0.61.0] - 2026-10-12
12
69
 
13
70
  ### Added
@@ -2662,7 +2719,8 @@ and long-lived sessions.
2662
2719
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
2663
2720
  hotkey panel that send selected text or the clipboard to your sessions.
2664
2721
 
2665
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.61.0...HEAD
2722
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.62.0...HEAD
2723
+ [0.62.0]: https://github.com/dm1try/samagotchi/compare/v0.61.0...v0.62.0
2666
2724
  [0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
2667
2725
  [0.60.0]: https://github.com/dm1try/samagotchi/compare/v0.59.0...v0.60.0
2668
2726
  [0.59.0]: https://github.com/dm1try/samagotchi/compare/v0.58.0...v0.59.0
data/docs/architecture.md CHANGED
@@ -326,6 +326,9 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
326
326
  turn's answer). A plain `?tail=1` (a tab opened before this) gets the whole timing.
327
327
  The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
328
328
  read's re-render) and pairs them with the bubble's links and text before the `path:line` scan.
329
+ A plugin tool's row refs (`refs`, `RowRefs`) ride its tool events and, on a reload, its part (saved as
330
+ `tool_refs`); `turn_view.js` binds them to the row's title (`refs.js bindRowRefs`: the first on the title,
331
+ the others in its menu).
329
332
  - `?timing=1`: the whole timing (`analytics.json` merged with the worker's `/state`) and
330
333
  `turn_count`, when the page's merge came up short.
331
334
  - `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
data/docs/cli.md CHANGED
@@ -689,14 +689,23 @@ the step's reasoning), the running tool with what it does, and the last
689
689
  three calls. A plugin's or hook's notice and a plugin's nudge ("check-in
690
690
  nudged the model") show ahead of those calls for about 4 s, as their rows
691
691
  wait in the closed block. A card that needs you (a question, an approval, check-in)
692
- sits in the stage too. The chip under it, `N steps · M tool calls` with one
693
- tick per call, opens the turn view's block in place (newest step first
692
+ sits in the stage too. While a question or approval waits, the stage shows
693
+ the whole text of the reply that asked it (in place of its newest
694
+ sentence) above the card, until you answer. Once answered, the model's
695
+ question or its own approval keeps that reply (a delegate's relayed
696
+ approval, a hook's question and the step limit don't): two muted lines
697
+ under its `question → answer` line, the whole text when you open the card,
698
+ in both views and after a reload. The chip at the stage's bottom edge, `N steps · M tool
699
+ calls` with one tick per call, opens the turn view's block right above it (newest step first
694
700
  while it runs; a tick opens its step). `▾` folds the stage to its status
695
- row, remembered in this browser. When the turn ends the answer shows in
701
+ row, remembered in this browser. The stage keeps its height while a turn
702
+ runs: it grows, with a short ease, when a card or the answer needs room and gives the room back
703
+ only when the turn moves into the history. When the turn ends the answer shows in
696
704
  the stage, and the whole turn moves up into the history once you are not
697
705
  using the stage (the pointer over it, a touch or scroll in the last 4 s,
698
- keyboard focus or a selection keep it) for 1.5 s; sending the next message
699
- moves it at once. The history is never scrolled while a turn runs, and only
706
+ keyboard focus or a selection keep it) for 1.5 s: the stage slides away
707
+ under the composer, then the turn appears in the history. Sending the next
708
+ message moves it at once. The history is never scrolled while a turn runs, and only
700
709
  follows the hand-off if you were at its end. The `/` command list opens
701
710
  over the stage's lower edge.
702
711
 
@@ -714,9 +723,14 @@ again). When a step ends its thinking closes to a plain `thinking` line
714
723
  (unless you opened it); a step that only thought shows that line alone. The
715
724
  live narration is a box of about three lines that fills sentence by
716
725
  sentence (a sentence shows once it is complete) and scrolls to the newest
717
- when full. When the turn ends the block collapses to its summary (`3 steps
718
- · 8 tool calls · execute ×7 · read ×1`) and the answer expands from the
719
- box into a normal bubble under it (Markdown, annotate). A plain answer
726
+ when full. When the turn ends the block shows its outline when any of its steps said
727
+ something: it stays open on one line per step that wrote text (that text's
728
+ first line, so a mid-turn note reads without a click), and each run of steps
729
+ that only called tools or thought sits behind one `· N steps` line that
730
+ shows them; old turns open the same way after a reload. A turn whose steps
731
+ said nothing collapses the block to its summary (`3 steps
732
+ · 8 tool calls · execute ×7 · read ×1`). Either way the answer expands from
733
+ the box into a normal bubble under it (Markdown, annotate). A plain answer
720
734
  without tools ends exactly as it does today. Rows the code collapses stay
721
735
  as you toggled them. A reloaded session shows the same block
722
736
  from the saved messages (each step's thinking, narration, tool parameters
@@ -521,9 +521,10 @@ What each backend gets:
521
521
 
522
522
  On chat hosts: llama.cpp honours both off switches but ignores the effort (when its `/props` says
523
523
  `chat_template_caps.supports_reasoning_effort: false`, chi says so once per session and host, from the `/props`
524
- answer the turn already fetched for the window); Splash takes `reasoning_effort` (off only
525
- through `none`) and scales with it; OpenRouter translates `reasoning_effort` per model (some can't turn thinking off:
526
- Qwen3-30B-A3B thinks anyway, gpt-oss refuses).
524
+ answer the turn already fetched for the window); Splash honours both off switches and passes the effort to the
525
+ model's template, which decides: Qwen3.6's ignores it (`low`, `medium` and `high` think the same), and the server's
526
+ `--default-reasoning-effort` decides what `default` means there; OpenRouter translates `reasoning_effort` per model
527
+ (some can't turn thinking off: Qwen3-30B-A3B thinks anyway, gpt-oss refuses).
527
528
 
528
529
  When the model thinks although the level is `off`, chi says so once per session and host
529
530
  (`thinking> warning: off wasn't honoured by … (N chars of thinking)`) and logs `thinking_not_honoured` each time.
@@ -558,26 +559,39 @@ hosts:
558
559
  url: https://api.deepseek.com/v1
559
560
  api: openai
560
561
  api_key_env: DEEPSEEK_API_KEY
561
- reasoning_history: all # its API wants every earlier reply's reasoning when tools are sent
562
+ reasoning_history: all # with tools its API wants every earlier reply's reasoning: under turn, turn 2 fails
562
563
  models:
563
564
  some-model:
564
565
  reasoning_history: drop
565
566
  ```
566
567
 
567
- - `turn` (the default) sends the running turn's reasoning: each reply since the last user message carries its
568
- reasoning as `reasoning_content`, which is what Qwen's, Gemma 4's and DeepSeek's chat templates and the hosted APIs
569
- (OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models) expect. The running turn starts at the
570
- last message you sent (a prompt, or a steer: as the chat template counts it) and never before the turn's own start,
571
- so a turn that goes on by itself (a reminder, a context wake, a delegate report's wake) sends nothing of the one
572
- before it, an interrupted reply's partial thought included. A **step-limit Continue** is the same turn going on: it
573
- keeps the reasoning of the run it continues (back to the first run of a chain of Continues), so its first request
574
- matches the last one before the limit and the server's prompt cache stays warm. Continue's text, or a `!cmd` run
575
- while the step-limit question stands, is a user message: the reasoning before it goes, as for any steer, and that
576
- Continue reads the conversation again once (a re-prefill on a local server). After a `/model` change while the
577
- question stands the Continue starts afresh (no other model's reasoning is sent).
568
+ - `turn` (the default) sends the running turn's reasoning: each reply since the turn's start carries its reasoning
569
+ as `reasoning_content` (OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models). A steer, a plugin's
570
+ nudge (check-in, agents-md) or a Continue's text merged into the turn doesn't reset it, as on a native host: the
571
+ request after it is the one before plus the steer, so the server's prompt cache holds. A turn that goes on by itself
572
+ (a reminder, a context wake, a delegate report's wake) sends nothing of the one before it, an interrupted reply's
573
+ partial thought included. A **step-limit Continue** is the same turn going on: it keeps the reasoning of the run it
574
+ continues (back to the first run of a chain of Continues), so its first request matches the last one before the
575
+ limit and the server's prompt cache stays warm; Continue's text keeps it too. A `!cmd` run while the step-limit
576
+ question stands is a user message added after the run: the reasoning before it goes, and that Continue reads the
577
+ conversation again once (a re-prefill on a local server). After a `/model` change while the question stands the
578
+ Continue starts afresh (no other model's reasoning is sent).
579
+ - **A stock Qwen template (Splash, vLLM, SGLang; llama.cpp with Qwen's own template)** renders an earlier reply's
580
+ reasoning only after the last user message: a steer hides the reasoning before it whatever chi sends, and the
581
+ prompt changes there. `sampling: {chat_template_kwargs: {preserve_thinking: true}}` on the host renders every
582
+ reasoning chi sends (a reply sent without any renders an empty think block; adding it restarts the prompt cache
583
+ once). What it costs by level, with it on: under `turn` a steer no longer re-reads the turn, but each new turn still
584
+ reads the conversation again from the previous turn's first reply (that turn's reasoning leaves the request); under
585
+ `all` every request extends the one before (append-only), at the cost of a prompt that keeps the reasoning of every
586
+ turn. Without it (the template's default) both re-read from the reply before each user message. A strict server
587
+ that rejects unknown fields ("Unrecognized request argument supplied: chat_template_kwargs": OpenAI proper, some
588
+ gateways) fails every request with it, so set it only on a host you know takes it.
589
+ - A **direct DeepSeek API** host (`api.deepseek.com`) with tools wants the reasoning of every earlier reply, earlier
590
+ turns included, and answers HTTP 400 without it: under `turn` every turn after one that thought fails. Set
591
+ `reasoning_history: all` there (the example above). DeepSeek through OpenRouter takes `turn`.
578
592
  - `drop` sends none (what chi did before this setting).
579
593
  - `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
580
- earlier turns' reasoning (Qwen's) needs `sampling: {chat_template_kwargs: {preserve_thinking: true}}` as well.
594
+ earlier turns' reasoning (Qwen's) needs `preserve_thinking` as well (above).
581
595
  - Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
582
596
  the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn`. Anything else than a level warns
583
597
  once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
@@ -597,9 +611,8 @@ models:
597
611
  | to or from `all`, once an earlier reply has thinking | every earlier reply gains or loses its reasoning: a server that renders it (hosted APIs, llama.cpp's default template) reads the conversation again from the first earlier thought |
598
612
  | nothing thought yet, the same level, or a native host | none |
599
613
 
600
- - The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included
601
- (the chat loop starts again after a steer), and a step-limit Continue keeps the thoughts of the run it continues,
602
- a mid-turn steer's earlier ones included.
614
+ - The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included,
615
+ and a step-limit Continue keeps the thoughts of the run it continues, a mid-turn steer's earlier ones included.
603
616
  - When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
604
617
  sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
605
618
  refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
data/docs/context.md CHANGED
@@ -346,6 +346,14 @@ show the answer as it is.
346
346
  at the PR's head commit (a removed one: at its base), which has every
347
347
  line.
348
348
  - Code blocks, markdown links and URLs are left alone.
349
+ - **The ref menu**: each linked reference is also a
350
+ [file ref](hooks.md#refs-on-the-answer-display-only) on the answer: the
351
+ file in this checkout (the repository's root + the PR's path), so its
352
+ menu has the editor, show-in-turn and copy targets, plus **PR diff** (or
353
+ **File at head**, **File at base** for a removed file), the link's URL.
354
+ Only for a PR of this checkout's repository (one of its git remotes
355
+ names it on github.com): another project's PR keeps its links only.
356
+ Bundle 0.5.0, chi 0.61.0.
349
357
  - The PR's files are read with `gh api` in the background when a turn
350
358
  starts (and when a PR is attached while it runs), at most once a minute,
351
359
  and read again only when the PR's head moved; the answer is linked from
data/docs/guardrails.md CHANGED
@@ -35,7 +35,8 @@ Approve tool call?
35
35
 
36
36
  In a terminal, answer with a number, the exact label, `y` (Allow once) or `n`
37
37
  (Deny); add `; reason` to tell the model why (`n; open a PR instead`). An empty
38
- answer, Ctrl-C, the web's Deny button or a cancelled turn deny it. The prompt stays open during turns: when the question comes up it turns
38
+ answer, Ctrl-C, the web's Deny button or a cancelled turn deny it (on the web card, Enter in the reason box or
39
+ on an option submits, never denies). The prompt stays open during turns: when the question comes up it turns
39
40
  into a yellow `? `, with the call and the options listed under it; only a line
40
41
  submitted there answers it (never one typed before), and what you had typed comes back
41
42
  once it closes. On a short terminal the list shrinks (the hint row, then the `in`/`why`
data/docs/hooks.md CHANGED
@@ -636,6 +636,7 @@ bundles:
636
636
  pattern: '\bGH-(\d+)\b' # full form: a regex
637
637
  url: 'https://github.com/org/repo/issues/{match}'
638
638
  case_insensitive: false # optional, default false
639
+ kind: issue # optional: the refs' kind, default issue
639
640
  max: 10 # optional: refs per line, default 10
640
641
  note: false # optional: no sources line, default true
641
642
  ```
@@ -654,6 +655,13 @@ text ([`fallback_for: :display`](#what-a-hook-can-do-the-runtime)); it shows
654
655
  when it names one the answer doesn't link (a ref only in code, two sources on
655
656
  one ref) and whenever markdown is off. Bundle 0.4.0 needs chi 0.35.0 for that.
656
657
 
658
+ Each ref the answer links is also a [ref on the
659
+ answer](#refs-on-the-answer-display-only): kind `issue` (or the source's
660
+ `kind:`, a short lowercase word other than `file`), target `{id: "JIRA-123"}`,
661
+ label `JIRA JIRA-123`, and one action, **Open in JIRA** (the source's name),
662
+ its URL. The web's ref menu on the link shows it. A ref whose URL isn't
663
+ http(s) stays a markdown link only. Bundle 0.5.0 needs chi 0.61.0 for that.
664
+
657
665
  The `prefix:` form compiles to `\b<prefix>-(\d+)\b` and the URL is
658
666
  `base_url` + the full ref text (`JIRA-123`). The `pattern:` form takes a
659
667
  regex and a `url:` template with placeholders (below). `case_insensitive:
@@ -196,14 +196,15 @@ The differences from the native loop:
196
196
  - A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
197
197
  `TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
198
198
  `Session#reasoning_history`, `/reasoning-history`): `all` on every model message, `turn` past
199
- `ChatLoop#reasoning_boundary`: the later of the last query (a `role: "user"` entry: a prompt, a steer, merged input,
200
- the chat template's `last_query_index`; or a `turn_start` entry) and the Run's start (`Run`'s `@start`, the
201
- conversation length at `#complete`), so a continue turn (reminder, context wake, delegate report wake) replays
202
- nothing of the run before. A step-limit Continue's Run starts where the run it resumes did: `#complete`'s
203
- `reasoning_since:` (the Engine's `resume_from`, checked against the messages' length in `#resumed_run_start`) is
204
- the Run's `@start`. `Run#prompt_chars` counts it (`#reasoning_chars`). The native loop differs on a steer:
205
- `KernelLoop#inject_pending_input!` doesn't re-sanitize, so native keeps the turn's reasoning across a mid-turn
206
- steer, where the chat loop starts again (as the template does).
199
+ `ChatLoop#reasoning_boundary`: the Run's `@reasoning_start`, fixed once per Run by `ReasoningHistory.run_start`
200
+ (the rule `KernelLoop#start_turn` uses too) from the Run's start (the conversation length at `#complete`), so a
201
+ continue turn (reminder, context wake, delegate report wake) replays nothing of the run before. Entries the Run
202
+ merges (a person's line, a plugin's steer, a Continue's text) don't move it, as natively (nothing re-sanitizes
203
+ within a run): the request after a steer extends the one before. A step-limit Continue's Run starts where the run
204
+ it resumes did: `#complete`'s `reasoning_since:` (the Engine's `resume_from`, checked against the messages' length
205
+ in `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
206
+ `since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
207
+ entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
207
208
  - A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
208
209
  without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
209
210
  `:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
@@ -240,13 +241,22 @@ One iteration with tool calls:
240
241
  - Runs the gate first, so `:tool_call_started` shows the call that will run (a before_tool_call hook may
241
242
  replace it; known-names corrects a misspelled name this way, and the model gets one line saying what ran).
242
243
  - Emits `:tool_call_started` {iteration, call_count, call_index, tool, call, params, label (plugin tools),
243
- title, called_as, view, ref}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
244
+ title, called_as, view, ref, refs}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
244
245
  reload (`Web::MessageParts#tool_part`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as) travel
245
- inside `activity` on the completed event; `EVENT_KEYS` (view, ref) go top-level on both events.
246
+ inside `activity` on the completed event; `EVENT_KEYS` (view, ref, refs) go top-level on both events.
246
247
  `Bridge::TurnAccumulator` (snapshot and replay) and the web (`activity.js` `ACTIVITY_FIELDS` /
247
248
  `EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
248
249
  read/write/edit's file against the cwd, `{kind: "file", path, line?, end_line?}`); the web opens it in the editor
249
250
  that `Web::Editor` (web.editor) names, for a local viewer only (`App#viewer_capabilities`).
251
+ `refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `Api.per_call`), run
252
+ once per call by `RowRefs.for` (never raises: a raise or a non-list gives none, `tool_refs_failed`; each entry a
253
+ `Ref`, a bad one dropped, `tool_refs_rejected`; a relative file resolved against the session's folder
254
+ (`KernelLoop#session_cwd`, else the process's), one outside it (its spelling or its real path) dropped, a kept one
255
+ stored under the real path; at most `RowRefs::MAX_REFS` and `MAX_BYTES`). `ToolRowFields.for` can't build it (no plugins on a reload), so the run carries it
256
+ as `refs:`, saved with the result as `tool_refs` (`ToolResponse::SAVED_KEYS`, never sent), and
257
+ `MessageParts#tool_part` reads it back through `RowRefs.for_display` (checked again). The page binds them to the
258
+ row's title (`refs.js bindRowRefs`, from `turn_view.js` live and on a reload): the first ref on it, the rest in its
259
+ menu.
250
260
  - Settles an ask verdict after `:tool_call_started`, so a UI shows the tool line and then the approval.
251
261
  - Dispatches through `KernelLoop#dispatch_tool_call`, or writes the denial text. Unknown tools are answered in
252
262
  `KernelLoop#dispatch` ("Error: no such tool ...", never repeating the wrong name).
@@ -319,7 +329,10 @@ end
319
329
  boundary) copies it, with the lines that boundary merges, into `Session#turn_checkpoint` (`TurnCheckpoint`):
320
330
  every save while the turn runs writes that copy with an `interrupted_turn` mark instead of the session's
321
331
  messages, written at the first boundary and then at most every 10 s, so a killed worker loses at most that much
322
- of its turn. `session.messages` itself is untouched until the turn ends.
332
+ of its turn. `session.messages` itself is untouched until the turn ends. When the checkpoint writes, the worker
333
+ also calls `Engine#checkpoint_metrics` (`SessionMetrics#persist(running_turn: true)`): `analytics.json` gets the
334
+ running turn's record so far as a killed turn's (canceled, `worker_stopped`), which the next worker loads; the
335
+ turn's end writes over it.
323
336
 
324
337
  ### GenerationPhase
325
338
 
@@ -127,7 +127,12 @@ is prefilled once. The native and the chat loop use the same plan.
127
127
  a per-session value in the stable part. A guard spec checks that model, location and session are the last
128
128
  sections (`spec/system_prompt_model_spec.rb`).
129
129
  2. **History is append-only.** Never rewrite an earlier message. Add a note at the tail instead. If a model's
130
- spec forces a rewrite (e.g. stripping previous-turn thinking), accept the re-prefill and note it. A worker's
130
+ spec forces a rewrite (e.g. stripping previous-turn thinking), accept the re-prefill and note it. Within a
131
+ turn both loops keep the turn's replies as sent across a mid-turn steer, a plugin's nudge and a Continue's text
132
+ (`ReasoningHistory.run_start` fixes where the run's reasoning starts), so the request after a steer is the one
133
+ before plus the steer; a new turn drops the previous turn's reasoning under `turn` (one re-prefill from its
134
+ first reply, on a server that renders it; a stock Qwen template without `preserve_thinking` re-renders from the
135
+ reply before every user message whatever chi sends). A worker's
131
136
  mid-turn checkpoint (`TurnCheckpoint`) is append-only too (the history plus what the loop appended) and
132
137
  never changes the request bytes.
133
138
  3. **Deterministic order.** Tools, memory lists and anything else that's rendered from a hash or a set needs a
data/docs/plugins.md CHANGED
@@ -144,7 +144,7 @@ chi's own, each bundle's with the bundle's name, and the terminal UIs' own
144
144
  `attached only`, `repl only`, or `terminal and web only` (`/cut`).
145
145
  It works in all three UIs.
146
146
 
147
- ### `chi.tool(name, description, params:, schema:, label:, preview:, targets:) { |args, ctx| … }`
147
+ ### `chi.tool(name, description, params:, schema:, label:, preview:, targets:, refs:) { |args, ctx| … }`
148
148
 
149
149
  This adds a tool the model can call. It is declared in the system prompt and
150
150
  in the chat path's `tools:`, after chi's own tools.
@@ -166,6 +166,8 @@ in the chat path's `tools:`, after chi's own tools.
166
166
  plugins.
167
167
  - `targets`: `->(args) { { paths: [...], command: "…", cwd: "…", acts_as: "…", args: {…}, label: "…" } }`,
168
168
  each key optional, says what a call acts on, for [guardrails](#guardrails).
169
+ - `refs`: `->(args) { [{ kind:, target:, label:, actions: [] }, …] }`, what the call's row traces to in the web
170
+ (display only, the model never sees them). See [Refs on a tool's row](#refs-on-a-tools-row).
169
171
 
170
172
  The block returns the result text. If it starts with `Error:`, it counts as
171
173
  a failure. If it raises, the model gets `Error: <message>`. To return images
@@ -174,7 +176,7 @@ too, see [Returning images](#returning-images).
174
176
  #### `args`
175
177
 
176
178
  `args` is a frozen Hash with **string keys**: the arguments the model gave,
177
- by name (`args["text"]`). The same Hash goes to `preview` and `targets`.
179
+ by name (`args["text"]`). The same Hash goes to `preview`, `targets` and `refs`.
178
180
 
179
181
  Each parser gives them structured: Gemma's native values (strings, numbers,
180
182
  booleans, lists, nested objects), Qwen's `<parameter=…>` text, and the chat
@@ -261,6 +263,42 @@ does loop-guard's `ignore_tools`.
261
263
  A tool without `targets:` is matched by its name only. A `targets:` that
262
264
  raises counts as nothing (it is logged). See [guardrails.md](guardrails.md).
263
265
 
266
+ #### Refs on a tool's row
267
+
268
+ `refs:` gives the call's web row its refs: a ticket the tool looked up, the PR it opened, a file it wrote.
269
+ Each is a ref as on the answer ([Refs on the answer](hooks.md#refs-on-the-answer-display-only), without
270
+ `match:`):
271
+
272
+ ```ruby
273
+ chi.tool "ticket", "Look up a Jira ticket", params: { id: { type: "string", required: true } },
274
+ refs: ->(args) {
275
+ [{ kind: "issue", target: { id: args["id"] }, label: "#{args["id"]} in Jira",
276
+ actions: [{ id: "open", label: "Open in Jira", url: "https://jira.example/browse/#{args["id"]}" }] }]
277
+ } do |args, ctx|
278
+ # …
279
+ end
280
+ ```
281
+
282
+ - `kind`: `"file"` with `target: { path:, line:, end_line: }`, or any short lowercase word (`issue`,
283
+ `commit`, …) with `target: { id: }`.
284
+ - A file's `path` is absolute or relative to the session's folder, and must be **under** it (its own
285
+ spelling or its real path: `/tmp/x` or `/private/tmp/x`): a file elsewhere is dropped. It is kept under
286
+ the folder's real path.
287
+ - `actions`: up to 8 `{ id:, label:, url: }`, each URL `http(s)` only. A file ref keeps the editor, show in
288
+ turn and copy; its actions add to them.
289
+ - The web puts the row's **first** ref on its title (a click runs its first action) and the others in the
290
+ title's `▾` menu, each as `<its label> · <action>` with the URL on hover. URL actions show for every viewer,
291
+ the phone too; a file's "Open in editor" only where the editor opens (this machine).
292
+ - It runs **once per call, on the tool path**, before the call runs: keep it cheap, with no I/O (no
293
+ network, no file reads); build the refs from `args` alone.
294
+ - If it raises or returns something other than a list, the row gets no refs and the tool still runs; a bad
295
+ ref (an unknown kind, a `javascript:` URL, a file outside the folder) is dropped. Both are logged (tag
296
+ `plugins`, `tool_refs_failed` / `tool_refs_rejected`).
297
+ - At most 20 refs per row (64 KiB of them). They are saved with the result (`tool_refs`), so a reload shows them without the
298
+ plugin, checked again.
299
+ - `chi.replace_tools` swaps a kept tool's block, `preview:`, `targets:` and `refs:` in place: none of them is a
300
+ change to the prompt.
301
+
264
302
  #### Returning images
265
303
 
266
304
  A tool can hand the model images too. The block returns a
@@ -1381,7 +1419,8 @@ and a summary of what changed in counts, authors and states only; it wakes
1381
1419
  the session for a review requesting changes, checks turning red, or the PR
1382
1420
  merged or closed. In the web, a `path:line` in a worker's answer links to
1383
1421
  that line of the session's PR ([Line links](context.md#line-links); `line_links:
1384
- always` for the REPL and `chi -p` too). `auto_attach:
1422
+ always` for the REPL and `chi -p` too), and its ref menu offers the file
1423
+ here and a "PR diff" action (`event[:refs]`). `auto_attach:
1385
1424
  offer` asks with a card first (`/pr-attach N`, `/pr-decline N`), `off`
1386
1425
  attaches nothing ([Attach, offer or off](context.md#attach-offer-or-off)).
1387
1426
  See [Attached context](context.md#github-prs-the-github-pr-bundle).
data/docs/sessions.md CHANGED
@@ -23,9 +23,9 @@ A session is deleted if **expired by age OR overflow by count** (unless `keep_st
23
23
 
24
24
  **A worker outlives whoever started it:** a spawned worker leaves its spawner's process tree (it is forked again, in a session of its own, and reparented to init), so closing the terminal of a `chi web`, or a tool tearing down a background `chi send --wait` and its children, no longer takes the worker and its turn with it.
25
25
 
26
- **A worker killed mid-turn:** while a turn runs, its worker keeps it on disk (`TurnCheckpoint`): the session file has the turn so far (the prompt from the turn's start, then the finished tool steps and lines merged into it) and `interrupted_turn: {id, started_at, at, iteration}`, written as the turn starts, at the loop's first iteration boundary and then at most every 10 s (`updated_at` moves with it). The turn's end saves the real messages without the mark. A worker that died without ending its turn (SIGKILL, a crash of the process) leaves the mark, and loses what came after its last written boundary: at most the last 10 s of steps, plus the step in flight (and the turn's metrics: `analytics.json` is written at a turn's end). The next worker to start for the session (a `chi send`, `--resume`, `--attach`, the web), or a `--no-shared` REPL resuming it, settles the turn before anything else (`TurnCheckpoint.settle`, log `interrupted_turn_settled`): a note `the previous turn was cancelled (worker-stopped) after Ns` goes at the tail, the status goes idle and the mark goes; `last_turn` stays the previous turn's (a new one saved before the turn a `chi send --wait` woke the worker for would end that wait at once). The session shows the turn's prompt and steps with that note; the dead turn is not run again, and the model reads on its next turn that it was cut.
26
+ **A worker killed mid-turn:** while a turn runs, its worker keeps it on disk (`TurnCheckpoint`): the session file has the turn so far (the prompt from the turn's start, then the finished tool steps and lines merged into it) and `interrupted_turn: {id, started_at, at, iteration}`, written as the turn starts, at the loop's first iteration boundary and then at most every 10 s (`updated_at` moves with it). The turn's end saves the real messages without the mark. A worker that died without ending its turn (SIGKILL, a crash of the process) leaves the mark, and loses what came after its last written boundary: at most the last 10 s of steps, plus the step in flight. Each checkpoint write also saves the turn's metrics so far to `analytics.json` (tokens, speeds, timings, its finished tool calls), as the record a killed turn leaves: canceled, `cancellation_reason: worker_stopped`; the turn's own end writes over it. The next worker to start for the session (a `chi send`, `--resume`, `--attach`, the web), or a `--no-shared` REPL resuming it, settles the turn before anything else (`TurnCheckpoint.settle`, log `interrupted_turn_settled`): a note `the previous turn was cancelled (worker-stopped) after Ns` goes at the tail, the status goes idle and the mark goes; `last_turn` stays the previous turn's (a new one saved before the turn a `chi send --wait` woke the worker for would end that wait at once). The session shows the turn's prompt and steps with that note; the dead turn is not run again, and the model reads on its next turn that it was cut.
27
27
 
28
- **Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`. A running turn is canceled over the Bridge first; the TERM stops whatever still runs as a Stop with reason `signal` and saves it (a spawned worker traps TERM, HUP and INT: `WorkerSignals`; a turn not ended within 3 s, 0.5 s after a stop or a second signal, is cut and the session saved as it was), and a stopped worker still saves its kept turn, since the marker, not the saved status, says stopped. Any TERM, HUP or INT does the same to a worker outside a stop: it leaves (log `worker stop reason=signal`) and the next send wakes a new one, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
28
+ **Stopping a worker:** `chi sessions stop ID` marks the session stopped (an empty `<id>/stopped` file, not a session.json field, which a worker saving at that moment would write over; a resume removes it), sends its worker TERM and waits (up to 10 s) until the worker has let go of `owner.lock`. A running turn is canceled over the Bridge first (so a command it was running says "stopped by the user"; only a bare signal to the worker makes it "stopped by a signal to chi"); the TERM stops whatever still runs as a Stop with reason `signal` and saves it (a spawned worker traps TERM, HUP and INT: `WorkerSignals`; a turn not ended within 3 s, 0.5 s after a stop or a second signal, is cut and the session saved as it was), and a stopped worker still saves its kept turn, since the marker, not the saved status, says stopped. Any TERM, HUP or INT does the same to a worker outside a stop: it leaves (log `worker stop reason=signal`) and the next send wakes a new one, so a `chi --resume ID` right after spawns a fresh worker. That is also how to restart a worker still running an older chi, which the attached terminal reports when a command or a question dismiss gets a 404, and the web when an answer, a dismiss, a command or a cancel does (`501 not_supported`; `BridgeClient.stale_worker_message`). A command this chi has that the older worker doesn't (`/llm-context`, the web's `llm ctx` chip, against a 0.39.0 worker) says the same, not `not a session command` (`SessionCommands.builtin_name`); a typo keeps its `400 unknown_command`. The web's `POST /api/sessions/:id/stop` waits the same way, for 2 s. A command sent during a turn that waits for its end (`/model X`, `!cmd`; see *Typing during a turn* in [cli.md](cli.md)) and hasn't run when the worker leaves (a stop, a crash) is answered `command_ran {status: "dropped"}`, so no UI keeps showing it waiting.
29
29
 
30
30
  **Deleting a session:** `chi sessions delete ID...`, `/exit --delete` in a terminal and the Web UI's delete all go through `SessionManager.delete_session`: it resolves a unique prefix, removes `<id>.json` and the whole `<id>/` dir, and returns what it removed. A session a plain REPL has open (`owner.lock` kind `tui`) is always refused. One a worker runs is refused unless the caller asks to stop it: then it stops the worker as `chi sessions stop` does and deletes once `owner.lock` is free (`--force` on the CLI, 10 s; the web always, 2 s; `/exit --delete` after the worker agreed to exit, 10 s). A worker that outlives the wait leaves the session in place. The web's route is `DELETE /api/sessions/:id` (200 `{status: "deleted", session_id, stopped}`; 409 `owned_by_tui` or `still_stopping`; 404).
31
31
 
@@ -72,9 +72,9 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
72
72
  **Ordering:**
73
73
 
74
74
  - `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
75
- - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
75
+ - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
76
76
  - Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
77
- - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
77
+ - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
78
78
  - Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
79
79
 
80
80
  **Test-session hygiene:**
data/docs/testing.md CHANGED
@@ -16,10 +16,13 @@ No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_
16
16
  clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
17
17
 
18
18
  The web specs run in plain node. One that loads lit-html (`turn_view.js`, `thinking_chip.js`, `llm_context_chip.js`,
19
- `info_bar.js`, `lists_view.js`, `vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a
19
+ `info_bar.js`, `lists_view.js`, `context_chips.js`, `families_view.js`, `chains.js`, `vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a
20
20
  devDependency only, never shipped; about 0.3 s per spec file that loads it). The shim's `htmlOf(nodes)` and `canon(html)` compare markup
21
21
  without lit's marker comments. happy-dom 20 picks the wrong option of a `<select>` whose options lit inserted with
22
22
  `?selected`: read the option marked `selected` (`option[selected]`), not `select.value`; the browser follows the mark.
23
+ A select bound with `.value` instead reads right in both, but lit sets it before the options exist on a first render:
24
+ `llm_context_chip.js`'s `renderForm` draws twice then. Assert a kept node with `assert.ok(a === b)`: a failing
25
+ `assert.equal` on two DOM nodes inspects the whole DOM (huge output, can stall the run).
23
26
 
24
27
  ## While you work: affected specs
25
28
 
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: github-pr
3
- version: 0.4.0
3
+ version: 0.5.0
4
4
  scope: system
5
5
  description: Attaches the branch's open GitHub pull request to a session as context (and any PR URL with chi context add or the web's "+ URL"); the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can offer the PR with a card first, or attach nothing
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.45.0"
7
+ requires_chi: ">= 0.61.0"
8
8
  needs:
9
9
  - command: gh
10
10
  why: reads the pull request
11
11
  hint: brew install gh, then gh auth login
12
12
  plugin:
13
13
  file: plugin.rb
14
- sha256: sha256:2510f8e5bfc77b2a6343b47d56bf6be9da09fd8741f52928462945a55d83f0ce
14
+ sha256: sha256:b9e3d7a837ecaa2b0e53f3d62d2ccedd532f4d1b5d6faefd5adc9570cb3c4efe
15
15
  scripts:
16
16
  pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
17
17
  context_providers: