@turri-dev/cli 0.1.25-next.fb3566e → 0.1.26-next.2fc445f
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.
- package/README.md +1 -0
- package/dist/chats.d.ts +11 -4
- package/dist/chats.js +115 -20
- package/dist/config-command.js +30 -0
- package/dist/config-edit.js +2 -2
- package/dist/config.d.ts +6 -3
- package/dist/config.js +2 -2
- package/dist/extension.js +7 -3
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +3 -2
- package/dist/notice.d.ts +15 -0
- package/dist/notice.js +14 -0
- package/dist/sentinel-chat.d.ts +5 -0
- package/dist/sentinel-chat.js +11 -2
- package/dist/sentinel.d.ts +8 -2
- package/dist/sentinel.js +32 -5
- package/docs/chats.md +1 -1
- package/docs/config.md +1 -1
- package/docs/hosting.md +1 -0
- package/docs/sentinel.md +18 -5
- package/docs/using.md +1 -0
- package/genome/rules/turri-regeneration.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,6 +159,7 @@ In a new project **turri** starts it itself, without asking how: it chooses the
|
|
|
159
159
|
- **`/halt`** halts a pulse, **`/lineage`** shows every generation of a module, and the **`drift`** tool says what is left to regenerate;
|
|
160
160
|
- **`/btw <question>`** asks turri something on the side - answered at once, even while a question waits for you (typed as the answer, under "Something else…"), and left out of the conversation;
|
|
161
161
|
- **`/turri-config`** changes any setting of the project's `config.turri.yaml` - the models, review, conformance tests, how many pulses at once - and says when it takes effect: now, at your next message, or at the next pulse. No restart: `/turri-config review false`, or `/turri-config` alone to pick from the list;
|
|
162
|
+
- **`/sentinel`** says whether the sentinel checks every pulse that finishes: `ask` (you are asked before it first opens), `on` or `off`;
|
|
162
163
|
- **`/stack`** chooses the stack to build with, in a project whose backend and frontend each have their own testing scripts in `config.turri.<stack>.yaml` - `/stack backend`, `/stack none` - for the session (`--stack <name>` on the command line). The chat's model builds each part with its own stack by itself, pinned to that module's pulses.
|
|
163
164
|
|
|
164
165
|
Other things to ask for:
|
package/dist/chats.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentSession, ExtensionAPI, ExtensionContext, ExtensionFactory, Ex
|
|
|
2
2
|
import { type ChatScope } from "./chat-scope.ts";
|
|
3
3
|
import { type RunRecord } from "./chat-records.ts";
|
|
4
4
|
import { type ChatUi } from "./chat-ui.ts";
|
|
5
|
+
import { type SentinelSetting } from "./config.ts";
|
|
5
6
|
import type { AskUserIO } from "./ask.ts";
|
|
6
7
|
import type { Job, JobRegistry } from "./regenerate/jobs.ts";
|
|
7
8
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
@@ -23,7 +24,9 @@ import { type ChatRole, type Conclusion, type Notice, type NoticeAnswer, type No
|
|
|
23
24
|
*
|
|
24
25
|
* With the project's sentinel on (sentinel.ts), every finished pulse goes to the sentinel - a chat of the session's own,
|
|
25
26
|
* opened in the background with the first, and again after it is closed - and the chat the person looks at shows only
|
|
26
|
-
* that it landed. What the sentinel finds wrong it puts to the person as a notice, in the chat they look at.
|
|
27
|
+
* that it landed. What the sentinel finds wrong it puts to the person as a notice, in the chat they look at. Under
|
|
28
|
+
* `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the session - the pulses
|
|
29
|
+
* that end meanwhile held until they answer.
|
|
27
30
|
*/
|
|
28
31
|
/** The most chats a session has open at once. */
|
|
29
32
|
export declare const MAX_CHATS = 9;
|
|
@@ -63,7 +66,7 @@ export interface TurriHost {
|
|
|
63
66
|
onConcluded?(conclusion: Conclusion & {
|
|
64
67
|
chat: string;
|
|
65
68
|
}): void;
|
|
66
|
-
/** What the sentinel puts to the person: the host draws it in the chat it names, and answers it with `answer`. */
|
|
69
|
+
/** What the sentinel puts to the person - and, `kind: "sentinel-start"`, whether to start it: the host draws it in the chat it names, and answers it with `answer`. */
|
|
67
70
|
onNotice?(notice: Notice): void;
|
|
68
71
|
/** A notice is over - answered, dismissed, or superseded by a newer one about its module: the host takes its card down. */
|
|
69
72
|
onNoticeOver?(notice: Notice): void;
|
|
@@ -229,8 +232,12 @@ export interface CoreOptions {
|
|
|
229
232
|
persist?: boolean;
|
|
230
233
|
/** The host's dialogs, a chat's own. */
|
|
231
234
|
dialogs?: (chat: string) => AskUserIO | undefined;
|
|
232
|
-
/** Whether the project wants a sentinel - read as each pulse finishes. Unset: none. */
|
|
233
|
-
sentinel?: () =>
|
|
235
|
+
/** Whether the project wants a sentinel - read as each pulse finishes: on, off, or ask the person first. Unset: none. */
|
|
236
|
+
sentinel?: () => SentinelSetting;
|
|
237
|
+
/** Under `sentinel: ask`: what the person is asked before the sentinel first opens, about the pulse that would open it. */
|
|
238
|
+
sentinelStart?: (job: Job) => NoticeInput;
|
|
239
|
+
/** The person chose for the project, asked whether to start the sentinel: `sentinel:` written. */
|
|
240
|
+
setSentinel?: (on: boolean) => void;
|
|
234
241
|
}
|
|
235
242
|
/** A chat's session, as stopping what it does reaches it. */
|
|
236
243
|
export type Stoppable = Pick<AgentSession, "isStreaming" | "isCompacting" | "isRetrying" | "isBashRunning" | "abort" | "abortBash">;
|
package/dist/chats.js
CHANGED
|
@@ -5,7 +5,7 @@ import { adoptRun, forgetChat, recordRun } from "./chat-records.js";
|
|
|
5
5
|
import { createChatUi } from "./chat-ui.js";
|
|
6
6
|
import { debugMode } from "./config.js";
|
|
7
7
|
import { record } from "./regenerate/ledger.js";
|
|
8
|
-
import { newNotice, noticeTask, SENTINEL, wentWrong } from "./notice.js";
|
|
8
|
+
import { newNotice, noticeTask, SENTINEL, sentinelStartChoice, wentWrong } from "./notice.js";
|
|
9
9
|
/**
|
|
10
10
|
* The chats of a turri session.
|
|
11
11
|
*
|
|
@@ -23,7 +23,9 @@ import { newNotice, noticeTask, SENTINEL, wentWrong } from "./notice.js";
|
|
|
23
23
|
*
|
|
24
24
|
* With the project's sentinel on (sentinel.ts), every finished pulse goes to the sentinel - a chat of the session's own,
|
|
25
25
|
* opened in the background with the first, and again after it is closed - and the chat the person looks at shows only
|
|
26
|
-
* that it landed. What the sentinel finds wrong it puts to the person as a notice, in the chat they look at.
|
|
26
|
+
* that it landed. What the sentinel finds wrong it puts to the person as a notice, in the chat they look at. Under
|
|
27
|
+
* `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the session - the pulses
|
|
28
|
+
* that end meanwhile held until they answer.
|
|
27
29
|
*/
|
|
28
30
|
/** The most chats a session has open at once. */
|
|
29
31
|
export const MAX_CHATS = 9;
|
|
@@ -90,6 +92,13 @@ export function createChats(options) {
|
|
|
90
92
|
const open = [];
|
|
91
93
|
/** The pulse the sentinel is on now, while its agent works: a newer one of the same module replaces a check of one that went well. */
|
|
92
94
|
let sentinelOnJob;
|
|
95
|
+
/**
|
|
96
|
+
* Under `sentinel: ask`, whether the person said to start the sentinel this session: asked - the notice `asking` open,
|
|
97
|
+
* the pulses that ended meanwhile `held` - started, or not this session.
|
|
98
|
+
*/
|
|
99
|
+
let consent = "unasked";
|
|
100
|
+
let asking;
|
|
101
|
+
const held = [];
|
|
93
102
|
const run = { run: randomUUID().slice(0, 8), pid: process.pid, started: new Date().toISOString(), chats: [] };
|
|
94
103
|
const changed = () => {
|
|
95
104
|
for (const listener of listeners) {
|
|
@@ -106,14 +115,19 @@ export function createChats(options) {
|
|
|
106
115
|
const sentinelChat = () => chats.find((chat) => chat.role === SENTINEL);
|
|
107
116
|
/** The person's chats: all but the sentinel. */
|
|
108
117
|
const people = () => chats.filter((chat) => chat.role !== SENTINEL);
|
|
109
|
-
const
|
|
118
|
+
const setting = () => {
|
|
110
119
|
try {
|
|
111
|
-
return options.sentinel?.()
|
|
120
|
+
return options.sentinel?.() ?? false;
|
|
112
121
|
}
|
|
113
122
|
catch {
|
|
114
123
|
return false;
|
|
115
124
|
}
|
|
116
125
|
};
|
|
126
|
+
/** On, or asked for - and not declined this session. */
|
|
127
|
+
const wantsSentinel = () => {
|
|
128
|
+
const now = setting();
|
|
129
|
+
return now === true || (now === "ask" && consent !== "declined");
|
|
130
|
+
};
|
|
117
131
|
/** The chat the person looks at, for what is put to them - never the sentinel: when it is shown, `fallback`, else their first. */
|
|
118
132
|
const personFacing = (fallback) => {
|
|
119
133
|
const shown = chats.find((chat) => chat.id === shownId);
|
|
@@ -250,6 +264,9 @@ export function createChats(options) {
|
|
|
250
264
|
}
|
|
251
265
|
const recorded = run.chats.find((kept) => kept.id === id);
|
|
252
266
|
chats.splice(chats.indexOf(chat), 1);
|
|
267
|
+
// The question whether to start the sentinel was in it: put again in the chat the person looks at now.
|
|
268
|
+
if (asking?.chat === id)
|
|
269
|
+
askAgain();
|
|
253
270
|
chat.unsubscribe?.();
|
|
254
271
|
chat.ui?.close();
|
|
255
272
|
inChat(chat.scope, () => record({ kind: "chat.closed" }, { project: cwd }));
|
|
@@ -343,26 +360,19 @@ export function createChats(options) {
|
|
|
343
360
|
},
|
|
344
361
|
sentinelOn: () => driver !== undefined && wantsSentinel(),
|
|
345
362
|
jobFinished(job) {
|
|
363
|
+
// The question was settled in config.turri.yaml while it was open: what it held goes as the file now says.
|
|
364
|
+
if (consent === "asking" && setting() !== "ask")
|
|
365
|
+
settleStart(setting() === true ? "given" : "declined", "superseded");
|
|
346
366
|
if (core.sentinelOn()) {
|
|
367
|
+
if (consent === "asking" || (setting() === "ask" && consent === "unasked" && sentinelChat() === undefined && askToStart(job))) {
|
|
368
|
+
held.push(job);
|
|
369
|
+
changed();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
347
372
|
sentinelFinished(job);
|
|
348
373
|
return;
|
|
349
374
|
}
|
|
350
|
-
|
|
351
|
-
const chat = asked?.instance !== undefined ? asked : (core.shownChat() ?? chats[0]);
|
|
352
|
-
if (chat === undefined)
|
|
353
|
-
return;
|
|
354
|
-
if (chat.id !== shownId)
|
|
355
|
-
chat.unread = true;
|
|
356
|
-
try {
|
|
357
|
-
chat.instance?.announce(job);
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
// A chat let go as the pulse ended: the one shown is told instead.
|
|
361
|
-
const shown = core.shownChat();
|
|
362
|
-
if (shown !== undefined && shown !== chat)
|
|
363
|
-
shown.instance?.announce(job);
|
|
364
|
-
}
|
|
365
|
-
changed();
|
|
375
|
+
toAsker(job);
|
|
366
376
|
},
|
|
367
377
|
jobsChanged() {
|
|
368
378
|
for (const chat of chats) {
|
|
@@ -437,6 +447,8 @@ export function createChats(options) {
|
|
|
437
447
|
return "missing";
|
|
438
448
|
if (notice.state !== "open")
|
|
439
449
|
return "over";
|
|
450
|
+
if (notice.kind === "sentinel-start")
|
|
451
|
+
return answerStart(notice, answer);
|
|
440
452
|
const task = noticeTask(notice, answer);
|
|
441
453
|
over(notice, "dismiss" in answer ? "dismissed" : "steer" in answer ? "steered" : "accepted");
|
|
442
454
|
if (task === undefined)
|
|
@@ -491,6 +503,89 @@ export function createChats(options) {
|
|
|
491
503
|
if (chat !== undefined)
|
|
492
504
|
inChat(chat.scope, () => record({ kind: "notice.over", notice: notice.id, state }, { project: cwd }));
|
|
493
505
|
}
|
|
506
|
+
/** A pulse ended, told to the chat that asked for it - as without a sentinel - or the chat shown, when that one is closed. */
|
|
507
|
+
function toAsker(job) {
|
|
508
|
+
const asked = chats.find((chat) => chat.id === job.chat);
|
|
509
|
+
const chat = asked?.instance !== undefined ? asked : (core.shownChat() ?? chats[0]);
|
|
510
|
+
if (chat === undefined)
|
|
511
|
+
return;
|
|
512
|
+
if (chat.id !== shownId)
|
|
513
|
+
chat.unread = true;
|
|
514
|
+
try {
|
|
515
|
+
chat.instance?.announce(job);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// A chat let go as the pulse ended: the one shown is told instead.
|
|
519
|
+
const shown = core.shownChat();
|
|
520
|
+
if (shown !== undefined && shown !== chat)
|
|
521
|
+
shown.instance?.announce(job);
|
|
522
|
+
}
|
|
523
|
+
changed();
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Under `sentinel: ask`, the first pulse that would open the sentinel this session: the person asked whether to start it,
|
|
527
|
+
* in the chat they look at. False when there is nothing to ask with, or no chat to ask in: the sentinel opens as asked.
|
|
528
|
+
*/
|
|
529
|
+
function askToStart(job) {
|
|
530
|
+
let input;
|
|
531
|
+
try {
|
|
532
|
+
input = options.sentinelStart?.(job);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
input = undefined;
|
|
536
|
+
}
|
|
537
|
+
if (input === undefined)
|
|
538
|
+
return false;
|
|
539
|
+
consent = "asking";
|
|
540
|
+
asking = core.notice({ ...input, kind: "sentinel-start" });
|
|
541
|
+
if (asking === undefined)
|
|
542
|
+
consent = "unasked";
|
|
543
|
+
return asking !== undefined;
|
|
544
|
+
}
|
|
545
|
+
/** The chat the question was put in is gone: it is put in the chat the person looks at - none left, the pulses go as without one. */
|
|
546
|
+
function askAgain() {
|
|
547
|
+
const question = asking;
|
|
548
|
+
over(question, "superseded");
|
|
549
|
+
asking = undefined;
|
|
550
|
+
consent = "unasked";
|
|
551
|
+
if (!askToStart(held[0]))
|
|
552
|
+
settleStart("declined", "superseded");
|
|
553
|
+
}
|
|
554
|
+
/** The person answered whether to start the sentinel: written to the project, when they chose for it, and what was held let go. */
|
|
555
|
+
function answerStart(notice, answer) {
|
|
556
|
+
const choice = sentinelStartChoice(notice, answer);
|
|
557
|
+
try {
|
|
558
|
+
if (choice === "always" || choice === "never")
|
|
559
|
+
options.setSentinel?.(choice === "always");
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
core.notify(`config.turri.yaml could not be written: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
563
|
+
}
|
|
564
|
+
settleStart(choice === "start" || choice === "always" ? "given" : "declined", "dismiss" in answer ? "dismissed" : "steer" in answer ? "steered" : "accepted");
|
|
565
|
+
// Their own words are the chat's to act on: the sentinel is not started.
|
|
566
|
+
if ("steer" in answer) {
|
|
567
|
+
const task = noticeTask(notice, answer);
|
|
568
|
+
const chat = chats.find((candidate) => candidate.id === notice.chat) ?? personFacing();
|
|
569
|
+
if (task !== undefined)
|
|
570
|
+
chat?.instance?.act(task);
|
|
571
|
+
}
|
|
572
|
+
return "dismiss" in answer ? "dismissed" : "done";
|
|
573
|
+
}
|
|
574
|
+
/** Whether to start the sentinel is settled: its question closed, and the pulses held go to it - or, declined, to their chats. */
|
|
575
|
+
function settleStart(now, state) {
|
|
576
|
+
consent = now;
|
|
577
|
+
const question = asking;
|
|
578
|
+
asking = undefined;
|
|
579
|
+
if (question !== undefined && question.state === "open")
|
|
580
|
+
over(question, state);
|
|
581
|
+
for (const job of held.splice(0)) {
|
|
582
|
+
if (core.sentinelOn())
|
|
583
|
+
sentinelFinished(job);
|
|
584
|
+
else
|
|
585
|
+
toAsker(job);
|
|
586
|
+
}
|
|
587
|
+
changed();
|
|
588
|
+
}
|
|
494
589
|
/**
|
|
495
590
|
* A pulse ended, with the sentinel on: the chat the person looks at says so - its agent told of one that landed, to
|
|
496
591
|
* tell the person it landed and what the sentinel does with it, and nothing more; of one that went wrong, not, the
|
package/dist/config-command.js
CHANGED
|
@@ -139,6 +139,36 @@ export function registerTurriConfig(pi, options) {
|
|
|
139
139
|
}
|
|
140
140
|
return top.find((setting) => chosen.startsWith(`${setting.key}:`));
|
|
141
141
|
};
|
|
142
|
+
// /sentinel: its one setting by its own name - asked before it first opens in a session, on without asking, or off.
|
|
143
|
+
const sentinelChoices = [
|
|
144
|
+
["ask", "ask", "you are asked, with what it does, before it first opens in a session"],
|
|
145
|
+
["on", "true", "it opens with the first pulse that finishes, without asking"],
|
|
146
|
+
["off", "false", "no sentinel: each pulse is told to the chat that asked for it"],
|
|
147
|
+
];
|
|
148
|
+
pi.registerCommand("sentinel", {
|
|
149
|
+
description: "Whether the sentinel checks every finished pulse: ask first, on, or off",
|
|
150
|
+
getArgumentCompletions: (prefix) => {
|
|
151
|
+
const items = sentinelChoices.filter(([word]) => word.startsWith(prefix.trim().toLowerCase()));
|
|
152
|
+
return items.length === 0 ? null : items.map(([word, , about]) => ({ value: word, label: word, description: about }));
|
|
153
|
+
},
|
|
154
|
+
handler: async (args, ctx) => {
|
|
155
|
+
const setting = settingOf("sentinel");
|
|
156
|
+
const typed = args.trim().toLowerCase();
|
|
157
|
+
let word = typed;
|
|
158
|
+
if (word === "") {
|
|
159
|
+
if (!ctx.hasUI)
|
|
160
|
+
return;
|
|
161
|
+
const now = currentValue(readConfigText(cwd), setting.key);
|
|
162
|
+
const lines = sentinelChoices.map(([choice, , about]) => `${choice} - ${about}`);
|
|
163
|
+
const chosen = await ctx.ui.select(`/sentinel - now: ${now === undefined ? `unset - ${setting.unset}` : shownValue(now)}`, lines);
|
|
164
|
+
if (chosen === undefined)
|
|
165
|
+
return;
|
|
166
|
+
word = chosen.slice(0, chosen.indexOf(" "));
|
|
167
|
+
}
|
|
168
|
+
const value = sentinelChoices.find(([choice]) => choice === word)?.[1] ?? word;
|
|
169
|
+
await apply(setting, value, ctx);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
142
172
|
pi.registerCommand("turri-config", {
|
|
143
173
|
description: `Change a setting of the project's ${CONFIG_FILE}`,
|
|
144
174
|
getArgumentCompletions: (prefix) => {
|
package/dist/config-edit.js
CHANGED
|
@@ -66,7 +66,7 @@ export function settings(rules = []) {
|
|
|
66
66
|
{ key: "mutation-testing", about: "whether the coding agent and the reviewer can put a bug in the code for one test run (can_it_fail)", kind: "boolean", unset: "true", effect: "next-pulse" },
|
|
67
67
|
{ key: "allow-to-stop-jobs", about: "whether turri stops a running pulse the ***plain has moved on from", kind: "boolean", unset: "true", effect: "now" },
|
|
68
68
|
{ key: "release-after-pulse", about: "whether every pulse that lands code releases it", kind: "boolean", unset: "false", effect: "next-landing" },
|
|
69
|
-
{ key: "sentinel", about: "whether a chat of its own checks every finished pulse, tests the release, and tells you what is wrong", kind: "
|
|
69
|
+
{ key: "sentinel", about: "whether a chat of its own checks every finished pulse, tests the release, and tells you what is wrong - ask: you are asked before it first opens", kind: "choice", options: ["ask", "true", "false"], unset: "ask", effect: "next-landing" },
|
|
70
70
|
{ key: "unittests-script", about: "the unit testing script, by path from the project root", kind: "path", unset: "the one run_unittests*.sh found", effect: "next-test-run" },
|
|
71
71
|
{ key: "conformance-tests-script", about: "the conformance testing script, by path from the project root", kind: "path", unset: "the one run_conformance_tests*.sh found", effect: "next-test-run" },
|
|
72
72
|
{ key: "prepare-environment-script", about: "run on the build before every conformance testing script run, by path from the project root - optional", kind: "path", unset: "the one prepare_environment*.sh found, else none", effect: "next-test-run" },
|
|
@@ -119,7 +119,7 @@ export function parseValue(setting, typed, known) {
|
|
|
119
119
|
case "choice": {
|
|
120
120
|
const word = text.toLowerCase();
|
|
121
121
|
if (setting.options?.includes(word))
|
|
122
|
-
return { value:
|
|
122
|
+
return { value: word === "false" ? false : word === "true" ? true : word };
|
|
123
123
|
return { error: `${setting.key} is one of ${setting.options?.join(", ")}.` };
|
|
124
124
|
}
|
|
125
125
|
case "number": {
|
package/dist/config.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* mutation-testing: false # give the coding agent and the reviewer can_it_fail (default: true)
|
|
20
20
|
* allow-to-stop-jobs: false # stop a running job the spec moved on from (default: true)
|
|
21
21
|
* release-after-pulse: true # release what each pulse lands, as its last stage, before the next pulse (default: false)
|
|
22
|
-
* sentinel:
|
|
22
|
+
* sentinel: true # a chat of its own every finished pulse goes to, which tests the release and tells you what is wrong: true, false, or ask - the person asked before it first opens in a session (default: ask)
|
|
23
23
|
* rules-off: [integrations] # ***plain rules to leave out of the prompts, by name (default: none)
|
|
24
24
|
* unittests-script: test_scripts/run_unittests.sh # the testing scripts - see test-scripts.ts
|
|
25
25
|
* conformance-tests-script: test_scripts/run_conformance_tests.sh
|
|
@@ -174,6 +174,8 @@ export declare function projectModels(config: Pick<ProjectConfig, "harnessModel"
|
|
|
174
174
|
export declare function roleModel(config: Pick<ProjectConfig, "harnessModel" | "judgeModel" | "models">, role: AgentRole): ModelChoice | undefined;
|
|
175
175
|
/** How long each turn of a role's agents may take, in milliseconds: its own, `default`'s, `turn-limit:`'s - or undefined, the harness's default. */
|
|
176
176
|
export declare function roleTurnLimitMs(config: Pick<ProjectConfig, "models" | "turnLimit">, role: AgentRole | undefined): number | undefined;
|
|
177
|
+
/** `sentinel:` - on, off, or `ask`: on once the person says so, asked before it first opens in a session. */
|
|
178
|
+
export type SentinelSetting = boolean | "ask";
|
|
177
179
|
export interface ProjectConfig {
|
|
178
180
|
/** The .plain file the module command builds when it is given none. */
|
|
179
181
|
module?: string;
|
|
@@ -228,9 +230,10 @@ export interface ProjectConfig {
|
|
|
228
230
|
releaseAfterPulse?: boolean;
|
|
229
231
|
/**
|
|
230
232
|
* Whether the session has a sentinel: a chat of its own that every finished pulse goes to - checked, a release tested,
|
|
231
|
-
* and what is wrong put to the person - instead of the chat that asked for it (docs/sentinel.md).
|
|
233
|
+
* and what is wrong put to the person - instead of the chat that asked for it (docs/sentinel.md). `ask`: the person is
|
|
234
|
+
* asked, before it first opens in a session, whether to start it. Unset: ask.
|
|
232
235
|
*/
|
|
233
|
-
sentinel?:
|
|
236
|
+
sentinel?: SentinelSetting;
|
|
234
237
|
/** Path from the root to the unit testing script. */
|
|
235
238
|
unittestsScript?: string;
|
|
236
239
|
/** Path from the root to the conformance testing script. */
|
package/dist/config.js
CHANGED
|
@@ -25,7 +25,7 @@ import { projectDirOf } from "./regenerate/paths.js";
|
|
|
25
25
|
* mutation-testing: false # give the coding agent and the reviewer can_it_fail (default: true)
|
|
26
26
|
* allow-to-stop-jobs: false # stop a running job the spec moved on from (default: true)
|
|
27
27
|
* release-after-pulse: true # release what each pulse lands, as its last stage, before the next pulse (default: false)
|
|
28
|
-
* sentinel:
|
|
28
|
+
* sentinel: true # a chat of its own every finished pulse goes to, which tests the release and tells you what is wrong: true, false, or ask - the person asked before it first opens in a session (default: ask)
|
|
29
29
|
* rules-off: [integrations] # ***plain rules to leave out of the prompts, by name (default: none)
|
|
30
30
|
* unittests-script: test_scripts/run_unittests.sh # the testing scripts - see test-scripts.ts
|
|
31
31
|
* conformance-tests-script: test_scripts/run_conformance_tests.sh
|
|
@@ -561,7 +561,7 @@ function readConfigFile(path) {
|
|
|
561
561
|
mutationTesting: typeof raw[CONFIG_KEYS.mutationTesting] === "boolean" ? raw[CONFIG_KEYS.mutationTesting] : undefined,
|
|
562
562
|
allowToStopJobs: typeof raw[CONFIG_KEYS.allowToStopJobs] === "boolean" ? raw[CONFIG_KEYS.allowToStopJobs] : undefined,
|
|
563
563
|
releaseAfterPulse: typeof raw[CONFIG_KEYS.releaseAfterPulse] === "boolean" ? raw[CONFIG_KEYS.releaseAfterPulse] : undefined,
|
|
564
|
-
sentinel: typeof raw[CONFIG_KEYS.sentinel] === "boolean" ? raw[CONFIG_KEYS.sentinel] : undefined,
|
|
564
|
+
sentinel: typeof raw[CONFIG_KEYS.sentinel] === "boolean" || raw[CONFIG_KEYS.sentinel] === "ask" ? raw[CONFIG_KEYS.sentinel] : undefined,
|
|
565
565
|
unittestsScript: text(raw[CONFIG_KEYS.unittestsScript]),
|
|
566
566
|
conformanceTestsScript: text(raw[CONFIG_KEYS.conformanceTestsScript]),
|
|
567
567
|
prepareEnvironmentScript: text(raw[CONFIG_KEYS.prepareEnvironmentScript]),
|
package/dist/extension.js
CHANGED
|
@@ -55,6 +55,7 @@ import { inheritProviders, inheritThinkingLevel } from "./pi.js";
|
|
|
55
55
|
import { beginCapture, captureConsole, describeCaptured, dropNoise } from "./quiet.js";
|
|
56
56
|
import { loadRules, renderRulesBlock } from "./rules.js";
|
|
57
57
|
import { registerTurriConfig } from "./config-command.js";
|
|
58
|
+
import { writeSetting } from "./config-edit.js";
|
|
58
59
|
import { describeStdlib } from "./stdlib.js";
|
|
59
60
|
import { DECLINED, peekKeyListener, runAsk } from "./ask.js";
|
|
60
61
|
import { QuestionDialog } from "./question-dialog.js";
|
|
@@ -64,7 +65,7 @@ import { supersededByTurri } from "./chat-skills.js";
|
|
|
64
65
|
import { conversationIdentity } from "./identity.js";
|
|
65
66
|
import { setSurface, surfaceOf } from "./surface.js";
|
|
66
67
|
import { createChats, scopedToChat } from "./chats.js";
|
|
67
|
-
import { landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief,
|
|
68
|
+
import { landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
|
|
68
69
|
import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
|
|
69
70
|
import { currentChat } from "./chat-scope.js";
|
|
70
71
|
import { registerChats } from "./chat-commands.js";
|
|
@@ -321,8 +322,11 @@ export function createTurriCore(host = {}, options = {}) {
|
|
|
321
322
|
host,
|
|
322
323
|
jobs,
|
|
323
324
|
extension: (pi, link) => turriExtension(pi, core, link),
|
|
324
|
-
// A finished pulse goes to the sentinel when the project wants one - read as each pulse finishes
|
|
325
|
-
sentinel:
|
|
325
|
+
// A finished pulse goes to the sentinel when the project wants one - read as each pulse finishes - the person asked
|
|
326
|
+
// first under `sentinel: ask`, and what they choose for the project written.
|
|
327
|
+
sentinel: () => sentinelSetting(cwd),
|
|
328
|
+
sentinelStart: (job) => sentinelStartNotice(job, cwd),
|
|
329
|
+
setSentinel: (on) => void writeSetting(cwd, "sentinel", on),
|
|
326
330
|
...(options.persist !== undefined ? { persist: options.persist } : {}),
|
|
327
331
|
...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
|
|
328
332
|
});
|
package/dist/host.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createTurri, type Turri, type TurriOptions } from "./host-session.ts";
|
|
8
8
|
export { MAX_CHATS, type ChatInfo, type ChatStatus, type TurriHost } from "./chats.ts";
|
|
9
|
-
export { SENTINEL, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState } from "./notice.ts";
|
|
9
|
+
export { SENTINEL, SENTINEL_START, type Conclusion, type Notice, type NoticeAction, type NoticeAnswer, type NoticeSeverity, type NoticeState } from "./notice.ts";
|
|
10
10
|
export type { AskUserIO } from "./ask.ts";
|
|
11
11
|
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
12
12
|
export * as pi from "@earendil-works/pi-coding-agent";
|
package/dist/host.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { createTurri } from "./host-session.js";
|
|
9
9
|
export { MAX_CHATS } from "./chats.js";
|
|
10
10
|
// The sentinel's notices: what it puts to the person, and how they answer.
|
|
11
|
-
export { SENTINEL } from "./notice.js";
|
|
11
|
+
export { SENTINEL, SENTINEL_START } from "./notice.js";
|
|
12
12
|
/** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
|
|
13
13
|
export * as pi from "@earendil-works/pi-coding-agent";
|
|
14
14
|
// The project and its settings.
|
package/dist/init-config.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export declare const DEFAULTS: {
|
|
|
113
113
|
readonly mutationTesting: true;
|
|
114
114
|
readonly allowToStopJobs: true;
|
|
115
115
|
readonly releaseAfterPulse: true;
|
|
116
|
-
readonly sentinel:
|
|
116
|
+
readonly sentinel: "ask";
|
|
117
117
|
readonly testingScriptsRunOn: "in-place";
|
|
118
118
|
readonly turnLimit: 120;
|
|
119
119
|
readonly compactAt: 100000;
|
package/dist/init-config.js
CHANGED
|
@@ -220,7 +220,7 @@ export const DEFAULTS = {
|
|
|
220
220
|
mutationTesting: true,
|
|
221
221
|
allowToStopJobs: true,
|
|
222
222
|
releaseAfterPulse: true,
|
|
223
|
-
sentinel:
|
|
223
|
+
sentinel: "ask",
|
|
224
224
|
testingScriptsRunOn: "in-place",
|
|
225
225
|
turnLimit: 120,
|
|
226
226
|
compactAt: 100000,
|
|
@@ -266,7 +266,8 @@ export function quickStartConfig(vendor = "other", bedrock, models) {
|
|
|
266
266
|
`release-after-pulse: ${DEFAULTS.releaseAfterPulse}`,
|
|
267
267
|
"",
|
|
268
268
|
"# Whether a chat of its own, the sentinel, checks every pulse that finishes - and, with release-after-pulse, tests what",
|
|
269
|
-
"# was released - and tells you in the chat you are in when something needs you.",
|
|
269
|
+
"# was released - and tells you in the chat you are in when something needs you. ask: you are asked, with what it does,",
|
|
270
|
+
"# before it first opens in a session; true: it opens without asking; false: none.",
|
|
270
271
|
`sentinel: ${DEFAULTS.sentinel}`,
|
|
271
272
|
"",
|
|
272
273
|
"# What the testing scripts run on: in-place - the code itself; copy - a fresh copy of it.",
|
package/dist/notice.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface NoticeAction {
|
|
|
13
13
|
task: string;
|
|
14
14
|
}
|
|
15
15
|
export interface NoticeInput {
|
|
16
|
+
/**
|
|
17
|
+
* `sentinel-start`: not something wrong but a question turri puts before the sentinel first opens in a session - its
|
|
18
|
+
* actions start it, or not, and are turri's to carry out, not the chat's agent's (SENTINEL_START). None: the sentinel's.
|
|
19
|
+
*/
|
|
20
|
+
kind?: "sentinel-start";
|
|
16
21
|
severity: NoticeSeverity;
|
|
17
22
|
/** What is wrong, in a line. */
|
|
18
23
|
summary: string;
|
|
@@ -33,6 +38,14 @@ export interface Notice extends NoticeInput {
|
|
|
33
38
|
state: NoticeState;
|
|
34
39
|
at: string;
|
|
35
40
|
}
|
|
41
|
+
/** The answers to the question put before the sentinel first opens: the actions of a `sentinel-start` notice, in order. */
|
|
42
|
+
export declare const SENTINEL_START: {
|
|
43
|
+
readonly start: "Start the sentinel";
|
|
44
|
+
readonly always: "Start it, and don't ask again";
|
|
45
|
+
readonly notNow: "Not this session";
|
|
46
|
+
readonly never: "Turn it off for this project";
|
|
47
|
+
};
|
|
48
|
+
export type SentinelStartChoice = keyof typeof SENTINEL_START;
|
|
36
49
|
/** How the person answered a notice: one of its actions (by index or label), their own words, or not at all. */
|
|
37
50
|
export type NoticeAnswer = {
|
|
38
51
|
action: number | string;
|
|
@@ -42,6 +55,8 @@ export type NoticeAnswer = {
|
|
|
42
55
|
dismiss: true;
|
|
43
56
|
};
|
|
44
57
|
export declare function newNotice(input: NoticeInput, chat: string): Notice;
|
|
58
|
+
/** The choice an answer to a `sentinel-start` notice makes: dismissed, or answered in the person's own words, it is not now. */
|
|
59
|
+
export declare function sentinelStartChoice(notice: Notice, answer: NoticeAnswer): SentinelStartChoice;
|
|
45
60
|
/** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
|
|
46
61
|
export declare function noticeTask(notice: Notice, answer: NoticeAnswer): string | undefined;
|
|
47
62
|
/** A notice in words: its summary, its evidence and what can be done - for the agent, the log and a chat without a card. */
|
package/dist/notice.js
CHANGED
|
@@ -5,9 +5,23 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
*/
|
|
6
6
|
/** Its name in the strip, the host's chat list, and the ledger. */
|
|
7
7
|
export const SENTINEL = "sentinel";
|
|
8
|
+
/** The answers to the question put before the sentinel first opens: the actions of a `sentinel-start` notice, in order. */
|
|
9
|
+
export const SENTINEL_START = {
|
|
10
|
+
start: "Start the sentinel",
|
|
11
|
+
always: "Start it, and don't ask again",
|
|
12
|
+
notNow: "Not this session",
|
|
13
|
+
never: "Turn it off for this project",
|
|
14
|
+
};
|
|
8
15
|
export function newNotice(input, chat) {
|
|
9
16
|
return { ...input, id: randomUUID().slice(0, 8), chat, state: "open", at: new Date().toISOString() };
|
|
10
17
|
}
|
|
18
|
+
/** The choice an answer to a `sentinel-start` notice makes: dismissed, or answered in the person's own words, it is not now. */
|
|
19
|
+
export function sentinelStartChoice(notice, answer) {
|
|
20
|
+
if (!("action" in answer))
|
|
21
|
+
return "notNow";
|
|
22
|
+
const label = typeof answer.action === "number" ? notice.actions[answer.action]?.label : answer.action;
|
|
23
|
+
return Object.keys(SENTINEL_START).find((choice) => SENTINEL_START[choice] === label) ?? "notNow";
|
|
24
|
+
}
|
|
11
25
|
/** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
|
|
12
26
|
export function noticeTask(notice, answer) {
|
|
13
27
|
if ("dismiss" in answer)
|
package/dist/sentinel-chat.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import type { ChatLink, TurriCore } from "./chats.ts";
|
|
|
3
3
|
import { type Conclusion, type Notice } from "./sentinel.ts";
|
|
4
4
|
/** The sentinel's tools and model, built into its chat. */
|
|
5
5
|
export declare function registerSentinel(pi: ExtensionAPI, session: TurriCore, cwd: string, hostTools: ToolDefinition[], allowed: Set<string>): void;
|
|
6
|
+
/**
|
|
7
|
+
* The terminal's question whether to start the sentinel: the sentence, and how to switch to it - it opens last in the
|
|
8
|
+
* strip, after the `open` chats there are. A host has its own way to it, and is given the sentence alone.
|
|
9
|
+
*/
|
|
10
|
+
export declare function startTitle(notice: Notice, open: number): string;
|
|
6
11
|
/**
|
|
7
12
|
* A chat's notices: each drawn as a card, and - with no host to answer it - put to the person as a dialog once the chat
|
|
8
13
|
* is idle, one at a time: an action accepted, their own words, or dismissed. A notice that is over closes its dialog.
|
package/dist/sentinel-chat.js
CHANGED
|
@@ -54,6 +54,13 @@ export function registerSentinel(pi, session, cwd, hostTools, allowed) {
|
|
|
54
54
|
pi.setThinkingLevel(choice.thinking);
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The terminal's question whether to start the sentinel: the sentence, and how to switch to it - it opens last in the
|
|
59
|
+
* strip, after the `open` chats there are. A host has its own way to it, and is given the sentence alone.
|
|
60
|
+
*/
|
|
61
|
+
export function startTitle(notice, open) {
|
|
62
|
+
return `${notice.summary}\nUse "/chat ${open + 1}" to open the sentinel.`;
|
|
63
|
+
}
|
|
57
64
|
/**
|
|
58
65
|
* A chat's notices: each drawn as a card, and - with no host to answer it - put to the person as a dialog once the chat
|
|
59
66
|
* is idle, one at a time: an action accepted, their own words, or dismissed. A notice that is over closes its dialog.
|
|
@@ -114,8 +121,10 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
114
121
|
const controller = new AbortController();
|
|
115
122
|
asking.set(notice.id, controller);
|
|
116
123
|
try {
|
|
117
|
-
|
|
118
|
-
const
|
|
124
|
+
// Whether to start the sentinel is turri's question, its answers its own: no words of the person's to act on.
|
|
125
|
+
const starting = notice.kind === "sentinel-start";
|
|
126
|
+
const options = [...notice.actions.map((action, index) => `${index + 1}. ${action.label}`), ...(starting ? [] : [STEER]), DISMISS];
|
|
127
|
+
const picked = await ctx.ui.select(starting ? startTitle(notice, session.chats().length) : `sentinel: ${notice.summary}`, options, { signal: controller.signal });
|
|
119
128
|
if (controller.signal.aborted || notice.state !== "open")
|
|
120
129
|
return;
|
|
121
130
|
if (picked === STEER) {
|
package/dist/sentinel.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type SentinelSetting } from "./config.ts";
|
|
2
3
|
import { type Job } from "./regenerate/jobs.ts";
|
|
3
4
|
import type { DeployedProcess } from "./regenerate/release.ts";
|
|
4
5
|
import { type Conclusion, type Notice, type NoticeInput } from "./notice.ts";
|
|
@@ -10,8 +11,13 @@ export * from "./notice.ts";
|
|
|
10
11
|
* file tools only read, its shell runs with the project read only, and it has none of the tools that change ***plain or
|
|
11
12
|
* start work. The chat the person talks to is never interrupted for a pulse that went well.
|
|
12
13
|
*/
|
|
13
|
-
/** Whether the project wants a sentinel: `sentinel:` in config.turri.yaml
|
|
14
|
-
export declare function
|
|
14
|
+
/** Whether the project wants a sentinel: `sentinel:` in config.turri.yaml - true, false, or ask, as it is unset. */
|
|
15
|
+
export declare function sentinelSetting(cwd: string): SentinelSetting;
|
|
16
|
+
/**
|
|
17
|
+
* What the person is asked before the sentinel first opens in a session, under `sentinel: ask`, in one sentence: the pulse
|
|
18
|
+
* that would open it and what it does - so a tab that starts working on its own is not a surprise - and whether to start it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sentinelStartNotice(job: Job, cwd: string): NoticeInput;
|
|
15
21
|
/** A pulse that stopped on ***plain that contradicts itself: put to the person by turri itself, the way out the chat's to work out. */
|
|
16
22
|
export declare function specConflictNotice(job: Job, task: string): NoticeInput;
|
|
17
23
|
export declare function guidanceFile(cwd: string): string;
|
package/dist/sentinel.js
CHANGED
|
@@ -10,7 +10,7 @@ import { formatDuration, pulseName } from "./regenerate/jobs.js";
|
|
|
10
10
|
import { readLedger } from "./regenerate/ledger.js";
|
|
11
11
|
import { describePulseView, pulseView } from "./regenerate/ledger-view.js";
|
|
12
12
|
import { MODULES_DIR, ensureInScratch } from "./regenerate/paths.js";
|
|
13
|
-
import { SENTINEL, wentWrong } from "./notice.js";
|
|
13
|
+
import { SENTINEL, SENTINEL_START, wentWrong } from "./notice.js";
|
|
14
14
|
export * from "./notice.js";
|
|
15
15
|
/**
|
|
16
16
|
* The sentinel: a chat of the session's own that every finished pulse goes to (docs/sentinel.md). It checks how each
|
|
@@ -19,15 +19,42 @@ export * from "./notice.js";
|
|
|
19
19
|
* file tools only read, its shell runs with the project read only, and it has none of the tools that change ***plain or
|
|
20
20
|
* start work. The chat the person talks to is never interrupted for a pulse that went well.
|
|
21
21
|
*/
|
|
22
|
-
/** Whether the project wants a sentinel: `sentinel:` in config.turri.yaml
|
|
23
|
-
export function
|
|
22
|
+
/** Whether the project wants a sentinel: `sentinel:` in config.turri.yaml - true, false, or ask, as it is unset. */
|
|
23
|
+
export function sentinelSetting(cwd) {
|
|
24
24
|
try {
|
|
25
|
-
return readProjectConfig(cwd).sentinel
|
|
25
|
+
return readProjectConfig(cwd).sentinel ?? "ask";
|
|
26
26
|
}
|
|
27
27
|
catch {
|
|
28
|
-
return
|
|
28
|
+
return "ask";
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What the person is asked before the sentinel first opens in a session, under `sentinel: ask`, in one sentence: the pulse
|
|
33
|
+
* that would open it and what it does - so a tab that starts working on its own is not a surprise - and whether to start it.
|
|
34
|
+
*/
|
|
35
|
+
export function sentinelStartNotice(job, cwd) {
|
|
36
|
+
let released = false;
|
|
37
|
+
try {
|
|
38
|
+
released = readProjectConfig(cwd).releaseAfterPulse === true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* a config that cannot be read releases nothing */
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: "sentinel-start",
|
|
45
|
+
severity: "info",
|
|
46
|
+
summary: `${pulseName(job)} finished - start the sentinel, a background tab that reviews each finished pulse${released ? " and tries to use what was released" : ""}?`,
|
|
47
|
+
evidence: [],
|
|
48
|
+
actions: [
|
|
49
|
+
{ label: SENTINEL_START.start, task: "The sentinel is started for this session: this pulse and each after it go to it." },
|
|
50
|
+
{ label: SENTINEL_START.always, task: "The sentinel is started, and sentinel: true written to config.turri.yaml: it opens without asking from now on." },
|
|
51
|
+
{ label: SENTINEL_START.notNow, task: "No sentinel this session: each pulse is told to the chat that asked for it. turri asks again next time." },
|
|
52
|
+
{ label: SENTINEL_START.never, task: "sentinel: false written to config.turri.yaml: no sentinel, and no asking." },
|
|
53
|
+
],
|
|
54
|
+
job: job.id,
|
|
55
|
+
...(job.pulse !== undefined ? { pulse: job.pulse } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
31
58
|
/** A pulse that stopped on ***plain that contradicts itself: put to the person by turri itself, the way out the chat's to work out. */
|
|
32
59
|
export function specConflictNotice(job, task) {
|
|
33
60
|
return {
|
package/docs/chats.md
CHANGED
|
@@ -21,7 +21,7 @@ The session's, shared by every chat:
|
|
|
21
21
|
- **what `activate` started** - it runs until turri ends or `/halt activated <name>`, whatever chats are opened or closed;
|
|
22
22
|
- **the ledger** - one run, each event with the id of the chat its work was asked for in (`chat`), and `chat.opened` and `chat.closed` as they happen.
|
|
23
23
|
|
|
24
|
-
A pulse's end goes to the [sentinel](sentinel.md) - a chat of turri's own, opened in the background with the first pulse that finishes - which checks it, tests what was released, and puts what needs you as a notice in the chat you are in. That chat shows the pulse's end; its agent is told of a pulse that landed, to tell you so and what the sentinel is doing with it - and when the sentinel is done with any pulse, what it found - and nothing more - and not of one that went wrong: the sentinel's notice is what it hears of that. With `sentinel: false`, a pulse's end is told to the chat that asked for it instead, shown or not - its agent reacts to it there; the chat's mark in the strip says it has something new. When that chat has been closed, the chat shown is told. Closing a chat stops its turn, never its pulses.
|
|
24
|
+
A pulse's end goes to the [sentinel](sentinel.md) - a chat of turri's own, opened in the background with the first pulse that finishes, once you said to start it under `sentinel: ask` - which checks it, tests what was released, and puts what needs you as a notice in the chat you are in. That chat shows the pulse's end; its agent is told of a pulse that landed, to tell you so and what the sentinel is doing with it - and when the sentinel is done with any pulse, what it found - and nothing more - and not of one that went wrong: the sentinel's notice is what it hears of that. With `sentinel: false`, a pulse's end is told to the chat that asked for it instead, shown or not - its agent reacts to it there; the chat's mark in the strip says it has something new. When that chat has been closed, the chat shown is told. Closing a chat stops its turn, never its pulses.
|
|
25
25
|
|
|
26
26
|
## In the terminal
|
|
27
27
|
|
package/docs/config.md
CHANGED
|
@@ -90,7 +90,7 @@ models: # each agent role's model, thinking and tur
|
|
|
90
90
|
| `rules-off` | a list of rule names - `integrations`, `concise-specs`, the files of `genome/rules/` without `.md` - left out of the conversation's prompt and every \*\*\*plain-writing agent's: a rule this project's own skills or AGENTS.md replace, such as one that says to probe a live API before any \*\*\*plain is written. `turri init-config` writes it with `integration-embedded`, `integration-embedded-testing` and `integration-standalone` - take them out when the project writes an integration against a REST API. The project's alone | none |
|
|
91
91
|
| `allow-to-stop-jobs` | `true` or `false`: whether turri stops a running job \*\*\*plain has moved on from - a functional spec it regenerates changed, or a file one links was edited - rolls it back and regenerates \*\*\*plain as it now is. `false`: the job runs to its end and lands, and what changed since is regenerated on top of it by the next job - see [regenerating the code](regenerating.md). Your own `halt` stops a job either way | `true` |
|
|
92
92
|
| `release-after-pulse` | `true` or `false`: whether every pulse that lands code releases it, in the chat, as its last stage - before the module's next pulse starts, whatever the chat is doing: an agent on the coding model releases it, following the project's release guidance as the chat finds it (what was passed to `release` this conversation, AGENTS.md, the project's skills) for how, or to `dist/<module>` without any - see [deploy](deploy.md#after-every-pulse). One release a pulse that lands; a release that fails is said in the pulse's report, and what it landed stays. `turri init-config` writes `true` | `false` |
|
|
93
|
-
| `sentinel` | `true` or `false`: whether a chat of turri's own, the sentinel, gets every pulse that finishes - checks it, tests what was released under `release-after-pulse`, and puts what needs you as a notice in the chat you are in - instead of the chat that asked for it.
|
|
93
|
+
| `sentinel` | `ask`, `true` or `false`: whether a chat of turri's own, the sentinel, gets every pulse that finishes - checks it, tests what was released under `release-after-pulse`, and puts what needs you as a notice in the chat you are in - instead of the chat that asked for it. `ask`: you are asked, with what it does, before it first opens in a session - see [starting it](sentinel.md#starting-it); `true`: it opens without asking. `/sentinel` sets it from the chat. `turri init-config` writes `ask` | `ask` |
|
|
94
94
|
| `unittests-script` | the unit testing script, by path from the project root - it runs the code's static checks too, exiting 80 when only they fail (see [the static checks](regenerating.md#the-static-checks)) | looked for - see below |
|
|
95
95
|
| `conformance-tests-script` | the conformance testing script, by path from the project root | looked for - see below |
|
|
96
96
|
| `prepare-environment-script` | optional: run as `<script> <build>` before every run of the conformance testing script, which runs only once it passes - see [the testing scripts](regenerating.md#the-testing-scripts). turri never writes one | the one `prepare_environment*.sh` at the root or in `test_scripts/`, else none |
|
package/docs/hosting.md
CHANGED
|
@@ -123,6 +123,7 @@ await turri.dispose(); // every chat let go, what activate start
|
|
|
123
123
|
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.
|
|
124
124
|
|
|
125
125
|
- **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.
|
|
126
|
+
- **Starting it.** Under `sentinel: ask` - as it is unset - the person is asked before it first opens in the session: a notice with `kind: "sentinel-start"`, `severity: "info"`, in the chat `chat` names, its `summary` the question in one sentence, no `evidence`, and its actions the answers - `Start the sentinel`, `Start it, and don't ask again`, `Not this session`, `Turn it off for this project`. Draw it as a question rather than as something wrong. `answer(id, { action })` answers it, by index or label - `"done"` - and `{ dismiss: true }` is not this session; turri writes `sentinel:` itself for the two that choose for the project. Until it is answered, the pulses that finish are held: neither the sentinel nor the chat hears of them. A host that offers the setting too writes it as `/turri-config` does - `ask`, `true` or `false` (see `configSettings`).
|
|
126
127
|
- **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.
|
|
127
128
|
- **Its model.** The `sentinel` role in `models:`, else the chat's model - `models.chat`, else `harness-model` - with its thinking, set as its chat opens: not pi's default model, which may not be one the project's provider has. What the person then picks in its chat stands. See [its model](sentinel.md#its-model).
|
|
128
129
|
- **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.
|
package/docs/sentinel.md
CHANGED
|
@@ -2,12 +2,25 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
|
|
5
|
+
`sentinel:` in `config.turri.yaml` says whether there is one: `ask`, as it is unset - you are asked before it first opens in a session (below); `true` - it opens without asking; `false` - none. 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
|
+
## Starting it
|
|
8
|
+
|
|
9
|
+
Under `sentinel: ask`, a tab that starts working on its own is not sprung on you. When the first pulse of a session finishes, turri asks first - a notice of its own in the chat you are in, before the sentinel opens, in one sentence: `app · regenerate pulse 3 finished - start the sentinel, a background tab that reviews each finished pulse and tries to use what was released?` - the release left out with `release-after-pulse` off. In the terminal it is a dialog once that chat is not mid-turn, with how to switch to the sentinel once it is open - `Use "/chat 2" to open the sentinel.`, the number the one it will have in the strip; in a host, a notice with `kind: "sentinel-start"` (see [in a host](#in-a-host)). You answer:
|
|
10
|
+
|
|
11
|
+
- **Start the sentinel** - it opens, and checks this pulse and each after it this session;
|
|
12
|
+
- **Start it, and don't ask again** - the same, and `sentinel: true` is written to `config.turri.yaml`;
|
|
13
|
+
- **Not this session**, or `esc` - no sentinel until turri starts again: each pulse is told to the chat that asked for it, as without one;
|
|
14
|
+
- **Turn it off for this project** - `sentinel: false` is written.
|
|
15
|
+
|
|
16
|
+
Nothing goes to the sentinel, or to your chat, until you answer: the pulses that finish meanwhile are held, and go to it - or to their chats - as you say. You are asked once a session: a sentinel closed and opened again by the next pulse is not asked for again, nor one `turri --continue` reopened. Close the chat it was asked in, and it is asked again in the one you look at.
|
|
17
|
+
|
|
18
|
+
`/sentinel` sets it from the chat - `/sentinel ask`, `/sentinel on`, `/sentinel off`, or `/sentinel` alone to pick - written to `config.turri.yaml` as `/turri-config sentinel` does.
|
|
6
19
|
|
|
7
20
|
## What you see
|
|
8
21
|
|
|
9
22
|
- **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.
|
|
23
|
+
- **The sentinel** opens with the first pulse that finishes - once you said to start it, under `sentinel: ask` - 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
24
|
- **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
25
|
- `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
26
|
- **Say what to do instead…** - your own words, and the agent does that;
|
|
@@ -37,7 +50,7 @@ Switch to its chat and say how to check your software - *test the sign-up flow a
|
|
|
37
50
|
|
|
38
51
|
## Closing it
|
|
39
52
|
|
|
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.
|
|
53
|
+
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 `/sentinel off` or `/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
54
|
|
|
42
55
|
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
56
|
|
|
@@ -47,8 +60,8 @@ The `sentinel` role in `models:` - else the chat's model, `models.chat` else `ha
|
|
|
47
60
|
|
|
48
61
|
## In a host
|
|
49
62
|
|
|
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. `sentinel: { surface }` says, in the host's words, where the person watches it use them: then it tries a release with them, where the person can watch, and keeps its shell for what they cannot show - logs, exit codes, files. Without it, the person does not watch the sentinel, and the host's tools are only to test with as well. See [running turri in a host](hosting.md#the-sentinel).
|
|
63
|
+
A host sees the sentinel as one of its chats - `role: "sentinel"` in `chats()` and `onChats` - and never has focus moved to it. Under `sentinel: ask`, the question before it first opens comes as a notice like the others, with `kind: "sentinel-start"`, its actions the four answers: `answer(id, { action })` answers it - by index or label - and `{ dismiss: true }` is not this session. 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. `sentinel: { surface }` says, in the host's words, where the person watches it use them: then it tries a release with them, where the person can watch, and keeps its shell for what they cannot show - logs, exit codes, files. Without it, the person does not watch the sentinel, and the host's tools are only to test with as well. See [running turri in a host](hosting.md#the-sentinel).
|
|
51
64
|
|
|
52
65
|
## In the ledger
|
|
53
66
|
|
|
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`.
|
|
67
|
+
`chat.opened` carries `role: "sentinel"` for its chat; `notice.put` and `notice.over` record each notice - the question before it opens among them - - 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
|
@@ -81,6 +81,7 @@ A regeneration takes minutes, so in the TUI every tool that regenerates starts t
|
|
|
81
81
|
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, one side has the keys and the other is drawn faint: `ctrl+h` switches sides - from the chat to the panel, from the panel to the chat, the panel staying and following the pulses - `esc` gives the chat the keys too, and a click gives them to the side clicked. `ctrl+c` closes the panel; so do `s`, over the whole screen, then `esc`. ([pi](https://pi.dev)'s regular screen, `--tui-mode regular`, reads no mouse: there it is the keys alone.) While the panel has the keys, every key is the panel's, whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat, unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting for your answer". Back in the chat, the question is there, with the keys.
|
|
82
82
|
- `turri` runs [pi](https://pi.dev) in fullscreen mode, so the conversation stays where you scroll it while the pulses redraw (PgUp/PgDn, the wheel); `--tui-mode regular` goes back to the terminal's own scrollback.
|
|
83
83
|
- **`/turri-config`** changes a setting of the project's `config.turri.yaml` - the models, review, conformance tests, any key - and says when it takes effect: now, at your next message, or at the next pulse; never only after a restart. See [changing it from the chat](config.md#changing-it-from-the-chat).
|
|
84
|
+
- **`/sentinel`** says whether the [sentinel](sentinel.md) checks every pulse that finishes: `ask` - you are asked before it first opens in a session - `on`, or `off`; written to `config.turri.yaml`.
|
|
84
85
|
- **`/stack`** chooses the stack everything is built with from then on, in a project whose parts are on different stacks: a list of the project's stacks, or `/stack backend`, `/stack none`. The stack's `config.turri.<stack>.yaml` is laid over `config.turri.yaml` - its testing scripts, toolchains, models - for the session; a module already asked for keeps the stack it was asked for with. The chat model switches it too when you ask - and, to build one part, passes that part's stack to `regenerate` instead, leaving the session's as it is. See [stacks](config.md#stacks).
|
|
85
86
|
- **`/regenerate [module | file.plain] [functionality ...] [from scratch [whole chain]] [how to cut the work]`** regenerates a module from its \*\*\*plain as it is now, as the `regenerate` tool does: what is left to regenerate is planned into pulses - one functional spec at a time, a few together or the whole module in one, as the planner judges - and the first starts. Words after the module are guidance for that plan (`/regenerate app models first`). A module's name typed with spaces for its underscores is that module - `/regenerate d365_function executor` is `d365_function_executor`. With no module named: the one `module:` in `config.turri.yaml` names, else the folder's top modules, to pick from. Asked for from scratch - `from scratch`, `start over`, `delete the module` - it starts at once, with nothing to confirm: it deletes the module's code and conformance tests in every codebase where they live, committed so `/lineage` keeps them, and regenerates the whole module from its \*\*\*plain, built though nothing in it changed: `/regenerate shorty from scratch`. The modules below it keep their code, and the module starts again from a copy of the code below it. Asked for its chain too - `/regenerate shorty from scratch, the whole chain` - the code of every module of its requires chain is deleted, the base's included, and the whole chain is regenerated from the bottom: what to do after modules were split, merged or moved, when the code below was laid out for modules that are no more. The chat's `regenerate` does the same with `from_scratch: "module"` or `"chain"`, when you ask for it; `turri regenerate --from-scratch` or `--from-scratch-chain` on the command line, not with `--dry-run`. Functionalities named after the module - `/regenerate shorty 3 5` - are regenerated though their \*\*\*plain did not change, on the model chosen for functionalities: as `turri regenerate shorty.plain 3 5` does, and the chat's `regenerate` with `functionalities`.
|
|
86
87
|
- **`/halt <id>`** halts a pulse - a queued one never starts - **`/halt <module>`** (or a planned row's id) halts a module's planned pulses until you ask for it again, a \*\*\*plain change not starting it, and **`/halt activated <name>`** halts a server `activate` started; it lists all three when no id is given.
|
|
@@ -43,7 +43,7 @@ The other rules say what a spec means. This one says what turri does with a chan
|
|
|
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
|
- Pulses regenerate in the background: the spec can be changed while they run, and need not wait for them. A change inside what a running pulse regenerates steers it; the rest is planned again around it
|
|
45
45
|
- 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
|
|
46
|
-
- A finished pulse goes to the sentinel, a chat of turri's own - unless `sentinel: false
|
|
46
|
+
- A finished pulse goes to the sentinel, a chat of turri's own - unless `sentinel: false`; under `sentinel: ask`, the default, only once the person said to start it, asked by turri before it first opens in a session (declined, a pulse goes to the chat that asked for it, as with `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
|
|
47
47
|
|
|
48
48
|
## Where the cost is
|
|
49
49
|
|