@turri-dev/cli 0.1.26-next.2fc445f → 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.
@@ -24,6 +24,10 @@ export interface RunRecord {
24
24
  /** How many runs' chats are kept: the most recent, the one `--continue` reopens, among them. */
25
25
  export declare const RUNS_KEPT = 5;
26
26
  export declare function chatsFile(project: string): string;
27
+ /** A chat's saved conversation it no longer has - the sentinel's, before a pulse's fresh one - deleted, unless kept. */
28
+ export declare function forgetSessionFile(file: string, options?: {
29
+ keep?: boolean;
30
+ }): void;
27
31
  /** The runs recorded here, the most recent first. */
28
32
  export declare function recordedRuns(project: string): RunRecord[];
29
33
  /**
@@ -31,6 +31,11 @@ function write(project, file) {
31
31
  // A chat not recorded is not reopened; nothing else is lost.
32
32
  }
33
33
  }
34
+ /** A chat's saved conversation it no longer has - the sentinel's, before a pulse's fresh one - deleted, unless kept. */
35
+ export function forgetSessionFile(file, options = {}) {
36
+ if (options.keep !== true)
37
+ deleteSession(file);
38
+ }
34
39
  function deleteSession(file) {
35
40
  try {
36
41
  rmSync(file, { force: true });
package/dist/chats.d.ts CHANGED
@@ -24,9 +24,10 @@ import { type ChatRole, type Conclusion, type Notice, type NoticeAnswer, type No
24
24
  *
25
25
  * With the project's sentinel on (sentinel.ts), every finished pulse goes to the sentinel - a chat of the session's own,
26
26
  * opened in the background with the first, and again after it is closed - and the chat the person looks at shows only
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
+ * that it landed. The sentinel checks each pulse in a conversation of its own: its chat keeps its id, and is given a new
28
+ * session for each pulse, one at a time. What the sentinel finds wrong it puts to the person as a notice, in the chat
29
+ * they look at. Under `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the
30
+ * session - the pulses that end meanwhile held until they answer.
30
31
  */
31
32
  /** The most chats a session has open at once. */
32
33
  export declare const MAX_CHATS = 9;
@@ -62,6 +63,11 @@ export interface TurriHost {
62
63
  * work: what it returns is what is sent. For a host that needs to change anything else.
63
64
  */
64
65
  systemPrompt?(prompt: string): string;
66
+ /**
67
+ * A chat's session replaced by a new one, under the same id: the sentinel's, a fresh conversation for each pulse it
68
+ * checks - told before the pulse is put to it. The host follows this session in place of the one it had.
69
+ */
70
+ onChatSession?(chat: string, session: AgentSession): void;
65
71
  /** The sentinel is done with a pulse - told to the chat it names, whose agent says so. */
66
72
  onConcluded?(conclusion: Conclusion & {
67
73
  chat: string;
@@ -91,11 +97,10 @@ export interface ChatInstance {
91
97
  landed(job: Job, options?: {
92
98
  tell?: boolean;
93
99
  }): void;
94
- /** The sentinel's: a pulse ended - its agent told, at once, or after what it does now; `replace`, what it does now stopped. */
100
+ /** The sentinel's: a pulse ended - its agent, in a conversation of its own for this pulse, told of it. */
95
101
  brief(job: Job, context: {
96
102
  asked?: string;
97
103
  focused?: string;
98
- replace: boolean;
99
104
  }): Promise<void>;
100
105
  /** A notice put in the chat: drawn, and - in the terminal - put to the person once the chat is idle. */
101
106
  notice(notice: Notice): void;
@@ -210,8 +215,11 @@ export interface TurriCore {
210
215
  concluded(conclusion: Conclusion): string | undefined;
211
216
  /** The notices put to the person and not yet answered. */
212
217
  notices(): Notice[];
213
- /** A notice answered: what it asked done in the chat it is in, or dismissed. */
214
- answer(id: string, answer: NoticeAnswer): "done" | "dismissed" | "missing" | "over";
218
+ /**
219
+ * A notice answered: what it asked done in the chat it is in, dismissed, or - an ignorable one - ignored for good.
220
+ * `refused`: `ignore` on one that is not ignorable.
221
+ */
222
+ answer(id: string, answer: NoticeAnswer): "done" | "dismissed" | "ignored" | "refused" | "missing" | "over";
215
223
  /** For the extension: the jobs changed - every chat draws them again. */
216
224
  jobsChanged(): void;
217
225
  /** The chat whose work this is, else the one shown: its instance. */
@@ -238,6 +246,8 @@ export interface CoreOptions {
238
246
  sentinelStart?: (job: Job) => NoticeInput;
239
247
  /** The person chose for the project, asked whether to start the sentinel: `sentinel:` written. */
240
248
  setSentinel?: (on: boolean) => void;
249
+ /** The person ignored a notice for good: kept in the sentinel's guidance, for every conversation after. */
250
+ ignored?: (notice: Notice) => void;
241
251
  }
242
252
  /** A chat's session, as stopping what it does reaches it. */
243
253
  export type Stoppable = Pick<AgentSession, "isStreaming" | "isCompacting" | "isRetrying" | "isBashRunning" | "abort" | "abortBash">;
package/dist/chats.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { currentChat, inChat } from "./chat-scope.js";
4
- import { adoptRun, forgetChat, recordRun } from "./chat-records.js";
4
+ import { adoptRun, forgetChat, forgetSessionFile, 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";
@@ -23,9 +23,10 @@ import { newNotice, noticeTask, SENTINEL, sentinelStartChoice, wentWrong } from
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. 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.
26
+ * that it landed. The sentinel checks each pulse in a conversation of its own: its chat keeps its id, and is given a new
27
+ * session for each pulse, one at a time. What the sentinel finds wrong it puts to the person as a notice, in the chat
28
+ * they look at. Under `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the
29
+ * session - the pulses that end meanwhile held until they answer.
29
30
  */
30
31
  /** The most chats a session has open at once. */
31
32
  export const MAX_CHATS = 9;
@@ -90,8 +91,12 @@ export function createChats(options) {
90
91
  let notices = [];
91
92
  /** What the sentinel put to the person, and turri itself, this session: open until answered or superseded. */
92
93
  const open = [];
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. */
94
- let sentinelOnJob;
94
+ /** The pulse the sentinel's conversation is on, from its brief until its agent is done: a newer one of the same module replaces a check of one that went well. */
95
+ let checking;
96
+ /** Pulses that finished while the sentinel was at work: each checked in turn, in a conversation of its own. */
97
+ const waiting = [];
98
+ /** What the sentinel is given is given one pulse at a time: its conversations made, and its pulses put to them, in order. */
99
+ let sentinelWork = Promise.resolve();
95
100
  /**
96
101
  * Under `sentinel: ask`, whether the person said to start the sentinel this session: asked - the notice `asking` open,
97
102
  * the pulses that ended meanwhile `held` - started, or not this session.
@@ -174,12 +179,15 @@ export function createChats(options) {
174
179
  }
175
180
  else if (event.type === "agent_end") {
176
181
  chat.working = false;
177
- if (chat.role === SENTINEL)
178
- sentinelOnJob = undefined;
179
182
  if (shownId !== chat.id)
180
183
  chat.unread = true;
181
184
  persist();
182
185
  changed();
186
+ // Done with its pulse - or with what the person said to it: the next pulse waiting, in a conversation of its own.
187
+ if (chat.role === SENTINEL && event.willRetry !== true) {
188
+ checking = undefined;
189
+ toSentinel(() => nextPulse(chat));
190
+ }
183
191
  }
184
192
  else if (event.type === "session_info_changed") {
185
193
  persist();
@@ -264,6 +272,11 @@ export function createChats(options) {
264
272
  }
265
273
  const recorded = run.chats.find((kept) => kept.id === id);
266
274
  chats.splice(chats.indexOf(chat), 1);
275
+ // Closed, the sentinel is let go with what it had to check: the next pulse that finishes opens it again.
276
+ if (chat.role === SENTINEL) {
277
+ checking = undefined;
278
+ waiting.length = 0;
279
+ }
267
280
  // The question whether to start the sentinel was in it: put again in the chat the person looks at now.
268
281
  if (asking?.chat === id)
269
282
  askAgain();
@@ -394,7 +407,9 @@ export function createChats(options) {
394
407
  for (const older of open.filter((notice) => notice.state === "open" && notice.module === input.module))
395
408
  over(older, "superseded");
396
409
  }
397
- const notice = newNotice(input, target.id);
410
+ // Only the sentinel's own may be ignored: never the question whether to start it.
411
+ const { ignorable, ...rest } = input;
412
+ const notice = newNotice(ignorable === true && input.kind !== "sentinel-start" ? input : rest, target.id);
398
413
  open.push(notice);
399
414
  if (target.id !== shownId)
400
415
  target.unread = true;
@@ -447,6 +462,19 @@ export function createChats(options) {
447
462
  return "missing";
448
463
  if (notice.state !== "open")
449
464
  return "over";
465
+ // Left as the build has it, for good: nothing done in the chat, and the sentinel told not to raise it again.
466
+ if ("ignore" in answer) {
467
+ if (notice.ignorable !== true)
468
+ return "refused";
469
+ try {
470
+ options.ignored?.(notice);
471
+ }
472
+ catch (error) {
473
+ core.notify(`The sentinel's guidance could not be written: ${error instanceof Error ? error.message : String(error)}`, "warning");
474
+ }
475
+ over(notice, "ignored");
476
+ return "ignored";
477
+ }
450
478
  if (notice.kind === "sentinel-start")
451
479
  return answerStart(notice, answer);
452
480
  const task = noticeTask(notice, answer);
@@ -589,8 +617,8 @@ export function createChats(options) {
589
617
  /**
590
618
  * A pulse ended, with the sentinel on: the chat the person looks at says so - its agent told of one that landed, to
591
619
  * tell the person it landed and what the sentinel does with it, and nothing more; of one that went wrong, not, the
592
- * sentinel's notice being what it hears of it - and the sentinel is told, opened in the background when it is not open. A newer pulse of a module replaces a check of the
593
- * module's last pulse that went well; one that went wrong is looked into to its end.
620
+ * sentinel's notice being what it hears of it - and the sentinel is given it, opened in the background when it is not
621
+ * open.
594
622
  */
595
623
  function sentinelFinished(job) {
596
624
  const asked = chats.find((chat) => chat.id === job.chat && chat.role !== SENTINEL);
@@ -606,7 +634,7 @@ export function createChats(options) {
606
634
  }
607
635
  }
608
636
  changed();
609
- void (async () => {
637
+ toSentinel(async () => {
610
638
  let sentinel = sentinelChat();
611
639
  if (sentinel === undefined) {
612
640
  try {
@@ -622,23 +650,137 @@ export function createChats(options) {
622
650
  }
623
651
  if (sentinel?.instance === undefined)
624
652
  return;
625
- const busy = sentinel.session?.isStreaming === true;
626
- const replace = busy && sentinelOnJob !== undefined && !sentinelOnJob.wrong && sentinelOnJob.module !== undefined && sentinelOnJob.module === job.module;
627
- sentinelOnJob = { module: job.module, wrong: wentWrong(job) };
628
- if (sentinel.id !== shownId)
629
- sentinel.unread = true;
653
+ await takePulse(sentinel, job);
654
+ });
655
+ }
656
+ /** The sentinel's work, after what it was given before. */
657
+ function toSentinel(work) {
658
+ sentinelWork = sentinelWork.then(work).catch((error) => {
659
+ core.notify(`The sentinel: ${error instanceof Error ? error.message : String(error)}`, "warning");
660
+ });
661
+ }
662
+ /** Whether the sentinel is at work: on a pulse - its turn starts as it is told of one - or on what the person said to it. */
663
+ const sentinelBusy = (sentinel) => sentinel.session?.isStreaming === true;
664
+ /**
665
+ * A finished pulse, for the sentinel: checked now when it is free. Busy, a newer pulse of a module replaces a check of
666
+ * the module's last pulse that went well - and a pulse of it that went well and waits - and one that went wrong is
667
+ * looked into to its end; the rest wait their turn.
668
+ */
669
+ function takePulse(sentinel, job) {
670
+ if (!sentinelBusy(sentinel))
671
+ return check(sentinel, job);
672
+ const sameModule = (other) => job.module !== undefined && other.module === job.module;
673
+ if (checking !== undefined && !checking.wrong && sameModule(checking.job))
674
+ return check(sentinel, job, checking.job);
675
+ const older = waiting.findIndex((other) => sameModule(other) && !wentWrong(other));
676
+ if (older !== -1)
677
+ waiting.splice(older, 1, job);
678
+ else
679
+ waiting.push(job);
680
+ }
681
+ /** The sentinel free again: the next pulse that waits, checked. */
682
+ function nextPulse(sentinel) {
683
+ if (!chats.includes(sentinel) || sentinelBusy(sentinel))
684
+ return;
685
+ const job = waiting.shift();
686
+ if (job !== undefined)
687
+ return check(sentinel, job);
688
+ }
689
+ /**
690
+ * The sentinel on a pulse, from scratch: a conversation of its own - its prompt, the project's guidance and this
691
+ * pulse's brief, nothing of the pulses before it - in place of the one it had, stopped when it was on `replacing`.
692
+ */
693
+ async function check(sentinel, job, replacing) {
694
+ checking = { job, wrong: wentWrong(job) };
695
+ const module = job.module;
696
+ inChat(sentinel.scope, () => record({ kind: "sentinel.checking", ...(module !== undefined ? { module } : {}), ...(replacing !== undefined ? { replaced: replacing.id } : {}) }, { project: cwd, job: job.id }));
697
+ const session = sentinel.session;
698
+ if (session !== undefined && (session.messages.length > 0 || session.isStreaming))
699
+ await freshConversation(sentinel);
700
+ if (sentinel.id !== shownId)
701
+ sentinel.unread = true;
702
+ const asked = chats.find((chat) => chat.id === job.chat && chat.role !== SENTINEL && chat.instance !== undefined);
703
+ const where = personFacing(asked);
704
+ try {
705
+ await sentinel.instance.brief(job, {
706
+ ...(asked !== undefined ? { asked: titleFor(asked) } : {}),
707
+ ...(where !== undefined ? { focused: titleFor(where) } : {}),
708
+ });
709
+ }
710
+ catch (error) {
711
+ core.notify(`The sentinel could not be told of ${job.id}: ${error instanceof Error ? error.message : String(error)}`, "warning");
712
+ checking = undefined;
713
+ }
714
+ changed();
715
+ }
716
+ /**
717
+ * The sentinel's chat given a new session, under the same id: what it does now stopped, and what it said let go - its
718
+ * saved conversation deleted outside debug mode. The model and thinking it had carry over: what the person picked in
719
+ * it stands. Shown in the terminal, the screen is drawn from the new one. A new session that cannot be made leaves it
720
+ * the one it had.
721
+ */
722
+ async function freshConversation(chat) {
723
+ const old = { session: chat.session, services: chat.services, instance: chat.instance };
724
+ chat.unsubscribe?.();
725
+ chat.unsubscribe = undefined;
726
+ try {
727
+ await old.session.abort();
728
+ }
729
+ catch {
730
+ /* stopped already */
731
+ }
732
+ chat.working = false;
733
+ const model = old.session.model;
734
+ const thinking = old.session.thinkingLevel;
735
+ // Made as a chat is opened, one at a time: its extension built while it is the one attaching.
736
+ const made = opening.then(async () => {
737
+ attaching = chat;
630
738
  try {
631
- await sentinel.instance.brief(job, {
632
- ...(asked !== undefined ? { asked: titleFor(asked) } : {}),
633
- ...(where !== undefined ? { focused: titleFor(where) } : {}),
634
- replace,
635
- });
739
+ await driver.create(chat, undefined);
636
740
  }
637
- catch (error) {
638
- core.notify(`The sentinel could not be told of ${job.id}: ${error instanceof Error ? error.message : String(error)}`, "warning");
741
+ finally {
742
+ attaching = undefined;
639
743
  }
640
- changed();
641
- })();
744
+ });
745
+ opening = made.catch(() => undefined);
746
+ try {
747
+ await made;
748
+ }
749
+ catch (error) {
750
+ Object.assign(chat, old);
751
+ follow(chat);
752
+ core.notify(`The sentinel goes on in the conversation it had - a new one could not be started: ${error instanceof Error ? error.message : String(error)}`, "warning");
753
+ return;
754
+ }
755
+ const fresh = chat.session;
756
+ try {
757
+ if (model !== undefined && fresh.model !== model)
758
+ await fresh.setModel(model);
759
+ if (thinking !== undefined)
760
+ fresh.setThinkingLevel(thinking);
761
+ }
762
+ catch {
763
+ /* the model its session started on, then */
764
+ }
765
+ follow(chat);
766
+ if (shownId === chat.id)
767
+ await driver?.show?.(chat, undefined);
768
+ try {
769
+ old.session.dispose();
770
+ }
771
+ catch {
772
+ /* let go already */
773
+ }
774
+ if (old.session.sessionFile !== undefined && old.session.sessionFile !== fresh.sessionFile)
775
+ forgetSessionFile(old.session.sessionFile, { keep: debugMode(cwd) });
776
+ try {
777
+ host.onChatSession?.(chat.id, fresh);
778
+ }
779
+ catch {
780
+ /* a host that throws still has the chat's new session in session(id) */
781
+ }
782
+ persist();
783
+ changed();
642
784
  }
643
785
  return core;
644
786
  }
package/dist/extension.js CHANGED
@@ -65,7 +65,7 @@ import { supersededByTurri } from "./chat-skills.js";
65
65
  import { conversationIdentity } from "./identity.js";
66
66
  import { setSurface, surfaceOf } from "./surface.js";
67
67
  import { createChats, scopedToChat } from "./chats.js";
68
- import { landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
68
+ import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
69
69
  import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
70
70
  import { currentChat } from "./chat-scope.js";
71
71
  import { registerChats } from "./chat-commands.js";
@@ -327,6 +327,7 @@ export function createTurriCore(host = {}, options = {}) {
327
327
  sentinel: () => sentinelSetting(cwd),
328
328
  sentinelStart: (job) => sentinelStartNotice(job, cwd),
329
329
  setSentinel: (on) => void writeSetting(cwd, "sentinel", on),
330
+ ignored: (notice) => keepGuidance(cwd, ignoredGuidance(notice)),
330
331
  ...(options.persist !== undefined ? { persist: options.persist } : {}),
331
332
  ...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
332
333
  });
@@ -441,11 +442,10 @@ function turriExtension(api, session, link) {
441
442
  details: { jobId: job.id, status: job.status },
442
443
  }, { triggerTurn: true, deliverAs: "followUp" });
443
444
  };
444
- // The sentinel's: what a pulse that ended is, told to its agent - after what it does now, or in its place.
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.
445
448
  const brief = async (job, context) => {
446
- if (context.replace)
447
- await link.session()?.abort();
448
- announceJob(pi, job);
449
449
  const config = readProjectConfig(cwd);
450
450
  const content = sentinelBrief(job, {
451
451
  project: cwd,
@@ -455,8 +455,7 @@ function turriExtension(api, session, link) {
455
455
  releaseAfterPulse: config.releaseAfterPulse === true,
456
456
  reaction: JOB_FINISHED_REACTION,
457
457
  });
458
- const busy = link.session()?.isStreaming === true;
459
- 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 });
460
459
  };
461
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
462
461
  // it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
@@ -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 dismissed. `over` when it was answered or superseded already, `missing` for one never put.
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/notice.d.ts CHANGED
@@ -29,8 +29,14 @@ export interface NoticeInput {
29
29
  job?: string;
30
30
  /** Its number among its module's pulses. */
31
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;
32
38
  }
33
- export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "superseded";
39
+ export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "ignored" | "superseded";
34
40
  export interface Notice extends NoticeInput {
35
41
  id: string;
36
42
  /** The chat it is put in: the one the person looks at, never the sentinel's. */
@@ -46,19 +52,29 @@ export declare const SENTINEL_START: {
46
52
  readonly never: "Turn it off for this project";
47
53
  };
48
54
  export type SentinelStartChoice = keyof typeof SENTINEL_START;
49
- /** How the person answered a notice: one of its actions (by index or label), their own words, or not at all. */
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
+ */
50
59
  export type NoticeAnswer = {
51
60
  action: number | string;
52
61
  } | {
53
62
  steer: string;
54
63
  } | {
55
64
  dismiss: true;
65
+ } | {
66
+ ignore: true;
56
67
  };
57
68
  export declare function newNotice(input: NoticeInput, chat: string): Notice;
58
69
  /** The choice an answer to a `sentinel-start` notice makes: dismissed, or answered in the person's own words, it is not now. */
59
70
  export declare function sentinelStartChoice(notice: Notice, answer: NoticeAnswer): SentinelStartChoice;
60
71
  /** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
61
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;
62
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. */
63
79
  export declare function describeNotice(notice: NoticeInput): string;
64
80
  /** Whether a pulse went wrong: failed, stopped, or ran to its end with what it built not passing. */
package/dist/notice.js CHANGED
@@ -24,7 +24,7 @@ export function sentinelStartChoice(notice, answer) {
24
24
  }
25
25
  /** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
26
26
  export function noticeTask(notice, answer) {
27
- if ("dismiss" in answer)
27
+ if ("dismiss" in answer || "ignore" in answer)
28
28
  return undefined;
29
29
  const head = [
30
30
  `The sentinel put this to the person, about ${notice.module !== undefined ? `${notice.module}${notice.pulse !== undefined ? ` pulse ${notice.pulse}` : ""}` : "the work"}:`,
@@ -38,6 +38,15 @@ export function noticeTask(notice, answer) {
38
38
  return undefined;
39
39
  return [...head, `The person chose: ${action.label}. Do it now:`, action.task.trim()].join("\n");
40
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
+ }
41
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. */
42
51
  export function describeNotice(notice) {
43
52
  return [
@@ -60,6 +60,10 @@ export type LedgerEvent = {
60
60
  kind: "notice.over";
61
61
  notice: string;
62
62
  state: string;
63
+ } | {
64
+ kind: "sentinel.checking";
65
+ module?: string;
66
+ replaced?: string;
63
67
  } | {
64
68
  kind: "sentinel.concluded";
65
69
  verdict: string;
@@ -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. */
@@ -99,7 +100,15 @@ export function noticeDialogs(pi, session, link, context) {
99
100
  const data = entry.data;
100
101
  if (data === undefined)
101
102
  return undefined;
102
- const said = data.state === "superseded" ? "superseded by a newer notice" : data.state === "dismissed" ? "dismissed" : data.state === "steered" ? "answered in your words" : "accepted";
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";
103
112
  const box = new Box(1, 0);
104
113
  box.addChild(new Text(theme.fg("muted", ` ${said}: ${data.summary}`), 0, 0));
105
114
  return box;
@@ -123,7 +132,12 @@ export function noticeDialogs(pi, session, link, context) {
123
132
  try {
124
133
  // Whether to start the sentinel is turri's question, its answers its own: no words of the person's to act on.
125
134
  const starting = notice.kind === "sentinel-start";
126
- const options = [...notice.actions.map((action, index) => `${index + 1}. ${action.label}`), ...(starting ? [] : [STEER]), DISMISS];
135
+ const options = [
136
+ ...notice.actions.map((action, index) => `${index + 1}. ${action.label}`),
137
+ ...(notice.ignorable === true ? [IGNORE] : []),
138
+ ...(starting ? [] : [STEER]),
139
+ DISMISS,
140
+ ];
127
141
  const picked = await ctx.ui.select(starting ? startTitle(notice, session.chats().length) : `sentinel: ${notice.summary}`, options, { signal: controller.signal });
128
142
  if (controller.signal.aborted || notice.state !== "open")
129
143
  return;
@@ -137,6 +151,10 @@ export function noticeDialogs(pi, session, link, context) {
137
151
  session.answer(notice.id, { steer: words });
138
152
  return;
139
153
  }
154
+ if (picked === IGNORE) {
155
+ session.answer(notice.id, { ignore: true });
156
+ return;
157
+ }
140
158
  const index = options.indexOf(picked ?? DISMISS);
141
159
  session.answer(notice.id, index >= 0 && index < notice.actions.length ? { action: index } : { dismiss: true });
142
160
  }
package/dist/sentinel.js CHANGED
@@ -137,7 +137,8 @@ export function sentinelPrompt(cwd, options = {}) {
137
137
  return [
138
138
  "<sentinel>",
139
139
  "You are turri's sentinel: a chat of turri's own, beside the chats the person talks to while they build their software",
140
- "with turri. Every pulse that finishes comes to you, as a message turri writes - what the pulse was, how it ended,",
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,",
141
142
  ...(surface === ""
142
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."]
143
144
  : [
@@ -145,7 +146,7 @@ export function sentinelPrompt(cwd, options = {}) {
145
146
  "they talk to you in another chat, and hear what you find only through notify_person.",
146
147
  ]),
147
148
  "",
148
- "For each pulse:",
149
+ "For the pulse:",
149
150
  "1. Check how it went, from what turri tells you - and, when that is not enough, its log, the ledger (ledger, pulses),",
150
151
  " diagnose, and the code and the ***plain, which you read.",
151
152
  "2. When it was released (the message says so - release-after-pulse is on), test what was released: it is running",
@@ -156,17 +157,21 @@ export function sentinelPrompt(cwd, options = {}) {
156
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",
157
158
  " change to the ***plain, a setting, a credential to provide, a pulse to regenerate again. Be exact: the task is",
158
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.",
159
163
  "4. When all is well, notify no one.",
160
- "5. Last, call conclude once for the pulse - holds or wrong, and in a line or two what you checked and what you found:",
164
+ "5. Last, call conclude once for it - holds or wrong, and in a line or two what you checked and what you found:",
161
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.",
162
166
  "",
163
167
  "You change nothing. Your file tools read the whole project and write nothing; your shell runs with the project",
164
168
  "read only. You never regenerate, steer, release or change ***plain yourself, and never ask the person questions -",
165
- "a notice is how you reach them. A newer message about the same module can arrive while you check an older pulse",
166
- "that went well: leave the older one and check the newer.",
169
+ "a notice is how you reach them.",
167
170
  "",
168
171
  "The person can talk to you here. When they tell you how to check - what to test, what to leave alone, where things",
169
- "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.",
170
175
  ...(surface !== ""
171
176
  ? [
172
177
  "",
@@ -265,6 +270,11 @@ export function notifyTool(notify) {
265
270
  }), { description: "What can be done, the best first" }),
266
271
  module: Type.Optional(Type.String({ description: "The module it is about" })),
267
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
+ })),
268
278
  }),
269
279
  async execute(_id, params) {
270
280
  const notice = notify({
@@ -272,6 +282,7 @@ export function notifyTool(notify) {
272
282
  summary: params.summary,
273
283
  evidence: params.evidence,
274
284
  actions: params.actions,
285
+ ...(params.ignorable === true ? { ignorable: true } : {}),
275
286
  ...(params.module !== undefined ? { module: params.module } : {}),
276
287
  ...(params.pulse !== undefined ? { job: params.pulse } : {}),
277
288
  });
package/docs/chats.md CHANGED
@@ -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/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,9 @@ 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
- - **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
+ - **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.
126
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`).
127
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.
128
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).
package/docs/sentinel.md CHANGED
@@ -20,17 +20,18 @@ Nothing goes to the sentinel, or to your chat, until you answer: the pulses that
20
20
  ## What you see
21
21
 
22
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.
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.
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: it shows the pulse it is on, or the last one it checked - each pulse has a conversation of its own (below).
24
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:
25
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;
26
+ - **Ignore - leave it as the build has it**, on a notice the sentinel marked ignorable: nothing is done, and it is not raised again - in this session or the next (below);
26
27
  - **Say what to do instead…** - your own words, and the agent does that;
27
- - **Dismiss**, or `esc`: nothing is done.
28
+ - **Dismiss**, or `esc`: nothing is done, this once - the next pulse may raise it again.
28
29
 
29
30
  Nothing is done without your answer. A notice goes to the chat you are in; while you are in the sentinel's, to the chat that asked for the pulse. There is one open notice a module: a newer one about the same module supersedes the older one, whose call to action would be stale, and the card says so.
30
31
 
31
32
  ## What it does
32
33
 
33
- For each pulse that finishes, turri writes the sentinel what it was and how it ended - its stages, rounds and failures as the [ledger](using.md#the-ledger) has them, its \*\*\*plain diff, its log, the chat that asked for it, the chat you are in, and what was released and where it runs. Then it:
34
+ Each pulse it checks from scratch, in a conversation of its own: its prompt, your guidance (`.turri/sentinel.md`, below) and what turri writes it of that pulse - nothing of the pulses before it. Its chat stays the same tab; the conversation in it is replaced as it starts on the next pulse, and the one before is let go - deleted, outside [debug mode](config.md). For each pulse that finishes, turri writes the sentinel what it was and how it ended - its stages, rounds and failures as the [ledger](using.md#the-ledger) has them, its \*\*\*plain diff, its log, the chat that asked for it, the chat you are in, and what was released and where it runs. Then it:
34
35
 
35
36
  1. **checks the pulse** - from what it is told, and when that is not enough, from the pulse's log, the ledger (`ledger`, `pulses`), `diagnose`, the code and the \*\*\*plain;
36
37
  2. **tests the release**, with `release-after-pulse` on: what was released is running where it is told, and it tries it - in the terminal with its shell (`curl`, the program itself, the logs), in a host also with what the host gives it, a browser say. How far it tests is its own judgement, from what the pulse changed and what the \*\*\*plain says it now does. With `release-after-pulse` off nothing is running, and it checks the pulse alone;
@@ -38,7 +39,11 @@ For each pulse that finishes, turri writes the sentinel what it was and how it e
38
39
  4. **notifies no one** when all is well;
39
40
  5. **concludes**, last, whether all is well or not: what it checked and what it found, in a line or two. The chat you are in shows it - `✓ sentinel · app · pulse 3 · checked, it holds` - and its agent tells you so in a line, and does nothing more; when something was wrong, that its notice says what.
40
41
 
41
- A pulse that went well is checked until a newer pulse of its module finishes: that one is checked instead. A pulse that went wrong is looked into to its end. Two functional specs that contradict each other stop a pulse in a way only you can settle: turri puts that notice itself, with an action that starts your chat on working it out with you.
42
+ It checks one pulse at a time. A pulse that finishes while it works waits its turn, and is checked once it is done - from scratch, as every pulse is. A pulse that went well is checked until a newer pulse of its module finishes: its check is stopped, and the newer one is checked instead, at once - and a newer pulse of a module takes the place of one of it that went well and waits. A pulse that went wrong is looked into to its end.
43
+
44
+ A notice may be **ignorable**: the sentinel marks one so when you may reasonably leave it as it is - chiefly a gap in the \*\*\*plain the build settled with an assumption: *the \*\*\*plain does not say what happens to passwords over 72 bytes; the build refuses them with 422*. Never something broken, a pulse that went wrong, a release that did not come up, nor the question whether to start the sentinel. Ignore one and it is left as the build has it, for good: nothing is done in your chat, and a line is written to the sentinel's guidance (`.turri/sentinel.md`, as `keep_guidance` writes) saying what was left as it is - its module, the gap, what the build does - and that you chose to. Every conversation after has it, and does not raise it again - unless the build no longer does what you accepted. To have it raised again, take the line out of `.turri/sentinel.md`.
45
+
46
+ Two functional specs that contradict each other stop a pulse in a way only you can settle: turri puts that notice itself, with an action that starts your chat on working it out with you.
42
47
 
43
48
  ## What it may do
44
49
 
@@ -46,22 +51,22 @@ Nothing that changes the project. Its file tools - `read`, `ls`, `grep`, `find`
46
51
 
47
52
  ## Talking to it
48
53
 
49
- Switch to its chat and say how to check your software - *test the sign-up flow at a phone's width*, *ignore the styling*, *the API runs on port 8080*. What should hold from now on it keeps with `keep_guidance`, in `.turri/sentinel.md`, and it is in its prompt from then on - after it is closed and opened again too.
54
+ Switch to its chat and say how to check your software - *test the sign-up flow at a phone's width*, *ignore the styling*, *the API runs on port 8080*. What should hold from now on it keeps with `keep_guidance`, in `.turri/sentinel.md`, and it is in its prompt from then on - for every pulse after, and after it is closed and opened again too. Nothing else you say carries over: the conversation you talk to it in is the pulse's, and the next pulse starts a fresh one. A pulse that finishes while it answers you waits until it has.
50
55
 
51
56
  ## Closing it
52
57
 
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.
58
+ It closes like any chat (`/chat close`, `alt+w`, a host's `closeChat`), and that does not turn it off: what it was checking is stopped, the pulses that waited for it are let go, and the next pulse that finishes opens it again, with a fresh conversation - and your guidance. `turri --continue` reopens it with the other chats, with the conversation of the last pulse it was on; the next pulse starts a fresh one. `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.
54
59
 
55
60
  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.
56
61
 
57
62
  ## Its model
58
63
 
59
- The `sentinel` role in `models:` - else the chat's model, `models.chat` else `harness-model`, with its thinking - see [models](config.md#models). A `sentinel` entry that gives only `thinking` runs on the chat's model, thinking as it says. It is set as the sentinel's chat opens, in the terminal and in a host alike - never the model its session started on, which in a host is [pi](https://pi.dev)'s default and may not be one the project's provider has. What the person then picks in its chat stands. Judging behaviour against \*\*\*plain, and driving a browser, want a capable model; point it at a cheaper one if it costs too much for every pulse.
64
+ The `sentinel` role in `models:` - else the chat's model, `models.chat` else `harness-model`, with its thinking - see [models](config.md#models). A `sentinel` entry that gives only `thinking` runs on the chat's model, thinking as it says. It is set as the sentinel's chat opens, in the terminal and in a host alike - never the model its session started on, which in a host is [pi](https://pi.dev)'s default and may not be one the project's provider has. What the person then picks in its chat stands, through each pulse's fresh conversation, until it is closed. Judging behaviour against \*\*\*plain, and driving a browser, want a capable model; point it at a cheaper one if it costs too much for every pulse.
60
65
 
61
66
  ## In a host
62
67
 
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).
68
+ A host sees the sentinel as one of its chats - `role: "sentinel"` in `chats()` and `onChats` - and never has focus moved to it. Each pulse's fresh conversation is a new pi session in the same chat, under the same id: `onChatSession(chat, session)` comes before the pulse is put to it, and `session(id)` returns the new one. 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 } | { ignore: true })` answers one, as the terminal's dialog does - `{ ignore: true }` only on a notice with `ignorable: true`, `"refused"` on any other; it ends `state: "ignored"`. `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).
64
69
 
65
70
  ## In the ledger
66
71
 
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`.
72
+ `chat.opened` carries `role: "sentinel"` for its chat; `sentinel.checking` each pulse it starts a conversation for - `replaced`, the pulse of the same module whose check it stopped; `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`, `ignored`, `superseded` - and `sentinel.concluded` each pulse it is done with, `holds` or `wrong`.
package/docs/using.md CHANGED
@@ -187,7 +187,7 @@ It is in `.turri/ledger/` in the project, a JSON line an event, one file a run -
187
187
  | the healers | `healer.started`, `healer.ended` with what it found, `healer.failed` and why - the debugger, the diagnosis of a failed pulse |
188
188
  | the history | `history.committed`, `history.failed`, `history.reset`, `history.rolledBack` |
189
189
  | the environment | `environment.error` |
190
- | chats and notices | `chat.opened` (`role: "sentinel"` for the [sentinel](sentinel.md)'s), `chat.closed`, `notice.put` (its severity and module), `notice.over` (accepted, steered, dismissed or superseded), `sentinel.concluded` (`holds` or `wrong`, a pulse the sentinel is done with) |
190
+ | chats and notices | `chat.opened` (`role: "sentinel"` for the [sentinel](sentinel.md)'s), `chat.closed`, `notice.put` (its severity and module), `notice.over` (accepted, steered, dismissed, ignored or superseded), `sentinel.checking` (a pulse the sentinel starts a conversation for - `replaced`, the one whose check it stopped), `sentinel.concluded` (`holds` or `wrong`, a pulse the sentinel is done with) |
191
191
 
192
192
  Never in it: prompts, replies, thinking, tool arguments and results, file contents - the one body it keeps is each pulse's \*\*\*plain diff, so what an earlier session regenerated can be read back. What an event does carry - a stage, an error's first line - is masked as the logs are.
193
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.26-next.2fc445f",
3
+ "version": "0.1.26-next.f80f259",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {