@turri-dev/cli 0.1.3-next.6fab174 → 0.1.3-next.e5599c1
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/dist/extension.js +24 -3
- package/dist/regenerate/conformance-agents.js +162 -7
- package/dist/regenerate/conformance.js +58 -11
- package/dist/regenerate/panel-dock.js +31 -2
- package/dist/regenerate/side-panel.js +1 -1
- package/dist/regenerate/stop-report.js +13 -0
- package/docs/regenerating.md +22 -2
- package/docs/using.md +4 -2
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -31,7 +31,7 @@ import { whyFailed } from "./regenerate/diagnose-job.js";
|
|
|
31
31
|
import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
|
|
32
32
|
import { brandBlue, plainPaint } from "./regenerate/plain-palette.js";
|
|
33
33
|
import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
|
|
34
|
-
import { clickToFocus, dockedWidth, narrowChat } from "./regenerate/panel-dock.js";
|
|
34
|
+
import { clickToFocus, dockedWidth, narrowChat, shade, shadeChat } from "./regenerate/panel-dock.js";
|
|
35
35
|
import { overPictureRow, PICTURE_AT } from "./picture-row.js";
|
|
36
36
|
import { skillAboutLine, skillProgressLine } from "./skill-progress.js";
|
|
37
37
|
import { createSidePanel } from "./regenerate/side-panel.js";
|
|
@@ -1531,7 +1531,10 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1531
1531
|
else
|
|
1532
1532
|
toPanel();
|
|
1533
1533
|
});
|
|
1534
|
+
// The side without the keys in the shade: the chat while the panel has them.
|
|
1535
|
+
const unshade = shadeChat(tui, () => panelBeside() > 0 && entry.keys);
|
|
1534
1536
|
undock = () => {
|
|
1537
|
+
unshade();
|
|
1535
1538
|
unwatch();
|
|
1536
1539
|
widen();
|
|
1537
1540
|
};
|
|
@@ -1624,6 +1627,17 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1624
1627
|
}
|
|
1625
1628
|
if (!state)
|
|
1626
1629
|
return undefined;
|
|
1630
|
+
// Beside the chat, ⌥⌘← and ⌥⌘→ go to the side they point at, whichever has the keys - on a terminal that tells
|
|
1631
|
+
// turri of ⌘ (the Kitty keyboard protocol). Its release goes nowhere.
|
|
1632
|
+
if (panelBeside() > 0 && (matchesKey(data, SIDE_KEYS.chat) || matchesKey(data, SIDE_KEYS.panel))) {
|
|
1633
|
+
if (!isKeyRelease(data)) {
|
|
1634
|
+
if (matchesKey(data, SIDE_KEYS.chat))
|
|
1635
|
+
toChat();
|
|
1636
|
+
else
|
|
1637
|
+
toPanel();
|
|
1638
|
+
}
|
|
1639
|
+
return { consume: true };
|
|
1640
|
+
}
|
|
1627
1641
|
// Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
|
|
1628
1642
|
// them to the panel. A click on the panel gives it the focus, and the keys with it.
|
|
1629
1643
|
if (!entry.keys && entry.handle?.isFocused() !== true)
|
|
@@ -1655,8 +1669,13 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1655
1669
|
}, WATCH_REDRAW_MS);
|
|
1656
1670
|
timer.unref?.();
|
|
1657
1671
|
component = {
|
|
1658
|
-
// Beside the chat, a rule down its left edge between them.
|
|
1659
|
-
render: (width) =>
|
|
1672
|
+
// Beside the chat, a rule down its left edge between them - and the panel in the shade while the chat has the keys.
|
|
1673
|
+
render: (width) => {
|
|
1674
|
+
if (panelBeside() === 0)
|
|
1675
|
+
return view.render(width);
|
|
1676
|
+
const lines = view.render(width - 2).map((line) => `${theme.fg("borderMuted", "│")} ${line}`);
|
|
1677
|
+
return entry.keys ? lines : lines.map(shade);
|
|
1678
|
+
},
|
|
1660
1679
|
// Reached only if the listener above is gone: the same, so the panel never passes a key on.
|
|
1661
1680
|
handleInput: (data) => {
|
|
1662
1681
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
@@ -1797,6 +1816,8 @@ export const JOB_FINISHED_REACTION = [
|
|
|
1797
1816
|
" to do, and that \"resume\" picks it up once it is in.",
|
|
1798
1817
|
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
1799
1818
|
].join("\n");
|
|
1819
|
+
/** The keys that go to the chat and to the panel, side by side: ⌥⌘← and ⌥⌘→. */
|
|
1820
|
+
const SIDE_KEYS = { chat: "alt+super+left", panel: "alt+super+right" };
|
|
1800
1821
|
/** Opens the side panel, and goes back to it from the conversation. */
|
|
1801
1822
|
/** The modules in this project that have a history, by name. */
|
|
1802
1823
|
function modulesWithHistory(cwd) {
|
|
@@ -659,15 +659,28 @@ function words(text) {
|
|
|
659
659
|
* in the specification - or when it hides why the software failed. A test asserting what the
|
|
660
660
|
* specification leaves open is the test's mistake, however plausible the assertion.
|
|
661
661
|
*/
|
|
662
|
-
export function unfoundedCodeVerdict(verdict, specText) {
|
|
662
|
+
export function unfoundedCodeVerdict(verdict, specText, scriptText) {
|
|
663
663
|
if (verdict.causeHidden === true)
|
|
664
664
|
return undefined;
|
|
665
|
+
// What the conformance testing script needs of the build - a file, an entry point, a module - binds the code as the
|
|
666
|
+
// specification does: neither agent can change the script. Its own words, as they are in it.
|
|
667
|
+
const needs = verdict.scriptNeeds?.trim() ?? "";
|
|
668
|
+
if (needs !== "" && scriptText !== undefined) {
|
|
669
|
+
const flat = (text) => text.replace(/\s+/g, " ");
|
|
670
|
+
if (flat(needs).length >= 4 && flat(scriptText).includes(flat(needs)))
|
|
671
|
+
return undefined;
|
|
672
|
+
if (verdict.specSays === undefined) {
|
|
673
|
+
return (`Your verdict blames the code for what the conformance testing script needs, but "${needs}" is not in the script. ` +
|
|
674
|
+
"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.");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
665
677
|
const quote = verdict.specSays?.trim() ?? "";
|
|
666
678
|
if (quote === "") {
|
|
667
679
|
return ("Your verdict blames the code, but quotes no words of the specification it does not meet. Call submit_verdict " +
|
|
668
680
|
"again: for code, put the specification's own words the code does not meet in spec_says, copied exactly from " +
|
|
669
|
-
"<specification>; or
|
|
670
|
-
"
|
|
681
|
+
"<specification>; or, when the run fails on something the conformance testing script needs the build to have, " +
|
|
682
|
+
"its own words in script_needs; or set cause_hidden when the output does not show why the software failed. If " +
|
|
683
|
+
"the specification does not say what the failing test asserts, the test is what is wrong: culprit tests.");
|
|
671
684
|
}
|
|
672
685
|
const quoted = words(quote);
|
|
673
686
|
if (quoted.trim().split(" ").length < 4 || !words(specText).includes(quoted)) {
|
|
@@ -677,6 +690,31 @@ export function unfoundedCodeVerdict(verdict, specText) {
|
|
|
677
690
|
}
|
|
678
691
|
return undefined;
|
|
679
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Why a verdict's evidence cannot be taken as it is, or nothing when it can. Asked to weigh an escalation, the judge has
|
|
695
|
+
* to rest its verdict on what it found itself: each piece of evidence from a file it opened, the testing script or the
|
|
696
|
+
* failing run - and at least one not the run alone, which every judge before it read too. What the explorer's account
|
|
697
|
+
* says is not evidence: it was written before the run, and may be wrong.
|
|
698
|
+
*/
|
|
699
|
+
export function unfoundedEvidence(evidence, opened) {
|
|
700
|
+
const given = (evidence ?? []).filter((item) => item.source.trim() !== "" && item.shows.trim() !== "");
|
|
701
|
+
if (given.length === 0) {
|
|
702
|
+
return ("Your verdict gives no evidence. Call submit_verdict again with `evidence`: what you found yourself - in a file you " +
|
|
703
|
+
"opened, the testing script, or the failing run - each with where. What <how to invoke it> says is not evidence: it " +
|
|
704
|
+
"may be wrong, so check it against the testing script and the files.");
|
|
705
|
+
}
|
|
706
|
+
const inPrompt = (source) => /^(the )?(conformance )?testing script$|^(the )?failing run$/i.test(source.trim());
|
|
707
|
+
const unread = given.filter((item) => !inPrompt(item.source) && !opened(item.source.trim()));
|
|
708
|
+
if (unread.length > 0) {
|
|
709
|
+
return (`Your evidence cites ${unread.map((item) => item.source.trim()).join(", ")}, which you did not open. Open each file you cite ` +
|
|
710
|
+
"and call submit_verdict again - evidence is what you found yourself, never what <how to invoke it> says.");
|
|
711
|
+
}
|
|
712
|
+
if (given.every((item) => /failing run$/i.test(item.source.trim()))) {
|
|
713
|
+
return ("Your evidence is only the failing run, which the judges before you read too. Check what it points at - the testing " +
|
|
714
|
+
"script, the code, the tests - and call submit_verdict again with what you found there.");
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
680
718
|
export async function judgeFailure(options) {
|
|
681
719
|
let verdict;
|
|
682
720
|
const submit = defineTool({
|
|
@@ -692,6 +730,13 @@ export async function judgeFailure(options) {
|
|
|
692
730
|
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
731
|
})),
|
|
694
732
|
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" })),
|
|
733
|
+
script_needs: Type.Optional(Type.String({
|
|
734
|
+
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",
|
|
735
|
+
})),
|
|
736
|
+
evidence: Type.Optional(Type.Array(Type.Object({
|
|
737
|
+
source: Type.String({ description: "Where: the path of a file you opened, `testing script`, or `failing run`" }),
|
|
738
|
+
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`" }),
|
|
739
|
+
}), { description: "What you found yourself, each with where - never what <how to invoke it> claims unchecked" })),
|
|
695
740
|
functionalities: Type.Optional(Type.Array(Type.Object({
|
|
696
741
|
module: Type.String({ description: "The module the functionality is in" }),
|
|
697
742
|
id: Type.String({ description: "Its id in that module" }),
|
|
@@ -704,6 +749,8 @@ export async function judgeFailure(options) {
|
|
|
704
749
|
reason: given.reason,
|
|
705
750
|
...(given.spec_says?.trim() ? { specSays: given.spec_says.trim() } : {}),
|
|
706
751
|
...(given.cause_hidden === true ? { causeHidden: true } : {}),
|
|
752
|
+
...(given.script_needs?.trim() ? { scriptNeeds: given.script_needs.trim() } : {}),
|
|
753
|
+
...(given.evidence !== undefined && given.evidence.length > 0 ? { evidence: given.evidence } : {}),
|
|
707
754
|
...(given.functionalities ? { functionalities: conflictRefs(given.functionalities) } : {}),
|
|
708
755
|
};
|
|
709
756
|
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
@@ -712,10 +759,27 @@ export async function judgeFailure(options) {
|
|
|
712
759
|
// It reads both, and changes neither: the file tools and the web - to look up what a service it calls answers -
|
|
713
760
|
// and no shell. It judges from the code, the tests, the spec and the failing run's output, which it is given.
|
|
714
761
|
const fence = confinedTools({ workdir: options.conformanceDir, read: [options.buildDir, options.conformanceDir], write: [], noBash: true });
|
|
762
|
+
// What it opens - read, listed, searched - is what its evidence can cite.
|
|
763
|
+
const openedPaths = new Set();
|
|
764
|
+
const tools = fence.tools.map((tool) => ({
|
|
765
|
+
...tool,
|
|
766
|
+
execute: (async (...args) => {
|
|
767
|
+
const path = args[1]?.path;
|
|
768
|
+
if (typeof path === "string" && path !== "")
|
|
769
|
+
openedPaths.add(resolve(options.conformanceDir, path));
|
|
770
|
+
return tool.execute(...args);
|
|
771
|
+
}),
|
|
772
|
+
}));
|
|
773
|
+
const opened = (cited) => {
|
|
774
|
+
// 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.
|
|
775
|
+
const candidates = [resolve(options.conformanceDir, cited), resolve(options.buildDir, cited)];
|
|
776
|
+
return candidates.some((path) => [...openedPaths].some((seen) => path === seen || path.startsWith(`${seen}/`)));
|
|
777
|
+
};
|
|
778
|
+
const scriptText = options.testingScripts?.map((script) => script.content).join("\n");
|
|
715
779
|
const before = { code: digestFiles(options.buildDir), tests: digestFiles(options.conformanceDir) };
|
|
716
780
|
const session = await createExecutor({
|
|
717
781
|
cwd: options.conformanceDir,
|
|
718
|
-
customTools: [submit, ...createWebTools(), ...
|
|
782
|
+
customTools: [submit, ...createWebTools(), ...tools],
|
|
719
783
|
tools: [...fence.names, ...WEB_TOOLS, "submit_verdict"],
|
|
720
784
|
model: options.model,
|
|
721
785
|
label: `${options.label} judge`, role: "judge",
|
|
@@ -730,8 +794,15 @@ export async function judgeFailure(options) {
|
|
|
730
794
|
await ask(session, "You have not called submit_verdict. Call it now: code, tests, spec or environment, and why.", options.onDelta);
|
|
731
795
|
continue;
|
|
732
796
|
}
|
|
797
|
+
// Asked to weigh an escalation, a verdict of code or tests rests on what it found itself.
|
|
798
|
+
const unproven = options.escalation !== undefined && (verdict.culprit === "code" || verdict.culprit === "tests") ? unfoundedEvidence(verdict.evidence, opened) : undefined;
|
|
799
|
+
if (unproven !== undefined) {
|
|
800
|
+
verdict = undefined;
|
|
801
|
+
await ask(session, unproven, options.onDelta);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
733
804
|
if (verdict.culprit === "code") {
|
|
734
|
-
const unfounded = unfoundedCodeVerdict(verdict, options.spec.text);
|
|
805
|
+
const unfounded = unfoundedCodeVerdict(verdict, options.spec.text, scriptText);
|
|
735
806
|
if (unfounded === undefined)
|
|
736
807
|
break;
|
|
737
808
|
verdict = undefined;
|
|
@@ -759,18 +830,26 @@ export async function judgeFailure(options) {
|
|
|
759
830
|
}
|
|
760
831
|
if (verdict?.culprit === "spec" && specConflictProblem(verdict.functionalities, options.scope) !== undefined)
|
|
761
832
|
verdict = undefined;
|
|
762
|
-
|
|
833
|
+
// An escalation weighed without evidence escalates nothing: the tests stay the conformance agent's, as before it.
|
|
834
|
+
if (options.escalation !== undefined && verdict?.culprit === "code" && unfoundedEvidence(verdict.evidence, opened) !== undefined) {
|
|
835
|
+
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.` };
|
|
836
|
+
}
|
|
837
|
+
if (verdict?.culprit === "code" && unfoundedCodeVerdict(verdict, options.spec.text, scriptText) !== undefined) {
|
|
763
838
|
// Blaming the code takes the specification's own words; without them, the test asserts what the spec does not say.
|
|
764
839
|
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
840
|
}
|
|
766
841
|
if (verdict?.culprit === "code" && verdict.specSays !== undefined) {
|
|
767
842
|
verdict = { ...verdict, reason: `${verdict.reason}\n\nThe specification says: "${verdict.specSays}"` };
|
|
768
843
|
}
|
|
844
|
+
if (verdict?.culprit === "code" && verdict.scriptNeeds !== undefined) {
|
|
845
|
+
verdict = { ...verdict, reason: `${verdict.reason}\n\nThe conformance testing script needs: "${verdict.scriptNeeds}"` };
|
|
846
|
+
}
|
|
769
847
|
// No verdict is no evidence the code is wrong: the tests are the newer work, and looked at first.
|
|
770
848
|
return verdict ?? { culprit: "tests", reason: "The judge gave no verdict that could be used; check the failing tests against the specification first." };
|
|
771
849
|
}
|
|
772
850
|
export function judgePrompt(options) {
|
|
773
851
|
const tested = options.scope.tested;
|
|
852
|
+
const scripts = options.testingScripts ?? [];
|
|
774
853
|
return [
|
|
775
854
|
specPrimer({ testReqs: true }),
|
|
776
855
|
`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 +859,9 @@ export function judgePrompt(options) {
|
|
|
780
859
|
"The specification is the judge, never either of them:",
|
|
781
860
|
"- tests: a test asserts something the specification does not say, misreads it, or reaches the software the",
|
|
782
861
|
" 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.",
|
|
862
|
+
"- code: the code does not do what the specification says, and the test checks exactly that. Or the run fails",
|
|
863
|
+
" on something the conformance testing script needs the code to provide - a build output, an entry point, a",
|
|
864
|
+
" module - and the code does not provide it: quote the script's own words for it in script_needs.",
|
|
784
865
|
" Only what the specification says counts: anything it leaves open - a name, coordinates, a version, a path,",
|
|
785
866
|
" a package, a port, a file layout - is what the code already has. The tests follow it, and it is never",
|
|
786
867
|
" code - changing the code to fit a test's guess moves what every other test points at.",
|
|
@@ -810,6 +891,19 @@ export function judgePrompt(options) {
|
|
|
810
891
|
"verdict of code without them is not taken. What the specification leaves open is never the code's fault: a test",
|
|
811
892
|
"that asserts it - a status code, a message, a format the specification does not name - is what is wrong.",
|
|
812
893
|
"",
|
|
894
|
+
...(scripts.length > 0
|
|
895
|
+
? [
|
|
896
|
+
"The conformance testing script below is how every run is made, and neither agent can change it: it is never",
|
|
897
|
+
"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",
|
|
898
|
+
"checks for, a command it builds with - the code has to provide. A verdict that the script is wrong is neither",
|
|
899
|
+
"code nor tests: say which of the two has to change to get past it.",
|
|
900
|
+
"",
|
|
901
|
+
]
|
|
902
|
+
: []),
|
|
903
|
+
"<how to invoke it> is the explorer's account, written before the run, and it can be wrong - a folder, a path, a",
|
|
904
|
+
"command. Never rest a verdict on it: check each claim you use against the testing script and the files, and give",
|
|
905
|
+
"in `evidence` what you found yourself, each with where.",
|
|
906
|
+
"",
|
|
813
907
|
`You have ${JUDGE_CALLS} tool calls to decide in: read what the failure points at, not the whole codebase.`,
|
|
814
908
|
"",
|
|
815
909
|
"When the code and the tests could both be what is wrong, choose tests. When both are wrong, say which to fix",
|
|
@@ -832,11 +926,35 @@ export function judgePrompt(options) {
|
|
|
832
926
|
"",
|
|
833
927
|
`<how to invoke it>\n${options.invocation.trim()}\n</how to invoke it>`,
|
|
834
928
|
"",
|
|
929
|
+
...scripts.flatMap((script) => [`<conformance testing script path="${script.path}" run-as="${script.runs}">`, script.content.trimEnd(), "</conformance testing script>", ""]),
|
|
835
930
|
`<failing run exit="${options.run.exitCode}" command="${options.run.command}">`,
|
|
836
931
|
options.run.output.trimEnd(),
|
|
837
932
|
"</failing run>",
|
|
933
|
+
...(options.escalation !== undefined ? ["", ...escalationBlock(options.escalation)] : []),
|
|
838
934
|
].join("\n");
|
|
839
935
|
}
|
|
936
|
+
/** For the judge: the tests were fixed for this failure, and it did not go away - is it the code's? */
|
|
937
|
+
function escalationBlock(escalation) {
|
|
938
|
+
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` : ""}`;
|
|
939
|
+
return [
|
|
940
|
+
"<escalation>",
|
|
941
|
+
"This failure was judged the tests' before, and the conformance agent - who can change only the tests - was sent to",
|
|
942
|
+
`fix it. It ${changed}, and the run fails the same way. What it was told:`,
|
|
943
|
+
"",
|
|
944
|
+
escalation.reason.trim(),
|
|
945
|
+
"",
|
|
946
|
+
"Decide again, now whether the failure belongs to the code rather than the tests. It is the code's when the run",
|
|
947
|
+
"fails on something the code is meant to produce - a build output, an entry point, a module the testing script needs",
|
|
948
|
+
"- or when the tests ran and what the software does is not what the specification says. It stays the tests' when a",
|
|
949
|
+
"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.",
|
|
950
|
+
"",
|
|
951
|
+
"Gather the evidence yourself: open the files the failure points at - the code's build configuration and what it",
|
|
952
|
+
"emits, the tests - and read the testing script. Give it in `evidence`: a verdict of code or tests without evidence",
|
|
953
|
+
"you found is not taken. The coding agent is handed your reason and evidence: say what the code has to provide, and",
|
|
954
|
+
"where it now falls short (e.g. `the script runs dist/cli.js; the build emits dist/src/cli.js`).",
|
|
955
|
+
"</escalation>",
|
|
956
|
+
];
|
|
957
|
+
}
|
|
840
958
|
/**
|
|
841
959
|
* The coding agent a conformance run sends the code to, when the judge says the code is wrong: it
|
|
842
960
|
* works in the code folder, may read the tests but never changes them, and keeps the unit
|
|
@@ -917,6 +1035,43 @@ export function codeFixPrompt(options) {
|
|
|
917
1035
|
"Fix the code, run the conformance tests and the unit tests, and finish.",
|
|
918
1036
|
].join("\n");
|
|
919
1037
|
}
|
|
1038
|
+
/** How much of the conformance tests an escalation hands the coding agent, whole: the rest it reads itself. */
|
|
1039
|
+
const ESCALATED_TESTS_CHARS = 24_000;
|
|
1040
|
+
/**
|
|
1041
|
+
* An escalated failure for the code fixer: the tests were fixed for it and it did not go away, and the judge found it
|
|
1042
|
+
* the code's. It is handed the conformance testing script's error, the tests as they stand and the judge's evidence.
|
|
1043
|
+
*/
|
|
1044
|
+
export function escalatedFixPrompt(options) {
|
|
1045
|
+
let room = ESCALATED_TESTS_CHARS;
|
|
1046
|
+
const shown = options.tests.flatMap((test) => {
|
|
1047
|
+
if (room <= 0)
|
|
1048
|
+
return [`<conformance test path="${test.path}" shown="no - read it yourself" />`];
|
|
1049
|
+
const content = test.content.length > room ? `${test.content.slice(0, room)}\n... (cut short - read the rest yourself)` : test.content;
|
|
1050
|
+
room -= content.length;
|
|
1051
|
+
return [`<conformance test path="${test.path}">`, content.trimEnd(), "</conformance test>"];
|
|
1052
|
+
});
|
|
1053
|
+
return [
|
|
1054
|
+
`The conformance tests failed (exit ${options.run.exitCode}). The conformance agent was sent to fix the tests for it, and`,
|
|
1055
|
+
"the run failed the same way. A judge then looked again, gathered the evidence below itself, and found that the",
|
|
1056
|
+
"failure belongs to the code:",
|
|
1057
|
+
"",
|
|
1058
|
+
`<why the code>\n${options.reason.trim()}\n</why the code>`,
|
|
1059
|
+
"",
|
|
1060
|
+
"<evidence>",
|
|
1061
|
+
...options.evidence.map((item) => `- ${item.source.trim()}: ${item.shows.trim()}`),
|
|
1062
|
+
"</evidence>",
|
|
1063
|
+
"",
|
|
1064
|
+
`<failing run command="${options.run.command}">`,
|
|
1065
|
+
options.run.output.trimEnd(),
|
|
1066
|
+
"</failing run>",
|
|
1067
|
+
"",
|
|
1068
|
+
"The conformance tests as they stand - not yours to change:",
|
|
1069
|
+
...shown,
|
|
1070
|
+
"",
|
|
1071
|
+
"Fix the code, run the conformance tests and the unit tests, and finish. This is the last attempt at this failure:",
|
|
1072
|
+
"if the run still fails the same way after your fix, the work stops.",
|
|
1073
|
+
].join("\n");
|
|
1074
|
+
}
|
|
920
1075
|
/** 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
1076
|
export function environmentCheckPrompt(options) {
|
|
922
1077
|
return [
|
|
@@ -16,7 +16,7 @@ import { createMyDiffTool } from "./self-check.js";
|
|
|
16
16
|
import { createServiceTools } from "./services.js";
|
|
17
17
|
import { missingToolchainError, 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";
|
|
@@ -194,6 +194,11 @@ export async function renderConformanceTests(options) {
|
|
|
194
194
|
let environmentRaises = 0;
|
|
195
195
|
/** 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
196
|
let idleFixes = 0;
|
|
197
|
+
/** The last fix was the tests', for a failure judged theirs: should the run fail the same way again, the judge weighs escalating it. */
|
|
198
|
+
let testsFix;
|
|
199
|
+
/** A failure escalated to the code: should the run still fail the same way after the code's fix, the run stops. */
|
|
200
|
+
let escalated;
|
|
201
|
+
let escalation;
|
|
197
202
|
/**
|
|
198
203
|
* One attempt at the failing run, written into the notes' pitfalls - what the next judge reads
|
|
199
204
|
* first - as cause and effect: what failed, how it was judged, and what was changed; the next
|
|
@@ -414,6 +419,15 @@ export async function renderConformanceTests(options) {
|
|
|
414
419
|
abandonWork(start, moduleName, "the conformance testing script could not run");
|
|
415
420
|
throw lacking;
|
|
416
421
|
}
|
|
422
|
+
// A failure escalated to the code, still the same after the code's fix: neither agent could fix it, and the run stops
|
|
423
|
+
// with why the judge found it the code's. Anything else it fails on is a new failure, judged as any other.
|
|
424
|
+
if (escalated !== undefined && recheck.exitCode !== 0 && failureSignature(recheck) === escalated.signature) {
|
|
425
|
+
escalation = { reason: escalated.reason, evidence: escalated.evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000) };
|
|
426
|
+
stage("the failure escalated to the code is still there after the coding agent's fix - stopping");
|
|
427
|
+
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`);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
escalated = undefined;
|
|
417
431
|
if (recheck.exitCode !== 0) {
|
|
418
432
|
// Nothing changing: the same failure, taken the same way, round after round. Once, the explorer
|
|
419
433
|
// is asked what the tests lack - most often how they reach the software; after that, it stops.
|
|
@@ -504,6 +518,12 @@ export async function renderConformanceTests(options) {
|
|
|
504
518
|
recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)}, and ${IDLE_FIXES} fixes in a row changed nothing`, `${label} conformance`);
|
|
505
519
|
return true;
|
|
506
520
|
};
|
|
521
|
+
// The tests were fixed for this very failure, and it is still here: the judge weighs whether it is the code's.
|
|
522
|
+
const signature = failureSignature(recheck);
|
|
523
|
+
const escalating = testsFix !== undefined && testsFix.signature === signature ? { reason: testsFix.reason, changed: testsFix.files } : undefined;
|
|
524
|
+
testsFix = undefined;
|
|
525
|
+
if (escalating !== undefined)
|
|
526
|
+
stage("the tests' fix did not make the failure go away - the judge weighs whether it is the code's");
|
|
507
527
|
const verdict = await judgeFailure({
|
|
508
528
|
buildDir,
|
|
509
529
|
conformanceDir,
|
|
@@ -513,6 +533,9 @@ export async function renderConformanceTests(options) {
|
|
|
513
533
|
scope,
|
|
514
534
|
// The tests' notes - every attempt so far - and the code's: it weighs the one against the other.
|
|
515
535
|
notes: [notesToRead(conformanceDir), notesToRead(buildDir)].filter((part) => part !== "").join("\n\n"),
|
|
536
|
+
// The script itself, so the judge checks the explorer's account against it rather than repeating it.
|
|
537
|
+
testingScripts,
|
|
538
|
+
...(escalating !== undefined ? { escalation: escalating } : {}),
|
|
516
539
|
label,
|
|
517
540
|
model: judgeModel,
|
|
518
541
|
onDelta: options.onDelta,
|
|
@@ -585,15 +608,34 @@ export async function renderConformanceTests(options) {
|
|
|
585
608
|
recordAttempt(rounds, recheck, `a spec conflict between functionalities ${key}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
586
609
|
}
|
|
587
610
|
else if (verdict.culprit === "code") {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
611
|
+
if (escalating !== undefined) {
|
|
612
|
+
// Escalated: the coding agent is handed the script's error, the tests as they stand and the judge's evidence.
|
|
613
|
+
// Its fix is the last attempt at this failure - should the run fail the same way after it, the run stops.
|
|
614
|
+
const evidence = verdict.evidence ?? [];
|
|
615
|
+
reportActivity(` escalated to the code: ${reason}`);
|
|
616
|
+
stage("the judge finds the failure the code's - escalated to the coding agent");
|
|
617
|
+
const tests = listFiles(conformanceDir).map((path) => ({ path, content: readFileSync(join(conformanceDir, path), "utf8") }));
|
|
618
|
+
const fixed = await changing(() => askFixer(escalatedFixPrompt({ run: recheck, reason: verdict.reason, evidence, tests })));
|
|
619
|
+
recordAttempt(rounds, recheck, `escalated to the code: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
620
|
+
if (fixed.files.length === 0) {
|
|
621
|
+
escalation = { reason: verdict.reason, evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000), changedNothing: true };
|
|
622
|
+
stage("the coding agent changed nothing for the failure escalated to it - stopping");
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
idleFixes = 0;
|
|
626
|
+
escalated = { signature, reason: verdict.reason, evidence };
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
reportActivity(` the code: ${reason}`);
|
|
630
|
+
stage("the code is wrong - the coding agent fixes it");
|
|
631
|
+
// One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
|
|
632
|
+
// it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
|
|
633
|
+
// whole.
|
|
634
|
+
const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
|
|
635
|
+
recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
636
|
+
if (idle(fixed.files))
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
597
639
|
// The fix has to build and pass its own unit tests before the suite runs again: a build
|
|
598
640
|
// error is the coding agent's to fix, not a judge's to work out from the suite's output.
|
|
599
641
|
for (let again = 0; again < FIX_UNIT_CHECKS; again += 1) {
|
|
@@ -613,7 +655,11 @@ export async function renderConformanceTests(options) {
|
|
|
613
655
|
const fixed = await changing(() => askConformance(testsFixPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
|
|
614
656
|
response = fixed.result;
|
|
615
657
|
recordAttempt(rounds, recheck, `the tests wrong: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
616
|
-
|
|
658
|
+
testsFix = { signature, reason: verdict.reason, files: fixed.files };
|
|
659
|
+
// A fix that changed nothing does not stop the run before the judge has weighed escalating the failure.
|
|
660
|
+
if (escalating === undefined)
|
|
661
|
+
idleFixes = fixed.files.length === 0 ? idleFixes + 1 : 0;
|
|
662
|
+
else if (idle(fixed.files))
|
|
617
663
|
break;
|
|
618
664
|
}
|
|
619
665
|
continue;
|
|
@@ -772,6 +818,7 @@ export async function renderConformanceTests(options) {
|
|
|
772
818
|
...(specConflict !== undefined ? { specConflict } : {}),
|
|
773
819
|
...(stuck !== undefined ? { stuck } : {}),
|
|
774
820
|
...(environment !== undefined ? { environment } : {}),
|
|
821
|
+
...(escalation !== undefined ? { escalation } : {}),
|
|
775
822
|
...(unlabelled !== undefined ? { unlabelled } : {}),
|
|
776
823
|
...(reviewWaived !== undefined ? { reviewWaived } : {}),
|
|
777
824
|
...(verified && green !== undefined ? { green } : {}),
|
|
@@ -9,9 +9,9 @@ import { HStack } from "@earendil-works/pi-tui";
|
|
|
9
9
|
*/
|
|
10
10
|
/** Narrower than this, the chat and the panel have no room side by side: the panel takes the whole screen. */
|
|
11
11
|
const SHARED_FROM = 100;
|
|
12
|
-
/** The columns the panel takes beside the chat - 0 when the terminal is too narrow to share. */
|
|
12
|
+
/** The columns the panel takes beside the chat: half the screen, the rule between them in the middle - 0 when the terminal is too narrow to share. */
|
|
13
13
|
export function dockedWidth(columns) {
|
|
14
|
-
return columns < SHARED_FROM ? 0 : Math.
|
|
14
|
+
return columns < SHARED_FROM ? 0 : Math.floor(columns / 2);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* The chat narrowed by `panelWidth()` columns, read at each frame - 0 gives it the whole width. Returns what gives it the
|
|
@@ -55,3 +55,32 @@ export function clickToFocus(tui, chatWidth, side) {
|
|
|
55
55
|
screen.handleMouseEvent = handle;
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A line drawn faint - the side that does not have the keys, in the shade. Faint (SGR 2) holds through a line's colours;
|
|
60
|
+
* only a reset - SGR 0, or 22 - ends it, so it is set again after each one. A line holding an image is left as it is.
|
|
61
|
+
*/
|
|
62
|
+
export function shade(line) {
|
|
63
|
+
// Kitty's image escape, or iTerm2's.
|
|
64
|
+
if (line.includes("\x1b_G") || line.includes("\x1b]1337;File="))
|
|
65
|
+
return line;
|
|
66
|
+
const again = line.replace(/\x1b\[([\d;]*)m/g, (sgr, params) => (params.split(";").some((param) => param === "" || param === "0" || param === "22") ? `${sgr}${FAINT}` : sgr));
|
|
67
|
+
return `${FAINT}${again}${NOT_FAINT}`;
|
|
68
|
+
}
|
|
69
|
+
const FAINT = "\x1b[2m";
|
|
70
|
+
const NOT_FAINT = "\x1b[22m";
|
|
71
|
+
/**
|
|
72
|
+
* The chat in the shade while `shaded()`: each of its rows drawn faint as the screen lays the overlays over it - the
|
|
73
|
+
* docked panel the last of them, which covers its own columns. Returns what takes the shade off.
|
|
74
|
+
*/
|
|
75
|
+
export function shadeChat(tui, shaded) {
|
|
76
|
+
const screen = tui;
|
|
77
|
+
const composite = screen.compositeLineAt;
|
|
78
|
+
if (typeof composite !== "function")
|
|
79
|
+
return () => { };
|
|
80
|
+
screen.compositeLineAt = function (base, overlay, col, width, total) {
|
|
81
|
+
return composite.call(this, shaded() ? shade(base) : base, overlay, col, width, total);
|
|
82
|
+
};
|
|
83
|
+
return () => {
|
|
84
|
+
screen.compositeLineAt = composite;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -16,7 +16,7 @@ export function createSidePanel(options) {
|
|
|
16
16
|
const backKeys = () => {
|
|
17
17
|
if (options.dock === undefined)
|
|
18
18
|
return [`esc or ${options.key} back to the chat`];
|
|
19
|
-
return options.dock.docked() ? ["s full screen", "esc to the chat", `${options.key} close`] : ["s beside the chat", `esc or ${options.key} back to the chat`];
|
|
19
|
+
return options.dock.docked() ? ["s full screen", "esc or ⌥⌘← to the chat", `${options.key} close`] : ["s beside the chat", `esc or ${options.key} back to the chat`];
|
|
20
20
|
};
|
|
21
21
|
const waitingNote = () =>
|
|
22
22
|
// A question asked while it showed waits in the chat, unanswered: the person is told it is there.
|
|
@@ -89,6 +89,19 @@ export function describeStop(facts) {
|
|
|
89
89
|
`though they were sent back for it again and again. Still missing:\n${facts.conformance.unlabelled.map((line) => ` - ${line}`).join("\n")}`;
|
|
90
90
|
todo = ["If the test reqs leave open where the tests go or how they are built, say so exactly - the folder, the files, the build."];
|
|
91
91
|
}
|
|
92
|
+
else if (facts.at === "conformance" && facts.conformance?.escalation !== undefined) {
|
|
93
|
+
const { error, reason, evidence, changedNothing } = facts.conformance.escalation;
|
|
94
|
+
why =
|
|
95
|
+
`its conformance tests kept failing with ${error}. The conformance agent's fix of the tests did not make it go away, so ` +
|
|
96
|
+
`a judge looked again and found it the code's; the coding agent ${changedNothing === true ? "changed nothing for it" : "fixed the code, and the run still failed the same way"}. ` +
|
|
97
|
+
`Why the judge found it the code's:\n${reason.trim().split("\n").map((line) => ` ${line}`).join("\n")}` +
|
|
98
|
+
(evidence.length > 0 ? `\n On the evidence:\n${evidence.map((item) => ` - ${item.source.trim()}: ${item.shows.trim().replace(/\s+/g, " ")}`).join("\n")}` : "");
|
|
99
|
+
todo = [
|
|
100
|
+
"If the code has to produce something the spec leaves open - a build output, an entry point, a module the conformance testing " +
|
|
101
|
+
"script runs or checks for - say exactly what in the implementation reqs: its path and how it is built.",
|
|
102
|
+
"If the conformance testing script expects something the spec does not ask for, change the script or the test reqs so the two agree.",
|
|
103
|
+
];
|
|
104
|
+
}
|
|
92
105
|
else if (facts.at === "conformance" && facts.conformance?.stuck !== undefined) {
|
|
93
106
|
const { error, times, rounds, changedNothing } = facts.conformance.stuck;
|
|
94
107
|
why =
|
package/docs/regenerating.md
CHANGED
|
@@ -906,10 +906,16 @@ agent is told that a failure of the software itself - it does not build or start
|
|
|
906
906
|
spec says it must not - is not the tests' to work around. When it fails, a judge - an agent that reads
|
|
907
907
|
the code and the tests, changes neither, and holds both to the spec - decides which is wrong. It has the
|
|
908
908
|
file tools and the web (`web_search`, `web_fetch`), to look up what a service the code calls answers - no shell: it
|
|
909
|
-
judges from the code, the tests, the spec and the failing run's output,
|
|
909
|
+
judges from the code, the tests, the spec, the conformance testing script word for word and the failing run's output,
|
|
910
|
+
and its file tools write nothing. The explorer's account of how the code is invoked is in its prompt too, marked as
|
|
911
|
+
written before the run and possibly wrong: the judge checks what it uses of it against the script and the files, and
|
|
912
|
+
gives in `evidence` what it found itself. The script is fixed - neither agent can change it - so it is never the
|
|
913
|
+
culprit: what it does with the tests, the tests fit; what it needs of the build, the code provides.
|
|
910
914
|
|
|
911
915
|
- **the code:** the coding agent - in the code's folder, reading the tests but never changing them - fixes it,
|
|
912
|
-
and keeps its unit tests passing
|
|
916
|
+
and keeps its unit tests passing. The judge quotes the spec's own words the code does not meet, or the testing
|
|
917
|
+
script's for what it needs of the build (`script_needs`: a build output, an entry point, a module) - each checked
|
|
918
|
+
by the harness against the text it quotes;
|
|
913
919
|
- **the tests:** the conformance agent corrects them to what the spec says;
|
|
914
920
|
- **the spec:** two functional specs cannot both hold, whatever the code does;
|
|
915
921
|
- **the environment:** neither the code nor the tests can fix it - only the person can, in what the run is given:
|
|
@@ -953,6 +959,20 @@ response, a UUID or a correlation id. Nothing changes either when the agent sent
|
|
|
953
959
|
changes no file: two such fixes in a row, and the run stops. The conformance agent has the testing script in its prompt,
|
|
954
960
|
and is told to fit the tests' own build to it.
|
|
955
961
|
|
|
962
|
+
**Escalation.** A failure judged the tests' goes to the conformance agent first: it owns the tests. When the run then
|
|
963
|
+
fails the same way - the fix changed nothing, or nothing that mattered - the judge is asked again, now whether the
|
|
964
|
+
failure belongs to the code rather than the tests: the run failing on something the code is meant to produce (a build
|
|
965
|
+
output, an entry point, a module the testing script needs), or the tests running and the behaviour itself being
|
|
966
|
+
wrong. Its verdict, either way, has to rest on evidence it gathered: each piece from a file it opened (the harness
|
|
967
|
+
keeps track of what it read, listed and searched), the testing script or the failing run - and not the run alone.
|
|
968
|
+
A verdict without it is sent back; a verdict of code still without it is taken as the tests'. Judged the tests'
|
|
969
|
+
again, the failure stays with the conformance agent, and a fix that changes nothing counts towards stopping as
|
|
970
|
+
before. Judged the code's, it is escalated to the coding agent, handed the script's error, the conformance tests as
|
|
971
|
+
they stand and the judge's reason and evidence (`the script runs dist/cli.js; the build emits dist/src/cli.js`). Its
|
|
972
|
+
fix has to build and pass the unit tests, then the suite runs again. Should the run still fail the same way - or the
|
|
973
|
+
fix change nothing - the run stops there, with the judge's reasoning and evidence in what the person is told. Each of
|
|
974
|
+
these runs is one of the run's 20 attempts, so a failure neither agent can fix stops in three of them.
|
|
975
|
+
|
|
956
976
|
Every attempt is written into the working notes' pitfalls by the harness as cause and effect - what
|
|
957
977
|
the run failed with, how it was judged, and which files the agent sent to fix it changed; the next
|
|
958
978
|
attempt's line says what came of it - and each judge reads them first, so it does not send a
|
package/docs/using.md
CHANGED
|
@@ -131,7 +131,7 @@ come waits for a free slot, and says so.
|
|
|
131
131
|
keyboard protocol sends as a line feed, the same as `shift+enter`, and which a dialog takes for
|
|
132
132
|
`enter`; both stay pi's new-line keys. `ctrl+h` is the byte old terminals sent for backspace; a
|
|
133
133
|
terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time -
|
|
134
|
-
or, `s`, sits beside the chat: the chat on the left, the panel in the right
|
|
134
|
+
or, `s`, sits beside the chat: the chat on the left, the panel in the right half of the screen, and `s` again
|
|
135
135
|
over the whole screen. It opens as it was last left, for the conversation; a terminal under 100 columns has
|
|
136
136
|
no room for both and shows the panel over the whole screen.
|
|
137
137
|
It opens on **regen**: the spec itself, in full, with how much of it the code has been regenerated from drawn
|
|
@@ -218,7 +218,9 @@ come waits for a free slot, and says so.
|
|
|
218
218
|
said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
219
219
|
|
|
220
220
|
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, a click decides which side has
|
|
221
|
-
the keys - the chat or the panel, the one clicked
|
|
221
|
+
the keys - the chat or the panel, the one clicked, or the one `⌥⌘←` / `⌥⌘→` points at (on a
|
|
222
|
+
terminal that passes ⌘ on, over the Kitty keyboard protocol - Ghostty, kitty, WezTerm, iTerm2; not Apple's Terminal);
|
|
223
|
+
the other is drawn faint - and `esc` gives the chat the keys too, the panel staying and
|
|
222
224
|
following the pulses; `ctrl+h` gives the panel the keys again, and from the panel closes it. (pi's regular screen,
|
|
223
225
|
`--tui-mode regular`, reads no mouse: there it is the keys alone.)
|
|
224
226
|
While the panel has the keys, every key is the panel's,
|