@turri-dev/cli 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/chat-commands.js +2 -1
  2. package/dist/chat-records.d.ts +2 -0
  3. package/dist/chat-terminal.d.ts +1 -0
  4. package/dist/chat-terminal.js +1 -1
  5. package/dist/chats.d.ts +60 -3
  6. package/dist/chats.js +201 -14
  7. package/dist/config-edit.js +2 -0
  8. package/dist/config.d.ts +8 -1
  9. package/dist/config.js +5 -2
  10. package/dist/extension.js +57 -1
  11. package/dist/host-session.d.ts +11 -0
  12. package/dist/host-session.js +2 -0
  13. package/dist/host.d.ts +1 -0
  14. package/dist/host.js +2 -0
  15. package/dist/identity.js +2 -2
  16. package/dist/init-config.d.ts +1 -0
  17. package/dist/init-config.js +6 -0
  18. package/dist/notice.d.ts +59 -0
  19. package/dist/notice.js +38 -0
  20. package/dist/pi-launch.js +9 -2
  21. package/dist/regenerate/agent-shell.d.ts +5 -0
  22. package/dist/regenerate/agent-shell.js +2 -2
  23. package/dist/regenerate/audit.js +1 -1
  24. package/dist/regenerate/codebase.js +1 -1
  25. package/dist/regenerate/command-check.js +1 -1
  26. package/dist/regenerate/confine.d.ts +2 -0
  27. package/dist/regenerate/confine.js +2 -2
  28. package/dist/regenerate/ledger.d.ts +14 -0
  29. package/dist/regenerate/paths.d.ts +2 -0
  30. package/dist/regenerate/paths.js +2 -0
  31. package/dist/regenerate/records.d.ts +1 -1
  32. package/dist/regenerate/records.js +3 -1
  33. package/dist/regenerate/release.js +1 -1
  34. package/dist/regenerate/testing.js +1 -1
  35. package/dist/regenerate/workspace.js +1 -1
  36. package/dist/sentinel-chat.d.ts +14 -0
  37. package/dist/sentinel-chat.js +178 -0
  38. package/dist/sentinel.d.ts +55 -0
  39. package/dist/sentinel.js +279 -0
  40. package/docs/README.md +2 -0
  41. package/docs/chats.md +4 -3
  42. package/docs/config.md +3 -1
  43. package/docs/deploy.md +2 -2
  44. package/docs/development.md +1 -0
  45. package/docs/hosting.md +18 -3
  46. package/docs/sentinel.md +54 -0
  47. package/docs/using.md +1 -0
  48. package/genome/rules/turri-regeneration.md +1 -0
  49. package/package.json +1 -1
package/dist/extension.js CHANGED
@@ -64,6 +64,8 @@ import { supersededByTurri } from "./chat-skills.js";
64
64
  import { conversationIdentity } from "./identity.js";
65
65
  import { setSurface, surfaceOf } from "./surface.js";
66
66
  import { createChats, scopedToChat } from "./chats.js";
67
+ import { landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelOn, sentinelPrompt, specConflictNotice } from "./sentinel.js";
68
+ import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
67
69
  import { currentChat } from "./chat-scope.js";
68
70
  import { registerChats } from "./chat-commands.js";
69
71
  import { turriIdentity } from "./environment.js";
@@ -319,6 +321,8 @@ export function createTurriCore(host = {}, options = {}) {
319
321
  host,
320
322
  jobs,
321
323
  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: () => sentinelOn(cwd),
322
326
  ...(options.persist !== undefined ? { persist: options.persist } : {}),
323
327
  ...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
324
328
  });
