@turri-dev/cli 0.1.3-next.2babf06 → 0.1.3-next.420ce7e
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/README.md +1 -1
- package/dist/config.js +3 -2
- package/dist/extension.js +86 -35
- package/dist/identity.js +1 -1
- package/dist/loop-cli.js +1 -1
- package/dist/loop-tools.js +1 -1
- package/dist/pi-launch.js +5 -0
- package/dist/question-dialog.js +8 -3
- package/dist/questioning.js +1 -1
- package/dist/regenerate/activity.js +2 -0
- package/dist/regenerate/confine.js +1 -0
- package/dist/regenerate/conform.js +5 -5
- package/dist/regenerate/conformance-agents.js +220 -11
- package/dist/regenerate/conformance.js +91 -35
- package/dist/regenerate/debugger.js +27 -3
- package/dist/regenerate/environment-error.js +63 -9
- package/dist/regenerate/loop.js +22 -4
- package/dist/regenerate/notes.js +66 -17
- package/dist/regenerate/panel-dock.js +31 -2
- package/dist/regenerate/prompt.js +3 -1
- package/dist/regenerate/regenerate.js +35 -14
- package/dist/regenerate/regression.js +2 -1
- package/dist/regenerate/requires.js +55 -2
- package/dist/regenerate/side-panel.js +1 -1
- package/dist/regenerate/stop-report.js +13 -0
- package/dist/regenerate/test-scripts.js +22 -1
- package/dist/regenerate/unit-review.js +11 -0
- package/dist/regenerate-cli.js +1 -1
- package/dist/resume-line.js +4 -0
- package/dist/spec-tools.js +8 -44
- package/docs/authoring-plain.md +9 -6
- package/docs/config.md +3 -2
- package/docs/headless.md +2 -2
- package/docs/regenerating.md +75 -18
- package/docs/skills-and-gates.md +2 -2
- package/docs/using.md +5 -3
- package/genome/skills/bud/SKILL.md +67 -36
- package/genome/skills/bud/references/intent.md +4 -0
- package/package.json +3 -3
- package/genome/skills/bud/references/checklist.md +0 -44
|
@@ -9,14 +9,14 @@ import { abandonWork, diffSinceSnapshot, moduleDirOf, recordHistory, startWork,
|
|
|
9
9
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
10
10
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
11
11
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
12
|
-
import { labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
|
|
12
|
+
import { acceptanceLabel, acceptanceText, labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
|
|
13
13
|
import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
|
|
14
14
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
15
15
|
import { createMyDiffTool } from "./self-check.js";
|
|
16
16
|
import { createServiceTools } from "./services.js";
|
|
17
|
-
import { missingToolchainError, toolchainError } from "./environment-error.js";
|
|
17
|
+
import { missingToolchainError, timeLimitError, toolchainError } from "./environment-error.js";
|
|
18
18
|
import { workspaceBlock } from "./workspace.js";
|
|
19
|
-
import { codeFixPrompt, conflictKey, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
|
|
19
|
+
import { codeFixPrompt, conflictKey, escalatedFixPrompt, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
|
|
20
20
|
import { resolveRequiresChain } from "./requires.js";
|
|
21
21
|
import { notesToRead, recordNote } from "./notes.js";
|
|
22
22
|
import { reportSpecConflict } from "./activity.js";
|
|
@@ -157,9 +157,11 @@ export async function renderConformanceTests(options) {
|
|
|
157
157
|
// may read the code but writes only tests. An explorer reads the code first and writes down how the
|
|
158
158
|
// software is invoked, which is what the tests are written against.
|
|
159
159
|
const label = `${moduleName} ${options.diff !== undefined ? "spec change" : frid}`;
|
|
160
|
-
const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, options.change ?? options.diff, sectionModules(tree, moduleName)) };
|
|
161
160
|
// For a spec change: the functionalities it touches, whose acceptance tests are labelled, checked and reviewed.
|
|
162
161
|
const touched = forChange ? touchedFunctionalities(tree, frids, options.diff) : [];
|
|
162
|
+
// The acceptance tests the run's tests carry out: every functionality the change touches, or the one under test.
|
|
163
|
+
const carriedOut = forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }];
|
|
164
|
+
const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, carriedOut, options.change ?? options.diff, sectionModules(tree, moduleName)) };
|
|
163
165
|
// Any functionality of the codebase - this module's, and those of the modules it requires. A
|
|
164
166
|
// functionality's run may raise a spec conflict only with that functionality; a spec diff's, between any two.
|
|
165
167
|
const own = (forChange ? frids : frids.slice(0, frids.indexOf(frid) + 1)).map((id) => ({ module: moduleName, frid: id }));
|
|
@@ -194,6 +196,11 @@ export async function renderConformanceTests(options) {
|
|
|
194
196
|
let environmentRaises = 0;
|
|
195
197
|
/** How many fixes in a row - the code's or the tests' - changed no file: an agent that will not, or cannot, act on the verdict. */
|
|
196
198
|
let idleFixes = 0;
|
|
199
|
+
/** The last fix was the tests', for a failure judged theirs: should the run fail the same way again, the judge weighs escalating it. */
|
|
200
|
+
let testsFix;
|
|
201
|
+
/** A failure escalated to the code: should the run still fail the same way after the code's fix, the run stops. */
|
|
202
|
+
let escalated;
|
|
203
|
+
let escalation;
|
|
197
204
|
/**
|
|
198
205
|
* One attempt at the failing run, written into the notes' pitfalls - what the next judge reads
|
|
199
206
|
* first - as cause and effect: what failed, how it was judged, and what was changed; the next
|
|
@@ -218,11 +225,15 @@ export async function renderConformanceTests(options) {
|
|
|
218
225
|
};
|
|
219
226
|
stage(`exploring the code for how it is invoked${options.model ? ` (${options.model})` : ""}`);
|
|
220
227
|
// The explorer reads the conformance testing script too: it is how the tests are built and run, and so how they reach the software.
|
|
228
|
+
// The prepare script, where there is one, runs on the build before it - and only once it passes does the suite run.
|
|
221
229
|
const testingScripts = [
|
|
230
|
+
...(scripts.prepare !== undefined
|
|
231
|
+
? [{ path: relative(root, scripts.prepare), runs: "<script> <copy of the build>, before every run of the conformance testing script", content: readFileSync(scripts.prepare, "utf8"), prepare: true }]
|
|
232
|
+
: []),
|
|
222
233
|
{ path: relative(root, scripts.conformance), runs: "<script> <copy of the build> <copy of the whole conformance suite>", content: readFileSync(scripts.conformance, "utf8") },
|
|
223
234
|
];
|
|
224
235
|
// The acceptance tests the explorer's plan has to carry out, step by step.
|
|
225
|
-
const toCarryOut =
|
|
236
|
+
const toCarryOut = carriedOut
|
|
226
237
|
.map((item) => ({ module: moduleName, frid: item.frid, count: item.acceptanceTests.length }))
|
|
227
238
|
.filter((item) => item.count > 0);
|
|
228
239
|
let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, acceptance: toCarryOut, label, model: options.model, onDelta: options.onDelta });
|
|
@@ -267,6 +278,7 @@ export async function renderConformanceTests(options) {
|
|
|
267
278
|
cwd: conformanceDir,
|
|
268
279
|
customTools: [
|
|
269
280
|
notes.tool,
|
|
281
|
+
notes.gapTool,
|
|
270
282
|
work.tool,
|
|
271
283
|
...createWebTools(),
|
|
272
284
|
...fence.tools,
|
|
@@ -280,7 +292,7 @@ export async function renderConformanceTests(options) {
|
|
|
280
292
|
}),
|
|
281
293
|
createCopyResourceTool({ resources: linkedResources, dir: conformanceDir }),
|
|
282
294
|
],
|
|
283
|
-
tools: [...fence.names, "run_conformance_tests", "my_diff", "ask_explorer", "update_notes", "submit_work", "copy_resource", ...WEB_TOOLS],
|
|
295
|
+
tools: [...fence.names, "run_conformance_tests", "my_diff", "ask_explorer", "update_notes", "report_spec_gap", "submit_work", "copy_resource", ...WEB_TOOLS],
|
|
284
296
|
model: options.model,
|
|
285
297
|
label: `${label} conformance agent`,
|
|
286
298
|
role: "conformance",
|
|
@@ -308,12 +320,14 @@ export async function renderConformanceTests(options) {
|
|
|
308
320
|
// The unit tests, and the conformance suite - to reproduce the failure and check the fix, seeing only its output.
|
|
309
321
|
tools: [
|
|
310
322
|
fixerNotes.tool,
|
|
323
|
+
fixerNotes.gapTool,
|
|
311
324
|
createScriptTool({ kind: "unit", script: scripts.unit, run: () => runUnitScript(buildDir, scripts.unit), runs: unitRuns }),
|
|
312
325
|
fixerConformanceRuns.tool,
|
|
313
326
|
...fixerServices.tools,
|
|
314
327
|
],
|
|
315
|
-
toolNames: ["run_unit_tests", "run_conformance_tests", ...fixerServices.names, "update_notes"],
|
|
328
|
+
toolNames: ["run_unit_tests", "run_conformance_tests", ...fixerServices.names, "update_notes", "report_spec_gap"],
|
|
316
329
|
checkEnvironment: { run: () => runUnitScript(buildDir, scripts.unit), script: scripts.unit },
|
|
330
|
+
testingScripts: { scripts, root },
|
|
317
331
|
});
|
|
318
332
|
fixerConformanceRuns.newTurn();
|
|
319
333
|
return ask(fixer, withNotes(text, buildDir), options.onDelta);
|
|
@@ -414,6 +428,15 @@ export async function renderConformanceTests(options) {
|
|
|
414
428
|
abandonWork(start, moduleName, "the conformance testing script could not run");
|
|
415
429
|
throw lacking;
|
|
416
430
|
}
|
|
431
|
+
// A failure escalated to the code, still the same after the code's fix: neither agent could fix it, and the run stops
|
|
432
|
+
// with why the judge found it the code's. Anything else it fails on is a new failure, judged as any other.
|
|
433
|
+
if (escalated !== undefined && recheck.exitCode !== 0 && failureSignature(recheck) === escalated.signature) {
|
|
434
|
+
escalation = { reason: escalated.reason, evidence: escalated.evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000) };
|
|
435
|
+
stage("the failure escalated to the code is still there after the coding agent's fix - stopping");
|
|
436
|
+
recordNote(conformanceDir, "pitfalls", `stopped after ${rounds} attempts: failed with ${failureLine(recheck)}, escalated to the code - ${escalated.reason.replace(/\s+/g, " ").slice(0, 240)} - and the code's fix did not change it`, `${label} conformance`);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
escalated = undefined;
|
|
417
440
|
if (recheck.exitCode !== 0) {
|
|
418
441
|
// Nothing changing: the same failure, taken the same way, round after round. Once, the explorer
|
|
419
442
|
// is asked what the tests lack - most often how they reach the software; after that, it stops.
|
|
@@ -480,19 +503,6 @@ export async function renderConformanceTests(options) {
|
|
|
480
503
|
idleFixes = 0;
|
|
481
504
|
continue;
|
|
482
505
|
}
|
|
483
|
-
// A run out of time is not a question of test or code: the tests' environment is the conformance agent's.
|
|
484
|
-
if (recheck.timedOut === true) {
|
|
485
|
-
const moved = await unmoved();
|
|
486
|
-
if (moved === "stuck")
|
|
487
|
-
break;
|
|
488
|
-
if (moved === "asked")
|
|
489
|
-
continue;
|
|
490
|
-
stage("the conformance run did not get to the tests - back to the conformance agent");
|
|
491
|
-
const fixed = await changing(() => askConformance(scriptFailure(recheck, "conformance testing"), options.onDelta));
|
|
492
|
-
response = fixed.result;
|
|
493
|
-
recordAttempt(rounds, recheck, `not a question of code or tests (${recheck.timedOut === true ? "the time limit" : `exit ${recheck.exitCode}`})`, { who: "conformance agent", files: fixed.files });
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
506
|
stage(`conformance tests failing - working out whether the code, the tests or the spec are wrong${judgeModel ? ` (${judgeModel})` : ""}`);
|
|
497
507
|
// A fix that changed no file leaves the next run as this one: IDLE_FIXES of them in a row, and nothing will change.
|
|
498
508
|
const idle = (files) => {
|
|
@@ -504,6 +514,12 @@ export async function renderConformanceTests(options) {
|
|
|
504
514
|
recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)}, and ${IDLE_FIXES} fixes in a row changed nothing`, `${label} conformance`);
|
|
505
515
|
return true;
|
|
506
516
|
};
|
|
517
|
+
// The tests were fixed for this very failure, and it is still here: the judge weighs whether it is the code's.
|
|
518
|
+
const signature = failureSignature(recheck);
|
|
519
|
+
const escalating = testsFix !== undefined && testsFix.signature === signature ? { reason: testsFix.reason, changed: testsFix.files } : undefined;
|
|
520
|
+
testsFix = undefined;
|
|
521
|
+
if (escalating !== undefined)
|
|
522
|
+
stage("the tests' fix did not make the failure go away - the judge weighs whether it is the code's");
|
|
507
523
|
const verdict = await judgeFailure({
|
|
508
524
|
buildDir,
|
|
509
525
|
conformanceDir,
|
|
@@ -513,11 +529,22 @@ export async function renderConformanceTests(options) {
|
|
|
513
529
|
scope,
|
|
514
530
|
// The tests' notes - every attempt so far - and the code's: it weighs the one against the other.
|
|
515
531
|
notes: [notesToRead(conformanceDir), notesToRead(buildDir)].filter((part) => part !== "").join("\n\n"),
|
|
532
|
+
// The script itself, so the judge checks the explorer's account against it rather than repeating it.
|
|
533
|
+
testingScripts,
|
|
534
|
+
...(escalating !== undefined ? { escalation: escalating } : {}),
|
|
516
535
|
label,
|
|
517
536
|
model: judgeModel,
|
|
518
537
|
onDelta: options.onDelta,
|
|
519
538
|
});
|
|
520
539
|
const reason = verdict.reason.split("\n")[0].slice(0, 160);
|
|
540
|
+
// Stopped by the time limit while it was still getting on: no agent can give it more time. The pulse stops now -
|
|
541
|
+
// rolled back, no retry - and the person is told how to raise the limit.
|
|
542
|
+
if (verdict.culprit === "environment" && recheck.timedOut === true) {
|
|
543
|
+
stage(`the conformance run needs more than the ${Math.round(recheck.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
544
|
+
if (start !== undefined)
|
|
545
|
+
abandonWork(start, moduleName, "the conformance testing script needs more time than a test run may take");
|
|
546
|
+
throw timeLimitError(recheck, { moduleName, script: scripts.conformance, kind: "conformance", reason: verdict.reason });
|
|
547
|
+
}
|
|
521
548
|
// The environment - what the run is given - is the person's to fix: once judged so twice in a row, the run stops.
|
|
522
549
|
if (verdict.culprit === "environment") {
|
|
523
550
|
environmentRaises += 1;
|
|
@@ -585,15 +612,34 @@ export async function renderConformanceTests(options) {
|
|
|
585
612
|
recordAttempt(rounds, recheck, `a spec conflict between functionalities ${key}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
586
613
|
}
|
|
587
614
|
else if (verdict.culprit === "code") {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
615
|
+
if (escalating !== undefined) {
|
|
616
|
+
// Escalated: the coding agent is handed the script's error, the tests as they stand and the judge's evidence.
|
|
617
|
+
// Its fix is the last attempt at this failure - should the run fail the same way after it, the run stops.
|
|
618
|
+
const evidence = verdict.evidence ?? [];
|
|
619
|
+
reportActivity(` escalated to the code: ${reason}`);
|
|
620
|
+
stage("the judge finds the failure the code's - escalated to the coding agent");
|
|
621
|
+
const tests = listFiles(conformanceDir).map((path) => ({ path, content: readFileSync(join(conformanceDir, path), "utf8") }));
|
|
622
|
+
const fixed = await changing(() => askFixer(escalatedFixPrompt({ run: recheck, reason: verdict.reason, evidence, tests })));
|
|
623
|
+
recordAttempt(rounds, recheck, `escalated to the code: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
624
|
+
if (fixed.files.length === 0) {
|
|
625
|
+
escalation = { reason: verdict.reason, evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000), changedNothing: true };
|
|
626
|
+
stage("the coding agent changed nothing for the failure escalated to it - stopping");
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
idleFixes = 0;
|
|
630
|
+
escalated = { signature, reason: verdict.reason, evidence };
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
reportActivity(` the code: ${reason}`);
|
|
634
|
+
stage("the code is wrong - the coding agent fixes it");
|
|
635
|
+
// One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
|
|
636
|
+
// it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
|
|
637
|
+
// whole.
|
|
638
|
+
const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
|
|
639
|
+
recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
640
|
+
if (idle(fixed.files))
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
597
643
|
// The fix has to build and pass its own unit tests before the suite runs again: a build
|
|
598
644
|
// error is the coding agent's to fix, not a judge's to work out from the suite's output.
|
|
599
645
|
for (let again = 0; again < FIX_UNIT_CHECKS; again += 1) {
|
|
@@ -613,7 +659,11 @@ export async function renderConformanceTests(options) {
|
|
|
613
659
|
const fixed = await changing(() => askConformance(testsFixPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
|
|
614
660
|
response = fixed.result;
|
|
615
661
|
recordAttempt(rounds, recheck, `the tests wrong: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
616
|
-
|
|
662
|
+
testsFix = { signature, reason: verdict.reason, files: fixed.files };
|
|
663
|
+
// A fix that changed nothing does not stop the run before the judge has weighed escalating the failure.
|
|
664
|
+
if (escalating === undefined)
|
|
665
|
+
idleFixes = fixed.files.length === 0 ? idleFixes + 1 : 0;
|
|
666
|
+
else if (idle(fixed.files))
|
|
617
667
|
break;
|
|
618
668
|
}
|
|
619
669
|
continue;
|
|
@@ -646,7 +696,7 @@ export async function renderConformanceTests(options) {
|
|
|
646
696
|
buildDir,
|
|
647
697
|
conformanceDir,
|
|
648
698
|
spec: specContext,
|
|
649
|
-
functionalities:
|
|
699
|
+
functionalities: carriedOut,
|
|
650
700
|
forChange,
|
|
651
701
|
invocation,
|
|
652
702
|
label,
|
|
@@ -772,6 +822,7 @@ export async function renderConformanceTests(options) {
|
|
|
772
822
|
...(specConflict !== undefined ? { specConflict } : {}),
|
|
773
823
|
...(stuck !== undefined ? { stuck } : {}),
|
|
774
824
|
...(environment !== undefined ? { environment } : {}),
|
|
825
|
+
...(escalation !== undefined ? { escalation } : {}),
|
|
775
826
|
...(unlabelled !== undefined ? { unlabelled } : {}),
|
|
776
827
|
...(reviewWaived !== undefined ? { reviewWaived } : {}),
|
|
777
828
|
...(verified && green !== undefined ? { green } : {}),
|
|
@@ -905,9 +956,10 @@ export function failureLine(run) {
|
|
|
905
956
|
}
|
|
906
957
|
/**
|
|
907
958
|
* The spec as the explorer, the judge and the coding agent are shown it - its functional specs
|
|
908
|
-
* numbered with their ids, so a spec conflict can name them
|
|
959
|
+
* numbered with their ids, so a spec conflict can name them, and the acceptance tests the run carries
|
|
960
|
+
* out labelled as their tests are, so a verdict can quote them.
|
|
909
961
|
*/
|
|
910
|
-
function specText(specifications, moduleName, frids, required, diff, modules) {
|
|
962
|
+
function specText(specifications, moduleName, frids, required, acceptance, diff, modules) {
|
|
911
963
|
// Every functionality in the codebase, each with its module: the required modules' first, then this one's.
|
|
912
964
|
const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
|
|
913
965
|
? [
|
|
@@ -915,10 +967,14 @@ function specText(specifications, moduleName, frids, required, diff, modules) {
|
|
|
915
967
|
]
|
|
916
968
|
: items;
|
|
917
969
|
// Each section in its tag, as every agent is shown it (see specPrimer).
|
|
918
|
-
const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS
|
|
970
|
+
const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS]
|
|
919
971
|
.map((key) => [key, specifications[key] ?? []])
|
|
920
972
|
.filter(([, items]) => items.length > 0)
|
|
921
|
-
.map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS
|
|
973
|
+
.map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
|
|
974
|
+
// Not only the last functionality's: a spec change's run carries out the acceptance tests of every one it touches.
|
|
975
|
+
const tests = acceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `${acceptanceLabel(item.frid, index + 1)}: ${acceptanceText(test)}`));
|
|
976
|
+
if (tests.length > 0)
|
|
977
|
+
sections.push(section(ACCEPTANCE_TESTS, tests));
|
|
922
978
|
if (diff !== undefined)
|
|
923
979
|
sections.push(specChangeBlock(diff));
|
|
924
980
|
return sections.join("\n\n");
|
|
@@ -4,6 +4,7 @@ import { Type } from "typebox";
|
|
|
4
4
|
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
5
5
|
import { confinedTools } from "./confine.js";
|
|
6
6
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
7
|
+
import { runTiming } from "./test-scripts.js";
|
|
7
8
|
/**
|
|
8
9
|
* A fresh look at a failure the coding agent keeps hitting.
|
|
9
10
|
*
|
|
@@ -24,9 +25,12 @@ export function diagnosisText(diagnosis) {
|
|
|
24
25
|
}
|
|
25
26
|
function debugPrompt(input) {
|
|
26
27
|
const script = input.script !== undefined && existsSync(input.script) ? readFileSync(input.script, "utf8") : undefined;
|
|
28
|
+
const timedOut = input.run?.timedOut === true;
|
|
27
29
|
return [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
timedOut
|
|
31
|
+
? `The unit testing script of the codebase in ${input.buildDir} did not finish: the time limit stopped it. You look at it with fresh eyes.`
|
|
32
|
+
: `The unit tests of the codebase in ${input.buildDir} have failed the same way ${input.times} times in a row. The coding agent`,
|
|
33
|
+
...(timedOut ? [] : ["keeps changing things and the failure does not move: it has not seen the cause. You look at it with fresh eyes."]),
|
|
30
34
|
"",
|
|
31
35
|
"Find the root cause - the one thing that makes this failure happen - and the change that fixes it. Read the failing",
|
|
32
36
|
"run below, the tests it names, the code they exercise, the build files (pyproject.toml, package.json, go.mod, pom.xml,",
|
|
@@ -37,6 +41,21 @@ function debugPrompt(input) {
|
|
|
37
41
|
"",
|
|
38
42
|
"You change nothing - you only read. When you know, call submit_diagnosis: the root cause, with the evidence for it,",
|
|
39
43
|
"and the fix - which files, and exactly what to change in them.",
|
|
44
|
+
...(timedOut
|
|
45
|
+
? [
|
|
46
|
+
"",
|
|
47
|
+
"<timed out>",
|
|
48
|
+
"The run's output ends where the time limit stopped it: the test running then, or the step after the last one it",
|
|
49
|
+
"printed, is what did not finish. Find what kept it:",
|
|
50
|
+
"- something hangs - a test or the code waits on input, keeps a server in the foreground, never exits, loops, blocks",
|
|
51
|
+
" on a lock or a network call that never answers: the root cause and the fix, as for any failure;",
|
|
52
|
+
"- nothing hangs - the output shows the run making steady progress right up to the limit (installing, building, test",
|
|
53
|
+
" after test passing) and it needs more time than a test run may take: set too_slow, and say in root_cause from the",
|
|
54
|
+
" output how far it got and what took the time. No agent can give it more time: the work stops, and the person is",
|
|
55
|
+
" told to raise the limit. Judge from the output and the files, never from the time alone.",
|
|
56
|
+
"</timed out>",
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
40
59
|
"",
|
|
41
60
|
"<what the change is meant to do>",
|
|
42
61
|
input.task.trim(),
|
|
@@ -44,6 +63,7 @@ function debugPrompt(input) {
|
|
|
44
63
|
...(script !== undefined ? ["", `<testing script path="${input.script}">`, script.trimEnd(), "</testing script>"] : []),
|
|
45
64
|
"",
|
|
46
65
|
"<failing run>",
|
|
66
|
+
...(input.run !== undefined ? [runTiming(input.run), ""] : []),
|
|
47
67
|
input.failure.trimEnd().slice(-8000),
|
|
48
68
|
"</failing run>",
|
|
49
69
|
].join("\n");
|
|
@@ -58,10 +78,14 @@ export async function debugFailure(input) {
|
|
|
58
78
|
parameters: Type.Object({
|
|
59
79
|
root_cause: Type.String({ description: "The one thing that makes the failure happen, and the evidence for it: the lines of the run, the code, the docs" }),
|
|
60
80
|
fix: Type.String({ description: "The change that fixes it: which files, and exactly what to change in them" }),
|
|
81
|
+
too_slow: Type.Optional(Type.Boolean({
|
|
82
|
+
description: "Only for a run the time limit stopped: true when nothing hangs - it was making progress up to the limit and needs more time than a test run may take",
|
|
83
|
+
})),
|
|
61
84
|
}),
|
|
62
85
|
async execute(_id, params) {
|
|
63
86
|
const given = params;
|
|
64
|
-
|
|
87
|
+
// Too slow is a verdict on a run the limit stopped, and on no other.
|
|
88
|
+
found = { rootCause: given.root_cause, fix: given.fix, ...(given.too_slow === true && input.run?.timedOut === true ? { tooSlow: true } : {}) };
|
|
65
89
|
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
66
90
|
},
|
|
67
91
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { basename, dirname, isAbsolute, sep } from "node:path";
|
|
3
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "typebox";
|
|
@@ -35,7 +36,7 @@ export class EnvironmentError extends Error {
|
|
|
35
36
|
sandbox;
|
|
36
37
|
constructor(details) {
|
|
37
38
|
super(details.sandbox !== undefined
|
|
38
|
-
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
|
+
? `${details.moduleName} stopped: turri's sandbox kept its agents from ${details.sandbox.access === "write" ? "writing" : "reading"} ${details.sandbox.paths[0]}, which is on this machine.`
|
|
39
40
|
: `${details.moduleName} stopped: the environment lacks ${details.missing}.`);
|
|
40
41
|
this.name = "EnvironmentError";
|
|
41
42
|
this.moduleName = details.moduleName;
|
|
@@ -50,6 +51,10 @@ export class EnvironmentError extends Error {
|
|
|
50
51
|
this.sandbox = details.sandbox;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
/** A file's name that says it is a lock: `lockfile`, `x.lock`, `x.lck`, `.lock-x`, `x.lock.y`. */
|
|
55
|
+
const LOCK_FILE = /(?:^|[._-])(?:lock|lck|lockfile)(?:$|[._-])/i;
|
|
56
|
+
/** What says the sandbox refused a write, not a read: writing, creating, a lock not taken - not a name with "lock" in it. */
|
|
57
|
+
const WRITE_REFUSED = /\b(?:writ(?:e|es|ing|able)|creat(?:e|es|ing)|acquir(?:e|ing)|(?:take|obtain|get) (?:a |the )?lock|locking|read-only file system)\b/i;
|
|
53
58
|
/** A folder named for a version - `24.16.0`, `v22.3.0`, `3.12.4` - the root of a version manager's install. */
|
|
54
59
|
const VERSION = /^v?\d+\.\d+/;
|
|
55
60
|
/** The parts of an install below its root, where the root ends: `…/24.16.0/lib/node_modules/…` ends before `lib`. */
|
|
@@ -74,27 +79,59 @@ export function sandboxGapOf(text, options = {}) {
|
|
|
74
79
|
const system = (options.platform ?? process.platform) === "darwin" ? MACOS_MACHINE : LINUX_MACHINE;
|
|
75
80
|
const within = (path, root) => path === root || path.startsWith(`${root}/`);
|
|
76
81
|
const named = [...text.matchAll(/(?:^|[\s'"`(=:])(\/[^\s'"`()<>,;]+)/g)].map((match) => match[1].replace(/[.:]+$/, ""));
|
|
77
|
-
const
|
|
78
|
-
|
|
82
|
+
const isLock = (path) => LOCK_FILE.test(basename(path));
|
|
83
|
+
// A lock file the tool could not create is not there: its folder is.
|
|
84
|
+
const found = [...new Set(named)].filter((path) => path.split("/").length > 3 &&
|
|
85
|
+
(exists(path) || (isLock(path) && exists(dirname(path)))) &&
|
|
86
|
+
!system.some((root) => within(path, root)) &&
|
|
87
|
+
(options.project === undefined || !within(path, options.project)));
|
|
88
|
+
if (found.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
// The lock first: it is what the build could not take.
|
|
91
|
+
const paths = [...found.filter(isLock), ...found.filter((path) => !isLock(path))];
|
|
92
|
+
const lock = isLock(paths[0]);
|
|
93
|
+
return { paths, folder: toolchainFolder(paths[0]), access: lock || WRITE_REFUSED.test(text) ? "write" : "read", ...(lock ? { lock } : {}) };
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* What the person is told, in the chat, at the end of `turri regenerate`, and in the job's log. `rerun`: the command that runs
|
|
82
97
|
* the work again, as the pulse knows it - the spec the person ran; without it, the module's own spec by its name.
|
|
83
98
|
*/
|
|
84
99
|
export function describeEnvironmentError(error, rerun) {
|
|
85
|
-
|
|
100
|
+
const again = `Then re-run: \`${rerun ?? `turri regenerate ${error.moduleName}.plain`}\`, or say "resume" in the chat.`;
|
|
101
|
+
const needed = `Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`;
|
|
102
|
+
const gap = error.sandbox;
|
|
103
|
+
if (gap?.access === "write") {
|
|
104
|
+
const what = gap.lock === true ? `the lock file \`${gap.paths[0]}\`, which the build takes as it runs` : `\`${gap.paths[0]}\``;
|
|
105
|
+
const home = homedir();
|
|
106
|
+
const inHome = gap.paths[0] === home || gap.paths[0].startsWith(`${home}/`);
|
|
86
107
|
return [
|
|
87
|
-
`**${error.moduleName} stopped: turri's sandbox kept its agents from
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from writing ${what} - it is on this machine; nothing is missing, and nothing needs installing.**`,
|
|
109
|
+
needed,
|
|
110
|
+
"An agent's commands write only their module's folders, their own home and temporary folder, and the package caches turri " +
|
|
111
|
+
"points the build tools at - no setting lets them write anywhere else, and `toolchains:` lets them read a folder, not write it.",
|
|
112
|
+
inHome
|
|
113
|
+
? "To fix: it is in your home, not the agents' own - the tool found your home without `$HOME`, as a JVM does for `user.home`, " +
|
|
114
|
+
"or a setting of yours points it there. Have it use `$HOME` instead - for a JVM tool, `-Duser.home=$HOME` where the build " +
|
|
115
|
+
"starts its JVM - and the agents' own home takes it."
|
|
116
|
+
: `To fix: it is in a toolchain's own install, \`${gap.folder}\`, which the build writes into as it runs - and the agents may ` +
|
|
117
|
+
"not change a toolchain. Have the build keep it elsewhere if the tool has a setting for it, or install a copy of the " +
|
|
118
|
+
"toolchain that keeps its writes in its cache.",
|
|
119
|
+
again,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
if (gap !== undefined) {
|
|
123
|
+
return [
|
|
124
|
+
`**${error.moduleName} stopped: turri's sandbox kept its agents from reading \`${gap.paths[0]}\` - it is on this machine; nothing is missing.**`,
|
|
125
|
+
needed,
|
|
126
|
+
`To fix: let the agents read that toolchain - add its folder to \`toolchains:\` in config.turri.yaml, keeping what is there: \`toolchains: [${gap.folder}]\`. Nothing needs installing.`,
|
|
127
|
+
again,
|
|
91
128
|
].join("\n");
|
|
92
129
|
}
|
|
93
130
|
return [
|
|
94
131
|
`**${error.moduleName} stopped: the environment lacks ${error.missing}.**`,
|
|
95
132
|
`Needed by: ${error.neededBy}${error.shownBy !== undefined ? ` (${error.shownBy})` : ""}.`,
|
|
96
133
|
`To fix: ${error.fix ?? "provide it on this machine, then run it again."}`,
|
|
97
|
-
|
|
134
|
+
again,
|
|
98
135
|
].join("\n");
|
|
99
136
|
}
|
|
100
137
|
/** The name agents know the tool by - their wall text names it. */
|
|
@@ -187,6 +224,23 @@ export function toolchainError(missing, details) {
|
|
|
187
224
|
source: "harness",
|
|
188
225
|
});
|
|
189
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* A testing script the time limit stopped while it was still making progress - judged so by the judge or the debugger,
|
|
229
|
+
* from where its output stops: nothing hangs, the suite needs more time than a test run may take. No agent can give it
|
|
230
|
+
* that, so the pulse stops and the person is told how to.
|
|
231
|
+
*/
|
|
232
|
+
export function timeLimitError(run, details) {
|
|
233
|
+
const limit = Math.round(run.timeoutMs / 1000);
|
|
234
|
+
return new EnvironmentError({
|
|
235
|
+
moduleName: details.moduleName,
|
|
236
|
+
missing: `more time for a test run than its ${limit}s limit`,
|
|
237
|
+
neededBy: `the ${details.kind} testing script, \`${basename(details.script)}\``,
|
|
238
|
+
shownBy: `it ran ${Math.round((run.elapsedMs ?? run.timeoutMs) / 1000)}s and was stopped at the limit, still making progress - ${details.reason.replace(/\s+/g, " ").trim().slice(0, 400)}`,
|
|
239
|
+
fix: `give a test run more time - set TURRI_COMMAND_TIMEOUT, in seconds, above ${limit} (e.g. TURRI_COMMAND_TIMEOUT=${limit * 3}) in the ` +
|
|
240
|
+
"environment turri runs in - or make the suite faster: what it installs or builds on every run, and the tests that take longest.",
|
|
241
|
+
source: "agent",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
190
244
|
/** The harness's run of a testing script, when it could not find its toolchain: the error that stops the pulse. */
|
|
191
245
|
export function missingToolchainError(run, details) {
|
|
192
246
|
const missing = missingToolchain(run);
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -16,7 +16,7 @@ import { modulesRoot } from "./paths.js";
|
|
|
16
16
|
import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn } from "../config.js";
|
|
17
17
|
import { describeSpecChange } from "./prompt.js";
|
|
18
18
|
import { resolveTemplateDirs } from "./paths.js";
|
|
19
|
-
import { builtTips, copyRequiredCode, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
19
|
+
import { builtTips, copyRequiredCode, importingModules, isImportModule, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
20
20
|
import { withTryItLine } from "./delivery.js";
|
|
21
21
|
import { describeSpecDiff, renderSpecDiff, specFilesOf } from "./regenerate.js";
|
|
22
22
|
import { locateDiffLines } from "./diff-view.js";
|
|
@@ -85,6 +85,23 @@ function chainsFor(plainFile) {
|
|
|
85
85
|
const tips = builtTips(plainFile);
|
|
86
86
|
return tips.length > 0 ? tips : [resolveRequiresChain(plainFile, [], undefined, { allowNoFunctionalities: true })];
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The modules a .plain file is built as: itself - or, an import module, the modules importing it whose code is started.
|
|
90
|
+
* An import module is no codebase; one no started module imports has nothing to build yet - its reqs are built with the
|
|
91
|
+
* first module that imports it.
|
|
92
|
+
*/
|
|
93
|
+
function codebasesOf(plainFile) {
|
|
94
|
+
if (!isImportModule(plainFile))
|
|
95
|
+
return [plainFile];
|
|
96
|
+
return importingModules(plainFile).filter((file) => {
|
|
97
|
+
try {
|
|
98
|
+
return builtTips(file).length > 0 || started(resolveRequiresChain(file, [], undefined, { allowNoFunctionalities: true }).at(-1));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
88
105
|
/** How many functional specs of its own a codebase has built - none when it was only started from the code below it. */
|
|
89
106
|
function ownBuilt(entry) {
|
|
90
107
|
return (readRecord(entry.buildDir)?.[FUNCTIONAL_REQUIREMENTS] ?? []).length;
|
|
@@ -989,9 +1006,10 @@ export function createWorkLoop(options) {
|
|
|
989
1006
|
};
|
|
990
1007
|
const loop = {
|
|
991
1008
|
async bringUp(plainFile, request = {}) {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
1009
|
+
const files = codebasesOf(resolve(options.cwd, plainFile));
|
|
1010
|
+
for (const file of files)
|
|
1011
|
+
asked.set(file, { ...asked.get(file), ...request });
|
|
1012
|
+
const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request)))));
|
|
995
1013
|
// Asked again, a halted codebase is tried again.
|
|
996
1014
|
for (const state of states)
|
|
997
1015
|
state.halted = undefined;
|