@turri-dev/cli 0.1.27-next.763d091 → 0.1.28-next.d5f2680
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 +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +71 -15
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -20
- package/dist/regenerate/conformance.js +118 -121
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +61 -49
- package/dist/regenerate/loop.js +473 -340
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +69 -31
- package/dist/regenerate/test-kinds.js +109 -80
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +89 -54
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +24 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -151,14 +151,14 @@ Then say what you want:
|
|
|
151
151
|
|
|
152
152
|
> A small API where people can register and sign in, in Python.
|
|
153
153
|
|
|
154
|
-
In a new project **turri** starts it itself, without asking how: it chooses the stack - the one you name, else a standard library stack this machine can build - writes the module and its unit testing script (and the conformance tests' when you ask for them), regenerates the smallest thing that runs at once, then the main thing you described, and says what it chose and what it would write next. Every change after is yours to ask for: **turri** writes it as \*\*\*plain and regenerates it. While it works:
|
|
154
|
+
In a new project **turri** starts it itself, without asking how: it chooses the stack - the one you name, else a standard library stack this machine can build - writes the module and its unit testing script (and the conformance tests' when you ask for them, or the [test advisor](docs/test-advisor.md) finds a functional spec that needs them), regenerates the smallest thing that runs at once, then the main thing you described, and says what it chose and what it would write next. Every change after is yours to ask for: **turri** writes it as \*\*\*plain and regenerates it. While it works:
|
|
155
155
|
|
|
156
156
|
- the **panel** above the input shows every build - its stage, what its agents are doing, its model and cost - and the queue behind it;
|
|
157
157
|
- ask to try it at any time - "let me try it" - and **`release`** puts what is built so far in `dist/<module>` and **`activate`** starts it, installing what it needs - nothing to approve;
|
|
158
158
|
- the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`, kept with `--debug` (`debug: true`);
|
|
159
159
|
- **`/halt`** halts a pulse, **`/lineage`** shows every generation of a module, and the **`drift`** tool says what is left to regenerate;
|
|
160
160
|
- **`/btw <question>`** asks turri something on the side - answered at once, even while a question waits for you (typed as the answer, under "Something else…"), and left out of the conversation;
|
|
161
|
-
- **`/turri-config`** changes any setting of the project's `config.turri.yaml` - the models, review,
|
|
161
|
+
- **`/turri-config`** changes any setting of the project's `config.turri.yaml` - the models, review, the test advisor, how many pulses at once - and says when it takes effect: now, at your next message, or at the next pulse. No restart: `/turri-config review false`, or `/turri-config` alone to pick from the list;
|
|
162
162
|
- **`/sentinel`** says whether the sentinel checks every pulse that finishes: `ask` (you are asked before it first opens), `on` or `off`;
|
|
163
163
|
- **`/stack`** chooses the stack to build with, in a project whose backend and frontend each have their own testing scripts in `config.turri.<stack>.yaml` - `/stack backend`, `/stack none` - for the session (`--stack <name>` on the command line). The chat's model builds each part with its own stack by itself, pinned to that module's pulses.
|
|
164
164
|
|
|
@@ -175,10 +175,10 @@ The last one holds for the rest of the session; the model can be chosen separate
|
|
|
175
175
|
Without a conversation, the same work is on the command line:
|
|
176
176
|
|
|
177
177
|
```bash
|
|
178
|
-
turri
|
|
178
|
+
turri parse app.plain # parse ***plain, its syntax and structure - no model, no API key
|
|
179
179
|
turri drift app.plain # what is left to regenerate - starts nothing
|
|
180
180
|
turri regenerate app.plain # regenerate the code from ***plain: planned into jobs, run until it is done
|
|
181
|
-
turri regenerate app.plain
|
|
181
|
+
turri regenerate app.plain app:add-task # regenerate one functional spec again, changed or not
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
More: [using **turri**](docs/using.md); to regenerate \*\*\*plain overnight, see [running unattended](docs/headless.md).
|
|
@@ -190,7 +190,7 @@ Start at the [documentation index](docs/README.md), or go straight to a page:
|
|
|
190
190
|
| | |
|
|
191
191
|
| --- | --- |
|
|
192
192
|
| [Installation](docs/installation.md) | doctor, what turri needs of this machine, **turri** and `pi`, environment variables |
|
|
193
|
-
| [Examples](examples/README.md) | projects to build with turri step by step: guestbook - a backend and a frontend in minutes - a CLI, and a backend service
|
|
193
|
+
| [Examples](examples/README.md) | projects to build with turri step by step: guestbook - a backend and a frontend in minutes - a CLI, and a backend service tested where it matters |
|
|
194
194
|
| [Using **turri**](docs/using.md) | the conversation, skills, tools, pulses and regen, choosing models, the command line |
|
|
195
195
|
| [The project config](docs/config.md) | `config.turri.yaml` and each stack's `config.turri.<stack>.yaml`: every key, models and their inheritance, conformance, the testing scripts, stacks and choosing one, the old `config.yaml` |
|
|
196
196
|
| [Deploy](docs/deploy.md) | trying what is built: the last commit in `dist/<module>`, started on your machine, nothing installed without asking |
|
|
@@ -199,7 +199,7 @@ Start at the [documentation index](docs/README.md), or go straight to a page:
|
|
|
199
199
|
| [Regenerating](docs/regenerating.md) | what each build does and in which order: functionalities, modules, regeneration, changes to \*\*\*plain, conformance and acceptance tests, working notes, what the code is built from |
|
|
200
200
|
| [The sandbox](docs/sandbox.md) | what agents can reach, the walls every command passes, time limits, the web tools |
|
|
201
201
|
| [Skills and gates](docs/skills-and-gates.md) | the gate syntax, what the harness enforces, judges, rules |
|
|
202
|
-
| [The \*\*\*plain parser](docs/parser.md) | the TypeScript port, `turri
|
|
202
|
+
| [The \*\*\*plain parser](docs/parser.md) | the TypeScript port, `turri parse`, fidelity to the reference parser |
|
|
203
203
|
| [Theming](docs/theming.md) | the two brand themes |
|
|
204
204
|
| [Development](docs/development.md) | tests, layout, known edges |
|
|
205
205
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ChatLink } from "./chats.ts";
|
|
3
|
+
import type { AdvisorVerdict, TestAdvisor } from "./regenerate/advisor-runs.ts";
|
|
4
|
+
import type { WorkLoop } from "./regenerate/loop.ts";
|
|
5
|
+
/** The line a no is drawn as: the functional spec, and the classifier's reading in a few words - a no has no evidence. */
|
|
6
|
+
export declare function noLine(verdict: AdvisorVerdict): string;
|
|
7
|
+
/** The line the chat is to tell the person a yes in. */
|
|
8
|
+
export declare function yesLine(verdict: AdvisorVerdict): string;
|
|
9
|
+
/** A yes's report, as the chat's agent is given it: the questions that were a yes with their evidence, and the inputs. */
|
|
10
|
+
export declare function describeReport(verdict: AdvisorVerdict): string[];
|
|
11
|
+
/** The turn a batch of yeses starts: each one's report, and what the chat is to do with them - asking the person nothing. */
|
|
12
|
+
export declare function yesTask(verdicts: AdvisorVerdict[]): string;
|
|
13
|
+
/** How long a verdict waits before the chat is looked at again, while the person types or it cannot be told yet. */
|
|
14
|
+
export declare const ADVISOR_RECHECK_MS = 2000;
|
|
15
|
+
/**
|
|
16
|
+
* A chat's verdicts: held until the chat is idle - its agent done, and nothing typed in its editor - then delivered
|
|
17
|
+
* together: one turn for every yes, one line for each no. A verdict no longer about its functional spec as it is - its
|
|
18
|
+
* text changed, or it was given scenarios meanwhile - is let go.
|
|
19
|
+
*/
|
|
20
|
+
export declare function verdictDelivery(pi: ExtensionAPI, link: ChatLink, context: () => ExtensionContext | undefined): {
|
|
21
|
+
put(verdict: AdvisorVerdict): void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The test advisor and the chat, in its system prompt while the session has the advisor and the project wants it: the
|
|
25
|
+
* two ways an evaluation starts, what to say while it runs, what to do with its verdict, and when to ask for one.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TEST_ADVISOR_BLOCK: string;
|
|
28
|
+
/** The test advisor of the session a work loop is in. */
|
|
29
|
+
export declare function bindAdvisor(loop: WorkLoop, advisor: () => TestAdvisor | undefined): void;
|
|
30
|
+
/**
|
|
31
|
+
* What a change's tool result tells the chat's agent of the test advisor: the functional specs it has taken up, so the
|
|
32
|
+
* agent tells the person they are being evaluated - nothing when it has taken up none.
|
|
33
|
+
*/
|
|
34
|
+
export declare function advisingNote(loop: WorkLoop, plainFile: string): string;
|
|
35
|
+
/** What an evaluation asked for came to, by functional spec - each named `module:func-id`, a missing one with why. */
|
|
36
|
+
export interface EvaluateOutcome {
|
|
37
|
+
/** Being evaluated now: the verdict comes to the chat the person looks at. */
|
|
38
|
+
started: string[];
|
|
39
|
+
/** Being evaluated already. */
|
|
40
|
+
already: string[];
|
|
41
|
+
/** With scenarios already: not evaluated. */
|
|
42
|
+
scenarios: string[];
|
|
43
|
+
/** Not found, with why. */
|
|
44
|
+
missing: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The test advisor asked to evaluate functional specs - `module:func-id`s, or func-ids of `plainFile`'s module (else the
|
|
48
|
+
* one the project's config names) - even a text it judged already, and whatever `test-advisor` says: `evaluate_tests`'s
|
|
49
|
+
* and a host's `evaluateTests`. It returns at once.
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateTests(cwd: string, advisor: TestAdvisor | undefined, funcIds: string[], options?: {
|
|
52
|
+
plainFile?: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
}): EvaluateOutcome;
|
|
55
|
+
/**
|
|
56
|
+
* evaluate_tests: the chat's own way to the test advisor - a functional spec evaluated in the background, whatever
|
|
57
|
+
* `test-advisor` says, and even when its text was judged already. It returns at once; the verdict comes back to the chat
|
|
58
|
+
* as the advisor's verdicts do. In the chat and a host only: the session has an advisor.
|
|
59
|
+
*/
|
|
60
|
+
export declare function evaluateTestsTool(options: {
|
|
61
|
+
cwd: string;
|
|
62
|
+
advisor: () => TestAdvisor | undefined;
|
|
63
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { chatBusy } from "./chat-idle.js";
|
|
7
|
+
import { conformanceTestsOnIn } from "./conformance-switch.js";
|
|
8
|
+
import { readProjectConfig } from "./config.js";
|
|
9
|
+
import { modulePlainFile } from "./plain/parser.js";
|
|
10
|
+
import { projectDirOf, resolveTemplateDirs } from "./regenerate/paths.js";
|
|
11
|
+
import { resolveRequiresChain } from "./regenerate/requires.js";
|
|
12
|
+
import { refuseTestPlainFile } from "./test-plain-files.js";
|
|
13
|
+
/**
|
|
14
|
+
* The test advisor in the chat (docs/test-advisor.md). The advisor decides; the chat is the only writer of the ***plain.
|
|
15
|
+
* A finished verdict reaches the chat the person looks at once it is idle - never interrupting it: a yes is a message
|
|
16
|
+
* that starts a turn, in which the chat tells the person and writes the scenarios with change_plain, asking nothing; a
|
|
17
|
+
* no is one line drawn by turri, starting no turn. What else the chat is told of the advisor - in its system prompt, in a
|
|
18
|
+
* change's result - is here too, and its own way to it, evaluate_tests.
|
|
19
|
+
*/
|
|
20
|
+
/** Why, in a few words, by the first question of a yes. */
|
|
21
|
+
const WHY = {
|
|
22
|
+
"silent-failure": "a wrong implementation could look right",
|
|
23
|
+
divergence: "it can be read two ways",
|
|
24
|
+
cost: "a bug in it would be costly",
|
|
25
|
+
};
|
|
26
|
+
const specOf = (verdict) => `${verdict.module}:${verdict.funcId}`;
|
|
27
|
+
/** The line a no is drawn as: the functional spec, and the classifier's reading in a few words - a no has no evidence. */
|
|
28
|
+
export function noLine(verdict) {
|
|
29
|
+
const only = verdict.report.questions.map((answered) => answered.question);
|
|
30
|
+
const reading = only.length > 0 && only.every((question) => question === "revisited") ? "its unit tests cover it as it is built on" : "nothing silent to get wrong";
|
|
31
|
+
return `${specOf(verdict)} doesn't need testing scenarios - ${reading}.`;
|
|
32
|
+
}
|
|
33
|
+
/** The line the chat is to tell the person a yes in. */
|
|
34
|
+
export function yesLine(verdict) {
|
|
35
|
+
const why = verdict.report.questions.map((answered) => WHY[answered.question]).find((reason) => reason !== undefined);
|
|
36
|
+
return `${specOf(verdict)} needs testing scenarios${why !== undefined ? ` - ${why}` : ""}. Writing them now.`;
|
|
37
|
+
}
|
|
38
|
+
/** A yes's report, as the chat's agent is given it: the questions that were a yes with their evidence, and the inputs. */
|
|
39
|
+
export function describeReport(verdict) {
|
|
40
|
+
return [
|
|
41
|
+
"Why it needs testing:",
|
|
42
|
+
...verdict.report.questions.map((answered) => `- ${answered.question}: ${answered.evidence}`),
|
|
43
|
+
"Inputs to cover:",
|
|
44
|
+
...(verdict.report.inputs.length === 0
|
|
45
|
+
? ["- none picked: cover its expected use, as the rules on test plain files say"]
|
|
46
|
+
: verdict.report.inputs.map((input, index) => "agreed" in input
|
|
47
|
+
? `${index + 1}. input: ${input.input} - agreed output: ${input.agreed}`
|
|
48
|
+
: `${index + 1}. input: ${input.input} - split: ${input.readings.map((reading, at) => `(${at + 1}) ${reading}`).join("; ")}`)),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/** The turn a batch of yeses starts: each one's report, and what the chat is to do with them - asking the person nothing. */
|
|
52
|
+
export function yesTask(verdicts) {
|
|
53
|
+
const where = (path, project) => relative(project, path) || path;
|
|
54
|
+
return [
|
|
55
|
+
`My test advisor judged that ${verdicts.length === 1 ? "this functional spec needs" : "these functional specs need"} explicit testing scenarios. You write them now.`,
|
|
56
|
+
"",
|
|
57
|
+
...verdicts.flatMap((verdict) => [
|
|
58
|
+
`<functional_spec name="${specOf(verdict)}" plain_file="${where(verdict.plainFile, verdict.project)}" tested_in="${where(verdict.tip, verdict.project)}" test_plain_file="${verdict.testFile}">`,
|
|
59
|
+
...describeReport(verdict),
|
|
60
|
+
"</functional_spec>",
|
|
61
|
+
"",
|
|
62
|
+
]),
|
|
63
|
+
"What to do, in this turn, asking the person nothing:",
|
|
64
|
+
`1. Tell the person in a line for each, e.g. "${yesLine(verdicts[0])}"`,
|
|
65
|
+
"2. With change_plain, write whatever the step-7 guard would refuse, in this order, only what is missing: the ***test reqs*** of the module",
|
|
66
|
+
" it is tested in - the stack's conformance template, stdlib/<kind>/conformance/<stack>, imported, or the module's own - then the",
|
|
67
|
+
" conformance testing script for its stack, with implement-conformance-testing-script - cultivate, plainFile the module it is tested in,",
|
|
68
|
+
" which returns at once: go straight on - then the scenarios in its test plain file.",
|
|
69
|
+
"3. Cover each input of the report that is this functional spec's: an agreed one with the readers' output, never a placeholder such as",
|
|
70
|
+
" <name> where a value belongs; a split one settled by the reading most consistent with the functional spec and its module - say which",
|
|
71
|
+
" reading you took. Leave out an input that exercises another functional spec, or nothing this one says.",
|
|
72
|
+
"4. Ask the person nothing: the change is visible, and turri tests the scenarios once the code is built.",
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
/** The line a no is drawn as, in the transcript and in no model's context. */
|
|
76
|
+
const ADVISOR_ENTRY = "turri_advisor";
|
|
77
|
+
/** How long a verdict waits before the chat is looked at again, while the person types or it cannot be told yet. */
|
|
78
|
+
export const ADVISOR_RECHECK_MS = 2000;
|
|
79
|
+
/**
|
|
80
|
+
* A chat's verdicts: held until the chat is idle - its agent done, and nothing typed in its editor - then delivered
|
|
81
|
+
* together: one turn for every yes, one line for each no. A verdict no longer about its functional spec as it is - its
|
|
82
|
+
* text changed, or it was given scenarios meanwhile - is let go.
|
|
83
|
+
*/
|
|
84
|
+
export function verdictDelivery(pi, link, context) {
|
|
85
|
+
const waiting = [];
|
|
86
|
+
let timer;
|
|
87
|
+
pi.registerEntryRenderer(ADVISOR_ENTRY, (entry, _options, theme) => {
|
|
88
|
+
const data = entry.data;
|
|
89
|
+
if (data === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
const box = new Box(1, 0);
|
|
92
|
+
box.addChild(new Text(theme.fg("muted", data.text), 0, 0));
|
|
93
|
+
return box;
|
|
94
|
+
});
|
|
95
|
+
const busy = () => chatBusy(link, context);
|
|
96
|
+
const later = (ms) => {
|
|
97
|
+
if (timer !== undefined)
|
|
98
|
+
return;
|
|
99
|
+
timer = setTimeout(() => {
|
|
100
|
+
timer = undefined;
|
|
101
|
+
deliver();
|
|
102
|
+
}, ms);
|
|
103
|
+
timer.unref?.();
|
|
104
|
+
};
|
|
105
|
+
const deliver = () => {
|
|
106
|
+
if (waiting.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
if (busy()) {
|
|
109
|
+
// Its agent ending says so (agent_settled); the person's typing does not, so it is looked at again.
|
|
110
|
+
later(ADVISOR_RECHECK_MS);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const now = waiting.splice(0).filter((verdict) => {
|
|
114
|
+
try {
|
|
115
|
+
return verdict.current();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const yes = now.filter((verdict) => verdict.report.verdict === "yes");
|
|
122
|
+
try {
|
|
123
|
+
for (const verdict of now.filter((verdict) => verdict.report.verdict === "no"))
|
|
124
|
+
pi.appendEntry(ADVISOR_ENTRY, { text: noLine(verdict) });
|
|
125
|
+
if (yes.length > 0) {
|
|
126
|
+
// A follow-up, never a steer: should a turn start in between, this waits for it to end.
|
|
127
|
+
void pi.sendMessage({ customType: "turri_advisor_verdict", content: yesTask(yes), display: false, details: { specs: yes.map(specOf) } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Not bound to its session yet: kept, and tried again.
|
|
132
|
+
waiting.unshift(...now);
|
|
133
|
+
later(ADVISOR_RECHECK_MS);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const verdict of now)
|
|
137
|
+
verdict.delivered();
|
|
138
|
+
};
|
|
139
|
+
pi.on("agent_settled", () => deliver());
|
|
140
|
+
pi.on("session_start", () => later(0));
|
|
141
|
+
return {
|
|
142
|
+
put(verdict) {
|
|
143
|
+
// A newer verdict on the same functional spec replaces the one waiting.
|
|
144
|
+
const at = waiting.findIndex((known) => known.project === verdict.project && specOf(known) === specOf(verdict));
|
|
145
|
+
if (at !== -1)
|
|
146
|
+
waiting.splice(at, 1);
|
|
147
|
+
waiting.push(verdict);
|
|
148
|
+
// The rest of what arrives at once comes with it.
|
|
149
|
+
later(0);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The test advisor and the chat, in its system prompt while the session has the advisor and the project wants it: the
|
|
155
|
+
* two ways an evaluation starts, what to say while it runs, what to do with its verdict, and when to ask for one.
|
|
156
|
+
*/
|
|
157
|
+
export const TEST_ADVISOR_BLOCK = [
|
|
158
|
+
"<test advisor>",
|
|
159
|
+
"My test advisor decides whether a functional spec needs explicit testing scenarios - whether it could fail silently, be read two ways, or",
|
|
160
|
+
"cost a lot when wrong - in the background. You are the only writer of the ***plain: it writes nothing, and you write what its yes calls for.",
|
|
161
|
+
"An evaluation starts two ways:",
|
|
162
|
+
"- By itself, for each new or changed functional spec that has no scenarios. A change's result names the ones it took up.",
|
|
163
|
+
"- When you call evaluate_tests - even for a text it judged already. Call it when the person asks whether a functional spec needs tests,",
|
|
164
|
+
" and on your own, as another way of saying what one means: when pulses keep failing on a complicated functional spec, scenarios may pin",
|
|
165
|
+
" down what it should do - say why in its reason. A functional spec with scenarios is not evaluated.",
|
|
166
|
+
"When a change's result or evaluate_tests says a functional spec is being evaluated, tell the person so in a line, and go on: nothing waits",
|
|
167
|
+
"for it. Its verdict comes back to this chat once you are idle:",
|
|
168
|
+
"- A yes is a message with its report: tell the person in a line, then write with change_plain what testing it needs - the test reqs, the",
|
|
169
|
+
" conformance testing script and its scenarios, in that order and only what is missing - as the message says, asking the person nothing.",
|
|
170
|
+
"- A no is a line turri draws itself: there is nothing for you to do.",
|
|
171
|
+
"When the person asks you for scenarios (\"write conformance scenarios for core:add-task\"), decide and write them yourself, with change_plain,",
|
|
172
|
+
"as a ***plain change they see, as the rules say - whatever test-advisor says.",
|
|
173
|
+
"</test advisor>",
|
|
174
|
+
].join("\n");
|
|
175
|
+
/** Each work loop's session's test advisor - the chat's and a host's loops have one. */
|
|
176
|
+
const advisorsOfLoops = new WeakMap();
|
|
177
|
+
/** The test advisor of the session a work loop is in. */
|
|
178
|
+
export function bindAdvisor(loop, advisor) {
|
|
179
|
+
advisorsOfLoops.set(loop, advisor);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* What a change's tool result tells the chat's agent of the test advisor: the functional specs it has taken up, so the
|
|
183
|
+
* agent tells the person they are being evaluated - nothing when it has taken up none.
|
|
184
|
+
*/
|
|
185
|
+
export function advisingNote(loop, plainFile) {
|
|
186
|
+
const taking = advisorsOfLoops.get(loop)?.()?.taking(projectDirOf(resolve(plainFile))) ?? [];
|
|
187
|
+
if (taking.length === 0)
|
|
188
|
+
return "";
|
|
189
|
+
return [
|
|
190
|
+
`My test advisor is evaluating ${taking.join(", ")} in the background, to see whether ${taking.length === 1 ? "it needs" : "they need"} explicit testing scenarios.`,
|
|
191
|
+
"Tell the person, in a line, that testing scenarios are being evaluated for them, and go on - there is nothing to wait for: the verdict",
|
|
192
|
+
"comes back to this chat.",
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
195
|
+
/** The functional specs evaluate_tests is given, each as the advisor is asked for it - or, not found, why. */
|
|
196
|
+
function namedSpecs(cwd, names, plainFile) {
|
|
197
|
+
const named = plainFile ?? readProjectConfig(cwd).module;
|
|
198
|
+
const base = named !== undefined ? resolve(cwd, named) : undefined;
|
|
199
|
+
if (base !== undefined)
|
|
200
|
+
refuseTestPlainFile(base);
|
|
201
|
+
if (base !== undefined && !existsSync(base))
|
|
202
|
+
throw new Error(`There is no .plain file at ${base}.`);
|
|
203
|
+
const chain = (() => {
|
|
204
|
+
try {
|
|
205
|
+
return base !== undefined ? resolveRequiresChain(base, [], undefined, { allowNoFunctionalities: true }) : [];
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
return names.map((given) => {
|
|
212
|
+
const name = given.trim();
|
|
213
|
+
const at = name.lastIndexOf(":");
|
|
214
|
+
if (at === -1 && base === undefined)
|
|
215
|
+
return { name, missing: `name it as module:func-id - there is no plain_file, and no module: in the project's config` };
|
|
216
|
+
const module = at === -1 ? basename(base, ".plain") : name.slice(0, at);
|
|
217
|
+
const funcId = at === -1 ? name : name.slice(at + 1);
|
|
218
|
+
const own = chain.find((link) => link.moduleName === module)?.plainFile ?? modulePlainFile(module, resolveTemplateDirs(base ?? join(cwd, `${module}.plain`)));
|
|
219
|
+
if (own === null || own === undefined || !existsSync(own))
|
|
220
|
+
return { name: `${module}:${funcId}`, missing: `there is no module ${module} here` };
|
|
221
|
+
// A module below the one named: its scenarios are tested in the one named, at the top of the chain.
|
|
222
|
+
const tip = base !== undefined && chain.some((link) => link.moduleName === module) && resolve(own) !== resolve(base) ? { tip: base } : {};
|
|
223
|
+
return { name: `${module}:${funcId}`, asked: { module, funcId, plainFile: resolve(own), ...tip } };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The test advisor asked to evaluate functional specs - `module:func-id`s, or func-ids of `plainFile`'s module (else the
|
|
228
|
+
* one the project's config names) - even a text it judged already, and whatever `test-advisor` says: `evaluate_tests`'s
|
|
229
|
+
* and a host's `evaluateTests`. It returns at once.
|
|
230
|
+
*/
|
|
231
|
+
export function evaluateTests(cwd, advisor, funcIds, options = {}) {
|
|
232
|
+
if (advisor === undefined)
|
|
233
|
+
throw new Error("This session has no test advisor: it runs in the chat and a host only.");
|
|
234
|
+
if (!conformanceTestsOnIn(cwd)) {
|
|
235
|
+
throw new Error("Conformance tests are off for this project (conformance-tests: false, or unset), and the test advisor with them. They are turned on in the chat - by asking for them - or with conformance-tests: true in config.turri.yaml.");
|
|
236
|
+
}
|
|
237
|
+
if (funcIds.length === 0)
|
|
238
|
+
throw new Error("Name the functional specs to evaluate, each as module:func-id.");
|
|
239
|
+
const said = { started: [], already: [], scenarios: [], missing: [] };
|
|
240
|
+
for (const spec of namedSpecs(cwd, funcIds, options.plainFile)) {
|
|
241
|
+
if (spec.asked === undefined) {
|
|
242
|
+
said.missing.push(`${spec.name}: ${spec.missing}`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const outcome = advisor.evaluate({ ...spec.asked, ...(options.reason !== undefined ? { reason: options.reason } : {}) });
|
|
246
|
+
if (outcome === "missing")
|
|
247
|
+
said.missing.push(`${spec.name}: its module has no such functional spec`);
|
|
248
|
+
else
|
|
249
|
+
said[outcome].push(spec.name);
|
|
250
|
+
}
|
|
251
|
+
return said;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* evaluate_tests: the chat's own way to the test advisor - a functional spec evaluated in the background, whatever
|
|
255
|
+
* `test-advisor` says, and even when its text was judged already. It returns at once; the verdict comes back to the chat
|
|
256
|
+
* as the advisor's verdicts do. In the chat and a host only: the session has an advisor.
|
|
257
|
+
*/
|
|
258
|
+
export function evaluateTestsTool(options) {
|
|
259
|
+
return defineTool({
|
|
260
|
+
name: "evaluate_tests",
|
|
261
|
+
label: "Evaluate tests",
|
|
262
|
+
description: "Have turri's test advisor evaluate, in the background, whether functional specs need explicit testing scenarios - even one it judged " +
|
|
263
|
+
"already, and whatever test-advisor says. It returns at once; the verdict comes back to this chat: a yes as a message to write the " +
|
|
264
|
+
"scenarios on, a no as a line. A functional spec that has scenarios is not evaluated.",
|
|
265
|
+
promptSnippet: "evaluate_tests: have the test advisor judge whether functional specs need testing scenarios, in the background",
|
|
266
|
+
promptGuidelines: [
|
|
267
|
+
"Call evaluate_tests when the person asks whether a functional spec needs tests (\"does add-task need tests?\"), rather than judging it yourself.",
|
|
268
|
+
"Call it on your own too, as another way of saying what a functional spec means: when pulses keep failing on a complicated functional spec, scenarios may pin down what it should do - say so in reason.",
|
|
269
|
+
"It returns at once: tell the person testing scenarios are being evaluated, and go on. On a yes, write the scenarios as the verdict's message says.",
|
|
270
|
+
],
|
|
271
|
+
parameters: Type.Object({
|
|
272
|
+
funcIds: Type.Array(Type.String(), { description: "The functional specs, each as module:func-id (core:add-task) - or a func-id alone, of plain_file's module" }),
|
|
273
|
+
plain_file: Type.Optional(Type.String({ description: "The .plain file a func-id alone is of, and whose chain the scenarios are tested in. Omitted: the module the project's config.turri.yaml names" })),
|
|
274
|
+
reason: Type.Optional(Type.String({ description: "Why you ask, for the advisor to weigh: \"the coding agent failed three times to implement it\"" })),
|
|
275
|
+
}),
|
|
276
|
+
async execute(_id, params) {
|
|
277
|
+
const said = evaluateTests(options.cwd, options.advisor(), params.funcIds, { ...(params.plain_file !== undefined ? { plainFile: params.plain_file } : {}), ...(params.reason !== undefined ? { reason: params.reason } : {}) });
|
|
278
|
+
const list = (names) => names.join(", ");
|
|
279
|
+
const one = (names, single, many) => (names.length === 1 ? single : many);
|
|
280
|
+
const text = [
|
|
281
|
+
...(said.started.length > 0 ? [`Testing scenarios for ${list(said.started)} are being evaluated in the background; the verdict comes back to this chat.`] : []),
|
|
282
|
+
...(said.already.length > 0 ? [`${list(said.already)} ${one(said.already, "is", "are")} being evaluated already; the verdict comes back to this chat.`] : []),
|
|
283
|
+
...(said.scenarios.length > 0 ? [`${list(said.scenarios)} ${one(said.scenarios, "has", "have")} scenarios already, so ${one(said.scenarios, "it is", "they are")} not evaluated: tested as the scenarios say.`] : []),
|
|
284
|
+
...(said.missing.length > 0 ? [`Not evaluated - ${said.missing.join("; ")}.`] : []),
|
|
285
|
+
...(said.started.length + said.already.length > 0 ? ["Tell the person so in a line, and go on: there is nothing to wait for."] : []),
|
|
286
|
+
].join("\n");
|
|
287
|
+
return { content: [{ type: "text", text }], details: said };
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ChatLink } from "./chats.ts";
|
|
3
|
+
import type { ScriptKind } from "./regenerate/test-scripts.ts";
|
|
4
|
+
/**
|
|
5
|
+
* A skill that runs in the background (`metadata.background: true`, docs/skills-and-gates.md): in the chat, cultivate
|
|
6
|
+
* starts it and returns at once, nothing shows while it runs, and nobody is asked anything. Its end comes back to the
|
|
7
|
+
* chat it was started from once that chat is idle - never interrupting it: one line drawn by turri when it did its work,
|
|
8
|
+
* a message that starts a turn when it did not, for the chat to tell the person and decide what next.
|
|
9
|
+
*/
|
|
10
|
+
/** How a background run ended, for the chat it was started from. */
|
|
11
|
+
export interface SkillRunEnd {
|
|
12
|
+
skill: string;
|
|
13
|
+
/** It did its work: a line, starting no turn. */
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** The line it is drawn as. */
|
|
16
|
+
line: string;
|
|
17
|
+
/** When it did not do its work: what the chat's agent is told, in a turn of its own. */
|
|
18
|
+
task?: string;
|
|
19
|
+
/** The testing script it writes, when it writes one. */
|
|
20
|
+
kind?: ScriptKind;
|
|
21
|
+
/** The module it is for, relative to the project, when it was given one. */
|
|
22
|
+
plainFile?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What a host is told of a background run, besides its skill: the testing script it writes and the module it is for,
|
|
26
|
+
* each when there is one - in cultivate's result as it starts, and in the message a run that did not do its work sends.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runDetails(kind: ScriptKind | undefined, plainFile: string | undefined): {
|
|
29
|
+
kind?: ScriptKind;
|
|
30
|
+
plainFile?: string;
|
|
31
|
+
};
|
|
32
|
+
/** What the agent of a background run is told, besides what the chat passed on. */
|
|
33
|
+
export declare const BACKGROUND_NOTE: string;
|
|
34
|
+
/** What cultivate says as it starts a background run. */
|
|
35
|
+
export declare function startedText(skill: string, options: {
|
|
36
|
+
model?: string;
|
|
37
|
+
plainFile?: string;
|
|
38
|
+
kind?: ScriptKind;
|
|
39
|
+
}): string;
|
|
40
|
+
/**
|
|
41
|
+
* What a background run changed of the files that changed in the project while it ran: the chat goes on meanwhile, and
|
|
42
|
+
* what it writes - its ***plain, its test plain files - is not the run's. A run that writes a testing script changes
|
|
43
|
+
* the script, a prepare script beside a conformance one, and a config file naming them; any other run, all of them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ownChanges(changed: string[], kind?: ScriptKind): string[];
|
|
46
|
+
/** The line a background run that did its work is drawn as. */
|
|
47
|
+
export declare function doneLine(skill: string, changed: string[], kind?: ScriptKind): string;
|
|
48
|
+
/** What the chat's agent is told when a background run did not do its work: why, and what is left to it. */
|
|
49
|
+
export declare function failedTask(skill: string, why: string, output: string | undefined, kind?: ScriptKind): string;
|
|
50
|
+
/** A chat's background runs' ends: held until it is idle - its agent done, nothing typed - then told together. */
|
|
51
|
+
export declare function skillRunDelivery(pi: ExtensionAPI, link: ChatLink, context: () => ExtensionContext | undefined): {
|
|
52
|
+
put(end: SkillRunEnd): void;
|
|
53
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { chatBusy } from "./chat-idle.js";
|
|
3
|
+
/**
|
|
4
|
+
* What a host is told of a background run, besides its skill: the testing script it writes and the module it is for,
|
|
5
|
+
* each when there is one - in cultivate's result as it starts, and in the message a run that did not do its work sends.
|
|
6
|
+
*/
|
|
7
|
+
export function runDetails(kind, plainFile) {
|
|
8
|
+
return { ...(kind !== undefined ? { kind } : {}), ...(plainFile !== undefined ? { plainFile } : {}) };
|
|
9
|
+
}
|
|
10
|
+
/** What the agent of a background run is told, besides what the chat passed on. */
|
|
11
|
+
export const BACKGROUND_NOTE = "You run in the background: nobody can be asked anything, and nobody watches. Where the module's stack, its ***plain and the project " +
|
|
12
|
+
"leave something open, decide it - the reading most consistent with them - and say what you decided in your result. If what you are " +
|
|
13
|
+
"to make cannot be told at all - no language, no test framework to go by - write nothing, and say why.";
|
|
14
|
+
const WHAT = { unit: "unit testing script", conformance: "conformance testing script" };
|
|
15
|
+
/** What cultivate says as it starts a background run. */
|
|
16
|
+
export function startedText(skill, options) {
|
|
17
|
+
const what = options.kind !== undefined ? `the ${WHAT[options.kind]}${options.plainFile !== undefined ? ` for ${options.plainFile}` : ""}` : "its work";
|
|
18
|
+
return [
|
|
19
|
+
`${skill} is writing ${what} in the background${options.model !== undefined ? `, on ${options.model}` : ""}. What it did comes back to this chat when it ends.`,
|
|
20
|
+
"Do not wait for it, and do not tell the person to: go on with the turn.",
|
|
21
|
+
...(options.kind !== undefined
|
|
22
|
+
? [
|
|
23
|
+
options.kind === "unit"
|
|
24
|
+
? "Regenerate now if there is more to do: a module's pulses wait for its unit testing script while it is written, and start once it is."
|
|
25
|
+
: "Write the scenarios now if they are next: the module's conformance tests wait for the script while it is written, and are written once it is.",
|
|
26
|
+
]
|
|
27
|
+
: []),
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What a background run changed of the files that changed in the project while it ran: the chat goes on meanwhile, and
|
|
32
|
+
* what it writes - its ***plain, its test plain files - is not the run's. A run that writes a testing script changes
|
|
33
|
+
* the script, a prepare script beside a conformance one, and a config file naming them; any other run, all of them.
|
|
34
|
+
*/
|
|
35
|
+
export function ownChanges(changed, kind) {
|
|
36
|
+
if (kind === undefined)
|
|
37
|
+
return changed;
|
|
38
|
+
const scripts = kind === "unit" ? ["run_unittests"] : ["run_conformance_tests", "prepare_environment"];
|
|
39
|
+
const name = (path) => path.split("/").at(-1) ?? path;
|
|
40
|
+
return changed.filter((path) => scripts.some((script) => name(path).startsWith(script)) || /^config(\.turri)?(\.[^.]+)?\.yaml$/.test(name(path)));
|
|
41
|
+
}
|
|
42
|
+
/** The line a background run that did its work is drawn as. */
|
|
43
|
+
export function doneLine(skill, changed, kind) {
|
|
44
|
+
const files = changed.join(", ");
|
|
45
|
+
if (kind === undefined)
|
|
46
|
+
return `${skill} finished in the background: ${files}.`;
|
|
47
|
+
const what = WHAT[kind];
|
|
48
|
+
return `${what.charAt(0).toUpperCase()}${what.slice(1)} written: ${files}.`;
|
|
49
|
+
}
|
|
50
|
+
/** What the chat's agent is told when a background run did not do its work: why, and what is left to it. */
|
|
51
|
+
export function failedTask(skill, why, output, kind) {
|
|
52
|
+
return [
|
|
53
|
+
`${skill}, which you started in the background, ${why}.`,
|
|
54
|
+
"",
|
|
55
|
+
...(output !== undefined && output.trim() !== "" ? ["<artifact>", output.trim(), "</artifact>", ""] : []),
|
|
56
|
+
"Tell the person in a line or two what went wrong" +
|
|
57
|
+
(kind === undefined
|
|
58
|
+
? "."
|
|
59
|
+
: kind === "unit"
|
|
60
|
+
? ", and that the module's pulses cannot be built until it has a unit testing script."
|
|
61
|
+
: ", and that the module's conformance tests cannot be written until it has a conformance testing script."),
|
|
62
|
+
"Start it again with cultivate only when you can change what went wrong - the context you pass it, the module it is for; otherwise say what the person can do. Ask the person nothing.",
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
/** The line drawn for a background run, in the transcript and in no model's context. */
|
|
66
|
+
const SKILL_RUN_ENTRY = "turri_skill_run";
|
|
67
|
+
/** How long an end waits before the chat is looked at again, while the person types or it cannot be told yet. */
|
|
68
|
+
const RECHECK_MS = 2000;
|
|
69
|
+
/** A chat's background runs' ends: held until it is idle - its agent done, nothing typed - then told together. */
|
|
70
|
+
export function skillRunDelivery(pi, link, context) {
|
|
71
|
+
const waiting = [];
|
|
72
|
+
let timer;
|
|
73
|
+
pi.registerEntryRenderer(SKILL_RUN_ENTRY, (entry, _options, theme) => {
|
|
74
|
+
const data = entry.data;
|
|
75
|
+
if (data === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const box = new Box(1, 0);
|
|
78
|
+
box.addChild(new Text(theme.fg("muted", data.text), 0, 0));
|
|
79
|
+
return box;
|
|
80
|
+
});
|
|
81
|
+
const later = (ms) => {
|
|
82
|
+
if (timer !== undefined)
|
|
83
|
+
return;
|
|
84
|
+
timer = setTimeout(() => {
|
|
85
|
+
timer = undefined;
|
|
86
|
+
deliver();
|
|
87
|
+
}, ms);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
};
|
|
90
|
+
const deliver = () => {
|
|
91
|
+
if (waiting.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
if (chatBusy(link, context)) {
|
|
94
|
+
later(RECHECK_MS);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const now = waiting.splice(0);
|
|
98
|
+
const failed = now.filter((end) => !end.ok && end.task !== undefined);
|
|
99
|
+
try {
|
|
100
|
+
for (const end of now.filter((end) => end.ok))
|
|
101
|
+
pi.appendEntry(SKILL_RUN_ENTRY, { text: end.line });
|
|
102
|
+
if (failed.length > 0) {
|
|
103
|
+
// A follow-up, never a steer: should a turn start in between, this waits for it to end.
|
|
104
|
+
void pi.sendMessage({ customType: SKILL_RUN_ENTRY, content: failed.map((end) => end.task).join("\n\n---\n\n"), display: false, details: { skills: failed.map((end) => end.skill), runs: failed.map((end) => ({ skill: end.skill, ...runDetails(end.kind, end.plainFile) })) } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Not bound to its session yet: kept, and tried again.
|
|
109
|
+
waiting.unshift(...now);
|
|
110
|
+
later(RECHECK_MS);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
pi.on("agent_settled", () => deliver());
|
|
114
|
+
pi.on("session_start", () => later(0));
|
|
115
|
+
return {
|
|
116
|
+
put(end) {
|
|
117
|
+
waiting.push(end);
|
|
118
|
+
later(0);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AWS region turri's Bedrock models are called in. pi calls Bedrock in AWS_REGION, else AWS_DEFAULT_REGION, else -
|
|
3
|
+
* with an AWS_PROFILE - the profile's region, else the region of the model's endpoint in its catalogue, us-east-1
|
|
4
|
+
* (pi-ai's bedrock-converse-stream.js). A model served in a geography that needs a region of its own - eu, jp, au, apac,
|
|
5
|
+
* in, ca, us-gov - answers only when it is called in a region of that geography, so turri sets AWS_REGION itself, in
|
|
6
|
+
* its own process, before any model is called: the terminal's chat, every command and a host's session alike. A region
|
|
7
|
+
* the person set is never touched. The environment is the lever: pi's per-call `region` is not reachable from the
|
|
8
|
+
* sessions pi makes itself - the chat's - so one region serves every model, and models that need two are said, not
|
|
9
|
+
* guessed between.
|
|
10
|
+
*/
|
|
11
|
+
/** Where the region pi calls Bedrock in comes from. */
|
|
12
|
+
export type BedrockRegionSource =
|
|
13
|
+
/** AWS_REGION or AWS_DEFAULT_REGION, set by the person. */
|
|
14
|
+
"set"
|
|
15
|
+
/** AWS_REGION, set by turri for the geography the models need. */
|
|
16
|
+
| "turri"
|
|
17
|
+
/** The region of the AWS_PROFILE's profile. */
|
|
18
|
+
| "profile"
|
|
19
|
+
/** Nothing names one: pi's default, us-east-1. */
|
|
20
|
+
| "default";
|
|
21
|
+
/** What turri does about the AWS region a project's Bedrock models are called in. */
|
|
22
|
+
export interface BedrockRegion {
|
|
23
|
+
/** The region pi calls Bedrock in - undefined when the models need two regions and nothing names one, or the project uses no Bedrock model. */
|
|
24
|
+
region?: string;
|
|
25
|
+
source: BedrockRegionSource;
|
|
26
|
+
/** The Bedrock models the project uses, each once. */
|
|
27
|
+
models: string[];
|
|
28
|
+
/** The region the models need, and its geography - when they need one, and one only. */
|
|
29
|
+
needs?: {
|
|
30
|
+
region: string;
|
|
31
|
+
geography: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The models need more than one region: one AWS_REGION serves every model, so turri sets none, and says which model
|
|
35
|
+
* needs which.
|
|
36
|
+
*/
|
|
37
|
+
conflict?: Array<{
|
|
38
|
+
region: string;
|
|
39
|
+
geography: string;
|
|
40
|
+
models: string[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** The variable turri marks its own AWS_REGION with, so the pi it starts - and a reload - tells it from the person's. */
|
|
44
|
+
export declare const TURRI_AWS_REGION = "TURRI_AWS_REGION";
|
|
45
|
+
/** Whether a region is in a geography: one of the regions Bedrock serves that geography's inference profiles from. */
|
|
46
|
+
export declare function regionInGeography(region: string, geography: string): boolean;
|
|
47
|
+
/** The region an AWS profile names, from AWS_CONFIG_FILE or ~/.aws/config - undefined when it names none, or the file cannot be read. */
|
|
48
|
+
export declare function profileRegion(profile: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* What turri does about the AWS region for a project, from the environment as it is - reading it, setting nothing.
|
|
51
|
+
* `also`: models used besides the config's - named on the command line, chosen in this session.
|
|
52
|
+
*/
|
|
53
|
+
export declare function bedrockRegionFor(projectDir: string, options?: {
|
|
54
|
+
also?: string[];
|
|
55
|
+
env?: NodeJS.ProcessEnv;
|
|
56
|
+
}): BedrockRegion;
|
|
57
|
+
/**
|
|
58
|
+
* Set AWS_REGION for a project's Bedrock models, as `bedrockRegionFor` says: when they need a region and the person
|
|
59
|
+
* set none. Done again when the models change, it moves or takes back what turri set before, never the person's.
|
|
60
|
+
*/
|
|
61
|
+
export declare function applyBedrockRegion(projectDir: string, options?: {
|
|
62
|
+
also?: string[];
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
}): BedrockRegion;
|
|
65
|
+
/** A line on the region, for a model check: what turri set and how to override it - or the conflict it would not guess. */
|
|
66
|
+
export declare function describeBedrockRegion(decided: BedrockRegion): string | undefined;
|