@turri-dev/cli 0.1.19 → 0.1.20

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 (49) hide show
  1. package/dist/chat-commands.js +2 -1
  2. package/dist/chat-records.d.ts +2 -0
  3. package/dist/chat-terminal.d.ts +1 -0
  4. package/dist/chat-terminal.js +1 -1
  5. package/dist/chats.d.ts +60 -3
  6. package/dist/chats.js +201 -14
  7. package/dist/config-edit.js +2 -0
  8. package/dist/config.d.ts +8 -1
  9. package/dist/config.js +5 -2
  10. package/dist/extension.js +57 -1
  11. package/dist/host-session.d.ts +11 -0
  12. package/dist/host-session.js +2 -0
  13. package/dist/host.d.ts +1 -0
  14. package/dist/host.js +2 -0
  15. package/dist/identity.js +2 -2
  16. package/dist/init-config.d.ts +1 -0
  17. package/dist/init-config.js +6 -0
  18. package/dist/notice.d.ts +59 -0
  19. package/dist/notice.js +38 -0
  20. package/dist/pi-launch.js +9 -2
  21. package/dist/regenerate/agent-shell.d.ts +5 -0
  22. package/dist/regenerate/agent-shell.js +2 -2
  23. package/dist/regenerate/audit.js +1 -1
  24. package/dist/regenerate/codebase.js +1 -1
  25. package/dist/regenerate/command-check.js +1 -1
  26. package/dist/regenerate/confine.d.ts +2 -0
  27. package/dist/regenerate/confine.js +2 -2
  28. package/dist/regenerate/ledger.d.ts +14 -0
  29. package/dist/regenerate/paths.d.ts +2 -0
  30. package/dist/regenerate/paths.js +2 -0
  31. package/dist/regenerate/records.d.ts +1 -1
  32. package/dist/regenerate/records.js +3 -1
  33. package/dist/regenerate/release.js +1 -1
  34. package/dist/regenerate/testing.js +1 -1
  35. package/dist/regenerate/workspace.js +1 -1
  36. package/dist/sentinel-chat.d.ts +14 -0
  37. package/dist/sentinel-chat.js +178 -0
  38. package/dist/sentinel.d.ts +55 -0
  39. package/dist/sentinel.js +279 -0
  40. package/docs/README.md +2 -0
  41. package/docs/chats.md +4 -3
  42. package/docs/config.md +3 -1
  43. package/docs/deploy.md +2 -2
  44. package/docs/development.md +1 -0
  45. package/docs/hosting.md +18 -3
  46. package/docs/sentinel.md +54 -0
  47. package/docs/using.md +1 -0
  48. package/genome/rules/turri-regeneration.md +1 -0
  49. package/package.json +1 -1
package/docs/hosting.md CHANGED
@@ -83,6 +83,10 @@ const turri = await createTurri({
83
83
  systemPrompt: (prompt) => prompt, // the last word on the whole system prompt, each turn
84
84
  extensions: [browser, terminal], // the host's own extensions, built into every chat after turri's
85
85
  restore: true, // reopen the last session's chats here, as `turri --continue` does
86
+ onNotice: (notice) => {}, // the sentinel puts something to the person: draw it in notice.chat
87
+ onNoticeOver: (notice) => {}, // ...answered, dismissed or superseded: take its card down
88
+ onConcluded: (conclusion) => {}, // the sentinel is done with a pulse: holds or wrong, told to conclusion.chat
89
+ sentinel: { tools: [browserTool] }, // the sentinel's own tools, beside turri's: what it tests a release with
86
90
  });
87
91
 
88
92
  const chat = await turri.openChat(); // { id, title, status, shown, session }: a fresh chat, shown
@@ -91,7 +95,9 @@ await chat.session.prompt("add a cart to the shop");
91
95
  turri.focus(other.id); // the chat the person looks at now
92
96
  await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, else "ended" the session - "ignored", "missing"
93
97
  await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
