@spinalfluid/ultrac 0.1.14 → 0.1.18

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 +18 -4
  2. package/dist/cli.js +314 -105
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -143,9 +143,12 @@ Inside a session:
143
143
  - Type to talk to Ultrac; it inspects the repository with tools as needed.
144
144
  - On supported terminals, the full-width input box stays at the bottom while tools and replies
145
145
  run above it. The footer shows only the current model. `/usage` and `/context` show token and context details.
146
- Thinking and tool activity lines show their own live elapsed timers and retain the measured
147
- duration when finished. Sub-second durations are hidden. Tool actions stay in a collapsed activity group;
148
- click it or press Ctrl+O to expand, then click an action for command and output details. Green and red
146
+ Working and tool activity lines show their own live elapsed timers and retain the measured
147
+ duration when finished. Sub-second durations are hidden. Tool calls stay in a collapsed group directly below the live working timer;
148
+ click it or press Ctrl+O to expand, then click an action for command and output details. When the
149
+ task ends, the group moves below its result in the transcript. Working time accumulates across
150
+ model calls and continuation rounds. Long-running tasks emit brief progress updates based on
151
+ completed calls and the active tool or model wait. Green and red
149
152
  dots show success and failure. Use `/output` for full saved results. Slash suggestions use aligned
150
153
  command and description columns on the terminal background.
151
154
  - Type `/` to browse built-in commands and available skills. Type to filter, use arrow keys
@@ -167,7 +170,8 @@ Inside a session:
167
170
  - Gated tools (file writes, shell, network) prompt for approval: `once` / `always` / `no`.
168
171
  - `/mcp` shows the connected servers in this session; `/doctor` checks local setup.
169
172
  - `/continue [focus]` continues an unfinished task using its current transcript. `/autonomous on`
170
- enables up to five rounds when a task reaches its step ceiling. Checkpoints, a six-hour elapsed
173
+ allows up to five rounds when a task reaches its step ceiling (enabled by default; `/autonomous off`
174
+ disables continuation). Checkpoints, a six-hour elapsed
171
175
  limit, stall detection, and Esc cancellation remain active. `/autonomous off` restores one round.
172
176
  Failed rounds are not automatically replayed; provider retry/failover handles request failures.
173
177
  - The agent can use `create_skill` in default/coding modes (or with `--tools all`) to create a
@@ -750,3 +754,13 @@ ultrac skills create project-check --description "Run project checks" --command
750
754
  Edit the generated `SKILL.md` instructions, approve the project workspace, and invoke
751
755
  `/project-check` in the terminal. The agent can also write a new `SKILL.md` with its file tools and load it with
752
756
  `skill`; normal write and tool permissions still apply.
757
+
758
+
759
+ Tool approval presets can be chosen at startup: `ultrac --permissions full` grants file writes,
760
+ shell execution, and network tools without tool approval prompts for that session. Use
761
+ `/permissions full` to enable it inside an existing session. `workspace` grants writes and
762
+ denies shell and network access; `readonly` denies all three permissions. The default mode asks
763
+ when a tool needs an ungranted permission.
764
+ These presets do not bypass operating-system permissions or the restrictions of a read-only
765
+ agent mode. Approval dialogs offer separate choices for one call, the session, denial, or
766
+ session-long refusal, with keyboard selection in interactive terminals.
package/dist/cli.js CHANGED
@@ -1174,7 +1174,7 @@ var COMPACTION_MARKER = "[compacted-context]";
1174
1174
  var SUMMARY_PREAMBLE = "The earlier part of this session was removed to fit the context window and replaced by the record below. It is a record of what happened, not a new instruction. Continue the work it describes.";
1175
1175
  var DEFAULT_MAX_SUMMARY_CHARS = 8e3;
1176
1176
  var MIN_SUMMARY_CHARS = 400;
1177
- var DEFAULT_SUMMARY_TIMEOUT_MS = 3e4;
1177
+ var DEFAULT_SUMMARY_TIMEOUT_MS = 12e4;
1178
1178
  var DEFAULT_MAX_TRANSCRIPT_CHARS = 6e4;
1179
1179
  var DEFAULT_MAX_MESSAGE_CHARS = 4e3;
