@turri-dev/cli 0.1.28-next.d5f2680 → 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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { delimiter, dirname, join, resolve, win32 } from "node:path";
|
|
3
|
+
import { AGENT_DIR, PACKAGE_ROOT, turriIdentity } from "./environment.js";
|
|
4
|
+
/**
|
|
5
|
+
* Which turri runs, which is newest, and which others are on this machine - so a person who installed a new turri and
|
|
6
|
+
* is still running an old one is told so, and how to fix it, where they look: the banner as turri starts (from what the
|
|
7
|
+
* last check found, never waiting on the network), and `turri --version` and `turri doctor` (asking npm then and there).
|
|
8
|
+
* A turri run from a clone is the developer's: it is never told to update.
|
|
9
|
+
*/
|
|
10
|
+
export const PACKAGE_NAME = "@turri-dev/cli";
|
|
11
|
+
const INSTALL = `npm install -g ${PACKAGE_NAME}@latest`;
|
|
12
|
+
/** Where the newest version npm has is kept between starts. */
|
|
13
|
+
const CACHE = join(AGENT_DIR, "turri-latest.json");
|
|
14
|
+
/** How long what npm said is trusted before the banner asks again, in the background. */
|
|
15
|
+
const STALE_MS = 6 * 60 * 60 * 1000;
|
|
16
|
+
/** The check is off in turri's own tests, and when TURRI_UPDATE_CHECK=off. */
|
|
17
|
+
const checkOff = () => process.env.TURRI_UPDATE_CHECK === "off";
|
|
18
|
+
/** Whether version `a` is newer than `b`: major, minor, patch - and a release over a prerelease of the same. */
|
|
19
|
+
export function newerThan(a, b) {
|
|
20
|
+
const parse = (version) => {
|
|
21
|
+
const [core = "", pre] = version.split("-", 2);
|
|
22
|
+
return { parts: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre: pre !== undefined };
|
|
23
|
+
};
|
|
24
|
+
const [x, y] = [parse(a), parse(b)];
|
|
25
|
+
for (let at = 0; at < 3; at += 1) {
|
|
26
|
+
if ((x.parts[at] ?? 0) !== (y.parts[at] ?? 0))
|
|
27
|
+
return (x.parts[at] ?? 0) > (y.parts[at] ?? 0);
|
|
28
|
+
}
|
|
29
|
+
return !x.pre && y.pre;
|
|
30
|
+
}
|
|
31
|
+
/** The newest version npm had when it was last asked, and when - nothing when it never was. */
|
|
32
|
+
export function latestKnown() {
|
|
33
|
+
try {
|
|
34
|
+
const cached = JSON.parse(readFileSync(CACHE, "utf8"));
|
|
35
|
+
return typeof cached.version === "string" && typeof cached.checkedAt === "number" ? { version: cached.version, checkedAt: cached.checkedAt } : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The newest version npm has now, kept for the next start - nothing when npm does not answer in time. */
|
|
42
|
+
export async function fetchLatest(timeoutMs = 3000) {
|
|
43
|
+
if (checkOff())
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2F")}/latest`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return undefined;
|
|
49
|
+
const version = (await response.json()).version;
|
|
50
|
+
if (typeof version !== "string")
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync(dirname(CACHE), { recursive: true });
|
|
54
|
+
writeFileSync(CACHE, `${JSON.stringify({ version, checkedAt: Date.now() })}\n`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* not kept: asked again next time */
|
|
58
|
+
}
|
|
59
|
+
return version;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Ask npm again in the background when what it said last is old: what it says now, once it answers - undefined when it
|
|
67
|
+
* was not asked, or did not answer. Never waited for by what starts turri.
|
|
68
|
+
*/
|
|
69
|
+
export function refreshLatestLater() {
|
|
70
|
+
if (checkOff())
|
|
71
|
+
return Promise.resolve(undefined);
|
|
72
|
+
const known = latestKnown();
|
|
73
|
+
if (known !== undefined && Date.now() - known.checkedAt < STALE_MS)
|
|
74
|
+
return Promise.resolve(undefined);
|
|
75
|
+
return fetchLatest().catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
/** Whether a command is a link - npm's on macOS and Linux - rather than a launcher file. */
|
|
78
|
+
const isLink = (command) => {
|
|
79
|
+
try {
|
|
80
|
+
return lstatSync(command).isSymbolicLink();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** The package a turri command runs: up from where it really is, to the folder whose package.json is turri's. */
|
|
87
|
+
function packageOf(command) {
|
|
88
|
+
// Beside its command, where npm puts it when the command is a launcher rather than a link: <prefix>/node_modules on
|
|
89
|
+
// Windows, <prefix>/lib/node_modules elsewhere.
|
|
90
|
+
for (const beside of [join(dirname(command), "node_modules", "@turri-dev", "cli"), join(dirname(command), "..", "lib", "node_modules", "@turri-dev", "cli")]) {
|
|
91
|
+
try {
|
|
92
|
+
if (JSON.parse(readFileSync(join(beside, "package.json"), "utf8")).name === PACKAGE_NAME && !isLink(command))
|
|
93
|
+
return resolve(beside);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* not there */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let dir;
|
|
100
|
+
try {
|
|
101
|
+
dir = dirname(realpathSync(command));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
for (let at = 0; at < 6; at += 1) {
|
|
107
|
+
try {
|
|
108
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
109
|
+
if (pkg.name === PACKAGE_NAME)
|
|
110
|
+
return dir;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* not here: up */
|
|
114
|
+
}
|
|
115
|
+
const up = dirname(dir);
|
|
116
|
+
if (up === dir)
|
|
117
|
+
return undefined;
|
|
118
|
+
dir = up;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/** Every turri on the PATH, in its order - the first is what a bare `turri` runs. */
|
|
123
|
+
export function turrisOnPath(path = process.env.PATH ?? "", self = PACKAGE_ROOT, platform = process.platform) {
|
|
124
|
+
// On Windows npm writes three launchers for one install - turri.cmd, turri.ps1, turri - side by side: one is taken.
|
|
125
|
+
const names = platform === "win32" ? ["turri.cmd", "turri.ps1", "turri"] : ["turri"];
|
|
126
|
+
const found = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const dir of path.split(delimiter).filter((entry) => entry !== "")) {
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
const command = join(dir, name);
|
|
131
|
+
if (seen.has(command))
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
if (!statSync(command).isFile())
|
|
135
|
+
continue;
|
|
136
|
+
// Windows has no execute bit: a launcher there is a file.
|
|
137
|
+
if (platform !== "win32")
|
|
138
|
+
accessSync(command, constants.X_OK);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seen.add(command);
|
|
144
|
+
const root = packageOf(command);
|
|
145
|
+
if (root === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
if (platform === "win32" && found.some((turri) => dirname(turri.path) === dir))
|
|
148
|
+
continue;
|
|
149
|
+
let version;
|
|
150
|
+
try {
|
|
151
|
+
version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
version = undefined;
|
|
155
|
+
}
|
|
156
|
+
found.push({ path: command, root, ...(version !== undefined ? { version } : {}), clone: existsSync(join(root, ".git")), runs: found.length === 0, self: resolve(root) === resolve(self) });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
/** How to take a turri off the PATH, by where it is: another Node version of nvm, a linked clone, or an npm prefix. */
|
|
162
|
+
export function removalOf(turri, platform = process.platform) {
|
|
163
|
+
const nvm = /[/\\]\.nvm[/\\]versions[/\\]node[/\\](v[^/\\]+)[/\\]bin[/\\]turri/.exec(turri.path);
|
|
164
|
+
if (nvm !== null)
|
|
165
|
+
return `nvm exec ${nvm[1]} npm uninstall -g ${PACKAGE_NAME}`;
|
|
166
|
+
// nvm for Windows keeps each Node version in a folder of its own: switched to it, npm uninstalls from there.
|
|
167
|
+
const nvmWindows = /[/\\]nvm[/\\](v?\d+\.\d+\.\d+)[/\\]turri/i.exec(turri.path);
|
|
168
|
+
if (nvmWindows !== null)
|
|
169
|
+
return `nvm use ${nvmWindows[1]}, then npm uninstall -g ${PACKAGE_NAME}`;
|
|
170
|
+
if (turri.clone)
|
|
171
|
+
return `npm unlink -g ${PACKAGE_NAME} (it links a clone, ${turri.root})`;
|
|
172
|
+
// npm's prefix: the launcher's own folder on Windows, the folder above bin elsewhere.
|
|
173
|
+
const prefix = platform === "win32" ? win32.dirname(turri.path) : dirname(dirname(turri.path));
|
|
174
|
+
return `npm uninstall -g ${PACKAGE_NAME} --prefix ${prefix} (or delete ${turri.path})`;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* What the turri running, the newest npm has (`latest`, when known) and the turris on the PATH say together: nothing when
|
|
178
|
+
* all is well. A turri from a clone is told nothing of npm's.
|
|
179
|
+
*/
|
|
180
|
+
export function versionFindings(options = {}) {
|
|
181
|
+
const identity = options.self ?? (() => {
|
|
182
|
+
const own = turriIdentity();
|
|
183
|
+
return { ...(own.version !== null ? { version: own.version } : {}), clone: own.git_commit !== null, root: PACKAGE_ROOT };
|
|
184
|
+
})();
|
|
185
|
+
const onPath = options.onPath ?? turrisOnPath();
|
|
186
|
+
const findings = [];
|
|
187
|
+
// A newer turri installed than the one running, behind it on the PATH: the person installed it, and runs the old one.
|
|
188
|
+
const newest = onPath.filter((turri) => !turri.clone && turri.version !== undefined).sort((a, b) => (newerThan(a.version, b.version) ? -1 : 1))[0];
|
|
189
|
+
const runs = onPath.find((turri) => turri.runs);
|
|
190
|
+
if (!identity.clone && identity.version !== undefined && newest !== undefined && newest.version !== undefined && newerThan(newest.version, identity.version) && !newest.self) {
|
|
191
|
+
// Every turri in front of the newest one: a bare `turri` finds each before it.
|
|
192
|
+
const inFront = onPath.slice(0, onPath.indexOf(newest));
|
|
193
|
+
findings.push({
|
|
194
|
+
kind: "shadowed",
|
|
195
|
+
text: `turri ${newest.version} is installed too, at ${newest.path}, but ${runs?.self === true ? `this turri ${identity.version} comes first on your PATH` : `turri ${runs?.version ?? identity.version} comes before it on your PATH`} - a bare \`turri\` runs the older one`,
|
|
196
|
+
short: `turri ${newest.version} is installed, but this older turri ${identity.version} comes first on your PATH - turri doctor shows which, and how to fix it`,
|
|
197
|
+
fixes: [
|
|
198
|
+
...inFront.map((turri) => `take ${turri.path} (turri ${turri.version ?? "?"}${turri.self ? ", this one" : ""}) off the PATH: ${removalOf(turri)}`),
|
|
199
|
+
process.platform === "win32" ? "then open a new terminal" : "then `hash -r`, or a new terminal - the shell remembers where it last found turri",
|
|
200
|
+
`meanwhile, run the newer one by its path - ${newest.path} - or npx ${PACKAGE_NAME}@latest`,
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// A newer turri on npm than the newest here.
|
|
205
|
+
const here = newest?.version !== undefined && identity.version !== undefined && newerThan(newest.version, identity.version) ? newest.version : identity.version;
|
|
206
|
+
if (!identity.clone && options.latest !== undefined && here !== undefined && newerThan(options.latest, here)) {
|
|
207
|
+
findings.push({ kind: "outdated", text: `turri ${options.latest} is out - this is turri ${identity.version}`, short: `This turri, ${identity.version}, is out of date: ${options.latest} is out. Upgrade: ${INSTALL}`, fixes: [`update: ${INSTALL}`] });
|
|
208
|
+
}
|
|
209
|
+
// The turri a bare `turri` runs is not this one, and older: it is run by its whole path, the old one in front.
|
|
210
|
+
if (runs !== undefined && !runs.self && !runs.clone && runs.version !== undefined && identity.version !== undefined && newerThan(identity.version, runs.version) && findings.every((finding) => finding.kind !== "shadowed")) {
|
|
211
|
+
findings.push({
|
|
212
|
+
kind: "behind",
|
|
213
|
+
text: `a bare \`turri\` runs ${runs.path}, turri ${runs.version} - older than this turri ${identity.version}`,
|
|
214
|
+
short: `a bare turri runs an older turri, ${runs.version}, from ${runs.path} - turri doctor shows how to fix it`,
|
|
215
|
+
fixes: [`take it off the PATH: ${removalOf(runs)}`, process.platform === "win32" ? "then open a new terminal" : "then `hash -r`, or a new terminal"],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return findings;
|
|
219
|
+
}
|
|
220
|
+
/** `turri --version`: the version line, then where this turri is, what npm has - asked now - and every turri on the PATH. */
|
|
221
|
+
export async function describeVersion(versionLine) {
|
|
222
|
+
const own = turriIdentity();
|
|
223
|
+
const onPath = turrisOnPath();
|
|
224
|
+
const latest = own.git_commit === null ? await fetchLatest() : undefined;
|
|
225
|
+
const lines = [versionLine, ` this turri: ${PACKAGE_ROOT}`];
|
|
226
|
+
if (own.git_commit === null) {
|
|
227
|
+
lines.push(latest === undefined
|
|
228
|
+
? " newest on npm: not known - npm did not answer"
|
|
229
|
+
: ` newest on npm: ${latest}${own.version !== null && !newerThan(latest, own.version) ? " - this is it" : ""}`);
|
|
230
|
+
}
|
|
231
|
+
if (onPath.length > 0) {
|
|
232
|
+
lines.push(" turri on PATH:");
|
|
233
|
+
for (const turri of onPath) {
|
|
234
|
+
const marks = [turri.runs ? "runs" : "hidden", ...(turri.self ? ["this one"] : []), ...(turri.clone ? ["a clone"] : [])].join(", ");
|
|
235
|
+
lines.push(` ${turri.path} turri ${turri.version ?? "?"} (${marks})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
lines.push(" turri on PATH: none - this one was run by its path");
|
|
240
|
+
}
|
|
241
|
+
const findings = versionFindings({ ...(latest !== undefined ? { latest } : {}), onPath, self: { ...(own.version !== null ? { version: own.version } : {}), clone: own.git_commit !== null, root: PACKAGE_ROOT } });
|
|
242
|
+
for (const finding of findings)
|
|
243
|
+
lines.push("", `! ${finding.text}`, ...finding.fixes.map((fix) => ` → ${fix}`));
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* What the banner says as turri starts: a line a finding, short - what is newer and what to run - from what npm said at
|
|
248
|
+
* the last check and the turris on the PATH, never waiting on the network; the check for next time started behind it.
|
|
249
|
+
*/
|
|
250
|
+
export function bannerWarnings() {
|
|
251
|
+
if (checkOff())
|
|
252
|
+
return { lines: [], later: Promise.resolve(undefined) };
|
|
253
|
+
let findings;
|
|
254
|
+
try {
|
|
255
|
+
const latest = latestKnown()?.version;
|
|
256
|
+
findings = versionFindings(latest !== undefined ? { latest } : {});
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
findings = [];
|
|
260
|
+
}
|
|
261
|
+
const lines = findings.map((finding) => finding.short);
|
|
262
|
+
// What npm says now, once it answers: a turri newer than the banner knew of is said then, in the same chat.
|
|
263
|
+
const later = refreshLatestLater().then((latest) => {
|
|
264
|
+
if (latest === undefined)
|
|
265
|
+
return undefined;
|
|
266
|
+
try {
|
|
267
|
+
const outdated = versionFindings({ latest }).find((finding) => finding.kind === "outdated");
|
|
268
|
+
return outdated !== undefined && !lines.includes(outdated.short) ? outdated.short : undefined;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return { lines, later };
|
|
275
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -20,6 +20,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
20
20
|
| what checks each pulse as it finishes, and tells me what needs me | [the sentinel](sentinel.md) |
|
|
21
21
|
| why the chat wrote scenarios I did not ask for, and how to turn that off | [the test advisor](test-advisor.md) |
|
|
22
22
|
| which model codes, and how to change it | [using](using.md#choosing-a-model), [the project config](config.md#models) |
|
|
23
|
+
| how to run turri on my Claude Pro, Max or Team subscription instead of an API key | [a Claude subscription](claude-subscription.md) |
|
|
23
24
|
| what `config.turri.yaml` can set, and what happens without it | [the project config](config.md) |
|
|
24
25
|
| how a backend and a frontend on different stacks share one project | [the project config](config.md#stacks) |
|
|
25
26
|
| which ready-made stacks \*\*\*plain can import - a FastAPI backend, a React frontend | [the standard library](authoring-plain.md#the-standard-library) |
|
|
@@ -46,6 +47,7 @@ turri is the first coding agent with the power of regenerative software: [\*\*\*
|
|
|
46
47
|
| [Chats](chats.md) | several chats in one session: what each keeps and what they share, `/chat` and its keys, `turri --continue` and `.turri/chats.json` |
|
|
47
48
|
| [The sentinel](sentinel.md) | the chat every finished pulse goes to: what it checks and tests, its notices and how to answer them, what it may do, closing it, its model, in a host |
|
|
48
49
|
| [The test advisor](test-advisor.md) | turri's background check of new and changed functional specs: when it runs, how it decides, what you see, its records, model and setting |
|
|
50
|
+
| [A Claude subscription](claude-subscription.md) | running every agent on a Claude Pro, Max or Team subscription through Claude Code: setting it up, its models, checking it yourself, in turri-desktop, its limits, when it does not work |
|
|
49
51
|
| [The project config](config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: where they are read from, every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
|
|
50
52
|
| [Deploy](deploy.md) | trying what is built: dist/<module>, the last commit, starting it on your machine, `turri release` |
|
|
51
53
|
| [Running unattended](headless.md) | regenerating overnight or in CI: before you leave it, the command, the results, resuming, rebuilding one functionality |
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# A Claude subscription
|
|
2
|
+
|
|
3
|
+
turri can run on your Claude Pro, Max or Team subscription instead of an API key. Every agent - the chat, the planner, the coding agents, the reviewers, the sentinel - then counts against the subscription's quota, as Claude Code does, and you set no key.
|
|
4
|
+
|
|
5
|
+
It goes through [pi-claude-code](https://github.com/earendil-works/pi-claude-code), a [pi](https://pi.dev) extension from pi's authors. It adds a provider, `claude-code`, whose every model request is sent through Claude Code's own CLI (`claude -p`), signed in as Claude Code is. Only the model call goes there: pi and turri keep the conversation, the history, compaction and every tool - turri's agents run their tools as they always do, in turri's [sandbox](sandbox.md). Claude Code runs no tools of its own.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
turri's agent ─ pi ─ claude-code provider ─ claude -p ─ your Claude subscription
|
|
9
|
+
▲ │
|
|
10
|
+
└──────── tool calls run by turri, as with any model ──────┘
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
turri does not depend on the extension: it finds it as pi does, once you have installed it, wherever it needs a model - the chat, a host such as turri-desktop, `turri regenerate` and every command run on its own, `turri doctor`, `turri init-config` ([models from a pi extension](using.md#models-from-a-pi-extension)).
|
|
14
|
+
|
|
15
|
+
## Set it up
|
|
16
|
+
|
|
17
|
+
1. **Claude Code**, installed and signed in with the Claude account that has the subscription:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @anthropic-ai/claude-code
|
|
21
|
+
claude auth login
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. **The extension**, installed into pi - turri runs on pi, and reads what pi has installed:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi install https://github.com/earendil-works/pi-claude-code
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`pi list` shows it; `pi update` updates it; `pi remove https://github.com/earendil-works/pi-claude-code` takes it out.
|
|
31
|
+
|
|
32
|
+
3. **Nothing in the environment that points Claude Code elsewhere.** The extension refuses to run while any of these is set - each sends Claude Code to an API key or another backend, not the subscription: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_FOUNDRY_API_KEY`, `CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`. Unset them where turri starts - your shell's profile, say. A project that also uses `anthropic/...` models with a key cannot run both from one shell.
|
|
33
|
+
|
|
34
|
+
4. **The project's models**, in its `config.turri.yaml`. A new project:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
turri init-config --vendor claude-code
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
writes
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
harness-model: claude-code/claude-opus-5-5:medium # you talk to it: it writes the specs and plans the work
|
|
44
|
+
coding-model: claude-code/claude-sonnet-5:low # it writes the code
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a project that has a `config.turri.yaml` already, it sets those two and leaves everything else as it was; or change them yourself, or with `/turri-config harness-model claude-code/claude-opus-5-5:medium` in the chat.
|
|
48
|
+
|
|
49
|
+
Two settings of the extension's own, read from the environment: `PI_CLAUDE_CODE_COMMAND`, the Claude Code to run when it is not `claude` on the PATH, and `PI_CLAUDE_CODE_CONFIG_DIR`, a Claude Code configuration folder of its own - another account's sign-in, say.
|
|
50
|
+
|
|
51
|
+
## Its models
|
|
52
|
+
|
|
53
|
+
The models are pi's Anthropic ones, under `claude-code/`, as far as your subscription has them:
|
|
54
|
+
|
|
55
|
+
| model | what it is |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `claude-code/claude-opus-5-5` | Opus 5.5 - to talk to, plan and judge |
|
|
58
|
+
| `claude-code/claude-sonnet-5` | Sonnet 5 - to write the code |
|
|
59
|
+
| `claude-code/claude-fable-5-1` | Fable 5.1 - the most capable, for the hardest work |
|
|
60
|
+
| `claude-code/claude-haiku-4-5-20251001` | Haiku 4.5 - the fastest |
|
|
61
|
+
| `claude-code/default` | whatever Claude Code itself recommends now |
|
|
62
|
+
|
|
63
|
+
Each takes a thinking level, as any model in `config.turri.yaml` does - `claude-code/claude-sonnet-5:low` - which goes to Claude Code as its effort. Give Opus 5.5 and Sonnet 5 one of `low`, `medium`, `high`, `xhigh` or `max`: through Claude Code they refuse a request with thinking turned off, so `:off` and `:minimal` fail on them. A role under `models:` takes them as any model - `models: { judge: claude-code/claude-fable-5-1:high }` - and they mix with other providers' in one config.
|
|
64
|
+
|
|
65
|
+
## Check it yourself
|
|
66
|
+
|
|
67
|
+
Each step says what you should see; the first one that does not is where it stops.
|
|
68
|
+
|
|
69
|
+
1. **Claude Code is signed in to the subscription.**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
claude auth status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`"loggedIn": true`, and a `"subscriptionType"` - `pro`, `max` or `team`. None, and Claude Code is signed in with a Console API key instead: `claude auth logout`, then `claude auth login` with the subscription's account.
|
|
76
|
+
|
|
77
|
+
2. **pi has the extension.**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pi list
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
lists `https://github.com/earendil-works/pi-claude-code`.
|
|
84
|
+
|
|
85
|
+
3. **turri finds it, and the models answer** - in the project's folder:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
turri doctor
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
✓ Model provider ..., claude-code (… models)
|
|
93
|
+
✓ Claude subscription pi-claude-code installed, Claude Code signed in to max - for claude-code/claude-opus-5-5, claude-code/claude-sonnet-5
|
|
94
|
+
✓ harness-model claude-code/claude-opus-5-5 answered (2.6s)
|
|
95
|
+
✓ coding-model claude-code/claude-sonnet-5 answered (2.2s)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`Claude subscription` is there whenever the config names a `claude-code/...` model, and checks what the extension needs, in the order it needs it: the extension installed, Claude Code on the PATH, signed in, to a subscription, and none of the environment above set. Each model line is one short request, thinking as the config says - a few tokens of your quota.
|
|
99
|
+
|
|
100
|
+
4. **An agent works on it** - start turri in the project and ask for something small, or regenerate a module on its own:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
turri regenerate app.plain
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The pulse's coding agent runs on `claude-code/claude-sonnet-5`, and the pulse lands as on any model. To watch it work, run it in [debug mode](config.md#debug-mode) (`--debug`): the pulse's log in `.turri/jobs/` names each agent's model and shows the tools it called. In the chat, `/engine` says which model each kind of work runs on.
|
|
107
|
+
|
|
108
|
+
## In turri-desktop
|
|
109
|
+
|
|
110
|
+
The desktop app runs turri's chats itself, and finds the extension as the terminal does: install it and sign in to Claude Code as above - in a terminal, once - and choose **Claude subscription** among the vendors of a new project. The app's engine runs with your login shell's PATH, so it finds the `claude` your terminal finds. The sign-in sheet shows `claude-code` among the providers, connected once Claude Code is signed in; its sign-in is Claude Code's own - `claude auth login` in a terminal - not the app's, and its button says so. Doctor's sheet has the `Claude subscription` line of step 3. A host's own part in it is in [running turri in a host](hosting.md#models-from-a-pi-extension-in-a-host).
|
|
111
|
+
|
|
112
|
+
## What to know
|
|
113
|
+
|
|
114
|
+
- **The quota is the subscription's**, with its rate limits, as any Claude Code use - and turri runs several agents at once: a pulse's coding agent, its reviewers, the sentinel, up to `parallel-jobs` pulses side by side. Lower `parallel-jobs` if you hit the limits.
|
|
115
|
+
- **It is experimental**, and its future is uncertain: Anthropic announced, then put on hold, moving `claude -p` use to a separate credit billed at API rates. The extension's [README](https://github.com/earendil-works/pi-claude-code) says what is known.
|
|
116
|
+
- **Each request starts Claude Code**: a little slower than an API key, more so after a tool call, when a fresh Claude Code process takes the conversation on.
|
|
117
|
+
- **Complex tool schemas may be simplified** on the way through Claude Code - nullable types, `oneOf`, `allOf`, `anyOf` - as the extension says. An agent whose tool calls come out wrong on `claude-code` but not on another provider is that.
|
|
118
|
+
- **Going back in a conversation** - a branch, an earlier message edited - starts Claude Code afresh with the conversation replayed: slower, and more of the quota.
|
|
119
|
+
|
|
120
|
+
## When it does not work
|
|
121
|
+
|
|
122
|
+
| what you see | what to do |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `claude-code/... need pi-claude-code, pi's extension for a Claude subscription, which is not installed` | `pi install https://github.com/earendil-works/pi-claude-code` |
|
|
125
|
+
| `claude-code - Claude subscription (Claude Code) - is not there` | the same: the extension is not installed, so pi has no `claude-code` provider |
|
|
126
|
+
| `Claude Code (claude) is not on the PATH` | `npm install -g @anthropic-ai/claude-code`, or name it in `PI_CLAUDE_CODE_COMMAND` |
|
|
127
|
+
| `Claude Code (...) is not signed in` | `claude auth login` |
|
|
128
|
+
| `Claude Code is signed in (console), but not to a Claude subscription` | `claude auth logout`, then `claude auth login` with the subscription's account |
|
|
129
|
+
| `ANTHROPIC_API_KEY ... is set: pi-claude-code refuses to run with it` - or, from a model call, `refuses conflicting auth/backend overrides` | unset it where turri starts |
|
|
130
|
+
| `"thinking.type.disabled" is not supported for this model` | give the model a thinking level - `:low` or more - in `config.turri.yaml` |
|
|
131
|
+
| a 429, `rate limit`, `usage limit` | the subscription's limits: wait, or lower `parallel-jobs` |
|
package/docs/config.md
CHANGED
|
@@ -11,6 +11,7 @@ A project says how its \*\*\*plain is built in `config.turri.yaml`, at the proje
|
|
|
11
11
|
| `google` | `google/gemini-3.1-pro-preview` | `google/gemini-3.1-flash-lite` | `GEMINI_API_KEY` |
|
|
12
12
|
| `amazon-bedrock`, Anthropic | `amazon-bedrock/<region>.anthropic.claude-opus-5-5` | `amazon-bedrock/<region>.anthropic.claude-haiku-5-5` | `AWS_PROFILE` or `AWS_BEARER_TOKEN_BEDROCK` |
|
|
13
13
|
| `amazon-bedrock`, OpenAI | `amazon-bedrock/<region>.openai.gpt-6-astra` | `amazon-bedrock/<region>.openai.gpt-6-luna` | `AWS_PROFILE` or `AWS_BEARER_TOKEN_BEDROCK` |
|
|
14
|
+
| `claude-code`, a Claude subscription | `claude-code/claude-opus-5-5` | `claude-code/claude-sonnet-5` | none: Claude Code's own sign-in (below) |
|
|
14
15
|
| `other` | `anthropic/claude-opus-5-5` | `google/gemini-3.1-flash-lite` | `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` |
|
|
15
16
|
|
|
16
17
|
Amazon Bedrock asks three more things, from [pi](https://pi.dev)'s catalogue: whose models (`--maker`) - any maker [pi](https://pi.dev) has there, Anthropic, OpenAI, xAI, Meta, Mistral, Qwen, DeepSeek, Amazon, Moonshot, Z.ai, ... - the geography of the inference profile they run in (`--region`) - `global`, `us`, `eu`, `jp`, `au`, `apac`, `in`, `ca` or `us-gov`, as the maker's models are served, or `in-region`, a model served in the AWS region it is called in - and which two. The pair offered is turri's own where it has one - the table's, for Anthropic's and OpenAI's in the geographies they are served in - and otherwise the catalogue's: the strongest model that reasons to talk to, by price, and the cheapest costing at least a fifth of it to code, ties to the newest. Enter takes it; `c` chooses each from the maker's models there. The region is turri's to set, not the person's: [pi](https://pi.dev) calls Bedrock in `AWS_REGION`, else `AWS_DEFAULT_REGION`, else the `AWS_PROFILE`'s region, else `us-east-1` - so every geography but `global`, `us` and `in-region` needs a region of its own (`eu-central-1` for `eu`, `ap-northeast-1` for `jp`, `ap-southeast-2` for `au`, `ap-southeast-1` for `apac`, `ap-south-1` for `in`, `ca-central-1` for `ca`, `us-gov-west-1` for `us-gov`), and turri sets `AWS_REGION` to it in its own process before any model is called (`src/bedrock-region.ts`) - the chat, every command that calls a model, a host's session. `AWS_REGION` or `AWS_DEFAULT_REGION`, set, is always used instead, and so is the profile's region when it is in the geography. Models of two geographies that need two regions get none: one `AWS_REGION` serves every model, and `turri doctor` says which model needs which. `init-config` says which region turri will use. A `config.turri.yaml` already there keeps everything but its two models, which it sets; `--force` writes the whole file again.
|
|
@@ -25,6 +26,10 @@ Below them it writes every other setting out, each with what it does and the val
|
|
|
25
26
|
|
|
26
27
|
Beside a `config.yaml` - another tool's, say - the new file takes from it the keys turri reads too, each in its place, its testing scripts above all, and says which it left (see [the old config.yaml](#the-old-configyaml)); `config.yaml` is left as it is. A `config.turri.yaml` already there keeps everything but its two models, which it sets - `--force` writes the whole file again - and a `config.yaml` turri read until then is copied to it first. It takes a folder other than the current one as its argument.
|
|
27
28
|
|
|
29
|
+
### A Claude subscription
|
|
30
|
+
|
|
31
|
+
`--vendor claude-code` runs every agent on your Claude Pro, Max or Team subscription instead of an API key, through Claude Code and [pi-claude-code](https://github.com/earendil-works/pi-claude-code), a [pi](https://pi.dev) extension: `claude-code/claude-opus-5-5` to talk to, `claude-code/claude-sonnet-5` to code. It needs the extension installed (`pi install https://github.com/earendil-works/pi-claude-code`) and Claude Code signed in (`claude auth login`), and none of `ANTHROPIC_API_KEY` and the like set; `turri doctor` checks all of it. Setting it up, its models, checking it yourself and what to know: [a Claude subscription](claude-subscription.md).
|
|
32
|
+
|
|
28
33
|
## Where it is read from
|
|
29
34
|
|
|
30
35
|
\*\*\*plain belongs to its **project**: the nearest folder, from its own folder up, that holds a `config.turri.yaml`, a stack's `config.turri.<stack>.yaml`, the old `config.yaml` or a `plain_modules/`. Without any of them, the \*\*\*plain description's own folder is the project. That folder's config is the one read, and its `plain_modules/` is where the code is built, wherever turri was started. So a project that keeps its \*\*\*plain in `plain/`, with `plain/config.turri.yaml`, builds `plain/app.plain` by `plain/config.turri.yaml` even when `turri` runs in the folder above.
|
package/docs/deploy.md
CHANGED
|
@@ -23,8 +23,8 @@ With `sentinel: false`, the chat reacts to every pulse that finishes as it finis
|
|
|
23
23
|
|
|
24
24
|
It runs on your machine, outside the [sandbox](sandbox.md) - with your toolchains, where your browser can reach it. The chat reads how the code runs (its README, manifests and entry points), then:
|
|
25
25
|
|
|
26
|
-
1. **Checks the toolchains** it needs (`node`, `python3`, `go`, ...). One is missing: it
|
|
27
|
-
2. **Installs what it needs
|
|
26
|
+
1. **Checks the toolchains** it needs (`node`, `python3`, `go`, ...) - under another name too: `python` where `python3` is not, of the version it needs. One is missing: it stops, and tells you the exact command to install it yourself (`brew install node`). turri installs nothing on your machine and changes none of its settings - not the release, not the chat, not any agent: its shell refuses it.
|
|
27
|
+
2. **Installs what it needs into the project.** Dependencies into a virtual environment or `node_modules`, a build that downloads packages, an image pull, a guided release's project scripts: it runs them in its own shell, as any agent with a shell would, and fixes what fails. Nothing is put to you - there is no dialog to answer. Never a global install, never a system setting.
|
|
28
28
|
3. **Starts what serves** with `activate`: in the background, in its own process group, its output in `.turri/deploy/<module>-<name>.log`. It waits until something answers at the server's URL - up to a minute - so the URL you are given has answered once; one that exits first comes back with the end of its output. A port something else holds is said, with what holds it, and nothing is started there: the app is not moved to a port you were not told about. It runs in the deployed folder, or a folder inside it the model names relative to it (`frontend`); a folder that is not there starts nothing, and the model is told so with what the deployed folder holds, to name the right one.
|
|
29
29
|
|
|
30
30
|
What it started is listed by the `pulses` tool and by **`/halt`**, and stopped by **`/halt activated <name>`**, by `halt`, by the next deploy of the module, and when the chat exits.
|
package/docs/hosting.md
CHANGED
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
| export | what it is |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| `createTurri(options)` | a turri session: its [chats](chats.md), each a [pi](https://pi.dev) session with every tool, slash command, rule and skill the terminal has, on one job registry and work loop. The host opens, closes and shows them, is told of their jobs and asks their questions - see [the session in a host](#the-session-in-a-host). |
|
|
11
|
+
| `createTurri(options)` | a turri session: its [chats](chats.md), each a [pi](https://pi.dev) session with every tool, slash command, rule and skill the terminal has, on one job registry and work loop. The host opens, closes and shows them, is told of their jobs and asks their questions, and asks the sentinel to check functional specs (`check`) - see [the session in a host](#the-session-in-a-host). |
|
|
12
|
+
| `SENTINEL`, `SENTINEL_START`, `Notice`, `NoticeAnswer`, `Conclusion`, `SentinelCheck`, `CheckRequest` | the [sentinel](sentinel.md)'s chat role, the answers to the question before it starts, its notices and how they are answered, its conclusions - a pulse's by `job`, a check's by `check` - and the checks a host asks of it - see [the sentinel](#the-sentinel). |
|
|
12
13
|
| `MAX_CHATS`, `ChatInfo`, `ChatStatus` | the most chats a session has open - nine - and what the host is told of each. |
|
|
13
14
|
| `pi` | [pi](https://pi.dev) itself, as turri is built against it. Make the host's own extensions and UI with this one, not another copy. |
|
|
14
15
|
| `readProjectConfig`, `projectModels`, `modelSpec`, `configForSpec`, `hasProjectConfig`, `stacksOf` | the project's settings, as [config.turri.yaml](config.md) has them. |
|
|
15
16
|
| `configSettings`, `currentValue`, `readConfigText`, `overriddenBy`, `EFFECT_TEXT` | every setting of config.turri.yaml as [/turri-config](config.md#changing-it-from-the-chat) knows it - its key, what it does, its kind (a yes or no, one of a few words, a number, a model, a list, a path, a budget), its words or the values offered first, what it is unset and when a change takes effect - and what the file has now (`currentValue(readConfigText(dir), key)`), and what wins over it. Change one as the person would, by sending `/turri-config <key> <value>` to the chat: turri checks it and writes it with the file's comments kept. A setting's values for a list are the session's `settingChoices(key, typed?)`: what /turri-config's completions offer after `typed`, in their order, but whole - uncapped, where the terminal shows forty - and without unset, which the host draws itself; the values alone. A model setting's are every model of every provider signed in to, from the model registry the chats use, whether or not one has run /turri-config - and after a colon, that model's thinking levels. An unknown key gives `[]`. |
|
|
16
17
|
| `debugMode` | whether turri keeps, in a project, what it writes only for debugging it - `debug:`, or `TURRI_DEBUG` over it (see [debug mode](config.md#debug-mode)). |
|
|
17
18
|
| `initConfig(dir, { vendor, bedrock, models, force, catalogue })`, `VENDORS`, `BEDROCK_REGIONS`, `BEDROCK_DEFAULT` | a project's first config.turri.yaml, as `turri init-config` writes it - async, it may read [pi](https://pi.dev)'s catalogue - see [a project's first config](#a-projects-first-config). |
|
|
19
|
+
| `machineChange(command)`, `machineChangeReason(why)` | whether a command line would install something on the machine or change a setting of the machine's or the person's shell - and why - and what turri says instead: for a host's own tool that types into a shell, which turri cannot see a command in. |
|
|
20
|
+
| `installedModelRuntime(cwd?)`, `EXTENSION_PROVIDERS` | pi's model runtime with the providers the person's installed pi extensions register - for a host's sign-in sheet before a chat is open - and the providers turri knows come from an extension - see [models from a pi extension in a host](#models-from-a-pi-extension-in-a-host). |
|
|
18
21
|
| `piModelCatalogue`, `bedrockMakers`, `bedrockModelsIn`, `bedrockDefaultModels`, `bedrockModels`, `bedrockPlaceOf`, `makerLabel`, `BEDROCK_GEOGRAPHIES`, `BEDROCK_IN_REGION` | [pi](https://pi.dev)'s model catalogue, read without a request, and Bedrock in it: whose models it serves, in which geographies, which two turri offers for a maker and geography. |
|
|
19
22
|
| `bedrockAwsRegion`, `bedrockNeedsAwsRegion` | the AWS region turri calls a Bedrock geography's models in, and whether that geography needs a region of its own - every one but `global`, `us` and `in-region`, which [pi](https://pi.dev)'s default `us-east-1` serves. |
|
|
20
23
|
| `bedrockRegionFor(projectDir)`, `BedrockRegion`, `BedrockRegionSource` | the AWS region the project's Bedrock models are called in, read without setting anything: `{ region?, source, models, needs?, conflict? }` - `source` `set` (the person's `AWS_REGION` or `AWS_DEFAULT_REGION`, never touched), `turri` (turri sets `AWS_REGION` as the session starts, the models needing it and none set), `profile` (the `AWS_PROFILE`'s region, in the models' geography) or `default` (nothing names one: `us-east-1`); `models` the Bedrock models the config names, each stack's too; `needs` the region and geography they need, when one; `conflict`, the models needing two regions, each with its models - then no region is set, one `AWS_REGION` serving every model. `region` is undefined with no Bedrock model, or with a conflict and nothing set. Undefined on an older turri. |
|
|
@@ -24,6 +27,8 @@
|
|
|
24
27
|
| `testDirOf(plainFile)`, `DEFAULT_TEST_DIR` | the absolute folder a module's test plain files are in - `test-dir`, its stack's over the project's, else `tests` (`DEFAULT_TEST_DIR`), from the project's folder - for a host that lists a project's modules by walking its .plain files and leaves the test plain files out. It is a module's, not a project's, because a stack's `config.turri.<stack>.yaml` may name a folder of its own. |
|
|
25
28
|
| `conceptsOf(plainFile)`, `findConcepts(text)`, `conceptBeingTyped(text)` | the concepts a module can name, with what each is and the module that defines it; the concepts written in a piece of text; the one being typed at its end. |
|
|
26
29
|
| `stdlibTemplates()`, `isStack`, `STDLIB_KINDS` | turri's standard library: every template with its kind, file, text, imports and its conformance template or the stack it tests, and the kinds in order with their titles. |
|
|
30
|
+
| `projectStanding(projectDir, { loop?, files? })`, `ModuleStanding` | how every module of a project stands, in one call - see [how a project stands](#how-a-project-stands). |
|
|
31
|
+
| `requiresOrder(projectDir, { files? })` | a project's .plain files, absolute, in the order they are built: each after every module it requires, as its requires chain (`resolveRequiresChain`) has them - the files taken in the order given, by name without `files`, each brought in after what it requires. A module whose chain does not resolve - it does not parse, a module it requires is not there, a cycle - goes after what its front matter says it requires, as far as those are found; none is left out. Only the files given are in it: `files`, absolute or from the project, else every .plain file in the project's folder. |
|
|
27
32
|
| `functionalityCompletion`, `createWorkLoop`, `createJobRegistry`, `promptOf`, `describeModules` | how far each functional spec is built - each module's `specified`, `withScenarios` (those tested, which tested-of-specified counts over), `completed`, `conformanceTested`, `tested` and `incomplete` - and - on a loop of the host's own with `dryRun` - the work a \*\*\*plain change would plan, as `turri glimpse` does. |
|
|
28
33
|
| `onModelEvent`, `describeTurnIncident` | a provider failing and waited out, a turn turri ended itself. |
|
|
29
34
|
| `onLedgerEvent`, `readLedger`, `ledgerDir`, `runViews`, `pulseView`, `describePulseView`, `describeRuns`, `ledgerProjects`, `earlierPulses`, `asJob` | what turri did, as facts - the [ledger](using.md#the-ledger): each event heard as it is recorded, and every run and pulse read back, earlier sessions' too - see [the ledger in a host](#the-ledger-in-a-host). |
|
|
@@ -46,13 +51,35 @@ const { written, message } = await initConfig(project, { models: { harness, codi
|
|
|
46
51
|
|
|
47
52
|
`initConfig` writes config.turri.yaml as `turri init-config` does, and returns what it did - `written`, and `message`, the lines the terminal prints. It is async.
|
|
48
53
|
|
|
49
|
-
- `vendor` - `anthropic`, `openai`, `google`, `amazon-bedrock` or `other` - fills in that vendor's own two models (`VENDORS` has each one's label and models).
|
|
54
|
+
- `vendor` - `anthropic`, `openai`, `google`, `amazon-bedrock`, `claude-code` (a Claude subscription, through the [pi-claude-code](https://github.com/earendil-works/pi-claude-code) extension) or `other` - fills in that vendor's own two models (`VENDORS` has each one's label and models).
|
|
50
55
|
- `bedrock: { maker?, region }`, on Amazon Bedrock, says whose and where. turri's own pair is written where it has one - Anthropic's and OpenAI's in `BEDROCK_REGIONS` - and otherwise the pair `bedrockDefaultModels` offers from [pi](https://pi.dev)'s catalogue: any maker and geography it has.
|
|
51
56
|
- `models: { harness, coding, strong? }` writes those two instead - any pair [pi](https://pi.dev)'s catalogue has as chat models, full model ids without a thinking level (`amazon-bedrock/global.xai.grok-4.7`). They go wherever the vendor's own would: the two models with the thinking turri writes, the commented-out `judge-model` and `models:` examples, `strong` - the judging examples' model - being `harness` unless given. The vendor, the Bedrock maker and the geography are read from them, so neither `vendor` nor `bedrock` is needed. A model the catalogue does not have, or that is not a chat model, writes nothing: `written` is false and `message` says which, with the closest the catalogue has.
|
|
52
57
|
- `catalogue`, the one the host already read, saves reading it again.
|
|
53
58
|
|
|
54
59
|
The message says whose models were written - "with xAI's models on Amazon Bedrock (global)" - and how to sign in: an `AWS_PROFILE` or a Bedrock API key, never a region. For a geography that needs one, it says which turri calls it in - `turri calls eu's models in eu-central-1. AWS_REGION, set, overrides it - and so does your AWS profile's region, when it is in eu.` - `bedrockAwsRegion(geography)` being that region, and `bedrockNeedsAwsRegion(geography)` whether the geography needs one. turri sets it itself: `createTurri` - and `turri` in a terminal, and every command that calls a model - sets `AWS_REGION` for the project's Bedrock models before any is called, when they need a region and neither `AWS_REGION` nor `AWS_DEFAULT_REGION` is set, and works it out again when the config's models change through `/turri-config` and as each pulse takes its model. A host that shows the person where their models run reads `bedrockRegionFor(projectDir)` - "turri calls these models in eu-central-1" - rather than asking them to set anything.
|
|
55
60
|
|
|
61
|
+
## How a project stands
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { projectStanding } from "@turri-dev/cli/host";
|
|
65
|
+
|
|
66
|
+
for (const { plainFile, module, drift, driftError, completion, completionError } of projectStanding(projectDir, { loop: turri.loop })) {
|
|
67
|
+
// drift: loop.drift(plainFile)'s reports; completion: functionalityCompletion(plainFile)'s chains.
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`projectStanding` gives each module of the project what `drift` and `functionalityCompletion` give it asked alone - `drift`, every codebase of its requires chain with what is left to regenerate there, and `completion`, its chain's functional specs completed and conformance tested - in one call. Asked module by module, each call parses and reads the whole chain below the module again: on a project of a dozen modules in one chain, seconds. Asked of the project, each file is parsed once and each codebase's record read once, the modules below shared by every chain they are in.
|
|
72
|
+
|
|
73
|
+
- `loop` is the work loop whose `drift` is read - the session's `turri.loop`, which knows the pulses running and the codebases halted. Without one, a loop of its own that starts nothing reads the codebases as their records have them, as `createWorkLoop({ dryRun: true })` does.
|
|
74
|
+
- `files` are the .plain files to look at, absolute or from the project. Without them, every .plain file in the project's folder.
|
|
75
|
+
- A module drift or completion cannot say - one that does not parse, a module it requires not there - has `driftError` or `completionError`, the message, in place of the answer; the others are answered all the same.
|
|
76
|
+
|
|
77
|
+
What it reads is kept between calls while it holds: a parse while the files it read are as they were - their text, and the files it looked for still not there - a codebase's record by the commit its history is at, a config file by its size and modification time. Asked again with nothing changed, it costs a fraction of the first call; a change is seen at the next. `plainGraph`, `checkPlainFile`, `plainFileParser`, `drift` and `functionalityCompletion` keep the same.
|
|
78
|
+
|
|
79
|
+
## Models from a pi extension in a host
|
|
80
|
+
|
|
81
|
+
A provider a [pi](https://pi.dev) extension registers - `claude-code`, a Claude subscription through Claude Code ([a Claude subscription](claude-subscription.md)), or a company's own gateway - is in a host's chats as in the terminal's: each chat's resource loader loads the person's installed pi packages, and turri's agents take the providers from the chat. Without a chat, turri loads the installed extensions itself - `piModelCatalogue`, `doctor` and `initConfig` see their providers. What a host does with pi's model runtime itself before a chat is open - its sign-in sheet listing the providers and whether each is connected, say - takes `installedModelRuntime(cwd?)`: pi's runtime with the installed extensions' providers in it, loaded once a process. A `ModelRuntime.create()` of the host's own has none of them, and `claude-code` would be missing from the list until a chat is open. `EXTENSION_PROVIDERS` names the providers turri knows come from an extension - `claude-code` - each with its label, the command that installs its extension, and how it is signed in to: `claude-code`'s sign-in is Claude Code's own, `claude auth login` in a terminal, and pi's login for it refuses, saying so. The host's process needs the person's PATH, as for doctor below, to find `claude`.
|
|
82
|
+
|
|
56
83
|
## doctor in a host
|
|
57
84
|
|
|
58
85
|
```js
|
|
@@ -62,12 +89,14 @@ const checks = await doctor({ dir: project, env: engineEnv });
|
|
|
62
89
|
// [{ level: "ok" | "missing" | "limited" | "info", name, detail, more?, fix? }, ...]
|
|
63
90
|
```
|
|
64
91
|
|
|
65
|
-
`doctor` runs the checks `turri doctor` prints, and returns them: Node, bash, git, which OS sandbox is up under the agents' commands, and whether any model provider has credentials. With `dir`, the project too: whether this machine has the toolchains its modules' implementation reqs name,
|
|
92
|
+
`doctor` runs the checks `turri doctor` prints, and returns them: Node, bash, git, which OS sandbox is up under the agents' commands, and whether any model provider has credentials. With `dir`, the project too: whether this machine has the toolchains its modules' implementation reqs name, what in its testing scripts it does not have, and - when its config names a `claude-code/...` model - the `Claude subscription` check: pi-claude-code installed, Claude Code on the PATH, signed in to a subscription, and nothing set that the extension refuses. It prints nothing and asks nothing; each check's `fix` is plain text, to show as it is. A check `missing` means turri cannot run until it is fixed, `limited` that it runs with less.
|
|
66
93
|
|
|
67
94
|
- `dir` is the project to check - not the process's working folder, which in a host is not the project. Without it, only the machine is checked.
|
|
68
95
|
- `models: true` asks each model the project's config.turri.yaml names for a word, with this machine's credentials - a request a model, which costs money. It is off by default: run it when the person asks.
|
|
69
96
|
- `env` is the environment whose PATH is looked in - for bash, git, the sandbox, the toolchains and what the testing scripts run - by default the process's own. A Mac app opened from Finder runs with `/usr/bin:/bin:/usr/sbin:/sbin`, not the person's shell's PATH: pass the environment the host's engine runs turri with, so doctor checks the PATH turri will actually have.
|
|
70
97
|
|
|
98
|
+
It never holds the host's thread up: every command it runs - bash, git, the sandbox, `java_home`, a base folder's `git ls-files` - runs without blocking, side by side, and the project's modules are parsed one at a time, the thread given back between them.
|
|
99
|
+
|
|
71
100
|
What matters only in a terminal - `turri` on the PATH, a plain `pi` an older turri made load turri - is left to `turri doctor`.
|
|
72
101
|
|
|
73
102
|
## The session in a host
|
|
@@ -87,7 +116,8 @@ const turri = await createTurri({
|
|
|
87
116
|
restore: true, // reopen the last session's chats here, as `turri --continue` does
|
|
88
117
|
onNotice: (notice) => {}, // the sentinel puts something to the person: draw it in notice.chat
|
|
89
118
|
onNoticeOver: (notice) => {}, // ...answered, dismissed, ignored or superseded: take its card down
|
|
90
|
-
onConcluded: (conclusion) => {}, // the sentinel is done with a pulse: holds or
|
|
119
|
+
onConcluded: (conclusion) => {}, // the sentinel is done with a pulse - or a check: holds, wrong or unchecked, told to conclusion.chat
|
|
120
|
+
pulseEndsShown: true, // the host shows each pulse that lands and each conclusion itself: the chat is told without a turn
|
|
91
121
|
onChatSession: (chat, session) => {}, // a chat's session replaced, same id: the sentinel's, fresh for each pulse - follow this one
|
|
92
122
|
sentinel: {
|
|
93
123
|
tools: [browserTool], // the sentinel's own tools, beside turri's: what it tests a release with
|
|
@@ -103,6 +133,7 @@ await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, e
|
|
|
103
133
|
await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
|
|
104
134
|
turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown, role? }]
|
|
105
135
|
turri.notices(); // the notices open now
|
|
136
|
+
turri.check({ module: "app", funcIds: ["add-task"] }); // the sentinel to test these in app's release: { check: id } or { refused: why }
|
|
106
137
|
turri.settingChoices("harness-model"); // every value /turri-config offers for a setting, whole: every model signed in to
|
|
107
138
|
turri.settingChoices("harness-model", "openai/gpt-6-astra:"); // after a colon, that model's thinking levels
|
|
108
139
|
turri.turnOnConformanceTests("app.plain"); // conformance tests on: the file written - config.turri.yaml, or the stack's
|
|
@@ -139,8 +170,15 @@ The [sentinel](sentinel.md) is one of the host's chats: turri opens it with the
|
|
|
139
170
|
- **A conversation a pulse.** The sentinel checks each pulse from scratch, in a conversation of its own - one pulse at a time, the others waiting their turn. The chat keeps its id; its session is replaced. As it starts on a pulse, the session it had is stopped and let go (`abort`, `dispose`), a new one is made in its place, and `onChatSession(chat, session)` comes with it - before the pulse is put to it - then `onChats`; `session(id)` returns the new one from then on. A host that follows the sentinel's chat subscribes to that session in place of the old - an `onChats` listener that follows only the ids it has not seen would hear nothing more of it. The first pulse goes to the session the chat was opened with: there is no `onChatSession` for it. Each conversation's first message is the hidden brief of its pulse - `customType: "turri_sentinel_pulse"`, `details.jobId` - so a host that splits the sentinel's work by pulse on it still can; the conversations before are not in the new session, and what the host showed of them is its own to keep.
|
|
140
171
|
|
|
141
172
|
- **Notices.** What it finds wrong comes as `onNotice(notice)`: `{ id, chat, severity: "error" | "warning" | "info", summary, evidence: string[], actions: [{ label, task }], module?, pulse?, job?, ignorable?, state: "open" }`. Draw it in the chat `chat` names - the one `focus` last named, or, while that is the sentinel, the chat that asked for the pulse. It is drawn there as a card in the transcript too. A host given `onNotice` answers it itself - the terminal's dialog is not opened - with `answer(id, …)`: `{ action: index | label }` starts that chat's agent on the action's task, `{ steer: text }` on the person's words, `{ dismiss: true }` on nothing, this once. A notice with `ignorable: true` - a gap in the \*\*\*plain the build settled with an assumption, which the person may leave as it is - takes `{ ignore: true }` too: offer it as its own button beside the actions. Nothing is done in the chat, the notice ends `state: "ignored"` (`"ignored"`), and turri writes it to the sentinel's guidance, so it is not raised again in this session or the next; on a notice without `ignorable`, `{ ignore: true }` is `"refused"` and the notice stays open. `onNoticeOver(notice)` comes as it is answered - and when a newer notice about its module supersedes it (`state: "superseded"`) - for the host to take the card down. `notices()` lists the open ones.
|
|
173
|
+
- **Fixes it made.** The sentinel fixes the project's testing scripts and their config itself ([what it may do](sentinel.md#what-it-may-do)), and says so as a notice with `kind: "fixed"`, `severity: "info"`, no actions, the lines before and after in `evidence`: a note, with nothing to answer. Draw it as one - its card with a way to put it away (`answer(id, { dismiss: true })`), no "say what to do instead". It never supersedes a notice about its module. The terminal draws its card and puts no dialog.
|
|
174
|
+
- **The machine is the person's.** turri installs nothing and changes no setting of the machine's or the person's shell, in any chat, the sentinel's included - and through a host's tools as through its own: a tool call whose input has a `command` string - a host's `terminal_run` - is refused when the command would (`machineChange`), with what to do instead (`machineChangeReason`), as turri's own `bash` is; and the sentinel's, also when it writes a `.plain` file. A tool that types into what already runs - a terminal's `terminal_type` at a shell prompt - is the host's to check: `machineChange(text)` says whether a line would change the machine, and why.
|
|
142
175
|
- **Starting it.** Under `sentinel: ask` - as it is unset - the person is asked before it first opens in the session: a notice with `kind: "sentinel-start"`, `severity: "info"`, in the chat `chat` names, its `summary` the question in one sentence, no `evidence`, and its actions the answers - `Start the sentinel`, `Start it, and don't ask again`, `Not this session`, `Turn it off for this project`. Draw it as a question rather than as something wrong. `answer(id, { action })` answers it, by index or label - `"done"` - and `{ dismiss: true }` is not this session; turri writes `sentinel:` itself for the two that choose for the project. Until it is answered, the pulses that finish are held: neither the sentinel nor the chat hears of them. A host that offers the setting too writes it as `/turri-config` does - `ask`, `true` or `false` (see `configSettings`).
|
|
143
176
|
- **Conclusions.** As the sentinel is done with each pulse - all well or not - `onConcluded({ job, verdict: "holds" | "wrong", summary, module, pulse, chat })` comes, and the chat `chat` names - the one `focus` last named, as for a notice - shows it and its agent says so in a line.
|
|
177
|
+
- **A host that shows them itself.** With the sentinel on, the chat the person is in is told of each pulse that lands - to say it landed and what the sentinel does with it - and of each conclusion, each in a turn of its own: its agent says it in the conversation, as the terminal has it. A host that shows both itself - its own notifications, from `onJobChange` and `onConcluded` - passes `pulseEndsShown: true`, and the chat then says neither. Both still reach it - `customType: "turri_job_landed"` and `"turri_sentinel_concluded"`, hidden - put in its conversation in order and starting no turn: its agent has them in its next turn, and knows what landed and what the sentinel found when the person asks. Their words say what happened, and ask nothing of it. The rest is as it was: the sentinel's briefs and checks, its notices, a pulse that stopped on \*\*\*plain that contradicts itself (a notice of turri's own), `onConcluded` itself - and, with the sentinel off, the turn in the chat that asked for a pulse, which does what the project's guidance says once a pulse finishes.
|
|
178
|
+
- **Checks.** `check({ module, funcIds })` asks the sentinel to test functional specs of one module - the person picked them on a list of its built ones, say - in the background, on its own judgement, reported as a pulse is. `module` is the module whose release is checked, by name; `funcIds` the functional specs to test, each `module:func-id` - of that module, or of any module its requires chain builds on, whose code its release holds: `check({ module: "predators", funcIds: ["depths:gene-fragments"] })`. A bare func-id is the module's own. The check and its brief name each as `module:func-id`. It returns `{ check: id }` at once, or `{ refused: why }`, saying which: the sentinel off (`sentinel: false`), or not started this session; a module not in the project's folder, an import module, one that does not parse; a name outside its chain - `Not in predators' release: arena:waves - arena is not predators or a module its requires chain builds on (depths, ocean).`; a func-id its module does not have; one not built in the module's own codebase as its \*\*\*plain has it now - with where the module's code is built on instead, when it is. What is tested is the module's release - its own codebase's last commit, which holds the code of every module its requires chain builds on - never what a pulse is writing, and never another module's: turri does not choose one for it. Under `sentinel: ask`, a check asked before the sentinel was started puts the start question first, as a pulse does - `check` set on the notice - and waits for the answer.
|
|
179
|
+
- **Queued with the pulses.** It waits its turn behind the sentinel's work, and is taken in a conversation of its own: `onChatSession`, then the conversation's first message, the hidden brief of the check - `customType: "turri_sentinel_check"`, `details: { checkId, module, funcIds }`. A pulse of its module that lands meanwhile waits for it, rather than stopping it.
|
|
180
|
+
- **Released first.** When what runs of the module is its last commit - released since it landed, and nothing it started has ended - it is used as it is. Otherwise the module is released as release-after-pulse releases it - its release guidance followed, or its last commit put in `dist/<module>` and started - before the sentinel is told; the brief says what runs and where, as a pulse's does, and gives each functional spec's text and scenarios, as turri reads them, to test those and not the module's others.
|
|
181
|
+
- **Reported as a pulse is.** Its notices come as `onNotice`, with `check` - its id - and its module. It ends with `onConcluded({ check, verdict, summary, module, chat })` - `check` in place of `job`, which it has none of. A release that fails ends it there, the sentinel told nothing: `verdict: "wrong"`, the summary saying why. A check that never runs - the person did not start the sentinel when asked, it could not be opened, it was closed before it got to it - ends `verdict: "unchecked"`, which the sentinel itself never gives. The ledger has `sentinel.asked` as it is asked for, and `sentinel.checking` and `sentinel.concluded` with `check`.
|
|
144
182
|
- **Its model.** The `sentinel` role in `models:`, else the chat's model - `models.chat`, else `harness-model` - with its thinking, set as its chat opens: not pi's default model, which may not be one the project's provider has. What the person then picks in its chat stands. See [its model](sentinel.md#its-model).
|
|
145
183
|
- **Its tools.** `sentinel: { tools }` are [pi](https://pi.dev) tool definitions registered in the sentinel's chat alone and told to it in its prompt - the host's browser, say, for it to test a release with. The rest of the host's `extensions` are built into its chat too, but their tools are refused it: only its own read, and these.
|
|
146
184
|
- **Its surface.** `sentinel: { surface }` is where the person watches the sentinel use those tools, in the host's own words - the preview pane its browser and terminal show in, say. Given, the sentinel's prompt has it, as a `<surface>` block, in place of the person not watching it: it tries a release with the host's tools, where the person can watch, and keeps its own shell for what those cannot show - logs, exit codes, files. Without it, the person does not watch the sentinel, and the host's tools are only to test with as well. As for the conversation, `systemPrompt` has the last word on its prompt.
|