@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
package/dist/config.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { load as loadYaml } from "js-yaml";
|
|
5
|
+
import { projectDirOf } from "./regenerate/paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* The project's `config.turri.yaml`, at its root - the folder that holds `plain_modules` - or, before it had one, its `config.yaml`.
|
|
8
|
+
*
|
|
9
|
+
* One file for what a project says about how it is built:
|
|
10
|
+
*
|
|
11
|
+
* ```yaml
|
|
12
|
+
* module: prism_cli.plain # what `turri regenerate` builds by default
|
|
13
|
+
* harness-model: # the model you chat with in `turri`
|
|
14
|
+
* model: anthropic/claude-opus-5-5
|
|
15
|
+
* thinking: high
|
|
16
|
+
* coding-model: # the rendering agents (default: harness-model)
|
|
17
|
+
* model: google/gemini-3.1-flash
|
|
18
|
+
* thinking: medium
|
|
19
|
+
* judge-model: # the judge and the test review (default: coding-model)
|
|
20
|
+
* model: google/gemini-3.1-pro
|
|
21
|
+
* conformance-tests: chosen # all, chosen (those with acceptance tests, and those the chooser picks after each pulse), module (the chooser picks, and plans, once the module is regenerated) or false (default: false)
|
|
22
|
+
* conformance-budget: 30% # under chosen or module: how many functional specs a module may have chosen, beside those with acceptance tests - a count or a share (default: no limit)
|
|
23
|
+
* review: false # review the code and its tests once they pass (default: true)
|
|
24
|
+
* allow-to-stop-jobs: false # stop a running job the spec moved on from (default: true)
|
|
25
|
+
* release-after-pulse: true # release what each pulse lands, as its last stage, before the next pulse (default: false)
|
|
26
|
+
* rules-off: [integrations] # ***plain rules to leave out of the prompts, by name (default: none)
|
|
27
|
+
* unittests-script: test_scripts/run_unittests.sh # the testing scripts - see test-scripts.ts
|
|
28
|
+
* conformance-tests-script: test_scripts/run_conformance_tests.sh
|
|
29
|
+
* prepare-environment-script: test_scripts/prepare_environment.sh # optional: run before the conformance testing script - see test-scripts.ts
|
|
30
|
+
* testing-scripts-run-on: in-place # what the testing scripts run on: in-place (default), or copy - see test-scripts.ts
|
|
31
|
+
* turn-limit: 150 # seconds each turn of an agent a render starts has (default: 120; 0 for none)
|
|
32
|
+
* models: # each agent role's own model, thinking and turn limit - see AGENT_ROLES
|
|
33
|
+
* default: amazon-bedrock/global.openai.gpt-6-luna # every role not named
|
|
34
|
+
* coding: { model: amazon-bedrock/eu.anthropic.claude-sonnet-5, thinking: high, turn-limit: 150 }
|
|
35
|
+
* explorer: google/gemini-3.1-flash-lite
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* `models:` names a model, a thinking level and a turn limit per agent role (AGENT_ROLES), each falling back as
|
|
39
|
+
* roleModel says: a role it does not name takes `default`, and what an entry leaves out it takes from there too.
|
|
40
|
+
* `harness-model`, `coding-model` and `judge-model` are the same as `models: chat`, `coding` and `judge`, which win
|
|
41
|
+
* over them. The turn limit is the role's own, else `default`'s, else `turn-limit:`, else two minutes - the
|
|
42
|
+
* config is the only place it is set.
|
|
43
|
+
*
|
|
44
|
+
* Each model is a model reference and a thinking level (off, minimal, low, medium, high, xhigh, max),
|
|
45
|
+
* either of which may be left out; a bare string is the model alone. What a model leaves out it takes
|
|
46
|
+
* from the one it inherits: judge-model from coding-model, coding-model from harness-model. They are
|
|
47
|
+
* defaults: `--harness-model` / `--coding-model` / `--judge-model` on the command line win over them,
|
|
48
|
+
* and so does a model chosen during the conversation (pi's /model, engine). `model:` is read
|
|
49
|
+
* as `coding-model`, which it was before there were two.
|
|
50
|
+
*
|
|
51
|
+
* Keys are written with dashes; underscores are read the same (`conformance_tests`). Keys it
|
|
52
|
+
* does not know - a codeplain `config.yaml`, read while it is the old file, carries several - are ignored, and so is a value of
|
|
53
|
+
* the wrong type. Everything is optional, and what is given on the command line wins.
|
|
54
|
+
*
|
|
55
|
+
* A project whose parts do not share a stack - a Python backend and a React frontend beside each other - keeps a
|
|
56
|
+
* stack's own settings in `config.turri.<stack>.yaml` beside `config.turri.yaml`: `config.turri.backend.yaml` for the
|
|
57
|
+
* stack `backend` - its testing scripts, its toolchains. A stack is chosen, never worked out: `/stack` in the chat,
|
|
58
|
+
* `--stack <name>` (or `TURRI_STACK`) on the command line. With one chosen, everything turri builds from then on is
|
|
59
|
+
* built with `config.turri.yaml` and the stack's file over it, key by key - a model it names takes what it leaves out
|
|
60
|
+
* from the project's - and a pulse already running keeps the stack it started with. With none, `config.turri.yaml`
|
|
61
|
+
* alone. `module:` and `harness-model:` are the project's alone - there is one conversation - and are not read from a
|
|
62
|
+
* stack's file.
|
|
63
|
+
*/
|
|
64
|
+
/** turri's settings for a project, in a file of its own: a `config.yaml` beside it may be another tool's. */
|
|
65
|
+
export const CONFIG_FILE = "config.turri.yaml";
|
|
66
|
+
/**
|
|
67
|
+
* Where turri's settings were before they had a file of their own. A project with no `config.turri*.yaml` yet is read
|
|
68
|
+
* from it - and a `config.<name>.yaml` beside it as the stack `<name>` - and turri never writes it: the first setting
|
|
69
|
+
* turri writes copies them to the new names (`adoptLegacyConfig`), and from then on the old files are not read.
|
|
70
|
+
*/
|
|
71
|
+
export const LEGACY_CONFIG_FILE = "config.yaml";
|
|
72
|
+
/** A stack's config file's name: `config.turri.backend.yaml` for the stack `backend`. */
|
|
73
|
+
export function stackConfigFile(stack) {
|
|
74
|
+
return `config.turri.${stack}.yaml`;
|
|
75
|
+
}
|
|
76
|
+
function legacyStackConfigFile(stack) {
|
|
77
|
+
return `config.${stack}.yaml`;
|
|
78
|
+
}
|
|
79
|
+
function namesIn(root) {
|
|
80
|
+
try {
|
|
81
|
+
return readdirSync(root);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The stacks in the old naming, `config.<stack>.yaml`. */
|
|
88
|
+
function legacyStacks(root) {
|
|
89
|
+
return namesIn(root)
|
|
90
|
+
.map((name) => /^config\.(?!turri(?:\.|$))(.+)\.yaml$/.exec(name)?.[1])
|
|
91
|
+
.filter((name) => name !== undefined)
|
|
92
|
+
.sort();
|
|
93
|
+
}
|
|
94
|
+
/** Whether a folder's settings are still in the old files: a config.yaml or a config.<stack>.yaml, and no config.turri*.yaml. */
|
|
95
|
+
export function usesLegacyConfig(root) {
|
|
96
|
+
const names = namesIn(root);
|
|
97
|
+
if (names.some((name) => /^config\.turri(?:\..+)?\.yaml$/.test(name)))
|
|
98
|
+
return false;
|
|
99
|
+
return names.includes(LEGACY_CONFIG_FILE) || legacyStacks(root).length > 0;
|
|
100
|
+
}
|
|
101
|
+
/** The file the project's settings are read from, by name: config.turri.yaml, or config.yaml while the folder has only the old files. */
|
|
102
|
+
export function projectConfigFile(root) {
|
|
103
|
+
return usesLegacyConfig(root) ? LEGACY_CONFIG_FILE : CONFIG_FILE;
|
|
104
|
+
}
|
|
105
|
+
/** The file a stack's settings are read from, by name - the old name while the folder has only the old files. */
|
|
106
|
+
export function stackConfigFileIn(root, stack) {
|
|
107
|
+
return usesLegacyConfig(root) ? legacyStackConfigFile(stack) : stackConfigFile(stack);
|
|
108
|
+
}
|
|
109
|
+
/** Whether a folder holds a project's settings: a config.turri.yaml, a stack's file, or the old files. */
|
|
110
|
+
export function hasProjectConfig(root) {
|
|
111
|
+
return existsSync(join(root, CONFIG_FILE)) || stacksOf(root).length > 0 || usesLegacyConfig(root);
|
|
112
|
+
}
|
|
113
|
+
/** The stacks a project folder has files for, by name. */
|
|
114
|
+
export function stacksOf(root) {
|
|
115
|
+
if (usesLegacyConfig(root))
|
|
116
|
+
return legacyStacks(root);
|
|
117
|
+
return namesIn(root)
|
|
118
|
+
.map((name) => /^config\.turri\.(.+)\.yaml$/.exec(name)?.[1])
|
|
119
|
+
.filter((name) => name !== undefined)
|
|
120
|
+
.sort();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Before turri first writes a setting in a folder whose settings are still in the old files: config.yaml copied to
|
|
124
|
+
* config.turri.yaml, and each config.<stack>.yaml to config.turri.<stack>.yaml, so nothing is lost when the old files
|
|
125
|
+
* stop being read. The old files are left as they are. Gives back the files written, by name.
|
|
126
|
+
*/
|
|
127
|
+
export function adoptLegacyConfig(root, options = {}) {
|
|
128
|
+
if (!usesLegacyConfig(root))
|
|
129
|
+
return [];
|
|
130
|
+
const copies = [
|
|
131
|
+
// `projectFile: false`: the stacks' files alone - turri init-config writes config.turri.yaml itself.
|
|
132
|
+
...(options.projectFile !== false && existsSync(join(root, LEGACY_CONFIG_FILE)) ? [[LEGACY_CONFIG_FILE, CONFIG_FILE]] : []),
|
|
133
|
+
...legacyStacks(root).map((stack) => [legacyStackConfigFile(stack), stackConfigFile(stack)]),
|
|
134
|
+
];
|
|
135
|
+
for (const [from, to] of copies)
|
|
136
|
+
copyFileSync(join(root, from), join(root, to));
|
|
137
|
+
return copies.map(([, to]) => to);
|
|
138
|
+
}
|
|
139
|
+
/** The stack a pulse runs with: the one chosen as it started, whatever is chosen since. */
|
|
140
|
+
const pinnedStack = new AsyncLocalStorage();
|
|
141
|
+
/** The stack chosen now - `/stack`, `--stack`, `TURRI_STACK` - or, inside a pulse, the one it started with. */
|
|
142
|
+
export function chosenStack() {
|
|
143
|
+
const pinned = pinnedStack.getStore();
|
|
144
|
+
if (pinned !== undefined)
|
|
145
|
+
return pinned.stack;
|
|
146
|
+
return text(process.env.TURRI_STACK);
|
|
147
|
+
}
|
|
148
|
+
/** Choose the stack everything is built with from now on - or none, the project's config alone. Kept in TURRI_STACK. */
|
|
149
|
+
export function chooseStack(stack) {
|
|
150
|
+
if (stack === undefined || stack.trim() === "")
|
|
151
|
+
delete process.env.TURRI_STACK;
|
|
152
|
+
else
|
|
153
|
+
process.env.TURRI_STACK = stack.trim();
|
|
154
|
+
}
|
|
155
|
+
/** Run `work` with `stack` as the one chosen, whatever is chosen meanwhile: a pulse keeps the stack it started with. */
|
|
156
|
+
export function withStack(stack, work) {
|
|
157
|
+
return pinnedStack.run({ stack }, work);
|
|
158
|
+
}
|
|
159
|
+
/** The stack a project folder is built with now: the one chosen, when the folder has a file for it. */
|
|
160
|
+
export function stackOf(root) {
|
|
161
|
+
const stack = chosenStack();
|
|
162
|
+
return stack !== undefined && stacksOf(root).includes(stack) ? stack : undefined;
|
|
163
|
+
}
|
|
164
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
165
|
+
/**
|
|
166
|
+
* The agents turri starts, by what they do - each can be given its own model, thinking and turn limit (`models:`).
|
|
167
|
+
* The code fixer of a conformance run and the repair agent are coding agents; the unit-test and conformance arbiters
|
|
168
|
+
* are both `arbiter`.
|
|
169
|
+
*/
|
|
170
|
+
export const AGENT_ROLES = ["chat", "chooser", "planner", "diagnosis", "release", "coding", "conformance", "explorer", "reviewer", "test-reviewer", "judge", "arbiter", "debugger"];
|
|
171
|
+
/**
|
|
172
|
+
* How hard each role thinks when nothing names a level - not the config's `models:`, nor a `:level` on the model it is
|
|
173
|
+
* given. The agents that do the work - write the code and the tests, explore, release - think little: they work from a
|
|
174
|
+
* spec that leaves little to guess, and every turn of theirs is many. The ones that judge - review, weigh, plan, find a
|
|
175
|
+
* cause - think more: a wrong call there costs pulses. The conversation keeps its own level, pi's.
|
|
176
|
+
*/
|
|
177
|
+
export const ROLE_THINKING = {
|
|
178
|
+
coding: "low",
|
|
179
|
+
conformance: "low",
|
|
180
|
+
explorer: "low",
|
|
181
|
+
release: "low",
|
|
182
|
+
reviewer: "medium",
|
|
183
|
+
"test-reviewer": "medium",
|
|
184
|
+
judge: "medium",
|
|
185
|
+
arbiter: "medium",
|
|
186
|
+
debugger: "medium",
|
|
187
|
+
planner: "medium",
|
|
188
|
+
chooser: "medium",
|
|
189
|
+
diagnosis: "medium",
|
|
190
|
+
};
|
|
191
|
+
/** The roles on the conversation's side: without their own entry they take the chat's model, not `default`. */
|
|
192
|
+
// The release is not: it follows steps - a script, a start command - and runs on the coding model, thinking low.
|
|
193
|
+
const CHAT_SIDE = ["chat", "chooser", "planner", "diagnosis"];
|
|
194
|
+
function isThinkingLevel(value) {
|
|
195
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* A model choice as the one string every agent is given, in pi's own form: "provider/id", with
|
|
199
|
+
* ":level" when it names how hard to think - ":level" alone for the default model thinking so.
|
|
200
|
+
*/
|
|
201
|
+
export function modelSpec(choice) {
|
|
202
|
+
if (choice === undefined || (choice.model === undefined && choice.thinking === undefined))
|
|
203
|
+
return undefined;
|
|
204
|
+
return `${choice.model ?? ""}${choice.thinking !== undefined ? `:${choice.thinking}` : ""}`;
|
|
205
|
+
}
|
|
206
|
+
/** A model spec back into its model and thinking level - "provider/id:high", "provider/id", ":high". */
|
|
207
|
+
export function splitModelSpec(spec) {
|
|
208
|
+
const given = spec?.trim() ?? "";
|
|
209
|
+
if (given === "")
|
|
210
|
+
return {};
|
|
211
|
+
const colon = given.lastIndexOf(":");
|
|
212
|
+
const level = colon === -1 ? undefined : given.slice(colon + 1);
|
|
213
|
+
if (colon !== -1 && isThinkingLevel(level)) {
|
|
214
|
+
const model = given.slice(0, colon);
|
|
215
|
+
return { ...(model !== "" ? { model } : {}), thinking: level };
|
|
216
|
+
}
|
|
217
|
+
return { model: given };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A model spec's model alone, its thinking left behind: what a judging role falls back on when it runs on the coding
|
|
221
|
+
* model - the model, not how hard the coding agent was told to think. Its role's level (ROLE_THINKING) applies instead.
|
|
222
|
+
*/
|
|
223
|
+
export function modelOnly(spec) {
|
|
224
|
+
const { model } = splitModelSpec(spec);
|
|
225
|
+
return model !== undefined ? { model } : {};
|
|
226
|
+
}
|
|
227
|
+
/** A choice with what it leaves out taken from the one it inherits from - or that one, when it is not given at all. */
|
|
228
|
+
export function inheritModel(own, from) {
|
|
229
|
+
if (own === undefined)
|
|
230
|
+
return from;
|
|
231
|
+
const model = own.model ?? from?.model;
|
|
232
|
+
const thinking = own.thinking ?? from?.thinking;
|
|
233
|
+
return { ...(model !== undefined ? { model } : {}), ...(thinking !== undefined ? { thinking } : {}) };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The three models as they hold: the judge inherits from the coding model, the coding model from the harness model -
|
|
237
|
+
* `models:` over them: `chat` over harness-model, `coding` over coding-model, `judge` over judge-model, and `default`
|
|
238
|
+
* between the harness model and the rest.
|
|
239
|
+
*/
|
|
240
|
+
export function projectModels(config) {
|
|
241
|
+
const models = config.models ?? {};
|
|
242
|
+
const harness = inheritModel(choiceOf(models.chat), config.harnessModel);
|
|
243
|
+
const coding = inheritModel(choiceOf(models.coding), inheritModel(config.codingModel, inheritModel(choiceOf(models.default), harness)));
|
|
244
|
+
// The judge falls back on the coding model, not on how hard it thinks: a judging role thinks as ROLE_THINKING has it.
|
|
245
|
+
const codingModel = coding?.model !== undefined ? { model: coding.model } : undefined;
|
|
246
|
+
return { harness, coding, judge: inheritModel(choiceOf(models.judge), inheritModel(config.judgeModel, inheritModel(choiceOf(models.default), codingModel))) };
|
|
247
|
+
}
|
|
248
|
+
/** A role entry's model and thinking alone. */
|
|
249
|
+
function choiceOf(entry) {
|
|
250
|
+
if (entry === undefined || (entry.model === undefined && entry.thinking === undefined))
|
|
251
|
+
return undefined;
|
|
252
|
+
return { ...(entry.model !== undefined ? { model: entry.model } : {}), ...(entry.thinking !== undefined ? { thinking: entry.thinking } : {}) };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* What the config itself says a role runs on - its own entry, the legacy key that names it (judge-model for the judge
|
|
256
|
+
* and the test review), then `default` (the chat's side: the chat's model instead) - or nothing when it says nothing
|
|
257
|
+
* of it, and the role runs on what it is given: the job's model, which the command line and the conversation choose.
|
|
258
|
+
* The coding role always runs on what it is given: its config is already in it (projectModels).
|
|
259
|
+
*/
|
|
260
|
+
export function roleModel(config, role) {
|
|
261
|
+
if (role === "coding")
|
|
262
|
+
return undefined;
|
|
263
|
+
const models = config.models ?? {};
|
|
264
|
+
const own = choiceOf(models[role]);
|
|
265
|
+
// The debugger looks at the coding agent's failure: without its own entry it runs on the model it is given - a stronger one.
|
|
266
|
+
if (role === "debugger")
|
|
267
|
+
return own;
|
|
268
|
+
if (CHAT_SIDE.includes(role))
|
|
269
|
+
return own === undefined ? undefined : inheritModel(own, inheritModel(choiceOf(models.chat), config.harnessModel));
|
|
270
|
+
const legacy = role === "judge" || role === "test-reviewer" ? config.judgeModel : undefined;
|
|
271
|
+
return inheritModel(own, inheritModel(legacy, choiceOf(models.default)));
|
|
272
|
+
}
|
|
273
|
+
/** How long each turn of a role's agents may take, in milliseconds: its own, `default`'s, `turn-limit:`'s - or undefined, the harness's default. */
|
|
274
|
+
export function roleTurnLimitMs(config, role) {
|
|
275
|
+
const models = config.models ?? {};
|
|
276
|
+
const seconds = (role !== undefined ? models[role]?.turnLimit : undefined) ?? models.default?.turnLimit ?? config.turnLimit;
|
|
277
|
+
return seconds !== undefined ? seconds * 1000 : undefined;
|
|
278
|
+
}
|
|
279
|
+
/** A whole number of at least 1, as the config gives it - or nothing. */
|
|
280
|
+
function positiveInteger(value) {
|
|
281
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : undefined;
|
|
282
|
+
}
|
|
283
|
+
/** A model as the config gives it: a string, or `model:` and `thinking:`. */
|
|
284
|
+
function modelChoice(value) {
|
|
285
|
+
if (typeof value === "string")
|
|
286
|
+
return text(value) !== undefined ? splitModelSpec(value) : undefined;
|
|
287
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
288
|
+
return undefined;
|
|
289
|
+
const raw = value;
|
|
290
|
+
const model = text(raw.model) ?? text(raw.name) ?? text(raw.ref);
|
|
291
|
+
const thinking = isThinkingLevel(raw.thinking) ? raw.thinking : undefined;
|
|
292
|
+
if (model === undefined && thinking === undefined)
|
|
293
|
+
return undefined;
|
|
294
|
+
return { ...(model !== undefined ? { model } : {}), ...(thinking !== undefined ? { thinking } : {}) };
|
|
295
|
+
}
|
|
296
|
+
/** A role's entry under `models:`: a model string, or `model:`, `thinking:` and `turn-limit:`. */
|
|
297
|
+
function roleChoice(value) {
|
|
298
|
+
const choice = modelChoice(value);
|
|
299
|
+
const raw = value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
300
|
+
const turnLimit = turnLimitSetting(raw["turn-limit"] ?? raw.turn_limit);
|
|
301
|
+
if (choice === undefined && turnLimit === undefined)
|
|
302
|
+
return undefined;
|
|
303
|
+
return { ...(choice ?? {}), ...(turnLimit !== undefined ? { turnLimit } : {}) };
|
|
304
|
+
}
|
|
305
|
+
/** A turn limit in seconds: a whole number, 0 for none. */
|
|
306
|
+
function turnLimitSetting(value) {
|
|
307
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : undefined;
|
|
308
|
+
}
|
|
309
|
+
/** `models:` - each role's entry, by its name; `default` for every role not named. */
|
|
310
|
+
function modelsSetting(value) {
|
|
311
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
312
|
+
return undefined;
|
|
313
|
+
const models = {};
|
|
314
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
315
|
+
const name = key.replace(/_/g, "-");
|
|
316
|
+
if (name !== "default" && !AGENT_ROLES.includes(name))
|
|
317
|
+
continue;
|
|
318
|
+
const choice = roleChoice(entry);
|
|
319
|
+
if (choice !== undefined)
|
|
320
|
+
models[name] = choice;
|
|
321
|
+
}
|
|
322
|
+
return Object.keys(models).length > 0 ? models : undefined;
|
|
323
|
+
}
|
|
324
|
+
/** The config keys, as written in the file. */
|
|
325
|
+
export const CONFIG_KEYS = {
|
|
326
|
+
module: "module",
|
|
327
|
+
harnessModel: "harness-model",
|
|
328
|
+
codingModel: "coding-model",
|
|
329
|
+
judgeModel: "judge-model",
|
|
330
|
+
conformanceTests: "conformance-tests",
|
|
331
|
+
conformanceBudget: "conformance-budget",
|
|
332
|
+
review: "review",
|
|
333
|
+
allowToStopJobs: "allow-to-stop-jobs",
|
|
334
|
+
releaseAfterPulse: "release-after-pulse",
|
|
335
|
+
unittestsScript: "unittests-script",
|
|
336
|
+
conformanceTestsScript: "conformance-tests-script",
|
|
337
|
+
prepareEnvironmentScript: "prepare-environment-script",
|
|
338
|
+
testingScriptsRunOn: "testing-scripts-run-on",
|
|
339
|
+
parallelJobs: "parallel-jobs",
|
|
340
|
+
rulesOff: "rules-off",
|
|
341
|
+
compactAt: "compact-at",
|
|
342
|
+
maxSpecsPerPulse: "max-specs-per-pulse",
|
|
343
|
+
maxLinesPerPulse: "max-lines-per-pulse",
|
|
344
|
+
agentEnv: "agent-env",
|
|
345
|
+
toolchains: "toolchains",
|
|
346
|
+
models: "models",
|
|
347
|
+
turnLimit: "turn-limit",
|
|
348
|
+
};
|
|
349
|
+
function whatScriptsRunOn(value) {
|
|
350
|
+
return value === "copy" || value === "in-place" ? value : undefined;
|
|
351
|
+
}
|
|
352
|
+
/** `conformance-tests:` - true or all, chosen, module, false. Anything else is not read. */
|
|
353
|
+
function conformanceSetting(value) {
|
|
354
|
+
if (typeof value === "boolean")
|
|
355
|
+
return value;
|
|
356
|
+
const word = typeof value === "string" ? value.trim().toLowerCase() : undefined;
|
|
357
|
+
return word === "all" ? true : word === "chosen" ? "chosen" : word === "module" ? "module" : word === "false" ? false : word === "true" ? true : undefined;
|
|
358
|
+
}
|
|
359
|
+
/** Whether `conformance-tests:` turns the conformance tests on: all (true), chosen or module. Not set, they are off, as with false. */
|
|
360
|
+
export function conformanceSwitchedOn(setting) {
|
|
361
|
+
return setting === true || setting === "chosen" || setting === "module";
|
|
362
|
+
}
|
|
363
|
+
/** When the chooser decides which functional specs get conformance tests: after each code pulse, once the module is regenerated, or never - every one, or none. */
|
|
364
|
+
export function choosingWhen(setting) {
|
|
365
|
+
return setting === "chosen" ? "pulse" : setting === "module" ? "module" : undefined;
|
|
366
|
+
}
|
|
367
|
+
/** `conformance-budget:` - a whole number, or a share such as `30%`. Anything else is not read. */
|
|
368
|
+
function budgetSetting(value) {
|
|
369
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0)
|
|
370
|
+
return { count: value };
|
|
371
|
+
const share = typeof value === "string" ? /^\s*(\d+(?:\.\d+)?)\s*%\s*$/.exec(value) : null;
|
|
372
|
+
if (share !== null && Number(share[1]) <= 100)
|
|
373
|
+
return { share: Number(share[1]) / 100 };
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
/** How many functional specs a module of `count` may have chosen under a budget: the count, or the share rounded up - at least one. */
|
|
377
|
+
export function budgetFor(budget, count) {
|
|
378
|
+
return "count" in budget ? budget.count : Math.max(1, Math.ceil(budget.share * count));
|
|
379
|
+
}
|
|
380
|
+
function text(value) {
|
|
381
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The project's config - with a stack's over it, when one is named - or nothing of it when there is no file or it does
|
|
385
|
+
* not parse.
|
|
386
|
+
*/
|
|
387
|
+
export function readProjectConfig(root, stack) {
|
|
388
|
+
const project = readConfigFile(join(root, projectConfigFile(root)));
|
|
389
|
+
if (stack === undefined)
|
|
390
|
+
return project;
|
|
391
|
+
const own = readConfigFile(join(root, stackConfigFileIn(root, stack)));
|
|
392
|
+
// The project's alone: one conversation, one module built when none is named.
|
|
393
|
+
const merged = { ...project };
|
|
394
|
+
for (const key of Object.keys(CONFIG_KEYS)) {
|
|
395
|
+
if (key === "module" || key === "harnessModel" || key === "parallelJobs" || key === "rulesOff" || key === "compactAt" || key === "toolchains" || key === "agentEnv" || own[key] === undefined)
|
|
396
|
+
continue;
|
|
397
|
+
merged[key] = own[key];
|
|
398
|
+
}
|
|
399
|
+
merged.agentEnv = joinAgentEnv(project.agentEnv, own.agentEnv);
|
|
400
|
+
merged.toolchains = project.toolchains !== undefined && own.toolchains !== undefined ? [...new Set([...project.toolchains, ...own.toolchains])] : (own.toolchains ?? project.toolchains);
|
|
401
|
+
merged.codingModel = inheritModel(own.codingModel, project.codingModel);
|
|
402
|
+
merged.judgeModel = inheritModel(own.judgeModel, project.judgeModel);
|
|
403
|
+
merged.models = joinModels(project.models, own.models);
|
|
404
|
+
return withoutUndefined(merged);
|
|
405
|
+
}
|
|
406
|
+
/** A module's `models:` over the project's, role by role - each entry taking what it leaves out from the project's; the chat's the project's alone. */
|
|
407
|
+
function joinModels(project, own) {
|
|
408
|
+
if (own === undefined)
|
|
409
|
+
return project;
|
|
410
|
+
const joined = { ...(project ?? {}) };
|
|
411
|
+
for (const [role, entry] of Object.entries(own)) {
|
|
412
|
+
if (role === "chat")
|
|
413
|
+
continue;
|
|
414
|
+
const base = project?.[role];
|
|
415
|
+
const choice = inheritModel(choiceOf(entry), choiceOf(base));
|
|
416
|
+
const turnLimit = entry.turnLimit ?? base?.turnLimit;
|
|
417
|
+
joined[role] = { ...(choice ?? {}), ...(turnLimit !== undefined ? { turnLimit } : {}) };
|
|
418
|
+
}
|
|
419
|
+
return joined;
|
|
420
|
+
}
|
|
421
|
+
/** A module's `agent-env:` added to the project's. */
|
|
422
|
+
function joinAgentEnv(project, own) {
|
|
423
|
+
if (project === undefined || own === undefined)
|
|
424
|
+
return own ?? project;
|
|
425
|
+
return { files: [...new Set([...project.files, ...own.files])], vars: [...new Set([...project.vars, ...own.vars])] };
|
|
426
|
+
}
|
|
427
|
+
/** An `agent-env:` as the config gives it: dotenv files and variable names, or nothing when it names neither. */
|
|
428
|
+
function agentEnvSetting(value) {
|
|
429
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
430
|
+
return undefined;
|
|
431
|
+
const list = (items) => (Array.isArray(items) ? items : typeof items === "string" ? [items] : []).filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim());
|
|
432
|
+
const raw = value;
|
|
433
|
+
const setting = { files: list(raw.files), vars: list(raw.vars) };
|
|
434
|
+
return setting.files.length > 0 || setting.vars.length > 0 ? setting : undefined;
|
|
435
|
+
}
|
|
436
|
+
function withoutUndefined(config) {
|
|
437
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
|
|
438
|
+
}
|
|
439
|
+
/** The config a spec is built with: its project's, with the stack chosen over it. */
|
|
440
|
+
export function configForSpec(plainFile) {
|
|
441
|
+
const root = projectDirOf(resolve(plainFile));
|
|
442
|
+
return readProjectConfig(root, stackOf(root));
|
|
443
|
+
}
|
|
444
|
+
/** One config file, read, or nothing of it when it is not there or does not parse. */
|
|
445
|
+
function readConfigFile(path) {
|
|
446
|
+
let parsed;
|
|
447
|
+
try {
|
|
448
|
+
if (!statSync(path).isFile())
|
|
449
|
+
return {};
|
|
450
|
+
parsed = loadYaml(readFileSync(path, "utf8"));
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return {};
|
|
454
|
+
}
|
|
455
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
456
|
+
return {};
|
|
457
|
+
const raw = Object.fromEntries(Object.entries(parsed).map(([key, value]) => [key.replace(/_/g, "-"), value]));
|
|
458
|
+
const conformance = raw[CONFIG_KEYS.conformanceTests];
|
|
459
|
+
return {
|
|
460
|
+
module: text(raw[CONFIG_KEYS.module]),
|
|
461
|
+
harnessModel: modelChoice(raw[CONFIG_KEYS.harnessModel]),
|
|
462
|
+
codingModel: modelChoice(raw[CONFIG_KEYS.codingModel]) ?? modelChoice(raw.model),
|
|
463
|
+
judgeModel: modelChoice(raw[CONFIG_KEYS.judgeModel]),
|
|
464
|
+
conformanceTests: conformanceSetting(conformance),
|
|
465
|
+
conformanceBudget: budgetSetting(raw[CONFIG_KEYS.conformanceBudget]),
|
|
466
|
+
review: typeof raw[CONFIG_KEYS.review] === "boolean" ? raw[CONFIG_KEYS.review] : undefined,
|
|
467
|
+
allowToStopJobs: typeof raw[CONFIG_KEYS.allowToStopJobs] === "boolean" ? raw[CONFIG_KEYS.allowToStopJobs] : undefined,
|
|
468
|
+
releaseAfterPulse: typeof raw[CONFIG_KEYS.releaseAfterPulse] === "boolean" ? raw[CONFIG_KEYS.releaseAfterPulse] : undefined,
|
|
469
|
+
unittestsScript: text(raw[CONFIG_KEYS.unittestsScript]),
|
|
470
|
+
conformanceTestsScript: text(raw[CONFIG_KEYS.conformanceTestsScript]),
|
|
471
|
+
prepareEnvironmentScript: text(raw[CONFIG_KEYS.prepareEnvironmentScript]),
|
|
472
|
+
testingScriptsRunOn: whatScriptsRunOn(raw[CONFIG_KEYS.testingScriptsRunOn]),
|
|
473
|
+
agentEnv: agentEnvSetting(raw[CONFIG_KEYS.agentEnv]),
|
|
474
|
+
toolchains: Array.isArray(raw[CONFIG_KEYS.toolchains])
|
|
475
|
+
? raw[CONFIG_KEYS.toolchains].filter((path) => typeof path === "string" && path.trim() !== "").map((path) => path.trim())
|
|
476
|
+
: undefined,
|
|
477
|
+
rulesOff: Array.isArray(raw[CONFIG_KEYS.rulesOff]) ? raw[CONFIG_KEYS.rulesOff].filter((name) => typeof name === "string") : undefined,
|
|
478
|
+
compactAt: typeof raw[CONFIG_KEYS.compactAt] === "number" && Number.isInteger(raw[CONFIG_KEYS.compactAt]) && raw[CONFIG_KEYS.compactAt] >= 1000
|
|
479
|
+
? raw[CONFIG_KEYS.compactAt]
|
|
480
|
+
: undefined,
|
|
481
|
+
parallelJobs: typeof raw[CONFIG_KEYS.parallelJobs] === "number" && Number.isInteger(raw[CONFIG_KEYS.parallelJobs]) && raw[CONFIG_KEYS.parallelJobs] >= 1
|
|
482
|
+
? raw[CONFIG_KEYS.parallelJobs]
|
|
483
|
+
: undefined,
|
|
484
|
+
maxSpecsPerPulse: positiveInteger(raw[CONFIG_KEYS.maxSpecsPerPulse]),
|
|
485
|
+
maxLinesPerPulse: positiveInteger(raw[CONFIG_KEYS.maxLinesPerPulse]),
|
|
486
|
+
models: modelsSetting(raw[CONFIG_KEYS.models]),
|
|
487
|
+
turnLimit: turnLimitSetting(raw[CONFIG_KEYS.turnLimit]),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseArgs, runLoop } from "./loop-cli.js";
|
|
2
|
+
const USAGE = `usage: turri conform <file.plain> [frid ...] [options]
|
|
3
|
+
|
|
4
|
+
Writes and runs the black-box conformance tests of a module's functional specs - those named, or
|
|
5
|
+
every one - on the code as it is built, as the chat's conform does, whatever the config's
|
|
6
|
+
conformance-tests says: from the spec's test reqs and acceptance tests, with the whole suite the
|
|
7
|
+
module inherits, a failure fixed in the test or the code, whichever is wrong. Every functional spec
|
|
8
|
+
tested goes in one pulse, as far as the size guardrail lets.
|
|
9
|
+
|
|
10
|
+
--one-by-one a pulse a functional spec instead
|
|
11
|
+
--model <provider/id> model for the agents (default: config.turri.yaml's coding-model)`;
|
|
12
|
+
const { positional, options } = parseArgs(process.argv.slice(2), ["--one-by-one"], USAGE);
|
|
13
|
+
const [plainFile, ...frids] = positional;
|
|
14
|
+
if (plainFile === undefined) {
|
|
15
|
+
console.error(USAGE);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
runLoop({
|
|
19
|
+
plainFile,
|
|
20
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
21
|
+
ask: (loop) => loop.conformance(plainFile, {
|
|
22
|
+
...(frids.length > 0 ? { frids } : {}),
|
|
23
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
24
|
+
...(options["one-by-one"] !== undefined ? { separately: true } : {}),
|
|
25
|
+
}),
|
|
26
|
+
}).catch((error) => {
|
|
27
|
+
console.error(error instanceof Error ? error.message : error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { describeGates, runSkill } from "./genome.js";
|
|
5
|
+
import { runAsk } from "./ask.js";
|
|
6
|
+
import { skillRunDir } from "./regenerate/paths.js";
|
|
7
|
+
import { loadSkill } from "./skill.js";
|
|
8
|
+
import { discoverSkills } from "./skills.js";
|
|
9
|
+
import { PACKAGE_ROOT } from "./environment.js";
|
|
10
|
+
const c = {
|
|
11
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
12
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
13
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
14
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
15
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
16
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
17
|
+
};
|
|
18
|
+
const reporter = {
|
|
19
|
+
step: (name, attempt) => console.log(`\n${c.bold(c.cyan(`> step ${name}`))} ${c.dim(`attempt ${attempt}`)}`),
|
|
20
|
+
artifact: (path, body) => {
|
|
21
|
+
console.log(c.dim(` ${path}:`));
|
|
22
|
+
for (const line of body.split("\n"))
|
|
23
|
+
console.log(c.dim(` | ${line}`));
|
|
24
|
+
},
|
|
25
|
+
gate: (name, judges) => console.log(judges === 0
|
|
26
|
+
? `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim("no artifact - failing without spending judges")}`
|
|
27
|
+
: `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim(`${judges} independent judges`)}`),
|
|
28
|
+
livePass: (step, scope, findings) => console.log(findings === 0
|
|
29
|
+
? c.dim(` review ${step}: ${scope} - clean`)
|
|
30
|
+
: ` ${c.yellow(`review ${step}`)}: ${scope} ${c.dim(`- ${findings} to fix`)}`),
|
|
31
|
+
liveReview: (name, passes, findings) => console.log(findings === 0
|
|
32
|
+
? `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim(`reviewer read along in ${passes} passes, nothing outstanding`)}`
|
|
33
|
+
: `${c.bold(c.yellow(`> gate ${name}`))} ${c.dim(`${findings} unresolved from the reviewer - failing without spending judges`)}`),
|
|
34
|
+
judge: (index, model, verdict, violations) => {
|
|
35
|
+
const mark = verdict === "pass" ? c.green("pass") : c.red(verdict);
|
|
36
|
+
console.log(` judge ${index + 1} ${c.dim(`(${model})`)}: ${mark}${violations ? c.dim(` (${violations})`) : ""}`);
|
|
37
|
+
},
|
|
38
|
+
outcome: (name, decision, critique) => {
|
|
39
|
+
const colour = {
|
|
40
|
+
advance: c.green,
|
|
41
|
+
retry: c.yellow,
|
|
42
|
+
escalate: c.yellow,
|
|
43
|
+
abort: c.red,
|
|
44
|
+
};
|
|
45
|
+
console.log(` ${c.bold(colour[decision](decision))} ${c.dim(`<- gate ${name}`)}`);
|
|
46
|
+
if (critique)
|
|
47
|
+
for (const line of critique.split("\n"))
|
|
48
|
+
console.log(c.dim(` ${line}`));
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
async function main() {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const modelIndex = args.indexOf("--model");
|
|
54
|
+
const model = modelIndex === -1 ? undefined : args[modelIndex + 1];
|
|
55
|
+
const skillArg = args.filter((arg, i) => !arg.startsWith("--") && i !== modelIndex + 1)[0];
|
|
56
|
+
if (!skillArg) {
|
|
57
|
+
console.error("usage: turri cultivate <skill name | path-to-skill-dir> [--model provider/id]");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const cwd = process.cwd();
|
|
61
|
+
// A folder with a SKILL.md, or a skill by name: turri's own or the project's (skills.ts).
|
|
62
|
+
const named = existsSync(join(skillArg, "SKILL.md")) ? undefined : discoverSkills(PACKAGE_ROOT, cwd).skills.find((found) => found.name === skillArg);
|
|
63
|
+
const skillDir = named?.dir ?? skillArg;
|
|
64
|
+
const skill = loadSkill(skillDir);
|
|
65
|
+
console.log(c.bold(`skill: ${skill.name}`));
|
|
66
|
+
for (const line of describeGates(skill))
|
|
67
|
+
console.log(` ${c.dim(line)}`);
|
|
68
|
+
const result = await runSkill({
|
|
69
|
+
skillDir,
|
|
70
|
+
cwd,
|
|
71
|
+
runDir: skillRunDir(cwd, skill.name),
|
|
72
|
+
model,
|
|
73
|
+
report: reporter,
|
|
74
|
+
// The CLI has no dialogs, so options are numbered and either the number or the text
|
|
75
|
+
// is accepted - the point is still to pick rather than to type the answer out.
|
|
76
|
+
askUser: (request) => runAsk(request, {
|
|
77
|
+
// The terminal scrolls a long brief itself: it is printed whole, above the question.
|
|
78
|
+
select: async (title, options, context) => {
|
|
79
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
const list = options.map((option, i) => ` ${c.bold(String(i + 1))}. ${option}`).join("\n");
|
|
81
|
+
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.cyan("? "))}${title}\n${list}\n> `)).trim();
|
|
82
|
+
rl.close();
|
|
83
|
+
const index = Number.parseInt(reply, 10);
|
|
84
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length)
|
|
85
|
+
return options[index - 1];
|
|
86
|
+
return reply === "" ? undefined : reply;
|
|
87
|
+
},
|
|
88
|
+
input: async (title, context) => {
|
|
89
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
+
const reply = (await rl.question(`\n${context !== undefined ? `${context}\n\n` : ""}${c.bold(c.cyan("? "))}${title}\n> `)).trim();
|
|
91
|
+
rl.close();
|
|
92
|
+
return reply === "" ? undefined : reply;
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
escalate: async ({ question, critique }) => {
|
|
96
|
+
console.log(`\n${c.bold(c.yellow("! human review required"))}`);
|
|
97
|
+
console.log(c.dim(critique));
|
|
98
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
99
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
100
|
+
rl.close();
|
|
101
|
+
return answer === "y";
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
console.log(`\n${result.ok ? c.green(c.bold("run complete")) : c.red(c.bold("run aborted at a gate"))} ${c.dim(`artifacts in ${relative(cwd, skillRunDir(cwd, skill.name))}`)}`);
|
|
105
|
+
process.exit(result.ok ? 0 : 1);
|
|
106
|
+
}
|
|
107
|
+
main().catch((err) => {
|
|
108
|
+
console.error(err);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readProjectConfig, CONFIG_FILE } from "./config.js";
|
|
2
|
+
import { createJobRegistry } from "./regenerate/jobs.js";
|
|
3
|
+
import { createWorkLoop, describeDrift } from "./regenerate/loop.js";
|
|
4
|
+
const USAGE = `usage: turri drift [file.plain]
|
|
5
|
+
|
|
6
|
+
How far the module's code has drifted from its spec, in each codebase where it lives: what is left to
|
|
7
|
+
regenerate, and the conformance tests left to write. Plans nothing, starts nothing, calls no model.`;
|
|
8
|
+
const [given] = process.argv.slice(2).filter((arg) => !arg.startsWith("-"));
|
|
9
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
10
|
+
console.log(USAGE);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
const named = given ?? readProjectConfig(process.cwd()).module;
|
|
14
|
+
if (named === undefined) {
|
|
15
|
+
console.error(`${USAGE}\n\nNo file given, and ${CONFIG_FILE} names no module.`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const loop = createWorkLoop({ registry: createJobRegistry(), cwd: process.cwd(), planner: false, dryRun: true });
|
|
20
|
+
console.log(describeDrift(loop.drift(named)));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error(error instanceof Error ? error.message : error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|