@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc
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/.pi/themes/turri-dark.json +80 -0
- package/.pi/themes/turri-light.json +82 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +252 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
- package/bin/turri.mjs +29 -0
- package/dist/ask.js +149 -0
- package/dist/brand.js +50 -0
- package/dist/btw.js +174 -0
- package/dist/chat-models.js +37 -0
- package/dist/chat-skills.js +18 -0
- package/dist/cli.js +178 -0
- package/dist/config-command.js +197 -0
- package/dist/config-edit.js +367 -0
- package/dist/config.js +489 -0
- package/dist/conform-cli.js +29 -0
- package/dist/cultivate-cli.js +110 -0
- package/dist/drift-cli.js +25 -0
- package/dist/environment.js +70 -0
- package/dist/extension.js +2372 -0
- package/dist/genome.js +301 -0
- package/dist/graph.js +246 -0
- package/dist/identity.js +35 -0
- package/dist/init-config.js +511 -0
- package/dist/judge.js +62 -0
- package/dist/lineage-cli.js +13 -0
- package/dist/loop-cli.js +132 -0
- package/dist/loop-tools.js +225 -0
- package/dist/mature-tools.js +193 -0
- package/dist/model-check.js +117 -0
- package/dist/module-report.js +9 -0
- package/dist/pi-launch.js +31 -0
- package/dist/pi.js +813 -0
- package/dist/plain/check.js +76 -0
- package/dist/plain/concepts.js +213 -0
- package/dist/plain/errors.js +43 -0
- package/dist/plain/files.js +57 -0
- package/dist/plain/highlight.js +195 -0
- package/dist/plain/index.js +15 -0
- package/dist/plain/liquid.js +129 -0
- package/dist/plain/markdown.js +208 -0
- package/dist/plain/parser.js +752 -0
- package/dist/plain/plain.tmLanguage.json +124 -0
- package/dist/plain/spec.js +291 -0
- package/dist/plain/stdlib.js +44 -0
- package/dist/question-dialog.js +125 -0
- package/dist/questioning.js +109 -0
- package/dist/quiet.js +125 -0
- package/dist/regenerate/activity.js +187 -0
- package/dist/regenerate/agent-env.js +190 -0
- package/dist/regenerate/agent-shell.js +114 -0
- package/dist/regenerate/arbiter.js +133 -0
- package/dist/regenerate/audit.js +203 -0
- package/dist/regenerate/cases.js +83 -0
- package/dist/regenerate/change-view.js +102 -0
- package/dist/regenerate/choose-conformance.js +117 -0
- package/dist/regenerate/chunk-plan.js +625 -0
- package/dist/regenerate/codebase.js +75 -0
- package/dist/regenerate/command-check.js +312 -0
- package/dist/regenerate/compaction.js +20 -0
- package/dist/regenerate/confine.js +310 -0
- package/dist/regenerate/conform.js +133 -0
- package/dist/regenerate/conformance-agents.js +955 -0
- package/dist/regenerate/conformance.js +936 -0
- package/dist/regenerate/conversation-writes.js +100 -0
- package/dist/regenerate/credentials.js +88 -0
- package/dist/regenerate/debug-log.js +228 -0
- package/dist/regenerate/debugger.js +90 -0
- package/dist/regenerate/delivery.js +19 -0
- package/dist/regenerate/diagnose-job.js +40 -0
- package/dist/regenerate/diagnose.js +81 -0
- package/dist/regenerate/diff-view.js +240 -0
- package/dist/regenerate/drift.js +194 -0
- package/dist/regenerate/engine.js +118 -0
- package/dist/regenerate/environment-error.js +220 -0
- package/dist/regenerate/job-log.js +102 -0
- package/dist/regenerate/job-panel.js +336 -0
- package/dist/regenerate/jobs-tree.js +300 -0
- package/dist/regenerate/jobs.js +427 -0
- package/dist/regenerate/lanes.js +32 -0
- package/dist/regenerate/lineage.js +384 -0
- package/dist/regenerate/loop.js +1435 -0
- package/dist/regenerate/masking.js +95 -0
- package/dist/regenerate/notes.js +397 -0
- package/dist/regenerate/os-sandbox.js +243 -0
- package/dist/regenerate/paths.js +165 -0
- package/dist/regenerate/plain-palette.js +83 -0
- package/dist/regenerate/prompt.js +704 -0
- package/dist/regenerate/records.js +294 -0
- package/dist/regenerate/regenerate.js +598 -0
- package/dist/regenerate/regression.js +181 -0
- package/dist/regenerate/release.js +262 -0
- package/dist/regenerate/requires.js +241 -0
- package/dist/regenerate/resources.js +164 -0
- package/dist/regenerate/self-check.js +111 -0
- package/dist/regenerate/services.js +155 -0
- package/dist/regenerate/shell.js +320 -0
- package/dist/regenerate/side-panel.js +278 -0
- package/dist/regenerate/spec-map-view.js +574 -0
- package/dist/regenerate/spec-map.js +561 -0
- package/dist/regenerate/spec-steps.js +527 -0
- package/dist/regenerate/state.js +10 -0
- package/dist/regenerate/steer.js +91 -0
- package/dist/regenerate/steering.js +119 -0
- package/dist/regenerate/stop-report.js +161 -0
- package/dist/regenerate/submit-work.js +71 -0
- package/dist/regenerate/test-kinds.js +124 -0
- package/dist/regenerate/test-scripts.js +695 -0
- package/dist/regenerate/testing.js +406 -0
- package/dist/regenerate/transcript-view.js +1 -0
- package/dist/regenerate/unit-gate.js +20 -0
- package/dist/regenerate/unit-review.js +347 -0
- package/dist/regenerate/web.js +256 -0
- package/dist/regenerate/workspace.js +99 -0
- package/dist/regenerate-cli.js +68 -0
- package/dist/release-agent.js +114 -0
- package/dist/release-cli.js +131 -0
- package/dist/release-tools.js +148 -0
- package/dist/review/live.js +165 -0
- package/dist/rules.js +47 -0
- package/dist/self-invocation.js +110 -0
- package/dist/setup.js +245 -0
- package/dist/skill-progress.js +78 -0
- package/dist/skill.js +149 -0
- package/dist/skills.js +74 -0
- package/dist/spec-diff-cli.js +37 -0
- package/dist/spec-tools.js +155 -0
- package/dist/stack.js +158 -0
- package/dist/stacks.js +97 -0
- package/dist/stdlib.js +100 -0
- package/dist/terminal-images.js +46 -0
- package/dist/tools.js +242 -0
- package/dist/validate-cli.js +52 -0
- package/docs/README.md +58 -0
- package/docs/authoring-plain.md +245 -0
- package/docs/config.md +353 -0
- package/docs/deploy.md +109 -0
- package/docs/development.md +88 -0
- package/docs/headless.md +148 -0
- package/docs/images/how-turri-works.svg +50 -0
- package/docs/images/turri-banner.png +0 -0
- package/docs/installation.md +119 -0
- package/docs/parser.md +108 -0
- package/docs/regenerating.md +1116 -0
- package/docs/sandbox.md +233 -0
- package/docs/skills-and-gates.md +254 -0
- package/docs/theming.md +76 -0
- package/docs/using.md +454 -0
- package/genome/rules/bullet-continuation.md +69 -0
- package/genome/rules/concise-specs-examples.md +431 -0
- package/genome/rules/concise-specs-integration-examples.md +270 -0
- package/genome/rules/concise-specs.md +80 -0
- package/genome/rules/definitions.md +107 -0
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
- package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
- package/genome/rules/exported-concepts.md +39 -0
- package/genome/rules/func-specs.md +129 -0
- package/genome/rules/impl-reqs.md +68 -0
- package/genome/rules/import-modules.md +48 -0
- package/genome/rules/integration-embedded-testing.md +276 -0
- package/genome/rules/integration-embedded.md +171 -0
- package/genome/rules/integration-standalone.md +135 -0
- package/genome/rules/integrations.md +137 -0
- package/genome/rules/linked-resources.md +69 -0
- package/genome/rules/module-structure.md +105 -0
- package/genome/rules/project-model.md +88 -0
- package/genome/rules/required-concepts.md +44 -0
- package/genome/rules/requires-modules.md +80 -0
- package/genome/rules/stdlib.md +73 -0
- package/genome/rules/test-reqs.md +57 -0
- package/genome/skills/add-acceptance-test/SKILL.md +100 -0
- package/genome/skills/add-concept/SKILL.md +71 -0
- package/genome/skills/add-functional-spec/SKILL.md +84 -0
- package/genome/skills/add-functional-specs/SKILL.md +118 -0
- package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
- package/genome/skills/add-resource/SKILL.md +105 -0
- package/genome/skills/add-template/SKILL.md +64 -0
- package/genome/skills/add-test-requirement/SKILL.md +73 -0
- package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
- package/genome/skills/analyze-func-specs/SKILL.md +123 -0
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
- package/genome/skills/break-down-func-spec/SKILL.md +205 -0
- package/genome/skills/brood/SKILL.md +229 -0
- package/genome/skills/brood/references/intent.md +35 -0
- package/genome/skills/brood/references/product.md +101 -0
- package/genome/skills/brood/references/stack.md +61 -0
- package/genome/skills/brood/references/testing.md +56 -0
- package/genome/skills/bud/SKILL.md +148 -0
- package/genome/skills/bud/references/checklist.md +44 -0
- package/genome/skills/bud/references/intent.md +30 -0
- package/genome/skills/check-plain-env/SKILL.md +285 -0
- package/genome/skills/consolidate-concepts/SKILL.md +192 -0
- package/genome/skills/create-import-module/SKILL.md +104 -0
- package/genome/skills/create-requires-module/SKILL.md +109 -0
- package/genome/skills/debug-specs/SKILL.md +188 -0
- package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
- package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
- package/genome/skills/init-plain-project/SKILL.md +117 -0
- package/genome/skills/init-plain-project/references/checklist.md +30 -0
- package/genome/skills/mature/SKILL.md +126 -0
- package/genome/skills/refactor-module/SKILL.md +196 -0
- package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
- package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
- package/genome/skills/spawn/SKILL.md +163 -0
- package/package.json +68 -4
- package/stdlib/backend/common.plain +25 -0
- package/stdlib/backend/conformance/common.plain +12 -0
- package/stdlib/backend/conformance/go-gin.plain +9 -0
- package/stdlib/backend/conformance/go-nethttp.plain +9 -0
- package/stdlib/backend/conformance/java-springboot.plain +9 -0
- package/stdlib/backend/conformance/python-django.plain +9 -0
- package/stdlib/backend/conformance/python-fastapi.plain +9 -0
- package/stdlib/backend/conformance/python-flask.plain +9 -0
- package/stdlib/backend/conformance/rust-axum.plain +9 -0
- package/stdlib/backend/conformance/typescript-express.plain +9 -0
- package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
- package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
- package/stdlib/backend/go-gin.plain +18 -0
- package/stdlib/backend/go-nethttp.plain +18 -0
- package/stdlib/backend/java-springboot.plain +19 -0
- package/stdlib/backend/python-django.plain +19 -0
- package/stdlib/backend/python-fastapi.plain +23 -0
- package/stdlib/backend/python-flask.plain +23 -0
- package/stdlib/backend/rust-axum.plain +17 -0
- package/stdlib/backend/typescript-express.plain +19 -0
- package/stdlib/backend/typescript-fastify.plain +19 -0
- package/stdlib/backend/typescript-nestjs.plain +19 -0
- package/stdlib/cli/common.plain +17 -0
- package/stdlib/cli/conformance/common.plain +11 -0
- package/stdlib/cli/conformance/go-cobra.plain +9 -0
- package/stdlib/cli/conformance/go-flag.plain +9 -0
- package/stdlib/cli/conformance/java-picocli.plain +9 -0
- package/stdlib/cli/conformance/python-argparse.plain +9 -0
- package/stdlib/cli/conformance/python-click.plain +9 -0
- package/stdlib/cli/conformance/python-typer.plain +9 -0
- package/stdlib/cli/conformance/rust-clap.plain +9 -0
- package/stdlib/cli/conformance/typescript-commander.plain +9 -0
- package/stdlib/cli/go-cobra.plain +14 -0
- package/stdlib/cli/go-flag.plain +14 -0
- package/stdlib/cli/java-picocli.plain +15 -0
- package/stdlib/cli/python-argparse.plain +18 -0
- package/stdlib/cli/python-click.plain +16 -0
- package/stdlib/cli/python-typer.plain +16 -0
- package/stdlib/cli/rust-clap.plain +14 -0
- package/stdlib/cli/typescript-commander.plain +17 -0
- package/stdlib/frontend/angular.plain +17 -0
- package/stdlib/frontend/common.plain +17 -0
- package/stdlib/frontend/conformance/angular.plain +5 -0
- package/stdlib/frontend/conformance/common.plain +11 -0
- package/stdlib/frontend/conformance/react-vite.plain +5 -0
- package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
- package/stdlib/frontend/conformance/vue-vite.plain +5 -0
- package/stdlib/frontend/react-vite.plain +16 -0
- package/stdlib/frontend/sveltekit-spa.plain +16 -0
- package/stdlib/frontend/vue-vite.plain +16 -0
- package/stdlib/lang/go.plain +13 -0
- package/stdlib/lang/java.plain +12 -0
- package/stdlib/lang/python.plain +18 -0
- package/stdlib/lang/rust.plain +12 -0
- package/stdlib/lang/typescript.plain +13 -0
- package/stdlib/library/common.plain +15 -0
- package/stdlib/library/conformance/common.plain +9 -0
- package/stdlib/library/conformance/go.plain +9 -0
- package/stdlib/library/conformance/java-gradle.plain +9 -0
- package/stdlib/library/conformance/java-maven.plain +9 -0
- package/stdlib/library/conformance/python.plain +9 -0
- package/stdlib/library/conformance/rust.plain +9 -0
- package/stdlib/library/conformance/typescript.plain +9 -0
- package/stdlib/library/go.plain +12 -0
- package/stdlib/library/java-gradle.plain +13 -0
- package/stdlib/library/java-maven.plain +13 -0
- package/stdlib/library/python.plain +13 -0
- package/stdlib/library/rust.plain +12 -0
- package/stdlib/library/typescript.plain +15 -0
package/dist/stacks.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing a stack: `config.turri.<stack>.yaml` laid over `config.turri.yaml` for everything built from then on - a
|
|
3
|
+
* backend's testing scripts, then a frontend's. `/stack` and the `stack` tool in the chat, `--stack <name>` on the
|
|
4
|
+
* command line (cli.ts). The choice itself is kept in config.ts (`chooseStack`), for the session only.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import { chooseStack, chosenStack, hasProjectConfig, stackConfigFile, stacksOf } from "./config.js";
|
|
11
|
+
/** The words that choose no stack: the project's config alone. */
|
|
12
|
+
const NONE = ["none", "off", "base", "-"];
|
|
13
|
+
/** The stacks of the projects here: the folder's own, and those of the projects one folder down (specs kept in plain/). */
|
|
14
|
+
export function knownStacks(root) {
|
|
15
|
+
const below = (() => {
|
|
16
|
+
try {
|
|
17
|
+
return readdirSync(root, { withFileTypes: true })
|
|
18
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "plain_modules")
|
|
19
|
+
.map((entry) => join(root, entry.name))
|
|
20
|
+
.filter(hasProjectConfig);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
return [...new Set([root, ...below].flatMap(stacksOf))].sort();
|
|
27
|
+
}
|
|
28
|
+
/** Choose `name` - or none - among the stacks here, and say what is chosen now; a stack the project has no file for is refused. */
|
|
29
|
+
export function switchStack(root, name) {
|
|
30
|
+
const stacks = knownStacks(root);
|
|
31
|
+
const wanted = name?.trim() ?? "";
|
|
32
|
+
if (wanted === "" || NONE.includes(wanted.toLowerCase())) {
|
|
33
|
+
chooseStack(undefined);
|
|
34
|
+
return "No stack chosen: everything is built with the project's config.turri.yaml alone, from now on.";
|
|
35
|
+
}
|
|
36
|
+
if (!stacks.includes(wanted)) {
|
|
37
|
+
throw new Error(stacks.length > 0
|
|
38
|
+
? `There is no stack ${wanted} here. The stacks: ${stacks.join(", ")} - or none.`
|
|
39
|
+
: `This project has no stacks: a stack is a ${stackConfigFile("<stack>")} beside config.turri.yaml.`);
|
|
40
|
+
}
|
|
41
|
+
chooseStack(wanted);
|
|
42
|
+
return `Stack ${wanted} chosen: everything is built with ${stackConfigFile(wanted)} over config.turri.yaml from now on - a pulse already running keeps the stack it started with.`;
|
|
43
|
+
}
|
|
44
|
+
/** /stack and the chat's stack tool. */
|
|
45
|
+
export function registerStacks(pi, cwd) {
|
|
46
|
+
pi.registerCommand("stack", {
|
|
47
|
+
description: "Choose the stack everything is built with - its config.turri.<stack>.yaml over config.turri.yaml: /stack [name | none]",
|
|
48
|
+
getArgumentCompletions: (prefix) => {
|
|
49
|
+
const items = [...knownStacks(cwd), "none"].filter((stack) => stack.startsWith(prefix)).map((stack) => ({ value: stack, label: stack }));
|
|
50
|
+
return items.length > 0 ? items : null;
|
|
51
|
+
},
|
|
52
|
+
handler: async (args, ctx) => {
|
|
53
|
+
let name = args.trim();
|
|
54
|
+
if (name === "") {
|
|
55
|
+
const stacks = knownStacks(cwd);
|
|
56
|
+
if (stacks.length === 0)
|
|
57
|
+
return ctx.ui.notify(`This project has no stacks: a stack is a ${stackConfigFile("<stack>")} beside config.turri.yaml.`, "info");
|
|
58
|
+
const now = chosenStack();
|
|
59
|
+
const label = (stack) => `${stack}${stack === now ? " (chosen now)" : ""}`;
|
|
60
|
+
const none = `none - config.turri.yaml alone${now === undefined ? " (chosen now)" : ""}`;
|
|
61
|
+
const picked = await ctx.ui.select("Build with which stack?", [...stacks.map(label), none]);
|
|
62
|
+
if (picked === undefined)
|
|
63
|
+
return;
|
|
64
|
+
name = picked === none ? "none" : (stacks.find((stack) => label(stack) === picked) ?? "");
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
ctx.ui.notify(switchStack(cwd, name), "info");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
pi.registerTool(defineTool({
|
|
75
|
+
name: "stack",
|
|
76
|
+
label: "Stack",
|
|
77
|
+
description: "Choose the stack everything is built with from now on - the project's config.turri.yaml with the stack's " +
|
|
78
|
+
"config.turri.<stack>.yaml over it: its testing scripts, toolchains, models - or none, the project's file alone. " +
|
|
79
|
+
"For the session; a pulse already running keeps the stack it started with. Without a name, says the stacks and the one chosen.",
|
|
80
|
+
promptSnippet: "choose the stack - its testing scripts and settings - that everything is built with",
|
|
81
|
+
promptGuidelines: [
|
|
82
|
+
"Switch the stack only when the user asks for it, or for work on a part of the project another stack builds - a backend's, a frontend's - and say which you chose.",
|
|
83
|
+
],
|
|
84
|
+
parameters: Type.Object({
|
|
85
|
+
name: Type.Optional(Type.String({ description: "The stack to choose, or none. Omitted: the stacks there are, and the one chosen" })),
|
|
86
|
+
}),
|
|
87
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
88
|
+
if (params.name === undefined) {
|
|
89
|
+
const stacks = knownStacks(cwd);
|
|
90
|
+
const text = stacks.length > 0 ? `The stacks: ${stacks.join(", ")}. Chosen now: ${chosenStack() ?? "none"}.` : "This project has no stacks.";
|
|
91
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
92
|
+
}
|
|
93
|
+
const text = switchStack(cwd, params.name);
|
|
94
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
}
|
package/dist/stdlib.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { STDLIB_DIR } from "./regenerate/paths.js";
|
|
4
|
+
export function stdlibTemplates(dir = STDLIB_DIR) {
|
|
5
|
+
const found = [];
|
|
6
|
+
const walk = (at) => {
|
|
7
|
+
let entries;
|
|
8
|
+
try {
|
|
9
|
+
entries = readdirSync(at, { withFileTypes: true });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.isDirectory())
|
|
16
|
+
walk(join(at, entry.name));
|
|
17
|
+
else if (entry.name.endsWith(".plain"))
|
|
18
|
+
found.push(join(at, entry.name));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
walk(dir);
|
|
22
|
+
return found
|
|
23
|
+
.map((file) => {
|
|
24
|
+
const text = readFileSync(file, "utf8").trim();
|
|
25
|
+
return {
|
|
26
|
+
name: `stdlib/${relative(dir, file).split(sep).join("/").replace(/\.plain$/, "")}`,
|
|
27
|
+
description: /^description:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "",
|
|
28
|
+
text,
|
|
29
|
+
};
|
|
30
|
+
})
|
|
31
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
32
|
+
}
|
|
33
|
+
/** A stack to import - not a kind's `common`, not a conformance template, and not a language's. */
|
|
34
|
+
export function isStack(template) {
|
|
35
|
+
return !template.name.endsWith("/common") && !template.name.includes("/conformance/") && !template.name.startsWith("stdlib/lang/");
|
|
36
|
+
}
|
|
37
|
+
/** A stack's conformance template: `stdlib/backend/python-fastapi` has `stdlib/backend/conformance/python-fastapi`. */
|
|
38
|
+
export function conformanceOf(stack) {
|
|
39
|
+
const slash = stack.lastIndexOf("/");
|
|
40
|
+
return `${stack.slice(0, slash)}/conformance/${stack.slice(slash + 1)}`;
|
|
41
|
+
}
|
|
42
|
+
const KINDS = [
|
|
43
|
+
{ kind: "backend", title: "Backends" },
|
|
44
|
+
{ kind: "frontend", title: "Frontends" },
|
|
45
|
+
{ kind: "cli", title: "Command-line tools" },
|
|
46
|
+
{ kind: "library", title: "Libraries" },
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* The catalogue appended to the stdlib rule: every stack with its conformance template and what it is, the
|
|
50
|
+
* shared templates, and the folder they are all in - an absolute path, so an agent reads the one it imports
|
|
51
|
+
* instead of looking for it. The text of each is not in it: there are too many to carry in every prompt.
|
|
52
|
+
*/
|
|
53
|
+
export function stdlibCatalogue(templates = stdlibTemplates(), dir = STDLIB_DIR) {
|
|
54
|
+
if (templates.length === 0)
|
|
55
|
+
return "";
|
|
56
|
+
const names = new Set(templates.map((template) => template.name));
|
|
57
|
+
const row = (template) => `| \`${template.name}\` | ${names.has(conformanceOf(template.name)) ? `\`${conformanceOf(template.name)}\`` : "none"} | ${template.description} |`;
|
|
58
|
+
const shared = templates.filter((template) => !isStack(template) && !template.name.includes("/conformance/"));
|
|
59
|
+
return [
|
|
60
|
+
"## What there is",
|
|
61
|
+
"",
|
|
62
|
+
`Every template is a file in ${dir}: \`stdlib/backend/python-fastapi\` is \`${dir}/backend/python-fastapi.plain\`. Read the ones a module imports - the stack, its language, its kind's \`common\`, and their conformance templates - before writing its specs.`,
|
|
63
|
+
"",
|
|
64
|
+
...KINDS.flatMap(({ kind, title }) => {
|
|
65
|
+
const stacks = templates.filter((template) => isStack(template) && template.name.startsWith(`stdlib/${kind}/`));
|
|
66
|
+
return stacks.length === 0 ? [] : [`### ${title}`, "", "| import | its conformance tests | what it is |", "| --- | --- | --- |", ...stacks.map(row), ""];
|
|
67
|
+
}),
|
|
68
|
+
"### What the stacks import",
|
|
69
|
+
"",
|
|
70
|
+
"| template | what it is |",
|
|
71
|
+
"| --- | --- |",
|
|
72
|
+
...shared.map((template) => `| \`${template.name}\` | ${template.description} |`),
|
|
73
|
+
]
|
|
74
|
+
.join("\n")
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
77
|
+
/** How many stacks the standard library has, for the banner. */
|
|
78
|
+
export function stdlibStackCount(templates = stdlibTemplates()) {
|
|
79
|
+
return templates.filter(isStack).length;
|
|
80
|
+
}
|
|
81
|
+
/** The standard library as the person reads it, with `/stdlib`: each kind's stacks, what each is, and how to import one. */
|
|
82
|
+
export function describeStdlib(templates = stdlibTemplates(), dir = STDLIB_DIR) {
|
|
83
|
+
if (templates.length === 0)
|
|
84
|
+
return "The standard library is empty: turri's stdlib/ folder holds no templates.";
|
|
85
|
+
const width = Math.max(...templates.filter(isStack).map((template) => template.name.length));
|
|
86
|
+
return [
|
|
87
|
+
`The ***plain standard library - ${stdlibStackCount(templates)} stacks a spec can import instead of writing its implementation reqs:`,
|
|
88
|
+
...KINDS.flatMap(({ kind, title }) => {
|
|
89
|
+
const stacks = templates.filter((template) => isStack(template) && template.name.startsWith(`stdlib/${kind}/`));
|
|
90
|
+
return stacks.length === 0 ? [] : ["", `${title}:`, ...stacks.map((template) => ` ${template.name.padEnd(width)} ${template.description}`)];
|
|
91
|
+
}),
|
|
92
|
+
"",
|
|
93
|
+
"Import one in a spec's frontmatter, and its conformance tests beside it when you want them:",
|
|
94
|
+
" import:",
|
|
95
|
+
" - stdlib/backend/python-flask",
|
|
96
|
+
" - stdlib/backend/conformance/python-flask",
|
|
97
|
+
"",
|
|
98
|
+
`The templates are in ${dir}. Or ask - "build this as a Flask backend" - and the agent imports it.`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resetCapabilitiesCache } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the terminal turri runs in can show images, and how, settled before pi-tui guesses wrong.
|
|
4
|
+
*
|
|
5
|
+
* pi-tui reads it from the environment, and a terminal started from another inherits that one's
|
|
6
|
+
* variables: Zed opened from Ghostty still has GHOSTTY_RESOURCES_DIR, which pi-tui takes for Ghostty,
|
|
7
|
+
* so it sends images Zed draws as nothing. The terminal a process is really in is the one that set
|
|
8
|
+
* TERM_PROGRAM - or, for Kitty and Ghostty, TERM - last. When that one shows no images, pi-tui is told
|
|
9
|
+
* so through its own PI_IMAGE_PROTOCOL, which it reads each time it looks again (pi re-applies its
|
|
10
|
+
* settings' overrides, so one set in code would not last). A PI_IMAGE_PROTOCOL the person set is kept.
|
|
11
|
+
*/
|
|
12
|
+
/** TERM_PROGRAM as the terminals that show images set it: Kitty's graphics protocol, or iTerm2's. */
|
|
13
|
+
const SHOWS_IMAGES = new Set(["kitty", "ghostty", "wezterm", "warpterminal", "iterm.app"]);
|
|
14
|
+
/** The first iTerm2 that speaks the Kitty graphics protocol */
|
|
15
|
+
const ITERM2_KITTY_FROM = [3, 7];
|
|
16
|
+
/** What PI_IMAGE_PROTOCOL turri sets for this terminal: none where it shows no images, kitty for an iTerm2 that speaks it, else nothing. */
|
|
17
|
+
export function imageProtocolFor(env = process.env) {
|
|
18
|
+
if (env.PI_IMAGE_PROTOCOL !== undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const term = env.TERM?.toLowerCase() ?? "";
|
|
21
|
+
if (term.includes("kitty") || term.includes("ghostty"))
|
|
22
|
+
return undefined;
|
|
23
|
+
const program = env.TERM_PROGRAM?.toLowerCase();
|
|
24
|
+
if (program === undefined || program === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!SHOWS_IMAGES.has(program))
|
|
27
|
+
return "none";
|
|
28
|
+
// pi-tui shows no images under tmux or screen; telling it Kitty's would override that.
|
|
29
|
+
const multiplexed = env.TMUX !== undefined || env.STY !== undefined || term.startsWith("tmux") || term.startsWith("screen");
|
|
30
|
+
if (program === "iterm.app" && !multiplexed && atLeast(env.TERM_PROGRAM_VERSION, ITERM2_KITTY_FROM))
|
|
31
|
+
return "kitty";
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Whether a version such as `3.7.4beta1` is `from` or later; an unreadable one is not. */
|
|
35
|
+
function atLeast(version, from) {
|
|
36
|
+
const parts = /^(\d+)\.(\d+)/.exec(version ?? "");
|
|
37
|
+
if (parts === null)
|
|
38
|
+
return false;
|
|
39
|
+
const [major, minor] = [Number(parts[1]), Number(parts[2])];
|
|
40
|
+
return major > from[0] || (major === from[0] && minor >= from[1]);
|
|
41
|
+
}
|
|
42
|
+
const protocol = imageProtocolFor();
|
|
43
|
+
if (protocol !== undefined) {
|
|
44
|
+
process.env.PI_IMAGE_PROTOCOL = protocol;
|
|
45
|
+
resetCapabilitiesCache();
|
|
46
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
/**
|
|
6
|
+
* Personal tools.
|
|
7
|
+
*
|
|
8
|
+
* `write_artifact` is the executor's only way to produce output: it can read the
|
|
9
|
+
* repo, but everything it makes lands in one sandboxed directory under a name the
|
|
10
|
+
* graph chose. That is what lets the gate know exactly what to review - there is
|
|
11
|
+
* no "find the thing the agent probably changed" step.
|
|
12
|
+
*/
|
|
13
|
+
export function createWriteArtifactTool(runDir) {
|
|
14
|
+
const root = resolve(runDir);
|
|
15
|
+
return defineTool({
|
|
16
|
+
name: "write_artifact",
|
|
17
|
+
label: "Write artifact",
|
|
18
|
+
description: "Write this step's deliverable. Path must be the artifact path given in the step instructions. " +
|
|
19
|
+
"Overwrites any previous attempt.",
|
|
20
|
+
promptSnippet: "write the current step's deliverable to its artifact path",
|
|
21
|
+
parameters: Type.Object({
|
|
22
|
+
path: Type.String({ description: "Artifact path, exactly as given in the step instructions" }),
|
|
23
|
+
content: Type.String({ description: "Full contents of the artifact" }),
|
|
24
|
+
}),
|
|
25
|
+
async execute(_id, params) {
|
|
26
|
+
const target = resolve(root, params.path);
|
|
27
|
+
if (relative(root, target).startsWith("..")) {
|
|
28
|
+
throw new Error(`write_artifact: '${params.path}' escapes the artifact directory`);
|
|
29
|
+
}
|
|
30
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
31
|
+
writeFileSync(target, params.content, "utf8");
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: `Wrote ${params.content.length} bytes to ${params.path}` }],
|
|
34
|
+
details: { path: params.path, bytes: params.content.length },
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export const VerdictSchema = Type.Object({
|
|
40
|
+
verdict: Type.Union([Type.Literal("pass"), Type.Literal("fail")], {
|
|
41
|
+
description: "'fail' if ANY rubric criterion is violated. There is no partial credit.",
|
|
42
|
+
}),
|
|
43
|
+
violations: Type.Array(Type.Object({
|
|
44
|
+
criterion: Type.String({ description: "The rubric criterion, quoted" }),
|
|
45
|
+
evidence: Type.String({ description: "The exact text from the artifact that violates it" }),
|
|
46
|
+
fix: Type.String({ description: "One concrete instruction that would resolve it" }),
|
|
47
|
+
}), { description: "Empty when verdict is 'pass'. Never empty when verdict is 'fail'." }),
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* The judge's only tool. Forcing the verdict through a schema is the difference
|
|
51
|
+
* between a decision the graph can branch on and a paragraph someone has to read.
|
|
52
|
+
* `terminate` stops the judge session as soon as the verdict lands.
|
|
53
|
+
*/
|
|
54
|
+
export function createVerdictTool(onVerdict) {
|
|
55
|
+
return defineTool({
|
|
56
|
+
name: "submit_verdict",
|
|
57
|
+
label: "Submit verdict",
|
|
58
|
+
description: "Submit your review verdict. Call this exactly once. Do not reply with prose.",
|
|
59
|
+
parameters: VerdictSchema,
|
|
60
|
+
async execute(_id, params) {
|
|
61
|
+
onVerdict(params);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: "Verdict recorded." }],
|
|
64
|
+
details: params,
|
|
65
|
+
terminate: true,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
import { isChat, runAsk } from "./ask.js";
|
|
71
|
+
import { describeFindings, describeQuestions } from "./review/live.js";
|
|
72
|
+
/**
|
|
73
|
+
* Lets a skill step put a question to the person running it.
|
|
74
|
+
*
|
|
75
|
+
* A gated skill otherwise runs to completion with nobody to ask, which is fine for work
|
|
76
|
+
* that is fully specified and useless for an interview. The harness still owns the
|
|
77
|
+
* sequence - the step decides what to ask, not whether the gate afterwards runs.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* `gate`, for an interview that holds its questions to rules - the questioning level's cap (questioning.ts), an answer
|
|
81
|
+
* written as a spec change before the next question: a question it refuses is not asked, and `confirms_phase` marks a
|
|
82
|
+
* phase's closing confirmation, which the cap never counts.
|
|
83
|
+
*
|
|
84
|
+
* `talks`: where the asker answers a person who chose "Chat about this" - `"reply"`, the conversation's agent, in its
|
|
85
|
+
* reply; `"context"`, a skill's agent, whose replies nobody sees, as the `context` of the question asked again. Such a
|
|
86
|
+
* choice is no answer: neither the cap nor `onAnswered` counts it.
|
|
87
|
+
*/
|
|
88
|
+
export function createAskUserTool(ask, onAnswered, said, gate, talks = "context") {
|
|
89
|
+
return defineTool({
|
|
90
|
+
name: "ask_user",
|
|
91
|
+
label: "Ask the user",
|
|
92
|
+
description: "Put one question to the user and wait for their answer. Ask exactly one thing at a time. " +
|
|
93
|
+
"Offer concrete options in `options` so the user can pick rather than type - that is the " +
|
|
94
|
+
"normal case, and a question with neither options nor free_form is refused rather than asked.",
|
|
95
|
+
promptSnippet: "ask the user one question, offering options to pick from",
|
|
96
|
+
promptGuidelines: [
|
|
97
|
+
"Every ask_user call should carry `options` unless the answer genuinely cannot be anticipated; picking from a list is far quicker for the user than typing. A free-form question needs free_form: true.",
|
|
98
|
+
"Use multi_select: true when several answers can apply at once, rather than asking the same question repeatedly.",
|
|
99
|
+
"The dialog shows the question, its options and `context` alone - nothing you wrote before it. When the user must read something to answer - the brief to confirm, the plan to approve - pass it as `context`; never put an account of your work there (what a probe returned, what a regeneration did).",
|
|
100
|
+
"Keep `context` short enough to take in at a glance - a heading or two and short bullets, a line each, about fifteen lines at most: what the person decides on, not every detail. The details are in the spec; say where, and they can ask.",
|
|
101
|
+
],
|
|
102
|
+
parameters: Type.Object({
|
|
103
|
+
question: Type.String({ description: "One question. Never bundle two." }),
|
|
104
|
+
options: Type.Optional(Type.Array(Type.String(), {
|
|
105
|
+
description: "Concrete choices to offer. The user can always pick \"Something else…\" and type instead, or " +
|
|
106
|
+
"\"Chat about this\" to talk the question over with you first, so offering options never traps them.",
|
|
107
|
+
})),
|
|
108
|
+
multi_select: Type.Optional(Type.Boolean({ description: "Let the user pick several of the options." })),
|
|
109
|
+
context: Type.Optional(Type.String({
|
|
110
|
+
description: "What the user must read to answer, shown above the question - the brief to confirm, the plan to approve. Only that: never an account of what you did.",
|
|
111
|
+
})),
|
|
112
|
+
free_form: Type.Optional(Type.Boolean({
|
|
113
|
+
description: "Ask without options, for a genuinely open question. Required when options is omitted.",
|
|
114
|
+
})),
|
|
115
|
+
...(gate !== undefined
|
|
116
|
+
? {
|
|
117
|
+
confirms_phase: Type.Optional(Type.Boolean({
|
|
118
|
+
description: "This asks the person to confirm the phase's summary before it ends. It is not counted among the phase's questions.",
|
|
119
|
+
})),
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
122
|
+
}),
|
|
123
|
+
async execute(_id, params) {
|
|
124
|
+
const confirmsPhase = params.confirms_phase === true;
|
|
125
|
+
const refusal = gate?.refusal(confirmsPhase);
|
|
126
|
+
if (refusal !== undefined) {
|
|
127
|
+
return { content: [{ type: "text", text: refusal }], details: { question: params.question, answer: "" } };
|
|
128
|
+
}
|
|
129
|
+
const answer = await ask({
|
|
130
|
+
question: params.question,
|
|
131
|
+
options: params.options,
|
|
132
|
+
multiSelect: params.multi_select,
|
|
133
|
+
freeForm: params.free_form,
|
|
134
|
+
...(params.context !== undefined && params.context.trim() !== "" ? { context: params.context } : {}),
|
|
135
|
+
...(said !== undefined ? { said: said() } : {}),
|
|
136
|
+
chattable: true,
|
|
137
|
+
});
|
|
138
|
+
if (isChat(answer)) {
|
|
139
|
+
return { content: [{ type: "text", text: `${answer}\n\n${talkBack(talks)}` }], details: { question: params.question, answer } };
|
|
140
|
+
}
|
|
141
|
+
gate?.answered(confirmsPhase);
|
|
142
|
+
onAnswered?.(answer, confirmsPhase);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: answer }],
|
|
145
|
+
details: { question: params.question, answer },
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export { runAsk };
|
|
151
|
+
/** What the asker is told to do when the person chose to talk about its question rather than answer it. */
|
|
152
|
+
function talkBack(talks) {
|
|
153
|
+
return talks === "reply"
|
|
154
|
+
? "Answer what they said in your reply, and end your turn there: they go on in the chat. Ask the question again " +
|
|
155
|
+
"with ask_user only once it is settled - changed, if what they said calls for it. It was not counted as answered."
|
|
156
|
+
: "Answer what they said, then ask the question again with ask_user - changed, if what they said calls for it. Put " +
|
|
157
|
+
"your answer to them in the question's `context`: nothing else you write reaches them. It was not counted as a " +
|
|
158
|
+
"question, and nothing is owed for it.";
|
|
159
|
+
}
|
|
160
|
+
/** Ends the questions of a phase the questioning level caps: what is left, the agent decides itself. */
|
|
161
|
+
export function createEndQuestionsTool(questioning) {
|
|
162
|
+
return defineTool({
|
|
163
|
+
name: "end_questions",
|
|
164
|
+
label: "End questions",
|
|
165
|
+
description: "End this phase's questions: you have asked what the person must decide, and you decide what is left " +
|
|
166
|
+
"yourself, saying what you chose. Afterwards ask_user asks only the phase's closing confirmation, and " +
|
|
167
|
+
"write_spec_diff needs no answer between changes.",
|
|
168
|
+
promptSnippet: "end this phase's questions and decide what is left yourself",
|
|
169
|
+
parameters: Type.Object({}),
|
|
170
|
+
async execute() {
|
|
171
|
+
return { content: [{ type: "text", text: questioning.end() }], details: {} };
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Everything the reviewer has to say since the last tool call.
|
|
177
|
+
*
|
|
178
|
+
* It rides along on the tool results the interviewer is already getting, so a finding
|
|
179
|
+
* lands between two questions without the interviewer having to remember to go looking
|
|
180
|
+
* for one.
|
|
181
|
+
*/
|
|
182
|
+
export function reviewNote(liveReview) {
|
|
183
|
+
if (liveReview === undefined)
|
|
184
|
+
return "";
|
|
185
|
+
const parts = [];
|
|
186
|
+
const findings = liveReview.take();
|
|
187
|
+
if (findings.length > 0)
|
|
188
|
+
parts.push(describeFindings(findings));
|
|
189
|
+
const questions = liveReview.takeQuestions();
|
|
190
|
+
if (questions.length > 0)
|
|
191
|
+
parts.push(describeQuestions(questions));
|
|
192
|
+
return parts.length === 0 ? "" : `\n\n${parts.join("\n\n")}`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* With a live reviewer, a way to answer the reviewer's questions. How the builds stand, a skill reads with the
|
|
196
|
+
* conversation's own drift (loop-tools.ts), the reviewer's findings riding along on it (`withReviewNote`).
|
|
197
|
+
*/
|
|
198
|
+
export function createReviewTools(liveReview) {
|
|
199
|
+
if (liveReview === undefined)
|
|
200
|
+
return [];
|
|
201
|
+
const reply = defineTool({
|
|
202
|
+
name: "reply_to_review",
|
|
203
|
+
label: "Reply to review",
|
|
204
|
+
description: "Answer a question the reviewer asked, after putting it to the user. The answer is carried " +
|
|
205
|
+
"into the reviewer's next pass. Use this only for questions the reviewer raised by id.",
|
|
206
|
+
promptSnippet: "hand the user's answer back to the reviewer",
|
|
207
|
+
parameters: Type.Object({
|
|
208
|
+
question_id: Type.String({ description: 'The id the reviewer gave, e.g. "q1"' }),
|
|
209
|
+
answer: Type.String({ description: "What the user decided, in their terms" }),
|
|
210
|
+
}),
|
|
211
|
+
async execute(_id, params) {
|
|
212
|
+
const accepted = liveReview.reply(params.question_id, params.answer);
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: accepted
|
|
218
|
+
? `Answer to ${params.question_id} passed to the reviewer. Make sure the specs say the same thing.`
|
|
219
|
+
: `No open review question with id '${params.question_id}'.`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
details: { accepted },
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
return [reply];
|
|
227
|
+
}
|
|
228
|
+
/** A tool whose every result carries what the reviewer has said since the last one. */
|
|
229
|
+
export function withReviewNote(tool, liveReview) {
|
|
230
|
+
if (liveReview === undefined)
|
|
231
|
+
return tool;
|
|
232
|
+
return {
|
|
233
|
+
...tool,
|
|
234
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
235
|
+
const result = await tool.execute(id, params, signal, onUpdate, ctx);
|
|
236
|
+
const note = reviewNote(liveReview);
|
|
237
|
+
if (note === "")
|
|
238
|
+
return result;
|
|
239
|
+
return { ...result, content: [...result.content, { type: "text", text: note.trimStart() }] };
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { checkPlainTargets, formatCheckResults } from "./plain/check.js";
|
|
2
|
+
const USAGE = `usage: turri validate <file.plain | directory> [options]
|
|
3
|
+
|
|
4
|
+
Validates a ***plain specification statically: parses it, resolves its imports and
|
|
5
|
+
required modules, and reports every error without rendering anything.
|
|
6
|
+
|
|
7
|
+
--template-dir <dir> extra template directory (repeatable)
|
|
8
|
+
--json machine-readable output
|
|
9
|
+
|
|
10
|
+
PLAIN_TEMPLATE_DIRS colon-separated template directories`;
|
|
11
|
+
const c = {
|
|
12
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
13
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
14
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
15
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
16
|
+
};
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const templateDirs = [];
|
|
19
|
+
const positional = [];
|
|
20
|
+
let json = false;
|
|
21
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === "--template-dir")
|
|
24
|
+
templateDirs.push(argv[++i]);
|
|
25
|
+
else if (arg === "--json")
|
|
26
|
+
json = true;
|
|
27
|
+
else if (arg === "-h" || arg === "--help") {
|
|
28
|
+
console.log(USAGE);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
positional.push(arg);
|
|
33
|
+
}
|
|
34
|
+
const target = positional[0] ?? ".";
|
|
35
|
+
const results = checkPlainTargets(target, templateDirs);
|
|
36
|
+
if (results.length === 0) {
|
|
37
|
+
console.error(`No .plain files found in ${target}.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (json) {
|
|
41
|
+
console.log(JSON.stringify(results, null, 2));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const output = formatCheckResults(results)
|
|
45
|
+
.split("\n")
|
|
46
|
+
.map((line) => line.startsWith("PASS") ? c.green(line) : line.startsWith("FAIL") ? c.red(line) : c.dim(line))
|
|
47
|
+
.join("\n");
|
|
48
|
+
console.log(output);
|
|
49
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
50
|
+
console.log(`\n${failed === 0 ? c.green(c.bold("all specs valid")) : c.red(c.bold(`${failed} of ${results.length} failed`))}`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(results.every((r) => r.ok) ? 0 : 1);
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# turri documentation
|
|
2
|
+
|
|
3
|
+
turri implements regenerative software: the \*\*\*plain specs are the durable definition of
|
|
4
|
+
what a system does, and the code is generated from them, validated against them, and regenerated
|
|
5
|
+
as they change. The [project README](../README.md) says why, and how to install and start; these
|
|
6
|
+
pages go into each part.
|
|
7
|
+
|
|
8
|
+
## Start here
|
|
9
|
+
|
|
10
|
+
1. [Installation](installation.md) - get a machine ready: `turri doctor`, and what turri needs of
|
|
11
|
+
this machine.
|
|
12
|
+
2. [Using turri](using.md) - the conversation, the skills and tools, background jobs and
|
|
13
|
+
their logs, choosing the model that renders, the command line.
|
|
14
|
+
3. [Authoring \*\*\*plain](authoring-plain.md) - writing the specs: what a spec is made of, the
|
|
15
|
+
authoring rules, the skills that write and fix specs.
|
|
16
|
+
4. [Rendering](regenerating.md) - turning the specs into code: what each build does, in which order,
|
|
17
|
+
and what has to pass before it moves on.
|
|
18
|
+
|
|
19
|
+
## By question
|
|
20
|
+
|
|
21
|
+
| to find out… | read |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| what my machine needs, and why a check failed | [installation](installation.md) |
|
|
24
|
+
| what turri needs of this machine, and what `turri doctor` checks | [installation](installation.md#what-turri-setup-checks) |
|
|
25
|
+
| how to start a project, or add a feature, in a conversation | [using](using.md#in-a-conversation) |
|
|
26
|
+
| which model renders, and how to change it | [using](using.md#choosing-a-model), [the project config](config.md#models) |
|
|
27
|
+
| what `config.turri.yaml` can set, and what happens without it | [the project config](config.md) |
|
|
28
|
+
| how a backend and a frontend on different stacks share one project | [the project config](config.md#stacks) |
|
|
29
|
+
| which ready-made stacks a spec can import - a FastAPI backend, a React frontend | [the standard library](authoring-plain.md#the-standard-library) |
|
|
30
|
+
| everything an agent was sent and did, to debug it | [using](using.md#agent-logs) |
|
|
31
|
+
| how to render a spec overnight, and resume it | [running unattended](headless.md) |
|
|
32
|
+
| how to rebuild one functionality | [running unattended](headless.md#rebuilding-one-functionality) |
|
|
33
|
+
| what goes in which section of a spec | [authoring \*\*\*plain](authoring-plain.md#what-a-spec-is-made-of) |
|
|
34
|
+
| which skill does what | [authoring \*\*\*plain](authoring-plain.md#the-skills) |
|
|
35
|
+
| what happens when a spec changes | [regenerating the code](regenerating.md#when-the-spec-changes) |
|
|
36
|
+
| why a test was not allowed to be deleted | [regenerating the code](regenerating.md#the-tests-must-actually-pass) |
|
|
37
|
+
| how to try what is built, and what deploy may install | [deploy](deploy.md) |
|
|
38
|
+
| what the agents can and cannot reach | [the sandbox](sandbox.md) |
|
|
39
|
+
| how skill review gates and judges work | [skills and gates](skills-and-gates.md) |
|
|
40
|
+
| how the parser matches the reference implementation | [the \*\*\*plain parser](parser.md) |
|
|
41
|
+
| where the code for something lives | [development](development.md#layout) |
|
|
42
|
+
|
|
43
|
+
## All pages
|
|
44
|
+
|
|
45
|
+
| page | covers |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| [Installation](installation.md) | doctor, what turri needs of this machine, turri and `pi`, environment variables |
|
|
48
|
+
| [Using turri](using.md) | the conversation, the tools, background jobs, choosing a model, module history, the command line |
|
|
49
|
+
| [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
|
+
| [Deploy](deploy.md) | trying what is built: dist/<module>, the last commit, starting it on your machine, `turri release` |
|
|
51
|
+
| [Running unattended](headless.md) | rendering overnight or in CI: before you leave it, the command, the results, resuming, rebuilding one functionality |
|
|
52
|
+
| [Authoring \*\*\*plain](authoring-plain.md) | a \*\*\*plain project, what a spec is made of, the rules, writing specs with the agent, the skills, checking a spec |
|
|
53
|
+
| [Rendering](regenerating.md) | where things go, one job, a whole module and how its work is cut into jobs, each job's cases, spec changes, conformance tests, the two records of what is built |
|
|
54
|
+
| [The sandbox](sandbox.md) | what an agent can reach, the file-tool fence, the command check, the OS sandbox, the audit |
|
|
55
|
+
| [Skills and gates](skills-and-gates.md) | the gate syntax, what the harness enforces, what a step can do, the rules, trying the gates |
|
|
56
|
+
| [The \*\*\*plain parser](parser.md) | the TypeScript port, `turri validate`, inheritance, the iterator, fidelity |
|
|
57
|
+
| [Theming](theming.md) | the turri themes, contrast, backgrounds, the font |
|
|
58
|
+
| [Development](development.md) | tests, the source layout, known edges |
|