@turri-dev/cli 0.2.0 → 0.2.1-next.47b578c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/chats.d.ts +31 -4
- package/dist/chats.js +146 -52
- package/dist/cli.js +4 -2
- package/dist/config.d.ts +4 -1
- package/dist/config.js +39 -4
- package/dist/environment.d.ts +0 -2
- package/dist/environment.js +0 -6
- package/dist/extension.d.ts +4 -3
- package/dist/extension.js +110 -23
- package/dist/host-session.d.ts +14 -0
- package/dist/host-session.js +2 -0
- package/dist/host.d.ts +6 -1
- package/dist/host.js +5 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +33 -3
- package/dist/installed-providers.d.ts +40 -0
- package/dist/installed-providers.js +98 -0
- package/dist/model-check.d.ts +4 -2
- package/dist/model-check.js +33 -17
- package/dist/notice.d.ts +30 -6
- package/dist/pi.js +8 -24
- package/dist/plain/files.d.ts +16 -0
- package/dist/plain/files.js +68 -5
- package/dist/plain/graph.js +2 -2
- package/dist/plain/module-concepts.js +3 -2
- package/dist/plain/parser.d.ts +8 -3
- package/dist/plain/parser.js +47 -6
- package/dist/plain/stdlib.js +3 -3
- package/dist/regenerate/base-folder.d.ts +2 -0
- package/dist/regenerate/base-folder.js +24 -6
- package/dist/regenerate/command-check.d.ts +12 -1
- package/dist/regenerate/command-check.js +74 -0
- package/dist/regenerate/confine.d.ts +5 -0
- package/dist/regenerate/confine.js +9 -9
- package/dist/regenerate/ledger.d.ts +7 -0
- package/dist/regenerate/lineage.js +43 -2
- package/dist/regenerate/loop.d.ts +8 -0
- package/dist/regenerate/loop.js +11 -4
- package/dist/regenerate/os-sandbox.d.ts +2 -0
- package/dist/regenerate/os-sandbox.js +55 -20
- package/dist/regenerate/release.d.ts +2 -0
- package/dist/regenerate/requires.d.ts +12 -0
- package/dist/regenerate/requires.js +70 -1
- package/dist/regenerate/spec-steps.js +18 -1
- package/dist/regenerate/standing.d.ts +28 -0
- package/dist/regenerate/standing.js +30 -0
- package/dist/regenerate/testing.d.ts +2 -0
- package/dist/regenerate/testing.js +14 -0
- package/dist/release-agent.d.ts +17 -0
- package/dist/release-agent.js +19 -7
- package/dist/release-tools.d.ts +6 -0
- package/dist/release-tools.js +24 -5
- package/dist/sentinel-chat.d.ts +1 -1
- package/dist/sentinel-chat.js +21 -8
- package/dist/sentinel-check.d.ts +36 -0
- package/dist/sentinel-check.js +78 -0
- package/dist/sentinel.d.ts +40 -9
- package/dist/sentinel.js +240 -37
- package/dist/setup.d.ts +24 -3
- package/dist/setup.js +117 -31
- package/dist/spawn-async.d.ts +18 -0
- package/dist/spawn-async.js +35 -0
- package/dist/stack.d.ts +14 -0
- package/dist/stack.js +44 -2
- package/dist/turri-versions.d.ts +71 -0
- package/dist/turri-versions.js +275 -0
- package/docs/README.md +2 -0
- package/docs/claude-subscription.md +131 -0
- package/docs/config.md +5 -0
- package/docs/deploy.md +2 -2
- package/docs/hosting.md +42 -4
- package/docs/installation.md +39 -2
- package/docs/parser.md +1 -1
- package/docs/regenerating.md +1 -1
- package/docs/sandbox.md +2 -1
- package/docs/sentinel.md +11 -3
- package/docs/using.md +4 -2
- package/genome/rules/turri-regeneration.md +1 -1
- package/package.json +1 -1
package/dist/sentinel.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { appendFileSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { load as loadYaml } from "js-yaml";
|
|
3
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
|
-
import { readProjectConfig } from "./config.js";
|
|
6
|
+
import { readConfigFile, readProjectConfig } from "./config.js";
|
|
6
7
|
import { confinedTools } from "./regenerate/confine.js";
|
|
7
8
|
import { describeSpecConflict } from "./regenerate/stop-report.js";
|
|
8
9
|
import { didNotPass } from "./regenerate/job-panel.js";
|
|
@@ -30,9 +31,10 @@ export function sentinelSetting(cwd) {
|
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* What the person is asked before the sentinel first opens in a session, under `sentinel: ask`, in one sentence: the pulse
|
|
33
|
-
* that would open it and what it does - so a tab that starts working on its own is not a surprise
|
|
34
|
+
* - or the host's check - that would open it and what it does - so a tab that starts working on its own is not a surprise
|
|
35
|
+
* - and whether to start it.
|
|
34
36
|
*/
|
|
35
|
-
export function sentinelStartNotice(
|
|
37
|
+
export function sentinelStartNotice(about, cwd) {
|
|
36
38
|
let released = false;
|
|
37
39
|
try {
|
|
38
40
|
released = readProjectConfig(cwd).releaseAfterPulse === true;
|
|
@@ -40,10 +42,28 @@ export function sentinelStartNotice(job, cwd) {
|
|
|
40
42
|
catch {
|
|
41
43
|
/* a config that cannot be read releases nothing */
|
|
42
44
|
}
|
|
45
|
+
const what = `a background tab that reviews each finished pulse${released ? " and tries to use what was released" : ""}`;
|
|
46
|
+
if (!("kind" in about)) {
|
|
47
|
+
return {
|
|
48
|
+
kind: "sentinel-start",
|
|
49
|
+
severity: "info",
|
|
50
|
+
summary: `Checking ${about.funcIds.join(", ")} of ${about.module} needs the sentinel, ${what} - start it?`,
|
|
51
|
+
evidence: [],
|
|
52
|
+
actions: [
|
|
53
|
+
{ label: SENTINEL_START.start, task: "The sentinel is started for this session: this check, and each pulse after it, go to it." },
|
|
54
|
+
{ label: SENTINEL_START.always, task: "The sentinel is started, and sentinel: true written to config.turri.yaml: it opens without asking from now on." },
|
|
55
|
+
{ label: SENTINEL_START.notNow, task: "No sentinel this session: the check is not done. turri asks again next time." },
|
|
56
|
+
{ label: SENTINEL_START.never, task: "sentinel: false written to config.turri.yaml: no sentinel, and no asking." },
|
|
57
|
+
],
|
|
58
|
+
module: about.module,
|
|
59
|
+
check: about.id,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const job = about;
|
|
43
63
|
return {
|
|
44
64
|
kind: "sentinel-start",
|
|
45
65
|
severity: "info",
|
|
46
|
-
summary: `${pulseName(job)} finished - start the sentinel,
|
|
66
|
+
summary: `${pulseName(job)} finished - start the sentinel, ${what}?`,
|
|
47
67
|
evidence: [],
|
|
48
68
|
actions: [
|
|
49
69
|
{ label: SENTINEL_START.start, task: "The sentinel is started for this session: this pulse and each after it go to it." },
|
|
@@ -112,9 +132,19 @@ export function pulseEndsBlock(sentinel) {
|
|
|
112
132
|
* What the chat the person is in is told of a pulse that landed, with the sentinel on: that it landed, what the sentinel
|
|
113
133
|
* does with it - and to tell the person so, and do nothing else about it.
|
|
114
134
|
*/
|
|
115
|
-
export function landedNote(job, released) {
|
|
135
|
+
export function landedNote(job, released, options = {}) {
|
|
116
136
|
const wasReleased = job.details?.released === true;
|
|
117
137
|
const where = released.map((item) => `${item.name} at ${item.url ?? item.command}`).join(", ");
|
|
138
|
+
// The host showed the person the pulse's end itself: what happened, for when they ask - nobody is told anything now.
|
|
139
|
+
if (options.shown === true) {
|
|
140
|
+
return [
|
|
141
|
+
`Pulse ${job.id} - ${pulseName(job)} - landed, after ${formatDuration(job)}.`,
|
|
142
|
+
"",
|
|
143
|
+
(job.report ?? "").trim(),
|
|
144
|
+
"",
|
|
145
|
+
`The sentinel - a chat of my own beside this one - checked it after it landed${wasReleased ? `, and tested what was released${where !== "" ? ` (${where})` : ""}` : ""}; the person was shown that it landed. The pulse is the sentinel's: nothing is to be tested, released, opened or run for it here.`,
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
118
148
|
return [
|
|
119
149
|
`Pulse ${job.id} - ${pulseName(job)} - landed, after ${formatDuration(job)}.`,
|
|
120
150
|
"",
|
|
@@ -137,8 +167,9 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
137
167
|
return [
|
|
138
168
|
"<sentinel>",
|
|
139
169
|
"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, each in a conversation of its own
|
|
141
|
-
"
|
|
170
|
+
"with turri. Every pulse that finishes comes to you, each in a conversation of its own - and functional specs the person",
|
|
171
|
+
"asks you to check. This conversation is for the pulse, or the check, in the message turri writes first. For a pulse:",
|
|
172
|
+
"what it was, how it ended,",
|
|
142
173
|
...(surface === ""
|
|
143
174
|
? ["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."]
|
|
144
175
|
: [
|
|
@@ -153,10 +184,18 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
153
184
|
" where the message says. How far you test is yours to judge, from what the pulse changed and what the ***plain says",
|
|
154
185
|
" it should do: that it came up, answers, and does what its ***plain diff says it now does.",
|
|
155
186
|
"3. When something is wrong - the pulse failed or stopped, it did not pass, the release did not come up, the software",
|
|
156
|
-
" does not do what its ***plain says - find out why
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"
|
|
187
|
+
" does not do what its ***plain says - find out why. Then:",
|
|
188
|
+
" - When it is the project's tooling - a testing script, or where config.turri.yaml says the scripts are and which",
|
|
189
|
+
" toolchain folders the agents may read (toolchains:) - fix it yourself, with edit: a script calling python3 where",
|
|
190
|
+
" this machine has python of the version the ***plain names, a path that moved, a flag the tool's version does not",
|
|
191
|
+
" take. Check what you rely on in your shell first (`command -v python`, `python --version`). Then call resume for",
|
|
192
|
+
" the module, so the work runs again with the fix - and notify_person once, fixed: true, severity info, no actions:",
|
|
193
|
+
" what you changed and why, the lines before and after in the evidence.",
|
|
194
|
+
" - Anything else, call notify_person once: what is wrong, what shows it, and what the person can do about it, each",
|
|
195
|
+
" a task the agent of their chat can carry out once they accept it - a change to the ***plain, a credential to",
|
|
196
|
+
" provide, a pulse to regenerate again. Be exact: the task is all that agent is given. What the machine lacks - a",
|
|
197
|
+
" toolchain, a tool, a system library - and any setting of the machine's or the person's shell: say exactly what",
|
|
198
|
+
" the person runs themselves (`brew install node`), and make the action resuming once they have.",
|
|
160
199
|
" When it is a gap in the ***plain the build settled with an assumption - something the person may reasonably leave",
|
|
161
200
|
" as it is - say so with ignorable: the gap in the summary, what the build does in the evidence. Never for",
|
|
162
201
|
" something broken, a pulse that went wrong, or a release that did not come up.",
|
|
@@ -164,9 +203,16 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
164
203
|
"5. Last, call conclude once for it - holds or wrong, and in a line or two what you checked and what you found:",
|
|
165
204
|
" the chat the person is in is told, whether all is well or not. Then say the same here, in a line or two.",
|
|
166
205
|
"",
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"a
|
|
206
|
+
"For a check: the person picked functional specs of a module for you to test in its release, which runs where the",
|
|
207
|
+
"message says. Test those - what each says and its scenarios - not the module's other functional specs. Notify the",
|
|
208
|
+
"person of what does not do what its ***plain says, as for a pulse, and call conclude once for the check, by its id.",
|
|
209
|
+
"",
|
|
210
|
+
"You fix the project's tooling and nothing else. Your file tools read the whole project; write and edit reach only the",
|
|
211
|
+
"testing scripts and config.turri.yaml - and in it only where the scripts are, and toolchains:. Your shell runs with",
|
|
212
|
+
"the project read only. You never change ***plain - it is the person's intent, never yours to fix - and never steer,",
|
|
213
|
+
"release, or regenerate but by resume after a fix of yours. You install nothing on this machine and change none of",
|
|
214
|
+
"its settings, nor the person's shell - under no circumstances: that is always the person's, said in a notice. You",
|
|
215
|
+
"never ask the person questions - a notice is how you reach them.",
|
|
170
216
|
"",
|
|
171
217
|
"The person can talk to you here. When they tell you how to check - what to test, what to leave alone, where things",
|
|
172
218
|
"run - keep it with keep_guidance, in their words, so it holds from now on: this conversation ends with its pulse,",
|
|
@@ -222,12 +268,7 @@ export function sentinelBrief(job, context) {
|
|
|
222
268
|
lines.push("", `Its log: ${job.logFile}`);
|
|
223
269
|
const released = job.details?.released === true;
|
|
224
270
|
if (released) {
|
|
225
|
-
lines.push("", "It was released, as its last stage (release-after-pulse). What runs now:");
|
|
226
|
-
if (context.released.length === 0)
|
|
227
|
-
lines.push(" nothing is running for it: the release did not leave anything running.");
|
|
228
|
-
for (const item of context.released)
|
|
229
|
-
lines.push(` ${item.name}: ${item.url ?? item.command} - in ${item.dir}, its output in ${item.log}`);
|
|
230
|
-
lines.push("Test it.");
|
|
271
|
+
lines.push("", "It was released, as its last stage (release-after-pulse). What runs now:", ...whatRuns(context.released), "Test it.");
|
|
231
272
|
}
|
|
232
273
|
else if (job.status === "succeeded" && !didNotPass(job)) {
|
|
233
274
|
lines.push("", context.releaseAfterPulse ? "Nothing was released after it." : "release-after-pulse is off: nothing was released, and you test nothing running - check the pulse alone.");
|
|
@@ -236,22 +277,144 @@ export function sentinelBrief(job, context) {
|
|
|
236
277
|
lines.push("", "It stopped on two functional specs that contradict each other: turri has put that to the person itself. Notify no one about it.");
|
|
237
278
|
}
|
|
238
279
|
else if (wentWrong(job)) {
|
|
239
|
-
lines.push("", "It went wrong: find out why, and notify the person. What the chat would have been told to do about it - each step of it
|
|
280
|
+
lines.push("", "It went wrong: find out why. A testing script, or where the config says the scripts are, is yours to fix - then resume", "the module, and tell the person what you changed. Anything else, notify the person. What the chat would have been told", "to do about it - each step of it that is not a fix to the tooling is a task for a notice's action, not yours, and an", "install is never anyone's but the person's:", context.reaction);
|
|
240
281
|
}
|
|
241
282
|
lines.push("", `When you are done with it, call conclude for ${job.id} - whether all is well or not.`);
|
|
242
283
|
return lines.join("\n");
|
|
243
284
|
}
|
|
285
|
+
/** What runs of a release, a line each: where, from where, its output - or that nothing was left running. */
|
|
286
|
+
function whatRuns(released) {
|
|
287
|
+
if (released.length === 0)
|
|
288
|
+
return [" nothing is running for it: the release did not leave anything running."];
|
|
289
|
+
return released.map((item) => ` ${item.name}: ${item.url ?? item.command} - in ${item.dir}, its output in ${item.log}`);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* What the sentinel is told of a host's check: the module and its release - what runs, and where - and the functional
|
|
293
|
+
* specs to test, with what each says and its scenarios, as turri reads them now.
|
|
294
|
+
*/
|
|
295
|
+
export function sentinelCheckBrief(check, context) {
|
|
296
|
+
const lines = [
|
|
297
|
+
`Check ${check.id}: the person asked you to check ${check.funcIds.length === 1 ? "a functional spec" : `${check.funcIds.length} functional specs`} of ${check.module}, in its release.`,
|
|
298
|
+
];
|
|
299
|
+
if (context.focused !== undefined)
|
|
300
|
+
lines.push(`The person is in the chat "${context.focused}": a notice goes there.`);
|
|
301
|
+
lines.push("", `${check.module}'s release holds its code and that of every module its requires chain builds on: its last commit${context.commit !== undefined ? `, ${context.commit}` : ""}, which landed whole - never what a pulse is writing now.`, context.releasedNow ? "It was released for this check. What runs now:" : "What runs was released from that commit already, and is left as it is. What runs now:", ...whatRuns(context.released), "", `Test these functional specs of ${check.module} - only these, not its others, though they are in the release too:`);
|
|
302
|
+
for (const spec of context.specs) {
|
|
303
|
+
lines.push("", `# ${spec.funcId}`, spec.text);
|
|
304
|
+
for (const [title, scenarios] of [["Conformance scenarios", spec.conformance], ["Acceptance scenarios", spec.acceptance]]) {
|
|
305
|
+
if (scenarios.length === 0)
|
|
306
|
+
continue;
|
|
307
|
+
lines.push(`${title}:`, ...scenarios.map((scenario) => `- ${scenario.id}: ${scenario.text.replace(/\n+/g, " ")}`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
lines.push("", "How far you test each is yours to judge, from what it says and its scenarios. When one does not do what its ***plain says, find out why and notify the person, as for a pulse, naming the check.", `When you are done, call conclude for check ${check.id} - whether all is well or not.`);
|
|
311
|
+
return lines.join("\n");
|
|
312
|
+
}
|
|
244
313
|
// ---------------------------------------------------------------------------------------------
|
|
245
314
|
// Its tools.
|
|
246
|
-
/** The tools the sentinel's agent may call: what reads, its notices and its guidance - and what a host gives it. */
|
|
247
|
-
export const SENTINEL_TOOLS = ["read", "ls", "grep", "find", "bash", "read_output", "ledger", "pulses", "diagnose", "parse", "doctor", "web_search", "web_fetch", "notify_person", "conclude", "keep_guidance"];
|
|
248
315
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
|
|
316
|
+
* The tools the sentinel's agent may call: what reads, write and edit for the project's tooling alone, resume, its
|
|
317
|
+
* notices and its guidance - and what a host gives it.
|
|
318
|
+
*/
|
|
319
|
+
export const SENTINEL_TOOLS = ["read", "ls", "grep", "find", "write", "edit", "bash", "read_output", "ledger", "pulses", "diagnose", "parse", "doctor", "web_search", "web_fetch", "resume", "notify_person", "conclude", "keep_guidance"];
|
|
320
|
+
/** A testing script by its name - `run_unittests_python.sh`, `prepare_environment_node.sh` - and a config file of turri's. */
|
|
321
|
+
const SCRIPT_FILE = /^(run_|prepare_environment).*\.(sh|ps1|bat|cmd)$/;
|
|
322
|
+
const CONFIG_FILE_NAME = /^config\.turri(\..+)?\.yaml$/;
|
|
323
|
+
/** The keys of config.turri.yaml the sentinel may change: where the testing scripts are, and the toolchains' folders. */
|
|
324
|
+
export const SENTINEL_CONFIG_KEYS = ["unittests-script", "conformance-tests-script", "prepare-environment-script", "toolchains"];
|
|
325
|
+
/**
|
|
326
|
+
* The project's tooling, which the sentinel fixes itself: its testing scripts - `test_scripts/`, a `run_*.sh` or
|
|
327
|
+
* `prepare_environment*.sh` at its root, and each script its config names - and its `config.turri*.yaml` files. In the
|
|
328
|
+
* project, and in a project one folder down (***plain kept in plain/). Never a .plain file: the ***plain is the person's.
|
|
329
|
+
*/
|
|
330
|
+
export function sentinelTooling(cwd) {
|
|
331
|
+
const root = resolve(cwd);
|
|
332
|
+
const projects = [root, ...listDirs(root).filter((dir) => listFiles(dir).some((name) => CONFIG_FILE_NAME.test(name)))];
|
|
333
|
+
return [
|
|
334
|
+
...new Set(projects.flatMap((project) => {
|
|
335
|
+
const configs = listFiles(project).filter((name) => CONFIG_FILE_NAME.test(name));
|
|
336
|
+
const named = configs.flatMap((name) => {
|
|
337
|
+
const config = readConfigFile(join(project, name));
|
|
338
|
+
return [config.unittestsScript, config.conformanceTestsScript, config.prepareEnvironmentScript].filter((path) => typeof path === "string").map((path) => resolve(project, path));
|
|
339
|
+
});
|
|
340
|
+
return [
|
|
341
|
+
join(project, "test_scripts"),
|
|
342
|
+
...listFiles(project).filter((name) => SCRIPT_FILE.test(name)).map((name) => join(project, name)),
|
|
343
|
+
...configs.map((name) => join(project, name)),
|
|
344
|
+
...named,
|
|
345
|
+
];
|
|
346
|
+
})),
|
|
347
|
+
].filter((path) => !path.endsWith(".plain"));
|
|
348
|
+
}
|
|
349
|
+
function listFiles(dir) {
|
|
350
|
+
try {
|
|
351
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function listDirs(dir) {
|
|
358
|
+
try {
|
|
359
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== MODULES_DIR && entry.name !== "node_modules").map((entry) => join(dir, entry.name));
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Its file tools and shell: the whole project to read, and to write only its tooling (`sentinelTooling`) - in a config
|
|
367
|
+
* file, only the keys of SENTINEL_CONFIG_KEYS. Its shell runs behind the command check, the OS sandbox where one is up,
|
|
368
|
+
* and the audit, with the project read only and the network open - and changes nothing of the machine's. Its home and
|
|
369
|
+
* temporary folder are its own, in the modules' scratch.
|
|
252
370
|
*/
|
|
253
371
|
export function sentinelFileTools(cwd) {
|
|
254
|
-
|
|
372
|
+
const tooling = sentinelTooling(cwd);
|
|
373
|
+
const { tools } = confinedTools({ workdir: cwd, read: [cwd], write: tooling, shellHome: join(cwd, MODULES_DIR, `.${SENTINEL}`), fixesScripts: true });
|
|
374
|
+
const shell = confinedTools({ workdir: cwd, read: [cwd], write: [], shellHome: join(cwd, MODULES_DIR, `.${SENTINEL}`) }).tools;
|
|
375
|
+
return tools.map((tool) => {
|
|
376
|
+
// Its shell writes nothing in the project: only write and edit reach the tooling.
|
|
377
|
+
if (tool.name === "bash")
|
|
378
|
+
return shell.find((each) => each.name === "bash");
|
|
379
|
+
if (tool.name !== "write" && tool.name !== "edit")
|
|
380
|
+
return tool;
|
|
381
|
+
return {
|
|
382
|
+
...tool,
|
|
383
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
384
|
+
const path = resolve(cwd, String(params.path ?? ""));
|
|
385
|
+
if (!CONFIG_FILE_NAME.test(basename(path)))
|
|
386
|
+
return tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
387
|
+
// A config file: only where the testing scripts are, and the toolchains, may change - anything else is put back.
|
|
388
|
+
const before = existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
389
|
+
const result = await tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
390
|
+
const changed = changedKeys(before ?? "", existsSync(path) ? readFileSync(path, "utf8") : "").filter((key) => !SENTINEL_CONFIG_KEYS.includes(key.replace(/_/g, "-")));
|
|
391
|
+
if (changed.length > 0) {
|
|
392
|
+
if (before === undefined)
|
|
393
|
+
rmSync(path, { force: true });
|
|
394
|
+
else
|
|
395
|
+
writeFileSync(path, before);
|
|
396
|
+
throw new Error(`Put back: in ${basename(path)} the sentinel changes only ${SENTINEL_CONFIG_KEYS.join(", ")} - not ${changed.join(", ")}. A setting of any other kind is the person's: put it to them with notify_person.`);
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/** The top-level keys whose values differ between two config texts - one that does not parse differing in every key. */
|
|
404
|
+
function changedKeys(before, after) {
|
|
405
|
+
const parse = (text) => {
|
|
406
|
+
try {
|
|
407
|
+
const value = loadYaml(text);
|
|
408
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : value === undefined || value === null ? {} : undefined;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const [was, now] = [parse(before), parse(after)];
|
|
415
|
+
if (was === undefined || now === undefined)
|
|
416
|
+
return ["the file as a whole - it no longer reads as YAML"];
|
|
417
|
+
return [...new Set([...Object.keys(was), ...Object.keys(now)])].filter((key) => JSON.stringify(was[key]) !== JSON.stringify(now[key]));
|
|
255
418
|
}
|
|
256
419
|
export function notifyTool(notify) {
|
|
257
420
|
return defineTool({
|
|
@@ -259,7 +422,8 @@ export function notifyTool(notify) {
|
|
|
259
422
|
label: "Notify",
|
|
260
423
|
description: "Put what is wrong with a finished pulse to the person, in the chat they are in: what is wrong, what shows it, and what " +
|
|
261
424
|
"they can do about it. Each action is a task the agent of their chat carries out once they accept it. Only when they " +
|
|
262
|
-
"need to act - never for a pulse that went well
|
|
425
|
+
"need to act - never for a pulse that went well - or, severity info and no actions, to say what you fixed yourself in " +
|
|
426
|
+
"the testing scripts or their config, and why.",
|
|
263
427
|
parameters: Type.Object({
|
|
264
428
|
severity: Type.Union([Type.Literal("error"), Type.Literal("warning"), Type.Literal("info")], { description: "error: it is broken; warning: it works but not as its ***plain says, or something is missing; info: worth their knowing" }),
|
|
265
429
|
summary: Type.String({ description: "What is wrong, in one line" }),
|
|
@@ -270,6 +434,8 @@ export function notifyTool(notify) {
|
|
|
270
434
|
}), { description: "What can be done, the best first" }),
|
|
271
435
|
module: Type.Optional(Type.String({ description: "The module it is about" })),
|
|
272
436
|
pulse: Type.Optional(Type.String({ description: "The pulse's id" })),
|
|
437
|
+
check: Type.Optional(Type.String({ description: "The check's id, when it is about a check the person asked for" })),
|
|
438
|
+
fixed: Type.Optional(Type.Boolean({ description: "true when this says what you fixed yourself in a testing script or its config - severity info, no actions: a note, nothing to answer" })),
|
|
273
439
|
ignorable: Type.Optional(Type.Boolean({
|
|
274
440
|
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
441
|
"assumption - then say the gap in the summary and what the build does in the evidence (the ***plain does not say what happens to " +
|
|
@@ -283,8 +449,10 @@ export function notifyTool(notify) {
|
|
|
283
449
|
evidence: params.evidence,
|
|
284
450
|
actions: params.actions,
|
|
285
451
|
...(params.ignorable === true ? { ignorable: true } : {}),
|
|
452
|
+
...(params.fixed === true ? { kind: "fixed" } : {}),
|
|
286
453
|
...(params.module !== undefined ? { module: params.module } : {}),
|
|
287
454
|
...(params.pulse !== undefined ? { job: params.pulse } : {}),
|
|
455
|
+
...(params.check !== undefined ? { check: params.check } : {}),
|
|
288
456
|
});
|
|
289
457
|
return {
|
|
290
458
|
content: [{ type: "text", text: notice !== undefined ? `Put to the person, in the chat they are in (notice ${notice.id}).` : "There is no chat to put it in: say it here." }],
|
|
@@ -297,15 +465,19 @@ export function concludeTool(conclude) {
|
|
|
297
465
|
return defineTool({
|
|
298
466
|
name: "conclude",
|
|
299
467
|
label: "Conclude",
|
|
300
|
-
description: "Say you are done with a pulse - whether what you checked holds, and in a line or two what
|
|
301
|
-
"the person is in is told, whether all is well or not. Once a pulse, last - after
|
|
468
|
+
description: "Say you are done with a pulse - or a check the person asked for - whether what you checked holds, and in a line or two what " +
|
|
469
|
+
"you checked and found. The chat the person is in is told, whether all is well or not. Once a pulse or check, last - after " +
|
|
470
|
+
"notify_person, when something is wrong. Name the pulse, or the check.",
|
|
302
471
|
parameters: Type.Object({
|
|
303
|
-
pulse: Type.String({ description: "The pulse's id" }),
|
|
472
|
+
pulse: Type.Optional(Type.String({ description: "The pulse's id" })),
|
|
473
|
+
check: Type.Optional(Type.String({ description: "The check's id, for a check the person asked for - in place of pulse" })),
|
|
304
474
|
verdict: Type.Union([Type.Literal("holds"), Type.Literal("wrong")], { description: "holds: it does what its ***plain says; wrong: it does not, or it went wrong" }),
|
|
305
475
|
summary: Type.String({ description: "What you checked and what you found, in a line or two: \"GET /api/ping answers pong; the new alias route redirects\"" }),
|
|
306
476
|
}),
|
|
307
477
|
async execute(_id, params) {
|
|
308
|
-
|
|
478
|
+
if ((params.pulse === undefined) === (params.check === undefined))
|
|
479
|
+
throw new Error("Name the pulse, or the check - one of them.");
|
|
480
|
+
const chat = conclude({ ...(params.pulse !== undefined ? { job: params.pulse } : { check: params.check }), verdict: params.verdict, summary: params.summary });
|
|
309
481
|
return {
|
|
310
482
|
content: [{ type: "text", text: chat !== undefined ? "The chat the person is in is told." : "There is no chat to tell: say it here." }],
|
|
311
483
|
details: {},
|
|
@@ -314,10 +486,27 @@ export function concludeTool(conclude) {
|
|
|
314
486
|
});
|
|
315
487
|
}
|
|
316
488
|
/** What the chat the person is in is told when the sentinel is done with a pulse: to say so, and do nothing more. */
|
|
317
|
-
export function concludedNote(conclusion) {
|
|
318
|
-
const about = conclusion.
|
|
489
|
+
export function concludedNote(conclusion, options = {}) {
|
|
490
|
+
const about = conclusion.check !== undefined
|
|
491
|
+
? `the check you asked for${conclusion.module !== undefined ? ` of ${conclusion.module}` : ""}`
|
|
492
|
+
: conclusion.module !== undefined
|
|
493
|
+
? `${conclusion.module}${conclusion.pulse !== undefined ? ` pulse ${conclusion.pulse}` : ""}`
|
|
494
|
+
: `pulse ${conclusion.job}`;
|
|
495
|
+
if (conclusion.verdict === "unchecked") {
|
|
496
|
+
if (options.shown === true)
|
|
497
|
+
return [`The sentinel did not do ${about} (${conclusion.check ?? conclusion.job}).`, conclusion.summary.trim()].join("\n");
|
|
498
|
+
return [`The sentinel did not do ${about} (${conclusion.check ?? conclusion.job}).`, conclusion.summary.trim(), "", "Tell the person, in a line, that it was not checked and why. Do nothing else about it."].join("\n");
|
|
499
|
+
}
|
|
500
|
+
// The host showed the person the conclusion itself: what the sentinel found, for when they ask.
|
|
501
|
+
if (options.shown === true) {
|
|
502
|
+
return [
|
|
503
|
+
`The sentinel is done with ${about} (${conclusion.check ?? conclusion.job}): ${conclusion.verdict === "holds" ? "it holds" : "something is wrong"}.`,
|
|
504
|
+
conclusion.summary.trim(),
|
|
505
|
+
...(conclusion.verdict === "wrong" ? ["", "Its notice in this chat says what is wrong and what can be done; nothing is done about it until the person answers it."] : []),
|
|
506
|
+
].join("\n");
|
|
507
|
+
}
|
|
319
508
|
return [
|
|
320
|
-
`The sentinel is done with ${about} (${conclusion.job}): ${conclusion.verdict === "holds" ? "it holds" : "something is wrong"}.`,
|
|
509
|
+
`The sentinel is done with ${about} (${conclusion.check ?? conclusion.job}): ${conclusion.verdict === "holds" ? "it holds" : "something is wrong"}.`,
|
|
321
510
|
conclusion.summary.trim(),
|
|
322
511
|
"",
|
|
323
512
|
conclusion.verdict === "holds"
|
|
@@ -325,6 +514,20 @@ export function concludedNote(conclusion) {
|
|
|
325
514
|
: "Tell the person, in a line, that the sentinel found something wrong, and that its notice in this chat says what and what can be done. Do nothing else about it until they answer the notice.",
|
|
326
515
|
].join("\n");
|
|
327
516
|
}
|
|
517
|
+
/** Bring a module up again once the sentinel fixed what stopped its pulse: what is left there is regenerated, its scripts run. */
|
|
518
|
+
export function resumeTool(resume) {
|
|
519
|
+
return defineTool({
|
|
520
|
+
name: "resume",
|
|
521
|
+
label: "Resume",
|
|
522
|
+
description: "Bring a module up again after you fixed what stopped its pulse - a testing script, a toolchains: entry: what is left to " +
|
|
523
|
+
"regenerate there runs again, its testing scripts with it, as when the person asks for it. Only after a fix of yours, " +
|
|
524
|
+
"never to try again what nothing was changed for. What it starts comes back to you as a pulse that finished.",
|
|
525
|
+
parameters: Type.Object({ plain_file: Type.String({ description: "The module's .plain file, from the project's folder: app.plain" }) }),
|
|
526
|
+
async execute(_id, params) {
|
|
527
|
+
return { content: [{ type: "text", text: await resume(params.plain_file) }], details: {} };
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}
|
|
328
531
|
export function keepGuidanceTool(cwd) {
|
|
329
532
|
return defineTool({
|
|
330
533
|
name: "keep_guidance",
|
package/dist/setup.d.ts
CHANGED
|
@@ -35,8 +35,8 @@ export declare function checkSandbox(wall?: Wall, platform?: NodeJS.Platform): C
|
|
|
35
35
|
export declare function checkProject(project: string, machine?: {
|
|
36
36
|
has?: (command: string) => boolean;
|
|
37
37
|
exists?: (path: string) => boolean;
|
|
38
|
-
jdk?: () => boolean
|
|
39
|
-
}): Check[]
|
|
38
|
+
jdk?: () => boolean | Promise<boolean>;
|
|
39
|
+
}): Promise<Check[]>;
|
|
40
40
|
/**
|
|
41
41
|
* In a project: each model its config names - with the stack chosen, that stack's over it - tried with one short
|
|
42
42
|
* request. A model that answers is ready; one that does not says what the provider said, and what to do about it.
|
|
@@ -55,11 +55,32 @@ export interface DoctorOptions {
|
|
|
55
55
|
* nothing, so a host can show them in its own way; `turri doctor` prints the same.
|
|
56
56
|
*/
|
|
57
57
|
export declare function doctor(options?: DoctorOptions): Promise<Check[]>;
|
|
58
|
+
/** The environment pi-claude-code refuses to run with: each points Claude Code at an API key or another backend, not the subscription. */
|
|
59
|
+
export declare const CLAUDE_CODE_CONFLICTS: string[];
|
|
60
|
+
/** What the Claude subscription check looks at - this machine's, or a test's. */
|
|
61
|
+
export interface ClaudeCodeProbe {
|
|
62
|
+
/** Whether pi has the claude-code provider: its extension, pi-claude-code, installed. */
|
|
63
|
+
installed(): Promise<boolean>;
|
|
64
|
+
/** `claude auth status`, as it answers: undefined when there is no claude to run. */
|
|
65
|
+
status(command: string, env: NodeJS.ProcessEnv): Promise<{
|
|
66
|
+
loggedIn?: boolean;
|
|
67
|
+
subscriptionType?: string;
|
|
68
|
+
authMethod?: string;
|
|
69
|
+
} | undefined>;
|
|
70
|
+
/** The executable a command name is, on the PATH - undefined when it is not there. */
|
|
71
|
+
which(command: string, env: NodeJS.ProcessEnv): string | undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A Claude subscription through Claude Code, when the project's config names a `claude-code/...` model: everything
|
|
75
|
+
* pi-claude-code needs, in the order it needs it - its extension installed, Claude Code on the PATH, signed in to a Pro or
|
|
76
|
+
* Max subscription, and nothing in the environment it refuses. Nothing when no model is a claude-code one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkClaudeSubscription(project: string, env?: NodeJS.ProcessEnv, probe?: ClaudeCodeProbe): Promise<Check[]>;
|
|
58
79
|
/**
|
|
59
80
|
* The base folders the project's config names - its own, and each stack's - each there with its files, or not: a codebase
|
|
60
81
|
* that requires nothing starts from one, and its first pulse stops when it is not there. Nothing when none is named.
|
|
61
82
|
*/
|
|
62
|
-
export declare function checkBaseFolders(project: string): Check[]
|
|
83
|
+
export declare function checkBaseFolders(project: string): Promise<Check[]>;
|
|
63
84
|
/**
|
|
64
85
|
* The AWS region a project's Bedrock models are called in, when it uses any: turri's, the person's or the profile's - and
|
|
65
86
|
* what is wrong with it, when the models need two, or the person's is not in their geography. Nothing when it uses none.
|