@@ -383,6 +387,13 @@ export function sessionLoop(session) {
383
387
  function turriExtension(api, session, link) {
384
388
  const pi = scopedToChat(api, link.scope);
385
389
  const { cwd, host, jobs } = session;
390
+ // The session's sentinel: its own prompt and tools - the project to read, notices, its guidance - and nothing that
391
+ // changes the project or starts work.
392
+ const isSentinel = link.role === SENTINEL;
393
+ const hostSentinelTools = isSentinel ? (host.sentinel?.tools ?? []) : [];
394
+ const sentinelAllowed = new Set([...SENTINEL_TOOLS, ...hostSentinelTools.map((tool) => tool.name)]);
395
+ if (isSentinel)
396
+ registerSentinel(pi, session, cwd, hostSentinelTools, sentinelAllowed);
386
397
  const dialogs = link.dialogs ?? host.dialogs;
387
398
  // turri's own skills, and the project's - .turri/skills, .agents/skills (skills.ts).
388
399
  const discoveredSkills = discoverSkills(PACKAGE_ROOT, cwd);
@@ -426,8 +437,45 @@ function turriExtension(api, session, link) {
426
437
  details: { jobId: job.id, status: job.status },
427
438
  }, { triggerTurn: true, deliverAs: "followUp" });
428
439
  };
440
+ // The sentinel's: what a pulse that ended is, told to its agent - after what it does now, or in its place.
441
+ const brief = async (job, context) => {
442
+ if (context.replace)
443
+ await link.session()?.abort();
444
+ announceJob(pi, job);
445
+ const config = readProjectConfig(cwd);
446
+ const content = sentinelBrief(job, {
447
+ project: cwd,
448
+ ...(context.asked !== undefined ? { asked: context.asked } : {}),
449
+ ...(context.focused !== undefined ? { focused: context.focused } : {}),
450
+ released: runningDeployed(job.module),
451
+ releaseAfterPulse: config.releaseAfterPulse === true,
452
+ reaction: JOB_FINISHED_REACTION,
453
+ });
454
+ const busy = link.session()?.isStreaming === true;
455
+ void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true, ...(busy ? { deliverAs: "followUp" } : {}) });
456
+ };
457
+ // 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
+ // it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
459
+ // contradicts itself is put to the person at once, by turri itself, as the chat would have been started on it.
460
+ const landed = (job, options = {}) => {
461
+ announceJob(pi, job);
462
+ if (job.specConflict !== undefined)
463
+ session.notice(specConflictNotice(job, specConflictTask(job.specConflict)));
464
+ if (options.tell !== true)
465
+ return;
466
+ void pi.sendMessage({ customType: "turri_job_landed", content: landedNote(job, runningDeployed(job.module)), display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true, deliverAs: "followUp" });
467
+ };
468
+ const notices = noticeDialogs(pi, session, link, contextNow);
429
469
  link.live({
430
470
  announce,
471
+ landed,
472
+ brief,
473
+ notice: notices.put,
474
+ noticeOver: notices.over,
475
+ concluded: notices.concluded,
476
+ act: (task) => {
477
+ void pi.sendMessage({ customType: "turri_notice_answer", content: task, display: false }, { triggerTurn: true, deliverAs: "followUp" });
478
+ },
431
479
  jobsChanged: () => panel.refresh(),
432
480
  thinkingLevel: () => pi.getThinkingLevel(),
433
481
  context: contextNow,
@@ -473,8 +521,12 @@ function turriExtension(api, session, link) {
473
521
  "the ***plain reference is in its prompt, and it runs its own skills with cultivate.", "info");
474
522
  }
475
523
  }
524
+ if (isSentinel) {
525
+ const prompt = [event.systemPrompt, sentinelPrompt(cwd, { extraTools: hostSentinelTools.map((tool) => tool.name) }), projectConfigBlock(cwd), rulesBlock()].join("\n\n");
526
+ return { systemPrompt: host.systemPrompt !== undefined ? host.systemPrompt(prompt) : prompt };
527
+ }
476
528
  return {
477
- systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [TOOLS_NOT_CLI, NEW_PROJECT, projectConfigBlock(cwd), machineBlock(cwd, machine.now()), rulesBlock()]),
529
+ systemPrompt: conversationSystemPrompt(event.systemPrompt, host, [TOOLS_NOT_CLI, NEW_PROJECT, pulseEndsBlock(session.sentinelOn()), projectConfigBlock(cwd), machineBlock(cwd, machine.now()), rulesBlock()]),
478
530
  };
