@turri-dev/cli 0.1.25 → 0.1.26-next.f80f259
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/chat-records.d.ts +4 -0
- package/dist/chat-records.js +5 -0
- package/dist/chats.d.ts +25 -8
- package/dist/chats.js +280 -43
- 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 +12 -9
- package/dist/host-session.d.ts +4 -2
- 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 +33 -2
- package/dist/notice.js +24 -1
- package/dist/regenerate/ledger.d.ts +4 -0
- package/dist/sentinel-chat.d.ts +5 -0
- package/dist/sentinel-chat.js +30 -3
- package/dist/sentinel.d.ts +8 -2
- package/dist/sentinel.js +49 -11
- package/docs/chats.md +2 -2
- package/docs/config.md +1 -1
- package/docs/hosting.md +7 -3
- package/docs/sentinel.md +28 -10
- package/docs/using.md +2 -1
- package/genome/rules/turri-regeneration.md +1 -1
- package/package.json +1 -1
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 { ignoredGuidance, keepGuidance, 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,12 @@ 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),
|
|
330
|
+
ignored: (notice) => keepGuidance(cwd, ignoredGuidance(notice)),
|
|
326
331
|
...(options.persist !== undefined ? { persist: options.persist } : {}),
|
|
327
332
|
...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
|
|
328
333
|
});
|
|
@@ -437,11 +442,10 @@ function turriExtension(api, session, link) {
|
|
|
437
442
|
details: { jobId: job.id, status: job.status },
|
|
438
443
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
439
444
|
};
|
|
440
|
-
// The sentinel's: what a pulse that ended is, told to its agent
|
|
445
|
+
// The sentinel's: what a pulse that ended is, told to its agent in a conversation of its own for the pulse. The hidden
|
|
446
|
+
// message is the conversation's first entry - a host splits the sentinel's work by pulse on it - so the pulse's end is
|
|
447
|
+
// not drawn above it here: the person's chat shows it.
|
|
441
448
|
const brief = async (job, context) => {
|
|
442
|
-
if (context.replace)
|
|
443
|
-
await link.session()?.abort();
|
|
444
|
-
announceJob(pi, job);
|
|
445
449
|
const config = readProjectConfig(cwd);
|
|
446
450
|
const content = sentinelBrief(job, {
|
|
447
451
|
project: cwd,
|
|
@@ -451,8 +455,7 @@ function turriExtension(api, session, link) {
|
|
|
451
455
|
releaseAfterPulse: config.releaseAfterPulse === true,
|
|
452
456
|
reaction: JOB_FINISHED_REACTION,
|
|
453
457
|
});
|
|
454
|
-
|
|
455
|
-
void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true, ...(busy ? { deliverAs: "followUp" } : {}) });
|
|
458
|
+
void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true });
|
|
456
459
|
};
|
|
457
460
|
// A pulse ended, with the sentinel on: said in the chat. One that landed is told to its agent too, to tell the person
|
|
458
461
|
// it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
|
package/dist/host-session.d.ts
CHANGED
|
@@ -68,9 +68,11 @@ export interface Turri {
|
|
|
68
68
|
notices(): Notice[];
|
|
69
69
|
/**
|
|
70
70
|
* A notice answered: one of its actions (by index or label) - the chat it is in started on it - the person's own words
|
|
71
|
-
* about it, or
|
|
71
|
+
* about it, dismissed, or - `ignorable: true` - ignored for good: left as the build has it, the sentinel told not to
|
|
72
|
+
* raise it again. `refused` for `ignore` on one that is not ignorable, `over` when it was answered or superseded
|
|
73
|
+
* already, `missing` for one never put.
|
|
72
74
|
*/
|
|
73
|
-
answer(id: string, answer: NoticeAnswer): Promise<"done" | "dismissed" | "missing" | "over">;
|
|
75
|
+
answer(id: string, answer: NoticeAnswer): Promise<"done" | "dismissed" | "ignored" | "refused" | "missing" | "over">;
|
|
74
76
|
/** Told when a chat is opened, closed, renamed, or starts or stops working. */
|
|
75
77
|
onChats(listener: (chats: ChatInfo[]) => void): () => void;
|
|
76
78
|
/** Every chat let go, and what activate started stopped. */
|
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;
|
|
@@ -24,8 +29,14 @@ export interface NoticeInput {
|
|
|
24
29
|
job?: string;
|
|
25
30
|
/** Its number among its module's pulses. */
|
|
26
31
|
pulse?: number;
|
|
32
|
+
/**
|
|
33
|
+
* The person may leave it as the build has it, for good (`{ ignore: true }`): a gap in the ***plain the build settled
|
|
34
|
+
* with an assumption - never something broken, a pulse that went wrong, a release that did not come up, nor the
|
|
35
|
+
* question whether to start the sentinel.
|
|
36
|
+
*/
|
|
37
|
+
ignorable?: boolean;
|
|
27
38
|
}
|
|
28
|
-
export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "superseded";
|
|
39
|
+
export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "ignored" | "superseded";
|
|
29
40
|
export interface Notice extends NoticeInput {
|
|
30
41
|
id: string;
|
|
31
42
|
/** The chat it is put in: the one the person looks at, never the sentinel's. */
|
|
@@ -33,17 +44,37 @@ export interface Notice extends NoticeInput {
|
|
|
33
44
|
state: NoticeState;
|
|
34
45
|
at: string;
|
|
35
46
|
}
|
|
36
|
-
/**
|
|
47
|
+
/** The answers to the question put before the sentinel first opens: the actions of a `sentinel-start` notice, in order. */
|
|
48
|
+
export declare const SENTINEL_START: {
|
|
49
|
+
readonly start: "Start the sentinel";
|
|
50
|
+
readonly always: "Start it, and don't ask again";
|
|
51
|
+
readonly notNow: "Not this session";
|
|
52
|
+
readonly never: "Turn it off for this project";
|
|
53
|
+
};
|
|
54
|
+
export type SentinelStartChoice = keyof typeof SENTINEL_START;
|
|
55
|
+
/**
|
|
56
|
+
* How the person answered a notice: one of its actions (by index or label), their own words, not at all this once
|
|
57
|
+
* (`dismiss`) - or, on an ignorable one, not at all and not again (`ignore`): left as the build has it.
|
|
58
|
+
*/
|
|
37
59
|
export type NoticeAnswer = {
|
|
38
60
|
action: number | string;
|
|
39
61
|
} | {
|
|
40
62
|
steer: string;
|
|
41
63
|
} | {
|
|
42
64
|
dismiss: true;
|
|
65
|
+
} | {
|
|
66
|
+
ignore: true;
|
|
43
67
|
};
|
|
44
68
|
export declare function newNotice(input: NoticeInput, chat: string): Notice;
|
|
69
|
+
/** The choice an answer to a `sentinel-start` notice makes: dismissed, or answered in the person's own words, it is not now. */
|
|
70
|
+
export declare function sentinelStartChoice(notice: Notice, answer: NoticeAnswer): SentinelStartChoice;
|
|
45
71
|
/** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
|
|
46
72
|
export declare function noticeTask(notice: Notice, answer: NoticeAnswer): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* What the sentinel keeps of a notice the person ignored, in its guidance: what was left as it is - its module, the gap,
|
|
75
|
+
* what the build does - and why, so a fresh conversation does not raise it again.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ignoredGuidance(notice: Notice): string;
|
|
47
78
|
/** A notice in words: its summary, its evidence and what can be done - for the agent, the log and a chat without a card. */
|
|
48
79
|
export declare function describeNotice(notice: NoticeInput): string;
|
|
49
80
|
/** Whether a pulse went wrong: failed, stopped, or ran to its end with what it built not passing. */
|
package/dist/notice.js
CHANGED
|
@@ -5,12 +5,26 @@ 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
|
-
if ("dismiss" in answer)
|
|
27
|
+
if ("dismiss" in answer || "ignore" in answer)
|
|
14
28
|
return undefined;
|
|
15
29
|
const head = [
|
|
16
30
|
`The sentinel put this to the person, about ${notice.module !== undefined ? `${notice.module}${notice.pulse !== undefined ? ` pulse ${notice.pulse}` : ""}` : "the work"}:`,
|
|
@@ -24,6 +38,15 @@ export function noticeTask(notice, answer) {
|
|
|
24
38
|
return undefined;
|
|
25
39
|
return [...head, `The person chose: ${action.label}. Do it now:`, action.task.trim()].join("\n");
|
|
26
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* What the sentinel keeps of a notice the person ignored, in its guidance: what was left as it is - its module, the gap,
|
|
43
|
+
* what the build does - and why, so a fresh conversation does not raise it again.
|
|
44
|
+
*/
|
|
45
|
+
export function ignoredGuidance(notice) {
|
|
46
|
+
const about = notice.module !== undefined ? ` in ${notice.module}` : "";
|
|
47
|
+
const shows = notice.evidence.length > 0 ? ` (${notice.evidence.join("; ")})` : "";
|
|
48
|
+
return `The person chose to leave this${about} as the build has it - do not raise it again unless the build no longer does what they accepted: ${notice.summary}${shows}`;
|
|
49
|
+
}
|
|
27
50
|
/** A notice in words: its summary, its evidence and what can be done - for the agent, the log and a chat without a card. */
|
|
28
51
|
export function describeNotice(notice) {
|
|
29
52
|
return [
|
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
|
@@ -12,6 +12,7 @@ const NOTICE_ENTRY = "turri_notice";
|
|
|
12
12
|
const NOTICE_OVER_ENTRY = "turri_notice_over";
|
|
13
13
|
/** The sentinel done with a pulse, in the chat the person is in. */
|
|
14
14
|
const CONCLUDED_ENTRY = "turri_sentinel_concluded";
|
|
15
|
+
const IGNORE = "Ignore - leave it as the build has it";
|
|
15
16
|
const STEER = "Say what to do instead…";
|
|
16
17
|
const DISMISS = "Dismiss";
|
|
17
18
|
/** The sentinel's tools and model, built into its chat. */
|
|
@@ -54,6 +55,13 @@ export function registerSentinel(pi, session, cwd, hostTools, allowed) {
|
|
|
54
55
|
pi.setThinkingLevel(choice.thinking);
|
|
55
56
|
});
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The terminal's question whether to start the sentinel: the sentence, and how to switch to it - it opens last in the
|
|
60
|
+
* strip, after the `open` chats there are. A host has its own way to it, and is given the sentence alone.
|
|
61
|
+
*/
|
|
62
|
+
export function startTitle(notice, open) {
|
|
63
|
+
return `${notice.summary}\nUse "/chat ${open + 1}" to open the sentinel.`;
|
|
64
|
+
}
|
|
57
65
|
/**
|
|
58
66
|
* 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
67
|
* is idle, one at a time: an action accepted, their own words, or dismissed. A notice that is over closes its dialog.
|
|
@@ -92,7 +100,15 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
92
100
|
const data = entry.data;
|
|
93
101
|
if (data === undefined)
|
|
94
102
|
return undefined;
|
|
95
|
-
const said = data.state === "superseded"
|
|
103
|
+
const said = data.state === "superseded"
|
|
104
|
+
? "superseded by a newer notice"
|
|
105
|
+
: data.state === "dismissed"
|
|
106
|
+
? "dismissed"
|
|
107
|
+
: data.state === "ignored"
|
|
108
|
+
? "ignored - left as the build has it, not raised again"
|
|
109
|
+
: data.state === "steered"
|
|
110
|
+
? "answered in your words"
|
|
111
|
+
: "accepted";
|
|
96
112
|
const box = new Box(1, 0);
|
|
97
113
|
box.addChild(new Text(theme.fg("muted", ` ${said}: ${data.summary}`), 0, 0));
|
|
98
114
|
return box;
|
|
@@ -114,8 +130,15 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
114
130
|
const controller = new AbortController();
|
|
115
131
|
asking.set(notice.id, controller);
|
|
116
132
|
try {
|
|
117
|
-
|
|
118
|
-
const
|
|
133
|
+
// Whether to start the sentinel is turri's question, its answers its own: no words of the person's to act on.
|
|
134
|
+
const starting = notice.kind === "sentinel-start";
|
|
135
|
+
const options = [
|
|
136
|
+
...notice.actions.map((action, index) => `${index + 1}. ${action.label}`),
|
|
137
|
+
...(notice.ignorable === true ? [IGNORE] : []),
|
|
138
|
+
...(starting ? [] : [STEER]),
|
|
139
|
+
DISMISS,
|
|
140
|
+
];
|
|
141
|
+
const picked = await ctx.ui.select(starting ? startTitle(notice, session.chats().length) : `sentinel: ${notice.summary}`, options, { signal: controller.signal });
|
|
119
142
|
if (controller.signal.aborted || notice.state !== "open")
|
|
120
143
|
return;
|
|
121
144
|
if (picked === STEER) {
|
|
@@ -128,6 +151,10 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
128
151
|
session.answer(notice.id, { steer: words });
|
|
129
152
|
return;
|
|
130
153
|
}
|
|
154
|
+
if (picked === IGNORE) {
|
|
155
|
+
session.answer(notice.id, { ignore: true });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
131
158
|
const index = options.indexOf(picked ?? DISMISS);
|
|
132
159
|
session.answer(notice.id, index >= 0 && index < notice.actions.length ? { action: index } : { dismiss: true });
|
|
133
160
|
}
|
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 {
|
|
@@ -110,7 +137,8 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
110
137
|
return [
|
|
111
138
|
"<sentinel>",
|
|
112
139
|
"You are turri's sentinel: a chat of turri's own, beside the chats the person talks to while they build their software",
|
|
113
|
-
"with turri. Every pulse that finishes comes to you,
|
|
140
|
+
"with turri. Every pulse that finishes comes to you, each in a conversation of its own: this one is for the pulse in",
|
|
141
|
+
"the message turri writes first - what the pulse was, how it ended,",
|
|
114
142
|
...(surface === ""
|
|
115
143
|
? ["its ***plain diff, its log, and what was released. The person does not watch you: they are in another chat, and", "hear from you only through notify_person."]
|
|
116
144
|
: [
|
|
@@ -118,7 +146,7 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
118
146
|
"they talk to you in another chat, and hear what you find only through notify_person.",
|
|
119
147
|
]),
|
|
120
148
|
"",
|
|
121
|
-
"For
|
|
149
|
+
"For the pulse:",
|
|
122
150
|
"1. Check how it went, from what turri tells you - and, when that is not enough, its log, the ledger (ledger, pulses),",
|
|
123
151
|
" diagnose, and the code and the ***plain, which you read.",
|
|
124
152
|
"2. When it was released (the message says so - release-after-pulse is on), test what was released: it is running",
|
|
@@ -129,17 +157,21 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
129
157
|
" and what the person can do about it, each a task the agent of their chat can carry out once they accept it - a",
|
|
130
158
|
" change to the ***plain, a setting, a credential to provide, a pulse to regenerate again. Be exact: the task is",
|
|
131
159
|
" all that agent is given.",
|
|
160
|
+
" When it is a gap in the ***plain the build settled with an assumption - something the person may reasonably leave",
|
|
161
|
+
" as it is - say so with ignorable: the gap in the summary, what the build does in the evidence. Never for",
|
|
162
|
+
" something broken, a pulse that went wrong, or a release that did not come up.",
|
|
132
163
|
"4. When all is well, notify no one.",
|
|
133
|
-
"5. Last, call conclude once for
|
|
164
|
+
"5. Last, call conclude once for it - holds or wrong, and in a line or two what you checked and what you found:",
|
|
134
165
|
" the chat the person is in is told, whether all is well or not. Then say the same here, in a line or two.",
|
|
135
166
|
"",
|
|
136
167
|
"You change nothing. Your file tools read the whole project and write nothing; your shell runs with the project",
|
|
137
168
|
"read only. You never regenerate, steer, release or change ***plain yourself, and never ask the person questions -",
|
|
138
|
-
"a notice is how you reach them.
|
|
139
|
-
"that went well: leave the older one and check the newer.",
|
|
169
|
+
"a notice is how you reach them.",
|
|
140
170
|
"",
|
|
141
171
|
"The person can talk to you here. When they tell you how to check - what to test, what to leave alone, where things",
|
|
142
|
-
"run - keep it with keep_guidance, in their words, so it holds from now on
|
|
172
|
+
"run - keep it with keep_guidance, in their words, so it holds from now on: this conversation ends with its pulse,",
|
|
173
|
+
"and the next pulse's starts afresh, with only what you kept. What the person chose to leave as the build has it is",
|
|
174
|
+
"in your guidance too: do not raise it again, unless the build no longer does what they accepted.",
|
|
143
175
|
...(surface !== ""
|
|
144
176
|
? [
|
|
145
177
|
"",
|
|
@@ -238,6 +270,11 @@ export function notifyTool(notify) {
|
|
|
238
270
|
}), { description: "What can be done, the best first" }),
|
|
239
271
|
module: Type.Optional(Type.String({ description: "The module it is about" })),
|
|
240
272
|
pulse: Type.Optional(Type.String({ description: "The pulse's id" })),
|
|
273
|
+
ignorable: Type.Optional(Type.Boolean({
|
|
274
|
+
description: "true when the person may reasonably leave it as the build has it, for good: chiefly a gap in the ***plain the build settled with an " +
|
|
275
|
+
"assumption - then say the gap in the summary and what the build does in the evidence (the ***plain does not say what happens to " +
|
|
276
|
+
"passwords over 72 bytes; the build refuses them with 422). Never for something broken, a pulse that went wrong, or a release that did not come up.",
|
|
277
|
+
})),
|
|
241
278
|
}),
|
|
242
279
|
async execute(_id, params) {
|
|
243
280
|
const notice = notify({
|
|
@@ -245,6 +282,7 @@ export function notifyTool(notify) {
|
|
|
245
282
|
summary: params.summary,
|
|
246
283
|
evidence: params.evidence,
|
|
247
284
|
actions: params.actions,
|
|
285
|
+
...(params.ignorable === true ? { ignorable: true } : {}),
|
|
248
286
|
...(params.module !== undefined ? { module: params.module } : {}),
|
|
249
287
|
...(params.pulse !== undefined ? { job: params.pulse } : {}),
|
|
250
288
|
});
|
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
|
|
|
@@ -45,7 +45,7 @@ On macOS, `alt` is Option (⌥), and reaches turri only when the terminal sends
|
|
|
45
45
|
- **The side panel** (`ctrl+h`) is the session's, not a chat's: docked beside the chat or over it, it stays as another chat is shown, and opening it from any chat finds it as it was left.
|
|
46
46
|
- **A long command** - `/regenerate`, planning a module from scratch - does its work in the background once it is under way: the chat, and switching to another, stay free while the planner plans.
|
|
47
47
|
- **`ctrl+c`** reaches the chat shown and nothing else: another chat's turn and skill go on, and so do the pulses - `/halt` stops one. A skill stopped keeps its run, for the model to continue with `resume`. At a question - the chat's `ask_user`, a skill's interview - `ctrl+c` declines it, and the asker is told so and goes on - each question it is pressed at, however many in a row. A skill is stopped by `ctrl+c` with no question open. A second press straight after one that stopped something does nothing, so pressing twice to be sure does not leave turri; once the chat is doing nothing, the next one does - with what every other chat is doing. A host has the same as `interrupt(id)`, and `decline(id)` for a question ([hosting](hosting.md)).
|
|
48
|
-
- **The sentinel** is a chat in the strip like the others, `sentinel`, and never shown by itself: it closes like any chat, and the next pulse that finishes opens it again. With it on, you open up to eight chats - the ninth slot is its. See [the sentinel](sentinel.md).
|
|
48
|
+
- **The sentinel** is a chat in the strip like the others, `sentinel`, and never shown by itself: it closes like any chat, and the next pulse that finishes opens it again. Each pulse it checks in a fresh conversation, replacing the one in its tab. With it on, you open up to eight chats - the ninth slot is its. See [the sentinel](sentinel.md).
|
|
49
49
|
- **The last chat** of yours is not closed: `/quit` leaves turri. Leaving turri leaves the terminal as it was: the conversation is not written out to it, nor a line on how to resume - the command run is all that stays. `turri --continue` opens the chats again.
|
|
50
50
|
- **`/fork`** and **`/reload`** work on the chat shown, as [pi](https://pi.dev)'s do: the chat's history forked, or turri's extension rebuilt in it.
|
|
51
51
|
- **A chat shown mid-turn** draws its reply as far as it is written, and follows it from there.
|
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
|
@@ -84,8 +84,9 @@ const turri = await createTurri({
|
|
|
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
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
|
|
87
|
+
onNoticeOver: (notice) => {}, // ...answered, dismissed, ignored or superseded: take its card down
|
|
88
88
|
onConcluded: (conclusion) => {}, // the sentinel is done with a pulse: holds or wrong, told to conclusion.chat
|
|
89
|
+
onChatSession: (chat, session) => {}, // a chat's session replaced, same id: the sentinel's, fresh for each pulse - follow this one
|
|
89
90
|
sentinel: {
|
|
90
91
|
tools: [browserTool], // the sentinel's own tools, beside turri's: what it tests a release with
|
|
91
92
|
surface: "...", // where the person watches it use them, in the host's words
|
|
@@ -100,7 +101,7 @@ await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, e
|
|
|
100
101
|
await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
|
|
101
102
|
turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown, role? }]
|
|
102
103
|
turri.notices(); // the notices open now
|
|
103
|
-
await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }: "done", "dismissed", "over", "missing"
|
|
104
|
+
await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }, { ignore: true }: "done", "dismissed", "ignored", "refused", "over", "missing"
|
|
104
105
|
turri.onChats((chats) => {}); // a chat opened, closed, renamed, working or done
|
|
105
106
|
await turri.closeChat(chat.id); // its turn stopped, its conversation deleted - its pulses run on
|
|
106
107
|
turri.jobs; // the session's job registry: every pulse any chat started
|
|
@@ -122,7 +123,10 @@ await turri.dispose(); // every chat let go, what activate start
|
|
|
122
123
|
|
|
123
124
|
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
125
|
|
|
125
|
-
- **
|
|
126
|
+
- **A conversation a pulse.** The sentinel checks each pulse from scratch, in a conversation of its own - one pulse at a time, the others waiting their turn. The chat keeps its id; its session is replaced. As it starts on a pulse, the session it had is stopped and let go (`abort`, `dispose`), a new one is made in its place, and `onChatSession(chat, session)` comes with it - before the pulse is put to it - then `onChats`; `session(id)` returns the new one from then on. A host that follows the sentinel's chat subscribes to that session in place of the old - an `onChats` listener that follows only the ids it has not seen would hear nothing more of it. The first pulse goes to the session the chat was opened with: there is no `onChatSession` for it. Each conversation's first message is the hidden brief of its pulse - `customType: "turri_sentinel_pulse"`, `details.jobId` - so a host that splits the sentinel's work by pulse on it still can; the conversations before are not in the new session, and what the host showed of them is its own to keep.
|
|
127
|
+
|
|
128
|
+
- **Notices.** What it finds wrong comes as `onNotice(notice)`: `{ id, chat, severity: "error" | "warning" | "info", summary, evidence: string[], actions: [{ label, task }], module?, pulse?, job?, ignorable?, 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, this once. A notice with `ignorable: true` - a gap in the \*\*\*plain the build settled with an assumption, which the person may leave as it is - takes `{ ignore: true }` too: offer it as its own button beside the actions. Nothing is done in the chat, the notice ends `state: "ignored"` (`"ignored"`), and turri writes it to the sentinel's guidance, so it is not raised again in this session or the next; on a notice without `ignorable`, `{ ignore: true }` is `"refused"` and the notice stays open. `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.
|
|
129
|
+
- **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
130
|
- **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
131
|
- **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
132
|
- **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.
|