@turri-dev/cli 0.1.3-next.79e2ad0 → 0.1.3-next.b675516

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.
@@ -7,6 +7,7 @@ import { confinedTools } from "./confine.js";
7
7
  import { ensureInModulesScratch, modulesScratchDir } from "./paths.js";
8
8
  import { workspaceBlock } from "./workspace.js";
9
9
  import { reportActivity } from "./activity.js";
10
+ import { runTiming, withTestingScripts } from "./test-scripts.js";
10
11
  import { createWebTools, WEB_TOOLS } from "./web.js";
11
12
  import { diffFiles, digestFiles } from "./codebase.js";
12
13
  import { ERROR_CAUSE_RULES, fixMethod, INTEGRITY_RULES, specPrimer } from "./prompt.js";
@@ -377,9 +378,17 @@ export function explorePrompt(buildDir, spec, scope, testingScripts, question, p
377
378
  ...(testingScripts !== undefined && testingScripts.length > 0
378
379
  ? [
379
380
  "",
380
- "This is the project's conformance testing script, which the harness runs to decide whether the",
381
- "tests pass - read it for what it builds or installs before the tests run, and so what the tests can depend on:",
382
- ...testingScripts.flatMap((script) => ["", `<script path="${script.path}" run-as="${script.runs}">`, script.content.trimEnd(), "</script>"]),
381
+ ...(testingScripts.some((script) => script.prepare === true)
382
+ ? [
383
+ "These are the project's prepare script, which the harness runs on the build first, and its conformance testing",
384
+ "script, which runs only once the prepare script passes and decides whether the tests pass - read both for what",
385
+ "they build or install before the tests run, and so what the tests can depend on:",
386
+ ]
387
+ : [
388
+ "This is the project's conformance testing script, which the harness runs to decide whether the",
389
+ "tests pass - read it for what it builds or installs before the tests run, and so what the tests can depend on:",
390
+ ]),
391
+ ...testingScripts.flatMap((script) => ["", `<script${script.prepare === true ? ' kind="prepare environment"' : ""} path="${script.path}" run-as="${script.runs}">`, script.content.trimEnd(), "</script>"]),
383
392
  ]
384
393
  : []),
385
394
  ...(previous !== undefined && question === undefined
@@ -659,15 +668,28 @@ function words(text) {
659
668
  * in the specification - or when it hides why the software failed. A test asserting what the
660
669
  * specification leaves open is the test's mistake, however plausible the assertion.
661
670
  */
662
- export function unfoundedCodeVerdict(verdict, specText) {
671
+ export function unfoundedCodeVerdict(verdict, specText, scriptText) {
663
672
  if (verdict.causeHidden === true)
664
673
  return undefined;
674
+ // What the conformance testing script needs of the build - a file, an entry point, a module - binds the code as the
675
+ // specification does: neither agent can change the script. Its own words, as they are in it.
676
+ const needs = verdict.scriptNeeds?.trim() ?? "";
677
+ if (needs !== "" && scriptText !== undefined) {
678
+ const flat = (text) => text.replace(/\s+/g, " ");
679
+ if (flat(needs).length >= 4 && flat(scriptText).includes(flat(needs)))
680
+ return undefined;
681
+ if (verdict.specSays === undefined) {
682
+ return (`Your verdict blames the code for what the conformance testing script needs, but "${needs}" is not in the script. ` +
683
+ "Call submit_verdict again: copy into script_needs the script's own words for what it needs of the build, exactly as they are in it.");
684
+ }
685
+ }
665
686
  const quote = verdict.specSays?.trim() ?? "";
666
687
  if (quote === "") {
667
688
  return ("Your verdict blames the code, but quotes no words of the specification it does not meet. Call submit_verdict " +
668
689
  "again: for code, put the specification's own words the code does not meet in spec_says, copied exactly from " +
669
- "<specification>; or set cause_hidden when the output does not show why the software failed. If the specification " +
670
- "does not say what the failing test asserts, the test is what is wrong: culprit tests.");
690
+ "<specification>; or, when the run fails on something the conformance testing script needs the build to have, " +
691
+ "its own words in script_needs; or set cause_hidden when the output does not show why the software failed. If " +
692
+ "the specification does not say what the failing test asserts, the test is what is wrong: culprit tests.");
671
693
  }
672
694
  const quoted = words(quote);
673
695
  if (quoted.trim().split(" ").length < 4 || !words(specText).includes(quoted)) {
@@ -677,6 +699,31 @@ export function unfoundedCodeVerdict(verdict, specText) {
677
699
  }
678
700
  return undefined;
679
701
  }
702
+ /**
703
+ * Why a verdict's evidence cannot be taken as it is, or nothing when it can. Asked to weigh an escalation, the judge has
704
+ * to rest its verdict on what it found itself: each piece of evidence from a file it opened, the testing script or the
705
+ * failing run - and at least one not the run alone, which every judge before it read too. What the explorer's account
706
+ * says is not evidence: it was written before the run, and may be wrong.
707
+ */
708
+ export function unfoundedEvidence(evidence, opened) {
709
+ const given = (evidence ?? []).filter((item) => item.source.trim() !== "" && item.shows.trim() !== "");
710
+ if (given.length === 0) {
711
+ return ("Your verdict gives no evidence. Call submit_verdict again with `evidence`: what you found yourself - in a file you " +
712
+ "opened, the testing script, or the failing run - each with where. What <how to invoke it> says is not evidence: it " +
713
+ "may be wrong, so check it against the testing script and the files.");
714
+ }
715
+ const inPrompt = (source) => /^(the )?(conformance )?testing script$|^(the )?prepare script$|^(the )?failing run$/i.test(source.trim());
716
+ const unread = given.filter((item) => !inPrompt(item.source) && !opened(item.source.trim()));
717
+ if (unread.length > 0) {
718
+ return (`Your evidence cites ${unread.map((item) => item.source.trim()).join(", ")}, which you did not open. Open each file you cite ` +
719
+ "and call submit_verdict again - evidence is what you found yourself, never what <how to invoke it> says.");
720
+ }
721
+ if (given.every((item) => /failing run$/i.test(item.source.trim()))) {
722
+ return ("Your evidence is only the failing run, which the judges before you read too. Check what it points at - the testing " +
723
+ "script, the code, the tests - and call submit_verdict again with what you found there.");
724
+ }
725
+ return undefined;
726
+ }
680
727
  export async function judgeFailure(options) {
681
728
  let verdict;
682
729
  const submit = defineTool({
@@ -692,6 +739,13 @@ export async function judgeFailure(options) {
692
739
  description: "For code: the specification's own words the code does not meet, copied exactly from <specification> - the sentence, or the part of it that says what the test checks",
693
740
  })),
694
741
  cause_hidden: Type.Optional(Type.Boolean({ description: "For code: true when it is wrong because the output does not show why the software failed - it hides the cause" })),
742
+ script_needs: Type.Optional(Type.String({
743
+ description: "For code: the conformance testing script's own words for what it needs the build to have - the file, entry point or module the run failed on - copied exactly from the script",
744
+ })),
745
+ evidence: Type.Optional(Type.Array(Type.Object({
746
+ source: Type.String({ description: "Where: the path of a file you opened, `testing script`, `prepare script` where there is one, or `failing run`" }),
747
+ shows: Type.String({ description: "What it shows, e.g. `the script runs node dist/cli.js` or `tsconfig.json emits src/cli.ts as dist/src/cli.js`" }),
748
+ }), { description: "What you found yourself, each with where - never what <how to invoke it> claims unchecked" })),
695
749
  functionalities: Type.Optional(Type.Array(Type.Object({
696
750
  module: Type.String({ description: "The module the functionality is in" }),
697
751
  id: Type.String({ description: "Its id in that module" }),
@@ -704,6 +758,8 @@ export async function judgeFailure(options) {
704
758
  reason: given.reason,
705
759
  ...(given.spec_says?.trim() ? { specSays: given.spec_says.trim() } : {}),
706
760
  ...(given.cause_hidden === true ? { causeHidden: true } : {}),
761
+ ...(given.script_needs?.trim() ? { scriptNeeds: given.script_needs.trim() } : {}),
762
+ ...(given.evidence !== undefined && given.evidence.length > 0 ? { evidence: given.evidence } : {}),
707
763
  ...(given.functionalities ? { functionalities: conflictRefs(given.functionalities) } : {}),
708
764
  };
709
765
  return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
@@ -712,10 +768,27 @@ export async function judgeFailure(options) {
712
768
  // It reads both, and changes neither: the file tools and the web - to look up what a service it calls answers -
713
769
  // and no shell. It judges from the code, the tests, the spec and the failing run's output, which it is given.
714
770
  const fence = confinedTools({ workdir: options.conformanceDir, read: [options.buildDir, options.conformanceDir], write: [], noBash: true });
771
+ // What it opens - read, listed, searched - is what its evidence can cite.
772
+ const openedPaths = new Set();
773
+ const tools = fence.tools.map((tool) => ({
774
+ ...tool,
775
+ execute: (async (...args) => {
776
+ const path = args[1]?.path;
777
+ if (typeof path === "string" && path !== "")
778
+ openedPaths.add(resolve(options.conformanceDir, path));
779
+ return tool.execute(...args);
780
+ }),
781
+ }));
782
+ const opened = (cited) => {
783
+ // A file it opened, or one in a folder it listed or searched; cited as it was opened, or relative to the code or the tests.
784
+ const candidates = [resolve(options.conformanceDir, cited), resolve(options.buildDir, cited)];
785
+ return candidates.some((path) => [...openedPaths].some((seen) => path === seen || path.startsWith(`${seen}/`)));
786
+ };
787
+ const scriptText = options.testingScripts?.map((script) => script.content).join("\n");
715
788
  const before = { code: digestFiles(options.buildDir), tests: digestFiles(options.conformanceDir) };
716
789
  const session = await createExecutor({
717
790
  cwd: options.conformanceDir,
718
- customTools: [submit, ...createWebTools(), ...fence.tools],
791
+ customTools: [submit, ...createWebTools(), ...tools],
719
792
  tools: [...fence.names, ...WEB_TOOLS, "submit_verdict"],
720
793
  model: options.model,
721
794
  label: `${options.label} judge`, role: "judge",
@@ -730,8 +803,15 @@ export async function judgeFailure(options) {
730
803
  await ask(session, "You have not called submit_verdict. Call it now: code, tests, spec or environment, and why.", options.onDelta);
731
804
  continue;
732
805
  }
806
+ // Asked to weigh an escalation, a verdict of code or tests rests on what it found itself.
807
+ const unproven = options.escalation !== undefined && (verdict.culprit === "code" || verdict.culprit === "tests") ? unfoundedEvidence(verdict.evidence, opened) : undefined;
808
+ if (unproven !== undefined) {
809
+ verdict = undefined;
810
+ await ask(session, unproven, options.onDelta);
811
+ continue;
812
+ }
733
813
  if (verdict.culprit === "code") {
734
- const unfounded = unfoundedCodeVerdict(verdict, options.spec.text);
814
+ const unfounded = unfoundedCodeVerdict(verdict, options.spec.text, scriptText);
735
815
  if (unfounded === undefined)
736
816
  break;
737
817
  verdict = undefined;
@@ -759,18 +839,26 @@ export async function judgeFailure(options) {
759
839
  }
760
840
  if (verdict?.culprit === "spec" && specConflictProblem(verdict.functionalities, options.scope) !== undefined)
761
841
  verdict = undefined;
762
- if (verdict?.culprit === "code" && unfoundedCodeVerdict(verdict, options.spec.text) !== undefined) {
842
+ // An escalation weighed without evidence escalates nothing: the tests stay the conformance agent's, as before it.
843
+ if (options.escalation !== undefined && verdict?.culprit === "code" && unfoundedEvidence(verdict.evidence, opened) !== undefined) {
844
+ return { culprit: "tests", reason: `${verdict.reason}\n\nBut the judge gave no evidence it found itself that the code is what is wrong, so the tests are looked at again.` };
845
+ }
846
+ if (verdict?.culprit === "code" && unfoundedCodeVerdict(verdict, options.spec.text, scriptText) !== undefined) {
763
847
  // Blaming the code takes the specification's own words; without them, the test asserts what the spec does not say.
764
848
  return { culprit: "tests", reason: `${verdict.reason}\n\nBut no words of the specification say so: a test that asserts what the specification does not say is what is wrong. Check each failing assertion against the specification, and assert only what it says.` };
765
849
  }
766
850
  if (verdict?.culprit === "code" && verdict.specSays !== undefined) {
767
851
  verdict = { ...verdict, reason: `${verdict.reason}\n\nThe specification says: "${verdict.specSays}"` };
768
852
  }
853
+ if (verdict?.culprit === "code" && verdict.scriptNeeds !== undefined) {
854
+ verdict = { ...verdict, reason: `${verdict.reason}\n\nThe conformance testing script needs: "${verdict.scriptNeeds}"` };
855
+ }
769
856
  // No verdict is no evidence the code is wrong: the tests are the newer work, and looked at first.
770
857
  return verdict ?? { culprit: "tests", reason: "The judge gave no verdict that could be used; check the failing tests against the specification first." };
771
858
  }
772
859
  export function judgePrompt(options) {
773
860
  const tested = options.scope.tested;
861
+ const scripts = options.testingScripts ?? [];
774
862
  return [
775
863
  specPrimer({ testReqs: true }),
776
864
  `The conformance tests of the \`${options.spec.moduleName}\` specification failed. Decide what is wrong: the code, the tests, the specification itself, or the run's environment.`,
@@ -780,7 +868,9 @@ export function judgePrompt(options) {
780
868
  "The specification is the judge, never either of them:",
781
869
  "- tests: a test asserts something the specification does not say, misreads it, or reaches the software the",
782
870
  " wrong way - a wrong command, route or setup (see <how to invoke it>).",
783
- "- code: the code does not do what the specification says, and the test checks exactly that.",
871
+ "- code: the code does not do what the specification says, and the test checks exactly that. Or the run fails",
872
+ " on something the conformance testing script needs the code to provide - a build output, an entry point, a",
873
+ " module - and the code does not provide it: quote the script's own words for it in script_needs.",
784
874
  " Only what the specification says counts: anything it leaves open - a name, coordinates, a version, a path,",
785
875
  " a package, a port, a file layout - is what the code already has. The tests follow it, and it is never",
786
876
  " code - changing the code to fit a test's guess moves what every other test points at.",
@@ -801,6 +891,8 @@ export function judgePrompt(options) {
801
891
  " the tests set up themselves - a credential, a variable or a fixture a test sets, overrides or names wrong: that is",
802
892
  " tests; nor for a toolchain or package missing where the tests run: the agents install those. The run stops on it",
803
893
  ` once it has been judged so ${ENVIRONMENT_RAISES} times in a row; until then the tests are checked for whether they cause it.`,
894
+ " A run the time limit stopped is environment only when nothing hangs and it needs more time than a test run may",
895
+ " take - and then it stops at once.",
804
896
  "",
805
897
  "When the output does not show why the software failed - it answered with an error that names no cause, and",
806
898
  "logged nothing about it - the code is what is wrong first: it hides the cause, and nobody can fix what they",
@@ -810,6 +902,25 @@ export function judgePrompt(options) {
810
902
  "verdict of code without them is not taken. What the specification leaves open is never the code's fault: a test",
811
903
  "that asserts it - a status code, a message, a format the specification does not name - is what is wrong.",
812
904
  "",
905
+ ...(scripts.length > 0
906
+ ? [
907
+ "The conformance testing script below is how every run is made, and neither agent can change it: it is never",
908
+ "what is wrong. What it does with the tests, the tests have to fit; what it needs of the code - a file it runs or",
909
+ "checks for, a command it builds with - the code has to provide. A verdict that the script is wrong is neither",
910
+ "code nor tests: say which of the two has to change to get past it.",
911
+ ...(scripts.some((script) => script.prepare === true)
912
+ ? [
913
+ "The prepare script below it runs on the build before every run, and the conformance testing script only once it",
914
+ "passes: it binds the same way - neither agent can change it, and what it needs of the code the code provides.",
915
+ ]
916
+ : []),
917
+ "",
918
+ ]
919
+ : []),
920
+ "<how to invoke it> is the explorer's account, written before the run, and it can be wrong - a folder, a path, a",
921
+ "command. Never rest a verdict on it: check each claim you use against the testing script and the files, and give",
922
+ "in `evidence` what you found yourself, each with where.",
923
+ "",
813
924
  `You have ${JUDGE_CALLS} tool calls to decide in: read what the failure points at, not the whole codebase.`,
814
925
  "",
815
926
  "When the code and the tests could both be what is wrong, choose tests. When both are wrong, say which to fix",
@@ -832,11 +943,63 @@ export function judgePrompt(options) {
832
943
  "",
833
944
  `<how to invoke it>\n${options.invocation.trim()}\n</how to invoke it>`,
834
945
  "",
946
+ ...scripts.flatMap((script) => {
947
+ const tag = script.prepare === true ? "prepare script" : "conformance testing script";
948
+ return [`<${tag} path="${script.path}" run-as="${script.runs}">`, script.content.trimEnd(), `</${tag}>`, ""];
949
+ }),
950
+ ...(options.run.timedOut === true ? [...timedOutBlock(options.run), ""] : []),
835
951
  `<failing run exit="${options.run.exitCode}" command="${options.run.command}">`,
952
+ runTiming(options.run),
953
+ "",
836
954
  options.run.output.trimEnd(),
837
955
  "</failing run>",
956
+ ...(options.escalation !== undefined ? ["", ...escalationBlock(options.escalation)] : []),
838
957
  ].join("\n");
839
958
  }
959
+ /**
960
+ * For the judge, of a run the time limit stopped: what kept it from finishing - something that hangs, the code's or a
961
+ * test's, or a suite that is only slow, which no agent can give more time.
962
+ */
963
+ export function timedOutBlock(run) {
964
+ const limit = Math.round(run.timeoutMs / 1000);
965
+ return [
966
+ "<timed out>",
967
+ `This run did not end on its own: the time limit stopped it at ${limit}s, and it counts as failed. Its output ends where it`,
968
+ "was stopped - the step running then, or the one after the last it printed, is what did not finish. Decide what kept it:",
969
+ "- code: the software hangs - it waits on input, keeps a server in the foreground, never exits, loops, or blocks on a",
970
+ " lock or a call that never answers - when the tests reach it as the specification says.",
971
+ "- tests: a test waits for something that never comes - a process it started and never stops, output it waits for that",
972
+ " the software never prints, a port or a file nobody makes, a sleep or a retry with no end.",
973
+ "- environment: nothing hangs - the output shows the run making steady progress right up to the limit (installing,",
974
+ " building, test after test passing) and it needs more time than a test run may take. No agent can give it that: the",
975
+ " run stops at once, and the person is told to raise the limit. Say from the output how far it got, and what took",
976
+ " the time.",
977
+ "Judge from the output and the files, never from the time alone.",
978
+ "</timed out>",
979
+ ];
980
+ }
981
+ /** For the judge: the tests were fixed for this failure, and it did not go away - is it the code's? */
982
+ function escalationBlock(escalation) {
983
+ const changed = escalation.changed.length === 0 ? "changed no file" : `changed ${escalation.changed.slice(0, 8).join(", ")}${escalation.changed.length > 8 ? ` and ${escalation.changed.length - 8} more` : ""}`;
984
+ return [
985
+ "<escalation>",
986
+ "This failure was judged the tests' before, and the conformance agent - who can change only the tests - was sent to",
987
+ `fix it. It ${changed}, and the run fails the same way. What it was told:`,
988
+ "",
989
+ escalation.reason.trim(),
990
+ "",
991
+ "Decide again, now whether the failure belongs to the code rather than the tests. It is the code's when the run",
992
+ "fails on something the code is meant to produce - a build output, an entry point, a module the testing script needs",
993
+ "- or when the tests ran and what the software does is not what the specification says. It stays the tests' when a",
994
+ "test is still wrong and the code is right: say exactly what in the tests, so the next fix is not the last one again.",
995
+ "",
996
+ "Gather the evidence yourself: open the files the failure points at - the code's build configuration and what it",
997
+ "emits, the tests - and read the testing script. Give it in `evidence`: a verdict of code or tests without evidence",
998
+ "you found is not taken. The coding agent is handed your reason and evidence: say what the code has to provide, and",
999
+ "where it now falls short (e.g. `the script runs dist/cli.js; the build emits dist/src/cli.js`).",
1000
+ "</escalation>",
1001
+ ];
1002
+ }
840
1003
  /**
841
1004
  * The coding agent a conformance run sends the code to, when the judge says the code is wrong: it
842
1005
  * works in the code folder, may read the tests but never changes them, and keeps the unit
@@ -858,7 +1021,10 @@ export async function createCodeFixer(options) {
858
1021
  model: options.model,
859
1022
  label: `${options.label} coding agent`, role: "coding",
860
1023
  system: `${fence.note}\n\n${workspaceBlock({ dir: options.buildDir, hasMyDiff: options.toolNames.includes("my_diff") })}`,
861
- task: codeFixerTask(options),
1024
+ // What the conformance testing script needs of the build, the code provides: the fixer reads it, not only a judge's quote of it.
1025
+ task: options.testingScripts !== undefined
1026
+ ? withTestingScripts(codeFixerTask(options), options.testingScripts.scripts, ["conformance", "unit"], options.testingScripts.root)
1027
+ : codeFixerTask(options),
862
1028
  compaction: { instructions: CODER_COMPACTION },
863
1029
  ...(options.checkEnvironment !== undefined ? { checkEnvironment: options.checkEnvironment } : {}),
864
1030
  });
@@ -911,12 +1077,53 @@ export function codeFixPrompt(options) {
911
1077
  `<why the code>\n${options.reason.trim()}\n</why the code>`,
912
1078
  "",
913
1079
  `<failing run command="${options.run.command}">`,
1080
+ runTiming(options.run),
1081
+ "",
914
1082
  options.run.output.trimEnd(),
915
1083
  "</failing run>",
916
1084
  "",
917
1085
  "Fix the code, run the conformance tests and the unit tests, and finish.",
918
1086
  ].join("\n");
919
1087
  }
1088
+ /** How much of the conformance tests an escalation hands the coding agent, whole: the rest it reads itself. */
1089
+ const ESCALATED_TESTS_CHARS = 24_000;
1090
+ /**
1091
+ * An escalated failure for the code fixer: the tests were fixed for it and it did not go away, and the judge found it
1092
+ * the code's. It is handed the conformance testing script's error, the tests as they stand and the judge's evidence.
1093
+ */
1094
+ export function escalatedFixPrompt(options) {
1095
+ let room = ESCALATED_TESTS_CHARS;
1096
+ const shown = options.tests.flatMap((test) => {
1097
+ if (room <= 0)
1098
+ return [`<conformance test path="${test.path}" shown="no - read it yourself" />`];
1099
+ const content = test.content.length > room ? `${test.content.slice(0, room)}\n... (cut short - read the rest yourself)` : test.content;
1100
+ room -= content.length;
1101
+ return [`<conformance test path="${test.path}">`, content.trimEnd(), "</conformance test>"];
1102
+ });
1103
+ return [
1104
+ `The conformance tests failed (exit ${options.run.exitCode}). The conformance agent was sent to fix the tests for it, and`,
1105
+ "the run failed the same way. A judge then looked again, gathered the evidence below itself, and found that the",
1106
+ "failure belongs to the code:",
1107
+ "",
1108
+ `<why the code>\n${options.reason.trim()}\n</why the code>`,
1109
+ "",
1110
+ "<evidence>",
1111
+ ...options.evidence.map((item) => `- ${item.source.trim()}: ${item.shows.trim()}`),
1112
+ "</evidence>",
1113
+ "",
1114
+ `<failing run command="${options.run.command}">`,
1115
+ runTiming(options.run),
1116
+ "",
1117
+ options.run.output.trimEnd(),
1118
+ "</failing run>",
1119
+ "",
1120
+ "The conformance tests as they stand - not yours to change:",
1121
+ ...shown,
1122
+ "",
1123
+ "Fix the code, run the conformance tests and the unit tests, and finish. This is the last attempt at this failure:",
1124
+ "if the run still fails the same way after your fix, the work stops.",
1125
+ ].join("\n");
1126
+ }
920
1127
  /** The run's environment judged what is wrong: the conformance agent checks the tests do not cause it, and changes nothing when they do not. */
921
1128
  export function environmentCheckPrompt(options) {
922
1129
  return [
@@ -944,6 +1151,8 @@ export function testsFixPrompt(options) {
944
1151
  `<why the tests>\n${options.reason.trim()}\n</why the tests>`,
945
1152
  "",
946
1153
  "<failing run>",
1154
+ runTiming(options.run),
1155
+ "",
947
1156
  options.run.output.trimEnd(),
948
1157
  "</failing run>",
949
1158
  "",
@@ -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 = (forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }])
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 });
@@ -314,6 +325,7 @@ export async function renderConformanceTests(options) {
314
325
  ],
315
326
  toolNames: ["run_unit_tests", "run_conformance_tests", ...fixerServices.names, "update_notes"],
316
327
  checkEnvironment: { run: () => runUnitScript(buildDir, scripts.unit), script: scripts.unit },
328
+ testingScripts: { scripts, root },
317
329
  });
318
330
  fixerConformanceRuns.newTurn();
319
331
  return ask(fixer, withNotes(text, buildDir), options.onDelta);
@@ -414,6 +426,15 @@ export async function renderConformanceTests(options) {
414
426
  abandonWork(start, moduleName, "the conformance testing script could not run");
415
427
  throw lacking;
416
428
  }
429
+ // A failure escalated to the code, still the same after the code's fix: neither agent could fix it, and the run stops
430
+ // with why the judge found it the code's. Anything else it fails on is a new failure, judged as any other.
431
+ if (escalated !== undefined && recheck.exitCode !== 0 && failureSignature(recheck) === escalated.signature) {
432
+ escalation = { reason: escalated.reason, evidence: escalated.evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000) };
433
+ stage("the failure escalated to the code is still there after the coding agent's fix - stopping");
434
+ 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`);
435
+ break;
436
+ }
437
+ escalated = undefined;
417
438
  if (recheck.exitCode !== 0) {
418
439
  // Nothing changing: the same failure, taken the same way, round after round. Once, the explorer
419
440
  // is asked what the tests lack - most often how they reach the software; after that, it stops.
@@ -480,19 +501,6 @@ export async function renderConformanceTests(options) {
480
501
  idleFixes = 0;
481
502
  continue;
482
503
  }
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
504
  stage(`conformance tests failing - working out whether the code, the tests or the spec are wrong${judgeModel ? ` (${judgeModel})` : ""}`);
497
505
  // 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
506
  const idle = (files) => {
@@ -504,6 +512,12 @@ export async function renderConformanceTests(options) {
504
512
  recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)}, and ${IDLE_FIXES} fixes in a row changed nothing`, `${label} conformance`);
505
513
  return true;
506
514
  };
515
+ // The tests were fixed for this very failure, and it is still here: the judge weighs whether it is the code's.
516
+ const signature = failureSignature(recheck);
517
+ const escalating = testsFix !== undefined && testsFix.signature === signature ? { reason: testsFix.reason, changed: testsFix.files } : undefined;
518
+ testsFix = undefined;
519
+ if (escalating !== undefined)
520
+ stage("the tests' fix did not make the failure go away - the judge weighs whether it is the code's");
507
521
  const verdict = await judgeFailure({
508
522
  buildDir,
509
523
  conformanceDir,
@@ -513,11 +527,22 @@ export async function renderConformanceTests(options) {
513
527
  scope,
514
528
  // The tests' notes - every attempt so far - and the code's: it weighs the one against the other.
515
529
  notes: [notesToRead(conformanceDir), notesToRead(buildDir)].filter((part) => part !== "").join("\n\n"),
530
+ // The script itself, so the judge checks the explorer's account against it rather than repeating it.
531
+ testingScripts,
532
+ ...(escalating !== undefined ? { escalation: escalating } : {}),
516
533
  label,
517
534
  model: judgeModel,
518
535
  onDelta: options.onDelta,
519
536
  });
520
537
  const reason = verdict.reason.split("\n")[0].slice(0, 160);
538
+ // Stopped by the time limit while it was still getting on: no agent can give it more time. The pulse stops now -
539
+ // rolled back, no retry - and the person is told how to raise the limit.
540
+ if (verdict.culprit === "environment" && recheck.timedOut === true) {
541
+ stage(`the conformance run needs more than the ${Math.round(recheck.timeoutMs / 1000)}s a test run may take - stopping`);
542
+ if (start !== undefined)
543
+ abandonWork(start, moduleName, "the conformance testing script needs more time than a test run may take");
544
+ throw timeLimitError(recheck, { moduleName, script: scripts.conformance, kind: "conformance", reason: verdict.reason });
545
+ }
521
546
  // The environment - what the run is given - is the person's to fix: once judged so twice in a row, the run stops.
522
547
  if (verdict.culprit === "environment") {
523
548
  environmentRaises += 1;
@@ -585,15 +610,34 @@ export async function renderConformanceTests(options) {
585
610
  recordAttempt(rounds, recheck, `a spec conflict between functionalities ${key}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
586
611
  }
587
612
  else if (verdict.culprit === "code") {
588
- reportActivity(` the code: ${reason}`);
589
- stage("the code is wrong - the coding agent fixes it");
590
- // One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
591
- // it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
592
- // whole.
593
- const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
594
- recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
595
- if (idle(fixed.files))
596
- break;
613
+ if (escalating !== undefined) {
614
+ // Escalated: the coding agent is handed the script's error, the tests as they stand and the judge's evidence.
615
+ // Its fix is the last attempt at this failure - should the run fail the same way after it, the run stops.
616
+ const evidence = verdict.evidence ?? [];
617
+ reportActivity(` escalated to the code: ${reason}`);
618
+ stage("the judge finds the failure the code's - escalated to the coding agent");
619
+ const tests = listFiles(conformanceDir).map((path) => ({ path, content: readFileSync(join(conformanceDir, path), "utf8") }));
620
+ const fixed = await changing(() => askFixer(escalatedFixPrompt({ run: recheck, reason: verdict.reason, evidence, tests })));
621
+ recordAttempt(rounds, recheck, `escalated to the code: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
622
+ if (fixed.files.length === 0) {
623
+ escalation = { reason: verdict.reason, evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000), changedNothing: true };
624
+ stage("the coding agent changed nothing for the failure escalated to it - stopping");
625
+ break;
626
+ }
627
+ idleFixes = 0;
628
+ escalated = { signature, reason: verdict.reason, evidence };
629
+ }
630
+ else {
631
+ reportActivity(` the code: ${reason}`);
632
+ stage("the code is wrong - the coding agent fixes it");
633
+ // One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
634
+ // it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
635
+ // whole.
636
+ const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
637
+ recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
638
+ if (idle(fixed.files))
639
+ break;
640
+ }
597
641
  // The fix has to build and pass its own unit tests before the suite runs again: a build
598
642
  // error is the coding agent's to fix, not a judge's to work out from the suite's output.
599
643
  for (let again = 0; again < FIX_UNIT_CHECKS; again += 1) {
@@ -613,7 +657,11 @@ export async function renderConformanceTests(options) {
613
657
  const fixed = await changing(() => askConformance(testsFixPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
614
658
  response = fixed.result;
615
659
  recordAttempt(rounds, recheck, `the tests wrong: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
616
- if (idle(fixed.files))
660
+ testsFix = { signature, reason: verdict.reason, files: fixed.files };
661
+ // A fix that changed nothing does not stop the run before the judge has weighed escalating the failure.
662
+ if (escalating === undefined)
663
+ idleFixes = fixed.files.length === 0 ? idleFixes + 1 : 0;
664
+ else if (idle(fixed.files))
617
665
  break;
618
666
  }
619
667
  continue;
@@ -646,7 +694,7 @@ export async function renderConformanceTests(options) {
646
694
  buildDir,
647
695
  conformanceDir,
648
696
  spec: specContext,
649
- functionalities: forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }],
697
+ functionalities: carriedOut,
650
698
  forChange,
651
699
  invocation,
652
700
  label,
@@ -772,6 +820,7 @@ export async function renderConformanceTests(options) {
772
820
  ...(specConflict !== undefined ? { specConflict } : {}),
773
821
  ...(stuck !== undefined ? { stuck } : {}),
774
822
  ...(environment !== undefined ? { environment } : {}),
823
+ ...(escalation !== undefined ? { escalation } : {}),
775
824
  ...(unlabelled !== undefined ? { unlabelled } : {}),
776
825
  ...(reviewWaived !== undefined ? { reviewWaived } : {}),
777
826
  ...(verified && green !== undefined ? { green } : {}),
@@ -905,9 +954,10 @@ export function failureLine(run) {
905
954
  }
906
955
  /**
907
956
  * 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.
957
+ * numbered with their ids, so a spec conflict can name them, and the acceptance tests the run carries
958
+ * out labelled as their tests are, so a verdict can quote them.
909
959
  */
910
- function specText(specifications, moduleName, frids, required, diff, modules) {
960
+ function specText(specifications, moduleName, frids, required, acceptance, diff, modules) {
911
961
  // Every functionality in the codebase, each with its module: the required modules' first, then this one's.
912
962
  const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
913
963
  ? [
@@ -915,10 +965,14 @@ function specText(specifications, moduleName, frids, required, diff, modules) {
915
965
  ]
916
966
  : items;
917
967
  // Each section in its tag, as every agent is shown it (see specPrimer).
918
- const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS, ACCEPTANCE_TESTS]
968
+ const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS]
919
969
  .map((key) => [key, specifications[key] ?? []])
920
970
  .filter(([, items]) => items.length > 0)
921
- .map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS || key === ACCEPTANCE_TESTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
971
+ .map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
972
+ // Not only the last functionality's: a spec change's run carries out the acceptance tests of every one it touches.
973
+ const tests = acceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `${acceptanceLabel(item.frid, index + 1)}: ${acceptanceText(test)}`));
974
+ if (tests.length > 0)
975
+ sections.push(section(ACCEPTANCE_TESTS, tests));
922
976
  if (diff !== undefined)
923
977
  sections.push(specChangeBlock(diff));
924
978
  return sections.join("\n\n");