nolo-cli 0.1.37 → 0.1.39

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 (52) hide show
  1. package/agent-runtime/agentRecordConfig.ts +0 -14
  2. package/agent-runtime/hostAdapter.ts +0 -1
  3. package/agent-runtime/index.ts +24 -18
  4. package/agent-runtime/localDialogRead.ts +23 -0
  5. package/agent-runtime/localToolPolicy.ts +2 -7
  6. package/agent-runtime/localWorkspaceTools.ts +9 -0
  7. package/agent-runtime/noloWorkspaceTools.ts +371 -0
  8. package/agent-runtime/runtimeToolPolicy.ts +1 -3
  9. package/agent-runtime/types.ts +1 -1
  10. package/agentAliases.ts +6 -0
  11. package/agentRunCommand.ts +57 -65
  12. package/agentRuntimeLocal.ts +0 -7
  13. package/chat/messages/fetchMessages.ts +84 -0
  14. package/chat/messages/types.ts +185 -0
  15. package/cli/agentAliases.ts +6 -0
  16. package/cli/agentRunCommand.ts +57 -65
  17. package/cli/agentRuntimeLocal.ts +0 -7
  18. package/cli/cliEnvHelpers.ts +11 -7
  19. package/cli/client/agentRun.ts +123 -24
  20. package/cli/client/localRuntimeAdapter.ts +87 -59
  21. package/cli/connectorRunArtifact.ts +0 -1
  22. package/cli/dialogCommands.ts +572 -0
  23. package/cli/dialogInternalCommandEntries.ts +4 -0
  24. package/cli/machineWsRunDispatch.ts +29 -37
  25. package/cli/scriptCommandEntries.ts +0 -2
  26. package/cli/tui/readlineWorkspace.ts +69 -0
  27. package/cli/tui/session.ts +176 -1
  28. package/cliEnvHelpers.ts +11 -7
  29. package/client/agentConfigResolver.test.ts +2 -3
  30. package/client/agentRun.test.ts +185 -1
  31. package/client/agentRun.ts +123 -24
  32. package/client/localRuntimeAdapter.test.ts +90 -25
  33. package/client/localRuntimeAdapter.ts +87 -59
  34. package/connectorRunArtifact.ts +0 -1
  35. package/database/server/db.ts +99 -0
  36. package/database/server/ensureDbOpen.ts +12 -0
  37. package/database/server/memoryAuthorityStore.ts +133 -0
  38. package/database/server/serverStoreFactory.ts +118 -0
  39. package/dialogCommands.ts +572 -0
  40. package/dialogInternalCommandEntries.ts +4 -0
  41. package/machineWsRunDispatch.ts +29 -37
  42. package/package.json +10 -9
  43. package/scriptCommandEntries.ts +0 -2
  44. package/tui/readlineWorkspace.ts +69 -0
  45. package/tui/session.ts +176 -1
  46. package/agent-runtime/taskWorkspace.ts +0 -193
  47. package/agent-runtime/workspaceSession.ts +0 -76
  48. package/cli/client/taskWorktree.ts +0 -8
  49. package/cli/client/workspaceSession.ts +0 -11
  50. package/client/taskWorktree.ts +0 -8
  51. package/client/workspaceSession.test.ts +0 -57
  52. package/client/workspaceSession.ts +0 -11
@@ -4,13 +4,7 @@ import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "./agent
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { extname, resolve } from "node:path";
6
6
  import { parseSkillDocProtocol, type WorkflowReferenceConfig } from "../ai/skills/skillDocProtocol";
7
- import { resolveCliAgentKeyInput } from "./agentAliases";
8
- import {
9
- ensureWorkspacePackageLinks,
10
- formatPreparedTaskWorktree,
11
- prepareTaskWorktree,
12
- type PreparedTaskWorktree,
13
- } from "./client/taskWorktree";
7
+ import { MIMO_MONTH_AGENT_KEY, resolveCliAgentKeyInput } from "./agentAliases";
14
8
  import type { TaskRunPromptContext } from "./client/taskRunPrompt";
15
9
 
16
10
  type EnvLike = Record<string, string | undefined>;
