@spinalfluid/ultrac 0.1.5 → 0.1.6

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 +4 -2
  2. package/dist/cli.js +991 -373
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import chalk15 from "chalk";
4
+ import chalk16 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/ask.ts
@@ -154,10 +154,10 @@ function readEnvBaseUrl() {
154
154
  }
155
155
  function resolveBaseUrl(stored) {
156
156
  if (baseUrlFlag) return { baseUrl: baseUrlFlag, source: "flag" };
157
- const env = readEnvBaseUrl();
158
- if (env) {
159
- assertAllowedUltracBaseUrl(env);
160
- return { baseUrl: env, source: "env" };
157
+ const env2 = readEnvBaseUrl();
158
+ if (env2) {
159
+ assertAllowedUltracBaseUrl(env2);
160
+ return { baseUrl: env2, source: "env" };
161
161
  }
162
162
  if (stored?.baseUrl) {
163
163
  const safe = safeStoredBaseUrl(stored.baseUrl, ULTRAC_DEFAULT_BASE_URL);
@@ -242,42 +242,51 @@ import { z as z5 } from "zod";
242
242
 
243
243
  // src/core/modes.ts
244
244
  var agentModeNames = ["default", "plan", "security", "coding", "review"];
245
- var sharedAgentTools = ["task", "skill", "memory_read", "ask_user"];
246
- var readOnlyCoreTools = [
245
+ var sharedAgentTools = [
246
+ "task",
247
+ "skill",
248
+ "list_skills",
249
+ "memory_read",
250
+ "ask_user"
251
+ ];
252
+ var sharedWriteMemory = ["memory_write"];
253
+ var readExploreTools = [
247
254
  "list_files",
248
255
  "read_file",
256
+ "read_many_files",
249
257
  "search_files",
258
+ "inspect_project",
259
+ "repo_map",
260
+ "git_status",
261
+ "git_worktree_summary",
250
262
  "git_diff",
251
- "git_log",
252
- ...sharedAgentTools
263
+ "git_log"
253
264
  ];
265
+ var readOnlyCoreTools = [...readExploreTools, ...sharedAgentTools];
254
266
  var coreTools = [
255
- "list_files",
256
- "read_file",
257
- "search_files",
267
+ ...readExploreTools,
258
268
  "run_shell",
259
269
  "write_file",
260
270
  "replace_in_file",
261
271
  "multi_edit",
262
272
  "apply_patch",
263
- "git_status",
264
- "git_diff",
273
+ "git_commit",
274
+ "fetch_url",
265
275
  "todo_write",
266
276
  "todo_read",
267
- ...sharedAgentTools
277
+ ...sharedAgentTools,
278
+ ...sharedWriteMemory
268
279
  ];
269
280
  var agentModePresets = {
270
281
  default: {
271
282
  name: "default",
272
283
  label: "Default",
273
284
  instructions: [
274
- "Gather repository context before acting.",
285
+ "Gather repository context with tools before acting; the startup header is not the repository.",
275
286
  "Prefer narrow, reversible changes.",
276
287
  "Keep final answers concise and include verification status."
277
288
  ],
278
- // The compact core, nothing else: the default mode does not know yet which
279
- // way the task will go, so it pays only for what every task needs.
280
- tools: coreTools,
289
+ tools: [...coreTools, "list_sessions", "read_session", "search_sessions"],
281
290
  externalTools: true
282
291
  },
283
292
  plan: {
@@ -288,10 +297,7 @@ var agentModePresets = {
288
297
  "Use read and search tools to build an accurate picture, then use todo_write to lay out the concrete steps.",
289
298
  "Finish by presenting the plan and the files it will touch, and wait for approval before switching to an editing mode."
290
299
  ],
291
- // Read-only, plus the two things planning is: a map of the repository and a
292
- // written list of steps. No editor, no shell - the mode promises not to
293
- // change anything, and the advertised set says the same.
294
- tools: [...readOnlyCoreTools, "repo_map", "todo_write", "todo_read"],
300
+ tools: [...readOnlyCoreTools, "repo_handbook", "verification_plan", "todo_write", "todo_read"],
295
301
  externalTools: false
296
302
  },
297
303
  security: {
@@ -304,14 +310,8 @@ var agentModePresets = {
304
310
  "Use fetch_url only against hosts the user named. It is an HTTP client, not a scanner.",
305
311
  "Separate confirmed findings from hypotheses and call out missing authorization or scope."
306
312
  ],
307
- // Core plus local evidence collection and the HTTP client. Editors stay so
308
- // a finding can become a patch in the same pass. list_processes is
309
- // callable by name under shell; it is not advertised here because a
310
- // security review of a checkout rarely needs the host process table.
311
313
  tools: [
312
314
  ...coreTools,
313
- "fetch_url",
314
- "inspect_project",
315
315
  "inspect_artifact",
316
316
  "hex_dump",
317
317
  "extract_strings",
@@ -329,10 +329,20 @@ var agentModePresets = {
329
329
  "Make the smallest coherent patch that satisfies the task.",
330
330
  "Run the most relevant verification when shell permission is available; otherwise say what was not run."
331
331
  ],
332
- // Core plus the verification loop - tests, type/lint diagnostics, package
333
- // scripts - and structural search, which is what makes an edit land in the
334
- // right place across a large codebase.
335
- tools: [...coreTools, "run_tests", "diagnostics", "run_package_script", "ast_search"],
332
+ tools: [
333
+ ...coreTools,
334
+ "ast_search",
335
+ "repo_handbook",
336
+ "verification_plan",
337
+ "git_show",
338
+ "git_push",
339
+ "run_tests",
340
+ "diagnostics",
341
+ "run_package_script",
342
+ "list_sessions",
343
+ "read_session",
344
+ "search_sessions"
345
+ ],
336
346
  externalTools: true
337
347
  },
338
348
  review: {
@@ -343,10 +353,14 @@ var agentModePresets = {
343
353
  "Lead with findings grounded in concrete file paths, lines, commands, or behavior.",
344
354
  "Avoid broad rewrites unless the user explicitly asks for an implementation pass."
345
355
  ],
346
- // Read-only: a review reads the diff and its history, checks what the
347
- // compiler and linter say, and writes its verdict through the verification
348
- // tools. Nothing here edits the tree.
349
- tools: [...readOnlyCoreTools, "diagnostics", "verification_plan", "verification_report"],
356
+ tools: [
357
+ ...readOnlyCoreTools,
358
+ "git_show",
359
+ "git_blame",
360
+ "diagnostics",
361
+ "verification_plan",
362
+ "verification_report"
363
+ ],
350
364
  externalTools: false
351
365
  }
352
366
  };
@@ -878,20 +892,32 @@ function contextUsageStatus(messages, usage, disposition) {
878
892
  ...disposition.error ? { error: disposition.error } : {}
879
893
  };
880
894
  }
895
+ function contextOverflowMessage(usage, freedChars, safeWindowChars, pinned) {
896
+ const numbers = `system ${count(usage.systemChars)} + tools ${count(usage.toolsChars)} + history ${count(
897
+ usage.historyChars
898
+ )} = ${count(usage.totalChars)} characters against a ${count(usage.windowChars)}-character window (${count(
899
+ usage.contextTokens
900
+ )} tokens at ${CHARS_PER_TOKEN} chars/token)` + (safeWindowChars < usage.windowChars ? ` with a ${count(safeWindowChars)}-character safe wire ceiling` : "") + `${freedChars > 0 ? `, after dropping ${count(freedChars)} characters of earlier turns` : ""}`;
901
+ if (pinned) {
902
+ return `Pinned user instructions cannot fit the model's context window and will not be discarded: ${numbers}. Shorten the prompt, advertise fewer tools (--tools, or a narrower --mode), or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`;
903
+ }
904
+ return `Request does not fit the model's context window: ${numbers}. Advertise fewer tools (--tools, or a narrower --mode), shorten the prompt, or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`;
905
+ }
881
906
  var ContextBudgetError = class extends Error {
882
907
  usage;
883
908
  constructor(usage, freedChars, safeWindowChars = usage.windowChars) {
884
- super(
885
- `Request does not fit the model's context window: system ${count(usage.systemChars)} + tools ${count(
886
- usage.toolsChars
887
- )} + history ${count(usage.historyChars)} = ${count(usage.totalChars)} characters against a ${count(
888
- usage.windowChars
889
- )}-character window (${count(usage.contextTokens)} tokens at ${CHARS_PER_TOKEN} chars/token)` + (safeWindowChars < usage.windowChars ? ` with a ${count(safeWindowChars)}-character safe wire ceiling` : "") + `${freedChars > 0 ? `, after dropping ${count(freedChars)} characters of earlier turns` : ""}. Advertise fewer tools (--tools, or a narrower --mode), shorten the prompt, or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`
890
- );
909
+ super(contextOverflowMessage(usage, freedChars, safeWindowChars, false));
891
910
  this.name = "ContextBudgetError";
892
911
  this.usage = usage;
893
912
  }
894
913
  };
914
+ var PinnedContextBudgetError = class extends ContextBudgetError {
915
+ constructor(usage, freedChars, safeWindowChars = usage.windowChars) {
916
+ super(usage, freedChars, safeWindowChars);
917
+ this.name = "PinnedContextBudgetError";
918
+ this.message = contextOverflowMessage(usage, freedChars, safeWindowChars, true);
919
+ }
920
+ };
895
921
  function planToolCallRequest(input) {
896
922
  return planRequest({
897
923
  units: groupNativeMessages(input.messages, input.isPinned),
@@ -964,6 +990,10 @@ function planRequest(input) {
964
990
  const usage = summarize(kept, input.toolsChars, window);
965
991
  const safeWindowChars = Math.floor(usage.windowChars * HARD_WINDOW_SAFETY_FRACTION);
966
992
  if (usage.totalChars > safeWindowChars) {
993
+ const droppableLeft = kept.some((unit) => !unit.pinned && !unit.system);
994
+ if (!droppableLeft && kept.some((unit) => unit.pinned && !unit.system)) {
995
+ throw new PinnedContextBudgetError(usage, freedChars, safeWindowChars);
996
+ }
967
997
  throw new ContextBudgetError(usage, freedChars, safeWindowChars);
968
998
  }
969
999
  return {
@@ -3769,12 +3799,17 @@ import { z as z6 } from "zod";
3769
3799
 
3770
3800
  // src/core/ask-runtime.ts
3771
3801
  var askUserRuntime;
3772
- function setAskUserRuntime(fn) {
3802
+ var askUserRuntimeRemote = false;
3803
+ function setAskUserRuntime(fn, options = {}) {
3773
3804
  askUserRuntime = fn;
3805
+ askUserRuntimeRemote = fn !== void 0 && options.remote === true;
3774
3806
  }
3775
3807
  function getAskUserRuntime() {
3776
3808
  return askUserRuntime;
3777
3809
  }
3810
+ function isAskUserRuntimeRemote() {
3811
+ return askUserRuntimeRemote;
3812
+ }
3778
3813
 
3779
3814
  // src/tools/workspace/ask-user.ts
3780
3815
  var AskUserInput = z6.object({
@@ -3796,7 +3831,7 @@ var askUserTool = {
3796
3831
  async execute(input, context) {
3797
3832
  const ask2 = getAskUserRuntime();
3798
3833
  if (!ask2) return { ok: false, content: NO_ONE_TO_ASK };
3799
- if (!stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
3834
+ if (!isAskUserRuntimeRemote() && !stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
3800
3835
  const answer = await ask2(input.question, input.options, context.signal);
3801
3836
  if (context.signal?.aborted) {
3802
3837
  return { ok: false, content: "Question cancelled; no answer was recorded." };
@@ -3927,17 +3962,17 @@ var SENSITIVE_ENV_PATTERNS = [
3927
3962
  /(?:^|_)WEBHOOK_SECRET$/
3928
3963
  ];
3929
3964
  function createSanitizedProcessEnv(source = process.env) {
3930
- const env = {};
3965
+ const env2 = {};
3931
3966
  const redactedEnvKeys = [];
3932
3967
  for (const [key, value] of Object.entries(source)) {
3933
3968
  if (isSensitiveEnvName(key)) {
3934
3969
  redactedEnvKeys.push(key);
3935
3970
  continue;
3936
3971
  }
3937
- env[key] = value;
3972
+ env2[key] = value;
3938
3973
  }
3939
3974
  redactedEnvKeys.sort();
3940
- return { env, redactedEnvKeys };
3975
+ return { env: env2, redactedEnvKeys };
3941
3976
  }
3942
3977
  function isSensitiveEnvName(name) {
3943
3978
  const normalized = name.toUpperCase();
@@ -4026,12 +4061,12 @@ function runProcess(command, args, options) {
4026
4061
  redactedEnvKeys: []
4027
4062
  });
4028
4063
  }
4029
- const { env, redactedEnvKeys } = createSanitizedProcessEnv();
4064
+ const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
4030
4065
  return new Promise((resolve15, reject) => {
4031
4066
  const child = spawn(command, args, {
4032
4067
  cwd: options.cwd,
4033
4068
  stdio: ["pipe", "pipe", "pipe"],
4034
- env,
4069
+ env: env2,
4035
4070
  shell: options.shell ?? false,
4036
4071
  // Its own process group, so the timeout can kill what the child started
4037
4072
  // as well as the child. Killing only the direct child leaves the grand-
@@ -5116,7 +5151,7 @@ function defaultContextFiles(files) {
5116
5151
  }
5117
5152
  function isPriorityContextFile(path) {
5118
5153
  const normalized = path.toLowerCase();
5119
- const basename3 = normalized.split("/").at(-1) ?? normalized;
5154
+ const basename4 = normalized.split("/").at(-1) ?? normalized;
5120
5155
  return [
5121
5156
  "agents.md",
5122
5157
  "claude.md",
@@ -5126,12 +5161,12 @@ function isPriorityContextFile(path) {
5126
5161
  "pyproject.toml",
5127
5162
  "readme.md",
5128
5163
  "tsconfig.json"
5129
- ].includes(basename3) || normalized === "docs/architecture.md" || normalized === "docs/roadmap.md";
5164
+ ].includes(basename4) || normalized === "docs/architecture.md" || normalized === "docs/roadmap.md";
5130
5165
  }
5131
5166
  function isLikelyEntrypoint(path) {
5132
5167
  const normalized = path.toLowerCase();
5133
- const basename3 = normalized.split("/").at(-1) ?? normalized;
5134
- return basename3 === "main.ts" || basename3 === "main.tsx" || basename3 === "cli.ts" || basename3 === "index.ts" || basename3 === "app.ts" || basename3.startsWith("next.config.") || basename3.startsWith("vite.config.") || normalized === "src/app/page.tsx" || normalized === "src/app/layout.tsx";
5168
+ const basename4 = normalized.split("/").at(-1) ?? normalized;
5169
+ return basename4 === "main.ts" || basename4 === "main.tsx" || basename4 === "cli.ts" || basename4 === "index.ts" || basename4 === "app.ts" || basename4.startsWith("next.config.") || basename4.startsWith("vite.config.") || normalized === "src/app/page.tsx" || normalized === "src/app/layout.tsx";
5135
5170
  }
5136
5171
  function summarizeRepository(files) {
5137
5172
  return {
@@ -7137,8 +7172,8 @@ var scanSecretsTool = {
7137
7172
  }
7138
7173
  scannedFiles += 1;
7139
7174
  const text = bytes.toString("utf8");
7140
- const basename3 = file.split("/").pop() ?? file;
7141
- const skipEntropy = lockfileNames.has(basename3);
7175
+ const basename4 = file.split("/").pop() ?? file;
7176
+ const skipEntropy = lockfileNames.has(basename4);
7142
7177
  collectLineFindings(file, text, rules, skipEntropy, findings, input.maxFindings);
7143
7178
  }
7144
7179
  const visible = findings.slice(0, input.maxFindings);
@@ -7166,7 +7201,7 @@ function compileCustomRules(rules) {
7166
7201
  }
7167
7202
  function collectLineFindings(path, text, rules, skipLockfileRules, findings, maxFindings) {
7168
7203
  const lines = text.split(/\r?\n/);
7169
- const basename3 = path.split("/").pop() ?? path;
7204
+ const basename4 = path.split("/").pop() ?? path;
7170
7205
  for (const [index, line] of lines.entries()) {
7171
7206
  if (findings.length >= maxFindings) return;
7172
7207
  if (line.length > 4e3) continue;
@@ -7183,7 +7218,7 @@ function collectLineFindings(path, text, rules, skipLockfileRules, findings, max
7183
7218
  snippet: redactSecret(match[0])
7184
7219
  });
7185
7220
  }
7186
- if (skipLockfileRules || lockfileNames.has(basename3) || findings.length >= maxFindings) continue;
7221
+ if (skipLockfileRules || lockfileNames.has(basename4) || findings.length >= maxFindings) continue;
7187
7222
  collectEntropyFinding(path, index + 1, line, findings);
7188
7223
  }
7189
7224
  }
@@ -8542,7 +8577,7 @@ function summarizeExtensions(files) {
8542
8577
  }
8543
8578
  function isImportantFile(path) {
8544
8579
  const normalized = path.toLowerCase();
8545
- const basename3 = normalized.split("/").at(-1) ?? normalized;
8580
+ const basename4 = normalized.split("/").at(-1) ?? normalized;
8546
8581
  if ([
8547
8582
  "agents.md",
8548
8583
  "claude.md",
@@ -8555,7 +8590,7 @@ function isImportantFile(path) {
8555
8590
  ].includes(normalized)) {
8556
8591
  return true;
8557
8592
  }
8558
- return basename3.startsWith("next.config.") || basename3.startsWith("vite.config.") || normalized.startsWith("docs/") || normalized.startsWith("infra/") || normalized === "src/cli.ts" || normalized === "src/main.tsx" || normalized === "src/app/page.tsx";
8593
+ return basename4.startsWith("next.config.") || basename4.startsWith("vite.config.") || normalized.startsWith("docs/") || normalized.startsWith("infra/") || normalized === "src/cli.ts" || normalized === "src/main.tsx" || normalized === "src/app/page.tsx";
8559
8594
  }
8560
8595
  function formatBytes(bytes) {
8561
8596
  if (bytes < 1024) return `${bytes} B`;
@@ -9864,59 +9899,40 @@ var AgentRunner = class {
9864
9899
  const mode = options.mode ?? "default";
9865
9900
  const modePreset = getAgentModePreset(mode);
9866
9901
  const modePermissions = getEffectiveModePermissions(mode, options.permissions);
9867
- const readOnlyContext = {
9868
- workspace: options.workspace,
9869
- permissions: { write: false, shell: false, network: false },
9870
- ...options.signal ? { signal: options.signal } : {}
9871
- };
9872
- const [project, repoMap, repoHandbook, verificationPlan, worktree, snapshot] = await Promise.all([
9873
- this.registry.execute({ tool: "inspect_project", input: {} }, readOnlyContext),
9874
- this.registry.execute({ tool: "repo_map", input: { maxItems: 10 } }, readOnlyContext),
9875
- this.registry.execute({ tool: "repo_handbook", input: { maxFilesPerBehavior: 6 } }, readOnlyContext),
9876
- this.registry.execute({ tool: "verification_plan", input: {} }, readOnlyContext),
9877
- this.registry.execute({ tool: "git_worktree_summary", input: { includeStats: false } }, readOnlyContext),
9878
- this.registry.execute(
9879
- { tool: "list_files", input: { patterns: ["**/*"], includeHidden: false, maxResults: 80 } },
9880
- readOnlyContext
9881
- )
9882
- ]);
9883
- const [memory, skills] = await Promise.all([
9884
- loadMemoryContext(options.workspace),
9885
- this.registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(options.workspace) : Promise.resolve("Skills are unavailable in this tool set.")
9886
- ]);
9902
+ const registered = this.registry.list();
9903
+ const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
9904
+ const workspaceContext = await gatherWorkspaceContext(
9905
+ this.registry,
9906
+ options.workspace,
9907
+ advertised.map((tool) => tool.name)
9908
+ );
9887
9909
  const fileProvenance = /* @__PURE__ */ new WeakMap();
9888
9910
  const restored = restorePreviousTranscript(previousSessions, options.maxToolResultChars, fileProvenance);
9889
9911
  if (previousSession) {
9890
9912
  noteContextEvent(describeRestore(previousSession, previousSessions, restored, priorConstraints));
9891
9913
  }
9914
+ const workspaceMessage = {
9915
+ role: "user",
9916
+ content: workspaceContext
9917
+ };
9892
9918
  const taskMessage = {
9893
9919
  role: "user",
9894
- content: buildInitialUserMessage(
9895
- options.task,
9896
- options.workspace,
9897
- contextSection(project),
9898
- contextSection(repoMap),
9899
- contextSection(repoHandbook),
9900
- contextSection(verificationPlan),
9901
- worktree.ok ? contextSection(worktree) : "No Git worktree detected.",
9902
- contextSection(snapshot),
9903
- modePreset.label,
9904
- memory,
9905
- skills,
9906
- options.previousSession,
9907
- restored
9908
- )
9920
+ content: buildTaskUserMessage(options.task, modePreset.label, options.previousSession, restored)
9909
9921
  };
9922
+ fileProvenance.set(workspaceMessage, unknownContextProvenance());
9910
9923
  fileProvenance.set(taskMessage, unknownContextProvenance());
9911
9924
  const constraintMessages = inheritedConstraints.map((constraint) => ({
9912
9925
  role: "user",
9913
9926
  content: constraint.content
9914
9927
  }));
9915
- const messages = [...restored.messages, ...constraintMessages, taskMessage];
9928
+ const messages = [
9929
+ ...restored.messages,
9930
+ ...constraintMessages,
9931
+ workspaceMessage,
9932
+ taskMessage
9933
+ ];
9916
9934
  const pinnedMessages = [...constraintMessages];
9917
9935
  const steps = [];
9918
- const registered = this.registry.list();
9919
- const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
9920
9936
  const toolsByName = new Map(
9921
9937
  registered.map((tool) => [tool.name, tool])
9922
9938
  );
@@ -9957,7 +9973,7 @@ var AgentRunner = class {
9957
9973
  break;
9958
9974
  }
9959
9975
  const nativeTools = options.nativeTools !== false && Boolean(options.provider.supportsToolCalling && options.provider.chatWithTools);
9960
- const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools);
9976
+ const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools, options.autonomous);
9961
9977
  const requestContext = {
9962
9978
  turn: index + 1,
9963
9979
  mode,
@@ -10027,6 +10043,8 @@ var AgentRunner = class {
10027
10043
  role: "assistant",
10028
10044
  content: JSON.stringify(parsed)
10029
10045
  });
10046
+ steps.push(step);
10047
+ await checkpoint();
10030
10048
  const modeAllowlist = getEnforcedModeToolAllowlist(mode);
10031
10049
  const toolContext = {
10032
10050
  workspace: options.workspace,
@@ -10055,6 +10073,7 @@ var AgentRunner = class {
10055
10073
  pinnedMessages,
10056
10074
  fileProvenance
10057
10075
  );
10076
+ await checkpoint();
10058
10077
  if (options.signal?.aborted) {
10059
10078
  stopReason = "aborted";
10060
10079
  break;
@@ -10107,6 +10126,7 @@ var AgentRunner = class {
10107
10126
  pinnedMessages,
10108
10127
  fileProvenance
10109
10128
  );
10129
+ await checkpoint();
10110
10130
  }
10111
10131
  publishAgentProgress({ type: "batch-end", batchId });
10112
10132
  if (options.signal?.aborted) {
@@ -10114,8 +10134,6 @@ var AgentRunner = class {
10114
10134
  break;
10115
10135
  }
10116
10136
  }
10117
- steps.push(step);
10118
- await checkpoint();
10119
10137
  if (stopReason === "aborted") break;
10120
10138
  if (parsed.done && parsed.toolCalls.length === 0) {
10121
10139
  stopReason = "completed";
@@ -10159,6 +10177,10 @@ async function compactIfNeeded(options, messages, system, task, constraints, con
10159
10177
  summarisedTurns: plan.summarisedTurns
10160
10178
  });
10161
10179
  } catch (error) {
10180
+ if (error instanceof PinnedContextBudgetError) {
10181
+ trace.end({ outcome: "budget-error", error });
10182
+ throw error;
10183
+ }
10162
10184
  if (!(error instanceof ContextBudgetError)) {
10163
10185
  trace.end({ outcome: "error", error });
10164
10186
  throw error;
@@ -10220,52 +10242,28 @@ function capResultForTranscript(result, maxChars) {
10220
10242
  });
10221
10243
  return capped.truncated ? { ...result, content: capped.content } : result;
10222
10244
  }
10223
- function contextSection(result) {
10224
- const content = capToolResultContent(result.content, { tool: result.tool }).content;
10225
- if (!hasTruncatedData(result.data)) return content;
10226
- return `${content}
10227
-
10228
- [ultrac: initial ${result.tool} context is incomplete; query the tool for a narrower view before assuming omitted items are absent.]`;
10229
- }
10230
- function hasTruncatedData(data) {
10231
- return typeof data === "object" && data !== null && "truncated" in data && data.truncated === true;
10232
- }
10233
- async function gatherWorkspaceContext(registry, workspace) {
10234
- const readOnlyContext = {
10235
- workspace,
10236
- permissions: { write: false, shell: false, network: false }
10237
- };
10238
- const [project, repoMap, repoHandbook, verificationPlan, worktree, snapshot] = await Promise.all([
10239
- registry.execute({ tool: "inspect_project", input: {} }, readOnlyContext),
10240
- registry.execute({ tool: "repo_map", input: { maxItems: 10 } }, readOnlyContext),
10241
- registry.execute({ tool: "repo_handbook", input: { maxFilesPerBehavior: 6 } }, readOnlyContext),
10242
- registry.execute({ tool: "verification_plan", input: {} }, readOnlyContext),
10243
- registry.execute({ tool: "git_worktree_summary", input: { includeStats: false } }, readOnlyContext),
10244
- registry.execute(
10245
- { tool: "list_files", input: { patterns: ["**/*"], includeHidden: false, maxResults: 80 } },
10246
- readOnlyContext
10247
- )
10245
+ var STARTUP_EXPLORE_TOOLS = [
10246
+ "inspect_project",
10247
+ "repo_map",
10248
+ "repo_handbook",
10249
+ "git_worktree_summary",
10250
+ "git_status",
10251
+ "list_files",
10252
+ "read_file",
10253
+ "read_many_files",
10254
+ "search_files"
10255
+ ];
10256
+ async function gatherWorkspaceContext(registry, workspace, advertisedNames) {
10257
+ const [memory, skills] = await Promise.all([
10258
+ loadMemoryContext(workspace),
10259
+ registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(workspace) : Promise.resolve("Skills are unavailable in this tool set.")
10248
10260
  ]);
10249
- const [memory, skills] = await Promise.all([loadMemoryContext(workspace), registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(workspace) : Promise.resolve("Skills are unavailable in this tool set.")]);
10261
+ const available = new Set(advertisedNames ?? registry.list().map((tool) => tool.name));
10262
+ const explore = STARTUP_EXPLORE_TOOLS.filter((name) => available.has(name));
10263
+ const exploreLine = explore.length ? `Call ${explore.join(", ")} for the files and structure this task needs. This header is a catalog, not the repository.` : "Inspect the workspace with the advertised tools before acting. This header is a catalog, not the repository.";
10250
10264
  return `Workspace: ${workspace}
10251
10265
 
10252
- Project summary:
10253
- ${contextSection(project)}
10254
-
10255
- Repository map:
10256
- ${contextSection(repoMap)}
10257
-
10258
- Repo handbook:
10259
- ${contextSection(repoHandbook)}
10260
-
10261
- Verification plan:
10262
- ${contextSection(verificationPlan)}
10263
-
10264
- Worktree summary:
10265
- ${worktree.ok ? contextSection(worktree) : "No Git worktree detected."}
10266
-
10267
- Workspace snapshot:
10268
- ${contextSection(snapshot)}
10266
+ ${exploreLine}
10269
10267
 
10270
10268
  Memory (stored facts - load a specific one with memory_read):
10271
10269
  ${memory}
@@ -10273,42 +10271,21 @@ ${memory}
10273
10271
  Skills (playbooks - load full steps with the skill tool before acting on a matching task):
10274
10272
  ${skills}`;
10275
10273
  }
10276
- function buildInitialUserMessage(task, workspace, project, repoMap, repoHandbook, verificationPlan, worktree, snapshot, modeLabel, memory, skills, previousSession, restored) {
10274
+ function buildTaskUserMessage(task, modeLabel, previousSession, restored) {
10277
10275
  const previousContext = previousSession ? `
10278
10276
  Previous session:
10279
10277
  ${summarizePreviousSession(previousSession, restored)}
10280
10278
  ` : "";
10281
- return `Workspace: ${workspace}
10282
- Mode: ${modeLabel}
10279
+ return `Mode: ${modeLabel}
10283
10280
  ${previousContext}
10284
-
10285
- Project summary:
10286
- ${project}
10287
-
10288
- Repository map:
10289
- ${repoMap}
10290
-
10291
- Repo handbook:
10292
- ${repoHandbook}
10293
-
10294
- Verification plan:
10295
- ${verificationPlan}
10296
-
10297
- Worktree summary:
10298
- ${worktree}
10299
-
10300
- Workspace snapshot:
10301
- ${snapshot}
10302
-
10303
- Memory (stored facts - load a specific one with memory_read):
10304
- ${memory}
10305
-
10306
- Skills (playbooks - load full steps with the skill tool before acting on a matching task):
10307
- ${skills}
10308
-
10309
10281
  Task:
10310
10282
  ${task}`;
10311
10283
  }
10284
+ function advertisedExploreGuidance(advertised) {
10285
+ const names = STARTUP_EXPLORE_TOOLS.filter((name) => advertised.has(name));
10286
+ if (!names.length) return void 0;
10287
+ return `The startup header is a catalog, not the repository. Call ${names.join(", ")} for the files and structure this task needs; do not assume a file exists because it was mentioned earlier.`;
10288
+ }
10312
10289
  var NOTHING_RESTORED = {
10313
10290
  messages: [],
10314
10291
  steps: 0,
@@ -10372,7 +10349,15 @@ function summarizePreviousSession(session, restored) {
10372
10349
  function plural3(value, noun) {
10373
10350
  return `${value.toLocaleString("en-US")} ${noun}${value === 1 ? "" : "s"}`;
10374
10351
  }
10375
- function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
10352
+ function autonomousGuidance(phone, askUserAdvertised) {
10353
+ const ask2 = !askUserAdvertised ? "" : phone ? "\n- ask_user reaches the user's phone and interrupts them. Use it only for what you cannot decide or obtain yourself: a credential, a payment, an irreversible action outside this workspace, or a choice between materially different outcomes. For everything else, decide, state the assumption, and continue." : "\n- ask_user has no one to reach in this run. Decide, state the assumption, and continue.";
10354
+ return `
10355
+ Autonomous run: no human is watching and every permission is granted.
10356
+ - Carry the task through to a verified finish. Do not stop to ask for confirmation, and do not end with a plan, a question or a list of next steps you could do yourself.
10357
+ - When something fails, diagnose it and try another approach before giving up.${ask2}
10358
+ `;
10359
+ }
10360
+ function buildSystemPrompt(tools, permissions, mode, nativeTools = false, autonomous) {
10376
10361
  const renderedTools = tools.map((tool) => {
10377
10362
  const permissionText = tool.permissions.length ? ` Requires: ${tool.permissions.join(", ")}.` : "";
10378
10363
  const optionalText = tool.optionalPermissions.length ? ` Uses when granted: ${tool.optionalPermissions.join(", ")}.` : "";
@@ -10380,6 +10365,7 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
10380
10365
  }).join("\n");
10381
10366
  const advertised = new Set(tools.map((tool) => tool.name));
10382
10367
  const guidance = [
10368
+ advertisedExploreGuidance(advertised),
10383
10369
  advertised.has("context_bundle") ? "Use context_bundle when a task needs a portable working set for model handoff, eval fixtures, desktop copy flows, or future replay. Save it only when write permission is available or the user asked for an artifact." : void 0,
10384
10370
  advertised.has("verification_report") ? "When you have run or accounted for verification, use verification_report to summarize the outcome and evidence. Save it only when write permission is available or the user asked for an artifact." : void 0,
10385
10371
  advertised.has("diagnostics") ? "After any edit, call diagnostics with paths set to the files you just wrote, and fix what it reports before declaring the work done." : void 0,
@@ -10389,11 +10375,12 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
10389
10375
  advertised.has("memory_write") ? "When you learn a durable fact, preference, or a correction from the user, persist it with memory_write so it survives future sessions; read the full text of a relevant memory with memory_read." : void 0,
10390
10376
  advertised.has("scan_secrets") ? "For local evidence prefer inspect_artifact, hex_dump, extract_strings, scan_secrets, and file_timeline over a shell. scan_secrets redacts matches; do not round-trip those findings into a write that stores live secrets." : void 0
10391
10377
  ].filter((line) => Boolean(line));
10378
+ const autonomyLines = autonomous ? autonomousGuidance(autonomous.phone, advertised.has("ask_user")) : "";
10392
10379
  return `You are ultrac, the terminal-native coding agent for Ultrac.
10393
10380
 
10394
10381
  Mode: ${mode.label}
10395
10382
  ${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
10396
-
10383
+ ${autonomyLines}
10397
10384
  ${UNTRUSTED_CONTENT_GUIDANCE}
10398
10385
 
10399
10386
  ${nativeTools ? "Write complete code files or append complete sections across tool calls for large implementations. Verify files with tests or a compiler. Never replace requested code with placeholders or ellipses. Keep final explanations concise. Use the provided native function tools for actions. Do not put a JSON tool-call protocol in your answer. When the requested work is complete, give the final answer without calling more tools." : `Respond only with a JSON object:
@@ -13953,24 +13940,24 @@ var McpConnection = class {
13953
13940
  };
13954
13941
  function childEnvFor(spec, source = process.env) {
13955
13942
  const { env: sanitized } = createSanitizedProcessEnv(source);
13956
- const env = { ...sanitized };
13943
+ const env2 = { ...sanitized };
13957
13944
  for (const key of spec.envPassthrough) {
13958
13945
  if (isSensitiveEnvName(key)) continue;
13959
13946
  const value = source[key];
13960
13947
  if (value !== void 0) {
13961
- env[key] = value;
13948
+ env2[key] = value;
13962
13949
  }
13963
13950
  }
13964
- Object.assign(env, spec.env);
13965
- return env;
13951
+ Object.assign(env2, spec.env);
13952
+ return env2;
13966
13953
  }
13967
13954
  function spawnStdioTransport(spec, workspace) {
13968
- const env = childEnvFor(spec);
13955
+ const env2 = childEnvFor(spec);
13969
13956
  const cwd = spec.cwd ? resolveInsideWorkspace(workspace, spec.cwd) : workspace;
13970
13957
  const child = spawn3(spec.command, spec.args, {
13971
13958
  cwd,
13972
13959
  stdio: ["pipe", "pipe", "pipe"],
13973
- env,
13960
+ env: env2,
13974
13961
  detached: process.platform !== "win32",
13975
13962
  windowsHide: true
13976
13963
  });
@@ -14562,10 +14549,479 @@ async function readStdin() {
14562
14549
  return Buffer.concat(chunks).toString("utf8");
14563
14550
  }
14564
14551
 
14552
+ // src/commands/notify.ts
14553
+ import chalk6 from "chalk";
14554
+
14555
+ // src/notify/config.ts
14556
+ import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
14557
+ import { join as join12 } from "path";
14558
+ import { z as z31 } from "zod";
14559
+ var TelegramSchema = z31.object({
14560
+ botToken: z31.string().min(1),
14561
+ chatId: z31.string().min(1)
14562
+ });
14563
+ var TwilioSchema = z31.object({
14564
+ accountSid: z31.string().regex(/^AC[0-9a-fA-F]{32}$/, "Twilio account SIDs start with AC followed by 32 hex characters."),
14565
+ authToken: z31.string().min(1),
14566
+ /** The Twilio number Ultrac texts and calls from, E.164. */
14567
+ from: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +14155550100."),
14568
+ /** The user's phone, E.164. */
14569
+ to: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +919812345678."),
14570
+ /** Escalate an unanswered question to a voice call. */
14571
+ call: z31.boolean().default(true)
14572
+ });
14573
+ var NotifyConfigSchema = z31.object({
14574
+ version: z31.literal(1).default(1),
14575
+ telegram: TelegramSchema.optional(),
14576
+ twilio: TwilioSchema.optional(),
14577
+ /** How long a question waits for a text reply before Ultrac phones. */
14578
+ callAfterSeconds: z31.number().int().min(0).max(3600).default(180),
14579
+ /** How long a question waits in total before the agent proceeds on its own judgement. */
14580
+ answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
14581
+ });
14582
+ function notifyConfigPath() {
14583
+ return join12(ultracHomeDir(), "notify.json");
14584
+ }
14585
+ async function readStoredNotifyConfig() {
14586
+ let text;
14587
+ try {
14588
+ text = await readFile12(notifyConfigPath(), "utf8");
14589
+ } catch (error) {
14590
+ if (error.code === "ENOENT") return null;
14591
+ throw error;
14592
+ }
14593
+ return NotifyConfigSchema.parse(JSON.parse(text));
14594
+ }
14595
+ async function writeNotifyConfig(config) {
14596
+ await ensureUltracHome();
14597
+ const path = notifyConfigPath();
14598
+ const parsed = NotifyConfigSchema.parse(config);
14599
+ await writeFile11(path, `${JSON.stringify(parsed, null, 2)}
14600
+ `, { mode: 384 });
14601
+ await chmod3(path, 384).catch(() => void 0);
14602
+ return path;
14603
+ }
14604
+ async function removeNotifyConfig() {
14605
+ await rm4(notifyConfigPath(), { force: true });
14606
+ }
14607
+ function env(name) {
14608
+ return process.env[name]?.trim() || void 0;
14609
+ }
14610
+ async function loadNotifyConfig() {
14611
+ const stored = await readStoredNotifyConfig();
14612
+ const base = stored ?? NotifyConfigSchema.parse({});
14613
+ const telegramToken = env("ULTRAC_TELEGRAM_BOT_TOKEN");
14614
+ const telegramChat = env("ULTRAC_TELEGRAM_CHAT_ID");
14615
+ const telegram = telegramToken && telegramChat ? { botToken: telegramToken, chatId: telegramChat } : base.telegram;
14616
+ const sid = env("ULTRAC_TWILIO_ACCOUNT_SID");
14617
+ const token = env("ULTRAC_TWILIO_AUTH_TOKEN");
14618
+ const from = env("ULTRAC_TWILIO_FROM");
14619
+ const to = env("ULTRAC_NOTIFY_PHONE");
14620
+ const twilio = sid && token && from && to ? { accountSid: sid, authToken: token, from, to, call: base.twilio?.call ?? true } : base.twilio;
14621
+ const merged = NotifyConfigSchema.parse({ ...base, telegram, twilio });
14622
+ return merged.telegram || merged.twilio ? merged : null;
14623
+ }
14624
+ function redact(secret) {
14625
+ return secret.length <= 4 ? "****" : `****${secret.slice(-4)}`;
14626
+ }
14627
+
14628
+ // src/notify/channel.ts
14629
+ function sleep(ms, signal) {
14630
+ if (signal?.aborted) return Promise.resolve(false);
14631
+ return new Promise((resolve15) => {
14632
+ const timer = setTimeout(() => {
14633
+ signal?.removeEventListener("abort", onAbort);
14634
+ resolve15(true);
14635
+ }, ms);
14636
+ const onAbort = () => {
14637
+ clearTimeout(timer);
14638
+ resolve15(false);
14639
+ };
14640
+ signal?.addEventListener("abort", onAbort, { once: true });
14641
+ });
14642
+ }
14643
+ function withTimeout2(signal, ms) {
14644
+ const timeout = AbortSignal.timeout(ms);
14645
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
14646
+ }
14647
+ var NotifyChannelError = class extends Error {
14648
+ constructor(channel, message) {
14649
+ super(`${channel}: ${message}`);
14650
+ this.name = "NotifyChannelError";
14651
+ }
14652
+ };
14653
+
14654
+ // src/notify/telegram.ts
14655
+ var API = "https://api.telegram.org";
14656
+ var LONG_POLL_SECONDS = 25;
14657
+ var RETRY_DELAY_MS = 5e3;
14658
+ var MAX_MESSAGE_CHARS = 4e3;
14659
+ async function callTelegram(fetchFn, botToken, method, body, signal) {
14660
+ const response = await fetchFn(`${API}/bot${botToken}/${method}`, {
14661
+ method: "POST",
14662
+ headers: { "content-type": "application/json" },
14663
+ body: JSON.stringify(body),
14664
+ signal
14665
+ });
14666
+ const json = await response.json().catch(() => ({ ok: false }));
14667
+ if (!response.ok || !json.ok || json.result === void 0) {
14668
+ throw new NotifyChannelError("telegram", json.description ?? `HTTP ${response.status}`);
14669
+ }
14670
+ return json.result;
14671
+ }
14672
+ var TelegramChannel = class {
14673
+ constructor(config, fetchFn = fetch) {
14674
+ this.config = config;
14675
+ this.fetchFn = fetchFn;
14676
+ }
14677
+ config;
14678
+ fetchFn;
14679
+ name = "telegram";
14680
+ offset;
14681
+ async send(text, signal) {
14682
+ await callTelegram(
14683
+ this.fetchFn,
14684
+ this.config.botToken,
14685
+ "sendMessage",
14686
+ { chat_id: this.config.chatId, text: text.slice(0, MAX_MESSAGE_CHARS) },
14687
+ withTimeout2(signal, 15e3)
14688
+ );
14689
+ }
14690
+ async waitForReply(since, signal) {
14691
+ const sinceSeconds = Math.floor(since.getTime() / 1e3);
14692
+ while (!signal.aborted) {
14693
+ let updates;
14694
+ try {
14695
+ updates = await callTelegram(
14696
+ this.fetchFn,
14697
+ this.config.botToken,
14698
+ "getUpdates",
14699
+ {
14700
+ timeout: LONG_POLL_SECONDS,
14701
+ allowed_updates: ["message"],
14702
+ ...this.offset !== void 0 ? { offset: this.offset } : {}
14703
+ },
14704
+ withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
14705
+ );
14706
+ } catch {
14707
+ if (signal.aborted) return null;
14708
+ await sleep(RETRY_DELAY_MS, signal);
14709
+ continue;
14710
+ }
14711
+ for (const update of updates) {
14712
+ this.offset = update.update_id + 1;
14713
+ const message = update.message;
14714
+ if (!message?.text) continue;
14715
+ if (String(message.chat.id) !== this.config.chatId) continue;
14716
+ if (message.date < sinceSeconds) continue;
14717
+ return message.text;
14718
+ }
14719
+ }
14720
+ return null;
14721
+ }
14722
+ };
14723
+ async function discoverTelegramChat(botToken, signal, fetchFn = fetch) {
14724
+ const startedSeconds = Math.floor(Date.now() / 1e3) - 60;
14725
+ let offset;
14726
+ while (!signal.aborted) {
14727
+ let updates;
14728
+ try {
14729
+ updates = await callTelegram(
14730
+ fetchFn,
14731
+ botToken,
14732
+ "getUpdates",
14733
+ { timeout: LONG_POLL_SECONDS, allowed_updates: ["message"], ...offset !== void 0 ? { offset } : {} },
14734
+ withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
14735
+ );
14736
+ } catch (error) {
14737
+ if (signal.aborted) return null;
14738
+ throw error;
14739
+ }
14740
+ for (const update of updates) {
14741
+ offset = update.update_id + 1;
14742
+ const message = update.message;
14743
+ if (!message || message.date < startedSeconds) continue;
14744
+ const chat = message.chat;
14745
+ return { chatId: String(chat.id), name: chat.title ?? chat.username ?? chat.first_name ?? String(chat.id) };
14746
+ }
14747
+ }
14748
+ return null;
14749
+ }
14750
+
14751
+ // src/notify/twilio.ts
14752
+ var API2 = "https://api.twilio.com/2010-04-01";
14753
+ var POLL_INTERVAL_MS = 1e4;
14754
+ var MAX_SMS_CHARS = 1500;
14755
+ var MAX_SPOKEN_CHARS = 1e3;
14756
+ function escapeXml(text) {
14757
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
14758
+ }
14759
+ var TwilioChannel = class {
14760
+ constructor(config, fetchFn = fetch) {
14761
+ this.config = config;
14762
+ this.fetchFn = fetchFn;
14763
+ }
14764
+ config;
14765
+ fetchFn;
14766
+ name = "sms";
14767
+ seen = /* @__PURE__ */ new Set();
14768
+ get authHeader() {
14769
+ return `Basic ${Buffer.from(`${this.config.accountSid}:${this.config.authToken}`).toString("base64")}`;
14770
+ }
14771
+ async request(method, path, params, signal) {
14772
+ const search = new URLSearchParams(params);
14773
+ const url = `${API2}/Accounts/${this.config.accountSid}/${path}${method === "GET" ? `?${search}` : ""}`;
14774
+ const response = await this.fetchFn(url, {
14775
+ method,
14776
+ headers: {
14777
+ authorization: this.authHeader,
14778
+ ...method === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
14779
+ },
14780
+ ...method === "POST" ? { body: search.toString() } : {},
14781
+ signal: withTimeout2(signal, 15e3)
14782
+ });
14783
+ const json = await response.json().catch(() => ({}));
14784
+ if (!response.ok) {
14785
+ throw new NotifyChannelError("twilio", json.message ?? `HTTP ${response.status}`);
14786
+ }
14787
+ return json;
14788
+ }
14789
+ async send(text, signal) {
14790
+ await this.request(
14791
+ "POST",
14792
+ "Messages.json",
14793
+ { To: this.config.to, From: this.config.from, Body: text.slice(0, MAX_SMS_CHARS) },
14794
+ signal
14795
+ );
14796
+ }
14797
+ async call(text, signal) {
14798
+ if (!this.config.call) return;
14799
+ const spoken = escapeXml(text.slice(0, MAX_SPOKEN_CHARS));
14800
+ const twiml = `<Response><Say>${spoken}</Say><Pause length="2"/><Say>Again. ${spoken}</Say></Response>`;
14801
+ await this.request("POST", "Calls.json", { To: this.config.to, From: this.config.from, Twiml: twiml }, signal);
14802
+ }
14803
+ async waitForReply(since, signal) {
14804
+ const day = since.toISOString().slice(0, 10);
14805
+ while (!signal.aborted) {
14806
+ try {
14807
+ const page = await this.request(
14808
+ "GET",
14809
+ "Messages.json",
14810
+ { To: this.config.from, From: this.config.to, "DateSent>": day, PageSize: "50" },
14811
+ signal
14812
+ );
14813
+ const replies = page.messages.filter((message) => message.direction === "inbound" && !this.seen.has(message.sid)).map((message) => ({ message, at: new Date(message.date_sent ?? message.date_created) })).filter(({ at }) => at.getTime() >= since.getTime()).sort((left, right) => left.at.getTime() - right.at.getTime());
14814
+ const first = replies[0];
14815
+ if (first) {
14816
+ this.seen.add(first.message.sid);
14817
+ return first.message.body;
14818
+ }
14819
+ } catch {
14820
+ if (signal.aborted) return null;
14821
+ }
14822
+ await sleep(POLL_INTERVAL_MS, signal);
14823
+ }
14824
+ return null;
14825
+ }
14826
+ };
14827
+
14828
+ // src/notify/notifier.ts
14829
+ var CLOCK_SKEW_MS = 5e3;
14830
+ var Notifier = class {
14831
+ constructor(channels, timing, heading = "Ultrac", now = () => /* @__PURE__ */ new Date()) {
14832
+ this.channels = channels;
14833
+ this.timing = timing;
14834
+ this.heading = heading;
14835
+ this.now = now;
14836
+ if (channels.length === 0) throw new Error("A notifier needs at least one channel.");
14837
+ }
14838
+ channels;
14839
+ timing;
14840
+ heading;
14841
+ now;
14842
+ describe() {
14843
+ return this.channels.map((channel) => channel.name).join(" + ");
14844
+ }
14845
+ /**
14846
+ * Fire-and-report: send `text` everywhere and return the channels that
14847
+ * failed. Throws only when every channel failed.
14848
+ */
14849
+ async send(text, signal) {
14850
+ const results = await Promise.allSettled(this.channels.map((channel) => channel.send(`${this.heading}: ${text}`, signal)));
14851
+ const failures = results.flatMap(
14852
+ (result, index) => result.status === "rejected" ? [`${this.channels[index].name}: ${String(result.reason?.message ?? result.reason)}`] : []
14853
+ );
14854
+ if (failures.length === this.channels.length) {
14855
+ throw new Error(`Could not reach the user on any channel (${failures.join("; ")}).`);
14856
+ }
14857
+ return failures;
14858
+ }
14859
+ async ask(question, options, signal) {
14860
+ const since = new Date(this.now().getTime() - CLOCK_SKEW_MS);
14861
+ const optionLines = options?.length ? `
14862
+
14863
+ ${options.map((option, index) => `${index + 1}. ${option}`).join("\n")}
14864
+
14865
+ Reply with a number or your own answer.` : "\n\nReply to this message with your answer.";
14866
+ await this.send(`needs your input.
14867
+
14868
+ ${question}${optionLines}`, signal);
14869
+ if (signal?.aborted) return null;
14870
+ const wait = new AbortController();
14871
+ const stop = () => wait.abort();
14872
+ signal?.addEventListener("abort", stop, { once: true });
14873
+ try {
14874
+ const replies = this.channels.map(
14875
+ (channel) => channel.waitForReply(since, wait.signal).catch(() => null).then((reply2) => {
14876
+ if (reply2 === null) return new Promise(() => void 0);
14877
+ return reply2;
14878
+ })
14879
+ );
14880
+ void (async () => {
14881
+ if (!await sleep(this.timing.callAfterMs, wait.signal)) return;
14882
+ const spoken = `This is Ultrac. Your agent needs your input. ${question} ${options?.length ? `Options: ${options.map((option, index) => `${index + 1}, ${option}.`).join(" ")}` : ""} Reply to the text message to answer.`;
14883
+ await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
14884
+ })();
14885
+ const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
14886
+ const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
14887
+ const reply = await Promise.race([...replies, timeout, cancelled]);
14888
+ return reply === null ? null : pickOption(reply, options);
14889
+ } finally {
14890
+ signal?.removeEventListener("abort", stop);
14891
+ wait.abort();
14892
+ }
14893
+ }
14894
+ };
14895
+ function pickOption(reply, options) {
14896
+ const answer = reply.trim();
14897
+ const picked = Number.parseInt(answer, 10);
14898
+ if (options?.length && String(picked) === answer && picked >= 1 && picked <= options.length) {
14899
+ return options[picked - 1];
14900
+ }
14901
+ return answer;
14902
+ }
14903
+ function createNotifier(config, heading, fetchFn = fetch) {
14904
+ const channels = [];
14905
+ if (config.telegram) channels.push(new TelegramChannel(config.telegram, fetchFn));
14906
+ if (config.twilio) channels.push(new TwilioChannel(config.twilio, fetchFn));
14907
+ return new Notifier(
14908
+ channels,
14909
+ { callAfterMs: config.callAfterSeconds * 1e3, timeoutMs: config.answerTimeoutSeconds * 1e3 },
14910
+ heading
14911
+ );
14912
+ }
14913
+
14914
+ // src/commands/notify.ts
14915
+ var DISCOVERY_TIMEOUT_MS = 12e4;
14916
+ function parseSeconds(value, flag) {
14917
+ if (value === void 0) return void 0;
14918
+ const parsed = Number.parseInt(value, 10);
14919
+ if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
14920
+ throw new Error(`${flag} must be a whole number of seconds.`);
14921
+ }
14922
+ return parsed;
14923
+ }
14924
+ function describeNotifyConfig(config) {
14925
+ const lines = [];
14926
+ if (config.telegram) {
14927
+ lines.push(`telegram chat ${config.telegram.chatId}, bot token ${redact(config.telegram.botToken)}`);
14928
+ }
14929
+ if (config.twilio) {
14930
+ lines.push(
14931
+ `sms ${config.twilio.from} -> ${config.twilio.to}, voice calls ${config.twilio.call ? "on" : "off"}, account ${redact(config.twilio.accountSid)}`
14932
+ );
14933
+ }
14934
+ lines.push(`timing calls after ${config.callAfterSeconds}s unanswered, gives up after ${config.answerTimeoutSeconds}s`);
14935
+ return lines;
14936
+ }
14937
+ function registerNotifyCommand(program2) {
14938
+ const notify = program2.command("notify").description("Let Ultrac reach your phone: questions from autonomous runs, and results when they finish");
14939
+ notify.command("setup").description("Configure Telegram and/or Twilio (SMS + voice calls). Stored in ~/.ultrac/notify.json, never in a workspace").option("--telegram-token <token>", "Telegram bot token from @BotFather").option("--telegram-chat <id>", "Telegram chat id (omit to detect it: send your bot any message during setup)").option("--twilio-sid <sid>", "Twilio account SID").option("--twilio-token <token>", "Twilio auth token").option("--twilio-from <number>", "Your Twilio phone number, E.164 (e.g. +14155550100)").option("--phone <number>", "Your phone number, E.164 (e.g. +919812345678)").option("--no-call", "Text only; never escalate an unanswered question to a voice call").option("--call-after <seconds>", "Seconds a question waits for a text reply before Ultrac calls (default 180)").option("--timeout <seconds>", "Seconds a question waits before the agent proceeds on its own (default 1800)").option("--no-test", "Skip the test message").action(async (options) => {
14940
+ const stored = await readStoredNotifyConfig() ?? NotifyConfigSchema.parse({});
14941
+ const next = { ...stored };
14942
+ if (options.telegramToken) {
14943
+ let chatId = options.telegramChat;
14944
+ if (!chatId) {
14945
+ console.log(chalk6.cyan("Open Telegram and send any message to your bot now. Waiting up to 2 minutes\u2026"));
14946
+ const found = await discoverTelegramChat(options.telegramToken, AbortSignal.timeout(DISCOVERY_TIMEOUT_MS));
14947
+ if (!found) throw new Error("No message reached the bot. Send it a message and run setup again, or pass --telegram-chat.");
14948
+ console.log(chalk6.green(`Found chat: ${found.name} (${found.chatId})`));
14949
+ chatId = found.chatId;
14950
+ }
14951
+ next.telegram = { botToken: options.telegramToken, chatId };
14952
+ } else if (options.telegramChat) {
14953
+ throw new Error("--telegram-chat needs --telegram-token.");
14954
+ }
14955
+ const twilioFlags = [options.twilioSid, options.twilioToken, options.twilioFrom, options.phone];
14956
+ if (twilioFlags.some(Boolean)) {
14957
+ if (!twilioFlags.every(Boolean)) {
14958
+ throw new Error("Twilio needs all four of --twilio-sid, --twilio-token, --twilio-from and --phone.");
14959
+ }
14960
+ next.twilio = {
14961
+ accountSid: options.twilioSid,
14962
+ authToken: options.twilioToken,
14963
+ from: options.twilioFrom,
14964
+ to: options.phone,
14965
+ call: options.call
14966
+ };
14967
+ } else if (next.twilio && options.call === false) {
14968
+ next.twilio = { ...next.twilio, call: false };
14969
+ }
14970
+ const callAfter = parseSeconds(options.callAfter, "--call-after");
14971
+ const timeout = parseSeconds(options.timeout, "--timeout");
14972
+ if (callAfter !== void 0) next.callAfterSeconds = callAfter;
14973
+ if (timeout !== void 0) next.answerTimeoutSeconds = timeout;
14974
+ if (!next.telegram && !next.twilio) {
14975
+ throw new Error(
14976
+ "Configure at least one channel: --telegram-token, or --twilio-sid/--twilio-token/--twilio-from/--phone."
14977
+ );
14978
+ }
14979
+ const path = await writeNotifyConfig(next);
14980
+ console.log(chalk6.green(`Saved ${path}`));
14981
+ for (const line of describeNotifyConfig(NotifyConfigSchema.parse(next))) console.log(` ${line}`);
14982
+ if (options.test) {
14983
+ const failures = await createNotifier(NotifyConfigSchema.parse(next)).send(
14984
+ "is connected. This is where your agent will reach you."
14985
+ );
14986
+ for (const failure of failures) console.error(chalk6.yellow(` ${failure}`));
14987
+ console.log(chalk6.green("Test message sent."));
14988
+ }
14989
+ });
14990
+ notify.command("status").description("Show the configured phone channels").action(async () => {
14991
+ const config = await loadNotifyConfig();
14992
+ if (!config) {
14993
+ console.log("No phone channel configured. Run `ultrac notify setup`.");
14994
+ return;
14995
+ }
14996
+ console.log(chalk6.bold(`Phone channels (${notifyConfigPath()} + environment)`));
14997
+ for (const line of describeNotifyConfig(config)) console.log(` ${line}`);
14998
+ });
14999
+ notify.command("test").description("Send a test message; with --ask, wait for your reply (and call you if it does not come)").option("--ask", "Ask a question and wait for the reply", false).action(async (options) => {
15000
+ const config = await loadNotifyConfig();
15001
+ if (!config) throw new Error("No phone channel configured. Run `ultrac notify setup`.");
15002
+ const notifier = createNotifier(config);
15003
+ if (!options.ask) {
15004
+ const failures = await notifier.send("test message. Your phone channel works.");
15005
+ for (const failure of failures) console.error(chalk6.yellow(failure));
15006
+ console.log(chalk6.green(`Sent via ${notifier.describe()}.`));
15007
+ return;
15008
+ }
15009
+ const controller = new AbortController();
15010
+ process.once("SIGINT", () => controller.abort());
15011
+ console.log(chalk6.cyan(`Asked via ${notifier.describe()}. Reply from your phone (Ctrl-C to stop)\u2026`));
15012
+ const answer = await notifier.ask("This is a test. Reply with anything.", ["Works", "Something is off"], controller.signal);
15013
+ console.log(answer === null ? chalk6.yellow("No reply before the timeout.") : chalk6.green(`Reply: ${answer}`));
15014
+ });
15015
+ notify.command("remove").description("Delete ~/.ultrac/notify.json (environment variables are not affected)").action(async () => {
15016
+ await removeNotifyConfig();
15017
+ console.log(`Removed ${notifyConfigPath()}`);
15018
+ });
15019
+ }
15020
+
14565
15021
  // src/commands/repl.ts
14566
15022
  import { createInterface as createInterface3 } from "readline";
14567
15023
  import { stdin as stdin5, stdout as stdout5 } from "process";
14568
- import chalk10 from "chalk";
15024
+ import chalk11 from "chalk";
14569
15025
 
14570
15026
  // src/config/budget.ts
14571
15027
  function applyBudgetFlags(flags, config) {
@@ -14595,7 +15051,7 @@ function parseIntFlag(raw, flag) {
14595
15051
 
14596
15052
  // src/commands/trust.ts
14597
15053
  import { resolve as resolve11 } from "path";
14598
- import chalk6 from "chalk";
15054
+ import chalk7 from "chalk";
14599
15055
 
14600
15056
  // src/ui/confirm.ts
14601
15057
  import { stdin as stdin4, stdout as stdout2 } from "process";
@@ -14666,41 +15122,41 @@ function redactCommand(command) {
14666
15122
  }
14667
15123
  function printTrustReview(workspace, raw, escalations, status) {
14668
15124
  if (escalations) {
14669
- console.log(chalk6.yellow(`
15125
+ console.log(chalk7.yellow(`
14670
15126
  This workspace's .ultrac/config.json asks for ${describeEscalations(escalations)}.`));
14671
15127
  } else if (status === "legacy") {
14672
- console.log(chalk6.yellow("\n This workspace has an older approval that was not bound to its config."));
15128
+ console.log(chalk7.yellow("\n This workspace has an older approval that was not bound to its config."));
14673
15129
  } else if (status === "changed") {
14674
- console.log(chalk6.yellow("\n This workspace's security-sensitive config changed since it was approved."));
15130
+ console.log(chalk7.yellow("\n This workspace's security-sensitive config changed since it was approved."));
14675
15131
  } else {
14676
- console.log(chalk6.dim("\n This workspace's config asks for no additional permissions or MCP servers."));
15132
+ console.log(chalk7.dim("\n This workspace's config asks for no additional permissions or MCP servers."));
14677
15133
  }
14678
15134
  const preview = renderMcpTrustPreview(raw);
14679
- for (const line of preview.lines) console.log(chalk6.dim(` ${line}`));
14680
- console.log(chalk6.dim(" Literal MCP env values are hidden; only variable names are shown."));
14681
- console.log(chalk6.dim(" MCP processes are not sandboxed and run with your OS user's privileges."));
14682
- console.log(chalk6.dim(" Argument redaction is best-effort; review .ultrac/config.json locally for secrets before approval."));
14683
- console.log(chalk6.dim(" Approval is local to this workspace and bound to its current permissions and MCP launch config."));
15135
+ for (const line of preview.lines) console.log(chalk7.dim(` ${line}`));
15136
+ console.log(chalk7.dim(" Literal MCP env values are hidden; only variable names are shown."));
15137
+ console.log(chalk7.dim(" MCP processes are not sandboxed and run with your OS user's privileges."));
15138
+ console.log(chalk7.dim(" Argument redaction is best-effort; review .ultrac/config.json locally for secrets before approval."));
15139
+ console.log(chalk7.dim(" Approval is local to this workspace and bound to its current permissions and MCP launch config."));
14684
15140
  return { hasInvalidServers: preview.hasInvalidServers };
14685
15141
  }
14686
15142
  async function offerWorkspaceTrust(workspace, escalations, snapshot, status) {
14687
15143
  const raw = await readRawConfig(workspace);
14688
15144
  if (workspaceTrustSnapshot(raw).securityFingerprint !== snapshot.securityFingerprint) {
14689
- console.log(chalk6.yellow("\n Workspace security config changed during review. Nothing was approved; review it again."));
15145
+ console.log(chalk7.yellow("\n Workspace security config changed during review. Nothing was approved; review it again."));
14690
15146
  return false;
14691
15147
  }
14692
15148
  const { hasInvalidServers } = printTrustReview(workspace, raw, escalations, status);
14693
15149
  if (hasInvalidServers) {
14694
- console.log(chalk6.yellow(" Invalid MCP entries cannot be approved. Fix the config and review it again.\n"));
15150
+ console.log(chalk7.yellow(" Invalid MCP entries cannot be approved. Fix the config and review it again.\n"));
14695
15151
  return false;
14696
15152
  }
14697
15153
  const granted = await confirmYesNo(` Trust ${escapeTerminalControls(workspace)}? [y/N]`, false);
14698
15154
  if (granted) {
14699
15155
  const recorded = await trustWorkspace(workspace, snapshot);
14700
- console.log(chalk6.green(` Trusted ${escapeTerminalControls(recorded)}.`));
15156
+ console.log(chalk7.green(` Trusted ${escapeTerminalControls(recorded)}.`));
14701
15157
  return true;
14702
15158
  }
14703
- console.log(chalk6.dim(` Not trusted. Those settings were not applied; run ${chalk6.cyan("ultrac trust")} here to allow them.
15159
+ console.log(chalk7.dim(` Not trusted. Those settings were not applied; run ${chalk7.cyan("ultrac trust")} here to allow them.
14704
15160
  `));
14705
15161
  return false;
14706
15162
  }
@@ -14709,7 +15165,7 @@ function registerTrustCommand(program2) {
14709
15165
  if (options.list) {
14710
15166
  const trusted = await listTrustedWorkspaces();
14711
15167
  if (trusted.length === 0) {
14712
- console.log(chalk6.dim("No trusted workspaces."));
15168
+ console.log(chalk7.dim("No trusted workspaces."));
14713
15169
  return;
14714
15170
  }
14715
15171
  for (const path of trusted) console.log(escapeTerminalControls(path));
@@ -14719,7 +15175,7 @@ function registerTrustCommand(program2) {
14719
15175
  if (options.revoke) {
14720
15176
  const had = await untrustWorkspace(workspace);
14721
15177
  const safeWorkspace = escapeTerminalControls(workspace);
14722
- console.log(had ? `No longer trusting ${safeWorkspace}.` : chalk6.dim(`${safeWorkspace} was not trusted.`));
15178
+ console.log(had ? `No longer trusting ${safeWorkspace}.` : chalk7.dim(`${safeWorkspace} was not trusted.`));
14723
15179
  return;
14724
15180
  }
14725
15181
  const raw = await readRawConfig(workspace);
@@ -14728,10 +15184,10 @@ function registerTrustCommand(program2) {
14728
15184
  if (status === "trusted") {
14729
15185
  const preview = printTrustReview(workspace, raw, snapshot.escalations, status);
14730
15186
  if (preview.hasInvalidServers) {
14731
- console.log(chalk6.yellow("This stored approval no longer has valid MCP launch config. Revoke it before changing the config."));
15187
+ console.log(chalk7.yellow("This stored approval no longer has valid MCP launch config. Revoke it before changing the config."));
14732
15188
  return;
14733
15189
  }
14734
- console.log(chalk6.green(`Already trusted for ${escapeTerminalControls(workspace)}.`));
15190
+ console.log(chalk7.green(`Already trusted for ${escapeTerminalControls(workspace)}.`));
14735
15191
  return;
14736
15192
  }
14737
15193
  await offerWorkspaceTrust(workspace, snapshot.escalations, snapshot, status);
@@ -14951,7 +15407,11 @@ var InteractiveSession = class {
14951
15407
  }
14952
15408
  async start() {
14953
15409
  if (this.started) return;
14954
- const context = await gatherWorkspaceContext(this.registry, this.options.workspace);
15410
+ const context = await gatherWorkspaceContext(
15411
+ this.registry,
15412
+ this.options.workspace,
15413
+ this.advertisedToolNames
15414
+ );
14955
15415
  const modeLabel = getAgentModePreset(this.activeMode).label;
14956
15416
  const contextMessage = {
14957
15417
  role: "user",
@@ -14965,6 +15425,13 @@ Acknowledge that you have the context and wait for the user's first request.`
14965
15425
  this.fileProvenance.set(contextMessage, unknownContextProvenance());
14966
15426
  this.started = true;
14967
15427
  }
15428
+ async checkpoint(handlers) {
15429
+ if (!handlers.onCheckpoint) return;
15430
+ try {
15431
+ await handlers.onCheckpoint(this.toSession());
15432
+ } catch {
15433
+ }
15434
+ }
14968
15435
  appendQueuedMessages(handlers) {
14969
15436
  const queued = handlers.drainQueuedMessages?.() ?? [];
14970
15437
  let appended = 0;
@@ -15043,6 +15510,8 @@ Acknowledge that you have the context and wait for the user's first request.`
15043
15510
  finalMessage = parsed.message;
15044
15511
  if (parsed.message) handlers.onAssistantMessage?.(parsed.message);
15045
15512
  this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
15513
+ this.steps.push(step);
15514
+ await this.checkpoint(handlers);
15046
15515
  for (const call of parsed.toolCalls) {
15047
15516
  if (handlers.signal?.aborted) break;
15048
15517
  const authorization = await this.authorize(call, handlers);
@@ -15060,6 +15529,7 @@ Acknowledge that you have the context and wait for the user's first request.`
15060
15529
  if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
15061
15530
  step.toolResults.push({ call, result: result2 });
15062
15531
  this.pushToolResult(call, result2);
15532
+ await this.checkpoint(handlers);
15063
15533
  continue;
15064
15534
  }
15065
15535
  handlers.onToolStart?.(call);
@@ -15074,8 +15544,8 @@ Acknowledge that you have the context and wait for the user's first request.`
15074
15544
  handlers.onToolResult?.(call, result);
15075
15545
  step.toolResults.push({ call, result });
15076
15546
  this.pushToolResult(call, result);
15547
+ await this.checkpoint(handlers);
15077
15548
  }
15078
- this.steps.push(step);
15079
15549
  if (handlers.signal?.aborted) {
15080
15550
  stopReason = "aborted";
15081
15551
  handlers.onInterrupted?.();
@@ -15151,6 +15621,10 @@ Acknowledge that you have the context and wait for the user's first request.`
15151
15621
  summarisedTurns: plan.summarisedTurns
15152
15622
  });
15153
15623
  } catch (error) {
15624
+ if (error instanceof PinnedContextBudgetError) {
15625
+ trace.end({ outcome: "budget-error", error });
15626
+ throw error;
15627
+ }
15154
15628
  if (!(error instanceof ContextBudgetError)) {
15155
15629
  trace.end({ outcome: "error", error });
15156
15630
  throw error;
@@ -15436,7 +15910,11 @@ var NativeSession = class {
15436
15910
  }
15437
15911
  async start() {
15438
15912
  if (this.started) return;
15439
- const context = await gatherWorkspaceContext(this.registry, this.options.workspace);
15913
+ const context = await gatherWorkspaceContext(
15914
+ this.registry,
15915
+ this.options.workspace,
15916
+ this.advertisedToolNames
15917
+ );
15440
15918
  this.messages.push({ role: "system", content: this.systemPrompt() });
15441
15919
  const contextMessage = {
15442
15920
  role: "user",
@@ -15448,6 +15926,13 @@ ${context}`
15448
15926
  this.fileProvenance.set(contextMessage, unknownContextProvenance());
15449
15927
  this.started = true;
15450
15928
  }
15929
+ async checkpoint(handlers) {
15930
+ if (!handlers.onCheckpoint) return;
15931
+ try {
15932
+ await handlers.onCheckpoint(this.toSession());
15933
+ } catch {
15934
+ }
15935
+ }
15451
15936
  appendQueuedMessages(handlers) {
15452
15937
  const queued = handlers.drainQueuedMessages?.() ?? [];
15453
15938
  let appended = 0;
@@ -15530,8 +16015,9 @@ ${context}`
15530
16015
  toolCalls: response.toolCalls
15531
16016
  });
15532
16017
  }
16018
+ this.steps.push(step);
16019
+ await this.checkpoint(handlers);
15533
16020
  if (!response.toolCalls.length) {
15534
- this.steps.push(step);
15535
16021
  if (handlers.signal?.aborted) {
15536
16022
  stopReason = "aborted";
15537
16023
  handlers.onInterrupted?.();
@@ -15561,6 +16047,7 @@ ${context}`
15561
16047
  if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
15562
16048
  step.toolResults.push({ call: displayCall, result: result2 });
15563
16049
  this.pushToolResult(call, result2);
16050
+ await this.checkpoint(handlers);
15564
16051
  continue;
15565
16052
  }
15566
16053
  handlers.onToolStart?.(displayCall);
@@ -15578,8 +16065,8 @@ ${context}`
15578
16065
  handlers.onToolResult?.(displayCall, result);
15579
16066
  step.toolResults.push({ call: displayCall, result });
15580
16067
  this.pushToolResult(call, result);
16068
+ await this.checkpoint(handlers);
15581
16069
  }
15582
- this.steps.push(step);
15583
16070
  if (handlers.signal?.aborted) {
15584
16071
  stopReason = "aborted";
15585
16072
  handlers.onInterrupted?.();
@@ -15651,6 +16138,10 @@ ${context}`
15651
16138
  summarisedTurns: plan.summarisedTurns
15652
16139
  });
15653
16140
  } catch (error) {
16141
+ if (error instanceof PinnedContextBudgetError) {
16142
+ trace.end({ outcome: "budget-error", error });
16143
+ throw error;
16144
+ }
15654
16145
  if (!(error instanceof ContextBudgetError)) {
15655
16146
  trace.end({ outcome: "error", error });
15656
16147
  throw error;
@@ -15750,6 +16241,7 @@ ${context}`
15750
16241
  const mode = getAgentModePreset(this.activeMode);
15751
16242
  const advertised = new Set(this.advertisedToolNames);
15752
16243
  const guidance = [
16244
+ advertisedExploreGuidance(advertised),
15753
16245
  advertised.has("diagnostics") ? "After any edit, call diagnostics with paths set to the files you just wrote, and fix what it reports before declaring the work done." : void 0,
15754
16246
  advertised.has("run_tests") && advertised.has("run_package_script") ? "Prefer run_tests over run_package_script when you want classified test failures." : void 0,
15755
16247
  advertised.has("task") ? "Delegate search-heavy exploration to task so its transcript stays out of this conversation." : void 0,
@@ -15844,7 +16336,7 @@ function createSessionCheckpointWriter(workspace, onFailure) {
15844
16336
  }
15845
16337
 
15846
16338
  // src/ui/output.ts
15847
- import chalk7 from "chalk";
16339
+ import chalk8 from "chalk";
15848
16340
  var stopReasonDetail = {
15849
16341
  "max-steps": "partial answer - hit the step cap before the model said it was done",
15850
16342
  aborted: "partial answer - the run was interrupted before the model said it was done",
@@ -15859,12 +16351,12 @@ function stopReasonLine(stopReason) {
15859
16351
  function printSession(session, sessionPath2) {
15860
16352
  for (const step of session.steps) {
15861
16353
  if (step.parsed?.message) {
15862
- console.log(chalk7.gray(`step ${step.index + 1}:`), step.parsed.message);
16354
+ console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
15863
16355
  }
15864
16356
  for (const toolResult of step.toolResults) {
15865
- const status = toolResult.result.ok ? chalk7.green("ok") : chalk7.red("failed");
15866
- console.log(chalk7.cyan(toolResult.call.tool), status);
15867
- console.log(chalk7.gray(indent2(toolResult.result.content)));
16357
+ 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)));
15868
16360
  }
15869
16361
  }
15870
16362
  if (session.finalMessage) {
@@ -15874,23 +16366,23 @@ function printSession(session, sessionPath2) {
15874
16366
  console.log();
15875
16367
  const stopped = stopReasonLine(session.stopReason);
15876
16368
  if (stopped) {
15877
- console.log(chalk7.yellow(stopped));
15878
- console.log(chalk7.gray(`Resume with: ultrac run --continue ${session.id}`));
16369
+ console.log(chalk8.yellow(stopped));
16370
+ console.log(chalk8.gray(`Resume with: ultrac run --continue ${session.id}`));
15879
16371
  }
15880
- console.log(chalk7.gray(`Session saved: ${sessionPath2}`));
16372
+ console.log(chalk8.gray(`Session saved: ${sessionPath2}`));
15881
16373
  }
15882
16374
  function printTools(tools) {
15883
16375
  for (const tool of tools) {
15884
- const permissionText = tool.permissions.length ? chalk7.yellow(` [${tool.permissions.join(", ")}]`) : "";
15885
- const optionalText = tool.optionalPermissions.length ? chalk7.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
15886
- console.log(`${chalk7.cyan(tool.name)}${permissionText}${optionalText}`);
15887
- console.log(chalk7.gray(indent2(tool.description)));
16376
+ const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
16377
+ const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
16378
+ console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
16379
+ console.log(chalk8.gray(indent2(tool.description)));
15888
16380
  }
15889
16381
  }
15890
16382
  function printToolResult(result) {
15891
- const status = result.ok ? chalk7.green("ok") : chalk7.red("failed");
15892
- console.log(`${chalk7.cyan(result.tool)} ${status}`);
15893
- console.log(chalk7.gray(indent2(result.content)));
16383
+ 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)));
15894
16386
  }
15895
16387
  function indent2(value) {
15896
16388
  return value.split("\n").map((line) => ` ${line}`).join("\n");
@@ -16096,7 +16588,7 @@ function formatSkillCommandHelp(index) {
16096
16588
 
16097
16589
  // src/ui/progress.ts
16098
16590
  import { stdout as stdout3 } from "process";
16099
- import chalk8 from "chalk";
16591
+ import chalk9 from "chalk";
16100
16592
  var PENDING_GLYPH = "\u25CB";
16101
16593
  var DONE_GLYPH = "\u25CF";
16102
16594
  var REPAINT_MS = 100;
@@ -16123,12 +16615,12 @@ function formatElapsed(ms) {
16123
16615
  }
16124
16616
  function formatProgressLine(snapshot) {
16125
16617
  const finished = snapshot.done >= snapshot.total;
16126
- const glyph = finished ? chalk8.green(DONE_GLYPH) : chalk8.cyan(PENDING_GLYPH);
16618
+ const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
16127
16619
  const noun = snapshot.total === 1 ? "agent" : "agents";
16128
16620
  const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
16129
16621
  if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
16130
- const rendered = stats.map((stat11) => chalk8.dim(stat11)).join(chalk8.dim(" \xB7 "));
16131
- return ` ${glyph} ${chalk8.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
16622
+ const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
16623
+ return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
16132
16624
  }
16133
16625
  var ProgressLine = class {
16134
16626
  constructor(label, description, total) {
@@ -16362,7 +16854,7 @@ function humanize(value) {
16362
16854
  }
16363
16855
 
16364
16856
  // src/ui/welcome.ts
16365
- import chalk9, { Chalk } from "chalk";
16857
+ import chalk10, { Chalk } from "chalk";
16366
16858
  var LOGO = [
16367
16859
  "\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
16368
16860
  "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
@@ -16373,7 +16865,7 @@ var LOGO = [
16373
16865
  ];
16374
16866
  function renderWelcome(info, options = {}) {
16375
16867
  const columns = normalizeColumns(options.columns);
16376
- const paint = new Chalk({ level: options.color === false ? 0 : chalk9.level });
16868
+ const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
16377
16869
  const cardWidth = Math.min(88, Math.max(42, columns));
16378
16870
  const innerWidth = cardWidth - 4;
16379
16871
  const wide = columns >= 64;
@@ -16403,7 +16895,7 @@ function renderWelcome(info, options = {}) {
16403
16895
  return lines.join("\n");
16404
16896
  }
16405
16897
  function renderComposer(columns = 80, color = true) {
16406
- const paint = new Chalk({ level: color ? chalk9.level : 0 });
16898
+ const paint = new Chalk({ level: color ? chalk10.level : 0 });
16407
16899
  const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
16408
16900
  const label = " message ";
16409
16901
  const fill = Math.max(1, width - label.length - 2);
@@ -16436,14 +16928,14 @@ function shortenMiddle(value, width) {
16436
16928
  // src/update/check.ts
16437
16929
  import { spawn as spawn4 } from "child_process";
16438
16930
  import { randomBytes as randomBytes4 } from "crypto";
16439
- import { readFile as readFile12, rename as rename2, unlink as unlink3, writeFile as writeFile11 } from "fs/promises";
16440
- import { join as join12 } from "path";
16931
+ import { readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
16932
+ import { join as join13 } from "path";
16441
16933
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
16442
16934
  var QUERY_TIMEOUT_MS = 5e3;
16443
16935
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
16444
16936
  async function checkForUpdate(options) {
16445
16937
  const now = options.now ?? Date.now();
16446
- const cachePath = options.cachePath ?? join12(await ensureUltracHome(), "update-check.json");
16938
+ const cachePath = options.cachePath ?? join13(await ensureUltracHome(), "update-check.json");
16447
16939
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
16448
16940
  let latestVersion;
16449
16941
  if (!options.force) {
@@ -16511,7 +17003,7 @@ function parseVersion(version) {
16511
17003
  }
16512
17004
  async function readCache(path) {
16513
17005
  try {
16514
- const raw = await readFile12(path, "utf8");
17006
+ const raw = await readFile13(path, "utf8");
16515
17007
  if (raw.length > 1024) return void 0;
16516
17008
  const parsed = JSON.parse(raw);
16517
17009
  if (typeof parsed !== "object" || parsed === null || !("checkedAt" in parsed) || typeof parsed.checkedAt !== "number" || !("latestVersion" in parsed) || parsed.latestVersion !== null && typeof parsed.latestVersion !== "string") {
@@ -16525,7 +17017,7 @@ async function readCache(path) {
16525
17017
  async function writeCache(path, cache) {
16526
17018
  const temporary = `${path}.${randomBytes4(8).toString("hex")}.tmp`;
16527
17019
  try {
16528
- await writeFile11(temporary, `${JSON.stringify(cache)}
17020
+ await writeFile12(temporary, `${JSON.stringify(cache)}
16529
17021
  `, { encoding: "utf8", mode: 384, flag: "wx" });
16530
17022
  await rename2(temporary, path);
16531
17023
  } catch {
@@ -16533,13 +17025,13 @@ async function writeCache(path, cache) {
16533
17025
  }
16534
17026
  }
16535
17027
  async function runNpm(args, options) {
16536
- const env = { ...process.env };
16537
- for (const key of Object.keys(env)) {
16538
- if (key.startsWith("ULTRAC_")) delete env[key];
17028
+ const env2 = { ...process.env };
17029
+ for (const key of Object.keys(env2)) {
17030
+ if (key.startsWith("ULTRAC_")) delete env2[key];
16539
17031
  }
16540
17032
  return new Promise((resolve15) => {
16541
17033
  const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
16542
- env,
17034
+ env: env2,
16543
17035
  stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
16544
17036
  windowsHide: true
16545
17037
  });
@@ -16659,14 +17151,14 @@ async function runRepl(options, initialTask = "") {
16659
17151
  if (loaded.needsApproval && await offerWorkspaceTrust(workspace, loaded.withheld, loaded.trustSnapshot, loaded.trustStatus)) {
16660
17152
  loaded = await loadConfigWithTrust(workspace);
16661
17153
  if (loaded.needsApproval) {
16662
- console.log(chalk10.yellow(`
17154
+ console.log(chalk11.yellow(`
16663
17155
  ${withheldNotice(workspace, loaded.withheld, loaded.trustStatus)}
16664
17156
  `));
16665
17157
  }
16666
17158
  }
16667
17159
  const config = loaded.config;
16668
17160
  const toolSelection = applyBudgetFlags(options, config);
16669
- setContextBudgetNotice((message) => console.log(chalk10.yellow(`
17161
+ setContextBudgetNotice((message) => console.log(chalk11.yellow(`
16670
17162
  ! ${message}`)));
16671
17163
  const providerName = options.provider ?? config.provider;
16672
17164
  if (!isProviderName(providerName)) {
@@ -16682,8 +17174,8 @@ async function runRepl(options, initialTask = "") {
16682
17174
  }
16683
17175
  const hasFallbackChain = Boolean(config.fallback?.chain?.length);
16684
17176
  if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
16685
- console.log(chalk10.yellow("You are not signed in to Ultrac."));
16686
- console.log(`Run ${chalk10.cyan("ultrac login")} first, or start with ${chalk10.cyan("--provider echo")}.`);
17177
+ 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")}.`);
16687
17179
  process.exitCode = EXIT_REPL_NOT_STARTED;
16688
17180
  return;
16689
17181
  }
@@ -16695,26 +17187,26 @@ async function runRepl(options, initialTask = "") {
16695
17187
  model,
16696
17188
  fallback: config.fallback,
16697
17189
  onFailover: (event) => {
16698
- console.log(chalk10.yellow(formatFailoverNotice(event)));
17190
+ console.log(chalk11.yellow(formatFailoverNotice(event)));
16699
17191
  if (event.to) {
16700
- console.log(chalk10.gray(` now answering: provider ${event.to.provider} \xB7 model ${event.to.model}`));
17192
+ console.log(chalk11.gray(` now answering: provider ${event.to.provider} \xB7 model ${event.to.model}`));
16701
17193
  }
16702
17194
  },
16703
17195
  // A tool-calling turn served by an entry without native tool calling still
16704
17196
  // answers, but over the JSON tool protocol. Say so rather than letting the
16705
17197
  // banner's "native tools" claim go stale.
16706
- onDowngrade: (event) => console.log(chalk10.yellow(formatDowngradeNotice(event))),
17198
+ onDowngrade: (event) => console.log(chalk11.yellow(formatDowngradeNotice(event))),
16707
17199
  onRetry: (event) => {
16708
17200
  diagnosticEvent("provider.retry", event);
16709
17201
  console.log(
16710
- chalk10.yellow(
17202
+ chalk11.yellow(
16711
17203
  ` retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
16712
17204
  )
16713
17205
  );
16714
17206
  }
16715
17207
  });
16716
17208
  } catch (error) {
16717
- console.log(chalk10.red(error instanceof Error ? error.message : String(error)));
17209
+ console.log(chalk11.red(error instanceof Error ? error.message : String(error)));
16718
17210
  process.exitCode = EXIT_REPL_NOT_STARTED;
16719
17211
  return;
16720
17212
  }
@@ -16746,7 +17238,7 @@ async function runRepl(options, initialTask = "") {
16746
17238
  permissions: basePermissions
16747
17239
  });
16748
17240
  const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
16749
- console.error(chalk10.yellow(message));
17241
+ console.error(chalk11.yellow(message));
16750
17242
  });
16751
17243
  const persistSession = async (target = session) => {
16752
17244
  const snapshot = target.toSession();
@@ -16764,7 +17256,7 @@ async function runRepl(options, initialTask = "") {
16764
17256
  diagnosticPath2
16765
17257
  );
16766
17258
  if (mcpRuntime && mcpRuntime.statuses.length > 0) {
16767
- console.log(chalk10.bold("\nMCP servers"));
17259
+ console.log(chalk11.bold("\nMCP servers"));
16768
17260
  console.log(renderMcpStatuses(mcpRuntime.statuses));
16769
17261
  }
16770
17262
  try {
@@ -16774,9 +17266,9 @@ async function runRepl(options, initialTask = "") {
16774
17266
  }
16775
17267
  const update = await updateCheck;
16776
17268
  if (update?.updateAvailable && update.latestVersion) {
16777
- console.log(chalk10.yellow(`
17269
+ console.log(chalk11.yellow(`
16778
17270
  \u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
16779
- console.log(chalk10.gray(" Run `ultrac update` after this session."));
17271
+ console.log(chalk11.gray(" Run `ultrac update` after this session."));
16780
17272
  }
16781
17273
  const rl = createInterface3({ input: stdin5, output: stdout5 });
16782
17274
  const reader = new LineReader(rl);
@@ -16786,13 +17278,13 @@ async function runRepl(options, initialTask = "") {
16786
17278
  };
16787
17279
  const drainQueuedMessages = () => reader.drainQueuedMessages();
16788
17280
  setAskUserRuntime(async (text, options2, signal) => {
16789
- console.log(chalk10.cyan(`
17281
+ console.log(chalk11.cyan(`
16790
17282
  ? ${text}`));
16791
17283
  if (options2?.length) {
16792
- options2.forEach((option, index) => console.log(chalk10.gray(` ${index + 1}. ${option}`)));
16793
- console.log(chalk10.gray(" (type a number, or answer in your own words)"));
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)"));
16794
17286
  }
16795
- const rawAnswer = await question(chalk10.cyan(" \u203A "), signal);
17287
+ const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
16796
17288
  if (signal?.aborted) return null;
16797
17289
  const answer = (rawAnswer ?? "").trim();
16798
17290
  const picked = Number.parseInt(answer, 10);
@@ -16806,7 +17298,7 @@ async function runRepl(options, initialTask = "") {
16806
17298
  rl.on("SIGINT", () => {
16807
17299
  if (handleReplInterrupt(turnAbort) === "abort-turn") return;
16808
17300
  running = false;
16809
- console.log(chalk10.gray("\nExiting ultrac\u2026"));
17301
+ console.log(chalk11.gray("\nExiting ultrac\u2026"));
16810
17302
  rl.close();
16811
17303
  });
16812
17304
  const terminate = () => {
@@ -16825,7 +17317,7 @@ async function runRepl(options, initialTask = "") {
16825
17317
  };
16826
17318
  try {
16827
17319
  if (initialTask) {
16828
- console.log(chalk10.cyan(`
17320
+ console.log(chalk11.cyan(`
16829
17321
  \u203A ${initialTask}`));
16830
17322
  await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
16831
17323
  }
@@ -16835,7 +17327,7 @@ async function runRepl(options, initialTask = "") {
16835
17327
  console.log(composer.frame);
16836
17328
  stdout5.write(composer.prompt);
16837
17329
  } else {
16838
- stdout5.write(chalk10.cyan("\n\u203A "));
17330
+ stdout5.write(chalk11.cyan("\n\u203A "));
16839
17331
  }
16840
17332
  const line = await reader.next();
16841
17333
  if (line === null) break;
@@ -16852,7 +17344,7 @@ async function runRepl(options, initialTask = "") {
16852
17344
  permissions: session.currentPermissions
16853
17345
  });
16854
17346
  resetContextBudgetNotices();
16855
- console.log(chalk10.gray("Started a fresh conversation."));
17347
+ console.log(chalk11.gray("Started a fresh conversation."));
16856
17348
  }, async (prompt) => {
16857
17349
  await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
16858
17350
  }, signal, async (id) => {
@@ -16892,18 +17384,23 @@ async function runRepl(options, initialTask = "") {
16892
17384
  await saveTranscript(session, writeCheckpoint);
16893
17385
  mcpRuntime?.close();
16894
17386
  }
16895
- console.log(chalk10.gray("Goodbye."));
17387
+ console.log(chalk11.gray("Goodbye."));
16896
17388
  }
16897
- async function runTurn(session, input, question, signal, drainQueuedMessages) {
17389
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist) {
16898
17390
  const spinner = new Spinner();
16899
17391
  const detachProgress = attachProgressUi();
16900
17392
  let deltaChars = 0;
16901
17393
  try {
16902
17394
  await session.ask(input, {
16903
17395
  ...signal ? { signal } : {},
17396
+ ...persist ? {
17397
+ onCheckpoint: async () => {
17398
+ await persist(session);
17399
+ }
17400
+ } : {},
16904
17401
  onInterrupted: () => {
16905
17402
  spinner.stop();
16906
- console.log(chalk10.dim("\n Interrupted \u2014 current work stopped. Session preserved."));
17403
+ console.log(chalk11.dim("\n Interrupted \u2014 current work stopped. Session preserved."));
16907
17404
  },
16908
17405
  onModelStart: () => {
16909
17406
  deltaChars = 0;
@@ -16921,41 +17418,41 @@ ${message.trim()}`);
16921
17418
  }
16922
17419
  },
16923
17420
  onToolStart: (call) => {
16924
- console.log(chalk10.dim(` ${toolActivityLabel(call)}`));
17421
+ console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
16925
17422
  },
16926
17423
  onToolResult: (_call, result) => printToolResult2(result),
16927
17424
  ...drainQueuedMessages ? {
16928
17425
  drainQueuedMessages,
16929
17426
  onQueuedMessage: () => {
16930
17427
  spinner.stop();
16931
- console.log(chalk10.dim(" Queued message added to the current turn."));
17428
+ console.log(chalk11.dim(" Queued message added to the current turn."));
16932
17429
  }
16933
17430
  } : {},
16934
17431
  onToolDenied: (call, permission) => {
16935
- console.log(chalk10.yellow(` \u2717 denied ${permission} for ${call.tool}`));
17432
+ console.log(chalk11.yellow(` \u2717 denied ${permission} for ${call.tool}`));
16936
17433
  },
16937
17434
  onProviderNotice: (notice) => {
16938
- console.log(chalk10.yellow(` ! ${notice}`));
17435
+ console.log(chalk11.yellow(` ! ${notice}`));
16939
17436
  },
16940
17437
  onStopped: (reason) => {
16941
17438
  if (reason === "aborted") return;
16942
17439
  const line = stopReasonLine(reason);
16943
17440
  const hint = turnStopHint(reason, session.maxSteps);
16944
17441
  if (!line || !hint) return;
16945
- console.log(chalk10.yellow(`
17442
+ console.log(chalk11.yellow(`
16946
17443
  \u26A0 ${line}`));
16947
- console.log(chalk10.gray(` ${hint}`));
17444
+ console.log(chalk11.gray(` ${hint}`));
16948
17445
  },
16949
17446
  requestPermission: async (call, permission) => {
16950
17447
  const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access. [o]nce (this call only) / [a]lways (all shell tools for this session) / [n]o (this call) / n[e]ver (this session) \u203A ` : ` ${call.tool} wants ${permission} access. [o]nce / [a]lways / [n]o / n[e]ver \u203A `;
16951
- const rawAnswer = await question(chalk10.yellow(prompt), signal);
17448
+ const rawAnswer = await question(chalk11.yellow(prompt), signal);
16952
17449
  if (signal?.aborted || rawAnswer === null) return "deny";
16953
17450
  const answer = rawAnswer.trim().toLowerCase();
16954
17451
  if (answer === "a" || answer === "always") return "always";
16955
17452
  if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
16956
17453
  if (answer === "e" || answer === "never") {
16957
17454
  console.log(
16958
- chalk10.gray(
17455
+ chalk11.gray(
16959
17456
  ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
16960
17457
  )
16961
17458
  );
@@ -16975,7 +17472,7 @@ ${message.trim()}`);
16975
17472
  }
16976
17473
  async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
16977
17474
  try {
16978
- return await runTurn(session, input, question, signal, drainQueuedMessages);
17475
+ return await runTurn(session, input, question, signal, drainQueuedMessages, persist);
16979
17476
  } finally {
16980
17477
  await persist(session);
16981
17478
  }
@@ -16992,27 +17489,27 @@ function turnStopHint(reason, maxSteps) {
16992
17489
  }
16993
17490
  function printTurnError(error) {
16994
17491
  if (error instanceof ProviderChainError) {
16995
- console.log(chalk10.red("\n \u2717 Turn failed: no provider in the chain could answer."));
17492
+ console.log(chalk11.red("\n \u2717 Turn failed: no provider in the chain could answer."));
16996
17493
  for (const failure of error.failures) {
16997
17494
  const attempts = failure.attempts === 1 ? "1 attempt" : `${failure.attempts} attempts`;
16998
17495
  console.log(
16999
- chalk10.red(` - ${failure.provider} (${failure.model}), ${attempts}: ${errorMessage2(failure.error)}`)
17496
+ chalk11.red(` - ${failure.provider} (${failure.model}), ${attempts}: ${errorMessage2(failure.error)}`)
17000
17497
  );
17001
17498
  }
17002
17499
  console.log(
17003
- chalk10.gray(" Fix the provider (ultrac login, or --provider echo) and try again; the session is still open.")
17500
+ chalk11.gray(" Fix the provider (ultrac login, or --provider echo) and try again; the session is still open.")
17004
17501
  );
17005
17502
  return;
17006
17503
  }
17007
- console.log(chalk10.red(`
17504
+ console.log(chalk11.red(`
17008
17505
  \u2717 Turn failed: ${errorMessage2(error)}`));
17009
- console.log(chalk10.gray(" The session is still open. Try again, or /exit to end it."));
17506
+ console.log(chalk11.gray(" The session is still open. Try again, or /exit to end it."));
17010
17507
  }
17011
17508
  async function saveTranscript(session, writeCheckpoint) {
17012
17509
  const snapshot = session.toSession();
17013
17510
  if (!hasSavedSessionContent(snapshot)) return;
17014
17511
  const stored = await writeCheckpoint(snapshot);
17015
- if (stored) console.log(chalk10.gray(`
17512
+ if (stored) console.log(chalk11.gray(`
17016
17513
  Session saved: ${stored}`));
17017
17514
  }
17018
17515
  function hasSavedSessionContent(session) {
@@ -17054,7 +17551,7 @@ async function handleSlashCommand(raw, session, providerName, workspace, onClear
17054
17551
  if (extra) console.log(`
17055
17552
  ${extra}`);
17056
17553
  for (const conflict of index.conflicts) {
17057
- console.log(chalk10.yellow(` ! ${conflict.message}`));
17554
+ console.log(chalk11.yellow(` ! ${conflict.message}`));
17058
17555
  }
17059
17556
  return "ok";
17060
17557
  }
@@ -17068,7 +17565,7 @@ ${extra}`);
17068
17565
  case "model":
17069
17566
  if (argument) {
17070
17567
  session.setModel(argument);
17071
- console.log(chalk10.gray(`Model set to ${argument}.`));
17568
+ console.log(chalk11.gray(`Model set to ${argument}.`));
17072
17569
  } else {
17073
17570
  console.log(`Model: ${session.model}`);
17074
17571
  }
@@ -17076,10 +17573,10 @@ ${extra}`);
17076
17573
  case "mode":
17077
17574
  if (argument) {
17078
17575
  if (!isAgentModeName(argument)) {
17079
- console.log(chalk10.red(`Unknown mode. Expected one of: ${agentModeNames.join(", ")}.`));
17576
+ console.log(chalk11.red(`Unknown mode. Expected one of: ${agentModeNames.join(", ")}.`));
17080
17577
  } else {
17081
17578
  session.setMode(argument);
17082
- console.log(chalk10.gray(`Mode set to ${argument}.`));
17579
+ console.log(chalk11.gray(`Mode set to ${argument}.`));
17083
17580
  }
17084
17581
  } else {
17085
17582
  console.log(`Mode: ${session.mode}`);
@@ -17128,7 +17625,7 @@ ${extra}`);
17128
17625
  console.log(
17129
17626
  `Selected ${info.selectedMessages.toLocaleString("en-US")} of ${info.messages.toLocaleString("en-US")} messages; would omit ${info.omittedMessages.toLocaleString("en-US")} messages in ${info.omittedTurns.toLocaleString("en-US")} earlier turns. Prompt headroom ${info.promptRemainingTokens.toLocaleString("en-US")} tokens; window headroom ${info.windowRemainingTokens.toLocaleString("en-US")} of ${info.contextTokens.toLocaleString("en-US")} tokens.`
17130
17627
  );
17131
- if (info.error) console.log(chalk10.yellow(`Context error: ${info.error}`));
17628
+ if (info.error) console.log(chalk11.yellow(`Context error: ${info.error}`));
17132
17629
  return "ok";
17133
17630
  }
17134
17631
  case "compact":
@@ -17136,7 +17633,7 @@ ${extra}`);
17136
17633
  await session.compactContext(argument, signal);
17137
17634
  console.log("Context compacted. User instructions were retained; earlier activity was replaced by a bounded continuation record.");
17138
17635
  } catch (error) {
17139
- console.log(chalk10.red(errorMessage2(error)));
17636
+ console.log(chalk11.red(errorMessage2(error)));
17140
17637
  }
17141
17638
  return "ok";
17142
17639
  case "resume":
@@ -17147,7 +17644,7 @@ ${extra}`);
17147
17644
  try {
17148
17645
  await resume(argument);
17149
17646
  } catch (error) {
17150
- console.log(chalk10.red(errorMessage2(error)));
17647
+ console.log(chalk11.red(errorMessage2(error)));
17151
17648
  }
17152
17649
  return "ok";
17153
17650
  case "save": {
@@ -17198,7 +17695,7 @@ ${extra}`);
17198
17695
  const refused = session.refusedPermissions;
17199
17696
  if (refused.length) {
17200
17697
  console.log(
17201
- chalk10.gray(`refused for this session: ${refused.join(", ")} (undo with /allow <perm>)`)
17698
+ chalk11.gray(`refused for this session: ${refused.join(", ")} (undo with /allow <perm>)`)
17202
17699
  );
17203
17700
  }
17204
17701
  return "ok";
@@ -17207,9 +17704,9 @@ ${extra}`);
17207
17704
  case "allow": {
17208
17705
  if (argument === "write" || argument === "shell" || argument === "network") {
17209
17706
  session.setPermission(argument, command === "allow");
17210
- console.log(chalk10.gray(`${command === "allow" ? "Granted" : "Revoked"} ${argument} for this session.`));
17707
+ console.log(chalk11.gray(`${command === "allow" ? "Granted" : "Revoked"} ${argument} for this session.`));
17211
17708
  } else {
17212
- console.log(chalk10.red(`Usage: /${command} <write|shell|network>`));
17709
+ console.log(chalk11.red(`Usage: /${command} <write|shell|network>`));
17213
17710
  }
17214
17711
  return "ok";
17215
17712
  }
@@ -17228,7 +17725,7 @@ ${extra}`);
17228
17725
  const names = session.advertisedToolNames;
17229
17726
  console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
17230
17727
  console.log(
17231
- chalk10.gray(
17728
+ chalk11.gray(
17232
17729
  "Registered tools remain subject to permission and mode checks. `ultrac tools` lists them all with schemas; --tools all advertises them all."
17233
17730
  )
17234
17731
  );
@@ -17237,7 +17734,7 @@ ${extra}`);
17237
17734
  case "memory": {
17238
17735
  const memories = await listMemories(workspace);
17239
17736
  if (!memories.length) {
17240
- console.log(chalk10.gray("No stored memories. Add one with `ultrac memory add`."));
17737
+ console.log(chalk11.gray("No stored memories. Add one with `ultrac memory add`."));
17241
17738
  } else {
17242
17739
  for (const memory of memories) {
17243
17740
  console.log(`- [${memory.scope}/${memory.name}] (${memory.type}) ${memory.description}`);
@@ -17248,7 +17745,7 @@ ${extra}`);
17248
17745
  case "skills": {
17249
17746
  const skills = await listSkills(workspace);
17250
17747
  if (!skills.length) {
17251
- console.log(chalk10.gray("No skills installed. Create one with `ultrac skills create`."));
17748
+ console.log(chalk11.gray("No skills installed. Create one with `ultrac skills create`."));
17252
17749
  } else {
17253
17750
  for (const skill of skills) {
17254
17751
  console.log(`- ${skill.name} (${skill.scope}): ${skill.description}`);
@@ -17264,12 +17761,12 @@ ${extra}`);
17264
17761
  force: true
17265
17762
  });
17266
17763
  if (!update.latestVersion) {
17267
- console.log(chalk10.yellow("Could not check npm. Confirm npm access, then run `ultrac update --check` outside this session."));
17764
+ console.log(chalk11.yellow("Could not check npm. Confirm npm access, then run `ultrac update --check` outside this session."));
17268
17765
  } else if (update.updateAvailable) {
17269
- console.log(chalk10.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
17270
- console.log(chalk10.gray("Exit this session, then run `ultrac update`."));
17766
+ console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
17767
+ console.log(chalk11.gray("Exit this session, then run `ultrac update`."));
17271
17768
  } else {
17272
- console.log(chalk10.green(`ultrac ${update.currentVersion} is up to date.`));
17769
+ console.log(chalk11.green(`ultrac ${update.currentVersion} is up to date.`));
17273
17770
  }
17274
17771
  return "ok";
17275
17772
  }
@@ -17281,27 +17778,27 @@ async function runSkillCommand(session, raw, workspace, question) {
17281
17778
  const resolved = await resolveSkillCommand(workspace, raw);
17282
17779
  if (resolved.status === "ok") {
17283
17780
  console.log(
17284
- chalk10.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
17781
+ chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
17285
17782
  );
17286
17783
  await runTurn(session, resolved.prompt, question);
17287
17784
  return;
17288
17785
  }
17289
17786
  if (resolved.status === "conflict") {
17290
- console.log(chalk10.red(resolved.message));
17787
+ console.log(chalk11.red(resolved.message));
17291
17788
  return;
17292
17789
  }
17293
17790
  if (resolved.status === "builtin") {
17294
- console.log(chalk10.red(`/${resolved.command} is a built-in command and was not handled. Try /help.`));
17791
+ console.log(chalk11.red(`/${resolved.command} is a built-in command and was not handled. Try /help.`));
17295
17792
  return;
17296
17793
  }
17297
17794
  if (resolved.status === "unknown") {
17298
- console.log(chalk10.red(`Unknown command: /${resolved.command}. Try /help.`));
17795
+ console.log(chalk11.red(`Unknown command: /${resolved.command}. Try /help.`));
17299
17796
  if (resolved.available.length) {
17300
- console.log(chalk10.gray(`Skill commands: ${resolved.available.map((name) => `/${name}`).join(", ")}`));
17797
+ console.log(chalk11.gray(`Skill commands: ${resolved.available.map((name) => `/${name}`).join(", ")}`));
17301
17798
  }
17302
17799
  return;
17303
17800
  }
17304
- console.log(chalk10.red("Unknown command. Try /help."));
17801
+ console.log(chalk11.red("Unknown command. Try /help."));
17305
17802
  }
17306
17803
  var LineReader = class {
17307
17804
  queue = [];
@@ -17416,9 +17913,9 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
17416
17913
  }
17417
17914
  function printToolResult2(result) {
17418
17915
  const failure = toolFailurePreview(result);
17419
- if (failure) console.log(chalk10.red(` Failed: ${failure}`));
17916
+ if (failure) console.log(chalk11.red(` Failed: ${failure}`));
17420
17917
  for (const warning of getMcpArchiveWarnings(result)) {
17421
- console.log(chalk10.yellow(indent3(`Warning: ${warning}`)));
17918
+ console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
17422
17919
  }
17423
17920
  }
17424
17921
  function shellApprovalPrompt(call, workspace) {
@@ -17445,31 +17942,68 @@ function indent3(value) {
17445
17942
  }
17446
17943
 
17447
17944
  // src/commands/run.ts
17448
- import chalk11 from "chalk";
17945
+ import { basename as basename3 } from "path";
17946
+ 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
+ function parsePositiveInt(value, flag) {
17971
+ const parsed = Number.parseInt(value, 10);
17972
+ if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
17973
+ throw new Error(`${flag} must be a positive integer.`);
17974
+ }
17975
+ return parsed;
17976
+ }
17449
17977
  var EXIT_INCOMPLETE_RUN = 2;
17978
+ var AUTONOMOUS_ERROR_BACKOFF_MS = 15e3;
17979
+ var NOTIFY_SUMMARY_CHARS = 1200;
17450
17980
  function registerRunCommand(program2) {
17451
17981
  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(
17452
17982
  "--allow-incomplete",
17453
17983
  `Exit 0 even when the run stops without finishing. By default an unfinished run (step cap, abort, error) exits ${EXIT_INCOMPLETE_RUN} so a script or a \`&&\` chain can tell a partial answer from a real one; pass this when hitting --max-steps on a long task is an acceptable outcome. The stop reason is still printed either way.`,
17454
17984
  false
17455
- ).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
17985
+ ).option(
17986
+ "--autonomous",
17987
+ `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
+ 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) => {
17456
17990
  const task = taskParts.join(" ").trim();
17457
17991
  if (!task) {
17458
17992
  throw new Error("A task is required.");
17459
17993
  }
17460
17994
  const workspace = await assertWorkspaceExists(options.workspace);
17461
17995
  const diagnosticPath2 = configureDiagnosticLog(workspace, "run");
17462
- console.error(chalk11.gray(`Diagnostic log: ${diagnosticPath2}`));
17996
+ console.error(chalk12.gray(`Diagnostic log: ${diagnosticPath2}`));
17463
17997
  if (options.baseUrl) {
17464
17998
  setBaseUrlFlag(options.baseUrl);
17465
17999
  }
17466
18000
  const loaded = await loadConfigWithTrust(workspace);
17467
18001
  const config = loaded.config;
17468
18002
  if (loaded.needsApproval) {
17469
- console.error(chalk11.yellow(withheldNotice(workspace, loaded.withheld, loaded.trustStatus)));
18003
+ console.error(chalk12.yellow(withheldNotice(workspace, loaded.withheld, loaded.trustStatus)));
17470
18004
  }
17471
18005
  const toolSelection = applyBudgetFlags(options, config);
17472
- setContextBudgetNotice((message) => console.error(chalk11.yellow(message)));
18006
+ setContextBudgetNotice((message) => console.error(chalk12.yellow(message)));
17473
18007
  const providerName = options.provider ?? config.provider;
17474
18008
  const mode = options.mode ?? config.mode;
17475
18009
  const previousSessionLineage = options.continue ? await loadSessionLineage(workspace, options.continue) : void 0;
@@ -17480,24 +18014,49 @@ function registerRunCommand(program2) {
17480
18014
  if (!isAgentModeName(mode)) {
17481
18015
  throw new Error(`Unknown mode: ${mode}. Expected one of: ${agentModeNames.join(", ")}.`);
17482
18016
  }
17483
- const maxSteps = Number.parseInt(options.maxSteps ?? String(config.maxSteps), 10);
18017
+ const autonomous = options.autonomous;
18018
+ const maxSteps = Number.parseInt(
18019
+ options.maxSteps ?? String(autonomous ? AUTONOMOUS_MAX_STEPS : config.maxSteps),
18020
+ 10
18021
+ );
17484
18022
  if (!Number.isInteger(maxSteps) || maxSteps < 1) {
17485
18023
  throw new Error("--max-steps must be a positive integer.");
17486
18024
  }
18025
+ if (options.maxRounds !== void 0 && !autonomous) {
18026
+ throw new Error("--max-rounds only applies with --autonomous.");
18027
+ }
18028
+ const maxRounds = autonomous ? parsePositiveInt(options.maxRounds ?? String(DEFAULT_AUTONOMOUS_MAX_ROUNDS), "--max-rounds") : 1;
18029
+ const notifyConfig = options.notify || autonomous ? await loadNotifyConfig() : null;
18030
+ if (options.notify && !notifyConfig) {
18031
+ throw new Error("--notify needs a phone channel. Run `ultrac notify setup` first.");
18032
+ }
18033
+ const notifier = notifyConfig ? createNotifier(notifyConfig, `Ultrac (${basename3(workspace)})`) : void 0;
18034
+ if (autonomous) {
18035
+ console.error(
18036
+ chalk12.yellow(
18037
+ `Autonomous run: file writes, unsandboxed shell and network are granted with no approvals, up to ${maxRounds} round${maxRounds === 1 ? "" : "s"} of ${maxSteps} steps.`
18038
+ )
18039
+ );
18040
+ console.error(
18041
+ chalk12.gray(
18042
+ notifier ? `Questions and the result go to your phone via ${notifier.describe()}.` : "No phone channel is configured, so the agent decides on its own. Run `ultrac notify setup` to be reached when it needs you."
18043
+ )
18044
+ );
18045
+ }
17487
18046
  const model = options.model ?? (options.provider ? providerDefaultModels[providerName] : config.model);
17488
18047
  const provider = createFallbackProvider({
17489
18048
  provider: providerName,
17490
18049
  model,
17491
18050
  fallback: config.fallback,
17492
18051
  // stderr, so --json keeps stdout parseable.
17493
- onFailover: (event) => console.error(chalk11.yellow(formatFailoverNotice(event))),
18052
+ onFailover: (event) => console.error(chalk12.yellow(formatFailoverNotice(event))),
17494
18053
  // AgentRunner uses native tool calls when supported; wired
17495
18054
  // anyway so a non-interactive run stays honest if that ever changes.
17496
- onDowngrade: (event) => console.error(chalk11.yellow(formatDowngradeNotice(event))),
18055
+ onDowngrade: (event) => console.error(chalk12.yellow(formatDowngradeNotice(event))),
17497
18056
  onRetry: (event) => {
17498
18057
  diagnosticEvent("provider.retry", event);
17499
18058
  console.error(
17500
- chalk11.yellow(
18059
+ chalk12.yellow(
17501
18060
  `Retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
17502
18061
  )
17503
18062
  );
@@ -17506,47 +18065,96 @@ function registerRunCommand(program2) {
17506
18065
  const registry = createDefaultToolRegistry();
17507
18066
  const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
17508
18067
  setSubagentRuntime({ provider, model, mode, registry });
18068
+ if (notifier) {
18069
+ setAskUserRuntime(
18070
+ async (question, choices, signal) => {
18071
+ console.error(chalk12.cyan(`Asking you on ${notifier.describe()}: ${question}`));
18072
+ try {
18073
+ const answer = await notifier.ask(question, choices, signal);
18074
+ console.error(chalk12.gray(answer === null ? "No reply; the agent will proceed on its own judgement." : `Reply: ${answer}`));
18075
+ return answer;
18076
+ } catch (error) {
18077
+ console.error(chalk12.yellow(`Could not reach your phone: ${error.message}`));
18078
+ return null;
18079
+ }
18080
+ },
18081
+ { remote: true }
18082
+ );
18083
+ }
17509
18084
  try {
17510
18085
  if (mcpRuntime && mcpRuntime.statuses.length > 0) {
17511
- console.error(chalk11.bold("MCP servers"));
18086
+ console.error(chalk12.bold("MCP servers"));
17512
18087
  console.error(renderMcpStatuses(mcpRuntime.statuses));
17513
18088
  }
17514
18089
  const detachProgress = attachProgressUi();
17515
18090
  const runner = new AgentRunner(registry);
17516
18091
  const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
17517
- console.error(chalk11.yellow(message));
18092
+ console.error(chalk12.yellow(message));
17518
18093
  });
17519
18094
  const controller = new AbortController();
17520
18095
  const abortRun = () => controller.abort();
17521
18096
  process.on("SIGINT", abortRun);
17522
18097
  process.on("SIGTERM", abortRun);
18098
+ const permissions = autonomous ? { write: true, shell: true, network: true } : {
18099
+ write: options.allowWrite || config.permissions.write,
18100
+ shell: options.allowShell || config.permissions.shell,
18101
+ network: options.allowNetwork || config.permissions.network
18102
+ };
17523
18103
  let result;
17524
18104
  try {
17525
- result = await runner.run({
17526
- task,
17527
- workspace,
17528
- provider,
17529
- model,
17530
- mode,
17531
- nativeTools: options.nativeTools,
17532
- maxSteps,
17533
- previousSession,
17534
- previousSessionLineage,
17535
- ...toolSelection ? { tools: toolSelection } : {},
17536
- permissions: {
17537
- write: options.allowWrite || config.permissions.write,
17538
- shell: options.allowShell || config.permissions.shell,
17539
- network: options.allowNetwork || config.permissions.network
17540
- },
18105
+ result = await runAutonomousLoop({
18106
+ maxRounds,
17541
18107
  signal: controller.signal,
17542
- // Write the session after every step, atomically, so a run that dies
17543
- // partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
17544
- // its last completed step and can be resumed with --continue rather than
17545
- // restarted from nothing.
17546
- onCheckpoint: async (session) => {
17547
- await writeCheckpoint(session);
18108
+ runRound: async (round, previous) => {
18109
+ const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
18110
+ let lastCheckpoint;
18111
+ try {
18112
+ const { session } = await runner.run({
18113
+ task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
18114
+ workspace,
18115
+ provider,
18116
+ model,
18117
+ mode,
18118
+ nativeTools: options.nativeTools,
18119
+ maxSteps,
18120
+ previousSession: previous ?? previousSession,
18121
+ previousSessionLineage: lineage,
18122
+ ...toolSelection ? { tools: toolSelection } : {},
18123
+ permissions,
18124
+ signal: controller.signal,
18125
+ ...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
18126
+ // Write the session after every step, atomically, so a run that dies
18127
+ // partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
18128
+ // its last completed step and can be resumed with --continue rather than
18129
+ // restarted from nothing.
18130
+ onCheckpoint: async (session2) => {
18131
+ lastCheckpoint = session2;
18132
+ await writeCheckpoint(session2);
18133
+ }
18134
+ });
18135
+ return session;
18136
+ } catch (error) {
18137
+ if (!autonomous || !lastCheckpoint || controller.signal.aborted) throw error;
18138
+ console.error(
18139
+ chalk12.yellow(`Round ${round} failed: ${error.message}. Resuming from its last checkpoint.`)
18140
+ );
18141
+ return { ...lastCheckpoint, stopReason: "error" };
18142
+ }
18143
+ },
18144
+ onRoundEnd: async (round, session, willResume) => {
18145
+ if (!willResume) return;
18146
+ await saveSession(workspace, session);
18147
+ console.error(
18148
+ chalk12.yellow(
18149
+ `Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
18150
+ )
18151
+ );
18152
+ if (session.stopReason === "error") await sleep(AUTONOMOUS_ERROR_BACKOFF_MS, controller.signal);
17548
18153
  }
17549
18154
  });
18155
+ } catch (error) {
18156
+ await notifier?.send(`stopped with an error: ${error.message}`).catch(() => void 0);
18157
+ throw error;
17550
18158
  } finally {
17551
18159
  process.off("SIGINT", abortRun);
17552
18160
  process.off("SIGTERM", abortRun);
@@ -17564,7 +18172,7 @@ function registerRunCommand(program2) {
17564
18172
  }
17565
18173
  const stopReason = result.session.stopReason;
17566
18174
  for (const warning of getSessionMcpArchiveWarnings(result.session)) {
17567
- console.error(chalk11.yellow(`Warning: ${warning}`));
18175
+ console.error(chalk12.yellow(`Warning: ${warning}`));
17568
18176
  }
17569
18177
  if (options.json) {
17570
18178
  console.log(
@@ -17573,10 +18181,19 @@ function registerRunCommand(program2) {
17573
18181
  } else {
17574
18182
  printSession(result.session, sessionPath2);
17575
18183
  }
18184
+ if (notifier) {
18185
+ const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
18186
+ const rounds = result.rounds > 1 ? ` after ${result.rounds} rounds` : "";
18187
+ const summary = result.session.finalMessage.trim().slice(0, NOTIFY_SUMMARY_CHARS);
18188
+ await notifier.send(`${outcome}${rounds}.${summary ? `
18189
+
18190
+ ${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not send the result to your phone: ${error.message}`)));
18191
+ }
17576
18192
  if (isIncompleteStop(stopReason) && !options.allowIncomplete) {
17577
18193
  process.exitCode = EXIT_INCOMPLETE_RUN;
17578
18194
  }
17579
18195
  } finally {
18196
+ if (notifier) setAskUserRuntime(void 0);
17580
18197
  mcpRuntime?.close();
17581
18198
  }
17582
18199
  });
@@ -17587,7 +18204,7 @@ import { resolve as resolve12 } from "path";
17587
18204
  function registerSessionsCommand(program2) {
17588
18205
  const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
17589
18206
  sessions.command("list").description("List saved sessions").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (options) => {
17590
- await runSessionTool("list_sessions", { maxResults: parsePositiveInt(options.maxResults) }, options);
18207
+ await runSessionTool("list_sessions", { maxResults: parsePositiveInt2(options.maxResults) }, options);
17591
18208
  });
17592
18209
  sessions.command("show").description("Show a saved session by id").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).action(async (id, options) => {
17593
18210
  await runSessionTool("read_session", { id }, options);
@@ -17595,14 +18212,14 @@ function registerSessionsCommand(program2) {
17595
18212
  sessions.command("search").description("Search saved sessions").argument("<query...>", "Search query").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (queryParts, options) => {
17596
18213
  await runSessionTool(
17597
18214
  "search_sessions",
17598
- { query: queryParts.join(" ").trim(), maxResults: parsePositiveInt(options.maxResults) },
18215
+ { query: queryParts.join(" ").trim(), maxResults: parsePositiveInt2(options.maxResults) },
17599
18216
  options
17600
18217
  );
17601
18218
  });
17602
18219
  sessions.command("export").description("Export a saved session as an auditable episode package").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--format <format>", "Output format: markdown or json", "markdown").option("--max-chars <number>", "Maximum characters per model/tool text block in markdown", "4000").option("--outcome <outcome>", "Outcome status: pass, fail, blocked, or unknown", "unknown").option("--notes <text>", "Outcome notes to include in the episode package").option("--out <path>", "Write export to a path inside the workspace instead of stdout").action(async (id, options) => {
17603
18220
  const workspace = resolve12(options.workspace);
17604
18221
  const format3 = parseExportFormat(options.format);
17605
- const maxChars = parsePositiveInt(options.maxChars);
18222
+ const maxChars = parsePositiveInt2(options.maxChars);
17606
18223
  const outcome = parseOutcomeStatus(options.outcome);
17607
18224
  const session = await loadSession(workspace, id);
17608
18225
  const episode = buildEpisodePackage(session, { outcome, notes: options.notes ?? "" });
@@ -17631,7 +18248,7 @@ async function runSessionTool(tool, input, options) {
17631
18248
  }
17632
18249
  console.log(result.content);
17633
18250
  }
17634
- function parsePositiveInt(value) {
18251
+ function parsePositiveInt2(value) {
17635
18252
  const parsed = Number.parseInt(value, 10);
17636
18253
  if (!Number.isInteger(parsed) || parsed < 1) {
17637
18254
  throw new Error("Value must be a positive integer.");
@@ -17849,7 +18466,7 @@ function parsePermissions(list) {
17849
18466
  }
17850
18467
 
17851
18468
  // src/commands/tasks.ts
17852
- import chalk12 from "chalk";
18469
+ import chalk13 from "chalk";
17853
18470
 
17854
18471
  // src/runs/api.ts
17855
18472
  var RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
@@ -18154,7 +18771,7 @@ function record2(value) {
18154
18771
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
18155
18772
  }
18156
18773
  async function followRun(client, runId, options = {}) {
18157
- const sleep = options.sleep ?? defaultSleep2;
18774
+ const sleep2 = options.sleep ?? defaultSleep2;
18158
18775
  const minBackoffMs = options.minBackoffMs ?? 1e3;
18159
18776
  const maxBackoffMs = options.maxBackoffMs ?? 15e3;
18160
18777
  const signal = options.signal;
@@ -18266,7 +18883,7 @@ async function followRun(client, runId, options = {}) {
18266
18883
  failures,
18267
18884
  message: errorMessage3(cause)
18268
18885
  });
18269
- await sleep(delayMs2, signal);
18886
+ await sleep2(delayMs2, signal);
18270
18887
  continue;
18271
18888
  }
18272
18889
  if (!response.ok) {
@@ -18281,7 +18898,7 @@ async function followRun(client, runId, options = {}) {
18281
18898
  failures,
18282
18899
  message: apiError.message
18283
18900
  });
18284
- await sleep(delayMs2, signal);
18901
+ await sleep2(delayMs2, signal);
18285
18902
  continue;
18286
18903
  }
18287
18904
  throw apiError;
@@ -18290,7 +18907,7 @@ async function followRun(client, runId, options = {}) {
18290
18907
  failures += 1;
18291
18908
  const delayMs2 = backoff();
18292
18909
  options.onReconnect?.({ reason: "stream_failed", delayMs: delayMs2, lastEventId: state.lastEventId, failures, message: "empty body" });
18293
- await sleep(delayMs2, signal);
18910
+ await sleep2(delayMs2, signal);
18294
18911
  continue;
18295
18912
  }
18296
18913
  let broke = null;
@@ -18308,7 +18925,7 @@ async function followRun(client, runId, options = {}) {
18308
18925
  const event = { id: message.id, event: message.event, data: parseData(message.data) };
18309
18926
  const applied = apply(event);
18310
18927
  options.onEvent?.(event, state);
18311
- const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal, sleep) : applied;
18928
+ const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal, sleep2) : applied;
18312
18929
  if (result) return result;
18313
18930
  throwIfAborted();
18314
18931
  }
@@ -18328,12 +18945,12 @@ async function followRun(client, runId, options = {}) {
18328
18945
  if (!sawEvent) failures = 0;
18329
18946
  options.onReconnect?.({ reason: "stream_ended", delayMs, lastEventId: state.lastEventId, failures });
18330
18947
  }
18331
- await sleep(delayMs, signal);
18948
+ await sleep2(delayMs, signal);
18332
18949
  }
18333
18950
  }
18334
18951
  var OUTPUT_READ_TIMEOUT_MS = 1500;
18335
18952
  var OUTPUT_READ_DELAYS_MS = [100, 300];
18336
- async function reconcileTerminalOutput(client, runId, pending, state, signal, sleep) {
18953
+ async function reconcileTerminalOutput(client, runId, pending, state, signal, sleep2) {
18337
18954
  const { reconcileStatus, ...result } = pending;
18338
18955
  for (let attempt = 0; attempt < OUTPUT_READ_DELAYS_MS.length + 1; attempt += 1) {
18339
18956
  if (signal?.aborted) throw new FollowAbortedError(state);
@@ -18351,7 +18968,7 @@ async function reconcileTerminalOutput(client, runId, pending, state, signal, sl
18351
18968
  } catch {
18352
18969
  if (signal?.aborted) throw new FollowAbortedError(state);
18353
18970
  }
18354
- if (attempt < OUTPUT_READ_DELAYS_MS.length) await sleep(OUTPUT_READ_DELAYS_MS[attempt], signal);
18971
+ if (attempt < OUTPUT_READ_DELAYS_MS.length) await sleep2(OUTPUT_READ_DELAYS_MS[attempt], signal);
18355
18972
  }
18356
18973
  return { ...result, text: state.text, outputSource: "incomplete" };
18357
18974
  }
@@ -18385,7 +19002,7 @@ function registerTasksCommand(program2, deps = {}) {
18385
19002
  }
18386
19003
  r.out(formatRunTable(page.data, r.now()));
18387
19004
  if (page.next_cursor) {
18388
- r.err(chalk12.gray(`More: ultrac tasks --cursor ${page.next_cursor}
19005
+ r.err(chalk13.gray(`More: ultrac tasks --cursor ${page.next_cursor}
18389
19006
  `));
18390
19007
  }
18391
19008
  });
@@ -18444,7 +19061,7 @@ async function runTask(promptParts, options, deps) {
18444
19061
  `);
18445
19062
  } else {
18446
19063
  r.err(
18447
- chalk12.gray(
19064
+ chalk13.gray(
18448
19065
  `Run ${run.id} queued. Following; Ctrl-C ${options.detach ? "leaves it running" : "cancels it"}.
18449
19066
  `
18450
19067
  )
@@ -18462,7 +19079,7 @@ async function follow(r, runId, options) {
18462
19079
  controller.abort();
18463
19080
  return;
18464
19081
  }
18465
- r.err(chalk12.yellow(`
19082
+ r.err(chalk13.yellow(`
18466
19083
  Leaving run ${runId} as it is.
18467
19084
  `));
18468
19085
  r.exit(EXIT_CANCELLED);
@@ -18478,7 +19095,7 @@ Leaving run ${runId} as it is.
18478
19095
  if (status === "running") {
18479
19096
  const attempt = typeof data?.attempt === "number" ? data.attempt : null;
18480
19097
  if (data?.restarted === true || attempt !== null && attempt > 1) {
18481
- r.err(chalk12.yellow(`${lead()}[attempt ${attempt ?? "?"} restarted; streaming from the start]
19098
+ r.err(chalk13.yellow(`${lead()}[attempt ${attempt ?? "?"} restarted; streaming from the start]
18482
19099
  `));
18483
19100
  }
18484
19101
  printed = "";
@@ -18493,7 +19110,7 @@ Leaving run ${runId} as it is.
18493
19110
  return;
18494
19111
  }
18495
19112
  case "tool_call": {
18496
- r.err(chalk12.cyan(`${lead()}[tool_call ${String(data?.name ?? "")} ${String(data?.arguments ?? "")}]
19113
+ r.err(chalk13.cyan(`${lead()}[tool_call ${String(data?.name ?? "")} ${String(data?.arguments ?? "")}]
18497
19114
  `));
18498
19115
  return;
18499
19116
  }
@@ -18533,7 +19150,7 @@ Leaving run ${runId} as it is.
18533
19150
  `);
18534
19151
  } else {
18535
19152
  const seconds = Math.round(info.delayMs / 100) / 10;
18536
- r.err(chalk12.gray(`${lead()}[reconnecting in ${seconds}s${info.message ? `: ${info.message}` : ""}]
19153
+ r.err(chalk13.gray(`${lead()}[reconnecting in ${seconds}s${info.message ? `: ${info.message}` : ""}]
18537
19154
  `));
18538
19155
  }
18539
19156
  }
@@ -18565,27 +19182,27 @@ Leaving run ${runId} as it is.
18565
19182
  return result.outputSource === "incomplete" ? EXIT_FAILED : exitCodeFor(result.status);
18566
19183
  }
18567
19184
  if (result.outputSource === "incomplete") {
18568
- r.err(chalk12.yellow(lead() + "[stored task output could not be confirmed; showing text received from the stream]\n"));
19185
+ r.err(chalk13.yellow(lead() + "[stored task output could not be confirmed; showing text received from the stream]\n"));
18569
19186
  }
18570
19187
  switch (result.status) {
18571
19188
  case "succeeded":
18572
19189
  finishHuman(result.text);
18573
19190
  if (result.toolCalls.length && !result.text) {
18574
19191
  const count3 = result.toolCalls.length;
18575
- r.err(chalk12.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
19192
+ r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
18576
19193
  `));
18577
19194
  }
18578
19195
  return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
18579
19196
  case "failed": {
18580
19197
  finishHuman(result.text);
18581
19198
  const code = result.error?.code ? ` (${result.error.code})` : "";
18582
- r.err(chalk12.red(`Run ${runId} failed${code}: ${result.error?.message ?? "The task failed."}
19199
+ r.err(chalk13.red(`Run ${runId} failed${code}: ${result.error?.message ?? "The task failed."}
18583
19200
  `));
18584
19201
  return EXIT_FAILED;
18585
19202
  }
18586
19203
  case "cancelled":
18587
19204
  finishHuman(result.text);
18588
- r.err(chalk12.yellow(`Run ${runId} was cancelled.
19205
+ r.err(chalk13.yellow(`Run ${runId} was cancelled.
18589
19206
  `));
18590
19207
  return EXIT_CANCELLED;
18591
19208
  }
@@ -18598,13 +19215,13 @@ async function interrupted(r, runId, options, printed) {
18598
19215
  `);
18599
19216
  } else {
18600
19217
  r.err(
18601
- chalk12.yellow(`${lead}Detached. Run ${runId} continues; follow it again with \`ultrac tasks follow ${runId}\`.
19218
+ chalk13.yellow(`${lead}Detached. Run ${runId} continues; follow it again with \`ultrac tasks follow ${runId}\`.
18602
19219
  `)
18603
19220
  );
18604
19221
  }
18605
19222
  return EXIT_CANCELLED;
18606
19223
  }
18607
- if (!options.json) r.err(chalk12.yellow(`${lead}Cancelling run ${runId}...
19224
+ if (!options.json) r.err(chalk13.yellow(`${lead}Cancelling run ${runId}...
18608
19225
  `));
18609
19226
  try {
18610
19227
  const cancelled = await r.client.cancelRun(runId);
@@ -18612,7 +19229,7 @@ async function interrupted(r, runId, options, printed) {
18612
19229
  r.out(`${JSON.stringify({ id: null, event: "cancel", data: cancelled })}
18613
19230
  `);
18614
19231
  } else {
18615
- r.err(chalk12.yellow(`${describeCancel(cancelled)}
19232
+ r.err(chalk13.yellow(`${describeCancel(cancelled)}
18616
19233
  `));
18617
19234
  }
18618
19235
  } catch (error) {
@@ -18621,12 +19238,12 @@ async function interrupted(r, runId, options, printed) {
18621
19238
  r.out(`${JSON.stringify({ id: null, event: "cancel_failed", data: { runId, message } })}
18622
19239
  `);
18623
19240
  } else if (error instanceof UltracApiError && error.code === "run_already_finished") {
18624
- r.err(chalk12.yellow(`${error.message} Nothing to cancel.
19241
+ r.err(chalk13.yellow(`${error.message} Nothing to cancel.
18625
19242
  `));
18626
19243
  } else {
18627
- r.err(chalk12.red(`Could not cancel run ${runId}: ${message}
19244
+ r.err(chalk13.red(`Could not cancel run ${runId}: ${message}
18628
19245
  `));
18629
- r.err(chalk12.gray(`Try again with \`ultrac tasks cancel ${runId}\`.
19246
+ r.err(chalk13.gray(`Try again with \`ultrac tasks cancel ${runId}\`.
18630
19247
  `));
18631
19248
  }
18632
19249
  }
@@ -18738,7 +19355,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
18738
19355
 
18739
19356
  // src/commands/tools.ts
18740
19357
  import { resolve as resolve14 } from "path";
18741
- import chalk13 from "chalk";
19358
+ import chalk14 from "chalk";
18742
19359
  function registerToolsCommand(program2) {
18743
19360
  const toolsCommand = program2.command("tools");
18744
19361
  toolsCommand.description("List built-in agent tools").option("--json", "Print tool metadata as JSON", false).option("--mode <name>", `Only the tools a mode advertises: ${agentModeNames.join(", ")}`).option("--budget", "Print what each mode's tool set costs in characters", false).action((options) => {
@@ -18758,7 +19375,7 @@ function registerToolsCommand(program2) {
18758
19375
  printTools(tools);
18759
19376
  if (options.mode) {
18760
19377
  console.log(
18761
- chalk13.gray(
19378
+ chalk14.gray(
18762
19379
  `
18763
19380
  ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${serializedToolsSize(
18764
19381
  toToolSchemas(tools)
@@ -18807,11 +19424,11 @@ function printToolBudget(all, json) {
18807
19424
  }
18808
19425
  for (const row of [...rows, { ...everything, names: all.map((tool) => tool.name) }]) {
18809
19426
  console.log(
18810
- `${chalk13.cyan(row.mode.padEnd(9))} ${String(row.tools).padStart(2)} tools ${String(
19427
+ `${chalk14.cyan(row.mode.padEnd(9))} ${String(row.tools).padStart(2)} tools ${String(
18811
19428
  row.chars.toLocaleString("en-US")
18812
19429
  ).padStart(7)} chars`
18813
19430
  );
18814
- console.log(chalk13.gray(` ${row.names.join(", ")}`));
19431
+ console.log(chalk14.gray(` ${row.names.join(", ")}`));
18815
19432
  }
18816
19433
  }
18817
19434
  function parseToolInput(inputText) {
@@ -18823,7 +19440,7 @@ function parseToolInput(inputText) {
18823
19440
  }
18824
19441
 
18825
19442
  // src/commands/update.ts
18826
- import chalk14 from "chalk";
19443
+ import chalk15 from "chalk";
18827
19444
  function registerUpdateCommand(program2) {
18828
19445
  const packageInfo = readPackageInfo();
18829
19446
  program2.command("update").alias("upgrade").description(`Check for and install the latest ${packageInfo.name} release`).option("--check", "Only check; do not install an available update", false).action(async (options) => {
@@ -18838,7 +19455,7 @@ function registerUpdateCommand(program2) {
18838
19455
  );
18839
19456
  }
18840
19457
  if (!status.updateAvailable) {
18841
- console.log(chalk14.green(`ultrac ${status.currentVersion} is up to date.`));
19458
+ console.log(chalk15.green(`ultrac ${status.currentVersion} is up to date.`));
18842
19459
  return;
18843
19460
  }
18844
19461
  if (options.check) {
@@ -18847,12 +19464,12 @@ function registerUpdateCommand(program2) {
18847
19464
  }
18848
19465
  console.log(`Updating ultrac ${status.currentVersion} \u2192 ${status.latestVersion}\u2026`);
18849
19466
  await installLatestVersion(packageInfo.name);
18850
- console.log(chalk14.green(`Updated to ultrac ${status.latestVersion}. Restart ultrac to use the new version.`));
19467
+ console.log(chalk15.green(`Updated to ultrac ${status.latestVersion}. Restart ultrac to use the new version.`));
18851
19468
  });
18852
19469
  }
18853
19470
  function printAvailable(currentVersion, latestVersion) {
18854
- console.log(chalk14.yellow(`Update available: ultrac ${currentVersion} \u2192 ${latestVersion}`));
18855
- console.log(chalk14.gray("Run `ultrac update` to install it."));
19471
+ console.log(chalk15.yellow(`Update available: ultrac ${currentVersion} \u2192 ${latestVersion}`));
19472
+ console.log(chalk15.gray("Run `ultrac update` to install it."));
18856
19473
  }
18857
19474
 
18858
19475
  // src/cli.ts
@@ -18878,10 +19495,11 @@ registerUpdateCommand(program);
18878
19495
  registerSessionsCommand(program);
18879
19496
  registerMemoryCommand(program);
18880
19497
  registerMcpCommand(program);
19498
+ registerNotifyCommand(program);
18881
19499
  registerSkillsCommand(program);
18882
19500
  registerReplCommand(program);
18883
19501
  program.parseAsync(process.argv).catch((error) => {
18884
19502
  const message = error instanceof Error ? error.message : String(error);
18885
- console.error(chalk15.red(message));
19503
+ console.error(chalk16.red(message));
18886
19504
  process.exitCode = 1;
18887
19505
  });