1180
1180
  function isCompactionSummary(message) {
@@ -2584,6 +2584,8 @@ function classifyProviderError(error) {
2584
2584
  return transient("timeout", "Provider request timed out.");
2585
2585
  }
2586
2586
  const status = extractStatus(error);
2587
+ const busy = [...errorChain(error)].some((current) => readString(current, "code") === "upstream_busy");
2588
+ if (busy) return { ...transient("server", "Model capacity or admission is temporarily unavailable (HTTP 503).", status ?? 503), code: "upstream_busy", retryAfterMs: retryDelay(error) ?? 5e3 };
2587
2589
  if (hasNotConfiguredCode(error)) {
2588
2590
  return terminal(
2589
2591
  "not_configured",
@@ -2598,8 +2600,8 @@ function classifyProviderError(error) {
2598
2600
  }
2599
2601
  if (typeof status === "number") {
2600
2602
  if (status === 408) return transient("timeout", "HTTP 408 request timeout.", status);
2601
- if (status === 429) return transient("rate_limit", "HTTP 429 rate limited.", status);
2602
- if (status >= 500) return transient("server", `HTTP ${status} server error.`, status);
2603
+ if (status === 429) return { ...transient("rate_limit", "HTTP 429 rate limited.", status), retryAfterMs: retryDelay(error) };
2604
+ if (status >= 500) return { ...transient("server", `HTTP ${status} server error.`, status), retryAfterMs: retryDelay(error) };
2603
2605
  if (status === 401 || status === 403) {
2604
2606
  return terminal("auth", `HTTP ${status} authentication failure.`, status);
2605
2607
  }
@@ -2640,9 +2642,24 @@ function safeClassification(classification) {
2640
2642
  ...status === void 0 ? {} : { status },
2641
2643
  retryable: classification.retryable,
2642
2644
  failoverable: classification.failoverable,
2643
- reason
2645
+ reason: classification.code === "upstream_busy" ? "Model capacity or admission is temporarily unavailable (HTTP 503)." : reason,
2646
+ ...classification.code ? { code: classification.code } : {},
2647
+ ...classification.retryAfterMs === void 0 ? {} : { retryAfterMs: classification.retryAfterMs }
2644
2648
  };
2645
2649
  }
2650
+ function retryDelay(error) {
2651
+ for (const current of errorChain(error)) {
2652
+ if (!isRecord2(current)) continue;
2653
+ const headers = current.headers;
2654
+ if (typeof headers?.get !== "function") continue;
2655
+ const raw = headers.get("retry-after");
2656
+ if (!raw) continue;
2657
+ const seconds = Number(raw);
2658
+ const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - Date.now();
2659
+ if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, 3e5);
2660
+ }
2661
+ return void 0;
2662
+ }
2646
2663
  function classificationReason(kind, status) {
2647
2664
  switch (kind) {
2648
2665
  case "aborted":
@@ -3043,7 +3060,7 @@ var FallbackProvider = class {
3043
3060
  }
3044
3061
  }
3045
3062
  async waitBeforeRetry(operation, target, attempt, classification, signal) {
3046
- const delayMs = computeBackoffDelay(attempt, this.backoff);
3063
+ const delayMs = Math.max(computeBackoffDelay(attempt, this.backoff), classification.retryAfterMs ?? 0);
3047
3064
  const safe = safeClassification(classification);
3048
3065
  this.onRetry?.({
3049
3066
  operation,
@@ -16646,6 +16663,9 @@ ${context}`
16646
16663
  this.userConstraints.push({ kind: "message", content: input });
16647
16664
  let finalMessage = "";
16648
16665
  let toolCallsUsed = 0;
16666
+ let consecutiveEmptyResponses = 0;
16667
+ const turnStarted = performance.now();
16668
+ const firstStep = this.steps.length;
16649
16669
  let stopReason = "max-steps";
16650
16670
  try {
16651
16671
  for (let index = 0; index < this.options.maxSteps; index += 1) {
@@ -16698,6 +16718,7 @@ ${context}`
16698
16718
  throw budgetError;
16699
16719
  }
16700
16720
  toolCallsUsed += response.toolCalls.length;
16721
+ if (response.content.trim() || response.toolCalls.length) consecutiveEmptyResponses = 0;
16701
16722
  if (response.content.trim()) {
16702
16723
  finalMessage = response.content;
16703
16724
  handlers.onAssistantMessage?.(response.content);
@@ -16717,6 +16738,21 @@ ${context}`
16717
16738
  handlers.onInterrupted?.();
16718
16739
  break;
16719
16740
  }
16741
+ if (!response.content.trim()) {
16742
+ consecutiveEmptyResponses += 1;
16743
+ const willRetry = consecutiveEmptyResponses <= 2 && index + 1 < this.options.maxSteps;
16744
+ diagnosticEvent("session.empty-response", {
16745
+ sessionId: this.id,
16746
+ attempt: consecutiveEmptyResponses,
16747
+ willRetry,
16748
+ maxRecoveries: 2,
16749
+ stepCount: this.steps.length
16750
+ });
16751
+ if (!willRetry) {
16752
+ throw Object.assign(new Error("The model returned an empty response without completing the task. Empty-response recovery was exhausted; the session checkpoint is preserved."), { status: 422 });
16753
+ }
16754
+ continue;
16755
+ }
16720
16756
  if (this.appendQueuedMessages(handlers) === 0) {
16721
16757
  stopReason = "completed";
16722
16758
  break;
@@ -16784,6 +16820,15 @@ ${context}`
16784
16820
  throw error;
16785
16821
  } finally {
16786
16822
  this.lastStopReason = stopReason;
16823
+ diagnosticEvent("session.turn.end", {
16824
+ sessionId: this.id,
16825
+ reason: stopReason,
16826
+ stepCount: this.steps.length,
16827
+ turnSteps: this.steps.length - firstStep,
16828
+ maxSteps: this.options.maxSteps,
16829
+ emptyResponses: consecutiveEmptyResponses,
16830
+ durationMs: Math.max(0, performance.now() - turnStarted)
16831
+ });
16787
16832
  this.subagents.push(...drainSubagentTranscripts());
16788
16833
  }
16789
16834
  handlers.onStopped?.(stopReason);
@@ -17087,6 +17132,11 @@ async function runAutonomousLoop(options) {
17087
17132
  let lastStepCount = 0;
17088
17133
  let stalls = 0;
17089
17134
  let consecutiveErrors = 0;
17135
+ const finish = (session, rounds, haltReason) => {
17136
+ diagnosticEvent("autonomous.end", { id, sessionId: session.id, rounds, reason: haltReason, elapsedMs: elapsed() });
17137
+ return { session, rounds, haltReason };
17138
+ };
17139
+ diagnosticEvent("autonomous.start", { id, maxRounds: options.maxRounds, maxElapsedMs });
17090
17140
  try {
17091
17141
  signal.throwIfAborted();
17092
17142
  for (let round = 1; ; round += 1) {
@@ -17117,8 +17167,8 @@ async function runAutonomousLoop(options) {
17117
17167
  else if (!shouldResume(session.stopReason)) haltReason = "stopped";
17118
17168
  else if (stalls >= stallLimit) haltReason = "stalled";
17119
17169
  else if (round >= options.maxRounds) haltReason = "max_rounds";
17120
- const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
17121
- if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
17170
+ const retryDelay2 = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
17171
+ if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay2 >= maxElapsedMs) haltReason = "elapsed_budget";
17122
17172
  diagnosticEvent("round.end", {
17123
17173
  id,
17124
17174
  round,
@@ -17132,10 +17182,10 @@ async function runAutonomousLoop(options) {
17132
17182
  });
17133
17183
  if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
17134
17184
  await options.onRoundEnd?.(round, session, !haltReason);
17135
- if (haltReason) return { session, rounds: round, haltReason };
17185
+ if (haltReason) return finish(session, round, haltReason);
17136
17186
  if (session.stopReason === "error") {
17137
- const delayMs = retryDelay;
17138
- if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
17187
+ const delayMs = retryDelay2;
17188
+ if (elapsed() + delayMs >= maxElapsedMs) return finish(session, round, "elapsed_budget");
17139
17189
  diagnosticEvent("round.retry", {
17140
17190
  id,
17141
17191
  round,
@@ -17149,12 +17199,15 @@ async function runAutonomousLoop(options) {
17149
17199
  await sleep2(delayMs, signal);
17150
17200
  } catch (error) {
17151
17201
  if (!signal.aborted) throw error;
17152
- return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
17202
+ return finish(session, round, budget.signal.aborted ? "elapsed_budget" : "aborted");
17153
17203
  }
17154
17204
  }
17155
- if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
17205
+ if (signal.aborted || elapsed() >= maxElapsedMs) return finish(session, round, budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted");
17156
17206
  previous = session;
17157
17207
  }
17208
+ } catch (error) {
17209
+ diagnosticEvent("autonomous.end", { id, reason: budget.signal.aborted ? "elapsed_budget" : signal.aborted ? "aborted" : "exception", elapsedMs: elapsed() });
17210
+ throw error;
17158
17211
  } finally {
17159
17212
  clearTimeout(deadline);
17160
17213
  }
@@ -17442,7 +17495,9 @@ var VERBS = {
17442
17495
  run_tests: "Run tests",
17443
17496
  diagnostics: "Check diagnostics",
17444
17497
  task: "Delegate",
17445
- fetch_url: "Fetch",
17498
+ fetch_url: "Opened",
17499
+ web_search: "Searched the web for",
17500
+ search_web: "Searched the web for",
17446
17501
  git_status: "Check Git status",
17447
17502
  git_diff: "Read Git diff",
17448
17503
  git_log: "Read Git log",
@@ -17470,7 +17525,7 @@ function toolTarget(call) {
17470
17525
  if (paths.length > 1) return `${paths.length} files`;
17471
17526
  if (call.tool === "run_shell") return compact2(stringValue(input.command), 72);
17472
17527
  if (call.tool === "run_package_script") return compact2(stringValue(input.script), 50);
17473
- if (call.tool === "search_files") return quote(stringValue(input.query) || stringValue(input.pattern));
17528
+ if (["search_files", "web_search", "search_web"].includes(call.tool)) return quote(stringValue(input.query) || stringValue(input.pattern));
17474
17529
  if (call.tool === "fetch_url") return compact2(stringValue(input.url), 72);
17475
17530
  if (call.tool === "task") return compact2(stringValue(input.description) || stringValue(input.task), 64);
17476
17531
  if (call.tool === "git_show") return compact2(stringValue(input.ref), 40);
@@ -17499,10 +17554,12 @@ function humanize(value) {
17499
17554
  import { stdout as stdout4 } from "process";
17500
17555
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17501
17556
  var Spinner = class {
17502
- constructor(label = "thinking") {
17557
+ constructor(label = "working", renderLine) {
17503
17558
  this.label = label;
17559
+ this.renderLine = renderLine;
17504
17560
  }
17505
17561
  label;
17562
+ renderLine;
17506
17563
  timer;
17507
17564
  index = 0;
17508
17565
  holdWhile;
@@ -17528,8 +17585,11 @@ var Spinner = class {
17528
17585
  this.timer = void 0;
17529
17586
  }
17530
17587
  if (stdout4.isTTY && elapsed !== void 0) {
17531
- stdout4.clearLine(0);
17532
- stdout4.cursorTo(0);
17588
+ if (this.renderLine) this.renderLine("");
17589
+ else {
17590
+ stdout4.clearLine(0);
17591
+ stdout4.cursorTo(0);
17592
+ }
17533
17593
  if (completion && elapsed >= 1e3) stdout4.write(` ${completion} ${formatSeconds(elapsed)}.
17534
17594
  `);
17535
17595
  }
@@ -17537,16 +17597,21 @@ var Spinner = class {
17537
17597
  render() {
17538
17598
  if (this.holdWhile?.()) {
17539
17599
  if (!this.held) {
17540
- stdout4.clearLine(0);
17541
- stdout4.cursorTo(0);
17600
+ if (this.renderLine) this.renderLine("");
17601
+ else {
17602
+ stdout4.clearLine(0);
17603
+ stdout4.cursorTo(0);
17604
+ }
17542
17605
  this.held = true;
17543
17606
  }
17544
17607
  return;
17545
17608
  }
17546
17609
  this.held = false;
17547
17610
  this.index = (this.index + 1) % FRAMES.length;
17548
- stdout4.clearLine(0);
17549
- stdout4.cursorTo(0);
17611
+ if (!this.renderLine) {
17612
+ stdout4.clearLine(0);
17613
+ stdout4.cursorTo(0);
17614
+ }
17550
17615
  const milliseconds = performance.now() - (this.startedAt ?? performance.now());
17551
17616
  const elapsed = milliseconds >= 1e3 ? formatElapsed(milliseconds) : "";
17552
17617
  const limit = Math.max(0, (stdout4.columns ?? 80) - elapsed.length - 10);
@@ -17558,7 +17623,9 @@ var Spinner = class {
17558
17623
  label += character;
17559
17624
  cells2 += size;
17560
17625
  }
17561
- stdout4.write(` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`);
17626
+ const line = ` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`;
17627
+ if (this.renderLine) this.renderLine(line);
17628
+ else stdout4.write(line);
17562
17629
  }
17563
17630
  };
17564
17631
  function formatSeconds(milliseconds) {
@@ -17569,6 +17636,13 @@ function formatSeconds(milliseconds) {
17569
17636
 
17570
17637
  // src/ui/activity.ts
17571
17638
  var sink;
17639
+ var workingSink;
17640
+ function setWorkingSink(value) {
17641
+ workingSink = value;
17642
+ }
17643
+ function getWorkingSink() {
17644
+ return workingSink;
17645
+ }
17572
17646
  function setActivitySink(value) {
17573
17647
  sink = value;
17574
17648
  }
@@ -17592,10 +17666,16 @@ function activityEntry(call, result, durationMs) {
17592
17666
  }
17593
17667
  function activityRows(snapshot, expanded, selected, end) {
17594
17668
  const failures = snapshot.entries.filter((entry) => !entry.ok).length;
17595
- const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Thought for ${formatSeconds(snapshot.thinkingMs)}` : "";
17596
- const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} actions${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17669
+ const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(snapshot.thinkingMs)}` : "";
17670
+ const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} ${snapshot.entries.length === 1 ? "tool call" : "tool calls"}${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17597
17671
  const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17598
- if (!expanded) return rows;
17672
+ if (!expanded) {
17673
+ if (!snapshot.finished) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17674
+ const entry = snapshot.entries[index];
17675
+ rows.push({ text: ` \u25CF ${entry.label}`, ok: entry.ok, entry: index });
17676
+ }
17677
+ return rows;
17678
+ }
17599
17679
  const last = Math.min(snapshot.entries.length, end);
17600
17680
  for (let index = Math.max(0, last - 5); index < last; index += 1) {
17601
17681
  const entry = snapshot.entries[index];
@@ -17610,6 +17690,32 @@ function activityRows(snapshot, expanded, selected, end) {
17610
17690
  return rows;
17611
17691
  }
17612
17692
 
17693
+ // src/ui/task-progress.ts
17694
+ function startTaskProgress(options) {
17695
+ let lastMessage = performance.now();
17696
+ let lastState = "";
17697
+ let stopped = false;
17698
+ const timer = setInterval(() => {
17699
+ if (stopped || options.holdWhile?.()) return;
17700
+ const state = options.snapshot();
17701
+ if (!state.activeTool && !state.waitingForModel) return;
17702
+ const fingerprint = `${state.completed}:${state.failed}:${state.activeTool ?? "model"}`;
17703
+ if (performance.now() - lastMessage < (fingerprint === lastState ? 6e4 : 3e4)) return;
17704
+ const completed = state.completed ? `I have completed ${state.completed} tool ${state.completed === 1 ? "call" : "calls"}.${state.failed ? ` Of those, ${state.failed} failed.` : ""} ` : "";
17705
+ const current = state.activeTool ? `I am running the ${state.activeTool} tool.` : "I am waiting for the model's next response.";
17706
+ options.write(`${completed}${current}`);
17707
+ lastMessage = performance.now();
17708
+ lastState = fingerprint;
17709
+ }, 5e3);
17710
+ timer.unref?.();
17711
+ return { markMessage: () => {
17712
+ lastMessage = performance.now();
17713
+ }, stop: () => {
17714
+ stopped = true;
17715
+ clearInterval(timer);
17716
+ } };
17717
+ }
17718
+
17613
17719
  // src/ui/interactive-choice.ts
17614
17720
  var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17615
17721
  var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
@@ -18018,9 +18124,8 @@ var TerminalLayout = class {
18018
18124
  suggestions = [];
18019
18125
  selected = 0;
18020
18126
  activity;
18021
- activityExpanded = false;
18022
- activitySelected = -1;
18023
- activityEnd = 0;
18127
+ liveActivity;
18128
+ completedActivities = [];
18024
18129
  restoreActivityInput;
18025
18130
  installed = false;
18026
18131
  closed = false;
@@ -18030,6 +18135,7 @@ var TerminalLayout = class {
18030
18135
  resize = () => {
18031
18136
  if (this.closed || !this.enabled) return;
18032
18137
  this.clearPreviousFooter();
18138
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1 && item.row + item.height - 1 <= this.contentRows);
18033
18139
  this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18034
18140
  this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
18035
18141
  this.rawWrite(`${CSI}1;${this.contentRows}r`);
@@ -18059,12 +18165,8 @@ var TerminalLayout = class {
18059
18165
  get suggestionRows() {
18060
18166
  return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
18061
18167
  }
18062
- get activityLines() {
18063
- if (!this.activity) return [];
18064
- return activityRows(this.activity, this.activityExpanded, this.activitySelected, this.activityEnd).slice(0, Math.max(1, Math.min(10, this.rows - BASE_FOOTER_ROWS - this.suggestionRows - 2)));
18065
- }
18066
18168
  get contentRows() {
18067
- return this.rows - BASE_FOOTER_ROWS - this.suggestionRows - this.activityLines.length;
18169
+ return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
18068
18170
  }
18069
18171
  get inputRow() {
18070
18172
  return this.rows - 2;
@@ -18094,7 +18196,7 @@ var TerminalLayout = class {
18094
18196
  let sequence = "";
18095
18197
  let sequenceTimer;
18096
18198
  const wrapper = (text, key) => {
18097
- if (this.activity && !this.suspended && key?.ctrl && key.name === "o") {
18199
+ if ((this.activity || this.completedActivities.length) && !this.suspended && key?.ctrl && key.name === "o") {
18098
18200
  this.toggleActivity();
18099
18201
  return;
18100
18202
  }
@@ -18129,50 +18231,112 @@ var TerminalLayout = class {
18129
18231
  this.paintFooter();
18130
18232
  }
18131
18233
  setActivity(snapshot) {
18132
- this.clearPreviousFooter();
18133
- const previous = this.activity?.entries.length ?? 0;
18134
- if (!snapshot.entries.length && !snapshot.finished) {
18135
- this.activityExpanded = false;
18136
- this.activitySelected = -1;
18137
- }
18138
18234
  this.activity = snapshot;
18139
- if (this.activityEnd >= previous || !previous) this.activityEnd = snapshot.entries.length;
18140
- this.reflowActivity();
18141
- }
18142
- toggleActivity() {
18143
- if (!this.activity) return;
18144
- this.clearPreviousFooter();
18145
- this.activityExpanded = !this.activityExpanded;
18146
- this.reflowActivity();
18147
- }
18148
- reflowActivity() {
18149
18235
  if (!this.enabled || !this.installed || this.closed) return;
18150
- this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18151
- this.rawWrite(`${CSI}1;${this.contentRows}r`);
18236
+ if (!this.liveActivity && !snapshot.finished) {
18237
+ this.writeTranscript(`
18238
+ ${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18239
+ `);
18240
+ this.liveActivity = { snapshot, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1, end: snapshot.entries.length };
18241
+ this.completedActivities.push(this.liveActivity);
18242
+ }
18243
+ const item = this.liveActivity;
18244
+ if (!item) return;
18245
+ const previous = item.snapshot.entries.length;
18246
+ item.snapshot = snapshot;
18247
+ if (item.end >= previous) item.end = snapshot.entries.length;
18248
+ if (!snapshot.finished) {
18249
+ this.redrawCompleted(item);
18250
+ return;
18251
+ }
18252
+ if (item.row >= 2) {
18253
+ const removed = item.height + 1;
18254
+ this.rawWrite(`${cup(item.row - 1)}${CSI}${removed}M`);
18255
+ if (this.transcriptRow > item.row) this.transcriptRow -= removed;
18256
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= removed;
18257
+ }
18258
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18259
+ this.liveActivity = void 0;
18260
+ this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18261
+ `);
18262
+ this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18152
18263
  this.paintFooter();
18153
18264
  }
18265
+ setWorkingStatus(text) {
18266
+ const item = this.liveActivity;
18267
+ if (!item) return;
18268
+ if (item.row < 2 || item.row + item.height > this.contentRows) {
18269
+ if (item.row >= 1 && item.row <= this.contentRows) {
18270
+ this.rawWrite(`${cup(item.row)}${CSI}${item.height}M`);
18271
+ if (this.transcriptRow > item.row) this.transcriptRow -= item.height;
18272
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= item.height;
18273
+ }
18274
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18275
+ this.writeTranscript(`
18276
+ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18277
+ `);
18278
+ item.row = this.transcriptRow - 1;
18279
+ item.height = 1;
18280
+ item.expanded = false;
18281
+ this.completedActivities.push(item);
18282
+ }
18283
+ this.rawWrite(`${cup(item.row - 1)}${CSI}0m${CSI}2K${fit(text, this.columns)}`);
18284
+ this.placeInputCursor();
18285
+ }
18286
+ colorActivityRow(row) {
18287
+ const text = fit(row.text, this.columns);
18288
+ const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
18289
+ const dot = text.indexOf("\u25CF");
18290
+ return dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18291
+ }
18292
+ shiftCompleted(rows) {
18293
+ for (const item of this.completedActivities) item.row -= rows;
18294
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18295
+ }
18296
+ redrawCompleted(item) {
18297
+ const rows = activityRows(item.snapshot, item.expanded, item.selected, item.end).slice(0, Math.max(1, this.contentRows - 2));
18298
+ const delta = rows.length - item.height;
18299
+ const scroll = Math.max(0, this.transcriptRow + delta - this.contentRows);
18300
+ if (scroll) {
18301
+ this.rawWrite(`${cup(this.contentRows)}${CSI}${scroll}S`);
18302
+ this.shiftCompleted(scroll);
18303
+ this.transcriptRow -= scroll;
18304
+ }
18305
+ if (item.row < 1) return;
18306
+ if (delta > 0) this.rawWrite(`${cup(item.row + item.height)}${CSI}${delta}L`);
18307
+ if (delta < 0) this.rawWrite(`${cup(item.row + rows.length)}${CSI}${-delta}M`);
18308
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row += delta;
18309
+ if (this.transcriptRow > item.row) this.transcriptRow += delta;
18310
+ item.height = rows.length;
18311
+ rows.forEach((row, index) => this.rawWrite(`${cup(item.row + index)}${CSI}0m${CSI}2K${this.colorActivityRow(row)}`));
18312
+ this.paintFooter();
18313
+ }
18314
+ toggleActivity() {
18315
+ if (this.liveActivity && (this.liveActivity.row < 2 || this.liveActivity.row + this.liveActivity.height > this.contentRows)) this.setWorkingStatus("working\u2026");
18316
+ const item = this.liveActivity ?? this.completedActivities.at(-1);
18317
+ if (item) {
18318
+ item.expanded = !item.expanded;
18319
+ this.redrawCompleted(item);
18320
+ }
18321
+ }
18154
18322
  activityMouse(button, row, press) {
18155
- if (!this.activity || !press) return;
18156
- if (button === 64 || button === 65) {
18157
- if (!this.activityExpanded) return;
18158
- this.clearPreviousFooter();
18159
- this.activityEnd = Math.max(Math.min(5, this.activity.entries.length), Math.min(this.activity.entries.length, this.activityEnd + (button === 64 ? -1 : 1)));
18160
- this.activitySelected = -1;
18161
- this.reflowActivity();
18323
+ if (!press) return;
18324
+ const completed = this.completedActivities.find((item) => row >= item.row && row < item.row + item.height);
18325
+ if (completed && (button === 64 || button === 65)) {
18326
+ if (!completed.expanded) return;
18327
+ completed.end = Math.max(Math.min(5, completed.snapshot.entries.length), Math.min(completed.snapshot.entries.length, completed.end + (button === 64 ? -1 : 1)));
18328
+ completed.selected = -1;
18329
+ this.redrawCompleted(completed);
18162
18330
  return;
18163
18331
  }
18164
- if (button !== 0) return;
18165
- const index = row - this.contentRows - 1;
18166
- if (index === 0) {
18167
- this.toggleActivity();
18332
+ if (completed && button === 0) {
18333
+ const rows = activityRows(completed.snapshot, completed.expanded, completed.selected, completed.end);
18334
+ const entry = rows[row - completed.row]?.entry;
18335
+ if (row === completed.row) completed.expanded = !completed.expanded;
18336
+ else if (entry !== void 0) completed.selected = completed.selected === entry ? -1 : entry;
18337
+ this.redrawCompleted(completed);
18168
18338
  return;
18169
18339
  }
18170
- const entry = this.activityLines[index]?.entry;
18171
- if (entry !== void 0) {
18172
- this.clearPreviousFooter();
18173
- this.activitySelected = this.activitySelected === entry ? -1 : entry;
18174
- this.reflowActivity();
18175
- }
18176
18340
  }
18177
18341
  setInputFormatter(formatter) {
18178
18342
  this.inputFormatter = formatter;
@@ -18238,6 +18402,7 @@ var TerminalLayout = class {
18238
18402
  if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters2(formatted))) {
18239
18403
  this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
18240
18404
  `);
18405
+ if (this.transcriptRow === this.contentRows) this.shiftCompleted(1);
18241
18406
  this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
18242
18407
  this.transcriptColumn = 1;
18243
18408
  this.wrapPending = false;
@@ -18302,6 +18467,7 @@ var TerminalLayout = class {
18302
18467
  this.wrapPending = true;
18303
18468
  }
18304
18469
  }
18470
+ if (this.transcriptRow > this.contentRows) this.shiftCompleted(this.transcriptRow - this.contentRows);
18305
18471
  this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
18306
18472
  }
18307
18473
  }
@@ -18351,13 +18517,10 @@ var TerminalLayout = class {
18351
18517
  const model = /^cosmic[- ]1$/i.test(this.details.model) ? "Cosmic 1" : this.details.model;
18352
18518
  const metadata = fit(model, this.columns);
18353
18519
  let frame = "";
18354
- const activity = this.activityLines;
18355
- if (activity.length && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
18520
+ const activity = [];
18521
+ if ((activity.length || this.completedActivities.length) && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
18356
18522
  activity.forEach((row, index) => {
18357
- const text = fit(row.text, this.columns);
18358
- const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
18359
- const dot = text.indexOf("\u25CF");
18360
- const rendered = dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18523
+ const rendered = this.colorActivityRow(row);
18361
18524
  frame += `${cup(this.contentRows + 1 + index)}${CSI}0m${CSI}2K${rendered}`;
18362
18525
  });
18363
18526
  const suggestions = this.suggestionRows;
@@ -18800,11 +18963,12 @@ Custom commands come from skills that declare a 'command:' key in their
18800
18963
  frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
18801
18964
  var EXIT_REPL_NOT_STARTED = 1;
18802
18965
  function registerReplCommand(program2) {
18803
- program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").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 per turn").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").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)").action(async (taskParts, options, command) => {
18966
+ program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").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 per turn").option("--permissions <mode>", "Tool permission preset: readonly, workspace, or full (no tool approval prompts)").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").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)").action(async (taskParts, options, command) => {
18804
18967
  const task = (taskParts ?? []).join(" ").trim();
18805
18968
  const invokedByName = namesReplCommand(command, command.parent?.args[0]);
18806
18969
  const mistyped = invokedByName ? void 0 : mistypedCommand(task, knownCommandNames(program2));
18807
18970
  if (mistyped) throw new Error(mistyped);
18971
+ if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
18808
18972
  await runRepl(options, task);
18809
18973
  });
18810
18974
  }
@@ -18912,7 +19076,7 @@ async function runRepl(options, initialTask = "") {
18912
19076
  diagnosticEvent("provider.retry", event);
18913
19077
  console.log(
18914
19078
  chalk11.yellow(
18915
- ` retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
19079
+ ` ${event.classification.reason} Retrying ${event.target.model} in ${(event.delayMs / 1e3).toFixed(1)} seconds (retry ${event.attempt}).`
18916
19080
  )
18917
19081
  );
18918
19082
  }
@@ -18923,9 +19087,9 @@ async function runRepl(options, initialTask = "") {
18923
19087
  return;
18924
19088
  }
18925
19089
  const basePermissions = {
18926
- write: options.allowWrite || config.permissions.write,
18927
- shell: options.allowShell || config.permissions.shell,
18928
- network: options.allowNetwork || config.permissions.network
19090
+ write: options.permissions ? options.permissions !== "readonly" : options.allowWrite || config.permissions.write,
19091
+ shell: options.permissions ? options.permissions === "full" : options.allowShell || config.permissions.shell,
19092
+ network: options.permissions ? options.permissions === "full" : options.allowNetwork || config.permissions.network
18929
19093
  };
18930
19094
  const registry = createDefaultToolRegistry();
18931
19095
  const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
@@ -18942,7 +19106,11 @@ async function runRepl(options, initialTask = "") {
18942
19106
  ...toolSelection ? { tools: toolSelection } : {}
18943
19107
  };
18944
19108
  setSubagentRuntime({ provider, model: init.model, mode: init.mode, registry });
18945
- return useNativeTools ? new NativeSession(opts) : new InteractiveSession(opts);
19109
+ const created = useNativeTools ? new NativeSession(opts) : new InteractiveSession(opts);
19110
+ if (options.permissions) for (const permission of ["write", "shell", "network"]) {
19111
+ created.setPermission(permission, init.permissions[permission]);
19112
+ }
19113
+ return created;
18946
19114
  };
18947
19115
  let session = createSession({
18948
19116
  model,
@@ -18956,6 +19124,7 @@ async function runRepl(options, initialTask = "") {
18956
19124
  details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
18957
19125
  }) : void 0;
18958
19126
  setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
19127
+ setWorkingSink(layout?.enabled ? (text) => layout.setWorkingStatus(text) : void 0);
18959
19128
  const refreshDetails = () => {
18960
19129
  if (!layout?.enabled) return;
18961
19130
  const steps = session.toSession().steps;
@@ -19039,7 +19208,7 @@ async function runRepl(options, initialTask = "") {
19039
19208
  }
19040
19209
  };
19041
19210
  const drainQueuedMessages = () => reader.drainQueuedMessages();
19042
- let autonomousRounds = 1;
19211
+ let autonomousRounds = 5;
19043
19212
  setAskUserRuntime(async (text, options2, signal) => {
19044
19213
  console.log(chalk11.white(`
19045
19214
  ? ${text}`));
@@ -19196,6 +19365,7 @@ async function runRepl(options, initialTask = "") {
19196
19365
  if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
19197
19366
  setAskUserRuntime(void 0);
19198
19367
  setActivitySink(void 0);
19368
+ setWorkingSink(void 0);
19199
19369
  rl.close();
19200
19370
  try {
19201
19371
  await saveTranscript(session, writeCheckpoint);
@@ -19206,16 +19376,22 @@ async function runRepl(options, initialTask = "") {
19206
19376
  }
19207
19377
  console.log(chalk11.gray("Goodbye."));
19208
19378
  }
19209
- async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true) {
19379
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true, activityState) {
19210
19380
  const activity = isGhostEnabled(session) ? void 0 : getActivitySink();
19211
- const entries = [];
19212
- let thinkingMs = 0;
19381
+ const entries = activityState?.entries ?? [];
19382
+ let thinkingMs = activityState?.thinkingMs ?? 0;
19213
19383
  let thinkingStarted;
19214
19384
  let turnFailed = false;
19215
- const refreshActivity = (finished = false) => activity?.({ entries: [...entries], thinkingMs, finished, failed: turnFailed });
19385
+ const refreshActivity = (finished = false) => {
19386
+ if (activityState) {
19387
+ activityState.thinkingMs = thinkingMs;
19388
+ activityState.failed ||= turnFailed;
19389
+ }
19390
+ activity?.({ entries: [...entries], thinkingMs, finished: finished && !activityState, failed: turnFailed });
19391
+ };
19216
19392
  refreshActivity();
19217
- const spinner = new Spinner();
19218
- const workSpinner = new Spinner("working");
19393
+ const spinner = new Spinner("working", activity ? getWorkingSink() : void 0);
19394
+ const workSpinner = new Spinner("working", activity ? getWorkingSink() : void 0);
19219
19395
  const activeTools = /* @__PURE__ */ new Map();
19220
19396
  let permissionPrompts = 0;
19221
19397
  const renderWorking = () => {
@@ -19229,6 +19405,17 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
19229
19405
  const detachProgress = quiet ? () => {
19230
19406
  } : attachProgressUi();
19231
19407
  let deltaChars = 0;
19408
+ const progress2 = quiet ? void 0 : startTaskProgress({
19409
+ snapshot: () => ({
19410
+ completed: entries.length,
19411
+ failed: entries.filter((entry) => !entry.ok).length,
19412
+ activeTool: [...activeTools.keys()].at(-1)?.tool,
19413
+ waitingForModel: thinkingStarted !== void 0
19414
+ }),
19415
+ holdWhile: () => permissionPrompts > 0 || [...activeTools.keys()].some((call) => call.tool === "ask_user"),
19416
+ write: (text) => console.log(`
19417
+ ${text}`)
19418
+ });
19232
19419
  try {
19233
19420
  const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19234
19421
  await session.ask(prepared.text, {
@@ -19247,22 +19434,23 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
19247
19434
  deltaChars = 0;
19248
19435
  thinkingStarted = performance.now();
19249
19436
  if (!quiet) {
19250
- spinner.update("thinking");
19251
- spinner.start(isComposing);
19437
+ spinner.update("working");
19438
+ spinner.start(isComposing, performance.now() - thinkingMs);
19252
19439
  }
19253
19440
  },
19254
19441
  onModelDelta: (text) => {
19255
19442
  deltaChars += text.length;
19256
- if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
19443
+ if (!quiet) spinner.update(`working (${deltaChars} chars)`);
19257
19444
  },
19258
19445
  onModelSettled: () => {
19259
19446
  if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19260
19447
  thinkingStarted = void 0;
19261
- spinner.stop(activity ? void 0 : signal?.aborted ? "Thinking stopped after" : "Thought for");
19448
+ spinner.stop(activity ? void 0 : signal?.aborted ? "Work stopped after" : "Worked for");
19262
19449
  refreshActivity();
19263
19450
  },
19264
19451
  onAssistantMessage: (message) => {
19265
19452
  if (message.trim()) {
19453
+ progress2?.markMessage();
19266
19454
  console.log(`
19267
19455
  ${message.trim()}`);
19268
19456
  }
@@ -19300,7 +19488,7 @@ ${message.trim()}`);
19300
19488
  console.log(chalk11.yellow(` ! ${notice}`));
19301
19489
  },
19302
19490
  onStopped: (reason) => {
19303
- if (reason === "aborted") return;
19491
+ if (reason === "aborted" || reason === "max-steps" && activityState?.continuing) return;
19304
19492
  const line = stopReasonLine(reason);
19305
19493
  const hint = turnStopHint(reason, session.maxSteps);
19306
19494
  if (!line || !hint) return;
@@ -19312,13 +19500,25 @@ ${message.trim()}`);
19312
19500
  permissionPrompts += 1;
19313
19501
  workSpinner.stop();
19314
19502
  try {
19315
- 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 `;
19316
- const rawAnswer = await question(chalk11.yellow(prompt), signal);
19503
+ const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access.
19504
+ 1. [o]nce (this call only)
19505
+ 2. [a]lways (this session)
19506
+ 3. [n]o (this call)
19507
+ 4. n[e]ver (this session)
19508
+ Answer: ` : ` ${call.tool} wants ${permission} access.
19509
+ 1. [o]nce
19510
+ 2. [a]lways (this session)
19511
+ 3. [n]o
19512
+ 4. n[e]ver (this session)
19513
+ Answer: `;
19514
+ const choices = ["Allow once", "Allow for this session", "Deny this call", "Never allow this permission this session"];
19515
+ const asker = getAskUserRuntime();
19516
+ const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal) : await question(chalk11.yellow(prompt), signal);
19317
19517
  if (signal?.aborted || rawAnswer === null) return "deny";
19318
19518
  const answer = rawAnswer.trim().toLowerCase();
19319
- if (answer === "a" || answer === "always") return "always";
19320
- if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
19321
- if (answer === "e" || answer === "never") {
19519
+ if (answer === "a" || answer === "always" || answer === "2" || answer === choices[1].toLowerCase()) return "always";
19520
+ if (answer === "o" || answer === "once" || answer === "y" || answer === "yes" || answer === "1" || answer === choices[0].toLowerCase()) return "once";
19521
+ if (answer === "e" || answer === "never" || answer === "4" || answer === choices[3].toLowerCase()) {
19322
19522
  console.log(
19323
19523
  chalk11.gray(
19324
19524
  ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
@@ -19345,19 +19545,21 @@ ${message.trim()}`);
19345
19545
  activeTools.clear();
19346
19546
  if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19347
19547
  refreshActivity(true);
19548
+ progress2?.stop();
19348
19549
  detachProgress();
19349
19550
  }
19350
19551
  }
19351
19552
  async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1, allowImageDrop = true) {
19553
+ const activityState = { entries: [], thinkingMs: 0, continuing: maxRounds > 1 };
19352
19554
  try {
19353
- if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop);
19555
+ if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
19354
19556
  let successful = true;
19355
19557
  const result = await runAutonomousLoop({
19356
19558
  maxRounds,
19357
19559
  signal,
19358
19560
  classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
19359
19561
  runRound: async (round, _previous, roundSignal) => {
19360
- successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop);
19562
+ successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
19361
19563
  await persist(session);
19362
19564
  const snapshot = session.toSession();
19363
19565
  return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
@@ -19369,6 +19571,7 @@ async function runAndCheckpointTurn(session, input, question, signal, persist, d
19369
19571
  if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
19370
19572
  return successful;
19371
19573
  } finally {
19574
+ if (!isGhostEnabled(session)) getActivitySink()?.({ ...activityState, entries: [...activityState.entries], finished: true });
19372
19575
  await persist(session);
19373
19576
  }
19374
19577
  }
@@ -19628,7 +19831,7 @@ ${extra}`);
19628
19831
  for (const permission of ["write", "shell", "network"]) {
19629
19832
  session.setPermission(permission, argument === "full" || argument === "workspace" && permission === "write");
19630
19833
  }
19631
- console.log("Permissions updated. These are tool grants; shell access is not an operating-system sandbox.");
19834
+ console.log(argument === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : "Permissions updated. Ungranted tools are denied without prompting; /allow restores access.");
19632
19835
  }
19633
19836
  const p = session.currentPermissions;
19634
19837
  console.log(`write: ${p.write} shell: ${p.shell} network: ${p.network}`);
@@ -19919,11 +20122,16 @@ function shellApprovalPrompt(call, workspace) {
19919
20122
  const timeoutMs = typeof call.input.timeoutMs === "number" ? call.input.timeoutMs : 3e4;
19920
20123
  return [
19921
20124
  " run_shell wants shell access.",
19922
- " WARNING: this command runs unsandboxed as your user. It can read or write files available to your account and use your network connection.",
20125
+ " Runs unsandboxed as your user.",
20126
+ " Can read or write your files and use your network connection.",
19923
20127
  ` Command: ${JSON.stringify(command)}`,
19924
20128
  ` Resolved cwd: ${JSON.stringify(resolvedCwd)}`,
19925
20129
  ` Timeout: ${timeoutMs.toLocaleString("en-US")} ms`,
19926
- " [o]nce runs only this command / [a]lways grants shell access for this whole session, including other shell-using tools / [n]o (this call) / n[e]ver (this session) \u203A "
20130
+ " 1. [o]nce runs only this command",
20131
+ " 2. [a]lways grants shell access for this whole session, including other shell-using tools",
20132
+ " 3. [n]o (this call)",
20133
+ " 4. n[e]ver (this session)",
20134
+ " Answer: "
19927
20135
  ].join("\n");
19928
20136
  }
19929
20137
  function indent3(value) {
@@ -19943,7 +20151,7 @@ function parsePositiveInt(value, flag) {
19943
20151
  var EXIT_INCOMPLETE_RUN = 2;
19944
20152
  var NOTIFY_SUMMARY_CHARS = 1200;
19945
20153
  function registerRunCommand(program2) {
19946
- 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(
20154
+ 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("--permissions <mode>", "Tool permission preset: readonly, workspace, or full").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(
19947
20155
  "--allow-incomplete",
19948
20156
  `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.`,
19949
20157
  false
@@ -20060,7 +20268,8 @@ function registerRunCommand(program2) {
20060
20268
  const abortRun = () => controller.abort();
20061
20269
  process.on("SIGINT", abortRun);
20062
20270
  process.on("SIGTERM", abortRun);
20063
- const permissions = autonomous ? { write: true, shell: true, network: true } : {
20271
+ if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
20272
+ const permissions = options.permissions ? { write: options.permissions !== "readonly", shell: options.permissions === "full", network: options.permissions === "full" } : autonomous ? { write: true, shell: true, network: true } : {
20064
20273
  write: options.allowWrite || config.permissions.write,
20065
20274
  shell: options.allowShell || config.permissions.shell,
20066
20275
  network: options.allowNetwork || config.permissions.network
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinalfluid/ultrac",
3
- "version": "0.1.14",
3
+ "version": "0.1.18",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Ultrac terminal agent harness for Cosmic 1, with local tools, browser sign-in, streamed answers, and resumable sessions.",
6
6
  "author": "Aditya Pundir",