@turri-dev/cli 0.1.19-next.ee6bc61 → 0.1.20-next.3ae4a0b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-commands.js +2 -1
- package/dist/chat-records.d.ts +2 -0
- package/dist/chat-terminal.d.ts +1 -0
- package/dist/chat-terminal.js +1 -1
- package/dist/chats.d.ts +60 -3
- package/dist/chats.js +201 -14
- package/dist/config-edit.js +2 -0
- package/dist/config.d.ts +8 -1
- package/dist/config.js +5 -2
- package/dist/extension.js +57 -1
- package/dist/host-session.d.ts +11 -0
- package/dist/host-session.js +2 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +2 -0
- package/dist/identity.js +2 -2
- package/dist/init-config.d.ts +1 -0
- package/dist/init-config.js +6 -0
- package/dist/notice.d.ts +59 -0
- package/dist/notice.js +38 -0
- package/dist/pi-launch.js +9 -2
- package/dist/regenerate/agent-shell.d.ts +5 -0
- package/dist/regenerate/agent-shell.js +2 -2
- package/dist/regenerate/audit.js +1 -1
- package/dist/regenerate/codebase.js +1 -1
- package/dist/regenerate/command-check.js +1 -1
- package/dist/regenerate/confine.d.ts +2 -0
- package/dist/regenerate/confine.js +2 -2
- package/dist/regenerate/ledger.d.ts +14 -0
- package/dist/regenerate/paths.d.ts +2 -0
- package/dist/regenerate/paths.js +2 -0
- package/dist/regenerate/records.d.ts +1 -1
- package/dist/regenerate/records.js +3 -1
- package/dist/regenerate/release.js +1 -1
- package/dist/regenerate/testing.js +1 -1
- package/dist/regenerate/workspace.js +1 -1
- package/dist/sentinel-chat.d.ts +14 -0
- package/dist/sentinel-chat.js +178 -0
- package/dist/sentinel.d.ts +55 -0
- package/dist/sentinel.js +279 -0
- package/docs/README.md +2 -0
- package/docs/chats.md +4 -3
- package/docs/config.md +3 -1
- package/docs/deploy.md +2 -2
- package/docs/development.md +1 -0
- package/docs/hosting.md +18 -3
- package/docs/sentinel.md +54 -0
- package/docs/using.md +1 -0
- package/genome/rules/turri-regeneration.md +1 -0
- 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);
|
package/dist/host-session.d.ts
CHANGED
|
@@ -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. */
|
package/dist/host-session.js
CHANGED
|
@@ -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
|
|
54
|
-
"
|
|
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:",
|
package/dist/init-config.d.ts
CHANGED
|
@@ -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;
|
package/dist/init-config.js
CHANGED
|
@@ -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"),
|
package/dist/notice.d.ts
ADDED
|
@@ -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
|
-
|
|
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 })),
|
|
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 "./
|
|
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);
|
package/dist/regenerate/audit.js
CHANGED
|
@@ -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 "./
|
|
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 "./
|
|
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 "./
|
|
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 "./
|
|
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).
|
package/dist/regenerate/paths.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 "./
|
|
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 "./
|
|
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 "./
|
|
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
|
+
};
|