@spinalfluid/ultrac 0.1.9 → 0.1.11

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 (3) hide show
  1. package/README.md +16 -0
  2. package/dist/cli.js +1374 -507
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -286,7 +286,7 @@ var agentModePresets = {
286
286
  "Prefer narrow, reversible changes.",
287
287
  "Keep final answers concise and include verification status."
288
288
  ],
289
- tools: [...coreTools, "list_sessions", "read_session", "search_sessions"],
289
+ tools: [...coreTools, "create_skill", "list_sessions", "read_session", "search_sessions"],
290
290
  externalTools: true
291
291
  },
292
292
  plan: {
@@ -331,6 +331,7 @@ var agentModePresets = {
331
331
  ],
332
332
  tools: [
333
333
  ...coreTools,
334
+ "create_skill",
334
335
  "ast_search",
335
336
  "repo_handbook",
336
337
  "verification_plan",
@@ -1222,20 +1223,20 @@ function compactRun(input, options = {}) {
1222
1223
  }
1223
1224
  async function compactShape(shape, options) {
1224
1225
  const reserveChars = options.summarise ? Math.max(0, options.reserveChars ?? shape.reserveChars ?? 0) : 0;
1225
- const plain = planKeepingSummaries(shape, shape.messages, reserveChars);
1226
+ const plain2 = planKeepingSummaries(shape, shape.messages, reserveChars);
1226
1227
  const skip = (reason) => ({
1227
- ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain,
1228
+ ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain2,
1228
1229
  compacted: false,
1229
1230
  summarisedMessages: 0,
1230
1231
  summarisedTurns: 0,
1231
1232
  skipped: reason
1232
1233
  });
1233
- if (plain.dropped.length === 0) return skip("nothing-dropped");
1234
+ if (plain2.dropped.length === 0) return skip("nothing-dropped");
1234
1235
  if (!options.summarise) return skip("no-summariser");
1235
- const source = plain.dropped.filter((message2) => !isCompactionSummary(message2));
1236
+ const source = plain2.dropped.filter((message2) => !isCompactionSummary(message2));
1236
1237
  if (source.length === 0) return skip("only-summaries");
1237
1238
  const ceiling = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS;
1238
- const slack = plain.usage.promptLimitChars - plain.usage.totalChars - summaryEnvelopeChars();
1239
+ const slack = plain2.usage.promptLimitChars - plain2.usage.totalChars - summaryEnvelopeChars();
1239
1240
  const maxChars = Math.min(ceiling, slack);
1240
1241
  if (maxChars < MIN_SUMMARY_CHARS) return skip("no-room");
1241
1242
  const transcript = renderDroppedTranscript(source, {
@@ -1260,21 +1261,21 @@ async function compactShape(shape, options) {
1260
1261
  const text = truncateToLine(summary.trim(), maxChars);
1261
1262
  if (!text) return skip("summariser-empty");
1262
1263
  const message = compactionSummaryMessage(text);
1263
- const plan = replan(shape, plain, message);
1264
+ const plan = replan(shape, plain2, message);
1264
1265
  if (!plan) return skip("would-drop-more");
1265
1266
  return {
1266
1267
  ...plan,
1267
1268
  // The re-plan saw a transcript the span was already out of, so it has
1268
1269
  // nothing to report about it. What was dropped is what `plain` dropped.
1269
- dropped: plain.dropped,
1270
- droppedMessages: plain.droppedMessages,
1271
- droppedTurns: plain.droppedTurns,
1272
- droppedChars: plain.droppedChars,
1270
+ dropped: plain2.dropped,
1271
+ droppedMessages: plain2.droppedMessages,
1272
+ droppedTurns: plain2.droppedTurns,
1273
+ droppedChars: plain2.droppedChars,
1273
1274
  compacted: true,
1274
1275
  summary: text,
1275
- summarisedMessages: plain.dropped.length,
1276
- summarisedTurns: plain.droppedTurns,
1277
- warning: compactionWarning(plan, plain)
1276
+ summarisedMessages: plain2.dropped.length,
1277
+ summarisedTurns: plain2.droppedTurns,
1278
+ warning: compactionWarning(plan, plain2)
1278
1279
  };
1279
1280
  }
1280
1281
  function planKeepingSummaries(shape, messages, reserveChars) {
@@ -1290,8 +1291,8 @@ function planKeepingSummaries(shape, messages, reserveChars) {
1290
1291
  return shape.plan(messages, callerPinned, reserveChars);
1291
1292
  }
1292
1293
  }
1293
- function replan(shape, plain, summary) {
1294
- const kept = plain.messages;
1294
+ function replan(shape, plain2, summary) {
1295
+ const kept = plain2.messages;
1295
1296
  const insertAt = kept.findIndex((message) => message.role !== "system");
1296
1297
  const at = insertAt < 0 ? kept.length : insertAt;
1297
1298
  const next = [...kept.slice(0, at), summary, ...kept.slice(at)];
@@ -1307,9 +1308,9 @@ function replan(shape, plain, summary) {
1307
1308
  if (!kept.every((message) => survived.has(message))) return void 0;
1308
1309
  return plan;
1309
1310
  }
1310
- function compactionWarning(plan, plain) {
1311
- const turns = plural2(plain.droppedTurns, "earlier turn");
1312
- const messages = plural2(plain.dropped.length, "message");
1311
+ function compactionWarning(plan, plain2) {
1312
+ const turns = plural2(plain2.droppedTurns, "earlier turn");
1313
+ const messages = plural2(plain2.dropped.length, "message");
1313
1314
  return `Context budget: compacted ${turns} (${messages}) into a summary to stay under the ${count2(plan.usage.promptLimitChars)}-character prompt limit. The summary is in the transcript in their place; the recent turns are untouched.`;
1314
1315
  }
1315
1316
  function truncateToLine(text, maxChars) {
@@ -4083,7 +4084,7 @@ function runProcess(command, args, options) {
4083
4084
  });
4084
4085
  const untrack = trackProcessGroup(child.pid);
4085
4086
  const stdout6 = createCapture(maxBytes);
4086
- const stderr = createCapture(maxBytes);
4087
+ const stderr2 = createCapture(maxBytes);
4087
4088
  let settled = false;
4088
4089
  let started = false;
4089
4090
  let termination;
@@ -4139,7 +4140,7 @@ function runProcess(command, args, options) {
4139
4140
  };
4140
4141
  const onAbort = () => beginTermination("aborted");
4141
4142
  child.stdout.on("data", (chunk) => stdout6.push(chunk));
4142
- child.stderr.on("data", (chunk) => stderr.push(chunk));
4143
+ child.stderr.on("data", (chunk) => stderr2.push(chunk));
4143
4144
  child.stdout.on("error", () => {
4144
4145
  });
4145
4146
  child.stderr.on("error", () => {
@@ -4152,7 +4153,7 @@ function runProcess(command, args, options) {
4152
4153
  if (termination === "aborted") {
4153
4154
  resolve15({
4154
4155
  stdout: stdout6.text(),
4155
- stderr: stderr.text(),
4156
+ stderr: stderr2.text(),
4156
4157
  started,
4157
4158
  aborted: true,
4158
4159
  exitCode: null,
@@ -4160,7 +4161,7 @@ function runProcess(command, args, options) {
4160
4161
  timedOut: false,
4161
4162
  timeoutMs,
4162
4163
  ...stdout6.truncation() ? { stdoutTruncation: stdout6.truncation() } : {},
4163
- ...stderr.truncation() ? { stderrTruncation: stderr.truncation() } : {},
4164
+ ...stderr2.truncation() ? { stderrTruncation: stderr2.truncation() } : {},
4164
4165
  redactedEnvKeys
4165
4166
  });
4166
4167
  return;
@@ -4170,11 +4171,11 @@ function runProcess(command, args, options) {
4170
4171
  });
4171
4172
  child.on("close", (exitCode, signal) => {
4172
4173
  const stdoutTruncation = stdout6.truncation();
4173
- const stderrTruncation = stderr.truncation();
4174
+ const stderrTruncation = stderr2.truncation();
4174
4175
  finish(
4175
4176
  () => resolve15({
4176
4177
  stdout: stdout6.text(),
4177
- stderr: stderr.text(),
4178
+ stderr: stderr2.text(),
4178
4179
  started,
4179
4180
  aborted: termination === "aborted",
4180
4181
  exitCode,
@@ -5184,7 +5185,7 @@ function topCounts(values) {
5184
5185
  for (const value of values) {
5185
5186
  counts.set(value, (counts.get(value) ?? 0) + 1);
5186
5187
  }
5187
- return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5188
+ return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5188
5189
  }
5189
5190
  async function collectGitContext(workspace, signal) {
5190
5191
  const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
@@ -6969,8 +6970,8 @@ function shannonEntropy(value) {
6969
6970
  counts.set(char, (counts.get(char) ?? 0) + 1);
6970
6971
  }
6971
6972
  let entropy = 0;
6972
- for (const count3 of counts.values()) {
6973
- const probability = count3 / value.length;
6973
+ for (const count4 of counts.values()) {
6974
+ const probability = count4 / value.length;
6974
6975
  entropy -= probability * Math.log2(probability);
6975
6976
  }
6976
6977
  return entropy;
@@ -8082,6 +8083,213 @@ async function loadSkillsContext(workspace) {
8082
8083
  }).join("\n") + notice;
8083
8084
  }
8084
8085
 
8086
+ // src/skills/commands.ts
8087
+ var BUILTIN_SLASH_COMMANDS = [
8088
+ "doctor",
8089
+ "mcp",
8090
+ "continue",
8091
+ "autonomous",
8092
+ "skill",
8093
+ "update",
8094
+ "logs",
8095
+ "ghost",
8096
+ "output",
8097
+ "usage",
8098
+ "max-steps",
8099
+ "resume",
8100
+ "deny",
8101
+ "plan",
8102
+ "review",
8103
+ "coding",
8104
+ "status",
8105
+ "context",
8106
+ "compact",
8107
+ "diff",
8108
+ "sessions",
8109
+ "save",
8110
+ "test",
8111
+ "init",
8112
+ "new",
8113
+ "allow",
8114
+ "clear",
8115
+ "exit",
8116
+ "help",
8117
+ "memory",
8118
+ "mode",
8119
+ "model",
8120
+ "permissions",
8121
+ "quit",
8122
+ "skills",
8123
+ "tools",
8124
+ "whoami"
8125
+ ];
8126
+ var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8127
+ var MAX_COMMAND_LENGTH = 64;
8128
+ function parseSlashInput(input) {
8129
+ const trimmed = input.trim();
8130
+ if (!trimmed.startsWith("/")) return null;
8131
+ const rest = trimmed.slice(1);
8132
+ const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
8133
+ if (!match) return null;
8134
+ return { command: match[1], args: (match[2] ?? "").trim() };
8135
+ }
8136
+ function isBuiltinSlashCommand(command) {
8137
+ return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
8138
+ }
8139
+ function normalizeCommandName(value) {
8140
+ const candidate = value.trim().replace(/^\/+/, "").trim();
8141
+ if (!candidate) {
8142
+ throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
8143
+ }
8144
+ if (candidate.length > MAX_COMMAND_LENGTH) {
8145
+ throw new Error(
8146
+ `Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
8147
+ );
8148
+ }
8149
+ if (!COMMAND_NAME_RE.test(candidate)) {
8150
+ throw new Error(
8151
+ `Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
8152
+ );
8153
+ }
8154
+ return candidate;
8155
+ }
8156
+ async function collectEntries(workspace) {
8157
+ const skills = await discoverSkills(workspace);
8158
+ const claimed = /* @__PURE__ */ new Map();
8159
+ const conflicts = [];
8160
+ for (const skill of skills) {
8161
+ let declaration;
8162
+ try {
8163
+ declaration = skill.command ?? null;
8164
+ } catch (error) {
8165
+ conflicts.push({
8166
+ command: "",
8167
+ skillName: skill.name,
8168
+ scope: skill.scope,
8169
+ path: skill.path,
8170
+ reason: "invalid",
8171
+ message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
8172
+ });
8173
+ continue;
8174
+ }
8175
+ if (!declaration) continue;
8176
+ let command;
8177
+ try {
8178
+ command = normalizeCommandName(declaration.raw);
8179
+ } catch (error) {
8180
+ conflicts.push({
8181
+ command: declaration.raw,
8182
+ skillName: skill.name,
8183
+ scope: skill.scope,
8184
+ path: skill.path,
8185
+ reason: "invalid",
8186
+ message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
8187
+ });
8188
+ continue;
8189
+ }
8190
+ if (isBuiltinSlashCommand(command)) {
8191
+ conflicts.push({
8192
+ command,
8193
+ skillName: skill.name,
8194
+ scope: skill.scope,
8195
+ path: skill.path,
8196
+ reason: "builtin",
8197
+ message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
8198
+ });
8199
+ continue;
8200
+ }
8201
+ const entry = {
8202
+ skill,
8203
+ command: {
8204
+ command,
8205
+ skillName: skill.name,
8206
+ description: skill.description,
8207
+ argumentHint: declaration.argumentHint,
8208
+ scope: skill.scope,
8209
+ path: skill.path
8210
+ }
8211
+ };
8212
+ const existing = claimed.get(command);
8213
+ if (!existing) {
8214
+ claimed.set(command, entry);
8215
+ continue;
8216
+ }
8217
+ const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
8218
+ const loser = winner === entry ? existing : entry;
8219
+ claimed.set(command, winner);
8220
+ conflicts.push({
8221
+ command,
8222
+ skillName: loser.skill.name,
8223
+ scope: loser.skill.scope,
8224
+ path: loser.skill.path,
8225
+ reason: "duplicate",
8226
+ message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
8227
+ });
8228
+ }
8229
+ const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
8230
+ return { entries, conflicts };
8231
+ }
8232
+ async function listSkillCommands(workspace) {
8233
+ const { entries, conflicts } = await collectEntries(workspace);
8234
+ return { commands: entries.map((entry) => entry.command), conflicts };
8235
+ }
8236
+ function buildSkillCommandPrompt(skill, command, args) {
8237
+ const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
8238
+ const header = [
8239
+ `The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
8240
+ skill.description ? `Description: ${skill.description}` : void 0,
8241
+ argumentLine,
8242
+ skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
8243
+ "Follow the instructions below now, applying the arguments where they are relevant."
8244
+ ].filter(Boolean).join("\n");
8245
+ return `${header}
8246
+
8247
+ --- instructions ---
8248
+ ${skill.body}`;
8249
+ }
8250
+ async function resolveSkillCommand(workspace, input) {
8251
+ const parsed = parseSlashInput(input);
8252
+ if (!parsed) return { status: "not-a-command" };
8253
+ const command = parsed.command.toLowerCase();
8254
+ if (isBuiltinSlashCommand(command)) {
8255
+ return { status: "builtin", command };
8256
+ }
8257
+ const { entries, conflicts } = await collectEntries(workspace);
8258
+ const match = entries.find((entry) => entry.command.command === command);
8259
+ if (!match) {
8260
+ const related = conflicts.filter((conflict) => conflict.command === command);
8261
+ if (related.length) {
8262
+ return {
8263
+ status: "conflict",
8264
+ command,
8265
+ message: related.map((conflict) => conflict.message).join("\n"),
8266
+ conflicts: related
8267
+ };
8268
+ }
8269
+ return {
8270
+ status: "unknown",
8271
+ command,
8272
+ available: entries.map((entry) => entry.command.command)
8273
+ };
8274
+ }
8275
+ return {
8276
+ status: "ok",
8277
+ command: match.command,
8278
+ skill: match.skill,
8279
+ args: parsed.args,
8280
+ prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
8281
+ };
8282
+ }
8283
+ function formatSkillCommandHelp(index) {
8284
+ if (!index.commands.length) return "";
8285
+ const lines = index.commands.map((command) => {
8286
+ const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
8287
+ const description = command.description || `Run the ${command.skillName} skill`;
8288
+ return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
8289
+ });
8290
+ return ["Skill commands:", ...lines].join("\n");
8291
+ }
8292
+
8085
8293
  // src/tools/workspace/skill-tools.ts
8086
8294
  var SkillInput = z17.object({
8087
8295
  name: z17.string().min(1).describe("Name of the skill to load, as shown in the skills list.")
@@ -8128,6 +8336,26 @@ var listSkillsTool = {
8128
8336
  };
8129
8337
  }
8130
8338
  };
8339
+ var CreateSkillInput = z17.object({
8340
+ name: z17.string().min(1).max(80),
8341
+ description: z17.string().trim().min(1).max(512),
8342
+ body: z17.string().trim().min(1).max(6e4),
8343
+ command: z17.string().min(1).max(64).optional(),
8344
+ argumentHint: z17.string().max(256).optional()
8345
+ });
8346
+ var createSkillTool = {
8347
+ name: "create_skill",
8348
+ description: "Create a project SKILL.md with instructions; never overwrites. Trust is required before loading project skills.",
8349
+ permissions: ["write"],
8350
+ inputSchema: CreateSkillInput,
8351
+ async execute(input, context) {
8352
+ const command = input.command ? normalizeCommandName(input.command) : void 0;
8353
+ if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is reserved.`);
8354
+ if (input.argumentHint && (!command || /[\r\n]/.test(input.argumentHint))) throw new Error("Argument hint requires a command and a single line.");
8355
+ const skill = await createSkill(context.workspace, { ...input, command, scope: "project" });
8356
+ return { changed: true, content: `Created ${skill.name}: ${skill.path}. Project skills require workspace trust before use.`, data: { name: skill.name, path: skill.path } };
8357
+ }
8358
+ };
8131
8359
 
8132
8360
  // src/tools/workspace/todo-tools.ts
8133
8361
  import { z as z18 } from "zod";
@@ -8568,7 +8796,7 @@ function summarizeDirectories(files) {
8568
8796
  const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
8569
8797
  counts.set(directory, (counts.get(directory) ?? 0) + 1);
8570
8798
  }
8571
- return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8799
+ return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8572
8800
  }
8573
8801
  function summarizeExtensions(files) {
8574
8802
  const summaries = /* @__PURE__ */ new Map();
@@ -11351,8 +11579,8 @@ function tokenizeCommand(command) {
11351
11579
  function renderCommandLine(plan) {
11352
11580
  return [plan.command, ...plan.args].join(" ");
11353
11581
  }
11354
- function parseTestOutput(runner, stdout6, stderr, workspace) {
11355
- const combined = [stdout6, stderr].filter(Boolean).join("\n");
11582
+ function parseTestOutput(runner, stdout6, stderr2, workspace) {
11583
+ const combined = [stdout6, stderr2].filter(Boolean).join("\n");
11356
11584
  if (runner === "vitest" || runner === "jest") {
11357
11585
  const report = extractJestStyleReport(stdout6) ?? extractJestStyleReport(combined);
11358
11586
  if (report) return parseJestStyleReport(report, workspace);
@@ -12798,64 +13026,67 @@ function decodeCodePoint(value, fallback) {
12798
13026
  // src/tools/workspace.ts
12799
13027
  function createDefaultToolRegistry() {
12800
13028
  const { todoWriteTool, todoReadTool } = createTodoTools();
12801
- return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
13029
+ return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(createSkillTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
12802
13030
  }
12803
13031
 
12804
13032
  // src/commands/doctor.ts
12805
13033
  function registerDoctorCommand(program2) {
12806
13034
  program2.command("doctor").description("Check the local ultrac setup").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).action(async (options) => {
12807
- const workspace = resolve7(options.workspace);
12808
- const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
12809
- const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
12810
- const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
12811
- const config = await loadConfig(workspace).catch(() => null);
12812
- const configExists = await hasConfig(workspace);
12813
- const isGitRepo = await pathExists(resolve7(workspace, ".git"));
12814
- const ultracCredentials = await resolveUltracCredentials();
12815
- const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
12816
- const tools = createDefaultToolRegistry().list();
12817
- const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
12818
- const checks = [
12819
- { label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
12820
- {
12821
- label: "workspace",
12822
- value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
12823
- ok: workspaceReady
12824
- },
12825
- { label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
12826
- {
12827
- label: "config",
12828
- value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
12829
- ok: config !== null
12830
- },
12831
- {
12832
- label: "tool schemas",
12833
- value: `${tools.length} exported`,
12834
- ok: toolSchemasReady
12835
- },
12836
- { label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
12837
- { label: "model", value: config?.model ?? "unavailable", ok: config !== null },
12838
- {
12839
- label: "Ultrac sign-in",
12840
- value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
12841
- ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
12842
- },
12843
- {
12844
- label: "Ultrac API base URL",
12845
- value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
12846
- ok: true
12847
- }
12848
- ];
12849
- if (options.json) {
12850
- console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
12851
- } else {
12852
- checks.forEach((check) => printCheck(check));
12853
- }
12854
- if (checks.some((check) => !check.ok)) {
12855
- process.exitCode = 1;
12856
- }
13035
+ await runDoctor(options.workspace, options.json);
12857
13036
  });
12858
13037
  }
13038
+ async function runDoctor(workspacePath, json = false, setExitCode = true) {
13039
+ const workspace = resolve7(workspacePath);
13040
+ const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
13041
+ const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
13042
+ const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
13043
+ const config = await loadConfig(workspace).catch(() => null);
13044
+ const configExists = await hasConfig(workspace);
13045
+ const isGitRepo = await pathExists(resolve7(workspace, ".git"));
13046
+ const ultracCredentials = await resolveUltracCredentials();
13047
+ const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
13048
+ const tools = createDefaultToolRegistry().list();
13049
+ const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
13050
+ const checks = [
13051
+ { label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
13052
+ {
13053
+ label: "workspace",
13054
+ value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
13055
+ ok: workspaceReady
13056
+ },
13057
+ { label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
13058
+ {
13059
+ label: "config",
13060
+ value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
13061
+ ok: config !== null
13062
+ },
13063
+ {
13064
+ label: "tool schemas",
13065
+ value: `${tools.length} exported`,
13066
+ ok: toolSchemasReady
13067
+ },
13068
+ { label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
13069
+ { label: "model", value: config?.model ?? "unavailable", ok: config !== null },
13070
+ {
13071
+ label: "Ultrac sign-in",
13072
+ value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
13073
+ ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
13074
+ },
13075
+ {
13076
+ label: "Ultrac API base URL",
13077
+ value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
13078
+ ok: true
13079
+ }
13080
+ ];
13081
+ if (json) {
13082
+ console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
13083
+ } else {
13084
+ checks.forEach((check) => printCheck(check));
13085
+ }
13086
+ if (setExitCode && checks.some((check) => !check.ok)) {
13087
+ process.exitCode = 1;
13088
+ }
13089
+ }
12859
13090
  async function pathExists(path) {
12860
13091
  try {
12861
13092
  await access2(path);
@@ -15277,7 +15508,7 @@ function registerNotifyCommand(program2) {
15277
15508
 
15278
15509
  // src/commands/repl.ts
15279
15510
  import { createInterface as createInterface3, emitKeypressEvents } from "readline";
15280
- import { stdin as stdin5, stdout as stdout5 } from "process";
15511
+ import { stdin as stdin5, stdout as stdout5, stderr } from "process";
15281
15512
  import chalk11 from "chalk";
15282
15513
 
15283
15514
  // src/config/budget.ts
@@ -16633,22 +16864,168 @@ function noteNativeCompactionFallback(reason) {
16633
16864
  );
16634
16865
  }
16635
16866
 
16636
- // src/commands/session-checkpoint.ts
16637
- var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
16638
- function createSessionCheckpointWriter(workspace, onFailure) {
16639
- let failureReported = false;
16640
- const warnFailure = () => {
16641
- if (failureReported) return;
16642
- failureReported = true;
16643
- onFailure(SESSION_CHECKPOINT_FAILURE_WARNING);
16644
- };
16645
- const writeCheckpoint = async (session) => {
16646
- const trace = startDiagnosticSpan("session.checkpoint", {
16647
- sessionId: session.id,
16648
- stepCount: session.steps.length
16649
- }, 5e3);
16650
- try {
16651
- const path = await checkpointSession(workspace, session);
16867
+ // src/core/autonomous.ts
16868
+ import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
16869
+ var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
16870
+ var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
16871
+ var AUTONOMOUS_MAX_STEPS = 200;
16872
+ var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
16873
+ var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
16874
+ var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
16875
+ var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
16876
+ function shouldResume(stopReason) {
16877
+ return stopReason === "max-steps" || stopReason === "error";
16878
+ }
16879
+ function boundedInteger(value, name, min, max) {
16880
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
16881
+ return value;
16882
+ }
16883
+ function abortableSleep(milliseconds, signal) {
16884
+ signal.throwIfAborted();
16885
+ return new Promise((resolve15, reject) => {
16886
+ const timer = setTimeout(() => {
16887
+ signal.removeEventListener("abort", abort);
16888
+ resolve15();
16889
+ }, milliseconds);
16890
+ const abort = () => {
16891
+ clearTimeout(timer);
16892
+ signal.removeEventListener("abort", abort);
16893
+ reject(signal.reason);
16894
+ };
16895
+ signal.addEventListener("abort", abort, { once: true });
16896
+ if (signal.aborted) abort();
16897
+ });
16898
+ }
16899
+ function progress(session, startStep = 0) {
16900
+ const hash = createHash7("sha256");
16901
+ let completedTools = 0;
16902
+ hash.update(session.finalMessage ?? "");
16903
+ for (const step of (session.steps ?? []).slice(startStep)) {
16904
+ hash.update(step.modelText ?? "");
16905
+ for (const tool of step.toolResults ?? []) {
16906
+ completedTools += 1;
16907
+ hash.update(tool.call.tool);
16908
+ hash.update(tool.result.ok ? "ok" : "failed");
16909
+ hash.update(tool.result.content ?? "");
16910
+ try {
16911
+ hash.update(JSON.stringify(tool.call.input));
16912
+ } catch {
16913
+ hash.update("unserializable-input");
16914
+ }
16915
+ }
16916
+ }
16917
+ return { fingerprint: hash.digest("hex"), completedTools };
16918
+ }
16919
+ async function runAutonomousLoop(options) {
16920
+ boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
16921
+ const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
16922
+ const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
16923
+ const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
16924
+ const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
16925
+ const now = options.now ?? (() => performance.now());
16926
+ const started = now();
16927
+ const elapsed = () => Math.max(0, now() - started);
16928
+ const budget = new AbortController();
16929
+ const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
16930
+ deadline.unref?.();
16931
+ const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
16932
+ const sleep2 = options.sleep ?? abortableSleep;
16933
+ const id = randomUUID5();
16934
+ let previous;
16935
+ let lastFingerprint;
16936
+ let lastSessionId;
16937
+ let lastStepCount = 0;
16938
+ let stalls = 0;
16939
+ let consecutiveErrors = 0;
16940
+ try {
16941
+ signal.throwIfAborted();
16942
+ for (let round = 1; ; round += 1) {
16943
+ const roundStart = now();
16944
+ diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
16945
+ let session;
16946
+ try {
16947
+ session = await options.runRound(round, previous, signal);
16948
+ } catch (error) {
16949
+ diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
16950
+ throw error;
16951
+ }
16952
+ const stepCount = session.steps?.length ?? 0;
16953
+ const startStep = session.id === lastSessionId && stepCount > lastStepCount ? lastStepCount : 0;
16954
+ const current = progress(session, startStep);
16955
+ lastSessionId = session.id;
16956
+ lastStepCount = stepCount;
16957
+ const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
16958
+ stalls = unchanged ? stalls + 1 : 0;
16959
+ lastFingerprint = current.fingerprint;
16960
+ const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
16961
+ consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
16962
+ let haltReason;
16963
+ if (session.stopReason === "completed") haltReason = "completed";
16964
+ else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
16965
+ else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
16966
+ else if (classification && !classification.retryable) haltReason = "permanent_error";
16967
+ else if (!shouldResume(session.stopReason)) haltReason = "stopped";
16968
+ else if (stalls >= stallLimit) haltReason = "stalled";
16969
+ else if (round >= options.maxRounds) haltReason = "max_rounds";
16970
+ const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
16971
+ if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
16972
+ diagnosticEvent("round.end", {
16973
+ id,
16974
+ round,
16975
+ sessionId: session.id,
16976
+ reason: haltReason ?? session.stopReason,
16977
+ failureKind: classification?.kind,
16978
+ attempt: consecutiveErrors,
16979
+ completedTools: current.completedTools,
16980
+ elapsedMs: elapsed(),
16981
+ durationMs: Math.max(0, now() - roundStart)
16982
+ });
16983
+ if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
16984
+ await options.onRoundEnd?.(round, session, !haltReason);
16985
+ if (haltReason) return { session, rounds: round, haltReason };
16986
+ if (session.stopReason === "error") {
16987
+ const delayMs = retryDelay;
16988
+ if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
16989
+ diagnosticEvent("round.retry", {
16990
+ id,
16991
+ round,
16992
+ nextRound: round + 1,
16993
+ attempt: consecutiveErrors,
16994
+ failureKind: classification?.kind ?? "unclassified",
16995
+ delayMs,
16996
+ elapsedMs: elapsed()
16997
+ });
16998
+ try {
16999
+ await sleep2(delayMs, signal);
17000
+ } catch (error) {
17001
+ if (!signal.aborted) throw error;
17002
+ return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
17003
+ }
17004
+ }
17005
+ if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
17006
+ previous = session;
17007
+ }
17008
+ } finally {
17009
+ clearTimeout(deadline);
17010
+ }
17011
+ }
17012
+
17013
+ // src/commands/session-checkpoint.ts
17014
+ var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
17015
+ function createSessionCheckpointWriter(workspace, onFailure) {
17016
+ let failureReported = false;
17017
+ const warnFailure = () => {
17018
+ if (failureReported) return;
17019
+ failureReported = true;
17020
+ onFailure(SESSION_CHECKPOINT_FAILURE_WARNING);
17021
+ };
17022
+ const writeCheckpoint = async (session) => {
17023
+ const trace = startDiagnosticSpan("session.checkpoint", {
17024
+ sessionId: session.id,
17025
+ stepCount: session.steps.length
17026
+ }, 5e3);
17027
+ try {
17028
+ const path = await checkpointSession(workspace, session);
16652
17029
  if (!path) warnFailure();
16653
17030
  trace.end({ outcome: path ? "ok" : "error", path });
16654
17031
  return path;
@@ -16690,7 +17067,7 @@ function printSession(session, sessionPath2, options = {}) {
16690
17067
  }
16691
17068
  for (const toolResult of step.toolResults) {
16692
17069
  const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
16693
- console.log(chalk8.cyan(toolResult.call.tool), status);
17070
+ console.log(chalk8.white(toolResult.call.tool), status);
16694
17071
  console.log(chalk8.gray(indent2(formatToolOutput(toolResult.result.content, { verbose: options.verboseTools, failed: !toolResult.result.ok, hint: "Use --verbose-tools or inspect the saved session for full output." }))));
16695
17072
  }
16696
17073
  }
@@ -16710,13 +17087,13 @@ function printTools(tools) {
16710
17087
  for (const tool of tools) {
16711
17088
  const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
16712
17089
  const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
16713
- console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
17090
+ console.log(`${chalk8.white(tool.name)}${permissionText}${optionalText}`);
16714
17091
  console.log(chalk8.gray(indent2(tool.description)));
16715
17092
  }
16716
17093
  }
16717
17094
  function printToolResult(result, options = {}) {
16718
17095
  const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
16719
- console.log(`${chalk8.cyan(result.tool)} ${status}`);
17096
+ console.log(`${chalk8.white(result.tool)} ${status}`);
16720
17097
  console.log(chalk8.gray(indent2(formatToolOutput(result.content, { verbose: options.verboseTools, failed: !result.ok, hint: "Use --verbose-tools or --result-json for full output." }))));
16721
17098
  }
16722
17099
  function indent2(value) {
@@ -16734,207 +17111,6 @@ function formatToolOutput(content, options = {}) {
16734
17111
  \u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
16735
17112
  }
16736
17113
 
16737
- // src/skills/commands.ts
16738
- var BUILTIN_SLASH_COMMANDS = [
16739
- "logs",
16740
- "ghost",
16741
- "output",
16742
- "usage",
16743
- "max-steps",
16744
- "resume",
16745
- "deny",
16746
- "plan",
16747
- "review",
16748
- "coding",
16749
- "status",
16750
- "context",
16751
- "compact",
16752
- "diff",
16753
- "sessions",
16754
- "save",
16755
- "test",
16756
- "init",
16757
- "new",
16758
- "allow",
16759
- "clear",
16760
- "exit",
16761
- "help",
16762
- "memory",
16763
- "mode",
16764
- "model",
16765
- "permissions",
16766
- "quit",
16767
- "skills",
16768
- "tools",
16769
- "whoami"
16770
- ];
16771
- var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16772
- var MAX_COMMAND_LENGTH = 64;
16773
- function parseSlashInput(input) {
16774
- const trimmed = input.trim();
16775
- if (!trimmed.startsWith("/")) return null;
16776
- const rest = trimmed.slice(1);
16777
- const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
16778
- if (!match) return null;
16779
- return { command: match[1], args: (match[2] ?? "").trim() };
16780
- }
16781
- function isBuiltinSlashCommand(command) {
16782
- return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
16783
- }
16784
- function normalizeCommandName(value) {
16785
- const candidate = value.trim().replace(/^\/+/, "").trim();
16786
- if (!candidate) {
16787
- throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
16788
- }
16789
- if (candidate.length > MAX_COMMAND_LENGTH) {
16790
- throw new Error(
16791
- `Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
16792
- );
16793
- }
16794
- if (!COMMAND_NAME_RE.test(candidate)) {
16795
- throw new Error(
16796
- `Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
16797
- );
16798
- }
16799
- return candidate;
16800
- }
16801
- async function collectEntries(workspace) {
16802
- const skills = await discoverSkills(workspace);
16803
- const claimed = /* @__PURE__ */ new Map();
16804
- const conflicts = [];
16805
- for (const skill of skills) {
16806
- let declaration;
16807
- try {
16808
- declaration = skill.command ?? null;
16809
- } catch (error) {
16810
- conflicts.push({
16811
- command: "",
16812
- skillName: skill.name,
16813
- scope: skill.scope,
16814
- path: skill.path,
16815
- reason: "invalid",
16816
- message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
16817
- });
16818
- continue;
16819
- }
16820
- if (!declaration) continue;
16821
- let command;
16822
- try {
16823
- command = normalizeCommandName(declaration.raw);
16824
- } catch (error) {
16825
- conflicts.push({
16826
- command: declaration.raw,
16827
- skillName: skill.name,
16828
- scope: skill.scope,
16829
- path: skill.path,
16830
- reason: "invalid",
16831
- message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
16832
- });
16833
- continue;
16834
- }
16835
- if (isBuiltinSlashCommand(command)) {
16836
- conflicts.push({
16837
- command,
16838
- skillName: skill.name,
16839
- scope: skill.scope,
16840
- path: skill.path,
16841
- reason: "builtin",
16842
- message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
16843
- });
16844
- continue;
16845
- }
16846
- const entry = {
16847
- skill,
16848
- command: {
16849
- command,
16850
- skillName: skill.name,
16851
- description: skill.description,
16852
- argumentHint: declaration.argumentHint,
16853
- scope: skill.scope,
16854
- path: skill.path
16855
- }
16856
- };
16857
- const existing = claimed.get(command);
16858
- if (!existing) {
16859
- claimed.set(command, entry);
16860
- continue;
16861
- }
16862
- const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
16863
- const loser = winner === entry ? existing : entry;
16864
- claimed.set(command, winner);
16865
- conflicts.push({
16866
- command,
16867
- skillName: loser.skill.name,
16868
- scope: loser.skill.scope,
16869
- path: loser.skill.path,
16870
- reason: "duplicate",
16871
- message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
16872
- });
16873
- }
16874
- const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
16875
- return { entries, conflicts };
16876
- }
16877
- async function listSkillCommands(workspace) {
16878
- const { entries, conflicts } = await collectEntries(workspace);
16879
- return { commands: entries.map((entry) => entry.command), conflicts };
16880
- }
16881
- function buildSkillCommandPrompt(skill, command, args) {
16882
- const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
16883
- const header = [
16884
- `The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
16885
- skill.description ? `Description: ${skill.description}` : void 0,
16886
- argumentLine,
16887
- skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
16888
- "Follow the instructions below now, applying the arguments where they are relevant."
16889
- ].filter(Boolean).join("\n");
16890
- return `${header}
16891
-
16892
- --- instructions ---
16893
- ${skill.body}`;
16894
- }
16895
- async function resolveSkillCommand(workspace, input) {
16896
- const parsed = parseSlashInput(input);
16897
- if (!parsed) return { status: "not-a-command" };
16898
- const command = parsed.command.toLowerCase();
16899
- if (isBuiltinSlashCommand(command)) {
16900
- return { status: "builtin", command };
16901
- }
16902
- const { entries, conflicts } = await collectEntries(workspace);
16903
- const match = entries.find((entry) => entry.command.command === command);
16904
- if (!match) {
16905
- const related = conflicts.filter((conflict) => conflict.command === command);
16906
- if (related.length) {
16907
- return {
16908
- status: "conflict",
16909
- command,
16910
- message: related.map((conflict) => conflict.message).join("\n"),
16911
- conflicts: related
16912
- };
16913
- }
16914
- return {
16915
- status: "unknown",
16916
- command,
16917
- available: entries.map((entry) => entry.command.command)
16918
- };
16919
- }
16920
- return {
16921
- status: "ok",
16922
- command: match.command,
16923
- skill: match.skill,
16924
- args: parsed.args,
16925
- prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
16926
- };
16927
- }
16928
- function formatSkillCommandHelp(index) {
16929
- if (!index.commands.length) return "";
16930
- const lines = index.commands.map((command) => {
16931
- const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
16932
- const description = command.description || `Run the ${command.skillName} skill`;
16933
- return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
16934
- });
16935
- return ["Skill commands:", ...lines].join("\n");
16936
- }
16937
-
16938
17114
  // src/ui/progress.ts
16939
17115
  import { stdout as stdout3 } from "process";
16940
17116
  import chalk9 from "chalk";
@@ -16964,12 +17140,12 @@ function formatElapsed(ms) {
16964
17140
  }
16965
17141
  function formatProgressLine(snapshot) {
16966
17142
  const finished = snapshot.done >= snapshot.total;
16967
- const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
17143
+ const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.white(PENDING_GLYPH);
16968
17144
  const noun = snapshot.total === 1 ? "agent" : "agents";
16969
17145
  const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
16970
17146
  if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
16971
17147
  const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
16972
- return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
17148
+ return ` ${glyph} ${chalk9.white(snapshot.label)} ${snapshot.description} ${rendered}`;
16973
17149
  }
16974
17150
  var ProgressLine = class {
16975
17151
  constructor(label, description, total) {
@@ -17203,15 +17379,691 @@ function compact2(value, max = 64) {
17203
17379
  if (!value) return "";
17204
17380
  return truncate2(value.replace(/\s+/g, " "), max);
17205
17381
  }
17206
- function quote(value, max = 48) {
17207
- return value ? `\u201C${compact2(value, max)}\u201D` : "";
17382
+ function quote(value, max = 48) {
17383
+ return value ? `\u201C${compact2(value, max)}\u201D` : "";
17384
+ }
17385
+ function truncate2(value, max) {
17386
+ return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
17387
+ }
17388
+ function humanize(value) {
17389
+ const words = value.replace(/[_-]+/g, " ").trim();
17390
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
17391
+ }
17392
+
17393
+ // src/ui/interactive-choice.ts
17394
+ var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17395
+ var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
17396
+ function displayLabel(value, width2) {
17397
+ const clean = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17398
+ let result = "";
17399
+ let used = 0;
17400
+ for (const character of clean) {
17401
+ const size = /[\p{Mark}\u200d\ufe0e\ufe0f]/u.test(character) ? 0 : character.codePointAt(0) < 128 ? 1 : 2;
17402
+ if (used + size > width2 - 1) return `${result}\u2026`;
17403
+ result += character;
17404
+ used += size;
17405
+ }
17406
+ return result;
17407
+ }
17408
+ function attachInteractiveChoices(readline, options, settings = {}) {
17409
+ const rl = readline;
17410
+ const output = settings.output ?? rl.output;
17411
+ const prompt = settings.prompt ?? " \u203A ";
17412
+ let resolveReady;
17413
+ const ready = new Promise((resolve15) => {
17414
+ resolveReady = resolve15;
17415
+ });
17416
+ const original = rl._ttyWrite;
17417
+ const priorPrompt = rl.getPrompt();
17418
+ const priorLine = rl.line;
17419
+ const priorCursor = rl.cursor;
17420
+ const priorHistory = rl.history?.slice();
17421
+ let injected = false;
17422
+ let selected = 0;
17423
+ let disposed = false;
17424
+ let mouse = false;
17425
+ let optionRow = null;
17426
+ let reportPending = false;
17427
+ let reportTimer;
17428
+ let sequenceTimer;
17429
+ let sequence = "";
17430
+ const maxRow = () => settings.maxRow?.() ?? output?.rows ?? 1e4;
17431
+ const columns = () => Math.max(16, output?.columns ?? 80);
17432
+ const write = (text) => {
17433
+ output?.write(text);
17434
+ };
17435
+ const labels = () => options.map((option, index) => {
17436
+ const prefix = ` ${selected === index ? "\u203A" : " "} ${index + 1}. `;
17437
+ const text = `${prefix}${displayLabel(option, columns() - 9)}`;
17438
+ return selected === index ? `\x1B[7m${text}\x1B[0m` : text;
17439
+ });
17440
+ const restoreLine = () => {
17441
+ if (!original || !rl.terminal || rl.closed) return;
17442
+ if (rl.line) {
17443
+ original.call(rl, void 0, { name: "end" });
17444
+ original.call(rl, void 0, { name: "u", ctrl: true });
17445
+ }
17446
+ if (priorLine) {
17447
+ original.call(rl, priorLine, {});
17448
+ for (const _character of Array.from(priorLine.slice(priorCursor))) original.call(rl, void 0, { name: "left" });
17449
+ }
17450
+ };
17451
+ const cleanup = () => {
17452
+ if (disposed) return;
17453
+ disposed = true;
17454
+ clearTimeout(reportTimer);
17455
+ clearTimeout(sequenceTimer);
17456
+ if (rl._ttyWrite === intercept) rl._ttyWrite = original;
17457
+ rl.off("line", onLine);
17458
+ rl.off("close", cleanup);
17459
+ output?.off("resize", onResize);
17460
+ output?.off("error", cleanup);
17461
+ settings.signal?.removeEventListener("abort", cleanup);
17462
+ try {
17463
+ if (mouse) write(MOUSE_OFF);
17464
+ } catch {
17465
+ }
17466
+ mouse = false;
17467
+ rl.setPrompt(priorPrompt);
17468
+ if (injected && priorHistory && rl.history) rl.history.splice(0, rl.history.length, ...priorHistory);
17469
+ try {
17470
+ restoreLine();
17471
+ } catch {
17472
+ }
17473
+ resolveReady(false);
17474
+ };
17475
+ function onLine() {
17476
+ cleanup();
17477
+ }
17478
+ function redraw() {
17479
+ if (disposed || rl.line || options.length + 2 > maxRow()) return;
17480
+ write("\x1B7");
17481
+ if (optionRow !== null) write(`\x1B[${optionRow};1H`);
17482
+ else write(`\x1B[${options.length + 1}A\r`);
17483
+ for (const label of labels()) write(`\x1B[2K${label}
17484
+ \r`);
17485
+ write("\x1B8");
17486
+ }
17487
+ function choose(index) {
17488
+ if (disposed || !original || index < 0 || index >= options.length) return;
17489
+ selected = index;
17490
+ injected = true;
17491
+ if (rl.line) {
17492
+ original.call(rl, void 0, { name: "end" });
17493
+ original.call(rl, void 0, { name: "u", ctrl: true });
17494
+ }
17495
+ original.call(rl, String(index + 1), {});
17496
+ original.call(rl, "\r", { name: "return", sequence: "\r" });
17497
+ }
17498
+ function requestPosition() {
17499
+ if (disposed) return;
17500
+ optionRow = null;
17501
+ reportPending = true;
17502
+ clearTimeout(reportTimer);
17503
+ write("\x1B[6n");
17504
+ reportTimer = setTimeout(() => {
17505
+ reportPending = false;
17506
+ resolveReady(false);
17507
+ }, Math.max(1, settings.cursorReportTimeoutMs ?? 250));
17508
+ }
17509
+ function onResize() {
17510
+ if (disposed) return;
17511
+ if (mouse) write(MOUSE_OFF);
17512
+ mouse = false;
17513
+ optionRow = null;
17514
+ requestPosition();
17515
+ }
17516
+ function consumeControl(value) {
17517
+ const position = /^\x1b\[(\d+);(\d+)R$/.exec(value);
17518
+ if (position) {
17519
+ const row = Number(position[1]);
17520
+ const column = Number(position[2]);
17521
+ if (reportPending && row > options.length + 1 && row <= maxRow() && column >= 1 && column <= columns()) {
17522
+ optionRow = row - options.length - 1;
17523
+ reportPending = false;
17524
+ clearTimeout(reportTimer);
17525
+ write(MOUSE_ON);
17526
+ mouse = true;
17527
+ resolveReady(true);
17528
+ }
17529
+ return true;
17530
+ }
17531
+ const click = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(value);
17532
+ if (click) {
17533
+ const button = Number(click[1]);
17534
+ const x = Number(click[2]);
17535
+ const y = Number(click[3]);
17536
+ if (mouse && optionRow !== null && click[4] === "M" && button < 32 && (button & 3) === 0 && x >= 1 && x <= columns() && y <= maxRow()) choose(y - optionRow);
17537
+ return true;
17538
+ }
17539
+ return false;
17540
+ }
17541
+ function intercept(text, key) {
17542
+ if (disposed) {
17543
+ original?.call(rl, text, key);
17544
+ return;
17545
+ }
17546
+ const value = key?.sequence ?? text ?? "";
17547
+ if (sequence || value.startsWith("\x1B[<") || /^\x1b\[\d/.test(value)) {
17548
+ sequence += value;
17549
+ clearTimeout(sequenceTimer);
17550
+ if (consumeControl(sequence)) {
17551
+ sequence = "";
17552
+ return;
17553
+ }
17554
+ if (/^\x1b\[(?:<[0-9;]*|[0-9;]*)$/.test(sequence) && sequence.length < 64) {
17555
+ sequenceTimer = setTimeout(() => {
17556
+ sequence = "";
17557
+ }, 250);
17558
+ return;
17559
+ }
17560
+ const unrecognized = sequence;
17561
+ sequence = "";
17562
+ original?.call(rl, unrecognized, key);
17563
+ return;
17564
+ }
17565
+ if (!rl.line && (key?.name === "up" || key?.name === "down")) {
17566
+ selected = (selected + (key.name === "down" ? 1 : options.length - 1)) % options.length;
17567
+ redraw();
17568
+ return;
17569
+ }
17570
+ if (!rl.line && (key?.name === "return" || key?.name === "enter")) {
17571
+ choose(selected);
17572
+ return;
17573
+ }
17574
+ if (key?.ctrl && key.name === "c") cleanup();
17575
+ if (key?.ctrl && key.name === "d" && !rl.line) {
17576
+ original?.call(rl, text, key);
17577
+ cleanup();
17578
+ return;
17579
+ }
17580
+ original?.call(rl, text, key);
17581
+ }
17582
+ if (settings.signal?.aborted) {
17583
+ disposed = true;
17584
+ resolveReady(false);
17585
+ return { cleanup, ready };
17586
+ }
17587
+ if (!output || !rl.terminal || !output.isTTY || process.env.TERM === "dumb" || !original || !options.length) {
17588
+ options.forEach((option, index) => write(` ${index + 1}. ${displayLabel(option, columns() - 6)}
17589
+ `));
17590
+ write(prompt);
17591
+ resolveReady(false);
17592
+ disposed = true;
17593
+ return { cleanup, ready };
17594
+ }
17595
+ try {
17596
+ if (priorLine) {
17597
+ original.call(rl, void 0, { name: "end" });
17598
+ original.call(rl, void 0, { name: "u", ctrl: true });
17599
+ }
17600
+ rl.setPrompt(prompt);
17601
+ rl._ttyWrite = intercept;
17602
+ rl.once("line", onLine);
17603
+ rl.once("close", cleanup);
17604
+ output.on("resize", onResize);
17605
+ output.on("error", cleanup);
17606
+ settings.signal?.addEventListener("abort", cleanup, { once: true });
17607
+ for (const label of labels()) write(`${label}\r
17608
+ `);
17609
+ write(" \u2191/\u2193 and Enter, a number, or your own answer.\r\n");
17610
+ rl.prompt();
17611
+ requestPosition();
17612
+ } catch (error) {
17613
+ cleanup();
17614
+ throw error;
17615
+ }
17616
+ return { cleanup, ready };
17617
+ }
17618
+
17619
+ // src/ui/slash-completion.ts
17620
+ function attachSlashCompletion(readline, loadEntries, render, initialEntries = []) {
17621
+ const rl = readline;
17622
+ const original = rl._ttyWrite;
17623
+ let entries = [...initialEntries];
17624
+ let matches = [];
17625
+ let selected = 0;
17626
+ let disposed = false;
17627
+ let suppressed = false;
17628
+ let opened = false;
17629
+ let epoch = 0;
17630
+ let handledEscape = false;
17631
+ const clear = () => {
17632
+ matches = [];
17633
+ selected = 0;
17634
+ render([], 0);
17635
+ };
17636
+ const refresh = () => {
17637
+ if (disposed || suppressed) return;
17638
+ const line = rl.line;
17639
+ const skillPrefix = /^\/skill\s+([a-z0-9-]*)$/i.exec(line);
17640
+ if (!/^\/[a-z0-9-]*$/i.test(line) && !skillPrefix) {
17641
+ opened = false;
17642
+ epoch += 1;
17643
+ clear();
17644
+ return;
17645
+ }
17646
+ if (!opened) {
17647
+ opened = true;
17648
+ const request = ++epoch;
17649
+ void loadEntries().then((items) => {
17650
+ if (disposed || request !== epoch || !opened) return;
17651
+ entries = items;
17652
+ refresh();
17653
+ }).catch(() => {
17654
+ if (!disposed && request === epoch) clear();
17655
+ });
17656
+ }
17657
+ const query = (skillPrefix?.[1] ?? line.slice(1)).toLowerCase();
17658
+ matches = entries.filter((entry) => (!skillPrefix || entry.skill) && (entry.text.toLowerCase().includes(query) || entry.label.toLowerCase().includes(query)));
17659
+ selected = Math.min(selected, Math.max(0, matches.length - 1));
17660
+ render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
17661
+ };
17662
+ const wrapper = function(text, key) {
17663
+ if (!suppressed && matches.length) {
17664
+ if (key?.name === "escape") {
17665
+ handledEscape = true;
17666
+ setTimeout(() => {
17667
+ handledEscape = false;
17668
+ }, 0);
17669
+ opened = false;
17670
+ epoch += 1;
17671
+ clear();
17672
+ return;
17673
+ }
17674
+ if (key?.name === "up" || key?.name === "down") {
17675
+ selected = (selected + (key.name === "up" ? -1 : 1) + matches.length) % matches.length;
17676
+ render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
17677
+ return;
17678
+ }
17679
+ if (key?.name === "tab" || key?.name === "return" || key?.name === "enter") {
17680
+ const choice = matches[selected];
17681
+ suppressed = true;
17682
+ try {
17683
+ rl.write(null, { ctrl: true, name: "u" });
17684
+ rl.write(`${choice.text} `);
17685
+ } finally {
17686
+ suppressed = false;
17687
+ opened = false;
17688
+ epoch += 1;
17689
+ clear();
17690
+ }
17691
+ return;
17692
+ }
17693
+ }
17694
+ original.call(this, text, key);
17695
+ refresh();
17696
+ };
17697
+ rl._ttyWrite = wrapper;
17698
+ return {
17699
+ handledEscape: () => handledEscape,
17700
+ suspend: () => {
17701
+ suppressed = true;
17702
+ opened = false;
17703
+ epoch += 1;
17704
+ clear();
17705
+ },
17706
+ resume: () => {
17707
+ suppressed = false;
17708
+ refresh();
17709
+ },
17710
+ cleanup: () => {
17711
+ disposed = true;
17712
+ epoch += 1;
17713
+ clear();
17714
+ if (rl._ttyWrite === wrapper) rl._ttyWrite = original;
17715
+ }
17716
+ };
17717
+ }
17718
+
17719
+ // src/ui/terminal-layout.ts
17720
+ import { Writable } from "stream";
17721
+ import { stripVTControlCharacters } from "util";
17722
+ var CSI = "\x1B[";
17723
+ var RESET_REGION = `${CSI}r`;
17724
+ var MOUSE_OFF2 = `${CSI}?1000l${CSI}?1002l${CSI}?1003l${CSI}?1006l`;
17725
+ var BASE_FOOTER_ROWS = 5;
17726
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
17727
+ var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17728
+ const code = segment.codePointAt(0) ?? 0;
17729
+ const wide = /\p{Extended_Pictographic}/u.test(segment) || code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072);
17730
+ return { text: segment, width: /^\p{Mark}+$/u.test(segment) ? 0 : wide ? 2 : 1 };
17731
+ });
17732
+ var width = (text) => cells(text).reduce((sum, cell) => sum + cell.width, 0);
17733
+ function plain(value) {
17734
+ return stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, " ");
17208
17735
  }
17209
- function truncate2(value, max) {
17210
- return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
17736
+ function fit(text, columns) {
17737
+ if (columns <= 0) return "";
17738
+ let result = "";
17739
+ let used = 0;
17740
+ for (const cell of cells(plain(text))) {
17741
+ if (used + cell.width > columns) break;
17742
+ result += cell.text;
17743
+ used += cell.width;
17744
+ }
17745
+ return result;
17211
17746
  }
17212
- function humanize(value) {
17213
- const words = value.replace(/[_-]+/g, " ").trim();
17214
- return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
17747
+ var cup = (row, column = 1) => `${CSI}${row};${column}H`;
17748
+ var count3 = (value) => value !== void 0 && Number.isFinite(value) && value >= 0 ? Math.floor(value).toLocaleString("en-US") : "unknown";
17749
+ var ComposerOutput = class extends Writable {
17750
+ constructor(layout) {
17751
+ super();
17752
+ this.layout = layout;
17753
+ }
17754
+ layout;
17755
+ get isTTY() {
17756
+ return this.layout.enabled;
17757
+ }
17758
+ get columns() {
17759
+ return this.layout.columns;
17760
+ }
17761
+ get rows() {
17762
+ return this.layout.rows;
17763
+ }
17764
+ _write(chunk, _encoding, callback) {
17765
+ try {
17766
+ this.layout.writeComposer(chunk);
17767
+ callback();
17768
+ } catch (error) {
17769
+ callback(error instanceof Error ? error : new Error("Terminal output failed"));
17770
+ }
17771
+ }
17772
+ };
17773
+ var TerminalLayout = class {
17774
+ outputProxy;
17775
+ enabled;
17776
+ output;
17777
+ errorOutput;
17778
+ originalWrite;
17779
+ originalErrorWrite;
17780
+ rawWrite;
17781
+ details;
17782
+ rl;
17783
+ prompt = "\u203A ";
17784
+ transcriptRow = 1;
17785
+ transcriptColumn = 1;
17786
+ wrapPending = false;
17787
+ savedTranscript;
17788
+ lastFooterTop = 0;
17789
+ suggestions = [];
17790
+ selected = 0;
17791
+ installed = false;
17792
+ closed = false;
17793
+ suspended = 0;
17794
+ paintQueued = false;
17795
+ bindSignals;
17796
+ resize = () => {
17797
+ if (this.closed || !this.enabled) return;
17798
+ this.clearPreviousFooter();
17799
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
17800
+ this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
17801
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
17802
+ this.outputProxy.emit("resize");
17803
+ this.paintFooter();
17804
+ };
17805
+ exit = () => {
17806
+ this.close();
17807
+ };
17808
+ constructor(options) {
17809
+ this.output = options.output;
17810
+ this.errorOutput = options.errorOutput;
17811
+ this.details = { ...options.details };
17812
+ this.bindSignals = options.bindSignals !== false;
17813
+ this.originalWrite = options.output.write;
17814
+ this.rawWrite = options.output.write.bind(options.output);
17815
+ this.originalErrorWrite = options.errorOutput?.write;
17816
+ this.enabled = Boolean(options.output.isTTY && (options.output.columns ?? 80) >= 12 && (options.output.rows ?? 24) >= 7);
17817
+ this.outputProxy = new ComposerOutput(this);
17818
+ }
17819
+ get columns() {
17820
+ return Math.max(12, this.output.columns ?? 80);
17821
+ }
17822
+ get rows() {
17823
+ return Math.max(7, this.output.rows ?? 24);
17824
+ }
17825
+ get suggestionRows() {
17826
+ return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
17827
+ }
17828
+ get contentRows() {
17829
+ return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
17830
+ }
17831
+ get inputRow() {
17832
+ return this.rows - 3;
17833
+ }
17834
+ install() {
17835
+ if (this.installed || this.closed || !this.enabled) return this;
17836
+ this.installed = true;
17837
+ this.output.write = ((chunk, encoding, callback) => this.writeTranscript(chunk, encoding, callback));
17838
+ if (this.errorOutput && this.errorOutput !== this.output) this.errorOutput.write = this.output.write;
17839
+ this.output.on("resize", this.resize);
17840
+ this.output.on("error", this.exit);
17841
+ if (this.errorOutput !== this.output) this.errorOutput?.on("error", this.exit);
17842
+ process.once("exit", this.exit);
17843
+ if (this.bindSignals) {
17844
+ process.once("SIGINT", this.exit);
17845
+ process.once("SIGTERM", this.exit);
17846
+ }
17847
+ this.rawWrite(`${CSI}2J${cup(1)}${CSI}1;${this.contentRows}r`);
17848
+ this.paintFooter();
17849
+ return this;
17850
+ }
17851
+ attachReadline(rl) {
17852
+ this.rl = rl;
17853
+ rl.setPrompt(this.prompt);
17854
+ this.paintFooter();
17855
+ }
17856
+ setPrompt(prompt) {
17857
+ this.prompt = prompt;
17858
+ this.rl?.setPrompt(prompt);
17859
+ this.paintFooter();
17860
+ }
17861
+ updateDetails(details) {
17862
+ this.details = { ...this.details, ...details };
17863
+ this.paintFooter();
17864
+ }
17865
+ setSuggestions(items, selectedIndex = 0) {
17866
+ if (this.enabled && this.installed && !this.closed) this.clearPreviousFooter();
17867
+ this.suggestions = [...items];
17868
+ this.selected = Math.max(0, Math.min(items.length - 1, selectedIndex));
17869
+ if (this.enabled && this.installed && !this.closed) {
17870
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
17871
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
17872
+ this.paintFooter();
17873
+ }
17874
+ }
17875
+ /** Freeze the footer and focus choices in the transcript; its reserved rows stay visible. */
17876
+ suspend() {
17877
+ if (this.closed || !this.enabled) return;
17878
+ this.suspended += 1;
17879
+ this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
17880
+ }
17881
+ resume() {
17882
+ if (this.suspended > 0) this.suspended -= 1;
17883
+ if (!this.suspended) this.paintFooter();
17884
+ }
17885
+ writeComposer(chunk) {
17886
+ if (this.closed || !this.enabled || !this.installed) {
17887
+ this.rawWrite(chunk);
17888
+ return;
17889
+ }
17890
+ if (this.suspended) {
17891
+ this.writeTranscript(chunk);
17892
+ return;
17893
+ }
17894
+ if (!this.paintQueued) {
17895
+ this.paintQueued = true;
17896
+ queueMicrotask(() => {
17897
+ this.paintQueued = false;
17898
+ if (!this.closed && !this.suspended) this.paintFooter();
17899
+ });
17900
+ }
17901
+ }
17902
+ writeTranscript(chunk, encoding, callback) {
17903
+ if (this.closed || !this.enabled || !this.installed) return this.rawWrite(chunk, encoding, callback);
17904
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8");
17905
+ const formatted = text.replace(/(?<!\r)\n/g, "\r\n").replace(/\x1b\[([012]?)J/g, (_escape, mode) => {
17906
+ const saved = cup(this.transcriptRow, this.transcriptColumn);
17907
+ const first = mode === "2" || mode === "1" ? 1 : this.transcriptRow;
17908
+ const last = mode === "1" ? this.transcriptRow : this.contentRows;
17909
+ let erased = "";
17910
+ for (let row = first; row <= last; row += 1) erased += `${cup(row)}${CSI}2K`;
17911
+ return erased + saved;
17912
+ });
17913
+ if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters(formatted))) {
17914
+ this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
17915
+ `);
17916
+ this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
17917
+ this.transcriptColumn = 1;
17918
+ this.wrapPending = false;
17919
+ }
17920
+ this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
17921
+ const accepted = this.rawWrite(
17922
+ formatted,
17923
+ typeof encoding === "string" ? encoding : void 0,
17924
+ typeof encoding === "function" ? encoding : callback
17925
+ );
17926
+ this.trackTranscript(formatted);
17927
+ if (!this.suspended) this.placeInputCursor();
17928
+ return accepted;
17929
+ }
17930
+ trackTranscript(text) {
17931
+ const parts = text.split(/(\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b.)/g);
17932
+ for (const part of parts) {
17933
+ if (part === "\x1B7") {
17934
+ this.savedTranscript = { row: this.transcriptRow, column: this.transcriptColumn, wrapPending: this.wrapPending };
17935
+ } else if (part === "\x1B8") {
17936
+ if (this.savedTranscript) {
17937
+ this.transcriptRow = this.savedTranscript.row;
17938
+ this.transcriptColumn = this.savedTranscript.column;
17939
+ this.wrapPending = this.savedTranscript.wrapPending;
17940
+ }
17941
+ } else if (part.startsWith("\x1B[")) {
17942
+ const match = /^\x1b\[([\d;]*)([A-GHf])$/.exec(part);
17943
+ if (!match) continue;
17944
+ const values = match[1].split(";").map((value) => Number(value) || 1);
17945
+ const n = values[0];
17946
+ this.wrapPending = false;
17947
+ if (match[2] === "A") this.transcriptRow -= n;
17948
+ else if (match[2] === "B") this.transcriptRow += n;
17949
+ else if (match[2] === "C") this.transcriptColumn += n;
17950
+ else if (match[2] === "D") this.transcriptColumn -= n;
17951
+ else if (match[2] === "G") this.transcriptColumn = n;
17952
+ else if (match[2] === "H" || match[2] === "f") {
17953
+ this.transcriptRow = n;
17954
+ this.transcriptColumn = values[1] ?? 1;
17955
+ }
17956
+ } else if (!part.startsWith("\x1B")) {
17957
+ for (const cell of cells(part)) {
17958
+ if (cell.text === "\r" || cell.text === "\r\n") {
17959
+ this.wrapPending = false;
17960
+ this.transcriptColumn = 1;
17961
+ if (cell.text === "\r\n") this.transcriptRow += 1;
17962
+ } else if (cell.text === "\n") {
17963
+ this.wrapPending = false;
17964
+ this.transcriptColumn = 1;
17965
+ this.transcriptRow += 1;
17966
+ } else if (cell.text === "\b") this.transcriptColumn -= 1;
17967
+ else if (cell.text === " ") this.transcriptColumn += 8 - (this.transcriptColumn - 1) % 8;
17968
+ else {
17969
+ if (this.wrapPending || this.transcriptColumn + cell.width - 1 > this.columns) {
17970
+ this.transcriptColumn = 1;
17971
+ this.transcriptRow += 1;
17972
+ this.wrapPending = false;
17973
+ }
17974
+ this.transcriptColumn += cell.width;
17975
+ if (this.transcriptColumn > this.columns) {
17976
+ this.transcriptColumn = this.columns;
17977
+ this.wrapPending = true;
17978
+ }
17979
+ }
17980
+ this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
17981
+ }
17982
+ }
17983
+ this.transcriptRow = Math.max(1, Math.min(this.contentRows, this.transcriptRow));
17984
+ this.transcriptColumn = Math.max(1, Math.min(this.columns, this.transcriptColumn));
17985
+ }
17986
+ }
17987
+ composer() {
17988
+ const available = this.columns - 4;
17989
+ const prompt = fit(this.rl?.getPrompt() ?? this.prompt, Math.min(24, Math.max(2, Math.floor(available / 3))));
17990
+ const input = plain(this.rl?.line ?? "");
17991
+ const cursor = width(plain((this.rl?.line ?? "").slice(0, this.rl?.cursor ?? 0)));
17992
+ const capacity = Math.max(1, available - width(prompt));
17993
+ const offset = Math.max(0, cursor - capacity + 1);
17994
+ let visible = "";
17995
+ let used = 0;
17996
+ let skipped = 0;
17997
+ for (const cell of cells(input)) {
17998
+ if (skipped + cell.width <= offset) {
17999
+ skipped += cell.width;
18000
+ continue;
18001
+ }
18002
+ if (used + cell.width > capacity) break;
18003
+ visible += cell.text;
18004
+ used += cell.width;
18005
+ }
18006
+ return { text: `${prompt}${visible}`, column: Math.min(this.columns - 1, 3 + width(prompt) + cursor - skipped) };
18007
+ }
18008
+ placeInputCursor() {
18009
+ const composer = this.composer();
18010
+ this.rawWrite(`${cup(this.inputRow, composer.column)}${CSI}?25h`);
18011
+ }
18012
+ clearPreviousFooter() {
18013
+ if (!this.lastFooterTop) return;
18014
+ let clear = "";
18015
+ for (let row = this.lastFooterTop; row <= this.rows; row += 1) clear += `${cup(row)}${CSI}2K`;
18016
+ this.rawWrite(clear);
18017
+ }
18018
+ paintFooter() {
18019
+ if (!this.enabled || !this.installed || this.closed || this.suspended) return;
18020
+ this.lastFooterTop = this.contentRows + 1;
18021
+ const inner = this.columns - 2;
18022
+ const border = "\u2500".repeat(inner);
18023
+ const composer = this.composer();
18024
+ const status = this.details.status ? `${plain(this.details.status)} \xB7 ` : "";
18025
+ const modelRoom = Math.max(6, this.columns - width(`${status}${this.details.provider} \xB7 \xB7 ${this.details.mode}`));
18026
+ const metadata = `${status}${this.details.provider} \xB7 ${fit(this.details.model, modelRoom)} \xB7 ${this.details.mode}`;
18027
+ const usage = `${count3(this.details.tokens)} reported tokens \xB7 ${count3(this.details.steps)} steps \xB7 est ctx ${count3(this.details.contextUsed)}/${count3(this.details.contextMax)}`;
18028
+ let frame = "";
18029
+ const suggestions = this.suggestionRows;
18030
+ const first = Math.max(0, Math.min(this.selected - Math.floor(suggestions / 2), this.suggestions.length - suggestions));
18031
+ for (let i = 0; i < suggestions; i += 1) {
18032
+ const index = first + i;
18033
+ const selected = index === this.selected;
18034
+ frame += `${cup(this.contentRows + 1 + i)}${CSI}2K${selected ? `${CSI}7m` : ""}${fit(`${selected ? "\u203A" : " "} ${this.suggestions[index] ?? ""}`, this.columns)}${CSI}0m`;
18035
+ }
18036
+ const inputText = fit(composer.text, this.columns - 4);
18037
+ const paddedInput = inputText + " ".repeat(Math.max(0, this.columns - 4 - width(inputText)));
18038
+ const lines = [`\u250C${border}\u2510`, `\u2502 ${paddedInput} \u2502`, `\u2514${border}\u2518`, fit(metadata, this.columns), fit(usage, this.columns)];
18039
+ lines.forEach((line, index) => {
18040
+ frame += `${cup(this.rows - 4 + index)}${CSI}2K${line}`;
18041
+ });
18042
+ this.rawWrite(`${frame}${cup(this.inputRow, composer.column)}${CSI}?25h`);
18043
+ }
18044
+ close() {
18045
+ if (this.closed) return;
18046
+ this.closed = true;
18047
+ if (!this.installed) return;
18048
+ this.output.write = this.originalWrite;
18049
+ if (this.errorOutput && this.errorOutput !== this.output && this.originalErrorWrite) this.errorOutput.write = this.originalErrorWrite;
18050
+ this.output.off("resize", this.resize);
18051
+ this.output.off("error", this.exit);
18052
+ if (this.errorOutput !== this.output) this.errorOutput?.off("error", this.exit);
18053
+ process.off("exit", this.exit);
18054
+ process.off("SIGINT", this.exit);
18055
+ process.off("SIGTERM", this.exit);
18056
+ try {
18057
+ this.rawWrite(`${RESET_REGION}${MOUSE_OFF2}${CSI}?25h`);
18058
+ this.clearPreviousFooter();
18059
+ this.rawWrite(`${cup(this.transcriptRow, this.transcriptColumn)}\r
18060
+ `);
18061
+ } catch {
18062
+ }
18063
+ }
18064
+ };
18065
+ function createTerminalLayout(options) {
18066
+ return new TerminalLayout(options).install();
17215
18067
  }
17216
18068
 
17217
18069
  // src/ui/welcome.ts
@@ -17227,29 +18079,28 @@ var LOGO = [
17227
18079
  function renderWelcome(info, options = {}) {
17228
18080
  const columns = normalizeColumns(options.columns);
17229
18081
  const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
17230
- const cardWidth = Math.min(88, Math.max(42, columns));
18082
+ const cardWidth = columns;
17231
18083
  const innerWidth = cardWidth - 4;
17232
18084
  const wide = columns >= 64;
17233
18085
  const lines = [];
17234
18086
  if (wide) {
17235
18087
  lines.push("");
17236
18088
  for (let index = 0; index < LOGO.length; index += 1) {
17237
- const color = index < 2 ? paint.hex("#38BDF8") : index < 4 ? paint.hex("#22D3EE") : paint.hex("#A78BFA");
17238
- lines.push(` ${color(LOGO[index])}`);
18089
+ lines.push(` ${paint.bold.white(LOGO[index])}`);
17239
18090
  }
17240
18091
  } else {
17241
- lines.push("", ` ${paint.bold.hex("#22D3EE")("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
18092
+ lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
17242
18093
  }
17243
18094
  const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
17244
18095
  const topFill = Math.max(0, cardWidth - topLabel.length - 2);
17245
- lines.push(paint.cyan(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
18096
+ lines.push(paint.gray(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
17246
18097
  lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
17247
18098
  lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
17248
18099
  lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
17249
- lines.push(paint.cyan(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
18100
+ lines.push(paint.gray(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
17250
18101
  lines.push(cardLine(info.budget, innerWidth, paint.gray));
17251
18102
  lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
17252
- lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
18103
+ lines.push(paint.gray(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
17253
18104
  lines.push(
17254
18105
  wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("Esc stop")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C exit")}` : paint.gray(" /help \xB7 Esc \xB7 Ctrl-C")
17255
18106
  );
@@ -17257,33 +18108,33 @@ function renderWelcome(info, options = {}) {
17257
18108
  }
17258
18109
  function renderComposer(columns = 80, color = true) {
17259
18110
  const paint = new Chalk({ level: color ? chalk10.level : 0 });
17260
- const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
18111
+ const width2 = normalizeColumns(columns);
17261
18112
  const label = " message ";
17262
- const fill = Math.max(1, width - label.length - 2);
18113
+ const fill = Math.max(1, width2 - label.length - 2);
17263
18114
  return {
17264
18115
  frame: `
17265
- ${paint.cyan(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
17266
- ${paint.cyan("\u2502")}`,
17267
- prompt: paint.bold.hex("#22D3EE")("\u2570\u2500\u276F ")
18116
+ ${paint.gray(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
18117
+ ${paint.gray("\u2502")}`,
18118
+ prompt: paint.bold.white("\u2570\u2500\u276F ")
17268
18119
  };
17269
18120
  }
17270
- function cardLine(value, width, style) {
17271
- const clipped = truncate3(value, width);
17272
- return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width - clipped.length))} \u2502`;
18121
+ function cardLine(value, width2, style) {
18122
+ const clipped = truncate3(value, width2);
18123
+ return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width2 - clipped.length))} \u2502`;
17273
18124
  }
17274
18125
  function normalizeColumns(columns) {
17275
- return Number.isFinite(columns) ? Math.max(28, Math.floor(columns)) : 80;
18126
+ return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
17276
18127
  }
17277
- function truncate3(value, width) {
17278
- if (value.length <= width) return value;
17279
- if (width <= 1) return "\u2026";
17280
- return `${value.slice(0, width - 1)}\u2026`;
18128
+ function truncate3(value, width2) {
18129
+ if (value.length <= width2) return value;
18130
+ if (width2 <= 1) return "\u2026";
18131
+ return `${value.slice(0, width2 - 1)}\u2026`;
17281
18132
  }
17282
- function shortenMiddle(value, width) {
17283
- if (value.length <= width) return value;
17284
- if (width < 9) return truncate3(value, width);
17285
- const side = Math.floor((width - 1) / 2);
17286
- return `${value.slice(0, side)}\u2026${value.slice(value.length - (width - side - 1))}`;
18133
+ function shortenMiddle(value, width2) {
18134
+ if (value.length <= width2) return value;
18135
+ if (width2 < 9) return truncate3(value, width2);
18136
+ const side = Math.floor((width2 - 1) / 2);
18137
+ return `${value.slice(0, side)}\u2026${value.slice(value.length - (width2 - side - 1))}`;
17287
18138
  }
17288
18139
 
17289
18140
  // src/update/check.ts
@@ -17418,6 +18269,10 @@ async function runNpm(args, options) {
17418
18269
  // src/commands/repl.ts
17419
18270
  var HELP = `Commands:
17420
18271
  /help Show this help
18272
+ /doctor Check local setup
18273
+ /mcp Show connected MCP servers
18274
+ /continue [focus] Continue unfinished work
18275
+ /autonomous [on|off|status] Continue step-limited turns (up to five rounds)
17421
18276
  /model [name] Show or set the model
17422
18277
  /mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
17423
18278
  /permissions [readonly|workspace|full] Show or set permissions
@@ -17444,6 +18299,7 @@ var HELP = `Commands:
17444
18299
  /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
17445
18300
  /output [number|list] Inspect saved tool output (latest by default)
17446
18301
  /memory List and inspect stored memories
18302
+ /skill <name> [args] Run an installed skill
17447
18303
  /skills List installed skills
17448
18304
  /update Show how to install an available CLI update
17449
18305
  /clear Start a fresh conversation
@@ -17543,7 +18399,7 @@ async function runRepl(options, initialTask = "") {
17543
18399
  const hasFallbackChain = Boolean(config.fallback?.chain?.length);
17544
18400
  if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
17545
18401
  console.log(chalk11.yellow("You are not signed in to Ultrac."));
17546
- console.log(`Run ${chalk11.cyan("ultrac login")} first, or start with ${chalk11.cyan("--provider echo")}.`);
18402
+ console.log(`Run ${chalk11.white("ultrac login")} first, or start with ${chalk11.white("--provider echo")}.`);
17547
18403
  process.exitCode = EXIT_REPL_NOT_STARTED;
17548
18404
  return;
17549
18405
  }
@@ -17605,10 +18461,37 @@ async function runRepl(options, initialTask = "") {
17605
18461
  mode,
17606
18462
  permissions: basePermissions
17607
18463
  });
18464
+ const layout = stdin5.isTTY && stdout5.isTTY && process.env.TERM !== "dumb" ? createTerminalLayout({
18465
+ output: stdout5,
18466
+ errorOutput: stderr,
18467
+ bindSignals: false,
18468
+ details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
18469
+ }) : void 0;
18470
+ const refreshDetails = () => {
18471
+ if (!layout?.enabled) return;
18472
+ const steps = session.toSession().steps;
18473
+ const sum = (key) => {
18474
+ const values = steps.flatMap((step) => typeof step.usage?.[key] === "number" ? [step.usage[key]] : []);
18475
+ return values.length ? values.reduce((a, b) => a + b, 0) : void 0;
18476
+ };
18477
+ const context = session.contextStatus();
18478
+ layout.updateDetails({
18479
+ contextUsed: context.estimatedPromptTokens,
18480
+ contextMax: context.contextTokens,
18481
+ provider: provider.name,
18482
+ model: session.model,
18483
+ mode: session.mode,
18484
+ tokens: sum("totalTokens"),
18485
+ inputTokens: sum("inputTokens"),
18486
+ outputTokens: sum("outputTokens"),
18487
+ steps: steps.length
18488
+ });
18489
+ };
17608
18490
  const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
17609
18491
  console.error(chalk11.yellow(message));
17610
18492
  });
17611
18493
  const persistSession = async (target = session) => {
18494
+ refreshDetails();
17612
18495
  const snapshot = target.toSession();
17613
18496
  if (!hasSavedSessionContent(snapshot)) return null;
17614
18497
  return writeCheckpoint(snapshot);
@@ -17630,6 +18513,7 @@ async function runRepl(options, initialTask = "") {
17630
18513
  try {
17631
18514
  await session.start();
17632
18515
  } catch (error) {
18516
+ layout?.close();
17633
18517
  throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
17634
18518
  }
17635
18519
  const update = await updateCheck;
@@ -17638,21 +18522,58 @@ async function runRepl(options, initialTask = "") {
17638
18522
  \u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
17639
18523
  console.log(chalk11.gray(" Run `ultrac update` after this session."));
17640
18524
  }
17641
- const rl = createInterface3({ input: stdin5, output: stdout5 });
18525
+ const rl = createInterface3({ input: stdin5, output: layout?.enabled ? layout.outputProxy : stdout5 });
18526
+ layout?.attachReadline(rl);
18527
+ refreshDetails();
18528
+ const builtinEntries = BUILTIN_SLASH_COMMANDS.map((command) => ({ text: `/${command}`, label: `/${command}` }));
18529
+ const slashCompletion = layout?.enabled ? attachSlashCompletion(rl, async () => {
18530
+ const [commands, skills] = await Promise.all([listSkillCommands(workspace), listSkills(workspace)]);
18531
+ const declared = new Set(commands.commands.map((command) => command.skillName));
18532
+ return [
18533
+ ...builtinEntries,
18534
+ ...commands.commands.map((command) => ({ text: `/${command.command}`, label: `/${command.command}`, description: command.description, skill: true })),
18535
+ ...skills.filter((skill) => !declared.has(skill.name)).map((skill) => ({ text: `/skill ${skill.name}`, label: `/skill ${skill.name}`, description: skill.description, skill: true }))
18536
+ ];
18537
+ }, (rows, index) => layout.setSuggestions(rows, index), builtinEntries) : void 0;
17642
18538
  const reader = new LineReader(rl);
17643
18539
  const question = async (prompt, signal) => {
17644
- stdout5.write(prompt);
17645
- return reader.nextFresh(signal);
18540
+ slashCompletion?.suspend();
18541
+ try {
18542
+ if (layout?.enabled) {
18543
+ console.log(prompt);
18544
+ layout.setPrompt("\u203A ");
18545
+ } else stdout5.write(prompt);
18546
+ return await reader.nextFresh(signal);
18547
+ } finally {
18548
+ layout?.setPrompt("\u203A ");
18549
+ slashCompletion?.resume();
18550
+ }
17646
18551
  };
17647
18552
  const drainQueuedMessages = () => reader.drainQueuedMessages();
18553
+ let autonomousRounds = 1;
17648
18554
  setAskUserRuntime(async (text, options2, signal) => {
17649
- console.log(chalk11.cyan(`
18555
+ console.log(chalk11.white(`
17650
18556
  ? ${text}`));
17651
- if (options2?.length) {
17652
- options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
17653
- console.log(chalk11.gray(" (type a number, or answer in your own words)"));
18557
+ let rawAnswer;
18558
+ if (options2?.length && layout?.enabled) {
18559
+ slashCompletion?.suspend();
18560
+ layout.setPrompt("\u203A ");
18561
+ let choices;
18562
+ try {
18563
+ choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
18564
+ rawAnswer = await reader.nextFresh(signal);
18565
+ } finally {
18566
+ choices?.cleanup();
18567
+ layout.setPrompt("\u203A ");
18568
+ slashCompletion?.resume();
18569
+ }
18570
+ } else {
18571
+ if (options2?.length) {
18572
+ options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
18573
+ console.log(chalk11.gray(" (type a number, or answer in your own words)"));
18574
+ }
18575
+ rawAnswer = await question(chalk11.white(" \u203A "), signal);
17654
18576
  }
17655
- const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
17656
18577
  if (signal?.aborted) return null;
17657
18578
  const answer = (rawAnswer ?? "").trim();
17658
18579
  const picked = Number.parseInt(answer, 10);
@@ -17672,7 +18593,7 @@ async function runRepl(options, initialTask = "") {
17672
18593
  rl.on("SIGINT", exitSession);
17673
18594
  const onKeypress = (_input, key) => {
17674
18595
  if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
17675
- handleReplEscape(turnAbort);
18596
+ if (!slashCompletion?.handledEscape()) handleReplEscape(turnAbort);
17676
18597
  };
17677
18598
  if (stdin5.isTTY) {
17678
18599
  emitKeypressEvents(stdin5, rl);
@@ -17684,30 +18605,44 @@ async function runRepl(options, initialTask = "") {
17684
18605
  rl.close();
17685
18606
  };
17686
18607
  process.on("SIGTERM", terminate);
18608
+ let turnStartedAt;
18609
+ const footerTimer = setInterval(() => {
18610
+ const seconds = turnStartedAt === void 0 ? void 0 : Math.floor((Date.now() - turnStartedAt) / 1e3);
18611
+ layout?.updateDetails({ status: seconds === void 0 ? "Idle" : `Running ${Math.floor(seconds / 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}` });
18612
+ }, 1e3);
18613
+ footerTimer.unref();
17687
18614
  const withInterrupt = async (run) => {
17688
18615
  turnAbort = new AbortController();
18616
+ turnStartedAt = Date.now();
18617
+ layout?.updateDetails({ status: "Running 00:00" });
17689
18618
  try {
17690
18619
  return await run(turnAbort.signal);
17691
18620
  } finally {
17692
18621
  turnAbort = void 0;
18622
+ turnStartedAt = void 0;
18623
+ refreshDetails();
18624
+ layout?.updateDetails({ status: "Idle" });
17693
18625
  }
17694
18626
  };
17695
18627
  try {
17696
18628
  if (initialTask) {
17697
- console.log(chalk11.cyan(`
18629
+ console.log(chalk11.white(`
17698
18630
  \u203A ${initialTask}`));
17699
- await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
18631
+ await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17700
18632
  }
17701
18633
  while (running) {
17702
- if (stdout5.isTTY) {
18634
+ if (layout?.enabled) {
18635
+ layout.setPrompt("\u203A ");
18636
+ } else if (stdout5.isTTY) {
17703
18637
  const composer = renderComposer(stdout5.columns, true);
17704
18638
  console.log(composer.frame);
17705
18639
  stdout5.write(composer.prompt);
17706
18640
  } else {
17707
- stdout5.write(chalk11.cyan("\n\u203A "));
18641
+ stdout5.write(chalk11.white("\n\u203A "));
17708
18642
  }
17709
18643
  const line = await reader.next();
17710
18644
  if (line === null) break;
18645
+ if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
17711
18646
  const raw = line.trim();
17712
18647
  if (!raw) continue;
17713
18648
  if (raw.startsWith("/")) {
@@ -17723,7 +18658,7 @@ async function runRepl(options, initialTask = "") {
17723
18658
  resetContextBudgetNotices();
17724
18659
  console.log(chalk11.gray("Started a fresh conversation."));
17725
18660
  }, async (prompt) => {
17726
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing);
18661
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
17727
18662
  }, signal, async (id) => {
17728
18663
  const lineage = await loadSessionLineage(workspace, id);
17729
18664
  const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
@@ -17732,7 +18667,11 @@ async function runRepl(options, initialTask = "") {
17732
18667
  session = restored;
17733
18668
  resetContextBudgetNotices();
17734
18669
  console.log(`Restored ${lineage.sessions.length} saved session(s). Permissions were kept from the current session.`);
17735
- });
18670
+ }, { mcpStatus: () => mcpRuntime?.statuses ?? [], autonomous: (setting) => {
18671
+ if (setting === "on") autonomousRounds = 5;
18672
+ if (setting === "off") autonomousRounds = 1;
18673
+ return autonomousRounds;
18674
+ } });
17736
18675
  });
17737
18676
  if (handled === "exit") {
17738
18677
  running = false;
@@ -17741,7 +18680,9 @@ async function runRepl(options, initialTask = "") {
17741
18680
  setSubagentRuntime({ provider, model: session.model, mode: session.mode, registry });
17742
18681
  if (handled === "unhandled") {
17743
18682
  try {
17744
- await runSkillCommand(session, raw, workspace, question);
18683
+ await withInterrupt((signal) => runSkillCommand(session, raw, workspace, question, async (prompt) => {
18684
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
18685
+ }));
17745
18686
  } finally {
17746
18687
  await persistSession();
17747
18688
  }
@@ -17752,15 +18693,21 @@ async function runRepl(options, initialTask = "") {
17752
18693
  }
17753
18694
  continue;
17754
18695
  }
17755
- await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
18696
+ await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17756
18697
  }
17757
18698
  } finally {
18699
+ clearInterval(footerTimer);
18700
+ slashCompletion?.cleanup();
17758
18701
  process.off("SIGTERM", terminate);
17759
18702
  if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
17760
18703
  setAskUserRuntime(void 0);
17761
18704
  rl.close();
17762
- await saveTranscript(session, writeCheckpoint);
17763
- mcpRuntime?.close();
18705
+ try {
18706
+ await saveTranscript(session, writeCheckpoint);
18707
+ } finally {
18708
+ mcpRuntime?.close();
18709
+ layout?.close();
18710
+ }
17764
18711
  }
17765
18712
  console.log(chalk11.gray("Goodbye."));
17766
18713
  }
@@ -17850,9 +18797,26 @@ ${message.trim()}`);
17850
18797
  detachProgress();
17851
18798
  }
17852
18799
  }
17853
- async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing) {
18800
+ async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
17854
18801
  try {
17855
- return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
18802
+ if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
18803
+ let successful = true;
18804
+ const result = await runAutonomousLoop({
18805
+ maxRounds,
18806
+ signal,
18807
+ classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
18808
+ runRound: async (round, _previous, roundSignal) => {
18809
+ successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing);
18810
+ await persist(session);
18811
+ const snapshot = session.toSession();
18812
+ return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
18813
+ },
18814
+ onRoundEnd: (round, _snapshot, willResume) => {
18815
+ if (willResume) console.log(chalk11.gray(`Continuing unfinished work: round ${round + 1}/${maxRounds}.`));
18816
+ }
18817
+ });
18818
+ if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
18819
+ return successful;
17856
18820
  } finally {
17857
18821
  await persist(session);
17858
18822
  }
@@ -17919,11 +18883,41 @@ function slashCommandChangesSession(raw) {
17919
18883
  function errorMessage2(error) {
17920
18884
  return error instanceof Error ? error.message : String(error);
17921
18885
  }
17922
- async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume) {
18886
+ async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume, services) {
17923
18887
  const [name, ...rest] = raw.slice(1).split(/\s+/);
17924
18888
  const command = (name ?? "").toLowerCase();
17925
18889
  const argument = rest.join(" ").trim();
17926
18890
  switch (command) {
18891
+ case "doctor":
18892
+ await runDoctor(workspace, false, false);
18893
+ return "ok";
18894
+ case "mcp": {
18895
+ const statuses = services?.mcpStatus?.() ?? [];
18896
+ console.log(statuses.length ? renderMcpStatuses(statuses) : "No connected MCP servers. Configure with ultrac mcp add, approve with ultrac trust, then restart the session.");
18897
+ return "ok";
18898
+ }
18899
+ case "autonomous": {
18900
+ const setting = argument || "status";
18901
+ if (setting !== "on" && setting !== "off" && setting !== "status") {
18902
+ console.log("Usage: /autonomous on|off|status");
18903
+ return "ok";
18904
+ }
18905
+ if (!services?.autonomous) {
18906
+ console.log("Automatic continuation is available in the interactive session.");
18907
+ return "ok";
18908
+ }
18909
+ const rounds = services.autonomous(setting);
18910
+ console.log(`Automatic continuation ${rounds > 1 ? "on" : "off"}: up to ${rounds} round(s), with checkpoint, elapsed and stall limits.`);
18911
+ return "ok";
18912
+ }
18913
+ case "continue": {
18914
+ if (!runPrompt || !hasSavedSessionContent(session.toSession())) {
18915
+ console.log("No current task to continue. Start a task or use /resume <id>.");
18916
+ return "ok";
18917
+ }
18918
+ await runPrompt(`${AUTONOMOUS_CONTINUE_TASK}${argument ? ` Focus: ${argument}` : ""}`);
18919
+ return "ok";
18920
+ }
17927
18921
  case "logs": {
17928
18922
  console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
17929
18923
  return "ok";
@@ -18006,7 +19000,7 @@ ${extra}`);
18006
19000
  else {
18007
19001
  for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
18008
19002
  const counts = reported.flatMap((step) => typeof step.usage?.[field] === "number" ? [step.usage[field]] : []);
18009
- console.log(`${field}: ${counts.length ? counts.reduce((sum, count3) => sum + count3, 0) : "not reported"}`);
19003
+ console.log(`${field}: ${counts.length ? counts.reduce((sum, count4) => sum + count4, 0) : "not reported"}`);
18010
19004
  }
18011
19005
  console.log(`Counts cover ${reported.length}/${steps.length} agent steps in this session. Compaction calls and ancestor sessions are excluded. Card charges are not inferred from tokens.`);
18012
19006
  }
@@ -18158,6 +19152,20 @@ ${extra}`);
18158
19152
  }
18159
19153
  return "ok";
18160
19154
  }
19155
+ case "skill": {
19156
+ const [skillName, ...args] = argument.split(/\s+/);
19157
+ if (!skillName || !runPrompt) {
19158
+ console.log("Usage: /skill <name> [args]");
19159
+ return "ok";
19160
+ }
19161
+ const skill = await readSkill(workspace, skillName);
19162
+ if (!skill) {
19163
+ console.log(`No available skill named ${skillName}. Use /skills to list them.`);
19164
+ return "ok";
19165
+ }
19166
+ await runPrompt(buildSkillCommandPrompt(skill, { command: "skill", skillName: skill.name, description: skill.description, scope: skill.scope, path: skill.path }, args.join(" ")));
19167
+ return "ok";
19168
+ }
18161
19169
  case "skills": {
18162
19170
  const skills = await listSkills(workspace);
18163
19171
  if (!skills.length) {
@@ -18190,13 +19198,14 @@ ${extra}`);
18190
19198
  return "unhandled";
18191
19199
  }
18192
19200
  }
18193
- async function runSkillCommand(session, raw, workspace, question) {
19201
+ async function runSkillCommand(session, raw, workspace, question, runPrompt) {
18194
19202
  const resolved = await resolveSkillCommand(workspace, raw);
18195
19203
  if (resolved.status === "ok") {
18196
19204
  console.log(
18197
19205
  chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
18198
19206
  );
18199
- await runTurn(session, resolved.prompt, question);
19207
+ if (runPrompt) await runPrompt(resolved.prompt);
19208
+ else await runTurn(session, resolved.prompt, question);
18200
19209
  return;
18201
19210
  }
18202
19211
  if (resolved.status === "conflict") {
@@ -18370,148 +19379,6 @@ function indent3(value) {
18370
19379
  // src/commands/run.ts
18371
19380
  import { basename as basename3 } from "path";
18372
19381
  import chalk12 from "chalk";
18373
-
18374
- // src/core/autonomous.ts
18375
- import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
18376
- var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
18377
- var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
18378
- var AUTONOMOUS_MAX_STEPS = 200;
18379
- var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
18380
- var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
18381
- var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
18382
- var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
18383
- function shouldResume(stopReason) {
18384
- return stopReason === "max-steps" || stopReason === "error";
18385
- }
18386
- function boundedInteger(value, name, min, max) {
18387
- if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
18388
- return value;
18389
- }
18390
- function abortableSleep(milliseconds, signal) {
18391
- signal.throwIfAborted();
18392
- return new Promise((resolve15, reject) => {
18393
- const timer = setTimeout(() => {
18394
- signal.removeEventListener("abort", abort);
18395
- resolve15();
18396
- }, milliseconds);
18397
- const abort = () => {
18398
- clearTimeout(timer);
18399
- signal.removeEventListener("abort", abort);
18400
- reject(signal.reason);
18401
- };
18402
- signal.addEventListener("abort", abort, { once: true });
18403
- if (signal.aborted) abort();
18404
- });
18405
- }
18406
- function progress(session) {
18407
- const hash = createHash7("sha256");
18408
- let completedTools = 0;
18409
- hash.update(session.finalMessage ?? "");
18410
- for (const step of session.steps ?? []) {
18411
- hash.update(step.modelText ?? "");
18412
- for (const tool of step.toolResults ?? []) {
18413
- completedTools += 1;
18414
- hash.update(tool.call.tool);
18415
- hash.update(tool.result.ok ? "ok" : "failed");
18416
- hash.update(tool.result.content ?? "");
18417
- try {
18418
- hash.update(JSON.stringify(tool.call.input));
18419
- } catch {
18420
- hash.update("unserializable-input");
18421
- }
18422
- }
18423
- }
18424
- return { fingerprint: hash.digest("hex"), completedTools };
18425
- }
18426
- async function runAutonomousLoop(options) {
18427
- boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
18428
- const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
18429
- const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
18430
- const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
18431
- const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
18432
- const now = options.now ?? (() => performance.now());
18433
- const started = now();
18434
- const elapsed = () => Math.max(0, now() - started);
18435
- const budget = new AbortController();
18436
- const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
18437
- deadline.unref?.();
18438
- const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
18439
- const sleep2 = options.sleep ?? abortableSleep;
18440
- const id = randomUUID5();
18441
- let previous;
18442
- let lastFingerprint;
18443
- let stalls = 0;
18444
- let consecutiveErrors = 0;
18445
- try {
18446
- signal.throwIfAborted();
18447
- for (let round = 1; ; round += 1) {
18448
- const roundStart = now();
18449
- diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
18450
- let session;
18451
- try {
18452
- session = await options.runRound(round, previous, signal);
18453
- } catch (error) {
18454
- diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
18455
- throw error;
18456
- }
18457
- const current = progress(session);
18458
- const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
18459
- stalls = unchanged ? stalls + 1 : 0;
18460
- lastFingerprint = current.fingerprint;
18461
- const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
18462
- consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
18463
- let haltReason;
18464
- if (session.stopReason === "completed") haltReason = "completed";
18465
- else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
18466
- else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
18467
- else if (classification && !classification.retryable) haltReason = "permanent_error";
18468
- else if (!shouldResume(session.stopReason)) haltReason = "stopped";
18469
- else if (stalls >= stallLimit) haltReason = "stalled";
18470
- else if (round >= options.maxRounds) haltReason = "max_rounds";
18471
- const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
18472
- if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
18473
- diagnosticEvent("round.end", {
18474
- id,
18475
- round,
18476
- sessionId: session.id,
18477
- reason: haltReason ?? session.stopReason,
18478
- failureKind: classification?.kind,
18479
- attempt: consecutiveErrors,
18480
- completedTools: current.completedTools,
18481
- elapsedMs: elapsed(),
18482
- durationMs: Math.max(0, now() - roundStart)
18483
- });
18484
- if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
18485
- await options.onRoundEnd?.(round, session, !haltReason);
18486
- if (haltReason) return { session, rounds: round, haltReason };
18487
- if (session.stopReason === "error") {
18488
- const delayMs = retryDelay;
18489
- if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
18490
- diagnosticEvent("round.retry", {
18491
- id,
18492
- round,
18493
- nextRound: round + 1,
18494
- attempt: consecutiveErrors,
18495
- failureKind: classification?.kind ?? "unclassified",
18496
- delayMs,
18497
- elapsedMs: elapsed()
18498
- });
18499
- try {
18500
- await sleep2(delayMs, signal);
18501
- } catch (error) {
18502
- if (!signal.aborted) throw error;
18503
- return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
18504
- }
18505
- }
18506
- if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
18507
- previous = session;
18508
- }
18509
- } finally {
18510
- clearTimeout(deadline);
18511
- }
18512
- }
18513
-
18514
- // src/commands/run.ts
18515
19382
  function parsePositiveInt(value, flag) {
18516
19383
  const parsed = Number.parseInt(value, 10);
18517
19384
  if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
@@ -19753,8 +20620,8 @@ Leaving run ${runId} as it is.
19753
20620
  case "succeeded":
19754
20621
  finishHuman(result.text);
19755
20622
  if (result.toolCalls.length && !result.text) {
19756
- const count3 = result.toolCalls.length;
19757
- r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
20623
+ const count4 = result.toolCalls.length;
20624
+ r.err(chalk13.gray(`Done with ${count4} tool call${count4 === 1 ? "" : "s"} and no text.
19758
20625
  `));
19759
20626
  }
19760
20627
  return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;