@@ -25,8 +19,6 @@ type AgentRunCommandDeps = {
25
19
  output?: OutputLike;
26
20
  runner?: typeof runAgentTurn;
27
21
  localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
28
- prepareTaskWorktree?: typeof prepareTaskWorktree;
29
- ensureWorkspacePackageLinks?: typeof ensureWorkspacePackageLinks;
30
22
  inspectLocalRunWorkspace?: typeof inspectLocalRunWorkspace;
31
23
  resolveWorkflowReference?: typeof resolveWorkflowReference;
32
24
  };
@@ -63,7 +55,7 @@ type ParsedAgentRunArgs = {
63
55
  maxToolRounds?: number;
64
56
  timeoutMs?: number;
65
57
  traceTools: boolean;
66
- useTaskWorktree: boolean;
58
+ injectFeatureWorktreeInstruction: boolean;
67
59
  taskRunContext?: TaskRunPromptContext;
68
60
  workflowRef?: string;
69
61
  };
@@ -102,6 +94,21 @@ function runtimeModeFromArgs(args: string[]): AgentRuntimeRequestedMode | undefi
102
94
  return undefined;
103
95
  }
104
96
 
97
+ function isMonthlyMimoAgentRef(raw: string | undefined, resolved: string) {
98
+ const normalized = raw?.trim().toLowerCase();
99
+ return (
100
+ resolved === MIMO_MONTH_AGENT_KEY ||
101
+ normalized === "fullstack" ||
102
+ normalized === "full-stack" ||
103
+ normalized === "nolo-fullstack" ||
104
+ normalized === "包月mimo" ||
105
+ normalized === "包月mimo2.5" ||
106
+ normalized === "mimo-month" ||
107
+ normalized === "全栈" ||
108
+ normalized === "nolo 全栈工程师"
109
+ );
110
+ }
111
+
105
112
  function parseNonNegativeInteger(value: string | undefined) {
106
113
  if (!value) return undefined;
107
114
  const parsed = Number(value);
@@ -123,7 +130,6 @@ function positionalArgs(args: string[]) {
123
130
  "--server",
124
131
  "--auto",
125
132
  "--dangerously-allow-shell",
126
- "--worktree-task",
127
133
  "--trace-tools",
128
134
  "--bg",
129
135
  "--no-stream",
@@ -146,7 +152,8 @@ export function parseAgentRunArgs(
146
152
  args: string[],
147
153
  options: ParseAgentRunArgsOptions = {}
148
154
  ): ParsedAgentRunArgs | null {
149
- const agentKey = readFlagValue(args, "--agent") ?? positionalArgs(args)[0];
155
+ const rawAgentKey = readFlagValue(args, "--agent") ?? positionalArgs(args)[0];
156
+ const agentKey = rawAgentKey ? resolveCliAgentKeyInput(rawAgentKey) : undefined;
150
157
  const explicitMsg = readFlagValue(args, "--msg");
151
158
  const msgFile = readFlagValue(args, "--msg-file");
152
159
  const fileMessage = msgFile
@@ -178,7 +185,8 @@ export function parseAgentRunArgs(
178
185
  ...readRepeatedFlagValues(args, "--image"),
179
186
  ...readRepeatedFlagValues(args, "--image-url"),
180
187
  ];
181
- const useTaskWorktree = args.includes("--worktree-task");
188
+ const mimoLocalWorkspaceDefault =
189
+ isMonthlyMimoAgentRef(rawAgentKey, agentKey) && runtimeMode !== "server";
182
190
  const workflowRef = readFlagValue(args, "--workflow");
183
191
  const taskRowDbKey = readFlagValue(args, "--task-row-dbkey");
184
192
  const artifactIds = readFlagValue(args, "--artifact-ids")
@@ -186,12 +194,12 @@ export function parseAgentRunArgs(
186
194
  .map((item) => item.trim())
187
195
  .filter(Boolean);
188
196
  return {
189
- agentKey: resolveCliAgentKeyInput(agentKey),
197
+ agentKey,
190
198
  message,
191
199
  imageUrls,
192
- allowShell: args.includes("--dangerously-allow-shell"),
200
+ allowShell: args.includes("--dangerously-allow-shell") || mimoLocalWorkspaceDefault,
193
201
  traceTools: args.includes("--trace-tools"),
194
- useTaskWorktree,
202
+ injectFeatureWorktreeInstruction: mimoLocalWorkspaceDefault,
195
203
  ...(workflowRef ? { workflowRef } : {}),
196
204
  ...(taskRowDbKey
197
205
  ? {
@@ -203,7 +211,11 @@ export function parseAgentRunArgs(
203
211
  },
204
212
  }
205
213
  : {}),
206
- ...(runtimeMode ? { runtimeMode } : useTaskWorktree ? { runtimeMode: "local" as const } : {}),
214
+ ...(runtimeMode
215
+ ? { runtimeMode }
216
+ : mimoLocalWorkspaceDefault || args.includes("--dangerously-allow-shell")
217
+ ? { runtimeMode: "local" as const }
218
+ : {}),
207
219
  background: args.includes("--bg"),
208
220
  noStream: args.includes("--no-stream"),
209
221
  noDefaultTestRoot,
@@ -269,6 +281,20 @@ export function prependWorkflowReferencePrompt(message: string, workflow?: Resol
269
281
  ].join("\n");
270
282
  }
271
283
 
284
+ function prependFeatureWorktreeInstruction(message: string, enabled: boolean) {
285
+ if (!enabled) return message;
286
+ return [
287
+ "Local execution rule:",
288
+ "- You are running in the current git checkout with shell access.",
289
+ "- For read-only checks, smoke tests, or answering questions, stay in the current directory.",
290
+ "- Before developing a new feature or making non-trivial code changes, create a separate git worktree yourself with git worktree and do the edits there.",
291
+ "- Commit and push only when the user explicitly asks or the task requires it.",
292
+ "",
293
+ "User task:",
294
+ message,
295
+ ].join("\n");
296
+ }
297
+
272
298
  function parseDialogReference(rawInput: string) {
273
299
  const normalized = rawInput.trim();
274
300
  if (normalized.startsWith("dialog-")) {
@@ -381,19 +407,13 @@ function resolveServerUrl(env: EnvLike) {
381
407
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
382
408
  }
383
409
 
384
- function shouldPrepareTaskWorktree(parsed: ParsedAgentRunArgs) {
385
- return !parsed.cwd && (parsed.useTaskWorktree || parsed.allowShell);
386
- }
387
-
388
- function buildTaskWorktreeEnv(args: {
410
+ function buildLocalRunEnv(args: {
389
411
  env: EnvLike;
390
- worktreePath?: string;
391
412
  allowShell: boolean;
392
413
  }) {
393
414
  return {
394
415
  ...args.env,
395
416
  ...(args.allowShell ? { NOLO_LOCAL_SHELL_MODE: "worktree" } : {}),
396
- ...(args.worktreePath ? { NOLO_LOCAL_WORKTREE: args.worktreePath } : {}),
397
417
  };
398
418
  }
399
419
 
@@ -426,52 +446,24 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
426
446
  }
427
447
  }
428
448
  let localRuntimeCwd = parsed.cwd;
429
- if (shouldPrepareTaskWorktree(parsed)) {
430
- try {
431
- const preparedWorktree = await (deps.prepareTaskWorktree ?? prepareTaskWorktree)({
432
- agentKey: parsed.agentKey,
433
- env,
434
- });
435
- localRuntimeCwd = preparedWorktree.path;
436
- output.write(formatPreparedTaskWorktree(preparedWorktree));
437
- } catch (error) {
438
- output.write(
439
- "[nolo] Could not prepare a local task worktree.\n" +
440
- "Run from inside a git repository, or set NOLO_LOCAL_WORKTREE to an existing isolated checkout.\n" +
441
- `Reason: ${error instanceof Error ? error.message : String(error)}\n`
442
- );
443
- return 1;
444
- }
449
+ if (!localRuntimeCwd && parsed.runtimeMode === "local") {
450
+ localRuntimeCwd = process.cwd();
445
451
  }
446
- const shouldBindLocalWorkspace =
447
- !!localRuntimeCwd &&
448
- (parsed.runtimeMode === "local" || parsed.allowShell || parsed.useTaskWorktree);
449
- if (shouldBindLocalWorkspace) {
450
- try {
451
- await (deps.ensureWorkspacePackageLinks ?? ensureWorkspacePackageLinks)(
452
- localRuntimeCwd!
453
- );
454
- } catch (error) {
455
- output.write(
456
- "[nolo] Dependency preflight failed for local task worktree.\n" +
457
- "The worktree may have node_modules entries that point outside the task checkout, or paths that cannot be repaired safely.\n" +
458
- `Reason: ${error instanceof Error ? error.message : String(error)}\n`
459
- );
460
- return 1;
461
- }
462
- }
463
- const runEnv = shouldBindLocalWorkspace
464
- ? buildTaskWorktreeEnv({
465
- env,
466
- worktreePath: localRuntimeCwd,
467
- allowShell: parsed.allowShell,
468
- })
469
- : env;
452
+ const runEnv = buildLocalRunEnv({
453
+ env,
454
+ allowShell: parsed.allowShell,
455
+ });
470
456
  const result: RunAgentTurnResult = await runner({
471
457
  agentName: parsed.agentKey,
472
458
  agentKey: parsed.agentKey,
473
459
  serverUrl: resolveServerUrl(env),
474
- message: prependWorkflowReferencePrompt(parsed.message, workflowReference),
460
+ message: prependWorkflowReferencePrompt(
461
+ prependFeatureWorktreeInstruction(
462
+ parsed.message,
463
+ parsed.injectFeatureWorktreeInstruction
464
+ ),
465
+ workflowReference
466
+ ),
475
467
  imageUrls: parsed.imageUrls.map(normalizeCliImageInput),
476
468
  scriptDir: deps.scriptDir,
477
469
  env: runEnv,
@@ -5,12 +5,9 @@ export {
5
5
  hasDirectOpenAiCompatibleProvider,
6
6
  createHybridRecordStore,
7
7
  executeLocalToolWithPolicy,
8
- activateWorkspaceSession,
9
- createWorkspaceSession,
10
8
  buildAgentRuntimeAgentLookupKeys,
11
9
  buildAgentRuntimeDialogWritePlan,
12
10
  dialogMessageRecordToAgentRuntimeMessage,
13
- formatWorkspaceSessionActivation,
14
11
  pickAgentRuntimeInferenceOptions,
15
12
  parseOpenAiCompatibleChatCompletionResponse,
16
13
  parsePlatformChatCompletionData,
@@ -44,8 +41,4 @@ export type {
44
41
  HybridRecordKvDb,
45
42
  HybridRecordStore,
46
43
  LocalToolPolicyDecision,
47
- PreparedTaskWorkspace,
48
- PrepareTaskWorkspace,
49
- WorkspaceSession,
50
- WorkspaceSessionMode,
51
44
  } from "../agent-runtime";
@@ -96,14 +96,18 @@ export function resolveAuthToken(
96
96
  }
97
97
 
98
98
  export function parseUserIdFromAuthToken(token: string) {
99
- const payload = token.trim().split(".")[0];
100
- if (!payload) return "";
101
- try {
102
- const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
103
- return typeof parsed?.userId === "string" ? parsed.userId : "";
104
- } catch {
105
- return "";
99
+ const parts = token.trim().split(".").filter(Boolean);
100
+ const payloadCandidates = parts.length >= 3 ? [parts[1], parts[0]] : [parts[0]];
101
+ for (const payload of payloadCandidates) {
102
+ if (!payload) continue;
103
+ try {
104
+ const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
105
+ if (typeof parsed?.userId === "string") return parsed.userId;
106
+ } catch {
107
+ // Try the next token shape.
108
+ }
106
109
  }
110
+ return "";
107
111
  }
108
112
 
109
113
  export function readOption(args: string[], flag: string) {
@@ -9,7 +9,7 @@ import {
9
9
  } from "../agentAliases";
10
10
  import type { LocalAgentToolEvent } from "../../agent-runtime/localLoop";
11
11
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
12
- import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
12
+ import { createCliLocalRuntimeAdapter, isBuiltinNoloAgentRef } from "./localRuntimeAdapter";
13
13
  import { createStreamingTextWriter } from "./streamingOutput";
14
14
  import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
15
15
 
@@ -73,6 +73,47 @@ export type RunAgentTurnResult = {
73
73
  streamInterrupted?: boolean;
74
74
  };
75
75
 
76
+ class Spinner {
77
+ private timer: any = null;
78
+ private frameIndex = 0;
79
+ private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
80
+ private isTTY: boolean;
81
+
82
+ constructor(
83
+ private output: OutputLike,
84
+ private text: string
85
+ ) {
86
+ this.isTTY = Boolean((output as any).isTTY ?? process.stdout.isTTY);
87
+ }
88
+
89
+ start() {
90
+ if (!this.isTTY) {
91
+ this.output.write(`\n${this.text}...\n`);
92
+ return;
93
+ }
94
+
95
+ this.frameIndex = 0;
96
+ this.output.write(`\x1b[36m${this.frames[0]}\x1b[39m ${this.text}...`);
97
+
98
+ this.timer = setInterval(() => {
99
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
100
+ this.output.write(`\r\x1b[36m${this.frames[this.frameIndex]}\x1b[39m ${this.text}...`);
101
+ }, 80);
102
+ }
103
+
104
+ stop() {
105
+ if (this.timer) {
106
+ clearInterval(this.timer);
107
+ this.timer = null;
108
+ }
109
+
110
+ if (this.isTTY) {
111
+ this.output.write("\r\x1b[K");
112
+ }
113
+ }
114
+ }
115
+
116
+
76
117
  const SERVER_PLATFORM_TOOL_NAMES = new Set([
77
118
  "addTableRow",
78
119
  "addTableRows",
@@ -139,10 +180,27 @@ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
139
180
  const normalizedKey = normalizeAgentRef(options.agentKey);
140
181
  return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
141
182
  }
142
-
143
- function resolveAuthToken(env: EnvLike) {
144
- return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
145
- }
183
+
184
+ function isMachineBoundLocalhostCustomProvider(agentConfig: any) {
185
+ const machineId =
186
+ agentConfig?.runtimeBinding && typeof agentConfig.runtimeBinding === "object"
187
+ ? String(agentConfig.runtimeBinding.machineId ?? "").trim()
188
+ : "";
189
+ const providerUrl = typeof agentConfig?.customProviderUrl === "string"
190
+ ? agentConfig.customProviderUrl.trim()
191
+ : "";
192
+ if (!machineId || !providerUrl) return false;
193
+ try {
194
+ const hostname = new URL(providerUrl).hostname.toLowerCase();
195
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ function resolveAuthToken(env: EnvLike) {
202
+ return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
203
+ }
146
204
 
147
205
  function shouldShowUsage(env: EnvLike) {
148
206
  return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
@@ -173,6 +231,10 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
173
231
  }
174
232
 
175
233
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
234
+ if (isBuiltinNoloAgentRef(options.agentKey)) return false;
235
+ if (options.localRuntimeCwd && options.env.NOLO_LOCAL_SHELL_MODE === "worktree") {
236
+ return false;
237
+ }
176
238
  if (isKnownServerPlatformAgent(options)) {
177
239
  options.output.write(
178
240
  `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
@@ -188,6 +250,13 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
188
250
  } catch {
189
251
  return false;
190
252
  }
253
+ if (isMachineBoundLocalhostCustomProvider(agentConfig)) {
254
+ options.output.write(
255
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a machine-bound localhost custom provider. ` +
256
+ "Use --local explicitly to force the current machine.\n"
257
+ );
258
+ return true;
259
+ }
191
260
  const serverTools = resolveServerPlatformToolNames(agentConfig);
192
261
  if (serverTools.length === 0) return false;
193
262
  options.output.write(
@@ -288,6 +357,13 @@ function formatToolTraceEvent(event: LocalAgentToolEvent) {
288
357
 
289
358
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
290
359
  if (options.localRuntimeAdapter || options.localRuntimeAdapterFactory) return true;
360
+ if (
361
+ options.env.NOLO_DISABLE_CLI_WORKSPACE_TOOLS !== "1" &&
362
+ isBuiltinNoloAgentRef(options.agentKey) &&
363
+ resolveAuthToken(options.env)
364
+ ) {
365
+ return true;
366
+ }
291
367
  return Boolean(
292
368
  options.env.NOLO_LOCAL_OPENAI_API_KEY ||
293
369
  options.env.OPENAI_API_KEY ||
@@ -502,7 +578,8 @@ function buildTransportErrorHint(serverUrl: string, error: unknown) {
502
578
  }
503
579
 
504
580
  async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
505
- options.output.write(`\n${options.agentName} -> working...\n`);
581
+ const spinner = new Spinner(options.output, `${options.agentName} -> working`);
582
+ spinner.start();
506
583
 
507
584
  const fetchImpl = options.fetchImpl ?? fetch;
508
585
  let res: Response;
@@ -556,14 +633,16 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
556
633
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
557
634
  stream: !options.noStream && !options.background,
558
635
  }),
559
- });
560
- } catch (error) {
561
- options.output.write(buildTransportErrorHint(options.serverUrl, error));
562
- return { exitCode: 1 };
563
- }
564
-
636
+ });
637
+ } catch (error) {
638
+ spinner.stop();
639
+ options.output.write(buildTransportErrorHint(options.serverUrl, error));
640
+ return { exitCode: 1 };
641
+ }
642
+
565
643
  const contentType = res.headers.get("content-type") || "";
566
644
  if (contentType.includes("text/event-stream") && res.body) {
645
+ spinner.stop();
567
646
  const result = await readStreamingAgentRun(options, res);
568
647
  await recordTaskRunOutcome({
569
648
  options,
@@ -577,18 +656,32 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
577
656
  });
578
657
  return result;
579
658
  }
580
-
581
- let data: any = {};
582
- try {
583
- data = await res.json();
584
- } catch {
585
- data = {};
586
- }
587
-
659
+
660
+ spinner.stop();
661
+ let data: any = {};
662
+ try {
663
+ data = await res.json();
664
+ } catch {
665
+ data = {};
666
+ }
667
+
588
668
  if (!res.ok) {
589
669
  options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
590
- if (data?.error || data?.message) {
591
- options.output.write(`${data.error || data.message}\n`);
670
+ const errorText = typeof data?.error === "string" ? data.error.trim() : "";
671
+ const messageText = typeof data?.message === "string" ? data.message.trim() : "";
672
+ const reasonText = typeof data?.reason === "string" ? data.reason.trim() : "";
673
+ const codeText = typeof data?.code === "string" ? data.code.trim() : "";
674
+ if (errorText || messageText) {
675
+ options.output.write(`${errorText || messageText}\n`);
676
+ if (messageText && messageText !== errorText) {
677
+ options.output.write(`${messageText}\n`);
678
+ }
679
+ if (codeText && codeText !== errorText && codeText !== messageText) {
680
+ options.output.write(`code=${codeText}\n`);
681
+ }
682
+ if (reasonText && reasonText !== errorText && reasonText !== messageText) {
683
+ options.output.write(`reason=${reasonText}\n`);
684
+ }
592
685
  }
593
686
  await recordTaskRunOutcome({
594
687
  options,
@@ -691,7 +784,8 @@ async function runLocalAgentTurnForCli(
691
784
  return { exitCode: 1 };
692
785
  }
693
786
 
694
- options.output.write(`\n${options.agentName} -> working locally...\n`);
787
+ const spinner = new Spinner(options.output, `${options.agentName} -> working locally`);
788
+ spinner.start();
695
789
  const toolEvents: LocalAgentToolEvent[] = [];
696
790
  try {
697
791
  const traceLocalTools = shouldTraceLocalTools(options);
@@ -716,11 +810,15 @@ async function runLocalAgentTurnForCli(
716
810
  ? {
717
811
  onToolEvent: (event) => {
718
812
  if (collectToolEvidence) toolEvents.push(event);
719
- if (traceLocalTools) options.output.write(formatToolTraceEvent(event));
813
+ if (traceLocalTools) {
814
+ spinner.stop();
815
+ options.output.write(formatToolTraceEvent(event));
816
+ }
720
817
  },
721
818
  }
722
819
  : {}),
723
820
  });
821
+ spinner.stop();
724
822
  const content = result.content.trim();
725
823
  if (content) {
726
824
  options.output.write(`\n${options.agentName} > ${content}\n`);
@@ -736,6 +834,7 @@ async function runLocalAgentTurnForCli(
736
834
  });
737
835
  return { exitCode: 0, dialogId: result.dialogId };
738
836
  } catch (error) {
837
+ spinner.stop();
739
838
  if (settings.reportFailure) {
740
839
  options.output.write(
741
840
  `[nolo] Local agent run failed: ${