@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,598 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { IMPORT_DIRECTIVE, plainFileParser, readPlainSourceMetadata, withParseCache } from "../plain/parser.js";
|
|
6
|
+
import { isStdlibModule, STDLIB_ROOT } from "../plain/stdlib.js";
|
|
7
|
+
import { FUNCTIONAL_REQUIREMENTS, getFrids, getInheritedFunctionalSpecs, getSpecificationsForFrid, TEST_REQUIREMENTS, } from "../plain/spec.js";
|
|
8
|
+
import { ask, BEGIN, createExecutor, refuseIfModelIsBlind } from "../pi.js";
|
|
9
|
+
import { reportActivity, reportLogNote, reportStage, unitOf } from "./activity.js";
|
|
10
|
+
import { debugFailure, diagnosisText } from "./debugger.js";
|
|
11
|
+
import { codebaseMap, diffFiles, digestFiles, wroteNothing } from "./codebase.js";
|
|
12
|
+
import { confinedTools } from "./confine.js";
|
|
13
|
+
import { buildSpecDiffPrompt, describeSpecChange, reviewedSpec, sectionModules } from "./prompt.js";
|
|
14
|
+
import { abandonWork, recordHistory, startWork } from "./lineage.js";
|
|
15
|
+
import { createWorkLedger, SUBMIT_WORK_NOTE } from "./submit-work.js";
|
|
16
|
+
import { CODER_COMPACTION } from "./compaction.js";
|
|
17
|
+
import { linesAddedSince, NOTES_TURN, notesForSession, passedOutcome, readNotes, recordNote, withNotes } from "./notes.js";
|
|
18
|
+
import { createLoopWatch, createScriptTool, failureSignature, findTestScripts, projectRootFor, requireTestScripts, runUnitScript, STUCK_ROUNDS, withoutConformance, withTestingScripts, } from "./test-scripts.js";
|
|
19
|
+
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
20
|
+
import { createCanItFailTool, createMyDiffTool, selfCheckNote } from "./self-check.js";
|
|
21
|
+
import { createServiceTools } from "./services.js";
|
|
22
|
+
import { createUnitGate } from "./unit-gate.js";
|
|
23
|
+
import { toolchainError } from "./environment-error.js";
|
|
24
|
+
import { workspaceBlock } from "./workspace.js";
|
|
25
|
+
import { ATTEMPTS, describeStop } from "./stop-report.js";
|
|
26
|
+
import { summarizeDiff } from "./job-panel.js";
|
|
27
|
+
import { conversationWriting } from "./conversation-writes.js";
|
|
28
|
+
import { resolveTemplateDirs } from "./paths.js";
|
|
29
|
+
import { copyRequiredCode, findCodebaseTip, newlyRequired } from "./requires.js";
|
|
30
|
+
import { createCopyResourceTool, loadLinkedResources, resourceImages } from "./resources.js";
|
|
31
|
+
import { markTold, waitingSteer } from "./steering.js";
|
|
32
|
+
import { misplacedConformanceTests, misplacedTestsPrompt } from "./test-kinds.js";
|
|
33
|
+
import { sectionsOf } from "./state.js";
|
|
34
|
+
import { inheritedRecord, readRecord, withResourceDigests, writeCodeRecord } from "./records.js";
|
|
35
|
+
import { changesBetween, sameSpec, serializeTree } from "./spec-steps.js";
|
|
36
|
+
import { untilUnitTestsReviewed } from "./unit-review.js";
|
|
37
|
+
import { configForSpec, inheritModel, modelOnly, modelSpec } from "../config.js";
|
|
38
|
+
import { findTestRemovals, inventoryTests, listFiles, removalChallengePrompt, draftPrompt, DRAFT_ASKS, } from "./testing.js";
|
|
39
|
+
/**
|
|
40
|
+
* Render a change to the specs, given as a git diff of the .plain files.
|
|
41
|
+
*
|
|
42
|
+
* The change is whatever the person made - specs added, changed or removed, in any section,
|
|
43
|
+
* across any number of them - and it is rendered the way a developer would take a spec
|
|
44
|
+
* change: one agent, given the diff and every spec it touches as it now stands, makes the
|
|
45
|
+
* code match. Starting from nothing is the same operation, with a diff that adds everything.
|
|
46
|
+
*
|
|
47
|
+
* It is given two things: the diff, and the whole spec as parsed right after the diff went in.
|
|
48
|
+
* The render works from that parse and the build record is written from it - every section,
|
|
49
|
+
* not only the functional specs - because that is what the codebase holds once the change is
|
|
50
|
+
* built. A change rendered from a queue runs after later ones have gone into the files, so
|
|
51
|
+
* the files are not what it is about; the parse taken with the diff is.
|
|
52
|
+
*
|
|
53
|
+
* After it, the order that makes the result trustworthy: the conformance tests are brought
|
|
54
|
+
* in line with the change, the whole suite runs with repairs, and the build record is
|
|
55
|
+
* rewritten.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_FOLLOW_UPS = ATTEMPTS["unit tests"];
|
|
58
|
+
/**
|
|
59
|
+
* Bring the .plain files to the state the diff describes, with git. A diff that is already
|
|
60
|
+
* applied is fine - the person may have made the edit themselves and sent its diff - but
|
|
61
|
+
* one that fits neither way is refused: rendering it would mean guessing.
|
|
62
|
+
*/
|
|
63
|
+
export function applySpecDiff(diff, cwd) {
|
|
64
|
+
// What it writes is the person's side's, not an agent command's: the audit leaves it.
|
|
65
|
+
const written = conversationWriting(plainFilesOf(diff).map((file) => resolve(cwd, file)));
|
|
66
|
+
const dir = mkdtempSync(join(tmpdir(), "turri-diff-"));
|
|
67
|
+
const patch = join(dir, "spec.diff");
|
|
68
|
+
writeFileSync(patch, diff.endsWith("\n") ? diff : `${diff}\n`);
|
|
69
|
+
// --recount: a diff written by hand, or by a model, rarely has its hunk line counts right,
|
|
70
|
+
// and they carry no information the hunk itself does not.
|
|
71
|
+
const git = (...args) => spawnSync("git", ["apply", "--recount", ...args, patch], { cwd, encoding: "utf8" });
|
|
72
|
+
try {
|
|
73
|
+
const forward = git("--check");
|
|
74
|
+
if (forward.status === 0) {
|
|
75
|
+
const applied = git();
|
|
76
|
+
if (applied.status !== 0)
|
|
77
|
+
throw new Error(`git apply failed: ${applied.stderr.trim()}`);
|
|
78
|
+
return "applied";
|
|
79
|
+
}
|
|
80
|
+
if (git("--reverse", "--check").status === 0)
|
|
81
|
+
return "already applied";
|
|
82
|
+
throw new Error(`The spec diff does not apply to the .plain files here, and is not already applied: ${forward.stderr.trim()}`);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
written();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Undo a spec diff this process applied, when what it produced cannot be used. */
|
|
90
|
+
export function revertSpecDiff(diff, cwd) {
|
|
91
|
+
const written = conversationWriting(plainFilesOf(diff).map((file) => resolve(cwd, file)));
|
|
92
|
+
const dir = mkdtempSync(join(tmpdir(), "turri-diff-"));
|
|
93
|
+
const patch = join(dir, "spec.diff");
|
|
94
|
+
writeFileSync(patch, diff.endsWith("\n") ? diff : `${diff}\n`);
|
|
95
|
+
try {
|
|
96
|
+
spawnSync("git", ["apply", "--recount", "--reverse", patch], { cwd, encoding: "utf8" });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
written();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The .plain files a diff writes to, relative to where it applies. */
|
|
104
|
+
export function plainFilesOf(diff) {
|
|
105
|
+
const files = new Set();
|
|
106
|
+
for (const line of diff.split("\n")) {
|
|
107
|
+
const match = /^\+\+\+ (?:b\/)?(.+\.plain)\s*$/.exec(line);
|
|
108
|
+
if (match)
|
|
109
|
+
files.add(match[1]);
|
|
110
|
+
}
|
|
111
|
+
return [...files];
|
|
112
|
+
}
|
|
113
|
+
/** A module's .plain file and every one it imports, transitively - the files its spec is read from. */
|
|
114
|
+
export function specFilesOf(plainFile, templateDirs = []) {
|
|
115
|
+
const dirs = resolveTemplateDirs(resolve(plainFile), templateDirs);
|
|
116
|
+
const files = [];
|
|
117
|
+
const visit = (path) => {
|
|
118
|
+
if (files.includes(path) || !existsSync(path))
|
|
119
|
+
return;
|
|
120
|
+
files.push(path);
|
|
121
|
+
const imports = readPlainSourceMetadata(readFileSync(path, "utf8")).metadata[IMPORT_DIRECTIVE] ?? [];
|
|
122
|
+
for (const name of imports) {
|
|
123
|
+
// A standard library template is turri's own, wherever the spec is (see plain/stdlib.ts).
|
|
124
|
+
const from = isStdlibModule(name) ? [STDLIB_ROOT] : [dirname(path), ...dirs];
|
|
125
|
+
const found = from.map((dir) => join(dir, `${name.replace(/^\.\//, "")}.plain`)).find((candidate) => existsSync(candidate));
|
|
126
|
+
if (found !== undefined)
|
|
127
|
+
visit(resolve(found));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
visit(resolve(plainFile));
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
/** A git diff cut into what it does to each file, in order: the file's path (the new side's), and its part of the diff. */
|
|
134
|
+
export function diffByFile(diff) {
|
|
135
|
+
const lines = diff.split("\n");
|
|
136
|
+
const starts = [];
|
|
137
|
+
const headed = lines.some((line) => line.startsWith("diff --git "));
|
|
138
|
+
lines.forEach((line, index) => {
|
|
139
|
+
if (headed ? line.startsWith("diff --git ") : line.startsWith("--- ") && lines[index + 1]?.startsWith("+++ "))
|
|
140
|
+
starts.push(index);
|
|
141
|
+
});
|
|
142
|
+
return starts.map((start, index) => {
|
|
143
|
+
const part = lines.slice(start, starts[index + 1] ?? lines.length);
|
|
144
|
+
const target = part.map((line) => /^\+\+\+ (?:b\/)?(.+?)\s*$/.exec(line)?.[1]).find((path) => path !== undefined && path !== "/dev/null");
|
|
145
|
+
const source = part.map((line) => /^--- (?:a\/)?(.+?)\s*$/.exec(line)?.[1]).find((path) => path !== undefined && path !== "/dev/null");
|
|
146
|
+
const text = part.join("\n");
|
|
147
|
+
return { file: target ?? source ?? "", diff: text.endsWith("\n") ? text : `${text}\n` };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** Parse the spec as it stands now, for a change to `plainFile`'s module. */
|
|
151
|
+
export function snapshotSpec(plainFile, options = {}) {
|
|
152
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
153
|
+
const templateDirs = options.templateDirs ?? [];
|
|
154
|
+
// The change lands in the chain's current codebase. A spec change is built whatever it
|
|
155
|
+
// touches - a definition, a req, a functional spec - so a module with no functional specs
|
|
156
|
+
// yet is still a module to build.
|
|
157
|
+
const { tip, chain } = withParseCache(() => findCodebaseTip(resolve(cwd, plainFile), { templateDirs, inModule: options.inModule, allowNoFunctionalities: true }));
|
|
158
|
+
const dirs = resolveTemplateDirs(tip.plainFile, templateDirs);
|
|
159
|
+
const [moduleName, parsed] = plainFileParser(basename(tip.plainFile), dirs, { allowNoFunctionalities: true });
|
|
160
|
+
// What a linked resource holds is part of the spec: hashed beside its link, as the work loop has it.
|
|
161
|
+
return { moduleName, plainFile: tip.plainFile, tree: withResourceDigests(parsed, dirs), chain };
|
|
162
|
+
}
|
|
163
|
+
export async function renderSpecDiff(options) {
|
|
164
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
165
|
+
const templateDirs = options.templateDirs ?? [];
|
|
166
|
+
const applied = options.applied ?? applySpecDiff(options.diff, cwd);
|
|
167
|
+
const spec = options.spec ?? snapshotSpec(options.plainFile, { cwd, templateDirs, inModule: options.inModule });
|
|
168
|
+
const { moduleName, tree, chain } = spec;
|
|
169
|
+
const tip = chain.at(-1);
|
|
170
|
+
const stage = (label) => reportStage(`${unitOf(moduleName)}: ${label}`);
|
|
171
|
+
// A module required now that the code was not built on is not something a change to the code can
|
|
172
|
+
// bring in: the code has to start again, on that module's.
|
|
173
|
+
const fresh = newlyRequired(chain);
|
|
174
|
+
if (fresh !== undefined) {
|
|
175
|
+
throw new Error(`${tip.moduleName} now requires ${fresh.modules.join(", ")}, which its code was not built on, so a spec diff cannot bring the change in. ` +
|
|
176
|
+
`Its code has to be started over on it - ${fresh.rebuild.map((entry) => entry.moduleName).join(", ")} taken out, kept in the history, and built ` +
|
|
177
|
+
`again from the bottom of the chain: regenerate it from scratch in the chat, once the user agrees, or turri regenerate ${relative(cwd, tip.plainFile)} --start-over.`);
|
|
178
|
+
}
|
|
179
|
+
stage(`the spec diff is ${applied === "applied" ? "applied to the .plain files" : "already in the .plain files"} - rendering it in ${moduleName}'s codebase`);
|
|
180
|
+
// The change is made to the codebase's last known good state, not to what an interrupted
|
|
181
|
+
// run left half-written.
|
|
182
|
+
// A dry run only reads: another job may be at work in the codebase.
|
|
183
|
+
const dryRun = options.dryRun === true;
|
|
184
|
+
if (!dryRun)
|
|
185
|
+
stage("starting from the codebase's last commit");
|
|
186
|
+
// The change is one unit - its code and its unit tests - landed whole, with the spec it was built from, or
|
|
187
|
+
// undone whole. Its conformance tests are a job of their own, after it (docs/regenerating.md).
|
|
188
|
+
const unit = dryRun ? undefined : startWork([tip.buildDir], moduleName);
|
|
189
|
+
// Only laid out differently - definitions moved to a common module, specs moved between modules
|
|
190
|
+
// of the chain: the code already is what the spec says. Nothing to render; the spec is recorded
|
|
191
|
+
// as it is now laid out.
|
|
192
|
+
const builtBefore = readRecord(tip.buildDir, { onDisk: true });
|
|
193
|
+
if (builtBefore !== null && sameSpec(builtBefore, tree) && !dryRun) {
|
|
194
|
+
stage("the specs were only restructured - nothing to render; recording the spec as it is now laid out");
|
|
195
|
+
writeCodeRecord(tip.buildDir, tree);
|
|
196
|
+
recordHistory(tip.buildDir, `spec restructured: ${summarizeDiff(options.diff)}`, options.diff);
|
|
197
|
+
return {
|
|
198
|
+
moduleName,
|
|
199
|
+
buildDir: tip.buildDir,
|
|
200
|
+
diff: applied,
|
|
201
|
+
response: "The specs were only restructured: the code is already built from the same spec, so nothing was rendered.",
|
|
202
|
+
runs: [],
|
|
203
|
+
attempts: 0,
|
|
204
|
+
testRemovals: [],
|
|
205
|
+
files: { added: [], changed: [], removed: [] },
|
|
206
|
+
passed: true,
|
|
207
|
+
stoppedAt: null,
|
|
208
|
+
restructured: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Whether the change is done is the project's testing scripts: without them, nothing is built.
|
|
212
|
+
stage("finding the project's testing scripts");
|
|
213
|
+
const root = projectRootFor(tip.plainFile, tip.buildDir);
|
|
214
|
+
// The code is judged by its unit tests alone: the conformance testing script is the conformance job's.
|
|
215
|
+
const scripts = withoutConformance(findTestScripts(root, tip.plainFile, tip.buildDir));
|
|
216
|
+
requireTestScripts(moduleName, scripts, { conformance: false });
|
|
217
|
+
reportLogNote(` unit testing script: ${relative(root, scripts.unit)}`);
|
|
218
|
+
const lastFrid = [...getFrids(tree)].at(-1);
|
|
219
|
+
const buildDir = tip.buildDir;
|
|
220
|
+
// From scratch there is no build directory yet, and everything the agent runs runs in it.
|
|
221
|
+
if (!dryRun)
|
|
222
|
+
mkdirSync(buildDir, { recursive: true });
|
|
223
|
+
const empty = listFiles(buildDir).length === 0;
|
|
224
|
+
const source = empty && tip.requires !== null ? chain.find((entry) => entry.moduleName === tip.requires) : undefined;
|
|
225
|
+
if (source !== undefined && !dryRun) {
|
|
226
|
+
stage(`no code here yet - starting from ${source.moduleName}'s code`);
|
|
227
|
+
copyRequiredCode(source.buildDir, buildDir, moduleName, source.moduleName);
|
|
228
|
+
}
|
|
229
|
+
// What the agent is shown it starts from: a dry run copies nothing, and reads the code it would start from where it is.
|
|
230
|
+
const seen = dryRun && source !== undefined ? source.buildDir : buildDir;
|
|
231
|
+
stage("mapping the codebase and writing the prompt");
|
|
232
|
+
// The agent is given the spec the code is built from - the state before the change - and the change: the spec
|
|
233
|
+
// before it, plus the diff, is the spec after it. Read from the codebase as it is now, code copied up from a
|
|
234
|
+
// required module included; with nothing built, there is no spec before, and the change is all of it.
|
|
235
|
+
const builtNow = (seen === buildDir ? readRecord(buildDir, { onDisk: true }) : (() => {
|
|
236
|
+
const below = readRecord(seen);
|
|
237
|
+
return below !== null ? inheritedRecord(below, source.moduleName) : null;
|
|
238
|
+
})()) ?? {};
|
|
239
|
+
// The change as it is to the code - from what it is built from to the spec this job records - section by section:
|
|
240
|
+
// what every agent of this change is shown, in place of the raw diff.
|
|
241
|
+
const changes = changesBetween(serializeTree(builtNow), serializeTree(tree));
|
|
242
|
+
const changeText = changes.length > 0 ? describeSpecChange(changes, moduleName) : undefined;
|
|
243
|
+
// The coding agent's view of the change leaves its test reqs out: they say how the conformance tests are written -
|
|
244
|
+
// the conformance job's to read, from the whole change (`changeText`), not the code's.
|
|
245
|
+
const codingChanges = changes.filter((change) => change.section !== TEST_REQUIREMENTS);
|
|
246
|
+
const codingText = codingChanges.length > 0 ? describeSpecChange(codingChanges, moduleName) : undefined;
|
|
247
|
+
/** Steered, the change as its agent has it now: from what the code is built from to what it was steered to. */
|
|
248
|
+
const steeredChange = () => {
|
|
249
|
+
if (options.steering === undefined || options.steering.steers === 0)
|
|
250
|
+
return undefined;
|
|
251
|
+
const now = changesBetween(serializeTree(builtNow), serializeTree(options.steering.agent)).filter((change) => change.section !== TEST_REQUIREMENTS);
|
|
252
|
+
return now.length > 0 ? describeSpecChange(now, moduleName) : undefined;
|
|
253
|
+
};
|
|
254
|
+
const builtFrids = [...getFrids(builtNow)];
|
|
255
|
+
const specificationsBefore = builtFrids.length > 0 ? getSpecificationsForFrid(builtNow, builtFrids.at(-1))[0] : { ...sectionsOf(builtNow), [FUNCTIONAL_REQUIREMENTS]: [] };
|
|
256
|
+
const linkedResources = loadLinkedResources(tree, lastFrid ?? null, resolveTemplateDirs(tip.plainFile, templateDirs));
|
|
257
|
+
const prompt = buildSpecDiffPrompt({
|
|
258
|
+
moduleName,
|
|
259
|
+
diff: options.diff,
|
|
260
|
+
// The change as it is to the code: from what it is built from to the spec this job records, section by section.
|
|
261
|
+
changes: codingChanges,
|
|
262
|
+
specifications: specificationsBefore,
|
|
263
|
+
modules: sectionModules(builtNow, moduleName),
|
|
264
|
+
required: getInheritedFunctionalSpecs(builtNow),
|
|
265
|
+
linkedResources,
|
|
266
|
+
codebase: codebaseMap(seen),
|
|
267
|
+
empty: listFiles(seen).length === 0,
|
|
268
|
+
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
269
|
+
});
|
|
270
|
+
const digestsBefore = digestFiles(buildDir);
|
|
271
|
+
// The notes as the agent finds them: what it adds - a spec gap above all - is told with the result.
|
|
272
|
+
const notesBefore = readNotes(buildDir);
|
|
273
|
+
/** What the debugger found, each time it was asked: the pulse's line in the notes says it. */
|
|
274
|
+
const diagnosed = [];
|
|
275
|
+
const runs = [];
|
|
276
|
+
// Its own runs of the unit tests, watched for a loop - edits that go back and forth while the failure stays.
|
|
277
|
+
const loop = createLoopWatch(buildDir);
|
|
278
|
+
const fence = confinedTools({ workdir: buildDir, reportsEnvironment: true });
|
|
279
|
+
const notes = notesForSession(buildDir, `${moduleName} spec change`);
|
|
280
|
+
// What it hands over at the end of a turn, for its reviewer.
|
|
281
|
+
const work = createWorkLedger();
|
|
282
|
+
// What it checks its own work with: the diff its reviewer is shown, and whether a test catches a bug - neither decides anything.
|
|
283
|
+
const gate = createUnitGate(buildDir, scripts.unit);
|
|
284
|
+
const selfChecks = [
|
|
285
|
+
createMyDiffTool({ dir: buildDir, what: "code" }),
|
|
286
|
+
...(scripts.runOn === "copy" ? [createCanItFailTool({ buildDir, script: scripts.unit })] : []),
|
|
287
|
+
];
|
|
288
|
+
// A server it built, it can start and try from outside - stopped when it is done.
|
|
289
|
+
const services = createServiceTools({ dir: buildDir });
|
|
290
|
+
// The script its work is judged by.
|
|
291
|
+
const task = [
|
|
292
|
+
withNotes(withTestingScripts(prompt, scripts, ["unit"], root), buildDir),
|
|
293
|
+
"",
|
|
294
|
+
...selfCheckNote([...selfChecks.map((tool) => tool.name), "start_service"]),
|
|
295
|
+
...SUBMIT_WORK_NOTE,
|
|
296
|
+
].join("\n");
|
|
297
|
+
if (dryRun) {
|
|
298
|
+
return { moduleName, buildDir, diff: applied, response: "", runs: [], attempts: 0, testRemovals: [], files: { added: [], changed: [], removed: [] }, passed: false, stoppedAt: null, task };
|
|
299
|
+
}
|
|
300
|
+
// The spec's images need a model that reads them: refused before the agent starts.
|
|
301
|
+
await refuseIfModelIsBlind(options.model, resourceImages(linkedResources).length);
|
|
302
|
+
stage(`starting the coding agent${options.model ? ` (${options.model})` : ""}`);
|
|
303
|
+
// One session for the whole change: its task is its instructions, its work compacted as it gets longer.
|
|
304
|
+
const executor = await createExecutor({
|
|
305
|
+
cwd: buildDir,
|
|
306
|
+
customTools: [
|
|
307
|
+
notes.tool,
|
|
308
|
+
work.tool,
|
|
309
|
+
...createWebTools(),
|
|
310
|
+
createScriptTool({ kind: "unit", script: scripts.unit, run: () => runUnitScript(buildDir, scripts.unit), runs, watch: loop }),
|
|
311
|
+
...selfChecks,
|
|
312
|
+
...services.tools,
|
|
313
|
+
...fence.tools,
|
|
314
|
+
createCopyResourceTool({ resources: linkedResources, dir: buildDir }),
|
|
315
|
+
],
|
|
316
|
+
tools: [...fence.names, "run_unit_tests", ...selfChecks.map((tool) => tool.name), ...services.names, "update_notes", "submit_work", "copy_resource", ...WEB_TOOLS],
|
|
317
|
+
model: options.model,
|
|
318
|
+
label: `${moduleName} spec change coding agent`,
|
|
319
|
+
role: "coding",
|
|
320
|
+
system: `${fence.note}\n\n${workspaceBlock({ dir: buildDir, hasMyDiff: true })}`,
|
|
321
|
+
task,
|
|
322
|
+
compaction: { instructions: CODER_COMPACTION },
|
|
323
|
+
images: resourceImages(linkedResources),
|
|
324
|
+
checkEnvironment: { run: () => runUnitScript(buildDir, scripts.unit), script: scripts.unit },
|
|
325
|
+
});
|
|
326
|
+
let response;
|
|
327
|
+
let verified = false;
|
|
328
|
+
let unitReviewStopped = false;
|
|
329
|
+
let followUp = 0;
|
|
330
|
+
/** The harness's runs of the unit testing script: each is an attempt it judged. */
|
|
331
|
+
let attempts = 0;
|
|
332
|
+
/** How many times the agent, having written nothing, was asked for a draft. */
|
|
333
|
+
let draftAsks = 0;
|
|
334
|
+
/** What the unit testing script needs and does not find, last run. */
|
|
335
|
+
let missing;
|
|
336
|
+
/** The last round failed on the script's static checks alone: the unit tests passed. */
|
|
337
|
+
let staticOnly = false;
|
|
338
|
+
const testRemovals = [];
|
|
339
|
+
const steering = options.steering;
|
|
340
|
+
// The loop reaches the agent in its turn while there is one; between turns a steer waits for the next.
|
|
341
|
+
if (steering !== undefined) {
|
|
342
|
+
steering.deliver = (text) => {
|
|
343
|
+
if (!executor.isStreaming)
|
|
344
|
+
return false;
|
|
345
|
+
void Promise.resolve(executor.steer(text)).catch(() => undefined);
|
|
346
|
+
stage("the spec changed inside what it regenerates - the coding agent is told, in its turn");
|
|
347
|
+
return true;
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** A steer the agent has not had yet, given as its next turn: true when there was one. */
|
|
351
|
+
const steerIfWaiting = async () => {
|
|
352
|
+
const text = steering !== undefined ? waitingSteer(steering) : undefined;
|
|
353
|
+
if (text === undefined)
|
|
354
|
+
return false;
|
|
355
|
+
markTold(steering);
|
|
356
|
+
stage("the spec changed inside what it regenerates - the coding agent is told");
|
|
357
|
+
response = await ask(executor, text, options.onDelta);
|
|
358
|
+
return true;
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
stage("turri is implementing the spec change");
|
|
362
|
+
let inventory = inventoryTests(buildDir);
|
|
363
|
+
response = await ask(executor, BEGIN, options.onDelta);
|
|
364
|
+
// Back and forth until the harness's run of the unit testing script passes - whatever the
|
|
365
|
+
// agent ran, and whether it ran anything - or the rounds run out. Picks up where it left off
|
|
366
|
+
// when called again.
|
|
367
|
+
// The same failure, round after round: how many times each has been seen running.
|
|
368
|
+
const seenFailures = new Map();
|
|
369
|
+
// The debugger's model: its own role's, when the config names one, else a stronger one than the agent it helps -
|
|
370
|
+
// the judge model when the config sets it, else the harness model.
|
|
371
|
+
const projectConfig = configForSpec(tip.plainFile);
|
|
372
|
+
const debuggerModel = modelSpec(inheritModel(projectConfig.judgeModel, inheritModel(projectConfig.harnessModel, modelOnly(options.model))));
|
|
373
|
+
const untilPassing = async () => {
|
|
374
|
+
for (; followUp < MAX_FOLLOW_UPS; followUp += 1) {
|
|
375
|
+
// A steer that came between its turns: its own turn, then the tests run again.
|
|
376
|
+
if (await steerIfWaiting())
|
|
377
|
+
continue;
|
|
378
|
+
const current = inventoryTests(buildDir);
|
|
379
|
+
const removals = findTestRemovals(inventory, current);
|
|
380
|
+
if (removals.length > 0) {
|
|
381
|
+
// A removed spec takes its tests with it; the agent says which and why, as always.
|
|
382
|
+
testRemovals.push(...removals);
|
|
383
|
+
inventory = current;
|
|
384
|
+
stage(`justifying ${removals.length} removed test${removals.length === 1 ? "" : "s"}`);
|
|
385
|
+
response = await ask(executor, removalChallengePrompt(removals, "unit tests"), options.onDelta);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// Conformance tests are the conformance step's, in the tests folder: none of them here.
|
|
389
|
+
const misplaced = misplacedConformanceTests(buildDir);
|
|
390
|
+
if (misplaced.length > 0) {
|
|
391
|
+
stage(`conformance tests in the code folder - sending ${misplaced.length === 1 ? "it" : "them"} back`);
|
|
392
|
+
response = await ask(executor, misplacedTestsPrompt(misplaced, buildDir), options.onDelta);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// Nothing written - a turn spent planning - leaves nothing to test or review: ask for a best-guess draft, or why none.
|
|
396
|
+
if (draftAsks < DRAFT_ASKS && wroteNothing(buildDir, digestsBefore)) {
|
|
397
|
+
draftAsks += 1;
|
|
398
|
+
stage("the coding agent has changed nothing yet - asking for a best-guess draft, or why the change needs no code");
|
|
399
|
+
response = await ask(executor, draftPrompt(), options.onDelta);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
stage(`running the unit testing script (attempt ${attempts + 1})`);
|
|
403
|
+
attempts += 1;
|
|
404
|
+
const verdict = await gate.verify(runs);
|
|
405
|
+
if (verdict.ok)
|
|
406
|
+
stage(`unit tests pass (attempt ${attempts})`);
|
|
407
|
+
staticOnly = !verdict.ok && verdict.staticOnly;
|
|
408
|
+
// A toolchain the script cannot find is no failure of the code, and the agents install nothing: the person installs
|
|
409
|
+
// it on this machine, so the job stops now and asks them.
|
|
410
|
+
missing = verdict.ok ? undefined : verdict.missing;
|
|
411
|
+
if (missing !== undefined) {
|
|
412
|
+
stage(`the unit testing script cannot run: ${missing.command ?? "a toolchain it needs"} is not installed on this machine - stopping`);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (!verdict.ok) {
|
|
416
|
+
// Failing the same way STUCK_ROUNDS times running, or the agent stopped for going round in circles in its own
|
|
417
|
+
// runs: a debugger looks at it with fresh eyes, on a stronger model, and the coding agent is given what it found
|
|
418
|
+
// with the failure. The rounds go on as before - and it looks again if it repeats.
|
|
419
|
+
const key = failureSignature(verdict.run);
|
|
420
|
+
const seen = key !== undefined ? (seenFailures.get(key) ?? 0) + 1 : 0;
|
|
421
|
+
seenFailures.clear();
|
|
422
|
+
if (key !== undefined)
|
|
423
|
+
seenFailures.set(key, seen);
|
|
424
|
+
let prompt = verdict.prompt;
|
|
425
|
+
const circling = loop.stuck;
|
|
426
|
+
if (circling || (key !== undefined && seen >= STUCK_ROUNDS)) {
|
|
427
|
+
if (key !== undefined)
|
|
428
|
+
seenFailures.delete(key);
|
|
429
|
+
const times = circling ? Math.max(seen, loop.failing) : seen;
|
|
430
|
+
loop.reset();
|
|
431
|
+
stage(circling
|
|
432
|
+
? `the coding agent is going round in circles - ${times} failing runs - a debugger looks at it with fresh eyes`
|
|
433
|
+
: `the same failure ${seen} times - a debugger looks at it with fresh eyes`);
|
|
434
|
+
const found = await debugFailure({
|
|
435
|
+
buildDir,
|
|
436
|
+
failure: verdict.run.output,
|
|
437
|
+
times,
|
|
438
|
+
task: steeredChange() ?? codingText ?? changeText ?? options.diff ?? "",
|
|
439
|
+
script: verdict.script,
|
|
440
|
+
label: `${moduleName} spec change`,
|
|
441
|
+
...(debuggerModel !== undefined ? { model: debuggerModel } : {}),
|
|
442
|
+
onDelta: options.onDelta,
|
|
443
|
+
}).catch(() => undefined);
|
|
444
|
+
if (found !== undefined) {
|
|
445
|
+
const cause = found.rootCause.split("\n")[0].slice(0, 240);
|
|
446
|
+
reportActivity(` the debugger: ${cause.slice(0, 160)}`);
|
|
447
|
+
diagnosed.push(cause);
|
|
448
|
+
// A lesson that cost a debugger to learn is kept for every agent after - not left to the coding agent to write down.
|
|
449
|
+
recordNote(buildDir, "pitfalls", `the debugger, after ${times} failing runs: ${cause} - the fix: ${found.fix.split("\n")[0].slice(0, 200)}`, `${moduleName} spec change`);
|
|
450
|
+
prompt = [
|
|
451
|
+
verdict.prompt,
|
|
452
|
+
"",
|
|
453
|
+
circling
|
|
454
|
+
? `You went round in circles - it has failed ${times} times since it last passed, the code back where it had been -`
|
|
455
|
+
: `It has failed this same way ${seen} times in a row,`,
|
|
456
|
+
"so a debugger looked at it with fresh eyes - it read the code, the tests, the build files and the testing script.",
|
|
457
|
+
"This is what it found. Check it against the code, then make the fix it names, rather than another variation on what",
|
|
458
|
+
"you tried before.",
|
|
459
|
+
"",
|
|
460
|
+
diagnosisText(found),
|
|
461
|
+
].join("\n");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
stage(`${verdict.staticOnly ? "static checks" : "unit tests"} not passing - fixing (round ${followUp + 1} of ${MAX_FOLLOW_UPS})`);
|
|
465
|
+
response = await ask(executor, prompt, options.onDelta);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
// Passing on what it was told before - a steer since, and they run again.
|
|
469
|
+
if (await steerIfWaiting())
|
|
470
|
+
continue;
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
};
|
|
475
|
+
verified = await untilPassing();
|
|
476
|
+
// The unit tests pass - now whether they test what the change says, so they fail when it breaks.
|
|
477
|
+
if (verified && (options.unitReview ?? configForSpec(tip.plainFile).review) === true) {
|
|
478
|
+
const reviewed = await untilUnitTestsReviewed({
|
|
479
|
+
buildDir,
|
|
480
|
+
atStart: digestsBefore,
|
|
481
|
+
what: `the spec change to module \`${moduleName}\``,
|
|
482
|
+
// As the coding agent had it: the spec the code was built from, and the change.
|
|
483
|
+
// Steered before the review, the change as the agent has it now.
|
|
484
|
+
spec: reviewedSpec({ moduleName, specifications: specificationsBefore, modules: sectionModules(builtNow, moduleName), required: getInheritedFunctionalSpecs(builtNow), change: steeredChange() ?? codingText ?? changeText ?? options.diff }),
|
|
485
|
+
forChange: true,
|
|
486
|
+
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
487
|
+
label: `${moduleName} spec change`,
|
|
488
|
+
...(scripts.runOn === "copy" ? { unitScript: scripts.unit } : {}),
|
|
489
|
+
model: options.judgeModel ?? modelSpec(inheritModel(configForSpec(tip.plainFile).judgeModel, modelOnly(options.model))),
|
|
490
|
+
send: async (next) => {
|
|
491
|
+
response = await ask(executor, next, options.onDelta);
|
|
492
|
+
},
|
|
493
|
+
untilPassing,
|
|
494
|
+
stage,
|
|
495
|
+
onDelta: options.onDelta,
|
|
496
|
+
work,
|
|
497
|
+
});
|
|
498
|
+
stage(`the review took ${reviewed.reviews} review${reviewed.reviews === 1 ? "" : "s"}`);
|
|
499
|
+
verified = reviewed.passed;
|
|
500
|
+
if (!reviewed.passed)
|
|
501
|
+
unitReviewStopped = true;
|
|
502
|
+
}
|
|
503
|
+
// A steer that came during the review: the agent's turn, and the tests until they pass - within its rounds.
|
|
504
|
+
while (verified && steering !== undefined && waitingSteer(steering) !== undefined)
|
|
505
|
+
verified = await untilPassing();
|
|
506
|
+
// Notes are kept only with work that lands: work that does not is rolled back, notes and all.
|
|
507
|
+
if (verified) {
|
|
508
|
+
stage("checking the working notes were left");
|
|
509
|
+
await notes.ensureWritten((next) => ask(executor, next, options.onDelta, NOTES_TURN), passedOutcome("unit testing script"));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
if (steering !== undefined)
|
|
514
|
+
steering.deliver = undefined;
|
|
515
|
+
executor.dispose();
|
|
516
|
+
await services.stopAll();
|
|
517
|
+
}
|
|
518
|
+
// Passing means the harness's own run of the unit testing script passed, last.
|
|
519
|
+
const unitPassed = verified;
|
|
520
|
+
const files = diffFiles(digestsBefore, digestFiles(buildDir));
|
|
521
|
+
const base = {
|
|
522
|
+
moduleName,
|
|
523
|
+
buildDir,
|
|
524
|
+
diff: applied,
|
|
525
|
+
response,
|
|
526
|
+
runs,
|
|
527
|
+
attempts,
|
|
528
|
+
testRemovals,
|
|
529
|
+
files,
|
|
530
|
+
...(steering !== undefined && steering.steers > 0 ? { steered: [...steering.notes] } : {}),
|
|
531
|
+
// Read before a rollback takes the notes back: a gap the agent met is the person's to decide, whether it landed or not.
|
|
532
|
+
specGaps: linesAddedSince(notesBefore, buildDir, "gaps"),
|
|
533
|
+
diagnosed,
|
|
534
|
+
};
|
|
535
|
+
/** The change landed: what the code is built from is now the spec with the change in, committed with it. */
|
|
536
|
+
const land = () => {
|
|
537
|
+
stage("recording what the code is built from, and committing it");
|
|
538
|
+
// Steered, it is built from the spec as it was steered to - the record the loop moved with it.
|
|
539
|
+
writeCodeRecord(buildDir, steering?.target ?? tree);
|
|
540
|
+
recordHistory(buildDir, `spec diff: ${summarizeDiff(options.diff)}`, options.diff);
|
|
541
|
+
};
|
|
542
|
+
if (!unitPassed) {
|
|
543
|
+
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
544
|
+
abandonWork(unit, moduleName, missing !== undefined
|
|
545
|
+
? "the unit testing script could not run"
|
|
546
|
+
: staticOnly
|
|
547
|
+
? "the spec change did not pass its static checks"
|
|
548
|
+
: "the spec change did not pass its unit tests");
|
|
549
|
+
// The environment lacks what the testing script needs: the pulse stops, rolled back, and the person is told.
|
|
550
|
+
if (missing !== undefined) {
|
|
551
|
+
throw toolchainError(missing, { moduleName, script: scripts.unit, kind: "unit" });
|
|
552
|
+
}
|
|
553
|
+
return { ...base, passed: false, stoppedAt: "unit tests", ...(staticOnly && !unitReviewStopped ? { staticChecks: true } : {}) };
|
|
554
|
+
}
|
|
555
|
+
stage("unit tests pass - landing the change on them");
|
|
556
|
+
land();
|
|
557
|
+
return { ...base, passed: true, stoppedAt: null };
|
|
558
|
+
}
|
|
559
|
+
/** Spec gaps a pulse's agents met, as its report says them: for the conversation to put to the person. */
|
|
560
|
+
export function specGapsText(gaps) {
|
|
561
|
+
if (gaps === undefined || gaps.length === 0)
|
|
562
|
+
return [];
|
|
563
|
+
return [
|
|
564
|
+
"",
|
|
565
|
+
"Spec gaps its agents met - the spec does not say, so they decided. Put each to the user, and write the ones they decide into the spec:",
|
|
566
|
+
...gaps.map((gap) => `- ${gap}`),
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
/** What a rendered spec change did, for the agent and the person. */
|
|
570
|
+
export function describeSpecDiff(result) {
|
|
571
|
+
const { added, changed, removed } = result.files;
|
|
572
|
+
if (result.restructured === true) {
|
|
573
|
+
return `The specs were only restructured: ${result.moduleName}'s code (${result.buildDir}) is already built from the same spec, so nothing was rendered - the spec is recorded as it is now laid out.`;
|
|
574
|
+
}
|
|
575
|
+
return [
|
|
576
|
+
result.passed
|
|
577
|
+
? `Rendered the spec change in ${result.moduleName}'s codebase (${result.buildDir}).`
|
|
578
|
+
: describeStop({
|
|
579
|
+
moduleName: result.moduleName,
|
|
580
|
+
at: result.stoppedAt,
|
|
581
|
+
runs: result.runs,
|
|
582
|
+
attempts: result.attempts,
|
|
583
|
+
...(result.missingToolchain !== undefined ? { missingToolchain: result.missingToolchain } : {}),
|
|
584
|
+
...(result.staticChecks === true ? { staticChecks: true } : {}),
|
|
585
|
+
}),
|
|
586
|
+
result.diff === "applied" ? "The diff was applied to the .plain files." : "The diff was already applied.",
|
|
587
|
+
`Files: ${added.length} added, ${changed.length} changed, ${removed.length} removed.`,
|
|
588
|
+
result.testRemovals.length > 0 ? `Unit tests removed and justified: ${result.testRemovals.map((r) => r.file).join(", ")}.` : "",
|
|
589
|
+
...(result.steered !== undefined ? [`Steered while it ran - the spec changed inside what it was regenerating, and its agent was told: ${result.steered.join("; ")}.`] : []),
|
|
590
|
+
...specGapsText(result.specGaps),
|
|
591
|
+
"",
|
|
592
|
+
"<agent report>",
|
|
593
|
+
result.response,
|
|
594
|
+
"</agent report>",
|
|
595
|
+
]
|
|
596
|
+
.filter((line) => line !== "")
|
|
597
|
+
.join("\n");
|
|
598
|
+
}
|