@turri-dev/cli 0.0.0-stage → 0.1.1
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 +319 -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 +75 -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 +118 -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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
|
4
|
+
import { adoptLegacyConfig, AGENT_ROLES, CONFIG_FILE, chosenStack, projectConfigFile, stackConfigFileIn, stacksOf, splitModelSpec, THINKING_LEVELS } from "./config.js";
|
|
5
|
+
/** What each effect is, said to the person. */
|
|
6
|
+
export const EFFECT_TEXT = {
|
|
7
|
+
now: "in effect now",
|
|
8
|
+
"next-message": "in effect from your next message",
|
|
9
|
+
"next-pulse": "in effect from the next pulse - one running now keeps what it started with",
|
|
10
|
+
"next-plan": "in effect the next time the work is planned - at the next pulse",
|
|
11
|
+
"next-test-run": "in effect from the next test run - the running pulse's too",
|
|
12
|
+
"next-landing": "in effect from the next pulse that lands - the running one too",
|
|
13
|
+
"next-regenerate": "in effect for the next regeneration that names no .plain file",
|
|
14
|
+
};
|
|
15
|
+
const SECONDS = ["0", "60", "120", "180", "300"];
|
|
16
|
+
/** A role's line: which agents it is, for the list. */
|
|
17
|
+
const ROLE_ABOUT = {
|
|
18
|
+
default: "every role not named",
|
|
19
|
+
chat: "the conversation - as harness-model, which it wins over",
|
|
20
|
+
chooser: "chooses what gets conformance tests under turri regenerate",
|
|
21
|
+
planner: "cuts the work into pulses",
|
|
22
|
+
diagnosis: "looks into a pulse that failed for good",
|
|
23
|
+
release: "the release agent",
|
|
24
|
+
coding: "writes the code - as coding-model, which it wins over",
|
|
25
|
+
conformance: "writes the conformance tests",
|
|
26
|
+
explorer: "explores the code for a conformance run",
|
|
27
|
+
reviewer: "reviews the code and its unit tests",
|
|
28
|
+
"test-reviewer": "reviews the conformance tests",
|
|
29
|
+
judge: "judges a failing conformance run - as judge-model, which it wins over",
|
|
30
|
+
arbiter: "weighs a second review",
|
|
31
|
+
debugger: "a fresh look at unit tests that keep failing the same way",
|
|
32
|
+
};
|
|
33
|
+
/** Every setting, in the order the list shows them. */
|
|
34
|
+
export function settings(rules = []) {
|
|
35
|
+
const roles = ["default", ...AGENT_ROLES].flatMap((role) => [
|
|
36
|
+
{
|
|
37
|
+
key: `models.${role}`,
|
|
38
|
+
about: `the ${role} role's model and thinking - ${ROLE_ABOUT[role]}`,
|
|
39
|
+
kind: "model",
|
|
40
|
+
unset: role === "default" ? "harness-model's, for the conversation's side; coding-model's for the rest" : "default's",
|
|
41
|
+
effect: role === "chat" ? "now" : "next-pulse",
|
|
42
|
+
...(role === "chat" ? { projectOnly: true } : {}),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: `models.${role}.turn-limit`,
|
|
46
|
+
about: `seconds each turn of the ${role} role's agents has - 0 for none`,
|
|
47
|
+
kind: "number",
|
|
48
|
+
unset: "default's, else turn-limit",
|
|
49
|
+
effect: "next-pulse",
|
|
50
|
+
options: SECONDS,
|
|
51
|
+
min: 0,
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
return [
|
|
55
|
+
{ key: "harness-model", about: "the model you talk to: it writes the specs and plans the work", kind: "model", unset: "pi's own choice", effect: "now", projectOnly: true },
|
|
56
|
+
{ key: "coding-model", about: "the model that writes the code", kind: "model", unset: "harness-model", effect: "next-pulse" },
|
|
57
|
+
{ key: "judge-model", about: "the judge and the test review of a conformance run", kind: "model", unset: "coding-model", effect: "next-pulse" },
|
|
58
|
+
{ key: "module", about: "the .plain file turri regenerate builds when none is named", kind: "path", unset: "the folder's .plain files", effect: "next-regenerate", projectOnly: true },
|
|
59
|
+
{ key: "conformance-tests", about: "which functional specs get conformance tests", kind: "choice", options: ["all", "chosen", "module", "false"], unset: "all, where a module has test reqs", effect: "next-plan" },
|
|
60
|
+
{ key: "conformance-budget", about: "under conformance-tests: chosen or module, how many may be chosen - a count or a share", kind: "budget", options: ["10%", "30%", "50%", "5", "10"], unset: "no limit", effect: "next-plan" },
|
|
61
|
+
{ key: "review", about: "whether the code and its tests are reviewed once they pass", kind: "boolean", unset: "true", effect: "next-pulse" },
|
|
62
|
+
{ key: "allow-to-stop-jobs", about: "whether turri stops a running pulse the spec has moved on from", kind: "boolean", unset: "true", effect: "now" },
|
|
63
|
+
{ key: "release-after-pulse", about: "whether every pulse that lands code releases it", kind: "boolean", unset: "false", effect: "next-landing" },
|
|
64
|
+
{ key: "unittests-script", about: "the unit testing script, by path from the project root", kind: "path", unset: "the one run_unittests*.sh found", effect: "next-test-run" },
|
|
65
|
+
{ key: "conformance-tests-script", about: "the conformance testing script, by path from the project root", kind: "path", unset: "the one run_conformance_tests*.sh found", effect: "next-test-run" },
|
|
66
|
+
{ key: "prepare-environment-script", about: "run on the build before every conformance testing script run, by path from the project root - optional", kind: "path", unset: "the one prepare_environment*.sh found, else none", effect: "next-test-run" },
|
|
67
|
+
{ key: "testing-scripts-run-on", about: "what the testing scripts run on: the code itself, or a fresh copy", kind: "choice", options: ["in-place", "copy"], unset: "in-place", effect: "next-test-run" },
|
|
68
|
+
{ key: "parallel-jobs", about: "how many pulses run at once, on modules that share nothing", kind: "number", options: ["1", "2", "3", "4", "6"], min: 1, unset: "half this machine's cores, one to four", effect: "now", projectOnly: true },
|
|
69
|
+
{ key: "max-specs-per-pulse", about: "the most functional specs the planner groups into one pulse", kind: "number", options: ["1", "2", "3", "4", "5", "6"], min: 1, unset: "3", effect: "next-plan" },
|
|
70
|
+
{ key: "max-lines-per-pulse", about: "the most lines of spec in one pulse before a group is split", kind: "number", options: ["60", "120", "200", "300"], min: 1, unset: "120", effect: "next-plan" },
|
|
71
|
+
{ key: "turn-limit", about: "seconds each agent turn has, for every role without its own - 0 for none", kind: "number", options: SECONDS, min: 0, unset: "120", effect: "next-pulse" },
|
|
72
|
+
{ key: "compact-at", about: "tokens of work an agent holds before it is compacted between turns", kind: "number", options: ["50000", "100000", "150000", "200000"], min: 1000, unset: "100000", effect: "next-pulse", projectOnly: true },
|
|
73
|
+
{ key: "rules-off", about: "***plain rules left out of the spec-writing agents' prompts", kind: "list", options: rules, unset: "none", effect: "next-message", projectOnly: true },
|
|
74
|
+
{ key: "agent-env.files", about: "dotenv files whose variables the agents' commands get", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
|
|
75
|
+
{ key: "agent-env.vars", about: "variables from turri's environment the agents' commands get, by name", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
|
|
76
|
+
{ key: "toolchains", about: "folders of this machine an agent's commands may read beside the usual ones", kind: "list", unset: "none", effect: "next-pulse", projectOnly: true },
|
|
77
|
+
...roles,
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/** A setting by its key - `review`, `models.coding` - or nothing. */
|
|
81
|
+
export function settingOf(key, rules = []) {
|
|
82
|
+
return settings(rules).find((setting) => setting.key === key.trim().toLowerCase().replace(/_/g, "-"));
|
|
83
|
+
}
|
|
84
|
+
/** The words that unset a setting. */
|
|
85
|
+
const UNSET = new Set(["unset", "default", "none", "-"]);
|
|
86
|
+
/**
|
|
87
|
+
* What was typed for a setting, read as the file takes it: `true`/`false`, one of its words, a number at least its least,
|
|
88
|
+
* a model `provider/id` with an optional `:thinking`, a list by commas. `known`: whether a model can be reached here -
|
|
89
|
+
* when it is given, one that cannot is refused.
|
|
90
|
+
*/
|
|
91
|
+
export function parseValue(setting, typed, known) {
|
|
92
|
+
const text = typed.trim();
|
|
93
|
+
if (UNSET.has(text.toLowerCase()))
|
|
94
|
+
return { unset: true };
|
|
95
|
+
if (text === "")
|
|
96
|
+
return { error: `${setting.key} needs a value - or unset, for ${setting.unset}.` };
|
|
97
|
+
switch (setting.kind) {
|
|
98
|
+
case "boolean": {
|
|
99
|
+
const word = text.toLowerCase();
|
|
100
|
+
if (["true", "yes", "on"].includes(word))
|
|
101
|
+
return { value: true };
|
|
102
|
+
if (["false", "no", "off"].includes(word))
|
|
103
|
+
return { value: false };
|
|
104
|
+
return { error: `${setting.key} is true or false.` };
|
|
105
|
+
}
|
|
106
|
+
case "choice": {
|
|
107
|
+
const word = text.toLowerCase();
|
|
108
|
+
if (setting.options?.includes(word))
|
|
109
|
+
return { value: setting.key === "conformance-tests" && word === "false" ? false : word };
|
|
110
|
+
return { error: `${setting.key} is one of ${setting.options?.join(", ")}.` };
|
|
111
|
+
}
|
|
112
|
+
case "number": {
|
|
113
|
+
const number = Number(text);
|
|
114
|
+
if (!Number.isInteger(number) || number < (setting.min ?? 0))
|
|
115
|
+
return { error: `${setting.key} is a whole number of at least ${setting.min ?? 0}.` };
|
|
116
|
+
return { value: number };
|
|
117
|
+
}
|
|
118
|
+
case "budget": {
|
|
119
|
+
if (/^\d+$/.test(text))
|
|
120
|
+
return { value: Number(text) };
|
|
121
|
+
const share = /^(\d+(?:\.\d+)?)\s*%$/.exec(text);
|
|
122
|
+
if (share !== null && Number(share[1]) <= 100)
|
|
123
|
+
return { value: `${share[1]}%` };
|
|
124
|
+
return { error: `${setting.key} is a count, such as 5, or a share, such as 30%.` };
|
|
125
|
+
}
|
|
126
|
+
case "model": {
|
|
127
|
+
const { model, thinking } = splitModelSpec(text);
|
|
128
|
+
const colon = text.lastIndexOf(":");
|
|
129
|
+
if (colon !== -1 && thinking === undefined && !text.slice(colon + 1).includes("/")) {
|
|
130
|
+
return { error: `How hard it thinks is one of ${THINKING_LEVELS.join(", ")}: ${text.slice(colon + 1)} is not.` };
|
|
131
|
+
}
|
|
132
|
+
if (model === undefined)
|
|
133
|
+
return { error: `${setting.key} is a model, provider/id - with :thinking if you like, such as :low.` };
|
|
134
|
+
if (!model.includes("/"))
|
|
135
|
+
return { error: `A model is written provider/id, such as anthropic/claude-opus-5-5: ${model} is not.` };
|
|
136
|
+
if (known !== undefined && !known(model))
|
|
137
|
+
return { error: `${model} is not a model turri can reach here - sign in to its provider, or choose another.` };
|
|
138
|
+
return { value: text };
|
|
139
|
+
}
|
|
140
|
+
case "list": {
|
|
141
|
+
const items = text.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
142
|
+
if (setting.options !== undefined && setting.options.length > 0) {
|
|
143
|
+
const unknown = items.filter((item) => !setting.options.includes(item));
|
|
144
|
+
if (unknown.length > 0)
|
|
145
|
+
return { error: `Not one of ${setting.key}'s: ${unknown.join(", ")}. It takes ${setting.options.join(", ")}.` };
|
|
146
|
+
}
|
|
147
|
+
return { value: items };
|
|
148
|
+
}
|
|
149
|
+
case "path":
|
|
150
|
+
return { value: text };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** The file's top-level mapping, or nothing of it. */
|
|
154
|
+
function loadMapping(text) {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = loadYaml(text);
|
|
157
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** A value as one YAML line's: `true`, `30%`, `[a, b]`, `anthropic/claude-opus-5-5:low`. */
|
|
164
|
+
function inline(value) {
|
|
165
|
+
return dumpYaml(value, { flowLevel: 0, lineWidth: -1 }).trim();
|
|
166
|
+
}
|
|
167
|
+
/** Where a top-level key is in the lines: its line and the end of what is indented under it. */
|
|
168
|
+
function blockOf(lines, key) {
|
|
169
|
+
const at = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
170
|
+
if (at === -1)
|
|
171
|
+
return undefined;
|
|
172
|
+
let end = at + 1;
|
|
173
|
+
while (end < lines.length && /^\s+\S/.test(lines[end]) && !/^\s*#/.test(lines[end]))
|
|
174
|
+
end += 1;
|
|
175
|
+
return { at, end };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The file with a top-level key set to these lines, or taken out (`undefined`): in its place, its trailing comment kept
|
|
179
|
+
* on a one-line value; a key not there goes under its commented-out example, else at the end.
|
|
180
|
+
*/
|
|
181
|
+
function withTopLevel(text, key, block) {
|
|
182
|
+
const lines = text.split("\n");
|
|
183
|
+
const found = blockOf(lines, key);
|
|
184
|
+
if (found !== undefined) {
|
|
185
|
+
const comment = found.end - found.at === 1 && block?.length === 1 ? /\s+#.*$/.exec(lines[found.at].replace(/"[^"]*"|'[^']*'/g, ""))?.[0] : undefined;
|
|
186
|
+
const replaced = block === undefined ? [] : comment !== undefined ? [`${block[0]}${comment}`] : block;
|
|
187
|
+
return [...lines.slice(0, found.at), ...replaced, ...lines.slice(found.end)].join("\n");
|
|
188
|
+
}
|
|
189
|
+
if (block === undefined)
|
|
190
|
+
return text;
|
|
191
|
+
const example = lines.findIndex((line) => new RegExp(`^#\\s?${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`).test(line));
|
|
192
|
+
if (example !== -1) {
|
|
193
|
+
let end = example + 1;
|
|
194
|
+
while (end < lines.length && /^#\s{2,}\S/.test(lines[end]))
|
|
195
|
+
end += 1;
|
|
196
|
+
return [...lines.slice(0, end), ...block, ...lines.slice(end)].join("\n");
|
|
197
|
+
}
|
|
198
|
+
const body = text === "" || text.endsWith("\n") ? text : `${text}\n`;
|
|
199
|
+
return `${body}${block.join("\n")}\n`;
|
|
200
|
+
}
|
|
201
|
+
/** A role's entry under `models:` - a model string, or `model`, `thinking` and `turn-limit` - as one object. */
|
|
202
|
+
function roleEntry(value) {
|
|
203
|
+
if (typeof value === "string") {
|
|
204
|
+
const { model, thinking } = splitModelSpec(value);
|
|
205
|
+
return { ...(model !== undefined ? { model } : {}), ...(thinking !== undefined ? { thinking } : {}) };
|
|
206
|
+
}
|
|
207
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
208
|
+
}
|
|
209
|
+
/** A role's entry as written: the model string alone when it has no turn limit, else the object - nothing when empty. */
|
|
210
|
+
function writtenEntry(entry) {
|
|
211
|
+
const turnLimit = entry["turn-limit"] ?? entry.turn_limit;
|
|
212
|
+
const model = typeof entry.model === "string" ? entry.model : undefined;
|
|
213
|
+
const thinking = typeof entry.thinking === "string" ? entry.thinking : undefined;
|
|
214
|
+
if (turnLimit === undefined) {
|
|
215
|
+
if (model === undefined && thinking === undefined)
|
|
216
|
+
return undefined;
|
|
217
|
+
return `${model ?? ""}${thinking !== undefined ? `:${thinking}` : ""}`;
|
|
218
|
+
}
|
|
219
|
+
return { ...(model !== undefined ? { model } : {}), ...(thinking !== undefined ? { thinking } : {}), "turn-limit": turnLimit };
|
|
220
|
+
}
|
|
221
|
+
/** The file with a top-level key set to a value as it is - a block, such as `models:`, written whole - in its line, or under its commented-out example. */
|
|
222
|
+
export function withTopLevelValue(text, key, value) {
|
|
223
|
+
const block = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
224
|
+
return withTopLevel(text, key, block ? dumpYaml({ [key]: value }, { flowLevel: 2, lineWidth: -1 }).trimEnd().split("\n") : [`${key}: ${inline(value)}`]);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The file with a setting set to a value, or unset (`undefined`). A top-level one is changed in its line; one under
|
|
228
|
+
* `models:` or `agent-env:` has that block written again whole.
|
|
229
|
+
*/
|
|
230
|
+
export function withSetting(text, key, value) {
|
|
231
|
+
const [top, ...path] = key.split(".");
|
|
232
|
+
if (path.length === 0)
|
|
233
|
+
return withTopLevel(text, top, value === undefined ? undefined : [`${top}: ${inline(value)}`]);
|
|
234
|
+
const current = loadMapping(text)[top];
|
|
235
|
+
const block = current !== null && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
|
|
236
|
+
if (top === "models") {
|
|
237
|
+
const [role, field] = path;
|
|
238
|
+
const entry = roleEntry(block[role]);
|
|
239
|
+
if (field === "turn-limit") {
|
|
240
|
+
delete entry.turn_limit;
|
|
241
|
+
if (value === undefined)
|
|
242
|
+
delete entry["turn-limit"];
|
|
243
|
+
else
|
|
244
|
+
entry["turn-limit"] = value;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
delete entry.model;
|
|
248
|
+
delete entry.thinking;
|
|
249
|
+
if (value !== undefined)
|
|
250
|
+
Object.assign(entry, roleEntry(value));
|
|
251
|
+
}
|
|
252
|
+
const written = writtenEntry(entry);
|
|
253
|
+
if (written === undefined)
|
|
254
|
+
delete block[role];
|
|
255
|
+
else
|
|
256
|
+
block[role] = written;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const field = path[0];
|
|
260
|
+
if (value === undefined)
|
|
261
|
+
delete block[field];
|
|
262
|
+
else
|
|
263
|
+
block[field] = value;
|
|
264
|
+
}
|
|
265
|
+
if (Object.keys(block).length === 0)
|
|
266
|
+
return withTopLevel(text, top, undefined);
|
|
267
|
+
const lines = dumpYaml({ [top]: block }, { flowLevel: 2, lineWidth: -1 }).trimEnd().split("\n");
|
|
268
|
+
return withTopLevel(text, top, lines);
|
|
269
|
+
}
|
|
270
|
+
/** A setting's value as the file has it - the raw value, before turri reads it - or undefined when it is not set. */
|
|
271
|
+
export function currentValue(text, key) {
|
|
272
|
+
const [top, ...path] = key.split(".");
|
|
273
|
+
const value = loadMapping(text)[top];
|
|
274
|
+
if (path.length === 0)
|
|
275
|
+
return value;
|
|
276
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
277
|
+
return undefined;
|
|
278
|
+
const block = value;
|
|
279
|
+
if (top === "models") {
|
|
280
|
+
const raw = block[path[0]];
|
|
281
|
+
if (raw === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
const entry = roleEntry(raw);
|
|
284
|
+
if (path[1] === "turn-limit")
|
|
285
|
+
return entry["turn-limit"] ?? entry.turn_limit;
|
|
286
|
+
return writtenEntry({ model: entry.model, thinking: entry.thinking });
|
|
287
|
+
}
|
|
288
|
+
return block[path[0]];
|
|
289
|
+
}
|
|
290
|
+
/** A value for showing: `true`, `anthropic/claude-opus-5-5:low`, `[.env, .env.local]`. */
|
|
291
|
+
export function shownValue(value) {
|
|
292
|
+
if (value === undefined)
|
|
293
|
+
return "";
|
|
294
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
295
|
+
const entry = value;
|
|
296
|
+
return `${entry.model ?? ""}${entry.thinking !== undefined ? `:${entry.thinking}` : ""}`;
|
|
297
|
+
}
|
|
298
|
+
return inline(value);
|
|
299
|
+
}
|
|
300
|
+
/** The project's config.turri.yaml - or the config.yaml it is read from until it has one - its text, empty when there is neither. */
|
|
301
|
+
export function readConfigText(dir) {
|
|
302
|
+
const path = join(dir, projectConfigFile(dir));
|
|
303
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
304
|
+
}
|
|
305
|
+
/** What a change does beside setting the value: the files and settings that win over it, said to the person. */
|
|
306
|
+
export function overriddenBy(dir, text, setting) {
|
|
307
|
+
const notes = [];
|
|
308
|
+
const wins = { "harness-model": "models.chat", "coding-model": "models.coding", "judge-model": "models.judge" };
|
|
309
|
+
const over = wins[setting.key];
|
|
310
|
+
if (over !== undefined && currentValue(text, over) !== undefined)
|
|
311
|
+
notes.push(`${over} is set too, and wins over ${setting.key} - change it with /turri-config ${over}.`);
|
|
312
|
+
if (setting.projectOnly === true)
|
|
313
|
+
return notes;
|
|
314
|
+
const top = setting.key.split(".")[0];
|
|
315
|
+
for (const stack of stacksOf(dir)) {
|
|
316
|
+
const file = stackConfigFileIn(dir, stack);
|
|
317
|
+
const own = loadMapping(safeRead(join(dir, file)))[top];
|
|
318
|
+
if (own !== undefined)
|
|
319
|
+
notes.push(`${file} sets ${top} too, and wins while the stack ${stack} is chosen${chosenStack() === stack ? " - as it is now" : ""}.`);
|
|
320
|
+
}
|
|
321
|
+
return notes;
|
|
322
|
+
}
|
|
323
|
+
function safeRead(path) {
|
|
324
|
+
try {
|
|
325
|
+
return readFileSync(path, "utf8");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return "";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Write a setting into the project's config.turri.yaml - made when it is not there, from config.yaml and each stack's
|
|
333
|
+
* file in the old naming when they are what the project is read from - and give back the file as written.
|
|
334
|
+
*/
|
|
335
|
+
export function writeSetting(dir, key, value) {
|
|
336
|
+
adoptLegacyConfig(dir);
|
|
337
|
+
const text = withSetting(readConfigText(dir), key, value);
|
|
338
|
+
writeFileSync(join(dir, CONFIG_FILE), text);
|
|
339
|
+
return text;
|
|
340
|
+
}
|
|
341
|
+
/** The files a path setting is offered: the testing scripts in the root and test_scripts/, or the .plain files of the folder. */
|
|
342
|
+
export function pathOptions(dir, key) {
|
|
343
|
+
const list = (folder, test) => {
|
|
344
|
+
try {
|
|
345
|
+
return readdirSync(join(dir, folder))
|
|
346
|
+
.filter((name) => test(name) && statSync(join(dir, folder, name)).isFile())
|
|
347
|
+
.map((name) => (folder === "" ? name : `${folder}/${name}`));
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
if (key === "module")
|
|
354
|
+
return list("", (name) => name.endsWith(".plain")).sort();
|
|
355
|
+
const prefix = key === "unittests-script" ? "run_unittests" : key === "conformance-tests-script" ? "run_conformance_tests" : "prepare_environment";
|
|
356
|
+
const script = (name) => name.startsWith(prefix) && /\.(sh|ps1)$/.test(name);
|
|
357
|
+
return [...list("", script), ...list("test_scripts", script)].sort();
|
|
358
|
+
}
|
|
359
|
+
/** The dotenv files of the project root, for agent-env.files. */
|
|
360
|
+
export function dotenvFiles(dir) {
|
|
361
|
+
try {
|
|
362
|
+
return readdirSync(dir).filter((name) => /^\.env(\..+)?$/.test(name)).sort();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|