@spinalfluid/ultrac 0.1.6 → 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 +77 -6
  2. package/dist/cli.js +1945 -513
  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",
@@ -548,6 +549,9 @@ function ensurePrivateDirectory(path) {
548
549
  }
549
550
  chmodSync(path, 448);
550
551
  }
552
+ function currentDiagnosticLogPath() {
553
+ return diagnosticPath;
554
+ }
551
555
  function diagnosticEvent(event, fields = {}) {
552
556
  if (!diagnosticPath) return;
553
557
  const record3 = {
@@ -1219,20 +1223,20 @@ function compactRun(input, options = {}) {
1219
1223
  }
1220
1224
  async function compactShape(shape, options) {
1221
1225
  const reserveChars = options.summarise ? Math.max(0, options.reserveChars ?? shape.reserveChars ?? 0) : 0;
1222
- const plain = planKeepingSummaries(shape, shape.messages, reserveChars);
1226
+ const plain2 = planKeepingSummaries(shape, shape.messages, reserveChars);
1223
1227
  const skip = (reason) => ({
1224
- ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain,
1228
+ ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain2,
1225
1229
  compacted: false,
1226
1230
  summarisedMessages: 0,
1227
1231
  summarisedTurns: 0,
1228
1232
  skipped: reason
1229
1233
  });
1230
- if (plain.dropped.length === 0) return skip("nothing-dropped");
1234
+ if (plain2.dropped.length === 0) return skip("nothing-dropped");
1231
1235
  if (!options.summarise) return skip("no-summariser");
1232
- const source = plain.dropped.filter((message2) => !isCompactionSummary(message2));
1236
+ const source = plain2.dropped.filter((message2) => !isCompactionSummary(message2));
1233
1237
  if (source.length === 0) return skip("only-summaries");
1234
1238
  const ceiling = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS;
1235
- const slack = plain.usage.promptLimitChars - plain.usage.totalChars - summaryEnvelopeChars();
1239
+ const slack = plain2.usage.promptLimitChars - plain2.usage.totalChars - summaryEnvelopeChars();
1236
1240
  const maxChars = Math.min(ceiling, slack);
1237
1241
  if (maxChars < MIN_SUMMARY_CHARS) return skip("no-room");
1238
1242
  const transcript = renderDroppedTranscript(source, {
@@ -1257,21 +1261,21 @@ async function compactShape(shape, options) {
1257
1261
  const text = truncateToLine(summary.trim(), maxChars);
1258
1262
  if (!text) return skip("summariser-empty");
1259
1263
  const message = compactionSummaryMessage(text);
1260
- const plan = replan(shape, plain, message);
1264
+ const plan = replan(shape, plain2, message);
1261
1265
  if (!plan) return skip("would-drop-more");
1262
1266
  return {
1263
1267
  ...plan,
1264
1268
  // The re-plan saw a transcript the span was already out of, so it has
1265
1269
  // nothing to report about it. What was dropped is what `plain` dropped.
1266
- dropped: plain.dropped,
1267
- droppedMessages: plain.droppedMessages,
1268
- droppedTurns: plain.droppedTurns,
1269
- droppedChars: plain.droppedChars,
1270
+ dropped: plain2.dropped,
1271
+ droppedMessages: plain2.droppedMessages,
1272
+ droppedTurns: plain2.droppedTurns,
1273
+ droppedChars: plain2.droppedChars,
1270
1274
  compacted: true,
1271
1275
  summary: text,
1272
- summarisedMessages: plain.dropped.length,
1273
- summarisedTurns: plain.droppedTurns,
1274
- warning: compactionWarning(plan, plain)
1276
+ summarisedMessages: plain2.dropped.length,
1277
+ summarisedTurns: plain2.droppedTurns,
1278
+ warning: compactionWarning(plan, plain2)
1275
1279
  };
1276
1280
  }
1277
1281
  function planKeepingSummaries(shape, messages, reserveChars) {
@@ -1287,8 +1291,8 @@ function planKeepingSummaries(shape, messages, reserveChars) {
1287
1291
  return shape.plan(messages, callerPinned, reserveChars);
1288
1292
  }
1289
1293
  }
1290
- function replan(shape, plain, summary) {
1291
- const kept = plain.messages;
1294
+ function replan(shape, plain2, summary) {
1295
+ const kept = plain2.messages;
1292
1296
  const insertAt = kept.findIndex((message) => message.role !== "system");
1293
1297
  const at = insertAt < 0 ? kept.length : insertAt;
1294
1298
  const next = [...kept.slice(0, at), summary, ...kept.slice(at)];
@@ -1304,9 +1308,9 @@ function replan(shape, plain, summary) {
1304
1308
  if (!kept.every((message) => survived.has(message))) return void 0;
1305
1309
  return plan;
1306
1310
  }
1307
- function compactionWarning(plan, plain) {
1308
- const turns = plural2(plain.droppedTurns, "earlier turn");
1309
- 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");
1310
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.`;
1311
1315
  }
1312
1316
  function truncateToLine(text, maxChars) {
@@ -4080,7 +4084,7 @@ function runProcess(command, args, options) {
4080
4084
  });
4081
4085
  const untrack = trackProcessGroup(child.pid);
4082
4086
  const stdout6 = createCapture(maxBytes);
4083
- const stderr = createCapture(maxBytes);
4087
+ const stderr2 = createCapture(maxBytes);
4084
4088
  let settled = false;
4085
4089
  let started = false;
4086
4090
  let termination;
@@ -4136,7 +4140,7 @@ function runProcess(command, args, options) {
4136
4140
  };
4137
4141
  const onAbort = () => beginTermination("aborted");
4138
4142
  child.stdout.on("data", (chunk) => stdout6.push(chunk));
4139
- child.stderr.on("data", (chunk) => stderr.push(chunk));
4143
+ child.stderr.on("data", (chunk) => stderr2.push(chunk));
4140
4144
  child.stdout.on("error", () => {
4141
4145
  });
4142
4146
  child.stderr.on("error", () => {
@@ -4149,7 +4153,7 @@ function runProcess(command, args, options) {
4149
4153
  if (termination === "aborted") {
4150
4154
  resolve15({
4151
4155
  stdout: stdout6.text(),
4152
- stderr: stderr.text(),
4156
+ stderr: stderr2.text(),
4153
4157
  started,
4154
4158
  aborted: true,
4155
4159
  exitCode: null,
@@ -4157,7 +4161,7 @@ function runProcess(command, args, options) {
4157
4161
  timedOut: false,
4158
4162
  timeoutMs,
4159
4163
  ...stdout6.truncation() ? { stdoutTruncation: stdout6.truncation() } : {},
4160
- ...stderr.truncation() ? { stderrTruncation: stderr.truncation() } : {},
4164
+ ...stderr2.truncation() ? { stderrTruncation: stderr2.truncation() } : {},
4161
4165
  redactedEnvKeys
4162
4166
  });
4163
4167
  return;
@@ -4167,11 +4171,11 @@ function runProcess(command, args, options) {
4167
4171
  });
4168
4172
  child.on("close", (exitCode, signal) => {
4169
4173
  const stdoutTruncation = stdout6.truncation();
4170
- const stderrTruncation = stderr.truncation();
4174
+ const stderrTruncation = stderr2.truncation();
4171
4175
  finish(
4172
4176
  () => resolve15({
4173
4177
  stdout: stdout6.text(),
4174
- stderr: stderr.text(),
4178
+ stderr: stderr2.text(),
4175
4179
  started,
4176
4180
  aborted: termination === "aborted",
4177
4181
  exitCode,
@@ -5181,7 +5185,7 @@ function topCounts(values) {
5181
5185
  for (const value of values) {
5182
5186
  counts.set(value, (counts.get(value) ?? 0) + 1);
5183
5187
  }
5184
- 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));
5185
5189
  }
5186
5190
  async function collectGitContext(workspace, signal) {
5187
5191
  const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
@@ -6966,8 +6970,8 @@ function shannonEntropy(value) {
6966
6970
  counts.set(char, (counts.get(char) ?? 0) + 1);
6967
6971
  }
6968
6972
  let entropy = 0;
6969
- for (const count3 of counts.values()) {
6970
- const probability = count3 / value.length;
6973
+ for (const count4 of counts.values()) {
6974
+ const probability = count4 / value.length;
6971
6975
  entropy -= probability * Math.log2(probability);
6972
6976
  }
6973
6977
  return entropy;
@@ -8040,7 +8044,9 @@ async function createSkill(workspace, input) {
8040
8044
  name,
8041
8045
  description: input.description.trim(),
8042
8046
  ...input.whenToUse ? { when_to_use: input.whenToUse.trim() } : {},
8043
- ...permissions.length ? { permissions } : {}
8047
+ ...permissions.length ? { permissions } : {},
8048
+ ...input.command ? { command: input.command } : {},
8049
+ ...input.argumentHint ? { "argument-hint": input.argumentHint } : {}
8044
8050
  },
8045
8051
  input.body?.trim() || `# ${name}
8046
8052
 
@@ -8057,7 +8063,8 @@ Describe the steps the agent should follow when this skill is invoked.`
8057
8063
  whenToUse: input.whenToUse?.trim(),
8058
8064
  permissions,
8059
8065
  body: document,
8060
- path
8066
+ path,
8067
+ ...input.command ? { command: { raw: input.command, argumentHint: input.argumentHint } } : {}
8061
8068
  };
8062
8069
  }
8063
8070
  async function loadSkillsContext(workspace) {
@@ -8076,6 +8083,213 @@ async function loadSkillsContext(workspace) {
8076
8083
  }).join("\n") + notice;
8077
8084
  }
8078
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
+
8079
8293
  // src/tools/workspace/skill-tools.ts
8080
8294
  var SkillInput = z17.object({
8081
8295
  name: z17.string().min(1).describe("Name of the skill to load, as shown in the skills list.")
@@ -8122,6 +8336,26 @@ var listSkillsTool = {
8122
8336
  };
8123
8337
  }
8124
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
+ };
8125
8359
 
8126
8360
  // src/tools/workspace/todo-tools.ts
8127
8361
  import { z as z18 } from "zod";
@@ -8562,7 +8796,7 @@ function summarizeDirectories(files) {
8562
8796
  const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
8563
8797
  counts.set(directory, (counts.get(directory) ?? 0) + 1);
8564
8798
  }
8565
- 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));
8566
8800
  }
8567
8801
  function summarizeExtensions(files) {
8568
8802
  const summaries = /* @__PURE__ */ new Map();
@@ -8831,6 +9065,124 @@ import { z as z25 } from "zod";
8831
9065
  import { mkdir as mkdir11, writeFile as writeFile8 } from "fs/promises";
8832
9066
  import { join as join9 } from "path";
8833
9067
 
9068
+ // src/core/periodic-compaction.ts
9069
+ var DEFAULT_SUMMARY_INTERVAL_STEPS = 12;
9070
+ var MIN_PERIODIC_HISTORY_CHARS = 12e3;
9071
+ function summaryIntervalSteps(value = process.env.ULTRAC_SUMMARY_INTERVAL_STEPS) {
9072
+ if (value === void 0 || value.trim() === "") return DEFAULT_SUMMARY_INTERVAL_STEPS;
9073
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) > 1e3) throw new Error("ULTRAC_SUMMARY_INTERVAL_STEPS must be an integer from 0 to 1000.");
9074
+ return Number(value);
9075
+ }
9076
+ function completeContextUnits(messages) {
9077
+ const units = [];
9078
+ for (const message of messages) {
9079
+ if (message.role === "tool") {
9080
+ const previous = units.at(-1);
9081
+ if (!previous || previous[0].role !== "assistant") return null;
9082
+ previous.push(message);
9083
+ } else units.push([message]);
9084
+ }
9085
+ for (const unit of units) {
9086
+ const first = unit[0];
9087
+ if (first.role !== "assistant") continue;
9088
+ if (!first.toolCalls?.length) {
9089
+ if (unit.slice(1).some((message) => "toolCallId" in message)) return null;
9090
+ continue;
9091
+ }
9092
+ const ids = new Set(first.toolCalls.map((call) => call.id));
9093
+ const results = unit.slice(1).map((message) => message.toolCallId);
9094
+ if (ids.size !== first.toolCalls.length || results.length !== ids.size || results.some((id) => !id || !ids.has(id)) || new Set(results).size !== ids.size) return null;
9095
+ }
9096
+ return units;
9097
+ }
9098
+ var PeriodicCompactor = class {
9099
+ constructor(options = {}) {
9100
+ this.options = options;
9101
+ }
9102
+ options;
9103
+ lastAttemptStep = 0;
9104
+ async compact(messages, pinned, provider, model, completedSteps, signal) {
9105
+ const unchanged = () => ({ messages: [...messages], compacted: false });
9106
+ const interval = this.options.intervalSteps ?? summaryIntervalSteps();
9107
+ if (!interval || signal?.aborted || completedSteps - this.lastAttemptStep < interval) return unchanged();
9108
+ const units = completeContextUnits(messages);
9109
+ if (!units) return unchanged();
9110
+ const recent = new Set(units.filter((unit) => unit[0].role === "assistant").slice(-2));
9111
+ const pinnedSet = new Set(pinned);
9112
+ const history = units.filter((unit) => !recent.has(unit) && !unit.some((message) => message.role === "user" || message.role === "system" || pinnedSet.has(message) || isCompactionSummary(message))).flat();
9113
+ const removedChars = history.reduce((size, message) => size + JSON.stringify(message).length, 0);
9114
+ if (history.length < 2 || removedChars < (this.options.minHistoryChars ?? MIN_PERIODIC_HISTORY_CHARS)) return unchanged();
9115
+ this.lastAttemptStep = completedSteps;
9116
+ const trace = startDiagnosticSpan("context.compaction", { path: "periodic", completedSteps, compactedMessageCount: history.length }, 5e3);
9117
+ const controller = new AbortController();
9118
+ let rejectCancelled;
9119
+ const interrupted2 = new Promise((_resolve, reject) => {
9120
+ rejectCancelled = reject;
9121
+ });
9122
+ const abort = () => {
9123
+ controller.abort();
9124
+ rejectCancelled(new Error("summary_interrupted"));
9125
+ };
9126
+ const timer = setTimeout(abort, this.options.timeoutMs ?? DEFAULT_SUMMARY_TIMEOUT_MS);
9127
+ signal?.addEventListener("abort", abort, { once: true });
9128
+ if (signal?.aborted) abort();
9129
+ try {
9130
+ const completion = await Promise.race([provider.complete({ model, system: summaryInstructions(DEFAULT_MAX_SUMMARY_CHARS), messages: [{ role: "user", content: renderDroppedTranscript(history, { maxTranscriptChars: DEFAULT_MAX_TRANSCRIPT_CHARS }) }], signal: controller.signal, context: { purpose: "compaction_summary", compactedMessages: history.length } }), interrupted2]);
9131
+ if (controller.signal.aborted) {
9132
+ trace.end({ outcome: "skipped", reason: "interrupted" });
9133
+ return unchanged();
9134
+ }
9135
+ const summary = completion.text.trim();
9136
+ if (!summary || summary.length > DEFAULT_MAX_SUMMARY_CHARS || summary.length + 300 >= removedChars) {
9137
+ trace.end({ outcome: "skipped", reason: "invalid-summary" });
9138
+ return unchanged();
9139
+ }
9140
+ const removed = new Set(history);
9141
+ const compacted = messages.filter((message) => !removed.has(message));
9142
+ const firstRemoved = messages.findIndex((message) => removed.has(message));
9143
+ const insertion = messages.slice(0, firstRemoved).filter((message) => !removed.has(message)).length;
9144
+ compacted.splice(insertion, 0, compactionSummaryMessage(summary));
9145
+ if (!completeContextUnits(compacted)) {
9146
+ trace.end({ outcome: "skipped", reason: "invalid-pairing" });
9147
+ return unchanged();
9148
+ }
9149
+ trace.end({ outcome: "compacted", summaryChars: summary.length });
9150
+ return { messages: compacted, compacted: true };
9151
+ } catch {
9152
+ trace.end({ outcome: "failed", reason: controller.signal.aborted ? "interrupted" : "summary-failed" });
9153
+ return unchanged();
9154
+ } finally {
9155
+ clearTimeout(timer);
9156
+ signal?.removeEventListener("abort", abort);
9157
+ }
9158
+ }
9159
+ };
9160
+ function snapshotMessages(messages) {
9161
+ const result = [];
9162
+ for (let index = 0; index < messages.length; index += 1) {
9163
+ const message = messages[index];
9164
+ const native = message;
9165
+ if (native.role !== "assistant" || !native.toolCalls?.length) {
9166
+ result.push(message);
9167
+ continue;
9168
+ }
9169
+ const replies = [];
9170
+ while (messages[index + 1]?.role === "tool") replies.push(messages[++index]);
9171
+ const ids = new Set(replies.map((reply) => reply.toolCallId));
9172
+ const calls = native.toolCalls.filter((call) => ids.has(call.id));
9173
+ const completedIds = new Set(calls.map((call) => call.id));
9174
+ const base = { role: native.role, content: native.content };
9175
+ result.push({ ...base, ...calls.length ? { toolCalls: calls } : {} });
9176
+ result.push(...replies.filter((reply) => completedIds.has(reply.toolCallId)));
9177
+ }
9178
+ return result;
9179
+ }
9180
+ function snapshotWithConstraints(messages, constraints) {
9181
+ const snapshot = snapshotMessages(messages);
9182
+ for (const constraint of constraints) if (!snapshot.some((message) => message.role === "user" && message.content === constraint.content)) snapshot.push({ role: "user", content: constraint.content });
9183
+ return snapshot;
9184
+ }
9185
+
8834
9186
  // src/core/progress-events.ts
8835
9187
  var listeners = /* @__PURE__ */ new Set();
8836
9188
  function onAgentProgress(listener) {
@@ -8997,18 +9349,18 @@ function isConcurrencySafe(tool) {
8997
9349
  }
8998
9350
  function planToolBatches(calls, tools) {
8999
9351
  const batches = [];
9000
- let open8;
9352
+ let open9;
9001
9353
  for (const call of calls) {
9002
9354
  if (!isConcurrencySafe(tools.get(call.tool))) {
9003
9355
  batches.push([call]);
9004
- open8 = void 0;
9356
+ open9 = void 0;
9005
9357
  continue;
9006
9358
  }
9007
- if (!open8) {
9008
- open8 = [];
9009
- batches.push(open8);
9359
+ if (!open9) {
9360
+ open9 = [];
9361
+ batches.push(open9);
9010
9362
  }
9011
- open8.push(call);
9363
+ open9.push(call);
9012
9364
  }
9013
9365
  return batches;
9014
9366
  }
@@ -9546,6 +9898,17 @@ function readSessionUserConstraints(session) {
9546
9898
  }
9547
9899
  return constraints;
9548
9900
  }
9901
+ function readSessionContextSnapshot(session) {
9902
+ if (session.contextSnapshot !== void 0) {
9903
+ const snapshot = session.contextSnapshot;
9904
+ if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
9905
+ if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
9906
+ const native = message;
9907
+ return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
9908
+ }) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
9909
+ }
9910
+ return session.contextSnapshot;
9911
+ }
9549
9912
  function createSessionId(date = /* @__PURE__ */ new Date()) {
9550
9913
  const stamp = date.toISOString().replace(/[:.]/g, "-");
9551
9914
  return `${stamp}-${randomBytes3(4).toString("hex")}`;
@@ -9823,6 +10186,7 @@ function validateSession(value, expectedId, label) {
9823
10186
  if (!isSafeSessionId(sessionId) || sessionId !== expectedId || session.parentSessionId !== void 0 && (typeof session.parentSessionId !== "string" || !isSafeSessionId(session.parentSessionId) || normalizedSessionId(session.parentSessionId) !== session.parentSessionId) || typeof session.task !== "string" || typeof session.workspace !== "string" || typeof session.provider !== "string" || typeof session.model !== "string" || typeof session.startedAt !== "string" || typeof session.endedAt !== "string" || typeof session.finalMessage !== "string" || !isToolPermissions(session.permissions) || !Array.isArray(session.steps) || session.mcpResultArchiveVersion !== void 0 && session.mcpResultArchiveVersion !== 1 || session.mode !== void 0 && !isAgentModeName2(session.mode) || session.stopReason !== void 0 && !isStopReason(session.stopReason) || session.subagents !== void 0 && !Array.isArray(session.subagents)) {
9824
10187
  invalid();
9825
10188
  }
10189
+ readSessionContextSnapshot(value);
9826
10190
  const steps = session.steps;
9827
10191
  for (const stepValue of steps) {
9828
10192
  if (!stepValue || typeof stepValue !== "object" || Array.isArray(stepValue)) invalid();
@@ -9933,6 +10297,7 @@ var AgentRunner = class {
9933
10297
  ];
9934
10298
  const pinnedMessages = [...constraintMessages];
9935
10299
  const steps = [];
10300
+ const periodicCompactor = new PeriodicCompactor();
9936
10301
  const toolsByName = new Map(
9937
10302
  registered.map((tool) => [tool.name, tool])
9938
10303
  );
@@ -9955,7 +10320,8 @@ var AgentRunner = class {
9955
10320
  ...subagents2.length ? { subagents: subagents2 } : {},
9956
10321
  constraintVersion: 1,
9957
10322
  userConstraints: [...userConstraints],
9958
- mcpResultArchiveVersion: 1
10323
+ mcpResultArchiveVersion: 1,
10324
+ ...messages.some((message) => message.role === "user" && message.content.startsWith("[compacted-context]")) ? { contextSnapshot: { version: 1, kind: "run", messages: snapshotWithConstraints(messages, userConstraints), completedSteps: steps.length } } : {}
9959
10325
  });
9960
10326
  const depth = options.depth ?? 0;
9961
10327
  const isTopLevel = depth === 0;
@@ -9964,6 +10330,7 @@ var AgentRunner = class {
9964
10330
  try {
9965
10331
  await options.onCheckpoint(buildSession(void 0, listSubagentTranscripts()));
9966
10332
  } catch {
10333
+ diagnosticEvent("session.checkpoint", { outcome: "failed", path: "run" });
9967
10334
  }
9968
10335
  };
9969
10336
  let stopReason = "max-steps";
@@ -9981,6 +10348,11 @@ var AgentRunner = class {
9981
10348
  transcriptKind: "run",
9982
10349
  fileProvenance
9983
10350
  };
10351
+ const periodic = await periodicCompactor.compact(messages, [...pinnedMessages, taskMessage], options.provider, options.model, steps.length, options.signal);
10352
+ if (periodic.compacted) {
10353
+ messages.splice(0, messages.length, ...periodic.messages);
10354
+ await checkpoint();
10355
+ }
9984
10356
  await compactIfNeeded(options, messages, system, taskMessage, pinnedMessages, requestContext);
9985
10357
  if (options.signal?.aborted) {
9986
10358
  stopReason = "aborted";
@@ -10294,7 +10666,7 @@ var NOTHING_RESTORED = {
10294
10666
  legacyMcpResults: 0
10295
10667
  };
10296
10668
  function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance) {
10297
- if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0)) return NOTHING_RESTORED;
10669
+ if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0 && !readSessionContextSnapshot(session))) return NOTHING_RESTORED;
10298
10670
  const messages = [];
10299
10671
  let toolResults = 0;
10300
10672
  let truncatedResults = 0;
@@ -10302,6 +10674,11 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
10302
10674
  let steps = 0;
10303
10675
  for (const session of sessions) {
10304
10676
  steps += session.steps.length;
10677
+ const snapshot = readSessionContextSnapshot(session);
10678
+ if (snapshot?.kind === "run" || snapshot?.kind === "text") {
10679
+ messages.splice(0, messages.length, ...snapshot.messages);
10680
+ continue;
10681
+ }
10305
10682
  for (const step of session.steps) {
10306
10683
  const content = step.parsed ? JSON.stringify(step.parsed) : step.modelText;
10307
10684
  if (content) messages.push({ role: "assistant", content });
@@ -10316,6 +10693,7 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
10316
10693
  fileProvenance?.set(entry.message, toolResultFileProvenance(result, entry.truncated));
10317
10694
  }
10318
10695
  }
10696
+ if (snapshot) messages.push(...snapshot.messages.filter((message) => message.role === "user" && message.content.startsWith("[compacted-context]")).map((message) => ({ role: "user", content: message.content })));
10319
10697
  }
10320
10698
  return { messages, steps, toolResults, truncatedResults, legacyMcpResults };
10321
10699
  }
@@ -11201,8 +11579,8 @@ function tokenizeCommand(command) {
11201
11579
  function renderCommandLine(plan) {
11202
11580
  return [plan.command, ...plan.args].join(" ");
11203
11581
  }
11204
- function parseTestOutput(runner, stdout6, stderr, workspace) {
11205
- const combined = [stdout6, stderr].filter(Boolean).join("\n");
11582
+ function parseTestOutput(runner, stdout6, stderr2, workspace) {
11583
+ const combined = [stdout6, stderr2].filter(Boolean).join("\n");
11206
11584
  if (runner === "vitest" || runner === "jest") {
11207
11585
  const report = extractJestStyleReport(stdout6) ?? extractJestStyleReport(combined);
11208
11586
  if (report) return parseJestStyleReport(report, workspace);
@@ -12648,64 +13026,67 @@ function decodeCodePoint(value, fallback) {
12648
13026
  // src/tools/workspace.ts
12649
13027
  function createDefaultToolRegistry() {
12650
13028
  const { todoWriteTool, todoReadTool } = createTodoTools();
12651
- 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);
12652
13030
  }
12653
13031
 
12654
13032
  // src/commands/doctor.ts
12655
13033
  function registerDoctorCommand(program2) {
12656
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) => {
12657
- const workspace = resolve7(options.workspace);
12658
- const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
12659
- const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
12660
- const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
12661
- const config = await loadConfig(workspace).catch(() => null);
12662
- const configExists = await hasConfig(workspace);
12663
- const isGitRepo = await pathExists(resolve7(workspace, ".git"));
12664
- const ultracCredentials = await resolveUltracCredentials();
12665
- const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
12666
- const tools = createDefaultToolRegistry().list();
12667
- const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
12668
- const checks = [
12669
- { label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
12670
- {
12671
- label: "workspace",
12672
- value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
12673
- ok: workspaceReady
12674
- },
12675
- { label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
12676
- {
12677
- label: "config",
12678
- value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
12679
- ok: config !== null
12680
- },
12681
- {
12682
- label: "tool schemas",
12683
- value: `${tools.length} exported`,
12684
- ok: toolSchemasReady
12685
- },
12686
- { label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
12687
- { label: "model", value: config?.model ?? "unavailable", ok: config !== null },
12688
- {
12689
- label: "Ultrac sign-in",
12690
- value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
12691
- ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
12692
- },
12693
- {
12694
- label: "Ultrac API base URL",
12695
- value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
12696
- ok: true
12697
- }
12698
- ];
12699
- if (options.json) {
12700
- console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
12701
- } else {
12702
- checks.forEach((check) => printCheck(check));
12703
- }
12704
- if (checks.some((check) => !check.ok)) {
12705
- process.exitCode = 1;
12706
- }
13035
+ await runDoctor(options.workspace, options.json);
12707
13036
  });
12708
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
+ }
12709
13090
  async function pathExists(path) {
12710
13091
  try {
12711
13092
  await access2(path);
@@ -13522,7 +13903,10 @@ function buildHarnessManifest() {
13522
13903
  }
13523
13904
 
13524
13905
  // src/commands/mcp.ts
13525
- import { resolve as resolve9 } from "path";
13906
+ import { resolve as resolve9, dirname as dirname12, join as join12 } from "path";
13907
+ import { constants as constants5 } from "fs";
13908
+ import { lstat as lstat3, mkdir as mkdir14, open as open8, rename as rename2, unlink as unlink3 } from "fs/promises";
13909
+ import { randomUUID as randomUUID4 } from "crypto";
13526
13910
  import chalk5 from "chalk";
13527
13911
 
13528
13912
  // src/mcp/client.ts
@@ -14348,7 +14732,47 @@ function asError(error) {
14348
14732
 
14349
14733
  // src/commands/mcp.ts
14350
14734
  function registerMcpCommand(program2) {
14351
- const mcpCommand = program2.command("mcp").description("Inspect the MCP servers this workspace connects to");
14735
+ const mcpCommand = program2.command("mcp").description("Configure and inspect workspace MCP servers");
14736
+ mcpCommand.command("add <name> <command> [args...]").description("Add a stdio MCP server without starting it").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--env <NAME=value>", "Explicit child environment value; prefer --env-pass for credentials", collect, []).option("--env-pass <NAME>", "Forward a named parent environment variable", collect, []).option("--cwd <path>", "Server working directory").option("--timeout <milliseconds>", "Per-request timeout", "30000").option("--disabled", "Keep the server disabled", false).option("--force", "Replace an existing server with this name", false).action(async (name, command, args, options) => {
14737
+ validateServerName(name);
14738
+ const env2 = {};
14739
+ for (const entry of options.env) {
14740
+ const separator = entry.indexOf("=");
14741
+ if (separator < 1) throw new Error("--env must use NAME=value.");
14742
+ const key = entry.slice(0, separator);
14743
+ validateEnvironmentName(key);
14744
+ Object.defineProperty(env2, key, { value: entry.slice(separator + 1), enumerable: true, configurable: true });
14745
+ }
14746
+ options.envPass.forEach(validateEnvironmentName);
14747
+ const spec = McpServerSchema.parse({
14748
+ command,
14749
+ args,
14750
+ env: env2,
14751
+ envPassthrough: [...new Set(options.envPass)],
14752
+ cwd: options.cwd,
14753
+ enabled: !options.disabled,
14754
+ timeoutMs: Number(options.timeout)
14755
+ });
14756
+ const workspace = await assertWorkspaceExists(options.workspace);
14757
+ await updateMcpConfig(workspace, (config) => {
14758
+ const servers = { ...config.mcpServers ?? {} };
14759
+ if (Object.hasOwn(servers, name) && !options.force) throw new Error(`MCP server ${name} already exists. Use --force to replace it.`);
14760
+ servers[name] = spec;
14761
+ return { ...config, mcpServers: servers };
14762
+ });
14763
+ console.log(`Added MCP server ${name}. Run ultrac trust in this workspace to approve the updated settings.`);
14764
+ });
14765
+ mcpCommand.command("remove <name>").description("Remove a workspace MCP server").option("-w, --workspace <path>", "Workspace directory", process.cwd()).action(async (name, options) => {
14766
+ validateServerName(name);
14767
+ const workspace = await assertWorkspaceExists(options.workspace);
14768
+ await updateMcpConfig(workspace, (config) => {
14769
+ const servers = { ...config.mcpServers ?? {} };
14770
+ if (!Object.hasOwn(servers, name)) throw new Error(`No MCP server named ${name}.`);
14771
+ delete servers[name];
14772
+ return { ...config, mcpServers: servers };
14773
+ });
14774
+ console.log(`Removed MCP server ${name}. Reapprove changed workspace settings with ultrac trust if required.`);
14775
+ });
14352
14776
  mcpCommand.command("list", { isDefault: true }).description("Connect every configured server and report what it exports").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--json", "Print the result as JSON", false).action(async (options) => {
14353
14777
  const workspace = resolve9(options.workspace);
14354
14778
  const loaded = await loadConfigWithTrust(workspace);
@@ -14374,6 +14798,7 @@ function registerMcpCommand(program2) {
14374
14798
  }
14375
14799
  const runtime = await withRuntime(workspace, servers, (active) => active);
14376
14800
  try {
14801
+ if (runtime.statuses.some((status) => !status.ok)) process.exitCode = 1;
14377
14802
  if (options.json) {
14378
14803
  console.log(JSON.stringify({ servers: runtime.statuses }, null, 2));
14379
14804
  return;
@@ -14396,7 +14821,7 @@ function registerMcpCommand(program2) {
14396
14821
  runtime.close();
14397
14822
  }
14398
14823
  });
14399
- mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").action(async (server, tool, options) => {
14824
+ mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").option("--json", "Print the result as JSON", false).action(async (server, tool, options) => {
14400
14825
  const workspace = resolve9(options.workspace);
14401
14826
  const loaded = await loadConfigWithTrust(workspace);
14402
14827
  const spec = loaded.config.mcpServers?.[server];
@@ -14414,6 +14839,7 @@ function registerMcpCommand(program2) {
14414
14839
  } catch (error) {
14415
14840
  throw new Error(`--input must be a JSON object: ${error instanceof Error ? error.message : String(error)}`);
14416
14841
  }
14842
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must be a JSON object.");
14417
14843
  const runtime = await withRuntime(workspace, { [server]: spec }, (active) => active);
14418
14844
  try {
14419
14845
  const status = runtime.statuses[0];
@@ -14438,6 +14864,68 @@ function registerMcpCommand(program2) {
14438
14864
  }
14439
14865
  });
14440
14866
  }
14867
+ function collect(value, previous) {
14868
+ return [...previous, value];
14869
+ }
14870
+ function validateServerName(name) {
14871
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(name) || name.includes("__") || ["constructor", "prototype"].includes(name)) {
14872
+ throw new Error("MCP server names must contain letters, numbers, dots, single underscores, or hyphens.");
14873
+ }
14874
+ }
14875
+ function validateEnvironmentName(name) {
14876
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("Environment variable names must contain letters, numbers, or underscores.");
14877
+ }
14878
+ async function updateMcpConfig(workspace, update) {
14879
+ const target = configPath(workspace);
14880
+ await resolveInsideWorkspaceReal(workspace, target);
14881
+ await mkdir14(dirname12(target), { recursive: true, mode: 448 });
14882
+ await resolveInsideWorkspaceReal(workspace, target);
14883
+ const lockPath = `${target}.mcp-lock`;
14884
+ const lock = await open8(lockPath, "wx", 384).catch((error) => {
14885
+ if (error.code === "EEXIST") throw new Error("Another MCP configuration update is in progress.");
14886
+ throw error;
14887
+ });
14888
+ const temporary = join12(dirname12(target), `.mcp-config-${randomUUID4()}.tmp`);
14889
+ try {
14890
+ let config = defaultConfig();
14891
+ const existing = await lstat3(target).catch((error) => {
14892
+ if (error.code === "ENOENT") return null;
14893
+ throw error;
14894
+ });
14895
+ if (existing) {
14896
+ if (!existing.isFile() || existing.isSymbolicLink() || existing.size > 1048576) throw new Error("MCP config must be a regular JSON file no larger than 1 MiB.");
14897
+ const handle2 = await open8(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
14898
+ try {
14899
+ const content = await handle2.readFile("utf8");
14900
+ try {
14901
+ config = JSON.parse(content);
14902
+ } catch {
14903
+ throw new Error("Workspace config is not valid JSON.");
14904
+ }
14905
+ } finally {
14906
+ await handle2.close();
14907
+ }
14908
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Workspace config must be a JSON object.");
14909
+ if (!UltracConfigSchema.safeParse(config).success) throw new Error("Workspace config does not match the Ultrac configuration schema.");
14910
+ }
14911
+ const next = update(config);
14912
+ UltracConfigSchema.parse(next);
14913
+ const handle = await open8(temporary, "wx", 384);
14914
+ try {
14915
+ await handle.writeFile(`${JSON.stringify(next, null, 2)}
14916
+ `, "utf8");
14917
+ await handle.sync();
14918
+ } finally {
14919
+ await handle.close();
14920
+ }
14921
+ await resolveInsideWorkspaceReal(workspace, target);
14922
+ await rename2(temporary, target);
14923
+ } finally {
14924
+ await unlink3(temporary).catch(() => void 0);
14925
+ await lock.close();
14926
+ await unlink3(lockPath).catch(() => void 0);
14927
+ }
14928
+ }
14441
14929
  async function withRuntime(workspace, servers, use) {
14442
14930
  const packageInfo = readPackageInfo();
14443
14931
  const runtime = await connectMcpServers(servers, {
@@ -14554,7 +15042,7 @@ import chalk6 from "chalk";
14554
15042
 
14555
15043
  // src/notify/config.ts
14556
15044
  import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
14557
- import { join as join12 } from "path";
15045
+ import { join as join13 } from "path";
14558
15046
  import { z as z31 } from "zod";
14559
15047
  var TelegramSchema = z31.object({
14560
15048
  botToken: z31.string().min(1),
@@ -14580,7 +15068,7 @@ var NotifyConfigSchema = z31.object({
14580
15068
  answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
14581
15069
  });
14582
15070
  function notifyConfigPath() {
14583
- return join12(ultracHomeDir(), "notify.json");
15071
+ return join13(ultracHomeDir(), "notify.json");
14584
15072
  }
14585
15073
  async function readStoredNotifyConfig() {
14586
15074
  let text;
@@ -15019,8 +15507,8 @@ function registerNotifyCommand(program2) {
15019
15507
  }
15020
15508
 
15021
15509
  // src/commands/repl.ts
15022
- import { createInterface as createInterface3 } from "readline";
15023
- import { stdin as stdin5, stdout as stdout5 } from "process";
15510
+ import { createInterface as createInterface3, emitKeypressEvents } from "readline";
15511
+ import { stdin as stdin5, stdout as stdout5, stderr } from "process";
15024
15512
  import chalk11 from "chalk";
15025
15513
 
15026
15514
  // src/config/budget.ts
@@ -15197,6 +15685,9 @@ function registerTrustCommand(program2) {
15197
15685
  // src/core/manual-compaction.ts
15198
15686
  async function compactManually(messages, pinned, provider, model, focus = "", signal) {
15199
15687
  const retained = new Set(pinned);
15688
+ const units = completeContextUnits(messages);
15689
+ if (!units) throw new Error("Compaction found incomplete tool units; original context was kept.");
15690
+ for (const unit of units) if (unit.some((message) => retained.has(message))) for (const message of unit) retained.add(message);
15200
15691
  const history = messages.filter(
15201
15692
  (message) => message.role !== "system" && !retained.has(message) && !isCompactionSummary(message)
15202
15693
  );
@@ -15252,6 +15743,10 @@ Give extra attention to this user-supplied focus: ${focus}` : ""
15252
15743
  }
15253
15744
 
15254
15745
  // src/core/interactive.ts
15746
+ var SKIPPED_FOR_QUEUED_MESSAGE = "Skipped; a new user message was queued after the previous tool. Follow that message instead of retrying this call.";
15747
+ function skippedForQueuedMessage(tool) {
15748
+ return { tool, ok: false, content: SKIPPED_FOR_QUEUED_MESSAGE };
15749
+ }
15255
15750
  function permissionDeniedMessage(tool, permission, sticky) {
15256
15751
  const denied = `User denied ${permission} permission for ${tool}.`;
15257
15752
  if (!sticky) return denied;
@@ -15310,6 +15805,7 @@ var InteractiveSession = class {
15310
15805
  started = false;
15311
15806
  /** How the last turn ended. Absent until one has. */
15312
15807
  lastStopReason;
15808
+ periodicCompactor = new PeriodicCompactor();
15313
15809
  pinnedMessages = [];
15314
15810
  /** Tools described in this turn's system prompt. `/mode` can change it. */
15315
15811
  advertisedTools() {
@@ -15348,6 +15844,7 @@ var InteractiveSession = class {
15348
15844
  }
15349
15845
  async restoreSessions(sessions) {
15350
15846
  if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
15847
+ for (const saved of sessions) readSessionContextSnapshot(saved);
15351
15848
  await this.start();
15352
15849
  this.parentSessionId = sessions.at(-1)?.id;
15353
15850
  this.firstTask = sessions.at(-1)?.task ?? "";
@@ -15368,6 +15865,17 @@ var InteractiveSession = class {
15368
15865
  for (const { call, result } of step.toolResults) this.messages.push({ role: "tool", content: JSON.stringify({ call, result: capResultForTranscript(result, this.options.maxToolResultChars) }) });
15369
15866
  }
15370
15867
  }
15868
+ const snapshot = sessions.at(-1)?.contextSnapshot;
15869
+ if (snapshot && snapshot.kind !== "text") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
15870
+ if (snapshot?.kind === "text") {
15871
+ this.messages.splice(0, this.messages.length, ...snapshot.messages);
15872
+ this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
15873
+ for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
15874
+ const message = { role: "user", content: constraint.content };
15875
+ this.messages.push(message);
15876
+ this.pinnedMessages.push(message);
15877
+ }
15878
+ }
15371
15879
  }
15372
15880
  contextStatus() {
15373
15881
  const mode = getAgentModePreset(this.activeMode);
@@ -15430,6 +15938,7 @@ Acknowledge that you have the context and wait for the user's first request.`
15430
15938
  try {
15431
15939
  await handlers.onCheckpoint(this.toSession());
15432
15940
  } catch {
15941
+ diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
15433
15942
  }
15434
15943
  }
15435
15944
  appendQueuedMessages(handlers) {
@@ -15472,6 +15981,11 @@ Acknowledge that you have the context and wait for the user's first request.`
15472
15981
  modePreset
15473
15982
  );
15474
15983
  const turn = this.steps.length + 1;
15984
+ const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
15985
+ if (periodic.compacted) {
15986
+ this.messages.splice(0, this.messages.length, ...periodic.messages);
15987
+ await this.checkpoint(handlers);
15988
+ }
15475
15989
  await this.compactIfNeeded(system, handlers, turn);
15476
15990
  handlers.onModelStart?.();
15477
15991
  let text;
@@ -15512,12 +16026,13 @@ Acknowledge that you have the context and wait for the user's first request.`
15512
16026
  this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
15513
16027
  this.steps.push(step);
15514
16028
  await this.checkpoint(handlers);
15515
- for (const call of parsed.toolCalls) {
16029
+ for (let callIndex = 0; callIndex < parsed.toolCalls.length; callIndex += 1) {
16030
+ const call = parsed.toolCalls[callIndex];
15516
16031
  if (handlers.signal?.aborted) break;
15517
16032
  const authorization = await this.authorize(call, handlers);
15518
16033
  if (handlers.signal?.aborted) break;
15519
16034
  if (authorization.denied) {
15520
- const result2 = {
16035
+ const result = {
15521
16036
  tool: call.tool,
15522
16037
  ok: false,
15523
16038
  content: authorization.message ?? permissionDeniedMessage(
@@ -15527,24 +16042,33 @@ Acknowledge that you have the context and wait for the user's first request.`
15527
16042
  )
15528
16043
  };
15529
16044
  if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
15530
- step.toolResults.push({ call, result: result2 });
15531
- this.pushToolResult(call, result2);
16045
+ step.toolResults.push({ call, result });
16046
+ this.pushToolResult(call, result);
16047
+ await this.checkpoint(handlers);
16048
+ } else {
16049
+ handlers.onToolStart?.(call);
16050
+ const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
16051
+ const rawResult = await this.registry.execute(call, {
16052
+ workspace: this.options.workspace,
16053
+ permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
16054
+ ...handlers.signal ? { signal: handlers.signal } : {},
16055
+ ...modeAllowlist ? { modeAllowlist } : {}
16056
+ });
16057
+ const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
16058
+ handlers.onToolResult?.(call, result);
16059
+ step.toolResults.push({ call, result });
16060
+ this.pushToolResult(call, result);
15532
16061
  await this.checkpoint(handlers);
15533
- continue;
15534
16062
  }
15535
- handlers.onToolStart?.(call);
15536
- const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
15537
- const rawResult = await this.registry.execute(call, {
15538
- workspace: this.options.workspace,
15539
- permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
15540
- ...handlers.signal ? { signal: handlers.signal } : {},
15541
- ...modeAllowlist ? { modeAllowlist } : {}
15542
- });
15543
- const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
15544
- handlers.onToolResult?.(call, result);
15545
- step.toolResults.push({ call, result });
15546
- this.pushToolResult(call, result);
15547
- await this.checkpoint(handlers);
16063
+ if (this.appendQueuedMessages(handlers) > 0) {
16064
+ for (const skipped of parsed.toolCalls.slice(callIndex + 1)) {
16065
+ const result = skippedForQueuedMessage(skipped.tool);
16066
+ step.toolResults.push({ call: skipped, result });
16067
+ this.pushToolResult(skipped, result);
16068
+ await this.checkpoint(handlers);
16069
+ }
16070
+ break;
16071
+ }
15548
16072
  }
15549
16073
  if (handlers.signal?.aborted) {
15550
16074
  stopReason = "aborted";
@@ -15604,7 +16128,7 @@ Acknowledge that you have the context and wait for the user's first request.`
15604
16128
  mode: this.activeMode,
15605
16129
  purpose: "turn"
15606
16130
  }),
15607
- // The turn's own signal: a Ctrl-C abandons the summary, not the turn.
16131
+ // The turn's own signal: Esc abandons the summary, not the turn.
15608
16132
  ...handlers.signal ? { signal: handlers.signal } : {}
15609
16133
  }
15610
16134
  );
@@ -15751,6 +16275,7 @@ Acknowledge that you have the context and wait for the user's first request.`
15751
16275
  constraintVersion: 1,
15752
16276
  userConstraints: [...this.userConstraints],
15753
16277
  mcpResultArchiveVersion: 1,
16278
+ ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
15754
16279
  // How the last turn ended, so a saved interactive transcript says whether
15755
16280
  // its final message is an answer or the place it ran out of room - the
15756
16281
  // same question `ultrac run` already answers for a one-shot run.
@@ -15803,6 +16328,7 @@ var NativeSession = class {
15803
16328
  started = false;
15804
16329
  /** How the last turn ended. Absent until one has. */
15805
16330
  lastStopReason;
16331
+ periodicCompactor = new PeriodicCompactor();
15806
16332
  pinnedMessages = [];
15807
16333
  /**
15808
16334
  * The tools this turn advertises. Resolved per turn rather than once in the
@@ -15844,6 +16370,7 @@ var NativeSession = class {
15844
16370
  }
15845
16371
  async restoreSessions(sessions) {
15846
16372
  if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
16373
+ for (const saved of sessions) readSessionContextSnapshot(saved);
15847
16374
  await this.start();
15848
16375
  this.parentSessionId = sessions.at(-1)?.id;
15849
16376
  this.firstTask = sessions.at(-1)?.task ?? "";
@@ -15871,6 +16398,17 @@ var NativeSession = class {
15871
16398
  }
15872
16399
  }
15873
16400
  }
16401
+ const snapshot = sessions.at(-1)?.contextSnapshot;
16402
+ if (snapshot && snapshot.kind !== "native") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
16403
+ if (snapshot?.kind === "native") {
16404
+ this.messages.splice(0, this.messages.length, ...snapshot.messages);
16405
+ this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
16406
+ for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
16407
+ const message = { role: "user", content: constraint.content };
16408
+ this.messages.push(message);
16409
+ this.pinnedMessages.push(message);
16410
+ }
16411
+ }
15874
16412
  }
15875
16413
  contextStatus() {
15876
16414
  const system = this.systemPrompt();
@@ -15931,6 +16469,7 @@ ${context}`
15931
16469
  try {
15932
16470
  await handlers.onCheckpoint(this.toSession());
15933
16471
  } catch {
16472
+ diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
15934
16473
  }
15935
16474
  }
15936
16475
  appendQueuedMessages(handlers) {
@@ -15969,6 +16508,11 @@ ${context}`
15969
16508
  this.refreshSystemPrompt();
15970
16509
  const tools = toToolSchemas(this.advertisedTools());
15971
16510
  const turn = this.steps.length + 1;
16511
+ const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
16512
+ if (periodic.compacted) {
16513
+ this.messages.splice(0, this.messages.length, ...periodic.messages);
16514
+ await this.checkpoint(handlers);
16515
+ }
15972
16516
  await this.compactIfNeeded(tools, handlers, turn);
15973
16517
  handlers.onModelStart?.();
15974
16518
  let response;
@@ -16029,13 +16573,14 @@ ${context}`
16029
16573
  }
16030
16574
  continue;
16031
16575
  }
16032
- for (const call of response.toolCalls) {
16576
+ for (let callIndex = 0; callIndex < response.toolCalls.length; callIndex += 1) {
16577
+ const call = response.toolCalls[callIndex];
16033
16578
  if (handlers.signal?.aborted) break;
16034
16579
  const displayCall = { tool: call.name, input: safeParseArgs(call.arguments) };
16035
16580
  const authorization = await this.authorize(call.name, handlers, displayCall);
16036
16581
  if (handlers.signal?.aborted) break;
16037
16582
  if (authorization.denied) {
16038
- const result2 = {
16583
+ const result = {
16039
16584
  tool: call.name,
16040
16585
  ok: false,
16041
16586
  content: authorization.message ?? permissionDeniedMessage(
@@ -16045,27 +16590,37 @@ ${context}`
16045
16590
  )
16046
16591
  };
16047
16592
  if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
16048
- step.toolResults.push({ call: displayCall, result: result2 });
16049
- this.pushToolResult(call, result2);
16593
+ step.toolResults.push({ call: displayCall, result });
16594
+ this.pushToolResult(call, result);
16595
+ await this.checkpoint(handlers);
16596
+ } else {
16597
+ handlers.onToolStart?.(displayCall);
16598
+ const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
16599
+ const rawResult = await this.registry.execute(
16600
+ { tool: call.name, input: displayCall.input },
16601
+ {
16602
+ workspace: this.options.workspace,
16603
+ permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
16604
+ ...handlers.signal ? { signal: handlers.signal } : {},
16605
+ ...modeAllowlist ? { modeAllowlist } : {}
16606
+ }
16607
+ );
16608
+ const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
16609
+ handlers.onToolResult?.(displayCall, result);
16610
+ step.toolResults.push({ call: displayCall, result });
16611
+ this.pushToolResult(call, result);
16050
16612
  await this.checkpoint(handlers);
16051
- continue;
16052
16613
  }
16053
- handlers.onToolStart?.(displayCall);
16054
- const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
16055
- const rawResult = await this.registry.execute(
16056
- { tool: call.name, input: displayCall.input },
16057
- {
16058
- workspace: this.options.workspace,
16059
- permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
16060
- ...handlers.signal ? { signal: handlers.signal } : {},
16061
- ...modeAllowlist ? { modeAllowlist } : {}
16614
+ if (this.appendQueuedMessages(handlers) > 0) {
16615
+ for (const skipped of response.toolCalls.slice(callIndex + 1)) {
16616
+ const skippedCall = { tool: skipped.name, input: safeParseArgs(skipped.arguments) };
16617
+ const result = skippedForQueuedMessage(skipped.name);
16618
+ step.toolResults.push({ call: skippedCall, result });
16619
+ this.pushToolResult(skipped, result);
16620
+ await this.checkpoint(handlers);
16062
16621
  }
16063
- );
16064
- const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
16065
- handlers.onToolResult?.(displayCall, result);
16066
- step.toolResults.push({ call: displayCall, result });
16067
- this.pushToolResult(call, result);
16068
- await this.checkpoint(handlers);
16622
+ break;
16623
+ }
16069
16624
  }
16070
16625
  if (handlers.signal?.aborted) {
16071
16626
  stopReason = "aborted";
@@ -16121,7 +16676,7 @@ ${context}`
16121
16676
  mode: this.activeMode,
16122
16677
  purpose: "turn"
16123
16678
  }),
16124
- // The turn's own signal: a Ctrl-C abandons the summary, not the turn.
16679
+ // The turn's own signal: Esc abandons the summary, not the turn.
16125
16680
  ...handlers.signal ? { signal: handlers.signal } : {}
16126
16681
  }
16127
16682
  );
@@ -16285,6 +16840,7 @@ ${guidance.join("\n")}` : ""}`;
16285
16840
  constraintVersion: 1,
16286
16841
  userConstraints: [...this.userConstraints],
16287
16842
  mcpResultArchiveVersion: 1,
16843
+ ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16288
16844
  // How the last turn ended, so a saved interactive transcript says whether
16289
16845
  // its final message is an answer or the place it ran out of room - the
16290
16846
  // same question `ultrac run` already answers for a one-shot run.
@@ -16308,6 +16864,152 @@ function noteNativeCompactionFallback(reason) {
16308
16864
  );
16309
16865
  }
16310
16866
 
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
+
16311
17013
  // src/commands/session-checkpoint.ts
16312
17014
  var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
16313
17015
  function createSessionCheckpointWriter(workspace, onFailure) {
@@ -16335,6 +17037,16 @@ function createSessionCheckpointWriter(workspace, onFailure) {
16335
17037
  return Object.assign(writeCheckpoint, { warnFailure });
16336
17038
  }
16337
17039
 
17040
+ // src/ui/ghost.ts
17041
+ var quietSessions = /* @__PURE__ */ new WeakSet();
17042
+ function isGhostEnabled(session) {
17043
+ return quietSessions.has(session);
17044
+ }
17045
+ function setGhostEnabled(session, enabled) {
17046
+ if (enabled) quietSessions.add(session);
17047
+ else quietSessions.delete(session);
17048
+ }
17049
+
16338
17050
  // src/ui/output.ts
16339
17051
  import chalk8 from "chalk";
16340
17052
  var stopReasonDetail = {
@@ -16348,15 +17060,15 @@ function stopReasonLine(stopReason) {
16348
17060
  const detail = stopReasonDetail[reason] ?? "partial answer";
16349
17061
  return `Stopped: ${reason} (${detail})`;
16350
17062
  }
16351
- function printSession(session, sessionPath2) {
17063
+ function printSession(session, sessionPath2, options = {}) {
16352
17064
  for (const step of session.steps) {
16353
17065
  if (step.parsed?.message) {
16354
17066
  console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
16355
17067
  }
16356
17068
  for (const toolResult of step.toolResults) {
16357
17069
  const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
16358
- console.log(chalk8.cyan(toolResult.call.tool), status);
16359
- console.log(chalk8.gray(indent2(toolResult.result.content)));
17070
+ console.log(chalk8.white(toolResult.call.tool), status);
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." }))));
16360
17072
  }
16361
17073
  }
16362
17074
  if (session.finalMessage) {
@@ -16375,279 +17087,92 @@ function printTools(tools) {
16375
17087
  for (const tool of tools) {
16376
17088
  const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
16377
17089
  const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
16378
- console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
17090
+ console.log(`${chalk8.white(tool.name)}${permissionText}${optionalText}`);
16379
17091
  console.log(chalk8.gray(indent2(tool.description)));
16380
17092
  }
16381
17093
  }
16382
- function printToolResult(result) {
17094
+ function printToolResult(result, options = {}) {
16383
17095
  const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
16384
- console.log(`${chalk8.cyan(result.tool)} ${status}`);
16385
- console.log(chalk8.gray(indent2(result.content)));
17096
+ console.log(`${chalk8.white(result.tool)} ${status}`);
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." }))));
16386
17098
  }
16387
17099
  function indent2(value) {
16388
17100
  return value.split("\n").map((line) => ` ${line}`).join("\n");
16389
17101
  }
17102
+ function formatToolOutput(content, options = {}) {
17103
+ if (options.verbose) return content;
17104
+ const lines = content.split(/\r?\n/);
17105
+ const shown = lines.length <= 6 ? lines : options.failed ? [...lines.slice(0, 3), ...lines.slice(-3)] : lines.slice(0, 6);
17106
+ const clipped = shown.some((line) => line.length > 200);
17107
+ const preview = shown.map((line) => line.length > 200 ? `${line.slice(0, 199)}\u2026` : line).join("\n");
17108
+ const hidden = lines.length - shown.length;
17109
+ if (!hidden && !clipped) return preview;
17110
+ return `${preview}
17111
+ \u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
17112
+ }
16390
17113
 
16391
- // src/skills/commands.ts
16392
- var BUILTIN_SLASH_COMMANDS = [
16393
- "usage",
16394
- "max-steps",
16395
- "resume",
16396
- "deny",
16397
- "plan",
16398
- "review",
16399
- "coding",
16400
- "status",
16401
- "context",
16402
- "compact",
16403
- "diff",
16404
- "sessions",
16405
- "save",
16406
- "test",
16407
- "init",
16408
- "new",
16409
- "allow",
16410
- "clear",
16411
- "exit",
16412
- "help",
16413
- "memory",
16414
- "mode",
16415
- "model",
16416
- "permissions",
16417
- "quit",
16418
- "skills",
16419
- "tools",
16420
- "whoami"
16421
- ];
16422
- var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16423
- var MAX_COMMAND_LENGTH = 64;
16424
- function parseSlashInput(input) {
16425
- const trimmed = input.trim();
16426
- if (!trimmed.startsWith("/")) return null;
16427
- const rest = trimmed.slice(1);
16428
- const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
16429
- if (!match) return null;
16430
- return { command: match[1], args: (match[2] ?? "").trim() };
17114
+ // src/ui/progress.ts
17115
+ import { stdout as stdout3 } from "process";
17116
+ import chalk9 from "chalk";
17117
+ var PENDING_GLYPH = "\u25CB";
17118
+ var DONE_GLYPH = "\u25CF";
17119
+ var REPAINT_MS = 100;
17120
+ function formatTokens(tokens) {
17121
+ if (!Number.isFinite(tokens) || tokens <= 0) return "0";
17122
+ if (tokens < 1e3) return String(Math.floor(tokens));
17123
+ if (tokens < 1e6) {
17124
+ const tenths2 = Math.floor(tokens / 100);
17125
+ return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
17126
+ }
17127
+ const tenths = Math.floor(tokens / 1e5);
17128
+ return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
16431
17129
  }
16432
- function isBuiltinSlashCommand(command) {
16433
- return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
17130
+ function formatElapsed(ms) {
17131
+ if (!Number.isFinite(ms) || ms <= 0) return "0s";
17132
+ const totalSeconds = Math.floor(ms / 1e3);
17133
+ const seconds = totalSeconds % 60;
17134
+ const totalMinutes = Math.floor(totalSeconds / 60);
17135
+ const minutes = totalMinutes % 60;
17136
+ const hours = Math.floor(totalMinutes / 60);
17137
+ if (totalMinutes === 0) return `${seconds}s`;
17138
+ if (hours === 0) return `${minutes}m ${seconds}s`;
17139
+ return `${hours}h ${String(minutes).padStart(2, "0")}m`;
16434
17140
  }
16435
- function normalizeCommandName(value) {
16436
- const candidate = value.trim().replace(/^\/+/, "").trim();
16437
- if (!candidate) {
16438
- throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
17141
+ function formatProgressLine(snapshot) {
17142
+ const finished = snapshot.done >= snapshot.total;
17143
+ const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.white(PENDING_GLYPH);
17144
+ const noun = snapshot.total === 1 ? "agent" : "agents";
17145
+ const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
17146
+ if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
17147
+ const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
17148
+ return ` ${glyph} ${chalk9.white(snapshot.label)} ${snapshot.description} ${rendered}`;
17149
+ }
17150
+ var ProgressLine = class {
17151
+ constructor(label, description, total) {
17152
+ this.label = label;
17153
+ this.description = description;
17154
+ this.total = total;
16439
17155
  }
16440
- if (candidate.length > MAX_COMMAND_LENGTH) {
16441
- throw new Error(
16442
- `Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
16443
- );
17156
+ label;
17157
+ description;
17158
+ total;
17159
+ timer;
17160
+ startedAt;
17161
+ doneCount = 0;
17162
+ tokens;
17163
+ /** True only while this instance owns a painted line it still has to release. */
17164
+ live = false;
17165
+ start() {
17166
+ if (this.startedAt !== void 0) return;
17167
+ this.startedAt = Date.now();
17168
+ if (!stdout3.isTTY) return;
17169
+ this.live = true;
17170
+ this.render();
17171
+ this.timer = setInterval(() => this.render(), REPAINT_MS);
16444
17172
  }
16445
- if (!COMMAND_NAME_RE.test(candidate)) {
16446
- throw new Error(
16447
- `Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
16448
- );
16449
- }
16450
- return candidate;
16451
- }
16452
- async function collectEntries(workspace) {
16453
- const skills = await discoverSkills(workspace);
16454
- const claimed = /* @__PURE__ */ new Map();
16455
- const conflicts = [];
16456
- for (const skill of skills) {
16457
- let declaration;
16458
- try {
16459
- declaration = skill.command ?? null;
16460
- } catch (error) {
16461
- conflicts.push({
16462
- command: "",
16463
- skillName: skill.name,
16464
- scope: skill.scope,
16465
- path: skill.path,
16466
- reason: "invalid",
16467
- message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
16468
- });
16469
- continue;
16470
- }
16471
- if (!declaration) continue;
16472
- let command;
16473
- try {
16474
- command = normalizeCommandName(declaration.raw);
16475
- } catch (error) {
16476
- conflicts.push({
16477
- command: declaration.raw,
16478
- skillName: skill.name,
16479
- scope: skill.scope,
16480
- path: skill.path,
16481
- reason: "invalid",
16482
- message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
16483
- });
16484
- continue;
16485
- }
16486
- if (isBuiltinSlashCommand(command)) {
16487
- conflicts.push({
16488
- command,
16489
- skillName: skill.name,
16490
- scope: skill.scope,
16491
- path: skill.path,
16492
- reason: "builtin",
16493
- 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}.`
16494
- });
16495
- continue;
16496
- }
16497
- const entry = {
16498
- skill,
16499
- command: {
16500
- command,
16501
- skillName: skill.name,
16502
- description: skill.description,
16503
- argumentHint: declaration.argumentHint,
16504
- scope: skill.scope,
16505
- path: skill.path
16506
- }
16507
- };
16508
- const existing = claimed.get(command);
16509
- if (!existing) {
16510
- claimed.set(command, entry);
16511
- continue;
16512
- }
16513
- const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
16514
- const loser = winner === entry ? existing : entry;
16515
- claimed.set(command, winner);
16516
- conflicts.push({
16517
- command,
16518
- skillName: loser.skill.name,
16519
- scope: loser.skill.scope,
16520
- path: loser.skill.path,
16521
- reason: "duplicate",
16522
- 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}.`
16523
- });
16524
- }
16525
- const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
16526
- return { entries, conflicts };
16527
- }
16528
- async function listSkillCommands(workspace) {
16529
- const { entries, conflicts } = await collectEntries(workspace);
16530
- return { commands: entries.map((entry) => entry.command), conflicts };
16531
- }
16532
- function buildSkillCommandPrompt(skill, command, args) {
16533
- const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
16534
- const header = [
16535
- `The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
16536
- skill.description ? `Description: ${skill.description}` : void 0,
16537
- argumentLine,
16538
- skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
16539
- "Follow the instructions below now, applying the arguments where they are relevant."
16540
- ].filter(Boolean).join("\n");
16541
- return `${header}
16542
-
16543
- --- instructions ---
16544
- ${skill.body}`;
16545
- }
16546
- async function resolveSkillCommand(workspace, input) {
16547
- const parsed = parseSlashInput(input);
16548
- if (!parsed) return { status: "not-a-command" };
16549
- const command = parsed.command.toLowerCase();
16550
- if (isBuiltinSlashCommand(command)) {
16551
- return { status: "builtin", command };
16552
- }
16553
- const { entries, conflicts } = await collectEntries(workspace);
16554
- const match = entries.find((entry) => entry.command.command === command);
16555
- if (!match) {
16556
- const related = conflicts.filter((conflict) => conflict.command === command);
16557
- if (related.length) {
16558
- return {
16559
- status: "conflict",
16560
- command,
16561
- message: related.map((conflict) => conflict.message).join("\n"),
16562
- conflicts: related
16563
- };
16564
- }
16565
- return {
16566
- status: "unknown",
16567
- command,
16568
- available: entries.map((entry) => entry.command.command)
16569
- };
16570
- }
16571
- return {
16572
- status: "ok",
16573
- command: match.command,
16574
- skill: match.skill,
16575
- args: parsed.args,
16576
- prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
16577
- };
16578
- }
16579
- function formatSkillCommandHelp(index) {
16580
- if (!index.commands.length) return "";
16581
- const lines = index.commands.map((command) => {
16582
- const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
16583
- const description = command.description || `Run the ${command.skillName} skill`;
16584
- return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
16585
- });
16586
- return ["Skill commands:", ...lines].join("\n");
16587
- }
16588
-
16589
- // src/ui/progress.ts
16590
- import { stdout as stdout3 } from "process";
16591
- import chalk9 from "chalk";
16592
- var PENDING_GLYPH = "\u25CB";
16593
- var DONE_GLYPH = "\u25CF";
16594
- var REPAINT_MS = 100;
16595
- function formatTokens(tokens) {
16596
- if (!Number.isFinite(tokens) || tokens <= 0) return "0";
16597
- if (tokens < 1e3) return String(Math.floor(tokens));
16598
- if (tokens < 1e6) {
16599
- const tenths2 = Math.floor(tokens / 100);
16600
- return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
16601
- }
16602
- const tenths = Math.floor(tokens / 1e5);
16603
- return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
16604
- }
16605
- function formatElapsed(ms) {
16606
- if (!Number.isFinite(ms) || ms <= 0) return "0s";
16607
- const totalSeconds = Math.floor(ms / 1e3);
16608
- const seconds = totalSeconds % 60;
16609
- const totalMinutes = Math.floor(totalSeconds / 60);
16610
- const minutes = totalMinutes % 60;
16611
- const hours = Math.floor(totalMinutes / 60);
16612
- if (totalMinutes === 0) return `${seconds}s`;
16613
- if (hours === 0) return `${minutes}m ${seconds}s`;
16614
- return `${hours}h ${String(minutes).padStart(2, "0")}m`;
16615
- }
16616
- function formatProgressLine(snapshot) {
16617
- const finished = snapshot.done >= snapshot.total;
16618
- const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
16619
- const noun = snapshot.total === 1 ? "agent" : "agents";
16620
- const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
16621
- if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
16622
- const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
16623
- return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
16624
- }
16625
- var ProgressLine = class {
16626
- constructor(label, description, total) {
16627
- this.label = label;
16628
- this.description = description;
16629
- this.total = total;
16630
- }
16631
- label;
16632
- description;
16633
- total;
16634
- timer;
16635
- startedAt;
16636
- doneCount = 0;
16637
- tokens;
16638
- /** True only while this instance owns a painted line it still has to release. */
16639
- live = false;
16640
- start() {
16641
- if (this.startedAt !== void 0) return;
16642
- this.startedAt = Date.now();
16643
- if (!stdout3.isTTY) return;
16644
- this.live = true;
16645
- this.render();
16646
- this.timer = setInterval(() => this.render(), REPAINT_MS);
16647
- }
16648
- /** One agent finished. */
16649
- complete() {
16650
- if (this.doneCount < this.total) this.doneCount += 1;
17173
+ /** One agent finished. */
17174
+ complete() {
17175
+ if (this.doneCount < this.total) this.doneCount += 1;
16651
17176
  }
16652
17177
  addTokens(tokens) {
16653
17178
  if (!Number.isFinite(tokens)) return;
@@ -16756,7 +17281,10 @@ var Spinner = class {
16756
17281
  label;
16757
17282
  timer;
16758
17283
  index = 0;
16759
- start() {
17284
+ holdWhile;
17285
+ held = false;
17286
+ start(holdWhile) {
17287
+ this.holdWhile = holdWhile;
16760
17288
  if (!stdout4.isTTY || this.timer) return;
16761
17289
  this.render();
16762
17290
  this.timer = setInterval(() => this.render(), 90);
@@ -16775,6 +17303,15 @@ var Spinner = class {
16775
17303
  }
16776
17304
  }
16777
17305
  render() {
17306
+ if (this.holdWhile?.()) {
17307
+ if (!this.held) {
17308
+ stdout4.clearLine(0);
17309
+ stdout4.cursorTo(0);
17310
+ this.held = true;
17311
+ }
17312
+ return;
17313
+ }
17314
+ this.held = false;
16778
17315
  this.index = (this.index + 1) % FRAMES.length;
16779
17316
  stdout4.clearLine(0);
16780
17317
  stdout4.cursorTo(0);
@@ -16853,6 +17390,682 @@ function humanize(value) {
16853
17390
  return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
16854
17391
  }
16855
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, " ");
17735
+ }
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;
17746
+ }
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();
18067
+ }
18068
+
16856
18069
  // src/ui/welcome.ts
16857
18070
  import chalk10, { Chalk } from "chalk";
16858
18071
  var LOGO = [
@@ -16866,76 +18079,75 @@ var LOGO = [
16866
18079
  function renderWelcome(info, options = {}) {
16867
18080
  const columns = normalizeColumns(options.columns);
16868
18081
  const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
16869
- const cardWidth = Math.min(88, Math.max(42, columns));
18082
+ const cardWidth = columns;
16870
18083
  const innerWidth = cardWidth - 4;
16871
18084
  const wide = columns >= 64;
16872
18085
  const lines = [];
16873
18086
  if (wide) {
16874
18087
  lines.push("");
16875
18088
  for (let index = 0; index < LOGO.length; index += 1) {
16876
- const color = index < 2 ? paint.hex("#38BDF8") : index < 4 ? paint.hex("#22D3EE") : paint.hex("#A78BFA");
16877
- lines.push(` ${color(LOGO[index])}`);
18089
+ lines.push(` ${paint.bold.white(LOGO[index])}`);
16878
18090
  }
16879
18091
  } else {
16880
- lines.push("", ` ${paint.bold.hex("#22D3EE")("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
18092
+ lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
16881
18093
  }
16882
18094
  const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
16883
18095
  const topFill = Math.max(0, cardWidth - topLabel.length - 2);
16884
- lines.push(paint.cyan(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
18096
+ lines.push(paint.gray(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
16885
18097
  lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
16886
18098
  lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
16887
18099
  lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
16888
- lines.push(paint.cyan(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
18100
+ lines.push(paint.gray(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
16889
18101
  lines.push(cardLine(info.budget, innerWidth, paint.gray));
16890
18102
  lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
16891
- lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
18103
+ lines.push(paint.gray(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
16892
18104
  lines.push(
16893
- wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("/exit quit")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C interrupt")}` : paint.gray(" /help \xB7 /exit \xB7 Ctrl-C")
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")
16894
18106
  );
16895
18107
  return lines.join("\n");
16896
18108
  }
16897
18109
  function renderComposer(columns = 80, color = true) {
16898
18110
  const paint = new Chalk({ level: color ? chalk10.level : 0 });
16899
- const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
18111
+ const width2 = normalizeColumns(columns);
16900
18112
  const label = " message ";
16901
- const fill = Math.max(1, width - label.length - 2);
18113
+ const fill = Math.max(1, width2 - label.length - 2);
16902
18114
  return {
16903
18115
  frame: `
16904
- ${paint.cyan(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
16905
- ${paint.cyan("\u2502")}`,
16906
- 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 ")
16907
18119
  };
16908
18120
  }
16909
- function cardLine(value, width, style) {
16910
- const clipped = truncate3(value, width);
16911
- 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`;
16912
18124
  }
16913
18125
  function normalizeColumns(columns) {
16914
- return Number.isFinite(columns) ? Math.max(28, Math.floor(columns)) : 80;
18126
+ return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
16915
18127
  }
16916
- function truncate3(value, width) {
16917
- if (value.length <= width) return value;
16918
- if (width <= 1) return "\u2026";
16919
- 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`;
16920
18132
  }
16921
- function shortenMiddle(value, width) {
16922
- if (value.length <= width) return value;
16923
- if (width < 9) return truncate3(value, width);
16924
- const side = Math.floor((width - 1) / 2);
16925
- 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))}`;
16926
18138
  }
16927
18139
 
16928
18140
  // src/update/check.ts
16929
18141
  import { spawn as spawn4 } from "child_process";
16930
18142
  import { randomBytes as randomBytes4 } from "crypto";
16931
- import { readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
16932
- import { join as join13 } from "path";
18143
+ import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
18144
+ import { join as join14 } from "path";
16933
18145
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
16934
18146
  var QUERY_TIMEOUT_MS = 5e3;
16935
18147
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
16936
18148
  async function checkForUpdate(options) {
16937
18149
  const now = options.now ?? Date.now();
16938
- const cachePath = options.cachePath ?? join13(await ensureUltracHome(), "update-check.json");
18150
+ const cachePath = options.cachePath ?? join14(await ensureUltracHome(), "update-check.json");
16939
18151
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
16940
18152
  let latestVersion;
16941
18153
  if (!options.force) {
@@ -17019,9 +18231,9 @@ async function writeCache(path, cache) {
17019
18231
  try {
17020
18232
  await writeFile12(temporary, `${JSON.stringify(cache)}
17021
18233
  `, { encoding: "utf8", mode: 384, flag: "wx" });
17022
- await rename2(temporary, path);
18234
+ await rename3(temporary, path);
17023
18235
  } catch {
17024
- await unlink3(temporary).catch(() => void 0);
18236
+ await unlink4(temporary).catch(() => void 0);
17025
18237
  }
17026
18238
  }
17027
18239
  async function runNpm(args, options) {
@@ -17057,6 +18269,10 @@ async function runNpm(args, options) {
17057
18269
  // src/commands/repl.ts
17058
18270
  var HELP = `Commands:
17059
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)
17060
18276
  /model [name] Show or set the model
17061
18277
  /mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
17062
18278
  /permissions [readonly|workspace|full] Show or set permissions
@@ -17079,11 +18295,19 @@ var HELP = `Commands:
17079
18295
  /allow <perm> Grant a permission for this session (write, shell, network)
17080
18296
  /whoami Show sign-in state
17081
18297
  /tools List available tools
18298
+ /logs Show the private diagnostic log path
18299
+ /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18300
+ /output [number|list] Inspect saved tool output (latest by default)
17082
18301
  /memory List and inspect stored memories
18302
+ /skill <name> [args] Run an installed skill
17083
18303
  /skills List installed skills
17084
18304
  /update Show how to install an available CLI update
17085
18305
  /clear Start a fresh conversation
17086
18306
  /exit, /quit End the session
18307
+ Keys:
18308
+ Type during a turn Queue a follow-up for after the current tool call
18309
+ Esc Stop the current turn; the session stays open
18310
+ Ctrl-C Exit and save the session
17087
18311
  Custom commands come from skills that declare a 'command:' key in their
17088
18312
  frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
17089
18313
  var EXIT_REPL_NOT_STARTED = 1;
@@ -17175,7 +18399,7 @@ async function runRepl(options, initialTask = "") {
17175
18399
  const hasFallbackChain = Boolean(config.fallback?.chain?.length);
17176
18400
  if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
17177
18401
  console.log(chalk11.yellow("You are not signed in to Ultrac."));
17178
- 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")}.`);
17179
18403
  process.exitCode = EXIT_REPL_NOT_STARTED;
17180
18404
  return;
17181
18405
  }
@@ -17237,10 +18461,37 @@ async function runRepl(options, initialTask = "") {
17237
18461
  mode,
17238
18462
  permissions: basePermissions
17239
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
+ };
17240
18490
  const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
17241
18491
  console.error(chalk11.yellow(message));
17242
18492
  });
17243
18493
  const persistSession = async (target = session) => {
18494
+ refreshDetails();
17244
18495
  const snapshot = target.toSession();
17245
18496
  if (!hasSavedSessionContent(snapshot)) return null;
17246
18497
  return writeCheckpoint(snapshot);
@@ -17262,6 +18513,7 @@ async function runRepl(options, initialTask = "") {
17262
18513
  try {
17263
18514
  await session.start();
17264
18515
  } catch (error) {
18516
+ layout?.close();
17265
18517
  throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
17266
18518
  }
17267
18519
  const update = await updateCheck;
@@ -17270,21 +18522,58 @@ async function runRepl(options, initialTask = "") {
17270
18522
  \u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
17271
18523
  console.log(chalk11.gray(" Run `ultrac update` after this session."));
17272
18524
  }
17273
- 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;
17274
18538
  const reader = new LineReader(rl);
17275
18539
  const question = async (prompt, signal) => {
17276
- stdout5.write(prompt);
17277
- 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
+ }
17278
18551
  };
17279
18552
  const drainQueuedMessages = () => reader.drainQueuedMessages();
18553
+ let autonomousRounds = 1;
17280
18554
  setAskUserRuntime(async (text, options2, signal) => {
17281
- console.log(chalk11.cyan(`
18555
+ console.log(chalk11.white(`
17282
18556
  ? ${text}`));
17283
- if (options2?.length) {
17284
- options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
17285
- 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);
17286
18576
  }
17287
- const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
17288
18577
  if (signal?.aborted) return null;
17289
18578
  const answer = (rawAnswer ?? "").trim();
17290
18579
  const picked = Number.parseInt(answer, 10);
@@ -17295,42 +18584,65 @@ async function runRepl(options, initialTask = "") {
17295
18584
  });
17296
18585
  let running = true;
17297
18586
  let turnAbort;
17298
- rl.on("SIGINT", () => {
17299
- if (handleReplInterrupt(turnAbort) === "abort-turn") return;
18587
+ const exitSession = () => {
18588
+ handleReplExit(turnAbort);
17300
18589
  running = false;
17301
18590
  console.log(chalk11.gray("\nExiting ultrac\u2026"));
17302
18591
  rl.close();
17303
- });
18592
+ };
18593
+ rl.on("SIGINT", exitSession);
18594
+ const onKeypress = (_input, key) => {
18595
+ if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
18596
+ if (!slashCompletion?.handledEscape()) handleReplEscape(turnAbort);
18597
+ };
18598
+ if (stdin5.isTTY) {
18599
+ emitKeypressEvents(stdin5, rl);
18600
+ stdin5.on("keypress", onKeypress);
18601
+ }
17304
18602
  const terminate = () => {
17305
18603
  running = false;
17306
18604
  turnAbort?.abort();
17307
18605
  rl.close();
17308
18606
  };
17309
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();
17310
18614
  const withInterrupt = async (run) => {
17311
18615
  turnAbort = new AbortController();
18616
+ turnStartedAt = Date.now();
18617
+ layout?.updateDetails({ status: "Running 00:00" });
17312
18618
  try {
17313
18619
  return await run(turnAbort.signal);
17314
18620
  } finally {
17315
18621
  turnAbort = void 0;
18622
+ turnStartedAt = void 0;
18623
+ refreshDetails();
18624
+ layout?.updateDetails({ status: "Idle" });
17316
18625
  }
17317
18626
  };
17318
18627
  try {
17319
18628
  if (initialTask) {
17320
- console.log(chalk11.cyan(`
18629
+ console.log(chalk11.white(`
17321
18630
  \u203A ${initialTask}`));
17322
- await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
18631
+ await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17323
18632
  }
17324
18633
  while (running) {
17325
- if (stdout5.isTTY) {
18634
+ if (layout?.enabled) {
18635
+ layout.setPrompt("\u203A ");
18636
+ } else if (stdout5.isTTY) {
17326
18637
  const composer = renderComposer(stdout5.columns, true);
17327
18638
  console.log(composer.frame);
17328
18639
  stdout5.write(composer.prompt);
17329
18640
  } else {
17330
- stdout5.write(chalk11.cyan("\n\u203A "));
18641
+ stdout5.write(chalk11.white("\n\u203A "));
17331
18642
  }
17332
18643
  const line = await reader.next();
17333
18644
  if (line === null) break;
18645
+ if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
17334
18646
  const raw = line.trim();
17335
18647
  if (!raw) continue;
17336
18648
  if (raw.startsWith("/")) {
@@ -17346,7 +18658,7 @@ async function runRepl(options, initialTask = "") {
17346
18658
  resetContextBudgetNotices();
17347
18659
  console.log(chalk11.gray("Started a fresh conversation."));
17348
18660
  }, async (prompt) => {
17349
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
18661
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
17350
18662
  }, signal, async (id) => {
17351
18663
  const lineage = await loadSessionLineage(workspace, id);
17352
18664
  const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
@@ -17355,7 +18667,11 @@ async function runRepl(options, initialTask = "") {
17355
18667
  session = restored;
17356
18668
  resetContextBudgetNotices();
17357
18669
  console.log(`Restored ${lineage.sessions.length} saved session(s). Permissions were kept from the current session.`);
17358
- });
18670
+ }, { mcpStatus: () => mcpRuntime?.statuses ?? [], autonomous: (setting) => {
18671
+ if (setting === "on") autonomousRounds = 5;
18672
+ if (setting === "off") autonomousRounds = 1;
18673
+ return autonomousRounds;
18674
+ } });
17359
18675
  });
17360
18676
  if (handled === "exit") {
17361
18677
  running = false;
@@ -17364,7 +18680,9 @@ async function runRepl(options, initialTask = "") {
17364
18680
  setSubagentRuntime({ provider, model: session.model, mode: session.mode, registry });
17365
18681
  if (handled === "unhandled") {
17366
18682
  try {
17367
- 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
+ }));
17368
18686
  } finally {
17369
18687
  await persistSession();
17370
18688
  }
@@ -17375,20 +18693,29 @@ async function runRepl(options, initialTask = "") {
17375
18693
  }
17376
18694
  continue;
17377
18695
  }
17378
- await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages));
18696
+ await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17379
18697
  }
17380
18698
  } finally {
18699
+ clearInterval(footerTimer);
18700
+ slashCompletion?.cleanup();
17381
18701
  process.off("SIGTERM", terminate);
18702
+ if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
17382
18703
  setAskUserRuntime(void 0);
17383
18704
  rl.close();
17384
- await saveTranscript(session, writeCheckpoint);
17385
- mcpRuntime?.close();
18705
+ try {
18706
+ await saveTranscript(session, writeCheckpoint);
18707
+ } finally {
18708
+ mcpRuntime?.close();
18709
+ layout?.close();
18710
+ }
17386
18711
  }
17387
18712
  console.log(chalk11.gray("Goodbye."));
17388
18713
  }
17389
- async function runTurn(session, input, question, signal, drainQueuedMessages, persist) {
18714
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
17390
18715
  const spinner = new Spinner();
17391
- const detachProgress = attachProgressUi();
18716
+ const quiet = isGhostEnabled(session);
18717
+ const detachProgress = quiet ? () => {
18718
+ } : attachProgressUi();
17392
18719
  let deltaChars = 0;
17393
18720
  try {
17394
18721
  await session.ask(input, {
@@ -17404,11 +18731,11 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
17404
18731
  },
17405
18732
  onModelStart: () => {
17406
18733
  deltaChars = 0;
17407
- spinner.start();
18734
+ if (!quiet) spinner.start(isComposing);
17408
18735
  },
17409
18736
  onModelDelta: (text) => {
17410
18737
  deltaChars += text.length;
17411
- spinner.update(`thinking (${deltaChars} chars)`);
18738
+ if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
17412
18739
  },
17413
18740
  onModelSettled: () => spinner.stop(),
17414
18741
  onAssistantMessage: (message) => {
@@ -17418,14 +18745,14 @@ ${message.trim()}`);
17418
18745
  }
17419
18746
  },
17420
18747
  onToolStart: (call) => {
17421
- console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
18748
+ if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
17422
18749
  },
17423
18750
  onToolResult: (_call, result) => printToolResult2(result),
17424
18751
  ...drainQueuedMessages ? {
17425
18752
  drainQueuedMessages,
17426
- onQueuedMessage: () => {
18753
+ onQueuedMessage: (message) => {
17427
18754
  spinner.stop();
17428
- console.log(chalk11.dim(" Queued message added to the current turn."));
18755
+ console.log(chalk11.dim(` Queued for after the current tool: ${message}`));
17429
18756
  }
17430
18757
  } : {},
17431
18758
  onToolDenied: (call, permission) => {
@@ -17470,9 +18797,26 @@ ${message.trim()}`);
17470
18797
  detachProgress();
17471
18798
  }
17472
18799
  }
17473
- async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
18800
+ async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
17474
18801
  try {
17475
- return await runTurn(session, input, question, signal, drainQueuedMessages, persist);
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;
17476
18820
  } finally {
17477
18821
  await persist(session);
17478
18822
  }
@@ -17539,11 +18883,56 @@ function slashCommandChangesSession(raw) {
17539
18883
  function errorMessage2(error) {
17540
18884
  return error instanceof Error ? error.message : String(error);
17541
18885
  }
17542
- async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume) {
18886
+ async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume, services) {
17543
18887
  const [name, ...rest] = raw.slice(1).split(/\s+/);
17544
18888
  const command = (name ?? "").toLowerCase();
17545
18889
  const argument = rest.join(" ").trim();
17546
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
+ }
18921
+ case "logs": {
18922
+ console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
18923
+ return "ok";
18924
+ }
18925
+ case "ghost": {
18926
+ if (argument && !["on", "off", "status"].includes(argument.toLowerCase())) {
18927
+ console.log("Usage: /ghost [on|off|status]");
18928
+ return "ok";
18929
+ }
18930
+ if (argument.toLowerCase() !== "status") {
18931
+ setGhostEnabled(session, argument ? argument.toLowerCase() === "on" : !isGhostEnabled(session));
18932
+ }
18933
+ console.log(`Ghost mode: ${isGhostEnabled(session) ? "on" : "off"}. Quiet activity output; private diagnostics remain enabled. Network routing and permissions are unchanged.`);
18934
+ return "ok";
18935
+ }
17547
18936
  case "help": {
17548
18937
  console.log(HELP);
17549
18938
  const index = await listSkillCommands(workspace);
@@ -17611,7 +19000,7 @@ ${extra}`);
17611
19000
  else {
17612
19001
  for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
17613
19002
  const counts = reported.flatMap((step) => typeof step.usage?.[field] === "number" ? [step.usage[field]] : []);
17614
- 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"}`);
17615
19004
  }
17616
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.`);
17617
19006
  }
@@ -17721,6 +19110,27 @@ ${extra}`);
17721
19110
  }
17722
19111
  return "ok";
17723
19112
  }
19113
+ case "output": {
19114
+ const results = session.toSession().steps.flatMap((step) => step.toolResults);
19115
+ if (!results.length) {
19116
+ console.log("No saved tool output in this session.");
19117
+ return "ok";
19118
+ }
19119
+ if (argument === "list") {
19120
+ for (const [index, entry2] of results.entries()) console.log(`${index + 1}. ${toolActivityLabel(entry2.call)} (${entry2.result.ok ? "ok" : "failed"})`);
19121
+ return "ok";
19122
+ }
19123
+ const selected = argument ? Number(argument) : results.length;
19124
+ if (!Number.isSafeInteger(selected) || selected < 1 || selected > results.length) {
19125
+ console.log(`Usage: /output [1-${results.length}|list]`);
19126
+ return "ok";
19127
+ }
19128
+ const entry = results[selected - 1];
19129
+ console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
19130
+ console.log(entry.result.content);
19131
+ for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
19132
+ return "ok";
19133
+ }
17724
19134
  case "tools": {
17725
19135
  const names = session.advertisedToolNames;
17726
19136
  console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
@@ -17742,6 +19152,20 @@ ${extra}`);
17742
19152
  }
17743
19153
  return "ok";
17744
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
+ }
17745
19169
  case "skills": {
17746
19170
  const skills = await listSkills(workspace);
17747
19171
  if (!skills.length) {
@@ -17774,13 +19198,14 @@ ${extra}`);
17774
19198
  return "unhandled";
17775
19199
  }
17776
19200
  }
17777
- async function runSkillCommand(session, raw, workspace, question) {
19201
+ async function runSkillCommand(session, raw, workspace, question, runPrompt) {
17778
19202
  const resolved = await resolveSkillCommand(workspace, raw);
17779
19203
  if (resolved.status === "ok") {
17780
19204
  console.log(
17781
19205
  chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
17782
19206
  );
17783
- await runTurn(session, resolved.prompt, question);
19207
+ if (runPrompt) await runPrompt(resolved.prompt);
19208
+ else await runTurn(session, resolved.prompt, question);
17784
19209
  return;
17785
19210
  }
17786
19211
  if (resolved.status === "conflict") {
@@ -17801,10 +19226,8 @@ async function runSkillCommand(session, raw, workspace, question) {
17801
19226
  console.log(chalk11.red("Unknown command. Try /help."));
17802
19227
  }
17803
19228
  var LineReader = class {
17804
- queue = [];
17805
- waiters = [];
17806
- closed = false;
17807
19229
  constructor(rl) {
19230
+ this.rl = rl;
17808
19231
  rl.on("line", (line) => {
17809
19232
  const waiter = this.waiters.shift();
17810
19233
  if (waiter) {
@@ -17820,6 +19243,10 @@ var LineReader = class {
17820
19243
  }
17821
19244
  });
17822
19245
  }
19246
+ rl;
19247
+ queue = [];
19248
+ waiters = [];
19249
+ closed = false;
17823
19250
  next(signal) {
17824
19251
  return this.waitForLine(signal, true);
17825
19252
  }
@@ -17830,6 +19257,10 @@ var LineReader = class {
17830
19257
  nextFresh(signal) {
17831
19258
  return this.waitForLine(signal, false);
17832
19259
  }
19260
+ /** True while the user is typing a line that has not been submitted yet. */
19261
+ get composing() {
19262
+ return this.rl.line.length > 0;
19263
+ }
17833
19264
  /**
17834
19265
  * Drain ordinary messages in arrival order. A slash command is a control
17835
19266
  * boundary and remains queued for the main REPL loop along with everything
@@ -17875,11 +19306,15 @@ var LineReader = class {
17875
19306
  waiter.resolve(line);
17876
19307
  }
17877
19308
  };
17878
- function handleReplInterrupt(activeTurn) {
19309
+ function handleReplEscape(activeTurn) {
17879
19310
  if (activeTurn && !activeTurn.signal.aborted) {
17880
19311
  activeTurn.abort();
17881
19312
  return "abort-turn";
17882
19313
  }
19314
+ return "idle";
19315
+ }
19316
+ function handleReplExit(activeTurn) {
19317
+ activeTurn?.abort();
17883
19318
  return "exit-session";
17884
19319
  }
17885
19320
  function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
@@ -17944,29 +19379,6 @@ function indent3(value) {
17944
19379
  // src/commands/run.ts
17945
19380
  import { basename as basename3 } from "path";
17946
19381
  import chalk12 from "chalk";
17947
-
17948
- // src/core/autonomous.ts
17949
- var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
17950
- var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
17951
- var AUTONOMOUS_MAX_STEPS = 200;
17952
- function shouldResume(stopReason) {
17953
- return stopReason === "max-steps" || stopReason === "error";
17954
- }
17955
- async function runAutonomousLoop(options) {
17956
- if (!Number.isInteger(options.maxRounds) || options.maxRounds < 1) {
17957
- throw new Error("maxRounds must be a positive integer.");
17958
- }
17959
- let previous;
17960
- for (let round = 1; ; round += 1) {
17961
- const session = await options.runRound(round, previous);
17962
- const willResume = shouldResume(session.stopReason) && round < options.maxRounds && !options.signal?.aborted;
17963
- await options.onRoundEnd?.(round, session, willResume);
17964
- if (!willResume) return { session, rounds: round };
17965
- previous = session;
17966
- }
17967
- }
17968
-
17969
- // src/commands/run.ts
17970
19382
  function parsePositiveInt(value, flag) {
17971
19383
  const parsed = Number.parseInt(value, 10);
17972
19384
  if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
@@ -17975,7 +19387,6 @@ function parsePositiveInt(value, flag) {
17975
19387
  return parsed;
17976
19388
  }
17977
19389
  var EXIT_INCOMPLETE_RUN = 2;
17978
- var AUTONOMOUS_ERROR_BACKOFF_MS = 15e3;
17979
19390
  var NOTIFY_SUMMARY_CHARS = 1200;
17980
19391
  function registerRunCommand(program2) {
17981
19392
  program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
@@ -17986,7 +19397,7 @@ function registerRunCommand(program2) {
17986
19397
  "--autonomous",
17987
19398
  `Run with no human in the loop: write, shell and network are granted, each round may take up to ${AUTONOMOUS_MAX_STEPS} steps, and a round that runs out of steps or fails resumes on its own. With a phone channel configured (\`ultrac notify setup\`), questions go to your phone and you are told when it finishes.`,
17988
19399
  false
17989
- ).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
19400
+ ).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--verbose-tools", "Print complete tool results instead of previews", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
17990
19401
  const task = taskParts.join(" ").trim();
17991
19402
  if (!task) {
17992
19403
  throw new Error("A task is required.");
@@ -18100,14 +19511,17 @@ function registerRunCommand(program2) {
18100
19511
  shell: options.allowShell || config.permissions.shell,
18101
19512
  network: options.allowNetwork || config.permissions.network
18102
19513
  };
19514
+ const roundFailures = /* @__PURE__ */ new Map();
18103
19515
  let result;
18104
19516
  try {
18105
19517
  result = await runAutonomousLoop({
18106
19518
  maxRounds,
18107
19519
  signal: controller.signal,
18108
- runRound: async (round, previous) => {
19520
+ classifyFailure: (session) => roundFailures.get(session.id),
19521
+ runRound: async (round, previous, roundSignal) => {
18109
19522
  const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
18110
19523
  let lastCheckpoint;
19524
+ let checkpointFailed = false;
18111
19525
  try {
18112
19526
  const { session } = await runner.run({
18113
19527
  task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
@@ -18121,22 +19535,30 @@ function registerRunCommand(program2) {
18121
19535
  previousSessionLineage: lineage,
18122
19536
  ...toolSelection ? { tools: toolSelection } : {},
18123
19537
  permissions,
18124
- signal: controller.signal,
19538
+ signal: roundSignal,
18125
19539
  ...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
18126
19540
  // Write the session after every step, atomically, so a run that dies
18127
19541
  // partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
18128
19542
  // its last completed step and can be resumed with --continue rather than
18129
19543
  // restarted from nothing.
18130
19544
  onCheckpoint: async (session2) => {
18131
- lastCheckpoint = session2;
18132
- await writeCheckpoint(session2);
19545
+ try {
19546
+ const checkpointPath = await writeCheckpoint(session2);
19547
+ checkpointFailed = !checkpointPath;
19548
+ if (checkpointPath) lastCheckpoint = await loadSession(workspace, session2.id);
19549
+ } catch (error) {
19550
+ checkpointFailed = true;
19551
+ throw error;
19552
+ }
18133
19553
  }
18134
19554
  });
18135
19555
  return session;
18136
19556
  } catch (error) {
18137
- if (!autonomous || !lastCheckpoint || controller.signal.aborted) throw error;
19557
+ if (!autonomous || !lastCheckpoint || checkpointFailed || roundSignal.aborted) throw error;
19558
+ const classification = classifyProviderError(error);
19559
+ roundFailures.set(lastCheckpoint.id, classification);
18138
19560
  console.error(
18139
- chalk12.yellow(`Round ${round} failed: ${error.message}. Resuming from its last checkpoint.`)
19561
+ chalk12.yellow(`Round ${round} failed (${classification.kind}). The latest checkpoint is saved.`)
18140
19562
  );
18141
19563
  return { ...lastCheckpoint, stopReason: "error" };
18142
19564
  }
@@ -18149,7 +19571,6 @@ function registerRunCommand(program2) {
18149
19571
  `Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
18150
19572
  )
18151
19573
  );
18152
- if (session.stopReason === "error") await sleep(AUTONOMOUS_ERROR_BACKOFF_MS, controller.signal);
18153
19574
  }
18154
19575
  });
18155
19576
  } catch (error) {
@@ -18170,16 +19591,19 @@ function registerRunCommand(program2) {
18170
19591
  { cause: error }
18171
19592
  );
18172
19593
  }
19594
+ if (["stalled", "elapsed_budget", "permanent_error"].includes(result.haltReason ?? "")) {
19595
+ console.error(chalk12.yellow(`Autonomous recovery stopped: ${result.haltReason}. The latest checkpoint is saved.`));
19596
+ }
18173
19597
  const stopReason = result.session.stopReason;
18174
19598
  for (const warning of getSessionMcpArchiveWarnings(result.session)) {
18175
19599
  console.error(chalk12.yellow(`Warning: ${warning}`));
18176
19600
  }
18177
19601
  if (options.json) {
18178
19602
  console.log(
18179
- JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, session: result.session }, null, 2)
19603
+ JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, haltReason: result.haltReason ?? null, rounds: result.rounds, session: result.session }, null, 2)
18180
19604
  );
18181
19605
  } else {
18182
- printSession(result.session, sessionPath2);
19606
+ printSession(result.session, sessionPath2, { verboseTools: options.verboseTools });
18183
19607
  }
18184
19608
  if (notifier) {
18185
19609
  const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
@@ -18433,17 +19857,25 @@ function registerSkillsCommand(program2) {
18433
19857
  console.log("");
18434
19858
  console.log(skill.body);
18435
19859
  });
18436
- skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("-w, --workspace <path>", "Workspace path", ".").action(
19860
+ skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("--command <name>", "Expose this skill as a custom slash command").option("--argument-hint <text>", "Describe the slash command arguments").option("-w, --workspace <path>", "Workspace path", ".").action(
18437
19861
  async (name, options) => {
19862
+ const command = options.command ? normalizeCommandName(options.command) : void 0;
19863
+ if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is a built-in command. Choose another skill command name.`);
19864
+ if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
19865
+ const argumentHint = options.argumentHint?.trim();
19866
+ if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
18438
19867
  const skill = await createSkill(resolve13(options.workspace), {
18439
19868
  name,
18440
19869
  description: options.description,
18441
19870
  whenToUse: options.when,
18442
19871
  permissions: parsePermissions(options.permissions),
18443
- scope: parseScope2(options.scope)
19872
+ scope: parseScope2(options.scope),
19873
+ command,
19874
+ argumentHint
18444
19875
  });
18445
19876
  console.log(`Created ${skill.scope} skill "${skill.name}" at ${skill.path}`);
18446
19877
  console.log("Edit that file to add the step-by-step instructions.");
19878
+ if (command) console.log(`Invoke this skill with /${command}${argumentHint ? ` ${argumentHint}` : ""} in a trusted workspace.`);
18447
19879
  }
18448
19880
  );
18449
19881
  skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
@@ -19188,8 +20620,8 @@ Leaving run ${runId} as it is.
19188
20620
  case "succeeded":
19189
20621
  finishHuman(result.text);
19190
20622
  if (result.toolCalls.length && !result.text) {
19191
- const count3 = result.toolCalls.length;
19192
- 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.
19193
20625
  `));
19194
20626
  }
19195
20627
  return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
@@ -19384,7 +20816,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
19384
20816
  );
19385
20817
  }
19386
20818
  });
19387
- toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
20819
+ toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--verbose-tools", "Print the complete tool result", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
19388
20820
  const input = parseToolInput(inputText);
19389
20821
  const result = await createDefaultToolRegistry().execute(
19390
20822
  { tool, input },
@@ -19400,7 +20832,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
19400
20832
  if (options.resultJson) {
19401
20833
  console.log(JSON.stringify({ result }, null, 2));
19402
20834
  } else {
19403
- printToolResult(result);
20835
+ printToolResult(result, { verboseTools: options.verboseTools });
19404
20836
  }
19405
20837
  if (!result.ok) {
19406
20838
  process.exitCode = 1;