94
- turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown }]
98
+ turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown, role? }]
99
+ turri.notices(); // the notices open now
100
+ await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }: "done", "dismissed", "over", "missing"
95
101
  turri.onChats((chats) => {}); // a chat opened, closed, renamed, working or done
96
102
  await turri.closeChat(chat.id); // its turn stopped, its conversation deleted - its pulses run on
97
103
  turri.jobs; // the session's job registry: every pulse any chat started
@@ -99,16 +105,25 @@ turri.loop; // its work loop, once a chat is open
99
105
  await turri.dispose(); // every chat let go, what activate started stopped
100
106
  ```
101
107
 
102
- - **The chats.** Up to nine, each its own history, model, thinking level and stack, all on the session's one job registry and work loop - see [chats](chats.md). `openChat({ sessionFile })` reopens a saved one. A pulse's end is told to the chat that asked for it, its agent reacting there; to the chat `focus` last named when that one is closed. A chat is never hidden from turri's side: what the host does not show, it still runs, and its questions go to the host's dialogs at once - the host decides where they wait. `status` says a turn is running (`working`) or that it said something since it was last focused (`unread`).
108
+ - **The chats.** Up to nine, each its own history, model, thinking level and stack, all on the session's one job registry and work loop - see [chats](chats.md). `openChat({ sessionFile })` reopens a saved one. A pulse's end goes to the sentinel (below), and is shown in the chat `focus` last named - its agent told of a pulse that landed only to say so and what the sentinel is doing with it, and not told of one that went wrong. What the `surface` says to do once something is released is then not done: the sentinel tests the release. With `sentinel: false`, it is told to the chat that asked for it, its agent reacting there - to the chat `focus` last named when that one is closed. A chat is never hidden from turri's side: what the host does not show, it still runs, and its questions go to the host's dialogs at once - the host decides where they wait. `status` says a turn is running (`working`) or that it said something since it was last focused (`unread`).
103
109
  - **Interrupting.** `interrupt(id)` is the terminal's ctrl+c for a host's key or button: it stops what that chat is doing - its turn, a skill or a tool in it, a `!` command, a compaction - and nothing in the other chats (`stopped`). A skill stopped keeps its run, to be continued. A question of the chat's open in the host's dialogs when its turn is stopped is over - declined - and its dialog's `signal` aborted, for the host to close it. A chat doing nothing ends the session, every chat with it, as `dispose` does (`ended`): turri never exits the host's process, so the host closes what showed it. An interrupt straight after one that stopped something is `ignored`, so pressing twice to be sure ends nothing. `session.abort()` is the chat's turn alone, and never ends the session.
104
110
  - **Declining a question.** `decline(id)` is ctrl+c at a question in turri's terminal: the question the chat asks now - its ask_user, a skill's interview, a review gate's escalation, whatever goes through the host's dialogs - declined. The asker is told "The user declined to answer the question." and the turn goes on; a review gate declined stands. It declines each question it is called at, however many in a row, and never stops the turn - that is `interrupt`. `none` when the chat asks nothing now, `missing` for a chat not open. A dialog that resolves with `undefined` is something else - dismissed, and in a question of several answers "done" with the boxes ticked so far.
105
111
  - **The record.** The chats are recorded in `.turri/chats.json` as the terminal records them, and `restore: true` reopens the last ended session's - without it, a host starts with none and deletes nothing. A chat closed is deleted; a session's chats are kept until five newer sessions have recorded theirs.
106
112
  - **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'.
107
113
  - **The questions.** `dialogs(chat)` gives a chat's two dialogs: `select(title, options, context, at, { signal })` and `input(title, context, { signal })`, each resolving with the answer, or undefined when the person dismissed it. `signal` is aborted when the question is over without the dialog's answer - declined by `decline`, or its turn stopped (`interrupt`, `session.abort()`): the host closes the dialog it opened for it, whatever it resolves with then being ignored. A host may leave it unread. The chat's questions, a skill's interview and a review gate's escalation are put in them; without them, in the `uiContext`'s own dialogs.
108
- - **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released.
114
+ - **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: with `sentinel: false`, after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released. With the sentinel on, the sentinel tests the release, and the conversation only says it landed.
109
115
  - **The system prompt.** `systemPrompt(prompt)` gets the whole system prompt for the turn - [pi](https://pi.dev)'s, turri's identity and blocks, the rules - after turri's own `before_agent_start` work, and returns what is sent. It is for what `surface` does not cover; to describe the UI, use `surface`, rather than words appended after turri's that contradict them.
110
116
  - **The mode.** Each chat is bound in [pi](https://pi.dev)'s `tui` mode: the work runs in the background and keeps the chat free, as the terminal does. Its other UI calls - its notices, its working line, a confirm - come to `uiContext(chat)`, as [pi](https://pi.dev)'s [SDK](https://github.com/earendil-works/pi) describes it.
111
117
 
118
+ ### The sentinel
119
+
120
+ The [sentinel](sentinel.md) is one of the host's chats: turri opens it with the first pulse that finishes, through the host's own `create` like any chat - `onChats` hears of it, `role: "sentinel"` - and never moves `focus` to it. It closes like any chat, and the next pulse that finishes opens it again; with it on, `openChat` opens up to eight.
121
+
122
+ - **Notices.** What it finds wrong comes as `onNotice(notice)`: `{ id, chat, severity: "error" | "warning" | "info", summary, evidence: string[], actions: [{ label, task }], module?, pulse?, job?, state: "open" }`. Draw it in the chat `chat` names - the one `focus` last named, or, while that is the sentinel, the chat that asked for the pulse. It is drawn there as a card in the transcript too. A host given `onNotice` answers it itself - the terminal's dialog is not opened - with `answer(id, …)`: `{ action: index | label }` starts that chat's agent on the action's task, `{ steer: text }` on the person's words, `{ dismiss: true }` on nothing. `onNoticeOver(notice)` comes as it is answered - and when a newer notice about its module supersedes it (`state: "superseded"`) - for the host to take the card down. `notices()` lists the open ones.
123
+ - **Conclusions.** As the sentinel is done with each pulse - all well or not - `onConcluded({ job, verdict: "holds" | "wrong", summary, module, pulse, chat })` comes, and the chat `chat` names - the one `focus` last named, as for a notice - shows it and its agent says so in a line.
124
+ - **Its tools.** `sentinel: { tools }` are [pi](https://pi.dev) tool definitions registered in the sentinel's chat alone and told to it in its prompt - the host's browser, say, for it to test a release with. The rest of the host's `extensions` are built into its chat too, but their tools are refused it: only its own read, and these.
125
+ - **Its surface.** What it is told is its own prompt, not the conversation's: the host's `surface` is not in it; `systemPrompt` still has the last word.
126
+
112
127
  A surface for a desktop app might read:
113
128
 
114
129
  ```js
