@turri-dev/cli 0.1.18-next.85984f7 → 0.1.19-next.ee6bc61

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.
Files changed (42) hide show
  1. package/dist/chat-terminal.js +4 -0
  2. package/dist/chat-waiting.d.ts +64 -0
  3. package/dist/chat-waiting.js +86 -0
  4. package/dist/chats.d.ts +2 -0
  5. package/dist/chats.js +1 -0
  6. package/dist/extension.js +36 -2
  7. package/dist/genome.d.ts +12 -0
  8. package/dist/genome.js +17 -0
  9. package/dist/new-project.js +1 -1
  10. package/dist/pi-launch.js +7 -4
  11. package/dist/regenerate/conformance-agents.d.ts +6 -2
  12. package/dist/regenerate/conformance-agents.js +40 -14
  13. package/dist/regenerate/conformance.d.ts +0 -8
  14. package/dist/regenerate/conformance.js +17 -14
  15. package/dist/regenerate/debugger.d.ts +5 -0
  16. package/dist/regenerate/debugger.js +21 -2
  17. package/dist/regenerate/environment-error.d.ts +25 -0
  18. package/dist/regenerate/environment-error.js +46 -0
  19. package/dist/regenerate/job-panel.d.ts +2 -2
  20. package/dist/regenerate/job-panel.js +12 -11
  21. package/dist/regenerate/jobs-tree.d.ts +1 -1
  22. package/dist/regenerate/jobs-tree.js +5 -6
  23. package/dist/regenerate/jobs.d.ts +4 -0
  24. package/dist/regenerate/jobs.js +24 -0
  25. package/dist/regenerate/ledger-view.d.ts +11 -3
  26. package/dist/regenerate/ledger-view.js +28 -10
  27. package/dist/regenerate/ledger.d.ts +9 -3
  28. package/dist/regenerate/ledger.js +4 -3
  29. package/dist/regenerate/loop.js +3 -4
  30. package/dist/regenerate/regenerate.js +20 -8
  31. package/dist/regenerate/stop-report.d.ts +1 -1
  32. package/dist/regenerate/stop-report.js +1 -15
  33. package/dist/resume-line.d.ts +2 -2
  34. package/dist/resume-line.js +3 -3
  35. package/docs/authoring-plain.md +2 -2
  36. package/docs/chats.md +3 -1
  37. package/docs/hosting.md +2 -2
  38. package/docs/regenerating.md +8 -6
  39. package/docs/using.md +7 -7
  40. package/genome/skills/brood/SKILL.md +1 -13
  41. package/genome/skills/spawn/SKILL.md +11 -16
  42. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { AgentSessionRuntime, InteractiveMode, SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { showWaitingMessages } from "./chat-waiting.js";
2
3
  import { interruptChat, terminalUi } from "./chats.js";
3
4
  /**
4
5
  * ctrl+c, in the chat shown: what it is doing stopped, the other chats left as they are (`interruptChat`). In a chat
@@ -34,6 +35,8 @@ export function driveTerminal(core, options = {}) {
34
35
  ui.show(real);
35
36
  runner.setUIContext(ui.ui, runner.mode);
36
37
  catchUp(session);
38
+ // What the chat shown was sent while its agent worked, and has not read yet: in its chat, shaded.
39
+ screen?.updatePendingMessagesDisplay?.();
37
40
  core.showing(chat);
38
41
  // The run `--continue` reopens: its first chat is pi's own, the rest are opened beside it.
39
42
  if (restore !== undefined) {
@@ -54,6 +57,7 @@ export function driveTerminal(core, options = {}) {
54
57
  if (writing !== undefined && writing.role === "assistant")
55
58
  void screen.handleEvent({ type: "message_start", message: writing });
56
59
  };
60
+ showWaitingMessages();
57
61
  const prototype = InteractiveMode.prototype;
58
62
  const rebind = prototype.rebindCurrentSession;
59
63
  prototype.rebindCurrentSession = async function (...args) {
@@ -0,0 +1,64 @@
1
+ import { type Component } from "@earendil-works/pi-tui";
2
+ /**
3
+ * A message sent while the chat's agent works, shown as a message at once: pi queues it until the agent's turn reaches
4
+ * a point it can read it - the end of a step, which a long tool call holds off for minutes - and until then lists it
5
+ * only as a dim `Steering:` line under the chat. Here each message waiting is drawn there as the message it is,
6
+ * shaded and marked not read yet - under the chat, so it stays below what the agent writes and the tools it calls
7
+ * meanwhile. When the agent reads it, it leaves the queue and pi puts it in the chat, a normal message, where it was
8
+ * read. Taken back into the editor (pi's dequeue, esc, ctrl+c), it goes with the queue.
9
+ *
10
+ * pi has no hook for it: its screen is patched, as chat-terminal.ts patches it - tests/chat-terminal.test.ts checks
11
+ * what it reaches against the pi installed.
12
+ */
13
+ /** What of pi's interactive mode this reaches: its own fields and methods, in plain JavaScript. */
14
+ export interface WaitingScreen {
15
+ chatContainer: Container;
16
+ pendingMessagesContainer: Container;
17
+ getAllQueuedMessages(): {
18
+ steering: string[];
19
+ followUp: string[];
20
+ };
21
+ addMessageToChat(message: unknown, options?: unknown): void;
22
+ getAppKeyDisplay?(action: string): string;
23
+ }
24
+ interface Container {
25
+ children: Component[];
26
+ addChild(component: Component): void;
27
+ clear(): void;
28
+ }
29
+ /** A part of a message waiting to be read, drawn shaded - with, under the last part, what it is waiting for. */
30
+ export declare class Shade implements Component {
31
+ inner: Component;
32
+ note: string;
33
+ constructor(inner: Component, note?: string);
34
+ render(width: number): string[];
35
+ invalidate(): void;
36
+ }
37
+ /**
38
+ * Under the chat, each message the agent has not read yet, drawn as pi draws a message in the chat - shaded, and
39
+ * marked not read yet - in the order they were sent.
40
+ */
41
+ export declare function showWaiting(screen: WaitingScreen): void;
42
+ /** What of pi's session `unqueue` reaches. */
43
+ export interface QueueOf {
44
+ getSteeringMessages(): readonly string[];
45
+ getFollowUpMessages(): readonly string[];
46
+ clearQueue(): {
47
+ steering: string[];
48
+ followUp: string[];
49
+ };
50
+ steer(text: string, images?: undefined, options?: {
51
+ source: "extension";
52
+ }): Promise<unknown>;
53
+ followUp(text: string, images?: undefined, options?: {
54
+ source: "extension";
55
+ }): Promise<unknown>;
56
+ }
57
+ /**
58
+ * A message taken out of the session's queue - read by another agent, a skill's - the rest put back as they were, in
59
+ * order. pi takes none out alone: the queue is emptied and filled again. False when it is not queued.
60
+ */
61
+ export declare function unqueue(session: QueueOf, text: string): Promise<boolean>;
62
+ /** pi's screen patched, once: the messages waiting for the agent drawn under the chat as messages, shaded. */
63
+ export declare function showWaitingMessages(): void;
64
+ export {};
@@ -0,0 +1,86 @@
1
+ import { InteractiveMode } from "@earendil-works/pi-coding-agent";
2
+ import { Spacer, truncateToWidth } from "@earendil-works/pi-tui";
3
+ const DIM = "\x1b[2m";
4
+ const UNDIM = "\x1b[22m";
5
+ /** A part of a message waiting to be read, drawn shaded - with, under the last part, what it is waiting for. */
6
+ export class Shade {
7
+ inner;
8
+ note;
9
+ constructor(inner, note = "") {
10
+ this.inner = inner;
11
+ this.note = note;
12
+ }
13
+ render(width) {
14
+ // Dim kept on past every reset in a line: a bold word or a colour must not end the shade.
15
+ const lines = this.inner.render(width).map((line) => `${DIM}${line.replace(/\x1b\[(?:0|22)?m/g, (reset) => `${reset}${DIM}`)}${UNDIM}`);
16
+ return this.note === "" ? lines : [...lines, `${DIM}${truncateToWidth(` ${this.note}`, width)}${UNDIM}`];
17
+ }
18
+ invalidate() {
19
+ this.inner.invalidate();
20
+ }
21
+ }
22
+ /**
23
+ * Under the chat, each message the agent has not read yet, drawn as pi draws a message in the chat - shaded, and
24
+ * marked not read yet - in the order they were sent.
25
+ */
26
+ export function showWaiting(screen) {
27
+ const area = screen.pendingMessagesContainer;
28
+ area.clear();
29
+ const { steering, followUp } = screen.getAllQueuedMessages();
30
+ const waiting = [...steering, ...followUp];
31
+ if (waiting.length === 0)
32
+ return;
33
+ const key = screen.getAppKeyDisplay?.("app.message.dequeue");
34
+ const note = `not read yet${key !== undefined && key !== "" ? ` · ${key} to edit` : ""}`;
35
+ // Drawn by pi's own drawing of a message in the chat, into the area under it instead.
36
+ const chat = screen.chatContainer;
37
+ screen.chatContainer = area;
38
+ try {
39
+ for (const text of waiting) {
40
+ // A line between it and the chat; pi puts one between messages itself.
41
+ if (area.children.length === 0)
42
+ area.addChild(new Spacer(1));
43
+ const before = area.children.length;
44
+ screen.addMessageToChat({ role: "user", content: [{ type: "text", text }], timestamp: Date.now() });
45
+ const parts = area.children.slice(before);
46
+ parts.forEach((part, index) => {
47
+ if (part instanceof Spacer)
48
+ return;
49
+ area.children[before + index] = new Shade(part, index === parts.length - 1 ? note : "");
50
+ });
51
+ }
52
+ }
53
+ finally {
54
+ screen.chatContainer = chat;
55
+ }
56
+ }
57
+ /**
58
+ * A message taken out of the session's queue - read by another agent, a skill's - the rest put back as they were, in
59
+ * order. pi takes none out alone: the queue is emptied and filled again. False when it is not queued.
60
+ */
61
+ export async function unqueue(session, text) {
62
+ if (!session.getSteeringMessages().includes(text) && !session.getFollowUpMessages().includes(text))
63
+ return false;
64
+ const { steering, followUp } = session.clearQueue();
65
+ const at = steering.indexOf(text);
66
+ if (at !== -1)
67
+ steering.splice(at, 1);
68
+ else
69
+ followUp.splice(followUp.indexOf(text), 1);
70
+ // Put back as turri's, not typed: what watches the person's input does not take them again.
71
+ for (const message of steering)
72
+ await session.steer(message, undefined, { source: "extension" });
73
+ for (const message of followUp)
74
+ await session.followUp(message, undefined, { source: "extension" });
75
+ return true;
76
+ }
77
+ /** pi's screen patched, once: the messages waiting for the agent drawn under the chat as messages, shaded. */
78
+ export function showWaitingMessages() {
79
+ const prototype = InteractiveMode.prototype;
80
+ if (prototype.turriWaiting === true)
81
+ return;
82
+ prototype.turriWaiting = true;
83
+ prototype.updatePendingMessagesDisplay = function () {
84
+ showWaiting(this);
85
+ };
86
+ }
package/dist/chats.d.ts CHANGED
@@ -69,6 +69,8 @@ export interface ChatLink {
69
69
  readonly scope: ChatScope;
70
70
  /** The host's dialogs, for this chat's questions - undefined in the terminal. */
71
71
  readonly dialogs: AskUserIO | undefined;
72
+ /** Its pi session, once it has one. */
73
+ session(): AgentSession | undefined;
72
74
  /** The extension built for the chat - again, when pi rebuilds it - and what it does for it. */
73
75
  live(instance: ChatInstance): void;
74
76
  }
package/dist/chats.js CHANGED
@@ -287,6 +287,7 @@ export function createChats(options) {
287
287
  id: chat.id,
288
288
  scope: chat.scope,
289
289
  dialogs: chat.dialogs,
290
+ session: () => chat.session,
290
291
  live: (instance) => {
291
292
  chat.instance = instance;
292
293
  },
package/dist/extension.js CHANGED
@@ -6,7 +6,7 @@ import { basename, dirname, join, relative, resolve } from "node:path";
6
6
  import { conversationCommand, conversationSettled, conversationWriting } from "./regenerate/conversation-writes.js";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { allocateImageId, Box, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
- import { defineTool } from "@earendil-works/pi-coding-agent";
9
+ import { defineTool, getMarkdownTheme, UserMessageComponent } from "@earendil-works/pi-coding-agent";
10
10
  import { Type } from "typebox";
11
11
  import { CODING_MODEL_ENV } from "./chat-models.js";
12
12
  import { AGENT_ROLES, CONFIG_FILE, configForSpec, hasProjectConfig, LEGACY_CONFIG_FILE, projectConfigFile, stackConfigFileIn, stackOf, stacksOf, withStack, modelOnly, modelSpec, projectModels, readProjectConfig, roleModel, splitModelSpec } from "./config.js";
@@ -19,6 +19,7 @@ import { createAskUserTool } from "./tools.js";
19
19
  import { asksFromScratch, asksWholeChain, driftTool, fromWorkingDir, haltTool, loopReply, MODEL_PARAM, regenerateTool, STACK_PARAM, stopJob, WAIT_PARAM, withCallStack, WORKING_DIR_PARAM } from "./loop-tools.js";
20
20
  import { createWebTools } from "./regenerate/web.js";
21
21
  import { describeGates, interruptedRun, runSkill } from "./genome.js";
22
+ import { unqueue } from "./chat-waiting.js";
22
23
  import { skillStatus } from "./skill.js";
23
24
  import { runningDeployed, stopDeployed } from "./regenerate/release.js";
24
25
  import { createDeployState, createDeployTools } from "./release-tools.js";
@@ -479,9 +480,15 @@ function turriExtension(api, session, link) {
479
480
  // The conversation's agent does not run turri again through its shell, unless the person
480
481
  // asked for the command line in the message it is answering.
481
482
  let lastUserMessage = "";
483
+ /** While a skill runs in this chat: what passes the person's message to its agent. */
484
+ let skillMessage;
482
485
  pi.on("input", (event) => {
483
486
  if (event.source !== "extension")
484
487
  lastUserMessage = event.text;
488
+ // A skill running in this chat holds its agent for the whole run: what the person types meanwhile is for the
489
+ // skill's agent. It stays queued here too, shown waiting, until that agent reads it - or, the run over first, this one.
490
+ if (event.source === "interactive" && skillMessage !== undefined && !event.text.startsWith("/"))
491
+ skillMessage(event.text);
485
492
  return { action: "continue" };
486
493
  });
487
494
  pi.on("tool_call", (event) => {
@@ -658,6 +665,22 @@ function turriExtension(api, session, link) {
658
665
  }
659
666
  // Its command is used up: started again later, it is typed again.
660
667
  gate.started(skill.name);
668
+ // What the person said to it while it ran, its agent having read it: told to this chat's agent with the result.
669
+ const heard = [];
670
+ const messages = {
671
+ open: (tell) => {
672
+ skillMessage = tell;
673
+ },
674
+ read: (text) => {
675
+ heard.push(text);
676
+ const session = link.session();
677
+ // Read by the skill's agent, not this one: out of this chat's queue, and into its chat where it was read.
678
+ void (session !== undefined ? unqueue(session, text) : Promise.resolve(false)).then((taken) => {
679
+ if (taken)
680
+ pi.appendEntry(SKILL_MESSAGE_ENTRY, { text, skill: skill.name });
681
+ });
682
+ },
683
+ };
661
684
  const trace = [`skill: ${skill.name}`, ...describeGates(skill).map((l) => ` ${l}`)];
662
685
  // While it runs, what its agent is doing leads: pi shows a running tool's first lines, and the trace's first
663
686
  // lines are the gates, which say nothing about now. The result keeps the trace alone.
@@ -754,6 +777,7 @@ function turriExtension(api, session, link) {
754
777
  resume: params.resume === true && interrupted !== undefined,
755
778
  report,
756
779
  context: conversationContext(ctx, params.context),
780
+ messages,
757
781
  askUser: askUserVia(ctx, (question, answer, said) => {
758
782
  // What the skill's agent said before asking stays in the run's record, not only in the dialog.
759
783
  for (const line of (said ?? "").trim().split("\n").filter((line) => line.trim() !== ""))
@@ -835,12 +859,14 @@ function turriExtension(api, session, link) {
835
859
  ? ""
836
860
  : `Time to first regeneration: ${formatElapsed(first.started)} - its first pulse started that long after ${skill.name} began` +
837
861
  (first.landed !== undefined ? `, and landed after ${formatElapsed(first.landed)}.` : ".");
862
+ const said = heard.length > 0 ? ["", `While it ran, the person said to it - its agent read and acted on each:`, ...heard.map((text) => `- ${text.replace(/\s+/g, " ").trim()}`)] : [];
838
863
  const text = result.ok
839
864
  ? [
840
865
  gated ? `${skill.name}: passed all gates.` : `${skill.name}: finished. It has no review gates, so nothing checked its work: read what it changed before you report it.`,
841
866
  changed,
842
867
  ...(unrendered !== "" ? [unrendered] : []),
843
868
  ...(ttfr !== "" ? [ttfr] : []),
869
+ ...said,
844
870
  "",
845
871
  "<artifact>",
846
872
  result.output,
@@ -850,7 +876,7 @@ function turriExtension(api, session, link) {
850
876
  ...trace,
851
877
  "</trace>",
852
878
  ].join("\n")
853
- : [`${skill.name}: stopped at a gate. No approved artifact.`, changed, "", "<trace>", ...trace, "</trace>"].join("\n");
879
+ : [`${skill.name}: stopped at a gate. No approved artifact.`, changed, ...said, "", "<trace>", ...trace, "</trace>"].join("\n");
854
880
  return { content: [{ type: "text", text }], details: { ok: result.ok, artifacts: result.artifacts } };
855
881
  },
856
882
  });
@@ -2338,6 +2364,8 @@ function registerSteering(pi, cwd) {
2338
2364
  * started is stopped when the session ends.
2339
2365
  */
2340
2366
  const SIDE_ENTRY = "turri_btw";
2367
+ /** A message the person sent while a skill ran in the chat, read by the skill's agent: drawn as the message it is. */
2368
+ const SKILL_MESSAGE_ENTRY = "turri_skill_message";
2341
2369
  /**
2342
2370
  * /btw, a question on the side (btw.ts): answered at once, even while a skill runs - pi runs a command mid-turn - and
2343
2371
  * shown in the transcript, never in a model's context.
@@ -2365,6 +2393,12 @@ function registerSideQuestions(pi, cwd, jobs) {
2365
2393
  box.addChild(new Text(lines.join("\n"), 0, 0));
2366
2394
  return box;
2367
2395
  });
2396
+ pi.registerEntryRenderer(SKILL_MESSAGE_ENTRY, (entry) => {
2397
+ const data = entry.data;
2398
+ if (data === undefined)
2399
+ return undefined;
2400
+ return new UserMessageComponent(data.text, getMarkdownTheme());
2401
+ });
2368
2402
  pi.registerEntryRenderer(SIDE_ENTRY, (entry, _options, theme) => {
2369
2403
  const data = entry.data;
2370
2404
  if (data === undefined)
package/dist/genome.d.ts CHANGED
@@ -17,6 +17,16 @@ export interface RunSkillResult {
17
17
  /** The last step's artifact - what the caller usually wants. */
18
18
  output: string;
19
19
  }
20
+ /**
21
+ * The person's messages to a run while it works - typed in the chat the skill was started from, whose own agent is busy
22
+ * running it: each is given to the run's agent, and `read` is told when that agent reads it.
23
+ */
24
+ export interface SkillMessages {
25
+ /** Given what passes a message to the run's agent as the run starts, and undefined as it ends. */
26
+ open(tell: ((text: string) => void) | undefined): void;
27
+ /** The run's agent read a message passed to it. */
28
+ read(text: string): void;
29
+ }
20
30
  export interface RunSkillOptions {
21
31
  skillDir: string;
22
32
  cwd: string;
@@ -26,6 +36,8 @@ export interface RunSkillOptions {
26
36
  escalate: (request: EscalationRequest) => Promise<boolean>;
27
37
  /** Lets a step interview the user. Omitted: the skill runs with nobody to ask. */
28
38
  askUser?: AskUser;
39
+ /** What the person says in the chat while it runs, for its agent. */
40
+ messages?: SkillMessages;
29
41
  /** "provider/model-id" for the executor and the judges. Omitted: pi's default. */
30
42
  model?: string;
31
43
  /** Lets steps build functionalities in the background while the interview continues. */
package/dist/genome.js CHANGED
@@ -156,8 +156,20 @@ async function runStoppable(opts) {
156
156
  attempts: {},
157
157
  updatedAt: "",
158
158
  };
159
+ // The person's messages, given to the agent at its next step - or with its next prompt, between steps.
160
+ const passed = [];
159
161
  // What the agent does, as it does it: its session is its own, so without this a long step is silence.
160
162
  executor.subscribe((event) => {
163
+ if (event.type === "message_start" && event.message.role === "user") {
164
+ const content = event.message.content;
165
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((part) => part?.type === "text").map((part) => part.text).join("") : "";
166
+ const at = passed.indexOf(text);
167
+ if (at !== -1) {
168
+ passed.splice(at, 1);
169
+ opts.messages?.read(text);
170
+ }
171
+ return;
172
+ }
161
173
  if (event.type === "tool_execution_start") {
162
174
  opts.report.activity?.(describeToolCall(event.toolName, event.args));
163
175
  return;
@@ -212,6 +224,10 @@ async function runStoppable(opts) {
212
224
  liveReviewers.set(liveStep.name, liveProxy);
213
225
  }
214
226
  try {
227
+ opts.messages?.open((text) => {
228
+ passed.push(text);
229
+ void Promise.resolve(executor.steer(text, undefined, { source: "extension" })).catch(() => undefined);
230
+ });
215
231
  const app = compileSkill({
216
232
  skill,
217
233
  cwd: opts.cwd,
@@ -265,6 +281,7 @@ async function runStoppable(opts) {
265
281
  };
266
282
  }
267
283
  finally {
284
+ opts.messages?.open(undefined);
268
285
  executor.dispose();
269
286
  }
270
287
  }
@@ -10,7 +10,7 @@ export const NEW_PROJECT = [
10
10
  "and there is no ***plain module for it yet, or they ask for a new project, ask them first how to start it, with",
11
11
  "ask_user: one question, these three options, word for word, so they know the skills are there:",
12
12
  `- "${"/brood - an interview: the stack, the testing and every feature settled with you, each answer regenerated"}"`,
13
- `- "${"/spawn - something running in minutes: the quick start, asking only what your message leaves open"}"`,
13
+ `- "${"/spawn - something running in minutes: the quick start, asking the stack in one question unless your message names it"}"`,
14
14
  `- "${"Just do it - no skill: I write it here and start regenerating"}"`,
15
15
  "Picked /brood or /spawn: the answer starts it - call cultivate for it at once, with what they said as its context,",
16
16
  "never asking them to type its command. Do not ask when their",
package/dist/pi-launch.js CHANGED
@@ -11,7 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { main, SettingsManager } from "@earendil-works/pi-coding-agent";
12
12
  import { ProcessTerminal } from "@earendil-works/pi-tui";
13
13
  import "./terminal-images.js";
14
- import { resumeLine } from "./resume-line.js";
14
+ import { withoutResumeLine } from "./resume-line.js";
15
15
  import { lastRun } from "./chat-records.js";
16
16
  import { driveTerminal } from "./chat-terminal.js";
17
17
  import turri, { createTurriCore, processSession } from "./extension.js";
@@ -37,10 +37,13 @@ const setTitle = ProcessTerminal.prototype.setTitle;
37
37
  ProcessTerminal.prototype.setTitle = function (title) {
38
38
  setTitle.call(this, title.replace(/^π(?= |$)/, "🪼 turri"));
39
39
  };
40
- // What pi prints on the way out to resume the session names pi - its APP_NAME, a constant - which may not be on the
41
- // PATH at all: turri takes the same flags, so it is turri the line names.
40
+ // Leaving turri leaves the terminal as it was, the command run its only trace. pi's fullscreen screen - its default -
41
+ // writes the conversation out to the terminal on the way out, unless its `fullscreenExitOutput` is `resume-hint`, which
42
+ // is answered for this run as `quietStartup` is; the line it prints instead, to resume the session, is left out too:
43
+ // `turri --continue` opens the last run again.
44
+ SettingsManager.prototype.getFullscreenExitOutput = () => "resume-hint";
42
45
  const write = process.stdout.write.bind(process.stdout);
43
- process.stdout.write = ((chunk, ...rest) => write(typeof chunk === "string" ? resumeLine(chunk) : chunk, ...rest));
46
+ process.stdout.write = ((chunk, ...rest) => write(typeof chunk === "string" ? withoutResumeLine(chunk) : chunk, ...rest));
44
47
  // pi's own keys stand: a new line is shift+enter or ctrl+j. turri's side panel is on ctrl+h (job-panel.ts PANEL_KEY).
45
48
  // What pi's CLI does before main: the process title, pi's environment markers, the HTTP
46
49
  // dispatcher. Not exported, so loaded from the file - beside pi's entry point, wherever npm put it.
@@ -213,7 +213,7 @@ export declare function testReviewRequest(options: {
213
213
  }): string;
214
214
  /** The review's problems, for the conformance agent to fix. */
215
215
  export declare function reviewFeedbackPrompt(problems: ReviewProblem[]): string;
216
- export type Culprit = "code" | "tests" | "spec" | "environment";
216
+ export type Culprit = "code" | "tests" | "spec" | "environment" | "script";
217
217
  /** One thing the judge found, and where: a file it opened, `testing script`, or `failing run`. */
218
218
  export interface Evidence {
219
219
  source: string;
@@ -394,10 +394,14 @@ export declare function escalatedFixPrompt(options: {
394
394
  content: string;
395
395
  }>;
396
396
  }): string;
397
- /** 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. */
397
+ /**
398
+ * The run's environment, or the testing script itself, judged what is wrong: the conformance agent checks the tests do
399
+ * not cause it, and changes nothing when they do not.
400
+ */
398
401
  export declare function environmentCheckPrompt(options: {
399
402
  run: ScriptRun;
400
403
  reason: string;
404
+ culprit?: "environment" | "script";
401
405
  }): string;
402
406
  export declare function testsFixPrompt(options: {
403
407
  run: ScriptRun;
@@ -729,10 +729,10 @@ export async function judgeFailure(options) {
729
729
  const submit = defineTool({
730
730
  name: "submit_verdict",
731
731
  label: "Submit verdict",
732
- description: "Say what is wrong - the code, the tests, the specification itself, or the run's environment - and why. Call this exactly once.",
732
+ description: "Say what is wrong - the code, the tests, the specification itself, the run's environment or the testing script - and why. Call this exactly once.",
733
733
  parameters: Type.Object({
734
- culprit: Type.Union([Type.Literal("code"), Type.Literal("tests"), Type.Literal("spec"), Type.Literal("environment")], {
735
- description: "What has to change to meet the specification - or spec, when the specification cannot be met at all, or environment, when only the person can fix what the run is given",
734
+ culprit: Type.Union([Type.Literal("code"), Type.Literal("tests"), Type.Literal("spec"), Type.Literal("environment"), Type.Literal("script")], {
735
+ description: "What has to change to meet the specification - or spec, when the specification cannot be met at all, environment, when only the person can fix what the run is given, or script, when the testing script itself fails whatever the code and the tests are",
736
736
  }),
737
737
  reason: Type.String({ description: "Why, against the specification: which test, which behaviour, what the spec says" }),
738
738
  spec_says: Type.Optional(Type.String({
@@ -800,7 +800,7 @@ export async function judgeFailure(options) {
800
800
  await ask(session, BEGIN, options.onDelta);
801
801
  for (let again = 0; again < 2; again += 1) {
802
802
  if (verdict === undefined) {
803
- await ask(session, "You have not called submit_verdict. Call it now: code, tests, spec or environment, and why.", options.onDelta);
803
+ await ask(session, "You have not called submit_verdict. Call it now: code, tests, spec, environment or script, and why.", options.onDelta);
804
804
  continue;
805
805
  }
806
806
  // Asked to weigh an escalation, a verdict of code or tests rests on what it found itself.
@@ -861,7 +861,7 @@ export function judgePrompt(options) {
861
861
  const scripts = options.testingScripts ?? [];
862
862
  return [
863
863
  specPrimer({ testReqs: true }),
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.`,
864
+ `The conformance tests of the \`${options.spec.moduleName}\` specification failed. Decide what is wrong: the code, the tests, the specification itself, the run's environment, or the testing script.`,
865
865
  "",
866
866
  `The code is in ${options.buildDir}; the conformance tests are in ${options.conformanceDir}. Read both.`,
867
867
  "",
@@ -889,10 +889,17 @@ export function judgePrompt(options) {
889
889
  " make up), an account, plan or quota a service refuses, a service the specification names that cannot be reached",
890
890
  " at all. Say what the person has to provide, and where the run read it from - the file, the variable. Not for what",
891
891
  " the tests set up themselves - a credential, a variable or a fixture a test sets, overrides or names wrong: that is",
892
- " tests; nor for a toolchain or package missing where the tests run: the agents install those. The run stops on it",
893
- ` once it has been judged so ${ENVIRONMENT_RAISES} times in a row; until then the tests are checked for whether they cause it.`,
892
+ " tests; nor for a package the code or the tests can declare: the agents add those. An interpreter or a toolchain",
893
+ " this machine lacks - `python`, `node` - is environment: the testing scripts run on this machine, and only the",
894
+ ` person installs on it. The run stops on it once it has been judged so ${ENVIRONMENT_RAISES} times in a row; until then`,
895
+ " the tests are checked for whether they cause it.",
894
896
  " A run the time limit stopped is environment only when nothing hangs and it needs more time than a test run may",
895
897
  " take - and then it stops at once.",
898
+ "- script: the conformance testing script, or the prepare script, is itself wrong - it fails whatever the code and the",
899
+ " tests are: it overwrites the tests' exit code, runs the wrong folder, calls a tool wrongly. Neither agent may change",
900
+ " it - only the person can. Only when no code and no tests could pass it: what the script needs of the build is code,",
901
+ " and how the tests are reached is tests. Quote its lines in evidence and say what to change in it. It counts as",
902
+ ` environment does: once judged so ${ENVIRONMENT_RAISES} times in a row, the run stops.`,
896
903
  "",
897
904
  "When the output does not show why the software failed - it answered with an error that names no cause, and",
898
905
  "logged nothing about it - the code is what is wrong first: it hides the cause, and nobody can fix what they",
@@ -1124,12 +1131,22 @@ export function escalatedFixPrompt(options) {
1124
1131
  "if the run still fails the same way after your fix, the work stops.",
1125
1132
  ].join("\n");
1126
1133
  }
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. */
1134
+ /**
1135
+ * The run's environment, or the testing script itself, judged what is wrong: the conformance agent checks the tests do
1136
+ * not cause it, and changes nothing when they do not.
1137
+ */
1128
1138
  export function environmentCheckPrompt(options) {
1129
1139
  return [
1130
1140
  `The harness ran the conformance testing script, \`${options.run.command}\`, on the whole suite, and it failed (exit ${options.run.exitCode}).`,
1131
- "It was checked against the specification, and what is wrong is judged to be the run's environment - something only",
1132
- "the person can fix, such as a credential the run is given:",
1141
+ ...(options.culprit === "script"
1142
+ ? [
1143
+ "It was checked against the specification, and what is wrong is judged to be the conformance testing script itself -",
1144
+ "it fails whatever the code and the tests are, and only the person can change it:",
1145
+ ]
1146
+ : [
1147
+ "It was checked against the specification, and what is wrong is judged to be the run's environment - something only",
1148
+ "the person can fix, such as a credential the run is given:",
1149
+ ]),
1133
1150
  "",
1134
1151
  `<why the environment>\n${options.reason.trim()}\n</why the environment>`,
1135
1152
  "",
@@ -1137,10 +1154,19 @@ export function environmentCheckPrompt(options) {
1137
1154
  options.run.output.trimEnd().slice(-6000),
1138
1155
  "</failing run>",
1139
1156
  "",
1140
- "Before that is taken as the answer, check that the tests do not cause it themselves: a credential, an environment",
1141
- "variable or a fixture a test sets, overrides, leaves behind for the next test, or reads under the wrong name. If",
1142
- "they do, fix the tests and run them. If they do not, change nothing - never make up a credential, write a .env",
1143
- "file, or weaken a test so it passes without what it needs - and say so in submit_work.",
1157
+ ...(options.culprit === "script"
1158
+ ? [
1159
+ "Before that is taken as the answer, check that the tests do not cause it themselves: tests in a folder, under a",
1160
+ "name or for a runner the script does not take, a test that fails for its own reasons. If they do, fix the tests",
1161
+ "and run them. If they do not, change nothing - never weaken a test, or work around the script - and say so in",
1162
+ "submit_work.",
1163
+ ]
1164
+ : [
1165
+ "Before that is taken as the answer, check that the tests do not cause it themselves: a credential, an environment",
1166
+ "variable or a fixture a test sets, overrides, leaves behind for the next test, or reads under the wrong name. If",
1167
+ "they do, fix the tests and run them. If they do not, change nothing - never make up a credential, write a .env",
1168
+ "file, or weaken a test so it passes without what it needs - and say so in submit_work.",
1169
+ ]),
1144
1170
  ].join("\n");
1145
1171
  }
1146
1172
  export function testsFixPrompt(options) {
@@ -95,14 +95,6 @@ export interface RenderConformanceTestsResult {
95
95
  times: number;
96
96
  changedNothing?: boolean;
97
97
  };
98
- /**
99
- * The run's environment is what is wrong - a credential missing or rejected, say - judged so ENVIRONMENT_RAISES
100
- * times in a row: only the person can fix it, so the run stopped.
101
- */
102
- environment?: {
103
- reason: string;
104
- times: number;
105
- };
106
98
  /**
107
99
  * The tests' fix for a failure did not make it go away, the judge found it the code's and escalated it to the coding
108
100
  * agent - and the run still failed the same way after the code's fix too: why the judge found it the code's, on what
@@ -15,7 +15,7 @@ import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts,
15
15
  import { createWebTools, WEB_TOOLS } from "./web.js";
16
16
  import { createMyDiffTool } from "./self-check.js";
17
17
  import { createServiceTools } from "./services.js";
18
- import { missingToolchainError, timeLimitError, toolchainError } from "./environment-error.js";
18
+ import { missingToolchainError, runEnvironmentError, scriptFaultError, timeLimitError, toolchainError } from "./environment-error.js";
19
19
  import { workspaceBlock } from "./workspace.js";
20
20
  import { codeFixPrompt, conflictKey, escalatedFixPrompt, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
21
21
  import { resolveRequiresChain } from "./requires.js";
@@ -192,8 +192,7 @@ export async function renderConformanceTests(options) {
192
192
  const reviewOn = configForSpec(options.plainFile).review === true;
193
193
  const reviewHistory = [];
194
194
  let specConflict;
195
- let environment;
196
- /** How many times in a row the run's environment was judged what is wrong. */
195
+ /** How many times in a row the run's environment, or the testing script, was judged what is wrong: the person's to fix. */
197
196
  let environmentRaises = 0;
198
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. */
199
198
  let idleFixes = 0;
@@ -548,20 +547,25 @@ export async function renderConformanceTests(options) {
548
547
  abandonWork(start, moduleName, "the conformance testing script needs more time than a test run may take");
549
548
  throw timeLimitError(recheck, { moduleName, script: scripts.conformance, kind: "conformance", reason: verdict.reason });
550
549
  }
551
- // The environment - what the run is given - is the person's to fix: once judged so twice in a row, the run stops.
552
- if (verdict.culprit === "environment") {
550
+ // The environment - what the run is given - or the testing script itself is the person's to fix: once judged so
551
+ // twice in a row, the pulse stops - rolled back, no retry - and the person is told what to fix.
552
+ if (verdict.culprit === "environment" || verdict.culprit === "script") {
553
+ const culprit = verdict.culprit;
554
+ const what = culprit === "script" ? "the conformance testing script itself" : "the run's environment";
553
555
  environmentRaises += 1;
554
- reportActivity(` the environment (judged ${environmentRaises} of ${ENVIRONMENT_RAISES} in a row): ${reason}`);
556
+ reportActivity(` ${what} (judged ${environmentRaises} of ${ENVIRONMENT_RAISES} in a row): ${reason}`);
555
557
  if (environmentRaises >= ENVIRONMENT_RAISES) {
556
- stage("the run's environment is what is wrong, and only the person can fix it - stopping");
557
- environment = { reason: verdict.reason, times: environmentRaises };
558
- recordNote(conformanceDir, "pitfalls", `the conformance run's environment is wrong, for the person to fix: ${verdict.reason.replace(/\s+/g, " ").slice(0, 300)}`, `${label} conformance`);
559
- break;
558
+ stage(`${what} is what is wrong, and only the person can fix it - stopping`);
559
+ if (start !== undefined)
560
+ abandonWork(start, moduleName, `${what} is what is wrong`);
561
+ throw culprit === "script"
562
+ ? scriptFaultError("script", { moduleName, script: scripts.conformance, kind: "conformance", reason: `judged so ${environmentRaises} times in a row: ${verdict.reason}` })
563
+ : runEnvironmentError({ moduleName, script: scripts.conformance, reason: verdict.reason, times: environmentRaises });
560
564
  }
561
- stage(`the run's environment is judged wrong (${environmentRaises} of ${ENVIRONMENT_RAISES}) - the conformance agent checks the tests do not cause it`);
562
- const fixed = await changing(() => askConformance(environmentCheckPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
565
+ stage(`${what} is judged wrong (${environmentRaises} of ${ENVIRONMENT_RAISES}) - the conformance agent checks the tests do not cause it`);
566
+ const fixed = await changing(() => askConformance(environmentCheckPrompt({ run: recheck, reason: verdict.reason, culprit }), options.onDelta));
563
567
  response = fixed.result;
564
- recordAttempt(rounds, recheck, `the environment: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
568
+ recordAttempt(rounds, recheck, `${what}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
565
569
  continue;
566
570
  }
567
571
  environmentRaises = 0;
@@ -824,7 +828,6 @@ export async function renderConformanceTests(options) {
824
828
  testRemovals,
825
829
  ...(specConflict !== undefined ? { specConflict } : {}),
826
830
  ...(stuck !== undefined ? { stuck } : {}),
827
- ...(environment !== undefined ? { environment } : {}),
828
831
  ...(escalation !== undefined ? { escalation } : {}),
829
832
  ...(unlabelled !== undefined ? { unlabelled } : {}),
830
833
  ...(reviewWaived !== undefined ? { reviewWaived } : {}),
@@ -24,6 +24,11 @@ export interface Diagnosis {
24
24
  * test run may take - which no agent can give it. Taken only of a run the limit stopped.
25
25
  */
26
26
  tooSlow?: boolean;
27
+ /**
28
+ * Not the code's: the testing script itself is wrong, or this machine lacks what it runs - it fails whatever the code
29
+ * and its tests are, and the coding agent may change neither. The pulse stops on it.
30
+ */
31
+ atFault?: "script" | "machine";
27
32
  }
28
33
  /** The debugger's account, as the coding agent is given it. */
29
34
  export declare function diagnosisText(diagnosis: Diagnosis): string;