479
531
  });
480
532
  // The conversation's agent does not run turri again through its shell, unless the person
@@ -492,6 +544,10 @@ function turriExtension(api, session, link) {
492
544
  return { action: "continue" };
493
545
  });
494
546
  pi.on("tool_call", (event) => {
547
+ // The sentinel reads, and puts what it finds to the person: whatever else it calls is refused.
548
+ if (isSentinel && !sentinelAllowed.has(event.toolName)) {
549
+ return { block: true, reason: `The sentinel changes nothing: ${event.toolName} is not one of its tools. Put what needs doing to the person with notify_person.` };
550
+ }
495
551
  const command = event.input.command;
496
552
  if (event.toolName === "bash" && typeof command === "string") {
497
553
  const what = selfInvocation(command);
@@ -3,6 +3,7 @@ import type { AskUserIO } from "./ask.ts";
3
3
  import { type ChatInfo, type TurriHost } from "./chats.ts";
4
4
  import type { JobRegistry } from "./regenerate/jobs.ts";
5
5
  import type { WorkLoop } from "./regenerate/loop.ts";
6
+ import type { Notice, NoticeAnswer } from "./notice.ts";
6
7
  /**
7
8
  * turri's session for a host that runs its chats itself - a desktop app, an editor: the chats, each a pi session with
8
9
  * turri in it, on one job registry and work loop. The host shows each chat in its own way - subscribes to its session,
@@ -60,6 +61,16 @@ export interface Turri {
60
61
  * stops the turn. `none` when the chat asks nothing now, `missing` for a chat not open.
61
62
  */
62
63
  decline(id: string): Promise<"declined" | "none" | "missing">;
64
+ /**
65
+ * The notices put to the person and not yet over - the sentinel's, and turri's own: each in the chat it names. A host
66
+ * hears each as it is put (`onNotice`) and as it ends (`onNoticeOver`).
67
+ */
68
+ notices(): Notice[];
69
+ /**
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.
72
+ */
73
+ answer(id: string, answer: NoticeAnswer): Promise<"done" | "dismissed" | "missing" | "over">;
63
74
  /** Told when a chat is opened, closed, renamed, or starts or stops working. */
64
75
  onChats(listener: (chats: ChatInfo[]) => void): () => void;
65
76
  /** Every chat let go, and what activate started stopped. */
@@ -67,6 +67,8 @@ export async function createTurri(options = {}) {
67
67
  return did;
68
68
  },
69
69
  decline: async (id) => core.decline(id),
70
+ notices: () => core.notices(),
71
+ answer: async (id, answer) => core.answer(id, answer),
70
72
  onChats: (listener) => core.onChats(() => listener(core.chats())),
71
73
  dispose,
72
74
  };
package/dist/host.d.ts CHANGED
@@ -6,6 +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
10
  export type { AskUserIO } from "./ask.ts";
10
11
  /** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
11
12
  export * as pi from "@earendil-works/pi-coding-agent";
package/dist/host.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // The session and its chats.
8
8
  export { createTurri } from "./host-session.js";
9
9
  export { MAX_CHATS } from "./chats.js";
10
+ // The sentinel's notices: what it puts to the person, and how they answer.
11
+ export { SENTINEL } from "./notice.js";
10
12
  /** pi itself, as turri is built against it: a host's own extensions and UI are made with this one, not another copy. */
11
13
  export * as pi from "@earendil-works/pi-coding-agent";
12
14
  // The project and its settings.
package/dist/identity.js CHANGED
@@ -50,8 +50,8 @@ const IDENTITY_BEFORE = [
50
50
  "",
51
51
  "The person may have several chats with you open on this project at once, this one among them. What regenerates is",
52
52
  "the same in every chat: the pulses and their plan, steering, the engine's coding model and what activate started -",
53
- "pulses lists every pulse, whichever chat asked for it, and you are told when one asked for here ends. What was said",
54
- "in another chat is not in this one: when the person speaks of it, ask.",
53
+ "pulses lists every pulse, whichever chat asked for it. What was said in another chat is not in this one: when the",
54
+ "person speaks of it, ask.",
55
55
  "",
56
56
  "Your words, and the older ones for the same things - a project's AGENTS.md, a skill, notes or the person may use",
57
57
  "either, and they mean the same:",
@@ -113,6 +113,7 @@ export declare const DEFAULTS: {
113
113
  readonly mutationTesting: true;
114
114
  readonly allowToStopJobs: true;
115
115
  readonly releaseAfterPulse: true;
116
+ readonly sentinel: true;
116
117
  readonly testingScriptsRunOn: "in-place";
117
118
  readonly turnLimit: 120;
118
119
  readonly compactAt: 100000;
@@ -220,6 +220,7 @@ export const DEFAULTS = {
220
220
  mutationTesting: true,
221
221
  allowToStopJobs: true,
222
222
  releaseAfterPulse: true,
223
+ sentinel: true,
223
224
  testingScriptsRunOn: "in-place",
224
225
  turnLimit: 120,
225
226
  compactAt: 100000,
@@ -264,6 +265,10 @@ export function quickStartConfig(vendor = "other", bedrock, models) {
264
265
  "# Whether every pulse that lands code releases it, as the project's release guidance says.",
265
266
  `release-after-pulse: ${DEFAULTS.releaseAfterPulse}`,
266
267
  "",
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.",
270
+ `sentinel: ${DEFAULTS.sentinel}`,
271
+ "",
267
272
  "# What the testing scripts run on: in-place - the code itself; copy - a fresh copy of it.",
268
273
  `testing-scripts-run-on: ${DEFAULTS.testingScriptsRunOn}`,
269
274
  "",
@@ -330,6 +335,7 @@ export function quickStartConfig(vendor = "other", bedrock, models) {
330
335
  "# models:",
331
336
  `# default: ${coding}`,
332
337
  `${column(`# chat: ${harness}`, 72)}# the conversation - as harness-model, which it wins over`,
338
+ `${column(`# sentinel: ${harness}`, 72)}# the sentinel: checks every finished pulse and tests the release`,
333
339
  role("chooser", harness, "chooses what gets conformance tests under turri regenerate"),
334
340
  role("planner", harness, "cuts the work into pulses"),
335
341
  role("diagnosis", harness, "looks into a pulse that failed for good"),
@@ -0,0 +1,59 @@
1
+ import type { Job } from "./regenerate/jobs.ts";
2
+ /**
3
+ * The sentinel's name and its notices - what it puts to the person - with nothing else: the session's chats (chats.ts)
4
+ * hold them, and load early, before the sentinel's tools and what they need (sentinel.ts) can be.
5
+ */
6
+ /** Its name in the strip, the host's chat list, and the ledger. */
7
+ export declare const SENTINEL = "sentinel";
8
+ export type ChatRole = typeof SENTINEL;
9
+ export type NoticeSeverity = "error" | "warning" | "info";
10
+ /** One thing the person can do about a notice: accepted, the focused chat's agent is started on `task`. */
11
+ export interface NoticeAction {
12
+ label: string;
13
+ task: string;
14
+ }
15
+ export interface NoticeInput {
16
+ severity: NoticeSeverity;
17
+ /** What is wrong, in a line. */
18
+ summary: string;
19
+ /** What shows it: a step that failed, a log's lines, a test's output. */
20
+ evidence: string[];
21
+ actions: NoticeAction[];
22
+ module?: string;
23
+ /** The pulse it is about: its job id. */
24
+ job?: string;
25
+ /** Its number among its module's pulses. */
26
+ pulse?: number;
27
+ }
28
+ export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "superseded";
29
+ export interface Notice extends NoticeInput {
30
+ id: string;
31
+ /** The chat it is put in: the one the person looks at, never the sentinel's. */
32
+ chat: string;
33
+ state: NoticeState;
34
+ at: string;
35
+ }
36
+ /** How the person answered a notice: one of its actions (by index or label), their own words, or not at all. */
37
+ export type NoticeAnswer = {
38
+ action: number | string;
39
+ } | {
40
+ steer: string;
41
+ } | {
42
+ dismiss: true;
43
+ };
44
+ export declare function newNotice(input: NoticeInput, chat: string): Notice;
45
+ /** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
46
+ export declare function noticeTask(notice: Notice, answer: NoticeAnswer): string | undefined;
47
+ /** 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
+ export declare function describeNotice(notice: NoticeInput): string;
49
+ /** Whether a pulse went wrong: failed, stopped, or ran to its end with what it built not passing. */
50
+ export declare function wentWrong(job: Job): boolean;
51
+ /** The sentinel done with a pulse: whether what it checked holds, and what it checked and found, in a line or two. */
52
+ export interface Conclusion {
53
+ /** The pulse: its job id. */
54
+ job: string;
55
+ verdict: "holds" | "wrong";
56
+ summary: string;
57
+ module?: string;
58
+ pulse?: number;
59
+ }
package/dist/notice.js ADDED
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * The sentinel's name and its notices - what it puts to the person - with nothing else: the session's chats (chats.ts)
4
+ * hold them, and load early, before the sentinel's tools and what they need (sentinel.ts) can be.
5
+ */
6
+ /** Its name in the strip, the host's chat list, and the ledger. */
7
+ export const SENTINEL = "sentinel";
8
+ export function newNotice(input, chat) {
9
+ return { ...input, id: randomUUID().slice(0, 8), chat, state: "open", at: new Date().toISOString() };
10
+ }
11
+ /** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
12
+ export function noticeTask(notice, answer) {
13
+ if ("dismiss" in answer)
14
+ return undefined;
15
+ const head = [
16
+ `The sentinel put this to the person, about ${notice.module !== undefined ? `${notice.module}${notice.pulse !== undefined ? ` pulse ${notice.pulse}` : ""}` : "the work"}:`,
17
+ describeNotice(notice),
18
+ "",
19
+ ];
20
+ if ("steer" in answer)
21
+ return [...head, `The person answered it in their own words - do what they say:`, answer.steer.trim()].join("\n");
22
+ const action = typeof answer.action === "number" ? notice.actions[answer.action] : notice.actions.find((candidate) => candidate.label === answer.action);
23
+ if (action === undefined)
24
+ return undefined;
25
+ return [...head, `The person chose: ${action.label}. Do it now:`, action.task.trim()].join("\n");
26
+ }
27
+ /** 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
+ export function describeNotice(notice) {
29
+ return [
30
+ `${notice.severity === "error" ? "✗" : notice.severity === "warning" ? "!" : "·"} ${notice.summary}`,
31
+ ...notice.evidence.map((line) => ` ${line}`),
32
+ ...(notice.actions.length > 0 ? ["What can be done:", ...notice.actions.map((action, index) => ` ${index + 1}. ${action.label}`)] : []),
33
+ ].join("\n");
34
+ }
35
+ /** Whether a pulse went wrong: failed, stopped, or ran to its end with what it built not passing. */
36
+ export function wentWrong(job) {
37
+ return job.status !== "succeeded" || job.details?.passed === false;
38
+ }
package/dist/pi-launch.js CHANGED
@@ -61,10 +61,17 @@ if (interactive) {
61
61
  let restore;
62
62
  const asked = args.findIndex((arg) => arg === "--continue" || arg === "-c");
63
63
  const saved = asked !== -1 ? lastRun(process.cwd()) : undefined;
64
+ // pi reopens the first of the person's chats as its own; the rest - the sentinel among them - are opened beside it.
65
+ const first = saved?.chats.find((chat) => chat.role === undefined);
64
66
  if (saved !== undefined) {
65
- args.splice(asked, 1, "--session", saved.chats[0].sessionFile);
67
+ if (first !== undefined)
68
+ args.splice(asked, 1, "--session", first.sessionFile);
69
+ else
70
+ args.splice(asked, 1);
66
71
  restore = saved;
67
72
  }
68
- driveTerminal(processSession(() => createTurriCore({}, { persist: true })), restore !== undefined ? { restore } : {});
73
+ driveTerminal(processSession(() => createTurriCore({}, { persist: true })),
74
+ // With none of the person's chats saved - only the sentinel's - pi starts a fresh one, and every saved chat is opened beside it.
75
+ restore !== undefined ? { restore, skipFirst: restore.chats.some((chat) => chat.role === undefined) } : {});
69
76
  }
70
77
  await main(args, { extensionFactories });
@@ -13,6 +13,11 @@ export interface ShellRole {
13
13
  read?: string[];
14
14
  /** The folders it may write; its workdir when not given, none for a role that only reads. */
15
15
  write?: string[];
16
+ /**
17
+ * The module folder its home, TMPDIR and caches are kept for - its workdir's module when not given: for a role whose
18
+ * folder is no module's, the sentinel's the project itself.
19
+ */
20
+ home?: string;
16
21
  }
17
22
  export interface AgentShell {
18
23
  /** The wall under the check and the audit. */
@@ -7,7 +7,7 @@ import { afterCommand, auditMessage, beforeCommand } from "./audit.js";
7
7
  import { checkCommand, machineReadable } from "./command-check.js";
8
8
  import { darwinUserDirs, machineFor, sandboxAvailable, sandboxWrap } from "./os-sandbox.js";
9
9
  import { MODULES_DIR, projectRootOf } from "./paths.js";
10
- import { RECORD_FILE } from "./records.js";
10
+ import { RECORD_FILE } from "./paths.js";
11
11
  import { runCommand } from "./shell.js";
12
12
  /**
13
13
  * What a module's shell writes beside its roots: its home, its TMPDIR, the package caches - and on macOS its own folder
@@ -28,7 +28,7 @@ export function agentShell(role, options = {}) {
28
28
  const workdir = resolve(role.workdir);
29
29
  const read = (role.read ?? [workdir]).map((dir) => resolve(dir));
30
30
  const write = (role.write ?? [workdir]).map((dir) => resolve(dir));
31
- const moduleDir = moduleOf(workdir);
31
+ const moduleDir = role.home !== undefined ? resolve(role.home) : moduleOf(workdir);
32
32
  const project = projectRootOf(workdir);
33
33
  const { env } = agentEnvironment(moduleDir);
34
34
  const root = projectRootOf(moduleDir);
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, wri
3
3
  import { basename, dirname, join, relative, sep } from "node:path";
4
4
  import { writtenByConversation } from "./conversation-writes.js";
5
5
  import { isCredentialFile } from "./credentials.js";
6
- import { RECORD_FILE } from "./records.js";
6
+ import { RECORD_FILE } from "./paths.js";
7
7
  /** Folders never looked in: dependencies, build output, caches, histories, turri's scratch. */
8
8
  const SKIPPED = new Set([
9
9
  "node_modules", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox", "target", "build",
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { NOTES_FILE } from "./notes.js";
5
- import { RECORD_FILE } from "./records.js";
5
+ import { RECORD_FILE } from "./paths.js";
6
6
  import { listFiles } from "./testing.js";
7
7
  /**
8
8
  * What a render agent is told about the code before it starts.
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { posix, win32 } from "node:path";
3
3
  import { isCredentialFile } from "./credentials.js";
4
- import { RECORD_FILE } from "./records.js";
4
+ import { RECORD_FILE } from "./paths.js";
5
5
  /** What of the machine an agent's command may read, beside its roots and its toolchains: the system's own folders. */
6
6
  export function machineReadable(platform = process.platform) {
7
7
  if (platform === "win32") {
@@ -29,6 +29,8 @@ export interface Confinement {
29
29
  * and run code: the coding agents and the conformance agent. Observers, explorers and judges never do.
30
30
  */
31
31
  reportsEnvironment?: boolean;
32
+ /** Whose home and temporary folder its shell has, by the module folder they are kept for - its workdir's module when not given. */
33
+ shellHome?: string;
32
34
  }
33
35
  export declare class ConfinementError extends Error {
34
36
  constructor(message: string);
@@ -4,7 +4,7 @@ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
4
4
  import { credentialRefusal, isCredentialFile } from "./credentials.js";
5
5
  import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
6
6
  import { NOTES_FILE } from "./notes.js";
7
- import { RECORD_FILE } from "./records.js";
7
+ import { RECORD_FILE } from "./paths.js";
8
8
  import { keptShellOutput, outputsDir, readOutputTool } from "./testing.js";
9
9
  import { reportLogNote } from "./activity.js";
10
10
  import { agentShell } from "./agent-shell.js";
@@ -266,7 +266,7 @@ export function confinedTools(c) {
266
266
  description += ", no bash";
267
267
  }
268
268
  else {
269
- const shell = agentShell({ workdir, read, write }, { busy: anotherPulseIn });
269
+ const shell = agentShell({ workdir, read, write, ...(c.shellHome !== undefined ? { home: c.shellHome } : {}) }, { busy: anotherPulseIn });
270
270
  const bash = createBashToolDefinition(workdir, { operations: hostBashOperations(shell), exposeSessionEnvironment: false });
271
271
  bash.description = `${bash.description}\n\n${hostEnvironmentNote()}\n\n${timeLimitNote()}`;
272
272
  tools.push(withKeptOutput(bash, workdir));
@@ -50,6 +50,20 @@ export type LedgerEvent = {
50
50
  } | {
51
51
  kind: "chat.opened";
52
52
  reopened: boolean;
53
+ role?: string;
54
+ } | {
55
+ kind: "notice.put";
56
+ notice: string;
57
+ severity: string;
58
+ module?: string;
59
+ } | {
60
+ kind: "notice.over";
61
+ notice: string;
62
+ state: string;
63
+ } | {
64
+ kind: "sentinel.concluded";
65
+ verdict: string;
66
+ module?: string;
53
67
  } | {
54
68
  kind: "chat.closed";
55
69
  } | {
@@ -1,3 +1,5 @@
1
+ /** A module's code record and its tests' - `code/.turri.json`, `tests/.turri.json` (records.ts). */
2
+ export declare const RECORD_FILE = ".turri.json";
1
3
  /**
2
4
  * The ***plain standard library: import modules for generic pieces of software that ship with turri,
3
5
  * imported as `stdlib/backend/python-fastapi` - a reserved name (see plain/stdlib.ts).
@@ -1,6 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve, sep } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ /** A module's code record and its tests' - `code/.turri.json`, `tests/.turri.json` (records.ts). */
5
+ export const RECORD_FILE = ".turri.json";
4
6
  /**
5
7
  * The ***plain standard library: import modules for generic pieces of software that ship with turri,
6
8
  * imported as `stdlib/backend/python-fastapi` - a reserved name (see plain/stdlib.ts).
@@ -12,7 +12,7 @@ import { type MarshalledPlainSource } from "../plain/spec.ts";
12
12
  * functionality, and only functionality in the code can be tested. Each record is committed with
13
13
  * what it describes, in the module's `.plaingit`, so rolling a unit of work back puts it back too.
14
14
  */
15
- export declare const RECORD_FILE = ".turri.json";
15
+ export { RECORD_FILE } from "./paths.ts";
16
16
  /**
17
17
  * A functional spec in the code record marked to regenerate again: its code is built, and is to be regenerated again
18
18
  * all the same - `regenerate` with functionalities named. The mark makes the record differ from the spec at that item,
@@ -16,7 +16,9 @@ import { committedText } from "./lineage.js";
16
16
  * functionality, and only functionality in the code can be tested. Each record is committed with
17
17
  * what it describes, in the module's `.plaingit`, so rolling a unit of work back puts it back too.
18
18
  */
19
- export const RECORD_FILE = ".turri.json";
19
+ // Defined beside the paths, which import nothing of turri's: what reads it as it loads - testing.ts - never waits on this module.
20
+ export { RECORD_FILE } from "./paths.js";
21
+ import { RECORD_FILE } from "./paths.js";
20
22
  /**
21
23
  * A functional spec in the code record marked to regenerate again: its code is built, and is to be regenerated again
22
24
  * all the same - `regenerate` with functionalities named. The mark makes the record differ from the spec at that item,
@@ -6,7 +6,7 @@ import { HISTORY_DIR } from "./lineage.js";
6
6
  import { NOTES_FILE } from "./notes.js";
7
7
  import { distDirFor, ensureInScratch, projectDirOf } from "./paths.js";
8
8
  import { findCodebaseTip } from "./requires.js";
9
- import { RECORD_FILE } from "./records.js";
9
+ import { RECORD_FILE } from "./paths.js";
10
10
  function git(moduleDir, args) {
11
11
  return spawnSync("git", ["--git-dir", join(moduleDir, HISTORY_DIR), "--work-tree", moduleDir, ...args], { cwd: moduleDir, encoding: "utf8" });
12
12
  }
@@ -6,7 +6,7 @@ import { reportActivity } from "./activity.js";
6
6
  import { maskSecrets } from "./masking.js";
7
7
  import { NOTES_FILE } from "./notes.js";
8
8
  import { modulesScratchDir } from "./paths.js";
9
- import { RECORD_FILE } from "./records.js";
9
+ import { RECORD_FILE } from "./paths.js";
10
10
  import { missingToolchain, runUnitScript, scriptFailure } from "./test-scripts.js";
11
11
  import { COMMAND_TIMEOUT_MS } from "./shell.js";
12
12
  import { pulseWrote } from "./pulse-files.js";
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { join, resolve, sep } from "node:path";
4
4
  import { projectImplementationReqs, stackOf } from "../stack.js";
5
5
  import { MODULES_DIR, projectRootOf } from "./paths.js";
6
- import { RECORD_FILE } from "./records.js";
6
+ import { RECORD_FILE } from "./paths.js";
7
7
  import { listFiles } from "./testing.js";
8
8
  /**
9
9
  * What an agent is told up front about its folder and this machine, beside its walls - what agents otherwise go looking
@@ -0,0 +1,14 @@
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type { ChatLink, TurriCore } from "./chats.ts";
3
+ import { type Conclusion, type Notice } from "./sentinel.ts";
4
+ /** The sentinel's tools and model, built into its chat. */
5
+ export declare function registerSentinel(pi: ExtensionAPI, session: TurriCore, cwd: string, hostTools: ToolDefinition[], allowed: Set<string>): void;
6
+ /**
7
+ * 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
+ * is idle, one at a time: an action accepted, their own words, or dismissed. A notice that is over closes its dialog.
9
+ */
10
+ export declare function noticeDialogs(pi: ExtensionAPI, session: TurriCore, link: ChatLink, context: () => ExtensionContext | undefined): {
11
+ put(notice: Notice): void;
12
+ over(notice: Notice): void;
13
+ concluded(conclusion: Conclusion): void;
14
+ };