@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,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many questions an interview may put to the person in each phase it caps, and how broad each is - the
|
|
3
|
+
* questioning level, the first thing a skill with `metadata.questioning` asks. Light, medium and thorough cap each such
|
|
4
|
+
* phase at 3, 5 and 10 questions; best judgement leaves it to the agent, which says when it has asked enough. Every
|
|
5
|
+
* level covers the same ground: fewer questions are broader ones, and what they leave open the agent decides itself.
|
|
6
|
+
* The level limits the questions, never the spec changes. The phase's closing confirmation, asked with
|
|
7
|
+
* `confirms_phase`, is never counted.
|
|
8
|
+
*/
|
|
9
|
+
export const QUESTIONING_LEVELS = [
|
|
10
|
+
{
|
|
11
|
+
name: "light",
|
|
12
|
+
cap: 3,
|
|
13
|
+
option: "Light - at most 3 broad questions a phase, turri decides the details",
|
|
14
|
+
breadth: "Ask broad questions: each settles a whole area of the phase at once - the features and their order, what the product does end to end - proposing what fits the brief for the person to confirm or correct. The details are yours.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "medium",
|
|
18
|
+
cap: 5,
|
|
19
|
+
option: "Medium - at most 5 questions a phase, each a feature or topic",
|
|
20
|
+
breadth: "Ask about one feature or topic a question, with its main rules in the same question. The edge cases are yours.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "thorough",
|
|
24
|
+
cap: 10,
|
|
25
|
+
option: "Thorough - at most 10 narrow questions a phase, edge cases included",
|
|
26
|
+
breadth: "Ask narrow questions: one feature, rule or edge case a question, with follow-ups on the ones that matter most.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "best judgement",
|
|
30
|
+
option: "Best judgement - turri decides how much each phase asks",
|
|
31
|
+
breadth: "Ask as broadly or as narrowly as each area needs - narrow where the person's answer changes the product, broad or not at all where the brief already says.",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
const BEST_JUDGEMENT = QUESTIONING_LEVELS.at(-1);
|
|
35
|
+
/** The level an answer picks: by its name, wherever it is in the answer; anything else is best judgement. */
|
|
36
|
+
export function levelOf(answer) {
|
|
37
|
+
const said = answer.toLowerCase();
|
|
38
|
+
return QUESTIONING_LEVELS.find((level) => said.startsWith(level.name)) ?? QUESTIONING_LEVELS.find((level) => said.includes(level.name.split(" ")[0])) ?? BEST_JUDGEMENT;
|
|
39
|
+
}
|
|
40
|
+
/** The question the harness puts first, and what the person reads above it. */
|
|
41
|
+
export function questioningQuestion(phases) {
|
|
42
|
+
const named = phases.length === 1 ? `the ${phases[0]} phase` : `the ${phases.slice(0, -1).join(", ")} and ${phases.at(-1)} phases`;
|
|
43
|
+
return {
|
|
44
|
+
question: `How many questions should I ask you in ${named}?`,
|
|
45
|
+
options: QUESTIONING_LEVELS.map((level) => level.option),
|
|
46
|
+
context: `Light, medium and thorough cap each of ${named} at 3, 5 and 10 questions - broader with fewer, so every level ` +
|
|
47
|
+
"covers the same ground, only in less or more depth. What the questions leave open, turri decides itself and " +
|
|
48
|
+
"says what it chose. With best judgement, turri asks as much as it judges each phase needs. Each phase still " +
|
|
49
|
+
"ends with your confirmation of its summary.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function createQuestioning(options) {
|
|
53
|
+
const { level, phases } = options;
|
|
54
|
+
const asked = { ...(options.from?.asked ?? {}) };
|
|
55
|
+
const over = new Set(options.from?.over ?? []);
|
|
56
|
+
let phase = "";
|
|
57
|
+
const capped = () => phases.includes(phase);
|
|
58
|
+
const state = () => ({ level: level.name, asked: { ...asked }, over: [...over] });
|
|
59
|
+
const changed = () => options.onChange?.(state());
|
|
60
|
+
const decideTheRest = "Decide what is left yourself, as a careful engineer would, in the light of the intent brief, and say in one line what you chose.";
|
|
61
|
+
return {
|
|
62
|
+
level,
|
|
63
|
+
enter(next) {
|
|
64
|
+
phase = next;
|
|
65
|
+
},
|
|
66
|
+
refusal(confirmsPhase) {
|
|
67
|
+
if (!capped() || confirmsPhase || !over.has(phase))
|
|
68
|
+
return undefined;
|
|
69
|
+
const why = level.cap !== undefined && (asked[phase] ?? 0) >= level.cap
|
|
70
|
+
? `${level.name} questioning allows ${level.cap} questions in the ${phase} phase, and all ${level.cap} are asked`
|
|
71
|
+
: `you ended the ${phase} phase's questions`;
|
|
72
|
+
return `This question was not asked: ${why}. ${decideTheRest} Only the phase's closing confirmation, with confirms_phase, may still be asked.`;
|
|
73
|
+
},
|
|
74
|
+
answered(confirmsPhase) {
|
|
75
|
+
if (!capped() || confirmsPhase)
|
|
76
|
+
return;
|
|
77
|
+
asked[phase] = (asked[phase] ?? 0) + 1;
|
|
78
|
+
if (level.cap !== undefined && asked[phase] >= level.cap)
|
|
79
|
+
over.add(phase);
|
|
80
|
+
changed();
|
|
81
|
+
},
|
|
82
|
+
end() {
|
|
83
|
+
if (!capped())
|
|
84
|
+
return `The ${phase} phase's questions are not capped: ask what it needs.`;
|
|
85
|
+
over.add(phase);
|
|
86
|
+
changed();
|
|
87
|
+
return `The ${phase} phase's questions are over. ${decideTheRest} End the phase with its summary and its confirmation, asked with confirms_phase.`;
|
|
88
|
+
},
|
|
89
|
+
note(next, first) {
|
|
90
|
+
const chosen = `The person chose ${level.name} questioning${first ? " - the harness asked it, so do not ask it again" : ""}.`;
|
|
91
|
+
if (!phases.includes(next)) {
|
|
92
|
+
return first ? `${chosen} It caps the questions of the ${phases.join(" and ")} phases, not this one.` : undefined;
|
|
93
|
+
}
|
|
94
|
+
const limit = level.cap !== undefined
|
|
95
|
+
? `Ask at most ${level.cap} questions with ask_user in this phase, follow-ups included; past them ask_user refuses. To stop sooner, call end_questions.`
|
|
96
|
+
: "Stop asking once the phase is settled enough to build: call end_questions.";
|
|
97
|
+
return [
|
|
98
|
+
chosen,
|
|
99
|
+
limit,
|
|
100
|
+
level.breadth,
|
|
101
|
+
"Whatever the level, the questions cover all of the phase's topics: the level sets their depth, not their reach.",
|
|
102
|
+
"The phase's closing confirmation is asked with confirms_phase: true and is not counted.",
|
|
103
|
+
`What the questions leave open: ${decideTheRest}`,
|
|
104
|
+
"The level limits the questions, never the spec changes: write as many as the phase needs, decided ones included.",
|
|
105
|
+
].join(" ");
|
|
106
|
+
},
|
|
107
|
+
state,
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/quiet.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps library chatter out of the terminal while a tool runs.
|
|
3
|
+
*
|
|
4
|
+
* A skill run spawns many nested agent sessions - an executor, two judges per gate, a fix
|
|
5
|
+
* agent, each queued render - and libraries underneath them write to the console. In the
|
|
6
|
+
* TUI every one of those writes interrupts the differential renderer, which abandons the
|
|
7
|
+
* current frame and leaves it stranded in scrollback. Dozens of "Working" lines pile up.
|
|
8
|
+
*
|
|
9
|
+
* Only the `console` methods are intercepted, never `process.stdout.write` itself: that is
|
|
10
|
+
* how pi draws the TUI, and silencing it would blank the screen. Library noise goes
|
|
11
|
+
* through `console`; pi's rendering does not.
|
|
12
|
+
*/
|
|
13
|
+
const METHODS = ["log", "info", "warn", "error", "debug", "trace"];
|
|
14
|
+
/**
|
|
15
|
+
* Warnings that say nothing a person can act on, dropped wherever they are printed. Google's SDK
|
|
16
|
+
* says this once per client, so once per agent, when both keys are set - pi offers the google
|
|
17
|
+
* provider on GEMINI_API_KEY, and a machine set up for other tools often has GOOGLE_API_KEY too.
|
|
18
|
+
*/
|
|
19
|
+
const NOISE = [/^Both GOOGLE_API_KEY and GEMINI_API_KEY are set\. Using GOOGLE_API_KEY\.$/];
|
|
20
|
+
function isNoise(args) {
|
|
21
|
+
return args.length === 1 && typeof args[0] === "string" && NOISE.some((pattern) => pattern.test(args[0]));
|
|
22
|
+
}
|
|
23
|
+
let noiseDropped = false;
|
|
24
|
+
/** Drop the NOISE warnings for good, from here on - captured or not. Safe to call more than once. */
|
|
25
|
+
export function dropNoise() {
|
|
26
|
+
if (noiseDropped)
|
|
27
|
+
return;
|
|
28
|
+
noiseDropped = true;
|
|
29
|
+
for (const method of ["warn", "error", "log", "info", "debug"]) {
|
|
30
|
+
const print = console[method];
|
|
31
|
+
console[method] = (...args) => {
|
|
32
|
+
if (!isNoise(args))
|
|
33
|
+
print.apply(console, args);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Buffers currently collecting. Nested and concurrent captures all see the same writes. */
|
|
38
|
+
const active = new Set();
|
|
39
|
+
const original = new Map();
|
|
40
|
+
function install() {
|
|
41
|
+
if (original.size > 0)
|
|
42
|
+
return;
|
|
43
|
+
for (const method of METHODS) {
|
|
44
|
+
original.set(method, console[method]);
|
|
45
|
+
console[method] = (...args) => {
|
|
46
|
+
if (isNoise(args))
|
|
47
|
+
return;
|
|
48
|
+
const line = args
|
|
49
|
+
.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg)))
|
|
50
|
+
.join(" ");
|
|
51
|
+
for (const buffer of active)
|
|
52
|
+
buffer.push(line);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function restore() {
|
|
57
|
+
if (active.size > 0 || original.size === 0)
|
|
58
|
+
return;
|
|
59
|
+
for (const [method, fn] of original)
|
|
60
|
+
console[method] = fn;
|
|
61
|
+
original.clear();
|
|
62
|
+
}
|
|
63
|
+
function safeStringify(value) {
|
|
64
|
+
try {
|
|
65
|
+
return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return String(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Run `fn` with console output collected instead of printed. */
|
|
72
|
+
export async function captureConsole(fn) {
|
|
73
|
+
// An escape hatch for debugging: with this set, library output goes to the terminal as
|
|
74
|
+
// it used to, which is how the capture itself can be measured.
|
|
75
|
+
if (process.env.TURRI_NO_CAPTURE === "1")
|
|
76
|
+
return { result: await fn(), output: [] };
|
|
77
|
+
const buffer = [];
|
|
78
|
+
active.add(buffer);
|
|
79
|
+
install();
|
|
80
|
+
try {
|
|
81
|
+
return { result: await fn(), output: buffer };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
active.delete(buffer);
|
|
85
|
+
restore();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Capture that spans something other than one promise - a whole agent turn, say.
|
|
90
|
+
*
|
|
91
|
+
* `captureConsole` covers work it can wrap. A conversation's own model calls happen inside
|
|
92
|
+
* pi's agent loop, where there is nothing to wrap, so those are bracketed by hand instead.
|
|
93
|
+
* `end` is safe to call twice and safe to skip: an unbalanced `begin` leaves the console
|
|
94
|
+
* patched, so callers must end it from a handler that always runs.
|
|
95
|
+
*/
|
|
96
|
+
export function beginCapture() {
|
|
97
|
+
if (process.env.TURRI_NO_CAPTURE === "1")
|
|
98
|
+
return { end: () => [] };
|
|
99
|
+
const buffer = [];
|
|
100
|
+
active.add(buffer);
|
|
101
|
+
install();
|
|
102
|
+
let ended = false;
|
|
103
|
+
return {
|
|
104
|
+
end: () => {
|
|
105
|
+
if (ended)
|
|
106
|
+
return buffer;
|
|
107
|
+
ended = true;
|
|
108
|
+
active.delete(buffer);
|
|
109
|
+
restore();
|
|
110
|
+
return buffer;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** One line noting what was swallowed, or nothing when it was quiet. */
|
|
115
|
+
export function describeCaptured(output) {
|
|
116
|
+
if (output.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const unique = [...new Set(output.map((line) => line.trim()).filter(Boolean))];
|
|
119
|
+
const shown = unique.slice(0, 3);
|
|
120
|
+
const rest = unique.length - shown.length;
|
|
121
|
+
return (`(${output.length} line${output.length === 1 ? "" : "s"} of library output suppressed: ` +
|
|
122
|
+
shown.join(" | ") +
|
|
123
|
+
(rest > 0 ? ` | and ${rest} more` : "") +
|
|
124
|
+
")");
|
|
125
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/** Say which functional spec is being built now - shown with the job, and in its transcript. */
|
|
3
|
+
export function reportSpec(spec) {
|
|
4
|
+
store.getStore()?.spec?.(spec);
|
|
5
|
+
}
|
|
6
|
+
/** Say the spec contradicts itself, to the job this runs for. */
|
|
7
|
+
export function reportSpecConflict(conflict) {
|
|
8
|
+
store.getStore()?.specConflict?.(conflict);
|
|
9
|
+
}
|
|
10
|
+
/** Raised inside a job that was asked to stop, so the work unwinds instead of carrying on. */
|
|
11
|
+
export class JobStoppedError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("Stopped on request.");
|
|
14
|
+
this.name = "JobStoppedError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The stop signal of the job this code is running for, if it is running for one. */
|
|
18
|
+
export function currentStopSignal() {
|
|
19
|
+
return store.getStore()?.signal;
|
|
20
|
+
}
|
|
21
|
+
/** The job this code is running for, if it is running for one. */
|
|
22
|
+
export function currentJob() {
|
|
23
|
+
return store.getStore()?.job;
|
|
24
|
+
}
|
|
25
|
+
export function reportTranscript(entry) {
|
|
26
|
+
store.getStore()?.transcript(entry);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What a stage is about, said so the person knows what the name is: "module backend", or "module
|
|
30
|
+
* backend, functionality 2". A stage leads with it: `${unitOf(moduleName, frid)}: running the tests`.
|
|
31
|
+
*/
|
|
32
|
+
export function unitOf(moduleName, frid) {
|
|
33
|
+
return frid === undefined ? `module ${moduleName}` : `module ${moduleName}, functionality ${frid}`;
|
|
34
|
+
}
|
|
35
|
+
export function reportUsage(usage) {
|
|
36
|
+
store.getStore()?.usage(usage);
|
|
37
|
+
}
|
|
38
|
+
export function reportAgent(report) {
|
|
39
|
+
store.getStore()?.agent?.(report);
|
|
40
|
+
}
|
|
41
|
+
/** Add one agent report to a job's tally, by label - its first report starts its row. */
|
|
42
|
+
export function addAgentReport(tallies, report) {
|
|
43
|
+
let tally = tallies.find((entry) => entry.label === report.label);
|
|
44
|
+
if (tally === undefined) {
|
|
45
|
+
tally = { label: report.label, turns: 0, compactions: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 } };
|
|
46
|
+
tallies.push(tally);
|
|
47
|
+
}
|
|
48
|
+
if (report.model !== undefined)
|
|
49
|
+
tally.model = report.model;
|
|
50
|
+
tally.turns += report.turns ?? 0;
|
|
51
|
+
tally.compactions += report.compactions ?? 0;
|
|
52
|
+
const spent = report.usage;
|
|
53
|
+
if (spent !== undefined) {
|
|
54
|
+
tally.usage = {
|
|
55
|
+
input: tally.usage.input + spent.input,
|
|
56
|
+
output: tally.usage.output + spent.output,
|
|
57
|
+
cacheRead: tally.usage.cacheRead + spent.cacheRead,
|
|
58
|
+
cacheWrite: tally.usage.cacheWrite + spent.cacheWrite,
|
|
59
|
+
cost: tally.usage.cost + spent.cost,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** A job's agents, a line each: "app 1 reviewer · 3 turns · 1 compaction · 210.4k tok · $0.41". */
|
|
64
|
+
export function describeAgents(tallies) {
|
|
65
|
+
return tallies.map((tally) => {
|
|
66
|
+
const spent = describeUsage(tally.usage);
|
|
67
|
+
return [
|
|
68
|
+
tally.label,
|
|
69
|
+
`${tally.turns} turn${tally.turns === 1 ? "" : "s"}`,
|
|
70
|
+
...(tally.compactions > 0 ? [`${tally.compactions} compaction${tally.compactions === 1 ? "" : "s"}`] : []),
|
|
71
|
+
...(spent !== "" ? [spent] : []),
|
|
72
|
+
...(tally.model !== undefined ? [tally.model] : []),
|
|
73
|
+
].join(" · ");
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** "1.2k tok · $0.03", or "" when nothing has been spent yet. */
|
|
77
|
+
export function describeUsage(usage) {
|
|
78
|
+
if (usage === undefined)
|
|
79
|
+
return "";
|
|
80
|
+
const tokens = usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
81
|
+
if (tokens === 0)
|
|
82
|
+
return "";
|
|
83
|
+
const size = tokens >= 1_000_000 ? `${(tokens / 1_000_000).toFixed(1)}M` : tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : `${tokens}`;
|
|
84
|
+
return usage.cost > 0 ? `${size} tok · $${usage.cost.toFixed(2)}` : `${size} tok`;
|
|
85
|
+
}
|
|
86
|
+
const store = new AsyncLocalStorage();
|
|
87
|
+
export function withActivity(sink, run) {
|
|
88
|
+
return store.run(sink, run);
|
|
89
|
+
}
|
|
90
|
+
export function reportStage(label) {
|
|
91
|
+
store.getStore()?.stage(label);
|
|
92
|
+
}
|
|
93
|
+
export function reportActivity(line) {
|
|
94
|
+
store.getStore()?.activity(line);
|
|
95
|
+
}
|
|
96
|
+
/** Say something about the machinery under the work: shown as activity, and kept in the job's log, which the activity is not. */
|
|
97
|
+
export function reportNote(line) {
|
|
98
|
+
const sink = store.getStore();
|
|
99
|
+
sink?.activity(line);
|
|
100
|
+
sink?.note?.(line);
|
|
101
|
+
}
|
|
102
|
+
/** Say something for the job's log alone: the machinery, not what its agents are doing. */
|
|
103
|
+
export function reportLogNote(line) {
|
|
104
|
+
store.getStore()?.note?.(line);
|
|
105
|
+
}
|
|
106
|
+
export function reportNow(label, options) {
|
|
107
|
+
store.getStore()?.now(label, options);
|
|
108
|
+
}
|
|
109
|
+
const MAX_ARG = 60;
|
|
110
|
+
function clip(text) {
|
|
111
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
112
|
+
return one.length > MAX_ARG ? `${one.slice(0, MAX_ARG - 1)}…` : one;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Paths as they are inside `plain_modules`: everything up to and including it left out, wherever
|
|
116
|
+
* a path to it appears - `/Users/…/project/plain_modules/app/code/main.py` is `app/code/main.py`.
|
|
117
|
+
* The rest of the path is the same for every line, and is what pushes the part that differs off
|
|
118
|
+
* the end of it.
|
|
119
|
+
*/
|
|
120
|
+
export function withinModules(text) {
|
|
121
|
+
return text.replace(/(?:(?:\/|~\/)(?:[^\s'"`/]+\/)*|(?<![\w.-]))plain_modules\//g, "");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* What the person is shown a tool as, where that is not the name its agent calls it by. The agent's name says
|
|
125
|
+
* what the call takes - `write_spec_diff` takes a diff of the spec - and the shown one speaks turri's language.
|
|
126
|
+
*/
|
|
127
|
+
const SHOWN_AS = {
|
|
128
|
+
write_spec_diff: "mutate",
|
|
129
|
+
};
|
|
130
|
+
/** A tool's name as the person is shown it. */
|
|
131
|
+
export function shownName(name) {
|
|
132
|
+
return SHOWN_AS[name] ?? name;
|
|
133
|
+
}
|
|
134
|
+
/** A tool call as one short line. Unknown tools still show up, by name. */
|
|
135
|
+
export function describeToolCall(name, args) {
|
|
136
|
+
const text = (key) => (typeof args?.[key] === "string" ? withinModules(args[key]) : undefined);
|
|
137
|
+
const path = text("path");
|
|
138
|
+
const command = text("command");
|
|
139
|
+
const pattern = text("pattern");
|
|
140
|
+
switch (name) {
|
|
141
|
+
case "write":
|
|
142
|
+
return `✎ write ${path ?? ""}`.trimEnd();
|
|
143
|
+
case "edit":
|
|
144
|
+
return `✎ edit ${path ?? ""}`.trimEnd();
|
|
145
|
+
// Every kind of call has its mark, so every line has the same shape - a reviewer that only reads as well.
|
|
146
|
+
case "read":
|
|
147
|
+
return `○ read ${path ?? ""}`.trimEnd();
|
|
148
|
+
case "ls":
|
|
149
|
+
case "find":
|
|
150
|
+
case "grep":
|
|
151
|
+
return `⌕ ${name} ${clip(pattern ?? path ?? "")}`.trimEnd();
|
|
152
|
+
case "web_search":
|
|
153
|
+
case "web_fetch":
|
|
154
|
+
return `⇢ ${name} ${clip(text("query") ?? text("url") ?? "")}`.trimEnd();
|
|
155
|
+
case "bash":
|
|
156
|
+
return `$ ${clip(command ?? "")}`;
|
|
157
|
+
case "run_unit_tests":
|
|
158
|
+
case "run_conformance_tests":
|
|
159
|
+
return `▶ ${name === "run_unit_tests" ? "unit tests" : "conformance tests"}: the project's script`;
|
|
160
|
+
// turri's own tools: what they do, at a glance.
|
|
161
|
+
case "read_output":
|
|
162
|
+
return `○ read_output`;
|
|
163
|
+
case "my_diff":
|
|
164
|
+
return `± my_diff`;
|
|
165
|
+
case "update_notes":
|
|
166
|
+
return `✐ update_notes`;
|
|
167
|
+
case "copy_resource":
|
|
168
|
+
return `⎘ copy_resource ${path ?? text("to") ?? ""}`.trimEnd();
|
|
169
|
+
case "can_it_fail":
|
|
170
|
+
return `? can_it_fail`;
|
|
171
|
+
case "ask_explorer":
|
|
172
|
+
return `? ask_explorer`;
|
|
173
|
+
default:
|
|
174
|
+
// A hand-over - submit_work, submit_review, a verdict - ends with a check; anything else, a dot.
|
|
175
|
+
return `${/^submit_/.test(name) ? "✓" : "•"} ${shownName(name)}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** The outcome of a test run, or nothing for tools whose outcome is not worth a line. */
|
|
179
|
+
export function describeToolResult(name, result, isError) {
|
|
180
|
+
if (name !== "run_unit_tests" && name !== "run_conformance_tests") {
|
|
181
|
+
return isError ? ` ✗ ${shownName(name)} failed` : undefined;
|
|
182
|
+
}
|
|
183
|
+
const exitCode = result?.details?.exitCode;
|
|
184
|
+
if (typeof exitCode !== "number")
|
|
185
|
+
return undefined;
|
|
186
|
+
return exitCode === 0 ? " ✓ passing" : ` ✗ failing (exit ${exitCode})`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { readProjectConfig, stackOf } from "../config.js";
|
|
6
|
+
import { registerSecrets } from "./masking.js";
|
|
7
|
+
import { projectRootOf, modulesScratchDir } from "./paths.js";
|
|
8
|
+
/**
|
|
9
|
+
* The environment every agent command gets, on this machine.
|
|
10
|
+
*
|
|
11
|
+
* Not the host's: the shell turri was started in holds the providers' API keys, cloud credentials, tokens and an SSH
|
|
12
|
+
* agent, and an agent's command - or the code it runs - could read any of them. So only an allow-list passes: the path,
|
|
13
|
+
* the locale, the terminal, the user, and the variables that locate the host's toolchains. Then turri's own: a HOME of
|
|
14
|
+
* the module's own, kept between pulses, so a tool's settings and an agent's dotfiles never land in the person's; a
|
|
15
|
+
* TMPDIR; the package caches in the project, so a dependency downloads once; and git stopped at the module, so a
|
|
16
|
+
* project that is itself a repository is not found from the code. Last, what the project passes on purpose -
|
|
17
|
+
* `agent-env:` in its config - which never replaces turri's own.
|
|
18
|
+
*/
|
|
19
|
+
/** Passed from the host as they are. */
|
|
20
|
+
const ALLOWED = ["PATH", "LANG", "TERM", "TZ", "USER", "LOGNAME", "COLORTERM"];
|
|
21
|
+
const ALLOWED_PREFIXES = ["LC_"];
|
|
22
|
+
/**
|
|
23
|
+
* The variables that locate a toolchain, each with where it is in the person's home when the host sets none - a fake
|
|
24
|
+
* HOME would otherwise hide it. Without a default, only the host's.
|
|
25
|
+
*/
|
|
26
|
+
const TOOLCHAIN_LOCATORS = {
|
|
27
|
+
JAVA_HOME: undefined,
|
|
28
|
+
GOROOT: undefined,
|
|
29
|
+
GOPATH: undefined,
|
|
30
|
+
DOTNET_ROOT: undefined,
|
|
31
|
+
ANDROID_HOME: undefined,
|
|
32
|
+
ANDROID_SDK_ROOT: undefined,
|
|
33
|
+
FLUTTER_ROOT: undefined,
|
|
34
|
+
M2_HOME: undefined,
|
|
35
|
+
MAVEN_HOME: undefined,
|
|
36
|
+
GRADLE_HOME: undefined,
|
|
37
|
+
RUSTUP_HOME: ".rustup",
|
|
38
|
+
NVM_DIR: ".nvm",
|
|
39
|
+
PYENV_ROOT: ".pyenv",
|
|
40
|
+
SDKMAN_DIR: ".sdkman",
|
|
41
|
+
VOLTA_HOME: ".volta",
|
|
42
|
+
ASDF_DATA_DIR: ".asdf",
|
|
43
|
+
// mise and fnm keep their installs under ~/.local/share; uv the Pythons it fetches.
|
|
44
|
+
MISE_DATA_DIR: ".local/share/mise",
|
|
45
|
+
FNM_DIR: ".local/share/fnm",
|
|
46
|
+
RBENV_ROOT: ".rbenv",
|
|
47
|
+
UV_PYTHON_INSTALL_DIR: ".local/share/uv/python",
|
|
48
|
+
BUN_INSTALL: ".bun",
|
|
49
|
+
PNPM_HOME: undefined,
|
|
50
|
+
};
|
|
51
|
+
/** The variables that locate a toolchain - their folders are what the OS sandbox lets a command read of the person's. */
|
|
52
|
+
export const TOOLCHAIN_VARIABLES = Object.keys(TOOLCHAIN_LOCATORS);
|
|
53
|
+
/** Where package managers keep their caches: in the project's scratch area, so a dependency downloads once. */
|
|
54
|
+
export function packageCacheDir(dir) {
|
|
55
|
+
return join(modulesScratchDir(dir), "cache");
|
|
56
|
+
}
|
|
57
|
+
/** Every package manager's cache, pointed into `cache`. */
|
|
58
|
+
export function cacheEnv(cache) {
|
|
59
|
+
return {
|
|
60
|
+
XDG_CACHE_HOME: cache,
|
|
61
|
+
MAVEN_OPTS: `-Dmaven.repo.local=${join(cache, "m2")}`,
|
|
62
|
+
GRADLE_USER_HOME: join(cache, "gradle"),
|
|
63
|
+
CARGO_HOME: join(cache, "cargo"),
|
|
64
|
+
npm_config_cache: join(cache, "npm"),
|
|
65
|
+
YARN_CACHE_FOLDER: join(cache, "yarn"),
|
|
66
|
+
PIP_CACHE_DIR: join(cache, "pip"),
|
|
67
|
+
UV_CACHE_DIR: join(cache, "uv"),
|
|
68
|
+
GOCACHE: join(cache, "go-build"),
|
|
69
|
+
GOMODCACHE: join(cache, "go-mod"),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The git identity an agent's commits in a scratch repository get - never the person's. */
|
|
73
|
+
const GITCONFIG = "[user]\n\tname = turri\n\temail = turri@localhost\n[init]\n\tdefaultBranch = main\n";
|
|
74
|
+
/**
|
|
75
|
+
* The environment of a command an agent runs in `moduleDir` - `plain_modules/<module>`. `hostEnv`: the environment
|
|
76
|
+
* turri was started in; `home`: the person's home folder.
|
|
77
|
+
*/
|
|
78
|
+
export function agentEnvironment(moduleDir, options = {}) {
|
|
79
|
+
const hostEnv = options.hostEnv ?? process.env;
|
|
80
|
+
const home = options.home ?? homedir();
|
|
81
|
+
const module = resolve(moduleDir);
|
|
82
|
+
const name = basename(module);
|
|
83
|
+
const env = {};
|
|
84
|
+
for (const [key, value] of Object.entries(hostEnv)) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
if (ALLOWED.includes(key) || ALLOWED_PREFIXES.some((prefix) => key.startsWith(prefix)))
|
|
88
|
+
env[key] = value;
|
|
89
|
+
}
|
|
90
|
+
for (const [key, inHome] of Object.entries(TOOLCHAIN_LOCATORS)) {
|
|
91
|
+
const given = hostEnv[key];
|
|
92
|
+
if (given !== undefined && given !== "")
|
|
93
|
+
env[key] = given;
|
|
94
|
+
else if (inHome !== undefined && existsSync(join(home, inHome)))
|
|
95
|
+
env[key] = join(home, inHome);
|
|
96
|
+
}
|
|
97
|
+
const { values, missing } = passedCredentials(module, hostEnv);
|
|
98
|
+
// From now on their values are masked in everything an agent is shown and turri writes.
|
|
99
|
+
registerSecrets(values);
|
|
100
|
+
Object.assign(env, values);
|
|
101
|
+
Object.assign(env, turrisOwn(module, name));
|
|
102
|
+
return { env, passed: Object.keys(values).sort(), values, missing };
|
|
103
|
+
}
|
|
104
|
+
/** HOME, TMPDIR, the caches and git's ceiling: what no setting of the project's replaces. */
|
|
105
|
+
function turrisOwn(module, name) {
|
|
106
|
+
const scratch = modulesScratchDir(module);
|
|
107
|
+
const fakeHome = join(scratch, "home", name);
|
|
108
|
+
mkdirSync(fakeHome, { recursive: true });
|
|
109
|
+
const gitconfig = join(fakeHome, ".gitconfig");
|
|
110
|
+
if (!existsSync(gitconfig))
|
|
111
|
+
writeFileSync(gitconfig, GITCONFIG);
|
|
112
|
+
const tmp = join(scratch, "tmp", name);
|
|
113
|
+
mkdirSync(tmp, { recursive: true });
|
|
114
|
+
// There before the first command: bubblewrap binds every writable folder, and refuses one that is not.
|
|
115
|
+
mkdirSync(packageCacheDir(module), { recursive: true });
|
|
116
|
+
// Java takes its temporary folder and its home from the system, not from TMPDIR and HOME - on macOS a per-user folder
|
|
117
|
+
// and the person's home, both outside the sandbox. Every JVM reads JAVA_TOOL_OPTIONS, a test JVM Surefire or Gradle
|
|
118
|
+
// forks too, and an agent's own MAVEN_OPTS does not replace it: Maven without its repository setting falls back to the
|
|
119
|
+
// module's home, not the person's ~/.m2. (Each JVM says on stderr that it picked them up.)
|
|
120
|
+
return {
|
|
121
|
+
...cacheEnv(packageCacheDir(module)),
|
|
122
|
+
JAVA_TOOL_OPTIONS: `-Djava.io.tmpdir=${tmp} -Duser.home=${fakeHome}`,
|
|
123
|
+
HOME: fakeHome,
|
|
124
|
+
TMPDIR: tmp,
|
|
125
|
+
GIT_CEILING_DIRECTORIES: module,
|
|
126
|
+
...(process.platform === "darwin" ? { [DARWIN_TEMP_SUFFIX]: darwinTempSuffix(module, name) } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* On macOS, what a program asks the system for as the per-user temporary folder - not TMPDIR - is that folder with this
|
|
131
|
+
* suffix added: a folder of the module's own inside it. The JVM keeps its attach sockets there (`.java_pid<pid>`), which
|
|
132
|
+
* Mockito's inline mocks, jcmd and jstack attach through; the sandbox reaches this folder alone, never the person's
|
|
133
|
+
* JVMs' sockets beside it.
|
|
134
|
+
*/
|
|
135
|
+
export const DARWIN_TEMP_SUFFIX = "DIRHELPER_USER_DIR_SUFFIX";
|
|
136
|
+
/** The module's own folder in macOS's per-user temporary folder: named for it, and for where it is. */
|
|
137
|
+
function darwinTempSuffix(module, name) {
|
|
138
|
+
return `turri-${name.replace(/[^\w.-]/g, "_")}-${createHash("sha256").update(resolve(module)).digest("hex").slice(0, 8)}`;
|
|
139
|
+
}
|
|
140
|
+
/** What `agent-env:` - the project's, with the chosen stack's added - passes, and what of it is not there. Reads only. */
|
|
141
|
+
export function passedCredentials(module, hostEnv) {
|
|
142
|
+
const root = projectRootOf(module);
|
|
143
|
+
const setting = readProjectConfig(root, stackOf(root)).agentEnv;
|
|
144
|
+
const values = {};
|
|
145
|
+
const missing = [];
|
|
146
|
+
if (setting === undefined)
|
|
147
|
+
return { values, missing };
|
|
148
|
+
for (const file of setting.files) {
|
|
149
|
+
const path = resolve(root, file);
|
|
150
|
+
if (!existsSync(path)) {
|
|
151
|
+
missing.push({ name: file, why: `no such file: ${path}` });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
Object.assign(values, parseDotenv(readFileSync(path, "utf8")));
|
|
155
|
+
}
|
|
156
|
+
for (const name of setting.vars) {
|
|
157
|
+
const value = hostEnv[name];
|
|
158
|
+
if (value === undefined)
|
|
159
|
+
missing.push({ name, why: "not set in the environment turri was started in" });
|
|
160
|
+
else if (value === "")
|
|
161
|
+
missing.push({ name, why: "set, but empty, in the environment turri was started in" });
|
|
162
|
+
else
|
|
163
|
+
values[name] = value;
|
|
164
|
+
}
|
|
165
|
+
return { values, missing };
|
|
166
|
+
}
|
|
167
|
+
/** A dotenv file's variables: `KEY=value`, `export KEY=value`, quoted or not; comments and blank lines skipped. */
|
|
168
|
+
export function parseDotenv(text) {
|
|
169
|
+
const values = {};
|
|
170
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
171
|
+
const line = raw.trim();
|
|
172
|
+
if (line === "" || line.startsWith("#"))
|
|
173
|
+
continue;
|
|
174
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
175
|
+
if (match === null)
|
|
176
|
+
continue;
|
|
177
|
+
let value = match[2];
|
|
178
|
+
const quote = value[0];
|
|
179
|
+
if ((quote === '"' || quote === "'") && value.lastIndexOf(quote) > 0) {
|
|
180
|
+
value = value.slice(1, value.lastIndexOf(quote));
|
|
181
|
+
if (quote === '"')
|
|
182
|
+
value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
value = value.replace(/\s+#.*$/, "");
|
|
186
|
+
}
|
|
187
|
+
values[match[1]] = value;
|
|
188
|
+
}
|
|
189
|
+
return values;
|
|
190
|
+
}
|