@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,310 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
4
|
+
import { credentialRefusal, isCredentialFile } from "./credentials.js";
|
|
5
|
+
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { NOTES_FILE } from "./notes.js";
|
|
7
|
+
import { RECORD_FILE } from "./records.js";
|
|
8
|
+
import { keptShellOutput, outputsDir, readOutputTool } from "./testing.js";
|
|
9
|
+
import { reportLogNote } from "./activity.js";
|
|
10
|
+
import { agentShell } from "./agent-shell.js";
|
|
11
|
+
import { ENVIRONMENT_ERROR_TOOL } from "./environment-error.js";
|
|
12
|
+
import { anotherPulseIn, COMMAND_TIMEOUT_MS, MAX_COMMAND_TIMEOUT_MS } from "./shell.js";
|
|
13
|
+
export class ConfinementError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ConfinementError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Resolve symlinks as far as the path exists, so a link cannot walk out of a root. */
|
|
20
|
+
function canonical(path) {
|
|
21
|
+
let head = resolve(path);
|
|
22
|
+
const tail = [];
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (existsSync(head))
|
|
25
|
+
return join(realpathSync(head), ...tail.reverse());
|
|
26
|
+
const parent = dirname(head);
|
|
27
|
+
if (parent === head)
|
|
28
|
+
return resolve(path);
|
|
29
|
+
tail.push(basename(head));
|
|
30
|
+
head = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function within(path, root) {
|
|
34
|
+
const target = canonical(path);
|
|
35
|
+
const base = canonical(root);
|
|
36
|
+
return target === base || target.startsWith(base + sep);
|
|
37
|
+
}
|
|
38
|
+
export function insideAny(path, roots) {
|
|
39
|
+
return roots.some((root) => within(path, root));
|
|
40
|
+
}
|
|
41
|
+
/** What the tool was given, turned into the absolute path it will actually open. */
|
|
42
|
+
function absolutize(raw, cwd) {
|
|
43
|
+
let path = raw.trim();
|
|
44
|
+
if (path.startsWith("@"))
|
|
45
|
+
path = path.slice(1);
|
|
46
|
+
if (path === "~" || path.startsWith(`~${sep}`))
|
|
47
|
+
path = join(homedir(), path.slice(1));
|
|
48
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
49
|
+
}
|
|
50
|
+
function refuse(path, roots, verb) {
|
|
51
|
+
throw new ConfinementError(`${path} is outside this agent's ${verb} directory. It may only ${verb} ` +
|
|
52
|
+
`${roots.join(" and ")}. Work inside those - nothing above them is yours to touch.`);
|
|
53
|
+
}
|
|
54
|
+
/** The working notes are written through update_notes, a line at a time - never as a file. */
|
|
55
|
+
function refuseNotes(path) {
|
|
56
|
+
if (basename(path) !== NOTES_FILE)
|
|
57
|
+
return;
|
|
58
|
+
throw new ConfinementError(`${NOTES_FILE} is not written as a file. Call update_notes to add, change or remove its lines.`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What is the harness's, not the agent's, to write: the project's testing scripts - the harness
|
|
62
|
+
* runs them, through run_unit_tests and run_conformance_tests - and the specs. A copy of either in
|
|
63
|
+
* the codebase is an agent running its own version of what the harness runs, or building from a
|
|
64
|
+
* spec it was never meant to hold.
|
|
65
|
+
*/
|
|
66
|
+
function refuseHarnessFiles(path) {
|
|
67
|
+
const name = basename(path);
|
|
68
|
+
if (isCredentialFile(path))
|
|
69
|
+
throw new ConfinementError(credentialRefusal(path));
|
|
70
|
+
if (path.split(sep).includes("test_scripts") || /^(run_unittests|run_unit_tests|run_conformance_tests|prepare_environment)[^/]*\.sh$/.test(name)) {
|
|
71
|
+
throw new ConfinementError(`${name} is a copy of the project's testing scripts, and those are not written here. They are not in your ` +
|
|
72
|
+
"workspace, and are never copied, recreated or run by you: call run_unit_tests and run_conformance_tests, " +
|
|
73
|
+
"which run them exactly as the harness does.");
|
|
74
|
+
}
|
|
75
|
+
if (name.endsWith(".plain")) {
|
|
76
|
+
throw new ConfinementError(`${name} is a ***plain spec, and specs are not written here. What you build from is in your prompt; the code is ` +
|
|
77
|
+
"what you write.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The record of what the code is built from is the harness's: no agent reads it either - nothing in it is theirs to go by. */
|
|
81
|
+
function refuseRecord(path) {
|
|
82
|
+
if (basename(path) !== RECORD_FILE)
|
|
83
|
+
return;
|
|
84
|
+
throw new ConfinementError(`${RECORD_FILE} is the harness's record of what the code is built from: no agent reads or writes it.`);
|
|
85
|
+
}
|
|
86
|
+
/** An output turri kept, named as the cut named it: not a file in the agent's folder, but read_output's to read. */
|
|
87
|
+
function refuseKeptOutput(raw, path, cwd) {
|
|
88
|
+
const id = basename(raw);
|
|
89
|
+
if (id !== raw.trim() || existsSync(path) || !existsSync(join(outputsDir(cwd), id)))
|
|
90
|
+
return;
|
|
91
|
+
throw new ConfinementError(`${id} is an output turri kept, not a file in your folder: call read_output with that id.`);
|
|
92
|
+
}
|
|
93
|
+
function check(raw, cwd, roots, verb) {
|
|
94
|
+
if (raw === undefined)
|
|
95
|
+
return;
|
|
96
|
+
const path = absolutize(raw, cwd);
|
|
97
|
+
if (verb === "read")
|
|
98
|
+
refuseKeptOutput(raw, path, cwd);
|
|
99
|
+
if (!insideAny(path, roots))
|
|
100
|
+
refuse(path, roots, verb);
|
|
101
|
+
refuseRecord(path);
|
|
102
|
+
if (verb === "write") {
|
|
103
|
+
refuseNotes(path);
|
|
104
|
+
refuseHarnessFiles(path);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** A glob is a path too: `../../*` escapes just as well as `..`. */
|
|
108
|
+
function checkPattern(pattern) {
|
|
109
|
+
if (pattern === undefined)
|
|
110
|
+
return;
|
|
111
|
+
const parts = pattern.split("/");
|
|
112
|
+
if (isAbsolute(pattern) || pattern.startsWith("~") || parts.includes("..")) {
|
|
113
|
+
throw new ConfinementError(`'${pattern}' reaches outside this agent's directory. Patterns must stay relative to it.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Same tool, its listing without the record of what the code is built from: ls, grep and find do not show it. */
|
|
117
|
+
function withoutRecord(def) {
|
|
118
|
+
return {
|
|
119
|
+
...def,
|
|
120
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
121
|
+
const result = await def.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
122
|
+
const content = result.content.map((part) => part.type === "text" ? { ...part, text: part.text.split("\n").filter((line) => !line.includes(RECORD_FILE)).join("\n") } : part);
|
|
123
|
+
return { ...result, content };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Same tool, with the path arguments checked before it runs. */
|
|
128
|
+
function guard(def, inspect) {
|
|
129
|
+
return {
|
|
130
|
+
...def,
|
|
131
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
132
|
+
inspect(params);
|
|
133
|
+
return def.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The same check again, one layer down, on the absolute path the tool resolved.
|
|
139
|
+
*
|
|
140
|
+
* The argument check reads the request; this reads what the tool decided the request meant.
|
|
141
|
+
* They agree in every ordinary case, and when they do not - a `~` spelt oddly, a filename
|
|
142
|
+
* normalised to another form on disk - this is the one that is true.
|
|
143
|
+
*/
|
|
144
|
+
function guardedOps(roots, verb) {
|
|
145
|
+
return (path) => {
|
|
146
|
+
if (!insideAny(path, roots))
|
|
147
|
+
refuse(path, roots, verb);
|
|
148
|
+
refuseRecord(path);
|
|
149
|
+
if (verb === "write") {
|
|
150
|
+
refuseNotes(path);
|
|
151
|
+
refuseHarnessFiles(path);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* An agent's walls, in words, for its system prompt - from its fence itself, so it says exactly what is enforced, the
|
|
157
|
+
* same on every platform. An agent that does not know where its walls are spends its turns walking into them. Its own
|
|
158
|
+
* folders by absolute path and no other; matter-of-fact about what happens on a reach outside - refused, or put back -
|
|
159
|
+
* and no warning. `environmentTool`: the tool an agent reports a missing piece of the machine with, when it has one.
|
|
160
|
+
*/
|
|
161
|
+
export function sandboxNote(fence) {
|
|
162
|
+
const list = (dirs) => dirs.map((dir) => `\`${dir}\``).join(" and ");
|
|
163
|
+
const readOnly = fence.read.filter((dir) => !fence.write.includes(dir));
|
|
164
|
+
const isTests = (dir) => basename(dir) === "tests" || dir.split(sep).includes("tests");
|
|
165
|
+
const lines = ["<sandbox>"];
|
|
166
|
+
if (fence.write.length === 0) {
|
|
167
|
+
lines.push(`You read ${list(fence.read)}, and write nothing${fence.shell ? ": not with your file tools, not with your shell." : "."}`);
|
|
168
|
+
}
|
|
169
|
+
else if (readOnly.length === 0) {
|
|
170
|
+
lines.push(fence.write.length === 1
|
|
171
|
+
? `You work in one folder: ${list(fence.write)}. Your file tools read and write it, and nothing else.`
|
|
172
|
+
: `You work in ${list(fence.write)}. Your file tools read and write them, and nothing else.`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const writesTests = fence.write.every(isTests);
|
|
176
|
+
const readsTests = readOnly.every(isTests);
|
|
177
|
+
const what = writesTests ? " - the code under test -" : readsTests ? " - the conformance tests the code must pass -" : "";
|
|
178
|
+
const why = writesTests ? ": you write tests, not code" : readsTests ? ": you fix the code, not the tests" : "";
|
|
179
|
+
lines.push(`You work in ${list(fence.write)}. Your file tools read and write it. They also read ${list(readOnly)}${what} but never write ` +
|
|
180
|
+
`${readOnly.length === 1 ? "it" : "them"}${why}.`);
|
|
181
|
+
}
|
|
182
|
+
if (fence.shell) {
|
|
183
|
+
const home = fence.write[0] ?? fence.read[0];
|
|
184
|
+
lines.push("", `Your shell runs on this machine, in ${home !== undefined ? `\`${home}\`` : "your folder"}, with this machine's toolchains. It also has a home ` +
|
|
185
|
+
"folder and a temporary folder of its own (`$HOME`, `$TMPDIR`), and the package caches your build tools already " +
|
|
186
|
+
"use. The network is open. Nothing else on the machine exists for you: not the specification files, not the " +
|
|
187
|
+
`project's other folders or modules, not the person's home.${readOnly.length > 0 && fence.write.length > 0 ? ` ${list(readOnly)} ${readOnly.length === 1 ? "is" : "are"} read only in your shell too.` : ""}`, "", "- Everything you need is in this prompt. Do not look outside your folder: no parent folders, no absolute paths", " elsewhere. A command that names a path outside is refused before it runs. Anything a command changes outside is", " put back, and you are told.");
|
|
188
|
+
const report = fence.environmentTool !== undefined
|
|
189
|
+
? `report it as an environment error with ${fence.environmentTool}`
|
|
190
|
+
: "say so plainly - what is missing and what needs it - and end your turn";
|
|
191
|
+
lines.push(fence.write.length > 0
|
|
192
|
+
? "- The project's dependencies are yours: declare them in the build manifest, and let the build tools fetch them."
|
|
193
|
+
: "- You install nothing.", `${fence.write.length > 0 ? " Installing anything else is not" : " Nor is installing anything on the machine yours"}: no toolchains, system packages, drivers, global packages or \`sudo\`. If the`, ` machine lacks something the code needs, ${report}, name what is missing, and go no further with what depends on it.`, " The person decides.", "- Ports and processes: you may stop only what you started. If a port is taken by something else, use another port.", " `ps` may show nothing but what you started: check a server you started with `kill -0 $!`, its log, or a request to it.", "- Temporary files go in `$TMPDIR`: `/tmp` is outside your walls.");
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push("You have no shell. Nothing else exists for you; everything you need is in this prompt.");
|
|
197
|
+
}
|
|
198
|
+
lines.push("</sandbox>");
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
/** pi's bash tool over an agent's shell: each command checked, run behind its walls, audited. */
|
|
202
|
+
export function hostBashOperations(shell) {
|
|
203
|
+
return {
|
|
204
|
+
exec: async (command, _cwd, { onData, signal, timeout }) => {
|
|
205
|
+
const result = await shell.run(command, {
|
|
206
|
+
onData,
|
|
207
|
+
...(signal !== undefined ? { signal } : {}),
|
|
208
|
+
// The agent's own timeout, up to the longest allowed; the limit when it gave none.
|
|
209
|
+
timeoutMs: timeout && timeout > 0 ? Math.min(timeout * 1000, MAX_COMMAND_TIMEOUT_MS) : COMMAND_TIMEOUT_MS,
|
|
210
|
+
});
|
|
211
|
+
return { exitCode: result.exitCode };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function confinedTools(c) {
|
|
216
|
+
const workdir = resolve(c.workdir);
|
|
217
|
+
// An agent's file tools reach its own folder and what its role is given beside it, and nothing else.
|
|
218
|
+
const read = (c.read ?? [workdir]).map((dir) => resolve(dir));
|
|
219
|
+
const write = (c.write ?? [workdir]).map((dir) => resolve(dir));
|
|
220
|
+
const readOps = guardedOps(read, "read");
|
|
221
|
+
const writeOps = guardedOps(write, "write");
|
|
222
|
+
const tools = [
|
|
223
|
+
guard(createReadToolDefinition(workdir, {
|
|
224
|
+
operations: {
|
|
225
|
+
readFile: async (path) => (readOps(path), readFileSync(path)),
|
|
226
|
+
access: async (path) => readOps(path),
|
|
227
|
+
},
|
|
228
|
+
}), (p) => check(p.path, workdir, read, "read")),
|
|
229
|
+
guard(createWriteToolDefinition(workdir, {
|
|
230
|
+
operations: {
|
|
231
|
+
writeFile: async (path, content) => (writeOps(path), writeFileSync(path, content)),
|
|
232
|
+
mkdir: async (dir) => {
|
|
233
|
+
writeOps(dir);
|
|
234
|
+
const { mkdirSync } = await import("node:fs");
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}), (p) => check(p.path, workdir, write, "write")),
|
|
239
|
+
guard(createEditToolDefinition(workdir, {
|
|
240
|
+
operations: {
|
|
241
|
+
readFile: async (path) => (writeOps(path), readFileSync(path)),
|
|
242
|
+
writeFile: async (path, content) => (writeOps(path), writeFileSync(path, content)),
|
|
243
|
+
access: async (path) => writeOps(path),
|
|
244
|
+
},
|
|
245
|
+
}), (p) => check(p.path, workdir, write, "write")),
|
|
246
|
+
withoutRecord(guard(createLsToolDefinition(workdir), (p) => check(p.path, workdir, read, "read"))),
|
|
247
|
+
withoutRecord(guard(createGrepToolDefinition(workdir), (p) => {
|
|
248
|
+
check(p.path, workdir, read, "read");
|
|
249
|
+
checkPattern(p.glob);
|
|
250
|
+
})),
|
|
251
|
+
withoutRecord(guard(createFindToolDefinition(workdir), (p) => {
|
|
252
|
+
check(p.path, workdir, read, "read");
|
|
253
|
+
checkPattern(p.pattern);
|
|
254
|
+
})),
|
|
255
|
+
];
|
|
256
|
+
// Nothing to write to: no tool that writes.
|
|
257
|
+
if (write.length === 0) {
|
|
258
|
+
for (const name of ["write", "edit"])
|
|
259
|
+
tools.splice(tools.findIndex((tool) => tool.name === name), 1);
|
|
260
|
+
}
|
|
261
|
+
let description = `file tools fenced to ${read.join(", ")}${write.length === 0 ? ", read only" : ""}`;
|
|
262
|
+
// The shell runs on this machine behind every wall at once: the check, the module's environment, the OS sandbox
|
|
263
|
+
// where one is up, the audit - its role's fence the same as its file tools'.
|
|
264
|
+
if (c.noBash === true) {
|
|
265
|
+
description += ", no bash";
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const shell = agentShell({ workdir, read, write }, { busy: anotherPulseIn });
|
|
269
|
+
const bash = createBashToolDefinition(workdir, { operations: hostBashOperations(shell), exposeSessionEnvironment: false });
|
|
270
|
+
bash.description = `${bash.description}\n\n${hostEnvironmentNote()}\n\n${timeLimitNote()}`;
|
|
271
|
+
tools.push(withKeptOutput(bash, workdir));
|
|
272
|
+
description += `, bash - ${shell.description}`;
|
|
273
|
+
// Which walls are up, in the job's log.
|
|
274
|
+
reportLogNote(` ${shell.description}`);
|
|
275
|
+
// A cut output - a command's, a test run's - is read whole through read_output, never by a path.
|
|
276
|
+
tools.push(readOutputTool(workdir));
|
|
277
|
+
}
|
|
278
|
+
// An agent that writes and runs code is given report_environment_error (pi.ts), and its walls name it.
|
|
279
|
+
return {
|
|
280
|
+
tools,
|
|
281
|
+
names: tools.map((t) => t.name),
|
|
282
|
+
description,
|
|
283
|
+
note: sandboxNote({ read, write, shell: c.noBash !== true, ...(c.noBash !== true && c.reportsEnvironment === true ? { environmentTool: ENVIRONMENT_ERROR_TOOL } : {}) }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** What the agent is told, with its bash, about where its commands run. */
|
|
287
|
+
function hostEnvironmentNote() {
|
|
288
|
+
return ("Commands run on this machine, from your folder, with this machine's toolchains, your own $HOME and $TMPDIR, and " +
|
|
289
|
+
"the open network. A command that names a path outside your folders is refused before it runs; what one changes " +
|
|
290
|
+
"outside them is put back, and you are told. You install nothing on the machine: the project's dependencies go " +
|
|
291
|
+
"into the project, through its manifest.");
|
|
292
|
+
}
|
|
293
|
+
/** The shell, its cut output kept where read_output reads it rather than in a temporary file on the host. */
|
|
294
|
+
function withKeptOutput(bash, workdir) {
|
|
295
|
+
return {
|
|
296
|
+
...bash,
|
|
297
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
298
|
+
const result = await bash.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
299
|
+
const content = result.content.map((part) => (part.type === "text" ? { ...part, text: keptShellOutput(workdir, part.text) } : part));
|
|
300
|
+
return { ...result, content };
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** What the agent is told about how long its commands may run. */
|
|
305
|
+
function timeLimitNote() {
|
|
306
|
+
return (`A command is stopped after ${Math.round(COMMAND_TIMEOUT_MS / 1000)} seconds unless you give it a timeout, and ` +
|
|
307
|
+
`after ${Math.round(MAX_COMMAND_TIMEOUT_MS / 1000)} at most. Never run one that does not end on its own in the ` +
|
|
308
|
+
"foreground: start a server or a watcher in the background (`cmd > app.log 2>&1 &`), and give a command that " +
|
|
309
|
+
"asks for input its input (`--yes`, `-B`, `CI=1`, `< /dev/null`).");
|
|
310
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { digestFiles } from "./codebase.js";
|
|
3
|
+
import { getFrids, getInheritedFunctionalSpecs, TEST_REQUIREMENTS } from "../plain/spec.js";
|
|
4
|
+
import { reportLogNote, reportStage, unitOf } from "./activity.js";
|
|
5
|
+
import { conformanceDirFor, notLandedWhy, renderConformanceTests } from "./conformance.js";
|
|
6
|
+
import { abandonWork, startWork } from "./lineage.js";
|
|
7
|
+
import { ensureRegressionPasses } from "./regression.js";
|
|
8
|
+
import { modulesRoot, projectDirOf } from "./paths.js";
|
|
9
|
+
import { applySpecDiff, snapshotSpec, specGapsText } from "./regenerate.js";
|
|
10
|
+
import { linesAddedSince, readNotes } from "./notes.js";
|
|
11
|
+
import { sectionsOf } from "./state.js";
|
|
12
|
+
import { describeStop } from "./stop-report.js";
|
|
13
|
+
import { missingTestLabels } from "./test-kinds.js";
|
|
14
|
+
import { findTestScripts, projectRootFor, requireTestScripts } from "./test-scripts.js";
|
|
15
|
+
export async function renderSpecDiffConformance(options) {
|
|
16
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
17
|
+
const templateDirs = options.templateDirs ?? [];
|
|
18
|
+
const applied = options.applied ?? applySpecDiff(options.diff, cwd);
|
|
19
|
+
const spec = options.spec ?? snapshotSpec(options.plainFile, { cwd, templateDirs, inModule: options.inModule });
|
|
20
|
+
const { moduleName, tree, chain } = spec;
|
|
21
|
+
const tip = chain.at(-1);
|
|
22
|
+
const stage = (label) => reportStage(`${unitOf(moduleName)}: ${label}`);
|
|
23
|
+
stage(`the spec diff is ${applied === "applied" ? "applied to the .plain files" : "already in the .plain files"} - ` +
|
|
24
|
+
`bringing the conformance tests in ${moduleName}'s codebase in line with it`);
|
|
25
|
+
// The change is tested whatever it touches - with or without a functional spec yet.
|
|
26
|
+
const frids = [...getFrids(tree)];
|
|
27
|
+
const lastFrid = frids.at(-1);
|
|
28
|
+
if ((sectionsOf(tree)[TEST_REQUIREMENTS] ?? []).length === 0) {
|
|
29
|
+
throw new Error(`${moduleName} has no ${TEST_REQUIREMENTS}, so conformance tests cannot be written: they say how conformance tests ` +
|
|
30
|
+
"are written and run. Add them first - a change to them is a spec diff like any other.");
|
|
31
|
+
}
|
|
32
|
+
stage("finding the project's testing scripts");
|
|
33
|
+
const root = projectRootFor(tip.plainFile, tip.buildDir);
|
|
34
|
+
const scripts = findTestScripts(root, tip.plainFile, tip.buildDir);
|
|
35
|
+
requireTestScripts(moduleName, scripts, { conformance: true });
|
|
36
|
+
reportLogNote(` conformance testing script: ${relative(root, scripts.conformance)}`);
|
|
37
|
+
const buildDir = tip.buildDir;
|
|
38
|
+
const conformanceRoot = modulesRoot(projectDirOf(tip.plainFile));
|
|
39
|
+
const conformanceDir = conformanceDirFor(moduleName, conformanceRoot);
|
|
40
|
+
// From the codebase's last commit, the tests' too: the change is made to the last known good
|
|
41
|
+
// state, not to what an interrupted run left half-written. The tests and the whole suite are
|
|
42
|
+
// one unit, landed whole or undone whole.
|
|
43
|
+
const unit = startWork([buildDir, conformanceDir], moduleName);
|
|
44
|
+
// The notes as its agents find them: a spec gap they add is told with the result.
|
|
45
|
+
const notesBefore = { code: readNotes(buildDir), tests: readNotes(conformanceDir) };
|
|
46
|
+
const conformance = await renderConformanceTests({
|
|
47
|
+
plainFile: tip.plainFile,
|
|
48
|
+
frid: lastFrid,
|
|
49
|
+
templateDirs,
|
|
50
|
+
buildDir,
|
|
51
|
+
conformanceDir,
|
|
52
|
+
diff: options.diff,
|
|
53
|
+
...(options.change !== undefined ? { change: options.change } : {}),
|
|
54
|
+
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
55
|
+
spec: { moduleName, tree },
|
|
56
|
+
model: options.model,
|
|
57
|
+
onDelta: options.onDelta,
|
|
58
|
+
});
|
|
59
|
+
// What the codebase now has conformance tests for, as their labels say.
|
|
60
|
+
const tested = () => frids.filter((frid) => missingTestLabels(conformanceDir, frid, []).length === 0);
|
|
61
|
+
// Read before a rollback takes the notes back: a gap is the person's to decide, whether the tests passed or not.
|
|
62
|
+
const base = { moduleName, buildDir, conformanceDir, diff: applied, conformance, specGaps: [...linesAddedSince(notesBefore.code, buildDir, "gaps"), ...linesAddedSince(notesBefore.tests, conformanceDir, "gaps")] };
|
|
63
|
+
if (!conformance.passed) {
|
|
64
|
+
abandonWork(unit, moduleName, notLandedWhy("for the spec change", conformance.unlabelled));
|
|
65
|
+
return { ...base, tested: tested(), passed: false, stoppedAt: "conformance" };
|
|
66
|
+
}
|
|
67
|
+
// The run that passed was of the whole suite - the module's tests folder holds the chain's, copied up - and the unit
|
|
68
|
+
// tests with it: on code and tests still as they were then, a second run would only say it again.
|
|
69
|
+
if (conformance.green !== undefined && sameFiles(conformance.green.code, digestFiles(buildDir)) && sameFiles(conformance.green.tests, digestFiles(conformanceDir))) {
|
|
70
|
+
stage("the whole conformance suite and the unit tests passed on the code and the tests as they are - not run again");
|
|
71
|
+
const regression = { passed: true, runs: [], repairs: [], command: scripts.conformance };
|
|
72
|
+
return { ...base, tested: tested(), regression, passed: true, stoppedAt: null };
|
|
73
|
+
}
|
|
74
|
+
stage("conformance tests pass - running the codebase's whole conformance suite");
|
|
75
|
+
const regression = await ensureRegressionPasses({
|
|
76
|
+
start: unit,
|
|
77
|
+
step: `${moduleName} spec change conformance`,
|
|
78
|
+
buildDir,
|
|
79
|
+
conformanceRoot,
|
|
80
|
+
moduleName,
|
|
81
|
+
frids,
|
|
82
|
+
tree,
|
|
83
|
+
required: getInheritedFunctionalSpecs(tree),
|
|
84
|
+
scripts,
|
|
85
|
+
model: options.model,
|
|
86
|
+
onDelta: options.onDelta,
|
|
87
|
+
});
|
|
88
|
+
return { ...base, tested: tested(), regression, passed: regression.passed, stoppedAt: regression.passed ? null : "regression" };
|
|
89
|
+
}
|
|
90
|
+
/** Whether two digests of a folder hold the same files, each with the same content. */
|
|
91
|
+
function sameFiles(a, b) {
|
|
92
|
+
if (a.size !== b.size)
|
|
93
|
+
return false;
|
|
94
|
+
for (const [file, digest] of a)
|
|
95
|
+
if (b.get(file) !== digest)
|
|
96
|
+
return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/** What the conformance for a spec change did, for the agent and the person. */
|
|
100
|
+
export function describeSpecDiffConformance(result) {
|
|
101
|
+
const repairs = result.regression?.repairs.length ?? 0;
|
|
102
|
+
return [
|
|
103
|
+
result.passed
|
|
104
|
+
? `The conformance tests in ${result.moduleName}'s codebase (${result.conformanceDir}) are in line with the spec change, and the whole suite passes.`
|
|
105
|
+
: describeStop({
|
|
106
|
+
moduleName: result.moduleName,
|
|
107
|
+
at: result.stoppedAt,
|
|
108
|
+
runs: [],
|
|
109
|
+
attempts: 0,
|
|
110
|
+
conformance: result.conformance,
|
|
111
|
+
repairs: result.regression?.repairs.length,
|
|
112
|
+
}),
|
|
113
|
+
result.diff === "applied" ? "The diff was applied to the .plain files." : "The diff was already applied.",
|
|
114
|
+
result.tested.length > 0
|
|
115
|
+
? `Functionalities with conformance tests: ${result.tested.join(", ")}.`
|
|
116
|
+
: "No functionality has labelled conformance tests yet.",
|
|
117
|
+
result.conformance.codeChanged ? "The code was changed where a test showed it did not do what the spec says." : "",
|
|
118
|
+
result.conformance.reviewWaived !== undefined
|
|
119
|
+
? `The tests were optimised as far as the review could take them; its last suggestions are in the notes: ${result.conformance.reviewWaived.map((problem) => problem.what.trim()).join(" ").slice(0, 400)}`
|
|
120
|
+
: "",
|
|
121
|
+
repairs > 0 ? `${repairs} regression repair(s) changed the code.` : "",
|
|
122
|
+
result.conformance.testRemovals.length > 0
|
|
123
|
+
? `Conformance tests removed and justified: ${result.conformance.testRemovals.map((r) => r.file).join(", ")}.`
|
|
124
|
+
: "",
|
|
125
|
+
...specGapsText(result.specGaps),
|
|
126
|
+
"",
|
|
127
|
+
"<agent report>",
|
|
128
|
+
result.conformance.response,
|
|
129
|
+
"</agent report>",
|
|
130
|
+
]
|
|
131
|
+
.filter((line) => line !== "")
|
|
132
|
+
.join("\n");
|
|
133
|
+
}
|