@turri-dev/cli 0.2.0 → 0.2.1-next.bc56a0d
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 +109 -22
- 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/README.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
npm install -g @turri-dev/cli # puts `turri` on your PATH
|
|
22
|
+
turri --version # the version you installed? if not, see docs/installation.md#an-older-turri-runs
|
|
22
23
|
turri doctor # checks the machine
|
|
23
24
|
```
|
|
24
25
|
|
|
@@ -30,7 +31,7 @@ Then start it in your project, signed in to your model providers:
|
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
33
|
cd ~/my-project
|
|
33
|
-
turri init-config # asks which vendor you use: Anthropic, OpenAI, Google, Amazon Bedrock, or other
|
|
34
|
+
turri init-config # asks which vendor you use: Anthropic, OpenAI, Google, Amazon Bedrock, a Claude subscription, or other
|
|
34
35
|
export ANTHROPIC_API_KEY=... # the key it names - or skip it, and type /login in turri once it has started
|
|
35
36
|
turri
|
|
36
37
|
```
|
|
@@ -42,7 +43,7 @@ harness-model: anthropic/claude-opus-5-5:medium # you talk to it
|
|
|
42
43
|
coding-model: anthropic/claude-haiku-5-5:low # it writes the code
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
To skip the question, name the vendor - `anthropic`, `openai`, `google`, `amazon-bedrock` (with `--maker` and `--region`) or `other`:
|
|
46
|
+
To skip the question, name the vendor - `anthropic`, `openai`, `google`, `amazon-bedrock` (with `--maker` and `--region`), `claude-code` - your Claude Pro or Max subscription, through Claude Code ([how](docs/claude-subscription.md)) - or `other`:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
turri init-config --vendor anthropic
|
package/dist/chats.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { AskUserIO } from "./ask.ts";
|
|
|
7
7
|
import type { Job, JobRegistry } from "./regenerate/jobs.ts";
|
|
8
8
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { AdvisorVerdict } from "./regenerate/advisor-runs.ts";
|
|
10
|
-
import { type ChatRole, type Conclusion, type Notice, type NoticeAnswer, type NoticeInput } from "./notice.ts";
|
|
10
|
+
import { type ChatRole, type Conclusion, type Notice, type NoticeAnswer, type NoticeInput, type SentinelCheck } from "./notice.ts";
|
|
11
11
|
/**
|
|
12
12
|
* The chats of a turri session.
|
|
13
13
|
*
|
|
@@ -73,6 +73,13 @@ export interface TurriHost {
|
|
|
73
73
|
onConcluded?(conclusion: Conclusion & {
|
|
74
74
|
chat: string;
|
|
75
75
|
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* The host shows the person each pulse that lands and each of the sentinel's conclusions itself - its own
|
|
78
|
+
* notifications, from onJobChange and onConcluded. With the sentinel on, the chat the person is in is then told of both
|
|
79
|
+
* without a turn of its own: its agent knows what landed and what the sentinel found when the person asks, and does
|
|
80
|
+
* not say it again in the conversation. Unset, it says both, as in the terminal.
|
|
81
|
+
*/
|
|
82
|
+
pulseEndsShown?: boolean;
|
|
76
83
|
/** What the sentinel puts to the person - and, `kind: "sentinel-start"`, whether to start it: the host draws it in the chat it names, and answers it with `answer`. */
|
|
77
84
|
onNotice?(notice: Notice): void;
|
|
78
85
|
/** A notice is over - answered, dismissed, or superseded by a newer one about its module: the host takes its card down. */
|
|
@@ -103,6 +110,14 @@ export interface ChatInstance {
|
|
|
103
110
|
asked?: string;
|
|
104
111
|
focused?: string;
|
|
105
112
|
}): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* The sentinel's: functional specs a host asked it to check - the module released first, unless what runs is its last
|
|
115
|
+
* commit, then its agent told, in a conversation of its own for the check. Why not, when the release failed: then its
|
|
116
|
+
* agent is told nothing.
|
|
117
|
+
*/
|
|
118
|
+
briefCheck(check: SentinelCheck, context: {
|
|
119
|
+
focused?: string;
|
|
120
|
+
}): Promise<string | undefined>;
|
|
106
121
|
/** A notice put in the chat: drawn, and - in the terminal - put to the person once the chat is idle. */
|
|
107
122
|
notice(notice: Notice): void;
|
|
108
123
|
/** A notice is over: its dialog closed, if it is open. */
|
|
@@ -219,8 +234,20 @@ export interface TurriCore {
|
|
|
219
234
|
sentinelOn(): boolean;
|
|
220
235
|
/** A notice put to the person, in the chat they look at - the sentinel's, or turri's own. Undefined: no chat to put it in. */
|
|
221
236
|
notice(input: NoticeInput): Notice | undefined;
|
|
222
|
-
/** The sentinel done with a pulse: told to the chat the person looks at. Undefined: no chat to tell. */
|
|
237
|
+
/** The sentinel done with a pulse or a check: told to the chat the person looks at. Undefined: no chat to tell. */
|
|
223
238
|
concluded(conclusion: Conclusion): string | undefined;
|
|
239
|
+
/**
|
|
240
|
+
* A host's check for the sentinel: refused with the sentinel off; else `prepare` finds its module and functional specs
|
|
241
|
+
* good (sentinel-check.ts), and it is queued with the pulses - under `sentinel: ask`, held while the person is asked
|
|
242
|
+
* whether to start it.
|
|
243
|
+
*/
|
|
244
|
+
check(prepare: () => SentinelCheck | {
|
|
245
|
+
refused: string;
|
|
246
|
+
}): {
|
|
247
|
+
check: string;
|
|
248
|
+
} | {
|
|
249
|
+
refused: string;
|
|
250
|
+
};
|
|
224
251
|
/** The test advisor's verdict, for the chat the person looks at - held until there is one. Undefined: no chat to hand it to yet. */
|
|
225
252
|
advised(verdict: AdvisorVerdict): string | undefined;
|
|
226
253
|
/** The notices put to the person and not yet answered. */
|
|
@@ -252,8 +279,8 @@ export interface CoreOptions {
|
|
|
252
279
|
dialogs?: (chat: string) => AskUserIO | undefined;
|
|
253
280
|
/** Whether the project wants a sentinel - read as each pulse finishes: on, off, or ask the person first. Unset: none. */
|
|
254
281
|
sentinel?: () => SentinelSetting;
|
|
255
|
-
/** Under `sentinel: ask`: what the person is asked before the sentinel first opens, about the pulse that would open it. */
|
|
256
|
-
sentinelStart?: (
|
|
282
|
+
/** Under `sentinel: ask`: what the person is asked before the sentinel first opens, about the pulse - or the check - that would open it. */
|
|
283
|
+
sentinelStart?: (about: Job | SentinelCheck) => NoticeInput;
|
|
257
284
|
/** The person chose for the project, asked whether to start the sentinel: `sentinel:` written. */
|
|
258
285
|
setSentinel?: (on: boolean) => void;
|
|
259
286
|
/** The person ignored a notice for good: kept in the sentinel's guidance, for every conversation after. */
|
package/dist/chats.js
CHANGED
|
@@ -91,11 +91,13 @@ export function createChats(options) {
|
|
|
91
91
|
let notices = [];
|
|
92
92
|
/** What the sentinel put to the person, and turri itself, this session: open until answered or superseded. */
|
|
93
93
|
const open = [];
|
|
94
|
-
/** The
|
|
94
|
+
/** The work the sentinel's conversation is on, from its brief until its agent is done: a newer pulse of the same module replaces a check of one that went well. */
|
|
95
95
|
let checking;
|
|
96
|
-
/** Pulses that finished while the sentinel was at work: each
|
|
96
|
+
/** Pulses that finished, and checks asked for, while the sentinel was at work: each taken in turn, in a conversation of its own. */
|
|
97
97
|
const waiting = [];
|
|
98
|
-
/**
|
|
98
|
+
/** The checks hosts asked for this session, by id: what a notice or a conclusion about one is about. */
|
|
99
|
+
const checks = new Map();
|
|
100
|
+
/** What the sentinel is given is given one at a time: its conversations made, and its pulses and checks put to them, in order. */
|
|
99
101
|
let sentinelWork = Promise.resolve();
|
|
100
102
|
/**
|
|
101
103
|
* Under `sentinel: ask`, whether the person said to start the sentinel this session: asked - the notice `asking` open,
|
|
@@ -188,7 +190,7 @@ export function createChats(options) {
|
|
|
188
190
|
// Done with its pulse - or with what the person said to it: the next pulse waiting, in a conversation of its own.
|
|
189
191
|
if (chat.role === SENTINEL && event.willRetry !== true) {
|
|
190
192
|
checking = undefined;
|
|
191
|
-
toSentinel(() =>
|
|
193
|
+
toSentinel(() => nextWork(chat));
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
else if (event.type === "session_info_changed") {
|
|
@@ -277,7 +279,10 @@ export function createChats(options) {
|
|
|
277
279
|
// Closed, the sentinel is let go with what it had to check: the next pulse that finishes opens it again.
|
|
278
280
|
if (chat.role === SENTINEL) {
|
|
279
281
|
checking = undefined;
|
|
280
|
-
|
|
282
|
+
// A check a host asked for ends all the same: it is told it was not done.
|
|
283
|
+
for (const work of waiting.splice(0))
|
|
284
|
+
if ("check" in work)
|
|
285
|
+
unchecked(work.check, "the sentinel was closed before it got to it");
|
|
281
286
|
}
|
|
282
287
|
// The question whether to start the sentinel was in it: put again in the chat the person looks at now.
|
|
283
288
|
if (asking?.chat === id)
|
|
@@ -384,7 +389,7 @@ export function createChats(options) {
|
|
|
384
389
|
settleStart(setting() === true ? "given" : "declined", "superseded");
|
|
385
390
|
if (core.sentinelOn()) {
|
|
386
391
|
if (consent === "asking" || (setting() === "ask" && consent === "unasked" && sentinelChat() === undefined && askToStart(job))) {
|
|
387
|
-
held.push(job);
|
|
392
|
+
held.push({ pulse: job });
|
|
388
393
|
changed();
|
|
389
394
|
return;
|
|
390
395
|
}
|
|
@@ -393,6 +398,30 @@ export function createChats(options) {
|
|
|
393
398
|
}
|
|
394
399
|
toAsker(job);
|
|
395
400
|
},
|
|
401
|
+
check(prepare) {
|
|
402
|
+
if (driver === undefined)
|
|
403
|
+
return { refused: "There is nowhere to open the sentinel in this session." };
|
|
404
|
+
// The question was settled in config.turri.yaml while it was open: what it held goes as the file now says.
|
|
405
|
+
if (consent === "asking" && setting() !== "ask")
|
|
406
|
+
settleStart(setting() === true ? "given" : "declined", "superseded");
|
|
407
|
+
const now = setting();
|
|
408
|
+
if (now === false)
|
|
409
|
+
return { refused: "The sentinel is off for this project (sentinel: false): nothing checks the functional specs. Turn it on with sentinel: true in config.turri.yaml." };
|
|
410
|
+
if (!wantsSentinel())
|
|
411
|
+
return { refused: "The sentinel was not started this session - the person said not now when asked: nothing checks the functional specs until turri starts again, or sentinel: true is set." };
|
|
412
|
+
const check = prepare();
|
|
413
|
+
if ("refused" in check)
|
|
414
|
+
return check;
|
|
415
|
+
checks.set(check.id, check);
|
|
416
|
+
record({ kind: "sentinel.asked", check: check.id, module: check.module, funcIds: check.funcIds }, { project: cwd });
|
|
417
|
+
if (consent === "asking" || (now === "ask" && consent === "unasked" && sentinelChat() === undefined && askToStart(check))) {
|
|
418
|
+
held.push({ check });
|
|
419
|
+
changed();
|
|
420
|
+
return { check: check.id };
|
|
421
|
+
}
|
|
422
|
+
sentinelChecks(check);
|
|
423
|
+
return { check: check.id };
|
|
424
|
+
},
|
|
396
425
|
jobsChanged() {
|
|
397
426
|
for (const chat of chats) {
|
|
398
427
|
try {
|
|
@@ -404,18 +433,22 @@ export function createChats(options) {
|
|
|
404
433
|
}
|
|
405
434
|
},
|
|
406
435
|
notice(input) {
|
|
436
|
+
// A check's notice is about its module.
|
|
437
|
+
const about = input.check !== undefined ? checks.get(input.check) : undefined;
|
|
438
|
+
if (about !== undefined && input.module === undefined)
|
|
439
|
+
input = { ...input, module: about.module };
|
|
407
440
|
const asked = chats.find((chat) => chat.id === (input.job !== undefined ? jobs.get(input.job)?.chat : undefined));
|
|
408
441
|
const target = personFacing(asked);
|
|
409
442
|
if (target === undefined)
|
|
410
443
|
return undefined;
|
|
411
|
-
// One notice open a module: a newer one makes the older one's call to action stale.
|
|
412
|
-
if (input.module !== undefined) {
|
|
444
|
+
// One notice open a module: a newer one makes the older one's call to action stale. A note of a fix asks nothing.
|
|
445
|
+
if (input.module !== undefined && input.kind !== "fixed") {
|
|
413
446
|
for (const older of open.filter((notice) => notice.state === "open" && notice.module === input.module))
|
|
414
447
|
over(older, "superseded");
|
|
415
448
|
}
|
|
416
449
|
// Only the sentinel's own may be ignored: never the question whether to start it.
|
|
417
450
|
const { ignorable, ...rest } = input;
|
|
418
|
-
const notice = newNotice(ignorable === true && input.kind
|
|
451
|
+
const notice = newNotice(ignorable === true && input.kind === undefined ? input : rest, target.id);
|
|
419
452
|
open.push(notice);
|
|
420
453
|
if (target.id !== shownId)
|
|
421
454
|
target.unread = true;
|
|
@@ -437,16 +470,17 @@ export function createChats(options) {
|
|
|
437
470
|
},
|
|
438
471
|
notices: () => open.filter((notice) => notice.state === "open"),
|
|
439
472
|
concluded(conclusion) {
|
|
440
|
-
const job = jobs.get(conclusion.job);
|
|
473
|
+
const job = conclusion.job !== undefined ? jobs.get(conclusion.job) : undefined;
|
|
474
|
+
const check = conclusion.check !== undefined ? checks.get(conclusion.check) : undefined;
|
|
441
475
|
const target = personFacing(chats.find((chat) => chat.id === job?.chat));
|
|
442
476
|
if (target === undefined)
|
|
443
477
|
return undefined;
|
|
444
|
-
const module = conclusion.module ?? job?.module;
|
|
478
|
+
const module = conclusion.module ?? job?.module ?? check?.module;
|
|
445
479
|
const pulse = conclusion.pulse ?? job?.pulse;
|
|
446
480
|
const told = { ...conclusion, ...(module !== undefined ? { module } : {}), ...(pulse !== undefined ? { pulse } : {}) };
|
|
447
481
|
if (target.id !== shownId)
|
|
448
482
|
target.unread = true;
|
|
449
|
-
inChat(target.scope, () => record({ kind: "sentinel.concluded", verdict: told.verdict, ...(module !== undefined ? { module } : {}) }, { project: cwd, job: told.job }));
|
|
483
|
+
inChat(target.scope, () => record({ kind: "sentinel.concluded", verdict: told.verdict, ...(module !== undefined ? { module } : {}), ...(told.check !== undefined ? { check: told.check } : {}) }, { project: cwd, ...(told.job !== undefined ? { job: told.job } : {}) }));
|
|
450
484
|
try {
|
|
451
485
|
target.instance?.concluded(told);
|
|
452
486
|
}
|
|
@@ -577,10 +611,10 @@ export function createChats(options) {
|
|
|
577
611
|
* Under `sentinel: ask`, the first pulse that would open the sentinel this session: the person asked whether to start it,
|
|
578
612
|
* in the chat they look at. False when there is nothing to ask with, or no chat to ask in: the sentinel opens as asked.
|
|
579
613
|
*/
|
|
580
|
-
function askToStart(
|
|
614
|
+
function askToStart(about) {
|
|
581
615
|
let input;
|
|
582
616
|
try {
|
|
583
|
-
input = options.sentinelStart?.(
|
|
617
|
+
input = options.sentinelStart?.(about);
|
|
584
618
|
}
|
|
585
619
|
catch {
|
|
586
620
|
input = undefined;
|
|
@@ -599,7 +633,8 @@ export function createChats(options) {
|
|
|
599
633
|
over(question, "superseded");
|
|
600
634
|
asking = undefined;
|
|
601
635
|
consent = "unasked";
|
|
602
|
-
|
|
636
|
+
const first = held[0];
|
|
637
|
+
if (!askToStart("pulse" in first ? first.pulse : first.check))
|
|
603
638
|
settleStart("declined", "superseded");
|
|
604
639
|
}
|
|
605
640
|
/** The person answered whether to start the sentinel: written to the project, when they chose for it, and what was held let go. */
|
|
@@ -629,11 +664,17 @@ export function createChats(options) {
|
|
|
629
664
|
asking = undefined;
|
|
630
665
|
if (question !== undefined && question.state === "open")
|
|
631
666
|
over(question, state);
|
|
632
|
-
for (const
|
|
633
|
-
if (
|
|
634
|
-
|
|
667
|
+
for (const work of held.splice(0)) {
|
|
668
|
+
if ("check" in work) {
|
|
669
|
+
if (core.sentinelOn())
|
|
670
|
+
sentinelChecks(work.check);
|
|
671
|
+
else
|
|
672
|
+
unchecked(work.check, "the person did not start the sentinel");
|
|
673
|
+
}
|
|
674
|
+
else if (core.sentinelOn())
|
|
675
|
+
sentinelFinished(work.pulse);
|
|
635
676
|
else
|
|
636
|
-
toAsker(
|
|
677
|
+
toAsker(work.pulse);
|
|
637
678
|
}
|
|
638
679
|
changed();
|
|
639
680
|
}
|
|
@@ -658,24 +699,41 @@ export function createChats(options) {
|
|
|
658
699
|
}
|
|
659
700
|
changed();
|
|
660
701
|
toSentinel(async () => {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
sentinel = sentinelChat();
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
// No room for it, or nowhere to open it: the pulse is told as it is without one.
|
|
669
|
-
core.notify(`The sentinel could not be opened: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
670
|
-
(asked ?? where)?.instance?.announce(job);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (sentinel?.instance === undefined)
|
|
675
|
-
return;
|
|
676
|
-
await takePulse(sentinel, job);
|
|
702
|
+
// No room for it, or nowhere to open it: the pulse is told as it is without one.
|
|
703
|
+
const sentinel = await openSentinel(() => (asked ?? where)?.instance?.announce(job));
|
|
704
|
+
if (sentinel !== undefined)
|
|
705
|
+
await takeWork(sentinel, { pulse: job });
|
|
677
706
|
});
|
|
678
707
|
}
|
|
708
|
+
/** A check a host asked for, for the sentinel - opened in the background when it is not open - in its turn. */
|
|
709
|
+
function sentinelChecks(check) {
|
|
710
|
+
toSentinel(async () => {
|
|
711
|
+
const sentinel = await openSentinel((why) => unchecked(check, `the sentinel could not be opened: ${why}`));
|
|
712
|
+
if (sentinel !== undefined)
|
|
713
|
+
await takeWork(sentinel, { check });
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/** The sentinel's chat, opened hidden when it is not open - none, and `failed` told why, when it cannot be. */
|
|
717
|
+
async function openSentinel(failed) {
|
|
718
|
+
let sentinel = sentinelChat();
|
|
719
|
+
if (sentinel === undefined) {
|
|
720
|
+
try {
|
|
721
|
+
await core.open({ hidden: true, role: SENTINEL });
|
|
722
|
+
sentinel = sentinelChat();
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
726
|
+
core.notify(`The sentinel could not be opened: ${why}`, "warning");
|
|
727
|
+
failed(why);
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return sentinel?.instance !== undefined ? sentinel : undefined;
|
|
732
|
+
}
|
|
733
|
+
/** A check that will not be done, told as a conclusion of turri's own - the host waits for one. */
|
|
734
|
+
function unchecked(check, why) {
|
|
735
|
+
core.concluded({ check: check.id, module: check.module, verdict: "unchecked", summary: `Not checked: ${why}.` });
|
|
736
|
+
}
|
|
679
737
|
/** The sentinel's work, after what it was given before. */
|
|
680
738
|
function toSentinel(work) {
|
|
681
739
|
sentinelWork = sentinelWork.then(work).catch((error) => {
|
|
@@ -685,36 +743,72 @@ export function createChats(options) {
|
|
|
685
743
|
/** Whether the sentinel is at work: on a pulse - its turn starts as it is told of one - or on what the person said to it. */
|
|
686
744
|
const sentinelBusy = (sentinel) => sentinel.session?.isStreaming === true;
|
|
687
745
|
/**
|
|
688
|
-
* A finished pulse, for the sentinel:
|
|
689
|
-
* the module's last pulse that went well - and a pulse of it that went well and waits - and one that
|
|
690
|
-
* looked into to its end; the rest wait their turn.
|
|
746
|
+
* A finished pulse or a host's check, for the sentinel: taken now when it is free. Busy, a newer pulse of a module
|
|
747
|
+
* replaces a check of the module's last pulse that went well - and a pulse of it that went well and waits - and one that
|
|
748
|
+
* went wrong is looked into to its end; the rest, checks among them, wait their turn.
|
|
691
749
|
*/
|
|
692
|
-
function
|
|
750
|
+
function takeWork(sentinel, work) {
|
|
693
751
|
if (!sentinelBusy(sentinel))
|
|
694
|
-
return
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return
|
|
698
|
-
|
|
752
|
+
return begin(sentinel, work);
|
|
753
|
+
if (!("pulse" in work)) {
|
|
754
|
+
waiting.push(work);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const job = work.pulse;
|
|
758
|
+
const sameModule = (other) => job.module !== undefined && "pulse" in other && other.pulse.module === job.module;
|
|
759
|
+
if (checking !== undefined && !checking.wrong && sameModule(checking.work) && "pulse" in checking.work)
|
|
760
|
+
return check(sentinel, job, checking.work.pulse);
|
|
761
|
+
const older = waiting.findIndex((other) => sameModule(other) && "pulse" in other && !wentWrong(other.pulse));
|
|
699
762
|
if (older !== -1)
|
|
700
|
-
waiting.splice(older, 1,
|
|
763
|
+
waiting.splice(older, 1, work);
|
|
701
764
|
else
|
|
702
|
-
waiting.push(
|
|
765
|
+
waiting.push(work);
|
|
703
766
|
}
|
|
704
|
-
/** The sentinel free again: the next pulse that waits,
|
|
705
|
-
function
|
|
767
|
+
/** The sentinel free again: the next pulse or check that waits, taken. */
|
|
768
|
+
function nextWork(sentinel) {
|
|
706
769
|
if (!chats.includes(sentinel) || sentinelBusy(sentinel))
|
|
707
770
|
return;
|
|
708
|
-
const
|
|
709
|
-
if (
|
|
710
|
-
return
|
|
771
|
+
const work = waiting.shift();
|
|
772
|
+
if (work !== undefined)
|
|
773
|
+
return begin(sentinel, work);
|
|
774
|
+
}
|
|
775
|
+
function begin(sentinel, work) {
|
|
776
|
+
return "pulse" in work ? check(sentinel, work.pulse) : checkSpecs(sentinel, work.check);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The sentinel on a host's check, from scratch, as on a pulse: a conversation of its own, the module released first
|
|
780
|
+
* unless what runs is its last commit. A release that fails ends the check - a conclusion of its own - and the next
|
|
781
|
+
* waiting is taken.
|
|
782
|
+
*/
|
|
783
|
+
async function checkSpecs(sentinel, check) {
|
|
784
|
+
checking = { work: { check }, wrong: false };
|
|
785
|
+
inChat(sentinel.scope, () => record({ kind: "sentinel.checking", module: check.module, check: check.id }, { project: cwd }));
|
|
786
|
+
const session = sentinel.session;
|
|
787
|
+
if (session !== undefined && (session.messages.length > 0 || session.isStreaming))
|
|
788
|
+
await freshConversation(sentinel);
|
|
789
|
+
if (sentinel.id !== shownId)
|
|
790
|
+
sentinel.unread = true;
|
|
791
|
+
const where = personFacing();
|
|
792
|
+
let why;
|
|
793
|
+
try {
|
|
794
|
+
why = await sentinel.instance.briefCheck(check, where !== undefined ? { focused: titleFor(where) } : {});
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
why = error instanceof Error ? error.message : String(error);
|
|
798
|
+
}
|
|
799
|
+
changed();
|
|
800
|
+
if (why === undefined)
|
|
801
|
+
return;
|
|
802
|
+
checking = undefined;
|
|
803
|
+
core.concluded({ check: check.id, module: check.module, verdict: "wrong", summary: `Not tested: ${check.module} could not be released - ${why}` });
|
|
804
|
+
await nextWork(sentinel);
|
|
711
805
|
}
|
|
712
806
|
/**
|
|
713
807
|
* The sentinel on a pulse, from scratch: a conversation of its own - its prompt, the project's guidance and this
|
|
714
808
|
* pulse's brief, nothing of the pulses before it - in place of the one it had, stopped when it was on `replacing`.
|
|
715
809
|
*/
|
|
716
810
|
async function check(sentinel, job, replacing) {
|
|
717
|
-
checking = { job, wrong: wentWrong(job) };
|
|
811
|
+
checking = { work: { pulse: job }, wrong: wentWrong(job) };
|
|
718
812
|
const module = job.module;
|
|
719
813
|
inChat(sentinel.scope, () => record({ kind: "sentinel.checking", ...(module !== undefined ? { module } : {}), ...(replacing !== undefined ? { replaced: replacing.id } : {}) }, { project: cwd, job: job.id }));
|
|
720
814
|
const session = sentinel.session;
|
package/dist/cli.js
CHANGED
|
@@ -142,9 +142,11 @@ if (command === "-h" || command === "--help") {
|
|
|
142
142
|
console.log(USAGE);
|
|
143
143
|
process.exit(0);
|
|
144
144
|
}
|
|
145
|
-
// turri's version, not pi's alone: pi's --version would answer for it
|
|
145
|
+
// turri's version, not pi's alone: pi's --version would answer for it - and where this turri is, the newest on npm,
|
|
146
|
+
// every turri on the PATH, and what to do when the one that runs is not the newest (turri-versions.ts).
|
|
146
147
|
if (command === "-v" || command === "--version") {
|
|
147
|
-
|
|
148
|
+
const { describeVersion } = await import("./turri-versions.js");
|
|
149
|
+
console.log(await describeVersion(versionLine()));
|
|
148
150
|
process.exit(0);
|
|
149
151
|
}
|
|
150
152
|
// The AWS region the project's Bedrock models need, set before any model is called - here, and in the pi the chat starts,
|
package/dist/config.d.ts
CHANGED
|
@@ -379,6 +379,9 @@ export declare const DEFAULT_TEST_DIR = "tests";
|
|
|
379
379
|
export declare function testDirOf(plainFile: string): string;
|
|
380
380
|
/** The config a spec is built with: its project's, with its module's stack over it (`stackForModule`). */
|
|
381
381
|
export declare function configForSpec(plainFile: string): ProjectConfig;
|
|
382
|
-
/**
|
|
382
|
+
/**
|
|
383
|
+
* One config file, read, or nothing of it when it is not there or does not parse. Parsed again only once the file has
|
|
384
|
+
* changed - its size or its modification time - and each caller given its own copy.
|
|
385
|
+
*/
|
|
383
386
|
export declare function readConfigFile(path: string): ProjectConfig;
|
|
384
387
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -81,14 +81,27 @@ export function stackConfigFile(stack) {
|
|
|
81
81
|
function legacyStackConfigFile(stack) {
|
|
82
82
|
return `config.${stack}.yaml`;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The names in a folder - read again only once the folder itself has changed: a name added, removed or renamed in it
|
|
86
|
+
* moves its modification time. Asked many times a look, for every module of every chain.
|
|
87
|
+
*/
|
|
84
88
|
function namesIn(root) {
|
|
85
89
|
try {
|
|
86
|
-
|
|
90
|
+
const at = statSync(root).mtimeMs;
|
|
91
|
+
const kept = keptNames.get(root);
|
|
92
|
+
if (kept !== undefined && kept.at === at)
|
|
93
|
+
return [...kept.names];
|
|
94
|
+
const names = readdirSync(root);
|
|
95
|
+
keptNames.set(root, { at, names });
|
|
96
|
+
if (keptNames.size > 64)
|
|
97
|
+
keptNames.delete(keptNames.keys().next().value);
|
|
98
|
+
return [...names];
|
|
87
99
|
}
|
|
88
100
|
catch {
|
|
89
101
|
return [];
|
|
90
102
|
}
|
|
91
103
|
}
|
|
104
|
+
const keptNames = new Map();
|
|
92
105
|
/** The stacks in the old naming, `config.<stack>.yaml`. */
|
|
93
106
|
function legacyStacks(root) {
|
|
94
107
|
return namesIn(root)
|
|
@@ -530,12 +543,34 @@ export function configForSpec(plainFile) {
|
|
|
530
543
|
const root = projectDirOf(resolve(plainFile));
|
|
531
544
|
return readProjectConfig(root, stackForModule(plainFile));
|
|
532
545
|
}
|
|
533
|
-
/**
|
|
546
|
+
/**
|
|
547
|
+
* One config file, read, or nothing of it when it is not there or does not parse. Parsed again only once the file has
|
|
548
|
+
* changed - its size or its modification time - and each caller given its own copy.
|
|
549
|
+
*/
|
|
534
550
|
export function readConfigFile(path) {
|
|
535
|
-
let
|
|
551
|
+
let stamp;
|
|
536
552
|
try {
|
|
537
|
-
|
|
553
|
+
const stat = statSync(path);
|
|
554
|
+
if (!stat.isFile())
|
|
538
555
|
return {};
|
|
556
|
+
stamp = `${stat.size}:${stat.mtimeMs}:${stat.ino}`;
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return {};
|
|
560
|
+
}
|
|
561
|
+
const kept = keptConfigs.get(path);
|
|
562
|
+
if (kept !== undefined && kept.stamp === stamp)
|
|
563
|
+
return structuredClone(kept.config);
|
|
564
|
+
const config = readConfigFileNow(path);
|
|
565
|
+
keptConfigs.set(path, { stamp, config });
|
|
566
|
+
if (keptConfigs.size > 64)
|
|
567
|
+
keptConfigs.delete(keptConfigs.keys().next().value);
|
|
568
|
+
return structuredClone(config);
|
|
569
|
+
}
|
|
570
|
+
const keptConfigs = new Map();
|
|
571
|
+
function readConfigFileNow(path) {
|
|
572
|
+
let parsed;
|
|
573
|
+
try {
|
|
539
574
|
parsed = loadYaml(readFileSync(path, "utf8"));
|
|
540
575
|
}
|
|
541
576
|
catch {
|
package/dist/environment.d.ts
CHANGED
|
@@ -22,8 +22,6 @@ export declare function themeFiles(): string[];
|
|
|
22
22
|
* about a duplicate we created ourselves.
|
|
23
23
|
*/
|
|
24
24
|
export declare function themesInstalledGlobally(): boolean;
|
|
25
|
-
/** Where `turri` on the PATH points, if it is on the PATH at all. */
|
|
26
|
-
export declare function turriOnPath(): string | undefined;
|
|
27
25
|
/** The version of the pi turri runs on, as Node finds it - undefined when it is not installed. */
|
|
28
26
|
export declare function piVersion(): string | undefined;
|
|
29
27
|
/**
|
package/dist/environment.js
CHANGED
|
@@ -62,12 +62,6 @@ export function themesInstalledGlobally() {
|
|
|
62
62
|
const names = themeFiles();
|
|
63
63
|
return names.length > 0 && names.every((name) => existsSync(join(AGENT_DIR, "themes", name)));
|
|
64
64
|
}
|
|
65
|
-
/** Where `turri` on the PATH points, if it is on the PATH at all. */
|
|
66
|
-
export function turriOnPath() {
|
|
67
|
-
const which = spawnSync(process.platform === "win32" ? "where" : "which", ["turri"], { encoding: "utf8" });
|
|
68
|
-
const found = which.status === 0 ? which.stdout.trim().split("\n")[0] : undefined;
|
|
69
|
-
return found === "" ? undefined : found;
|
|
70
|
-
}
|
|
71
65
|
/** The version of the pi turri runs on, as Node finds it - undefined when it is not installed. */
|
|
72
66
|
export function piVersion() {
|
|
73
67
|
try {
|
package/dist/extension.d.ts
CHANGED
|
@@ -107,13 +107,14 @@ export declare function parallelJobs(cwd: string): number;
|
|
|
107
107
|
export declare function machineBlock(cwd: string, modules?: ReturnType<typeof stackOnMachine>): string;
|
|
108
108
|
/**
|
|
109
109
|
* What the project's modules need of this machine, kept between turns - working it out parses every spec, seconds on a
|
|
110
|
-
* project of long requires chains
|
|
111
|
-
*
|
|
110
|
+
* project of long requires chains. Worked out when the first turn asks - not as the session starts: a host's thread
|
|
111
|
+
* would be held up opening a chat no one may write in - and again, once known, when a turn has settled and a spec
|
|
112
|
+
* changed since: a turn is told what was known last, at most a turn behind.
|
|
112
113
|
*/
|
|
113
114
|
export declare function machineNeeds(cwd: string, work?: (cwd: string) => ReturnType<typeof stackOnMachine>): {
|
|
114
115
|
/** What is known - worked out now only when nothing is yet. */
|
|
115
116
|
now(): ReturnType<typeof stackOnMachine>;
|
|
116
|
-
/** Worked out again, if a spec changed - once the chat has drawn what it is drawing. */
|
|
117
|
+
/** Worked out again, if it is known and a spec changed - once the chat has drawn what it is drawing. */
|
|
117
118
|
refreshSoon(): void;
|
|
118
119
|
};
|
|
119
120
|
/**
|