@@ -0,0 +1,54 @@
1
+ # The sentinel
2
+
3
+ The sentinel is a chat of turri's own, beside the chats you talk to. Every pulse that finishes goes to it: it checks how the pulse went - and, with `release-after-pulse` on, tests what was released - and when something needs you, it says so in the chat you are in, with what can be done about it. You go on talking about the software, and building it, while it watches what lands.
4
+
5
+ It is on unless `config.turri.yaml` says `sentinel: false`. It is there in the terminal and in a host such as turri-desktop - wherever turri holds [chats](chats.md) - and not in pi's print, json or rpc modes, nor on the command line (`turri regenerate`, `turri change-plain`), which have no chat to tell: there a pulse is told as it is without it.
6
+
7
+ ## What you see
8
+
9
+ - **In the chat you are in**, a pulse that finishes shows as it always has - `✓ app · regenerate pulse 3 finished after 4m 10s`, and for one that went wrong, where to see why. A pulse that landed is told to the chat's agent, which tells you in a line or two what landed and what the sentinel is doing with it - and again, in a line, when the sentinel is done with it - checking it, testing what was released - and does nothing else about it: it does not test, release or open it, nor do what the project's guidance says to do once a pulse finishes. The pulse is the sentinel's. A pulse that went wrong is not told to it: the sentinel looks into it, and you hear of it as a notice.
10
+ - **The sentinel** opens with the first pulse that finishes - in the background: it never takes the screen, and you stay in your chat. It is a tab in the strip like any other chat - `sentinel` - marked while it works and when it has something new. Switch to it to read what it checked.
11
+ - **A notice**, when something needs you: a card in the chat you are in - what is wrong, what shows it, and what can be done, numbered - and, in the terminal, once that chat is not mid-turn, a dialog on it:
12
+ - `enter` on an action accepts it: the chat's agent is started on it - a change to the \*\*\*plain, a pulse regenerated again, a credential to provide, a setting to change;
13
+ - **Say what to do instead…** - your own words, and the agent does that;
14
+ - **Dismiss**, or `esc`: nothing is done.
15
+
16
+ Nothing is done without your answer. A notice goes to the chat you are in; while you are in the sentinel's, to the chat that asked for the pulse. There is one open notice a module: a newer one about the same module supersedes the older one, whose call to action would be stale, and the card says so.
17
+
18
+ ## What it does
19
+
20
+ For each pulse that finishes, turri writes the sentinel what it was and how it ended - its stages, rounds and failures as the [ledger](using.md#the-ledger) has them, its \*\*\*plain diff, its log, the chat that asked for it, the chat you are in, and what was released and where it runs. Then it:
21
+
22
+ 1. **checks the pulse** - from what it is told, and when that is not enough, from the pulse's log, the ledger (`ledger`, `pulses`), `diagnose`, the code and the \*\*\*plain;
23
+ 2. **tests the release**, with `release-after-pulse` on: what was released is running where it is told, and it tries it - in the terminal with its shell (`curl`, the program itself, the logs), in a host also with what the host gives it, a browser say. How far it tests is its own judgement, from what the pulse changed and what the \*\*\*plain says it now does. With `release-after-pulse` off nothing is running, and it checks the pulse alone;
24
+ 3. **finds out why**, when something is wrong - the pulse failed or stopped, it did not pass, the release did not come up, the software does not do what its \*\*\*plain says - and puts it to you as a notice, each action a task your chat's agent can carry out once you accept it;
25
+ 4. **notifies no one** when all is well;
26
+ 5. **concludes**, last, whether all is well or not: what it checked and what it found, in a line or two. The chat you are in shows it - `✓ sentinel · app · pulse 3 · checked, it holds` - and its agent tells you so in a line, and does nothing more; when something was wrong, that its notice says what.
27
+
28
+ A pulse that went well is checked until a newer pulse of its module finishes: that one is checked instead. A pulse that went wrong is looked into to its end. Two functional specs that contradict each other stop a pulse in a way only you can settle: turri puts that notice itself, with an action that starts your chat on working it out with you.
29
+
30
+ ## What it may do
31
+
32
+ Nothing that changes the project. Its file tools - `read`, `ls`, `grep`, `find` - read the whole project, its \*\*\*plain among them, and write nothing; its shell runs [behind the walls](sandbox.md) every agent's does - the command check, the OS sandbox where one is up, the audit - with the project read only and the network open, its home and temporary folder its own. It has `ledger`, `pulses`, `diagnose`, `validate`, `doctor` and the web, `notify_person` for its notices, `conclude` for when it is done with a pulse, `keep_guidance`, and what a host gives it. Whatever else it calls is refused: it never regenerates, steers, releases or changes \*\*\*plain, and never asks you a question - a notice is how it reaches you. What it found to do is yours to accept.
33
+
34
+ ## Talking to it
35
+
36
+ Switch to its chat and say how to check your software - *test the sign-up flow at a phone's width*, *ignore the styling*, *the API runs on port 8080*. What should hold from now on it keeps with `keep_guidance`, in `.turri/sentinel.md`, and it is in its prompt from then on - after it is closed and opened again too.
37
+
38
+ ## Closing it
39
+
40
+ It closes like any chat (`/chat close`, `alt+w`, a host's `closeChat`), and that does not turn it off: the next pulse that finishes opens it again, with a fresh conversation - and your guidance. `turri --continue` reopens it with the other chats, as it was. `sentinel: false` - in `config.turri.yaml`, or with `/turri-config` - turns it off: from the next pulse that finishes, a pulse is told to the chat that asked for it, whose agent reacts to it, as without one.
41
+
42
+ It has a slot of its own: with it on, you open up to eight chats, and the ninth is the sentinel's, open or not.
43
+
44
+ ## Its model
45
+
46
+ The `sentinel` role in `models:` - else the chat's model, `harness-model` - see [models](config.md#models). Judging behaviour against \*\*\*plain, and driving a browser, want a capable model; point it at a cheaper one if it costs too much for every pulse.
47
+
48
+ ## In a host
49
+
50
+ A host sees the sentinel as one of its chats - `role: "sentinel"` in `chats()` and `onChats` - and never has focus moved to it. Each notice comes as `onNotice(notice)` - `{ id, chat, module, pulse, job, severity, summary, evidence, actions, state }`, `chat` the one to draw it in - and `onNoticeOver(notice)` when it is answered or superseded; `notices()` lists those open. `answer(id, { action } | { steer } | { dismiss: true })` answers one, as the terminal's dialog does. `onConcluded({ job, verdict: "holds" | "wrong", summary, module, pulse, chat })` comes as it is done with a pulse, all well or not - told to the chat `chat` too. The sentinel's own tools - a browser - are given with `createTurri({ sentinel: { tools } })`, and it is told it has them. See [running turri in a host](hosting.md#the-sentinel).
51
+
52
+ ## In the ledger
53
+
54
+ `chat.opened` carries `role: "sentinel"` for its chat; `notice.put` and `notice.over` record each notice - its severity and module, and how it ended: `accepted`, `steered`, `dismissed`, `superseded` - and `sentinel.concluded` each pulse it is done with, `holds` or `wrong`.
package/docs/using.md CHANGED
@@ -194,6 +194,7 @@ It is in `.turri/ledger/` in the project, a JSON line an event, one file a run -
194
194
  | the healers | `healer.started`, `healer.ended` with what it found, `healer.failed` and why - the debugger, the diagnosis of a failed pulse |
195
195
  | the history | `history.committed`, `history.failed`, `history.reset`, `history.rolledBack` |
196
196
  | the environment | `environment.error` |
197
+ | chats and notices | `chat.opened` (`role: "sentinel"` for the [sentinel](sentinel.md)'s), `chat.closed`, `notice.put` (its severity and module), `notice.over` (accepted, steered, dismissed or superseded), `sentinel.concluded` (`holds` or `wrong`, a pulse the sentinel is done with) |
197
198
 
198
199
  Never in it: prompts, replies, thinking, tool arguments and results, file contents - the one body it keeps is each pulse's \*\*\*plain diff, so what an earlier session regenerated can be read back. What an event does carry - a stage, an error's first line - is masked as the logs are.
199
200
 
@@ -42,6 +42,7 @@ The other rules say what a spec means. This one says what turri does with a chan
42
42
  - Never edit the code under `plain_modules/`: the next regeneration does not keep it, and the code record no longer says what the code is
43
43
  - In order of cost: change the spec where it is wrong or silent; regenerate the functional specs whose code went bad again, by name; regenerate the module from scratch, or its whole chain - only when the person asks for it or agrees to it
44
44
  - A pulse that does not land is rolled back whole - code, tests and record together - and its module waits until the spec changes or the module is asked for again. Running it again unchanged usually ends the same way: look at why it did not land first
45
+ - A finished pulse goes to the sentinel, a chat of turri's own - unless `sentinel: false`: it checks the pulse, tests what was released, and puts what is wrong to the person as a notice with what can be done. A pulse that landed is told to the chat the person is in too, only to say it landed and what the sentinel does with it, and when the sentinel is done with a pulse, what it found - the chat does nothing else about either; a pulse that went wrong is not told to it - the chat's agent is given a notice's task once the person accepts it, and does that
45
46
 
46
47
  ## Where the cost is
47
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {