@kimbho/kimbho-cli 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3346,7 +3346,7 @@ var {
3346
3346
  // package.json
3347
3347
  var package_default = {
3348
3348
  name: "@kimbho/kimbho-cli",
3349
- version: "0.1.8",
3349
+ version: "0.1.11",
3350
3350
  description: "Kimbho CLI is a terminal-native coding agent for planning, execution, and verification.",
3351
3351
  type: "module",
3352
3352
  engines: {
@@ -7996,6 +7996,178 @@ var SessionSnapshotSchema = external_exports.object({
7996
7996
  events: external_exports.array(SessionEventSchema).default([])
7997
7997
  });
7998
7998
 
7999
+ // ../core/dist/jsonish.js
8000
+ function stripCodeFences(raw) {
8001
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
8002
+ return (fenced?.[1] ?? raw).trim();
8003
+ }
8004
+ function normalizeSmartQuotes(value) {
8005
+ return value.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'");
8006
+ }
8007
+ function maybeUnwrapQuotedJson(value) {
8008
+ const trimmed = value.trim();
8009
+ if (!trimmed.startsWith('"')) {
8010
+ return trimmed;
8011
+ }
8012
+ try {
8013
+ const parsed = JSON.parse(trimmed);
8014
+ return typeof parsed === "string" ? parsed.trim() : trimmed;
8015
+ } catch {
8016
+ return trimmed;
8017
+ }
8018
+ }
8019
+ function findBalancedObject(value) {
8020
+ for (let start = 0; start < value.length; start += 1) {
8021
+ if (value[start] !== "{") {
8022
+ continue;
8023
+ }
8024
+ let depth = 0;
8025
+ let inString = false;
8026
+ let escaping = false;
8027
+ for (let index = start; index < value.length; index += 1) {
8028
+ const character = value[index];
8029
+ if (escaping) {
8030
+ escaping = false;
8031
+ continue;
8032
+ }
8033
+ if (character === "\\") {
8034
+ escaping = true;
8035
+ continue;
8036
+ }
8037
+ if (character === '"') {
8038
+ inString = !inString;
8039
+ continue;
8040
+ }
8041
+ if (inString) {
8042
+ continue;
8043
+ }
8044
+ if (character === "{") {
8045
+ depth += 1;
8046
+ continue;
8047
+ }
8048
+ if (character === "}") {
8049
+ depth -= 1;
8050
+ if (depth === 0) {
8051
+ return value.slice(start, index + 1);
8052
+ }
8053
+ }
8054
+ }
8055
+ }
8056
+ return null;
8057
+ }
8058
+ function removeTrailingCommas(value) {
8059
+ let output = "";
8060
+ let inString = false;
8061
+ let escaping = false;
8062
+ for (let index = 0; index < value.length; index += 1) {
8063
+ const character = value[index];
8064
+ if (escaping) {
8065
+ output += character;
8066
+ escaping = false;
8067
+ continue;
8068
+ }
8069
+ if (character === "\\") {
8070
+ output += character;
8071
+ escaping = true;
8072
+ continue;
8073
+ }
8074
+ if (character === '"') {
8075
+ output += character;
8076
+ inString = !inString;
8077
+ continue;
8078
+ }
8079
+ if (!inString && character === ",") {
8080
+ let lookahead = index + 1;
8081
+ while (lookahead < value.length && /\s/.test(value[lookahead] ?? "")) {
8082
+ lookahead += 1;
8083
+ }
8084
+ if (value[lookahead] === "}" || value[lookahead] === "]") {
8085
+ continue;
8086
+ }
8087
+ }
8088
+ output += character;
8089
+ }
8090
+ return output;
8091
+ }
8092
+ function escapeControlCharsInStrings(value) {
8093
+ let output = "";
8094
+ let inString = false;
8095
+ let escaping = false;
8096
+ for (const character of value) {
8097
+ if (escaping) {
8098
+ output += character;
8099
+ escaping = false;
8100
+ continue;
8101
+ }
8102
+ if (character === "\\") {
8103
+ output += character;
8104
+ escaping = true;
8105
+ continue;
8106
+ }
8107
+ if (character === '"') {
8108
+ output += character;
8109
+ inString = !inString;
8110
+ continue;
8111
+ }
8112
+ if (inString && character === "\n") {
8113
+ output += "\\n";
8114
+ continue;
8115
+ }
8116
+ if (inString && character === "\r") {
8117
+ output += "\\r";
8118
+ continue;
8119
+ }
8120
+ if (inString && character === " ") {
8121
+ output += "\\t";
8122
+ continue;
8123
+ }
8124
+ output += character;
8125
+ }
8126
+ return output;
8127
+ }
8128
+ function parseCandidate(candidate) {
8129
+ return JSON.parse(candidate);
8130
+ }
8131
+ function extractJsonObjectish(raw, context) {
8132
+ const stripped = maybeUnwrapQuotedJson(normalizeSmartQuotes(stripCodeFences(raw)));
8133
+ const extracted = findBalancedObject(stripped) ?? stripped;
8134
+ const candidates = Array.from(new Set([
8135
+ stripped,
8136
+ extracted,
8137
+ removeTrailingCommas(extracted),
8138
+ escapeControlCharsInStrings(removeTrailingCommas(extracted))
8139
+ ].filter((candidate) => candidate.trim().length > 0)));
8140
+ let lastError;
8141
+ for (const candidate of candidates) {
8142
+ try {
8143
+ const parsed = parseCandidate(candidate);
8144
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
8145
+ throw new Error(`Expected a JSON object in the ${context}.`);
8146
+ }
8147
+ return parsed;
8148
+ } catch (error) {
8149
+ lastError = error;
8150
+ }
8151
+ }
8152
+ if (lastError instanceof Error) {
8153
+ throw lastError;
8154
+ }
8155
+ throw new Error(`Expected a JSON object in the ${context}.`);
8156
+ }
8157
+ function summarizeStructuredOutputError(context, error) {
8158
+ const message = error instanceof Error ? error.message : String(error);
8159
+ if (/Expected a JSON object/i.test(message)) {
8160
+ return `${context} returned non-machine-readable output.`;
8161
+ }
8162
+ if (/Unexpected end of JSON input/i.test(message)) {
8163
+ return `${context} returned truncated JSON.`;
8164
+ }
8165
+ if (/JSON|property|position/i.test(message)) {
8166
+ return `${context} returned malformed JSON.`;
8167
+ }
8168
+ return `${context} returned invalid structured output.`;
8169
+ }
8170
+
7999
8171
  // ../core/dist/config/config.js
8000
8172
  var import_promises = require("node:fs/promises");
8001
8173
  var import_node_path = __toESM(require("node:path"), 1);
@@ -8415,6 +8587,10 @@ async function requestJson(url, init, timeoutMs = 3e4) {
8415
8587
  }
8416
8588
  return response.json();
8417
8589
  }
8590
+ function isBadRequestError(error) {
8591
+ const message = error instanceof Error ? error.message : String(error);
8592
+ return /Request failed with 400\b/i.test(message);
8593
+ }
8418
8594
  function trimTrailingSlash2(value) {
8419
8595
  return value.replace(/\/+$/, "");
8420
8596
  }
@@ -8796,20 +8972,39 @@ var OpenAICompatibleProvider = class {
8796
8972
  }
8797
8973
  const apiKey = resolveApiKey(this.definition);
8798
8974
  const model = requireModel(this.definition, input);
8799
- const response = await requestJson(`${this.definition.baseUrl}/chat/completions`, {
8800
- method: "POST",
8801
- headers: buildProviderHeaders(this.definition, apiKey),
8802
- body: JSON.stringify({
8803
- model,
8804
- messages: toChatMessages(input),
8805
- ...typeof input.temperature === "number" ? {
8806
- temperature: input.temperature
8807
- } : {},
8808
- ...typeof input.maxTokens === "number" ? {
8809
- max_tokens: input.maxTokens
8810
- } : {}
8811
- })
8812
- }, GENERATION_TIMEOUT_MS);
8975
+ const requestBody = {
8976
+ model,
8977
+ messages: toChatMessages(input),
8978
+ ...input.responseFormat === "json_object" ? {
8979
+ response_format: {
8980
+ type: "json_object"
8981
+ }
8982
+ } : {},
8983
+ ...typeof input.temperature === "number" ? {
8984
+ temperature: input.temperature
8985
+ } : {},
8986
+ ...typeof input.maxTokens === "number" ? {
8987
+ max_tokens: input.maxTokens
8988
+ } : {}
8989
+ };
8990
+ let response;
8991
+ try {
8992
+ response = await requestJson(`${this.definition.baseUrl}/chat/completions`, {
8993
+ method: "POST",
8994
+ headers: buildProviderHeaders(this.definition, apiKey),
8995
+ body: JSON.stringify(requestBody)
8996
+ }, GENERATION_TIMEOUT_MS);
8997
+ } catch (error) {
8998
+ if (!input.responseFormat || !isBadRequestError(error)) {
8999
+ throw error;
9000
+ }
9001
+ const { response_format: _ignored, ...fallbackBody } = requestBody;
9002
+ response = await requestJson(`${this.definition.baseUrl}/chat/completions`, {
9003
+ method: "POST",
9004
+ headers: buildProviderHeaders(this.definition, apiKey),
9005
+ body: JSON.stringify(fallbackBody)
9006
+ }, GENERATION_TIMEOUT_MS);
9007
+ }
8813
9008
  const choices = Array.isArray(response.choices) ? response.choices : [];
8814
9009
  const firstChoice = choices.length > 0 && typeof choices[0] === "object" && choices[0] !== null ? choices[0] : null;
8815
9010
  const message = firstChoice && typeof firstChoice.message === "object" && firstChoice.message !== null ? firstChoice.message : null;
@@ -8852,26 +9047,46 @@ var OllamaProvider = class {
8852
9047
  async generateText(input) {
8853
9048
  const baseUrl = this.definition.baseUrl ?? "http://localhost:11434";
8854
9049
  const model = requireModel(this.definition, input);
8855
- const response = await requestJson(`${baseUrl}/api/generate`, {
8856
- method: "POST",
8857
- headers: {
8858
- "content-type": "application/json",
8859
- ...this.definition.headers
8860
- },
8861
- body: JSON.stringify({
8862
- model,
8863
- prompt: input.userPrompt ?? toPromptText(input),
8864
- ...input.systemPrompt ? {
8865
- system: input.systemPrompt
8866
- } : {},
8867
- stream: false,
8868
- ...typeof input.temperature === "number" ? {
8869
- options: {
8870
- temperature: input.temperature
8871
- }
8872
- } : {}
8873
- })
8874
- }, GENERATION_TIMEOUT_MS);
9050
+ const requestBody = {
9051
+ model,
9052
+ prompt: input.userPrompt ?? toPromptText(input),
9053
+ ...input.systemPrompt ? {
9054
+ system: input.systemPrompt
9055
+ } : {},
9056
+ stream: false,
9057
+ ...input.responseFormat === "json_object" ? {
9058
+ format: "json"
9059
+ } : {},
9060
+ ...typeof input.temperature === "number" ? {
9061
+ options: {
9062
+ temperature: input.temperature
9063
+ }
9064
+ } : {}
9065
+ };
9066
+ let response;
9067
+ try {
9068
+ response = await requestJson(`${baseUrl}/api/generate`, {
9069
+ method: "POST",
9070
+ headers: {
9071
+ "content-type": "application/json",
9072
+ ...this.definition.headers
9073
+ },
9074
+ body: JSON.stringify(requestBody)
9075
+ }, GENERATION_TIMEOUT_MS);
9076
+ } catch (error) {
9077
+ if (!input.responseFormat || !isBadRequestError(error)) {
9078
+ throw error;
9079
+ }
9080
+ const { format: _ignored, ...fallbackBody } = requestBody;
9081
+ response = await requestJson(`${baseUrl}/api/generate`, {
9082
+ method: "POST",
9083
+ headers: {
9084
+ "content-type": "application/json",
9085
+ ...this.definition.headers
9086
+ },
9087
+ body: JSON.stringify(fallbackBody)
9088
+ }, GENERATION_TIMEOUT_MS);
9089
+ }
8875
9090
  const usage = extractUsage(response);
8876
9091
  return usage ? {
8877
9092
  text: typeof response.response === "string" ? response.response : "",
@@ -10475,7 +10690,7 @@ async function generateNextPrisma(cwd, projectName) {
10475
10690
  ]
10476
10691
  }),
10477
10692
  "next-env.d.ts": '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n',
10478
- "next.config.js": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n",
10693
+ "next.config.mjs": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n",
10479
10694
  ".env.example": 'DATABASE_URL="postgresql://postgres:postgres@localhost:5432/app"\n',
10480
10695
  "prisma/schema.prisma": [
10481
10696
  "generator client {",
@@ -10507,6 +10722,8 @@ async function generateNextPrisma(cwd, projectName) {
10507
10722
  "}"
10508
10723
  ].join("\n"),
10509
10724
  "src/app/page.tsx": [
10725
+ `const title = "${title}";`,
10726
+ "",
10510
10727
  "export default function HomePage() {",
10511
10728
  " return (",
10512
10729
  " <main style={{ fontFamily: 'Georgia, serif', padding: '4rem 1.5rem', maxWidth: 960, margin: '0 auto' }}>",
@@ -10873,6 +11090,29 @@ var READ_ONLY_SHELL_PREFIXES = [
10873
11090
  "pnpm ls",
10874
11091
  "yarn list"
10875
11092
  ];
11093
+ var VERIFICATION_SHELL_PREFIXES = [
11094
+ "npm test",
11095
+ "npm run test",
11096
+ "npm run build",
11097
+ "npm run lint",
11098
+ "pnpm test",
11099
+ "pnpm run test",
11100
+ "pnpm build",
11101
+ "pnpm run build",
11102
+ "pnpm lint",
11103
+ "pnpm run lint",
11104
+ "yarn test",
11105
+ "yarn build",
11106
+ "yarn lint",
11107
+ "bun test",
11108
+ "bun run test",
11109
+ "bun run build",
11110
+ "bun run lint",
11111
+ "tsc",
11112
+ "vitest",
11113
+ "jest",
11114
+ "next build"
11115
+ ];
10876
11116
  var DESTRUCTIVE_SHELL_PATTERNS = [
10877
11117
  /\brm\s+-rf\b/i,
10878
11118
  /\brm\s+-fr\b/i,
@@ -10894,6 +11134,10 @@ function isReadOnlyShellCommand(command) {
10894
11134
  const normalized = command.trim().toLowerCase();
10895
11135
  return READ_ONLY_SHELL_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(prefix));
10896
11136
  }
11137
+ function isVerificationShellCommand(command) {
11138
+ const normalized = command.trim().toLowerCase();
11139
+ return VERIFICATION_SHELL_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix} `));
11140
+ }
10897
11141
  function isDestructiveShellCommand(command) {
10898
11142
  return DESTRUCTIVE_SHELL_PATTERNS.some((pattern) => pattern.test(command));
10899
11143
  }
@@ -10963,7 +11207,7 @@ function enforceToolPolicy(toolId, input, descriptor, context) {
10963
11207
  const approvalMode = context.approvalMode ?? "manual";
10964
11208
  const command = typeof input.command === "string" ? input.command : "";
10965
11209
  if (sandboxMode === "read-only") {
10966
- if (toolId === "shell.exec" && isReadOnlyShellCommand(command)) {
11210
+ if (toolId === "shell.exec" && (isReadOnlyShellCommand(command) || isVerificationShellCommand(command))) {
10967
11211
  return null;
10968
11212
  }
10969
11213
  if (descriptor.permission !== "safe") {
@@ -10975,6 +11219,9 @@ function enforceToolPolicy(toolId, input, descriptor, context) {
10975
11219
  return new ToolApprovalRequiredError(createApprovalRequest(toolId, input, descriptor, context, `Approval required for destructive shell command: ${command}`));
10976
11220
  }
10977
11221
  }
11222
+ if (toolId === "shell.exec" && isVerificationShellCommand(command)) {
11223
+ return null;
11224
+ }
10978
11225
  if ((toolId === "file.write" || toolId === "file.patch") && sandboxMode === "workspace-write") {
10979
11226
  const targets = extractProspectiveWriteTargets(toolId, input, context.cwd);
10980
11227
  const protectedTargets = targets.filter((target) => isProtectedWritePath(target));
@@ -11884,16 +12131,6 @@ function truncateForModel(value) {
11884
12131
  return `${value.slice(0, MAX_TOOL_OUTPUT_CHARS)}
11885
12132
  ... [truncated]`;
11886
12133
  }
11887
- function extractJsonObject(raw) {
11888
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
11889
- const candidate = (fenced?.[1] ?? raw).trim();
11890
- const firstBrace = candidate.indexOf("{");
11891
- const lastBrace = candidate.lastIndexOf("}");
11892
- if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
11893
- throw new Error("Expected a JSON object in the model response.");
11894
- }
11895
- return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
11896
- }
11897
12134
  function renderToolResultForModel(result) {
11898
12135
  const sections = [
11899
12136
  `tool: ${result.toolId}`,
@@ -12009,6 +12246,7 @@ function buildSystemPrompt(agent, task, request, allowedTools, plan) {
12009
12246
  `- Use browser.open, browser.inspect, browser.click, browser.fill, and browser.close when a web UI needs browser-level verification.`,
12010
12247
  `- Use process.start/process.logs/process.stop for long-running dev servers or watchers when they are available.`,
12011
12248
  `- Keep paths relative to the workspace.`,
12249
+ `- In an existing repo, inspect and change the likely source files before running build or test verification. Do not front-load verification unless you are confirming an already-completed change or diagnosing a failure.`,
12012
12250
  `- After changing code, run verification with tests.run or shell.exec when appropriate.`,
12013
12251
  `- Do not claim success unless the task acceptance criteria are satisfied.`,
12014
12252
  `- If the task is underspecified, make a pragmatic implementation choice and continue.`
@@ -12061,6 +12299,9 @@ function summarizeSavedSessionContext(sessionId, session) {
12061
12299
  ...notes
12062
12300
  ];
12063
12301
  }
12302
+ function summarizeModelActionError(error) {
12303
+ return summarizeStructuredOutputError("Model action", error);
12304
+ }
12064
12305
  function buildInitialUserPrompt(task, request) {
12065
12306
  return [
12066
12307
  `Complete this task in the workspace.`,
@@ -12071,6 +12312,114 @@ function buildInitialUserPrompt(task, request) {
12071
12312
  `Choose the next single action now.`
12072
12313
  ].join("\n");
12073
12314
  }
12315
+ function extractCodeFences(raw) {
12316
+ const matches = Array.from(raw.matchAll(/```([a-z0-9_-]*)\s*\n([\s\S]*?)```/gi));
12317
+ return matches.map((match) => ({
12318
+ language: (match[1] ?? "").trim().toLowerCase(),
12319
+ code: (match[2] ?? "").trim()
12320
+ })).filter((match) => match.code.length > 0);
12321
+ }
12322
+ function normalizeWritableTaskFiles(task) {
12323
+ return Array.from(new Set(task.filesLikelyTouched.map((filePath) => filePath.trim()).filter((filePath) => filePath.length > 0 && !filePath.endsWith("/") && !filePath.includes("*") && !filePath.startsWith(".kimbho/"))));
12324
+ }
12325
+ function extractMentionedFilePaths(raw, task) {
12326
+ const mentioned = /* @__PURE__ */ new Set();
12327
+ const normalizedTaskFiles = normalizeWritableTaskFiles(task);
12328
+ const directMatches = raw.match(/([A-Za-z0-9_./-]+\.(?:tsx|ts|jsx|js|css|html|md|json|sql|prisma))/g) ?? [];
12329
+ for (const match of directMatches) {
12330
+ mentioned.add(match);
12331
+ }
12332
+ for (const candidate of normalizedTaskFiles) {
12333
+ const basename = import_node_path9.default.basename(candidate);
12334
+ if (raw.includes(candidate) || raw.includes(basename)) {
12335
+ mentioned.add(candidate);
12336
+ }
12337
+ }
12338
+ return Array.from(mentioned);
12339
+ }
12340
+ function inferFallbackFilePath(raw, task, fence) {
12341
+ const mentioned = extractMentionedFilePaths(raw, task);
12342
+ if (mentioned.length === 1) {
12343
+ return mentioned[0] ?? null;
12344
+ }
12345
+ const writableFiles = normalizeWritableTaskFiles(task);
12346
+ const sourceFiles = writableFiles.filter((filePath) => /\.[a-z0-9]+$/i.test(filePath));
12347
+ if (sourceFiles.length === 1) {
12348
+ return sourceFiles[0] ?? null;
12349
+ }
12350
+ if (fence.language === "html") {
12351
+ return sourceFiles.find((filePath) => filePath.endsWith(".html")) ?? null;
12352
+ }
12353
+ if (fence.language === "css") {
12354
+ return sourceFiles.find((filePath) => filePath.endsWith(".css")) ?? null;
12355
+ }
12356
+ if ([
12357
+ "tsx",
12358
+ "ts",
12359
+ "jsx",
12360
+ "js",
12361
+ "typescript",
12362
+ "javascript"
12363
+ ].includes(fence.language)) {
12364
+ return sourceFiles.find((filePath) => /\.(tsx?|jsx?)$/i.test(filePath)) ?? null;
12365
+ }
12366
+ if (fence.language === "json") {
12367
+ return sourceFiles.find((filePath) => filePath.endsWith(".json")) ?? null;
12368
+ }
12369
+ return sourceFiles[0] ?? null;
12370
+ }
12371
+ function extractCommandFromResponse(raw) {
12372
+ const trimmed = raw.trim();
12373
+ if (/^(npm|pnpm|yarn|bun|npx|tsx|tsc|vitest|jest|next)\b/i.test(trimmed) && !trimmed.includes("\n")) {
12374
+ return trimmed;
12375
+ }
12376
+ const bashFence = extractCodeFences(raw).find((fence) => [
12377
+ "bash",
12378
+ "sh",
12379
+ "shell",
12380
+ "zsh"
12381
+ ].includes(fence.language));
12382
+ if (!bashFence) {
12383
+ return null;
12384
+ }
12385
+ const lines = bashFence.code.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
12386
+ return lines.length === 1 ? lines[0] ?? null : null;
12387
+ }
12388
+ function inferFallbackActionFromResponse(raw, task, allowedTools) {
12389
+ if (allowedTools.includes("shell.exec")) {
12390
+ const command = extractCommandFromResponse(raw);
12391
+ if (command) {
12392
+ return {
12393
+ type: "tool",
12394
+ tool: "shell.exec",
12395
+ input: {
12396
+ command
12397
+ },
12398
+ reason: "Run the command inferred from the model response."
12399
+ };
12400
+ }
12401
+ }
12402
+ if (!allowedTools.includes("file.write")) {
12403
+ return null;
12404
+ }
12405
+ const fences = extractCodeFences(raw);
12406
+ for (const fence of fences) {
12407
+ const filePath = inferFallbackFilePath(raw, task, fence);
12408
+ if (!filePath) {
12409
+ continue;
12410
+ }
12411
+ return {
12412
+ type: "tool",
12413
+ tool: "file.write",
12414
+ input: {
12415
+ path: filePath,
12416
+ content: fence.code
12417
+ },
12418
+ reason: `Apply the drafted contents for ${filePath} inferred from the model response.`
12419
+ };
12420
+ }
12421
+ return null;
12422
+ }
12074
12423
  function buildToolResultUserMessage(step, result) {
12075
12424
  return [
12076
12425
  `Step ${step} tool result:`,
@@ -12568,17 +12917,44 @@ var AutonomousTaskExecutor = class {
12568
12917
  let responseText = "";
12569
12918
  let parsedAction = null;
12570
12919
  for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt += 1) {
12571
- const response = await brain.client.generateText({
12572
- model: brain.model,
12573
- systemPrompt: buildSystemPrompt(agent, task, request, allowedTools, options.plan),
12574
- messages,
12575
- ...typeof brain.settings.temperature === "number" ? {
12576
- temperature: brain.settings.temperature
12577
- } : {},
12578
- ...typeof brain.settings.maxTokens === "number" ? {
12579
- maxTokens: brain.settings.maxTokens
12580
- } : {}
12920
+ await emitProgress({
12921
+ type: "task-note",
12922
+ sessionId,
12923
+ taskId: task.id,
12924
+ agentRole: task.agentRole,
12925
+ step,
12926
+ message: attempt === 0 ? "Thinking through the next safe action." : `Re-asking the model for a stricter machine-readable action (${attempt + 1}/${MAX_PARSE_RETRIES + 1}).`
12581
12927
  });
12928
+ let response;
12929
+ try {
12930
+ response = await brain.client.generateText({
12931
+ model: brain.model,
12932
+ systemPrompt: buildSystemPrompt(agent, task, request, allowedTools, options.plan),
12933
+ messages,
12934
+ responseFormat: "json_object",
12935
+ ...typeof brain.settings.temperature === "number" ? {
12936
+ temperature: brain.settings.temperature
12937
+ } : {},
12938
+ ...typeof brain.settings.maxTokens === "number" ? {
12939
+ maxTokens: brain.settings.maxTokens
12940
+ } : {}
12941
+ });
12942
+ } catch (error) {
12943
+ transcript.push({
12944
+ step,
12945
+ response: "",
12946
+ runtimeNote: `Model request failed: ${error instanceof Error ? error.message : String(error)}`
12947
+ });
12948
+ const transcriptPath2 = await writeTranscriptArtifact(request.cwd, sessionId, task.id, transcript);
12949
+ artifacts.add(transcriptPath2);
12950
+ return {
12951
+ status: "blocked",
12952
+ summary: `Model request failed before ${task.id} could choose a safe action: ${error instanceof Error ? error.message : String(error)}`,
12953
+ toolResults,
12954
+ artifacts: Array.from(artifacts),
12955
+ usage: usageTotals
12956
+ };
12957
+ }
12582
12958
  if (options.signal?.aborted) {
12583
12959
  const transcriptPath2 = await writeTranscriptArtifact(request.cwd, sessionId, task.id, transcript);
12584
12960
  artifacts.add(transcriptPath2);
@@ -12607,11 +12983,46 @@ var AutonomousTaskExecutor = class {
12607
12983
  } : {}
12608
12984
  });
12609
12985
  try {
12610
- parsedAction = actionSchema.parse(extractJsonObject(response.text));
12986
+ parsedAction = actionSchema.parse(extractJsonObjectish(response.text, "model response"));
12611
12987
  break;
12612
12988
  } catch (error) {
12989
+ const parseSummary = summarizeModelActionError(error);
12990
+ transcript.push({
12991
+ step,
12992
+ response: response.text,
12993
+ runtimeNote: `${parseSummary} ${error instanceof Error ? error.message : String(error)}`
12994
+ });
12613
12995
  if (attempt === MAX_PARSE_RETRIES) {
12614
- throw error;
12996
+ const inferredFallbackAction = inferFallbackActionFromResponse(response.text, task, allowedTools);
12997
+ if (inferredFallbackAction) {
12998
+ parsedAction = inferredFallbackAction;
12999
+ await emitProgress({
13000
+ type: "task-note",
13001
+ sessionId,
13002
+ taskId: task.id,
13003
+ agentRole: task.agentRole,
13004
+ step,
13005
+ message: `Model stayed out of structured mode; inferred ${inferredFallbackAction.tool} from the response.`
13006
+ });
13007
+ break;
13008
+ }
13009
+ const transcriptPath2 = await writeTranscriptArtifact(request.cwd, sessionId, task.id, transcript);
13010
+ artifacts.add(transcriptPath2);
13011
+ await emitProgress({
13012
+ type: "task-note",
13013
+ sessionId,
13014
+ taskId: task.id,
13015
+ agentRole: task.agentRole,
13016
+ step,
13017
+ message: "Model stayed out of structured mode after multiple retries."
13018
+ });
13019
+ return {
13020
+ status: "blocked",
13021
+ summary: `${parseSummary} The task stopped before a safe tool action could be chosen.`,
13022
+ toolResults,
13023
+ artifacts: Array.from(artifacts),
13024
+ usage: usageTotals
13025
+ };
12615
13026
  }
12616
13027
  await emitProgress({
12617
13028
  type: "task-note",
@@ -12619,7 +13030,7 @@ var AutonomousTaskExecutor = class {
12619
13030
  taskId: task.id,
12620
13031
  agentRole: task.agentRole,
12621
13032
  step,
12622
- message: `Model response was invalid JSON; retrying parse (${attempt + 1}/${MAX_PARSE_RETRIES + 1}).`
13033
+ message: `Reformatting model output to match Kimbho's action schema (${attempt + 1}/${MAX_PARSE_RETRIES + 1}).`
12623
13034
  });
12624
13035
  messages.push({
12625
13036
  role: "assistant",
@@ -12628,9 +13039,11 @@ var AutonomousTaskExecutor = class {
12628
13039
  messages.push({
12629
13040
  role: "user",
12630
13041
  content: [
12631
- "Your previous response was invalid.",
13042
+ "Your previous response did not parse as a valid JSON object.",
13043
+ parseSummary,
12632
13044
  error instanceof Error ? error.message : String(error),
12633
- "Return exactly one valid JSON object matching the required action schema."
13045
+ "Return exactly one valid JSON object matching the required action schema.",
13046
+ "Do not include prose, markdown fences, comments, or trailing explanations."
12634
13047
  ].join("\n")
12635
13048
  });
12636
13049
  }
@@ -12850,16 +13263,27 @@ function foundationMilestone(shape) {
12850
13263
  "docs/",
12851
13264
  "packages/"
12852
13265
  ], "medium"),
12853
- buildTask("t3-scaffold", "Scaffold the repository foundation", "Create the workspace layout, TypeScript build config, package manifests, and initial CLI entry points.", shape === "cli-agent" ? "backend-specialist" : "infra-specialist", "scaffold", [
13266
+ buildTask("t3-scaffold", "Scaffold the repository foundation", shape === "static-site" ? "Create or adapt the primary landing-page entrypoint, styles, and workspace shell needed for the requested site." : "Create the workspace layout, TypeScript build config, package manifests, and initial CLI entry points.", shape === "cli-agent" ? "backend-specialist" : "infra-specialist", "scaffold", [
12854
13267
  "t2-architecture"
12855
13268
  ], [
12856
- "The monorepo or app workspace builds cleanly.",
12857
- "Core commands or entry points exist."
12858
- ], [
12859
- "package manifests",
12860
- "TypeScript config",
12861
- "CLI or app shell"
13269
+ shape === "static-site" ? "The primary page entry exists and renders the requested brand direction." : "The monorepo or app workspace builds cleanly.",
13270
+ shape === "static-site" ? "Core layout files or styles exist for the landing page." : "Core commands or entry points exist."
12862
13271
  ], [
13272
+ ...shape === "static-site" ? [
13273
+ "Page entrypoint",
13274
+ "Stylesheet or layout shell"
13275
+ ] : [
13276
+ "package manifests",
13277
+ "TypeScript config",
13278
+ "CLI or app shell"
13279
+ ]
13280
+ ], shape === "static-site" ? [
13281
+ "src/app/page.tsx",
13282
+ "src/app/layout.tsx",
13283
+ "src/pages/index.tsx",
13284
+ "index.html",
13285
+ "styles.css"
13286
+ ] : [
12863
13287
  "package.json",
12864
13288
  "packages/",
12865
13289
  "src/"
@@ -12882,6 +13306,9 @@ function implementationMilestone(shape) {
12882
13306
  ], [
12883
13307
  "Customized landing page"
12884
13308
  ], [
13309
+ "src/app/page.tsx",
13310
+ "src/app/layout.tsx",
13311
+ "src/pages/index.tsx",
12885
13312
  "index.html",
12886
13313
  "styles.css",
12887
13314
  "src/"
@@ -13147,16 +13574,6 @@ function createPlan(input) {
13147
13574
  var DEFAULT_PLANNER_TEMPERATURE = 0.1;
13148
13575
  var DEFAULT_PLANNER_MAX_TOKENS = 2400;
13149
13576
  var MAX_REPLAN_CONTEXT_CHARS = 8e3;
13150
- function extractJsonObject2(raw) {
13151
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
13152
- const candidate = (fenced?.[1] ?? raw).trim();
13153
- const firstBrace = candidate.indexOf("{");
13154
- const lastBrace = candidate.lastIndexOf("}");
13155
- if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
13156
- throw new Error("Expected a JSON object in the planner response.");
13157
- }
13158
- return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
13159
- }
13160
13577
  function validateTaskGraph(plan) {
13161
13578
  const taskIds = /* @__PURE__ */ new Set();
13162
13579
  for (const task of flattenPlanTasks(plan)) {
@@ -13300,7 +13717,7 @@ async function generatePlannerPlan(request, seedPlan, invocation, userPrompt, pr
13300
13717
  temperature: invocation.temperature ?? DEFAULT_PLANNER_TEMPERATURE,
13301
13718
  maxTokens: invocation.maxTokens ?? DEFAULT_PLANNER_MAX_TOKENS
13302
13719
  });
13303
- const parsed = extractJsonObject2(response.text);
13720
+ const parsed = extractJsonObjectish(response.text, "planner response");
13304
13721
  const candidatePlan = KimbhoPlanSchema.parse(parsed);
13305
13722
  const normalized = normalizeCandidatePlan(request, seedPlan, candidatePlan, true, preserveStatusesFrom);
13306
13723
  return {
@@ -13318,7 +13735,7 @@ async function generatePlannerPlan(request, seedPlan, invocation, userPrompt, pr
13318
13735
  return {
13319
13736
  plan: preserveStatusesFrom ? preserveTaskStatuses(preserveStatusesFrom, seedPlan) : seedPlan,
13320
13737
  source: "fallback",
13321
- warning: error instanceof Error ? error.message : String(error),
13738
+ warning: summarizeStructuredOutputError("Planner", error),
13322
13739
  ...invocation.modelLabel ? {
13323
13740
  modelLabel: invocation.modelLabel
13324
13741
  } : {}
@@ -13361,16 +13778,6 @@ function truncate2(value, maxChars = MAX_EXPANSION_CONTEXT_CHARS) {
13361
13778
  return `${value.slice(0, maxChars)}
13362
13779
  ... [truncated]`;
13363
13780
  }
13364
- function extractJsonObject3(raw) {
13365
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
13366
- const candidate = (fenced?.[1] ?? raw).trim();
13367
- const firstBrace = candidate.indexOf("{");
13368
- const lastBrace = candidate.lastIndexOf("}");
13369
- if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
13370
- throw new Error("Expected a JSON object in the task expansion response.");
13371
- }
13372
- return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
13373
- }
13374
13781
  function slugify(value) {
13375
13782
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
13376
13783
  }
@@ -13676,7 +14083,7 @@ async function generateExpandedTasks(request, plan, task, invocation, repoSummar
13676
14083
  temperature: invocation.temperature ?? DEFAULT_EXPANDER_TEMPERATURE,
13677
14084
  maxTokens: invocation.maxTokens ?? DEFAULT_EXPANDER_MAX_TOKENS
13678
14085
  });
13679
- const parsed = extractJsonObject3(response.text);
14086
+ const parsed = extractJsonObjectish(response.text, "task expansion response");
13680
14087
  const payload = TaskExpansionPayloadSchema.parse(parsed);
13681
14088
  const normalizedTasks = normalizeExpandedTasks(plan, task, payload, "model");
13682
14089
  const note = `Planner expanded ${task.id} into ${normalizedTasks.length} subtasks via ${invocation.modelLabel ?? "planner model"}: ${normalizedTasks.map((candidate) => candidate.id).join(", ")}.`;
@@ -13715,7 +14122,7 @@ async function generateExpandedTasks(request, plan, task, invocation, repoSummar
13715
14122
  expandedTaskId: task.id,
13716
14123
  createdTaskIds: fallbackTasks.map((candidate) => candidate.id),
13717
14124
  note,
13718
- warning: error instanceof Error ? error.message : String(error),
14125
+ warning: summarizeStructuredOutputError("Planner task expansion", error),
13719
14126
  ...invocation.modelLabel ? {
13720
14127
  modelLabel: invocation.modelLabel
13721
14128
  } : {}
@@ -15446,14 +15853,25 @@ var ExecutionOrchestrator = class {
15446
15853
  async executeRepoAnalysisTask(sessionId, task, request, emitProgress, signal) {
15447
15854
  const context = { cwd: request.cwd };
15448
15855
  const toolResults = [];
15449
- const probes = request.workspaceState === "empty" ? [
15856
+ const probes = [
15450
15857
  { toolId: "repo.index", input: {} }
15451
- ] : [
15452
- { toolId: "repo.index", input: {} },
15453
- { toolId: "git.status", input: {} },
15454
- { toolId: "file.read", input: { path: "package.json" } },
15455
- { toolId: "file.read", input: { path: "README.md" } }
15456
15858
  ];
15859
+ if (request.workspaceState !== "empty") {
15860
+ const candidatePaths = await Promise.all([
15861
+ (0, import_promises11.access)(import_node_path11.default.join(request.cwd, ".git")).then(() => true).catch(() => false),
15862
+ (0, import_promises11.access)(import_node_path11.default.join(request.cwd, "package.json")).then(() => true).catch(() => false),
15863
+ (0, import_promises11.access)(import_node_path11.default.join(request.cwd, "README.md")).then(() => true).catch(() => false)
15864
+ ]);
15865
+ if (candidatePaths[0]) {
15866
+ probes.push({ toolId: "git.status", input: {} });
15867
+ }
15868
+ if (candidatePaths[1]) {
15869
+ probes.push({ toolId: "file.read", input: { path: "package.json" } });
15870
+ }
15871
+ if (candidatePaths[2]) {
15872
+ probes.push({ toolId: "file.read", input: { path: "README.md" } });
15873
+ }
15874
+ }
15457
15875
  for (const probe of probes) {
15458
15876
  if (emitProgress) {
15459
15877
  await emitProgress({
@@ -15570,6 +15988,7 @@ var ExecutionOrchestrator = class {
15570
15988
  input.systemPrompt
15571
15989
  ].filter(Boolean).join("\n\n"),
15572
15990
  userPrompt: input.userPrompt,
15991
+ responseFormat: "json_object",
15573
15992
  ...typeof input.temperature === "number" ? {
15574
15993
  temperature: input.temperature
15575
15994
  } : {},
@@ -15587,7 +16006,7 @@ var ExecutionOrchestrator = class {
15587
16006
  if (result.warning) {
15588
16007
  return {
15589
16008
  plan,
15590
- note: `Planner replan fell back to the existing graph: ${result.warning}`
16009
+ note: `${result.warning} Keeping the current task graph.`
15591
16010
  };
15592
16011
  }
15593
16012
  return {
@@ -15653,6 +16072,7 @@ var ExecutionOrchestrator = class {
15653
16072
  input.systemPrompt
15654
16073
  ].filter(Boolean).join("\n\n"),
15655
16074
  userPrompt: input.userPrompt,
16075
+ responseFormat: "json_object",
15656
16076
  ...typeof input.temperature === "number" ? {
15657
16077
  temperature: input.temperature
15658
16078
  } : {},
@@ -16330,6 +16750,7 @@ async function generatePlanForRequest(request) {
16330
16750
  input.systemPrompt
16331
16751
  ].filter(Boolean).join("\n\n"),
16332
16752
  userPrompt: input.userPrompt,
16753
+ responseFormat: "json_object",
16333
16754
  ...typeof input.temperature === "number" ? {
16334
16755
  temperature: input.temperature
16335
16756
  } : {},
@@ -16359,7 +16780,8 @@ function renderPlanGenerationNotes(result) {
16359
16780
  lines.push(`planner tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`);
16360
16781
  }
16361
16782
  if (result.warning) {
16362
- lines.push(`planner note: ${result.warning}`);
16783
+ const warning = result.source === "fallback" ? `${summarizeStructuredOutputError("Planner", new Error(result.warning))} Using the safe default plan.` : result.warning;
16784
+ lines.push(`planner note: ${warning}`);
16363
16785
  }
16364
16786
  return lines;
16365
16787
  }
@@ -17037,6 +17459,7 @@ function createProgram(onOpenShell) {
17037
17459
  }
17038
17460
 
17039
17461
  // src/shell.ts
17462
+ var import_node_readline = require("node:readline");
17040
17463
  var import_promises14 = require("node:readline/promises");
17041
17464
  var import_node_process12 = __toESM(require("node:process"), 1);
17042
17465
  var AMBER = "\x1B[38;5;214m";
@@ -17045,7 +17468,9 @@ var BOLD = "\x1B[1m";
17045
17468
  var DIM = "\x1B[2m";
17046
17469
  var RESET = "\x1B[0m";
17047
17470
  var TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
17471
+ "approval",
17048
17472
  "approve",
17473
+ "approve-all",
17049
17474
  "agents",
17050
17475
  "brain",
17051
17476
  "brains",
@@ -17089,6 +17514,20 @@ var MAX_CHAT_MESSAGES = 12;
17089
17514
  var DEFAULT_MAX_AUTO_TASKS = 3;
17090
17515
  var DEFAULT_MAX_AGENT_STEPS = 8;
17091
17516
  var DEFAULT_MAX_REPAIR_ATTEMPTS2 = 2;
17517
+ var SPINNER_FRAMES = [
17518
+ "-",
17519
+ "\\",
17520
+ "|",
17521
+ "/"
17522
+ ];
17523
+ var IDLE_STATUS_LABELS = [
17524
+ "thinking",
17525
+ "musing",
17526
+ "discombobulating",
17527
+ "assembling",
17528
+ "drafting",
17529
+ "tinkering"
17530
+ ];
17092
17531
  var EXECUTION_PREFIXES = [
17093
17532
  "build ",
17094
17533
  "create ",
@@ -17148,6 +17587,71 @@ function createExecutionTelemetry() {
17148
17587
  outputTokens: 0
17149
17588
  };
17150
17589
  }
17590
+ var ShellActivityIndicator = class {
17591
+ interval = null;
17592
+ frameIndex = 0;
17593
+ label;
17594
+ activeLine = false;
17595
+ constructor(label) {
17596
+ this.label = label;
17597
+ }
17598
+ start() {
17599
+ if (!import_node_process12.default.stdout.isTTY || this.interval) {
17600
+ return;
17601
+ }
17602
+ this.activeLine = true;
17603
+ this.render();
17604
+ this.interval = setInterval(() => {
17605
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
17606
+ this.render();
17607
+ }, 120);
17608
+ this.interval.unref?.();
17609
+ }
17610
+ update(label) {
17611
+ this.label = label;
17612
+ if (this.interval) {
17613
+ this.render();
17614
+ }
17615
+ }
17616
+ suspend() {
17617
+ if (!import_node_process12.default.stdout.isTTY) {
17618
+ return;
17619
+ }
17620
+ this.clear();
17621
+ }
17622
+ resume() {
17623
+ if (!import_node_process12.default.stdout.isTTY || !this.interval) {
17624
+ return;
17625
+ }
17626
+ this.render();
17627
+ }
17628
+ stop() {
17629
+ if (this.interval) {
17630
+ clearInterval(this.interval);
17631
+ this.interval = null;
17632
+ }
17633
+ this.clear();
17634
+ this.activeLine = false;
17635
+ }
17636
+ render() {
17637
+ if (!import_node_process12.default.stdout.isTTY) {
17638
+ return;
17639
+ }
17640
+ const frame = color(AMBER, SPINNER_FRAMES[this.frameIndex]);
17641
+ const status = color(BOLD, this.label);
17642
+ const raw = `${frame} ${status}${color(DIM, "...")}`;
17643
+ this.clear();
17644
+ (0, import_node_readline.cursorTo)(import_node_process12.default.stdout, 0);
17645
+ import_node_process12.default.stdout.write(raw);
17646
+ }
17647
+ clear() {
17648
+ if (!import_node_process12.default.stdout.isTTY || !this.activeLine) {
17649
+ return;
17650
+ }
17651
+ (0, import_node_readline.cursorTo)(import_node_process12.default.stdout, 0);
17652
+ (0, import_node_readline.clearLine)(import_node_process12.default.stdout, 0);
17653
+ }
17654
+ };
17151
17655
  function renderExecutionTelemetry(telemetry, startedAt) {
17152
17656
  return `telemetry: ${((Date.now() - startedAt) / 1e3).toFixed(1)}s | ${telemetry.toolCalls} tools | ${telemetry.modelCalls} model calls | ${telemetry.inputTokens} in / ${telemetry.outputTokens} out`;
17153
17657
  }
@@ -17337,7 +17841,9 @@ function renderHelp() {
17337
17841
  "/plan <goal> Create a structured implementation plan.",
17338
17842
  "/run <goal> Start a Kimbho execution session for a goal.",
17339
17843
  "/resume Show the latest saved session.",
17844
+ "/approval [mode] Show or set approval mode: manual or auto.",
17340
17845
  "/approve [id] Approve a pending risky action and continue the session.",
17846
+ "/approve-all Approve all pending actions in the current session.",
17341
17847
  "/deny [id] Deny a pending risky action.",
17342
17848
  "/agents Inspect agent roles and the active session.",
17343
17849
  "/review Review the current git diff and summarize risk.",
@@ -17362,7 +17868,7 @@ function renderStartupCard(cwd, state) {
17362
17868
  renderCardLine("approval", state.approvalMode),
17363
17869
  renderCardLine("sandbox", state.sandboxMode),
17364
17870
  renderCardLine("preset", state.stackPreset),
17365
- renderCardLine("shortcuts", "/ask /run /approve /brain /providers /models /quit")
17871
+ renderCardLine("shortcuts", "/ask /run /approval /approve-all /models /quit")
17366
17872
  ];
17367
17873
  if (!state.configured) {
17368
17874
  cardLines.push("setup: run /init or /providers add <template> to create .kimbho/config.json");
@@ -17446,6 +17952,106 @@ function renderEventType(type) {
17446
17952
  function renderInlineMarkdown(value) {
17447
17953
  return value.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => `${color(CYAN, label)} ${color(DIM, `(${url})`)}`).replace(/`([^`]+)`/g, (_match, code) => color(CYAN, code)).replace(/\*\*([^*]+)\*\*/g, (_match, boldText) => color(BOLD, boldText)).replace(/\*([^*]+)\*/g, (_match, italicText) => color(DIM, italicText));
17448
17954
  }
17955
+ function hashString(value) {
17956
+ let hash = 0;
17957
+ for (let index = 0; index < value.length; index += 1) {
17958
+ hash = (hash << 5) - hash + value.charCodeAt(index);
17959
+ hash |= 0;
17960
+ }
17961
+ return Math.abs(hash);
17962
+ }
17963
+ function pickStatusLabel(message) {
17964
+ const lower = message.toLowerCase();
17965
+ const choose = (labels) => labels[hashString(message) % labels.length];
17966
+ if (lower.includes("synthesizing") || lower.includes("architecture brief")) {
17967
+ return choose([
17968
+ "planning",
17969
+ "mapping",
17970
+ "sketching"
17971
+ ]);
17972
+ }
17973
+ if (lower.includes("repo context") || lower.includes("repo analysis") || lower.includes("deterministic repo context")) {
17974
+ return choose([
17975
+ "orienting",
17976
+ "surveying",
17977
+ "mapping"
17978
+ ]);
17979
+ }
17980
+ if (lower.includes("using ") && lower.includes(" via ")) {
17981
+ return choose([
17982
+ "warming up",
17983
+ "locking in",
17984
+ "switching gears"
17985
+ ]);
17986
+ }
17987
+ if (lower.includes("reformatting model output") || lower.includes("structured mode")) {
17988
+ return choose([
17989
+ "reformatting",
17990
+ "straightening",
17991
+ "coaxing"
17992
+ ]);
17993
+ }
17994
+ if (lower.includes("expanded") || lower.includes("revised the task graph") || lower.includes("keeping the current task graph")) {
17995
+ return choose([
17996
+ "replanning",
17997
+ "reshaping",
17998
+ "rerouting"
17999
+ ]);
18000
+ }
18001
+ if (lower.includes("approval")) {
18002
+ return choose([
18003
+ "waiting",
18004
+ "holding",
18005
+ "pausing"
18006
+ ]);
18007
+ }
18008
+ if (lower.includes("integration") || lower.includes("worktree")) {
18009
+ return choose([
18010
+ "reconciling",
18011
+ "merging",
18012
+ "untangling"
18013
+ ]);
18014
+ }
18015
+ return choose([
18016
+ "thinking",
18017
+ "musing",
18018
+ "assembling",
18019
+ "checking"
18020
+ ]);
18021
+ }
18022
+ function pickIdleStatusLabel(seed) {
18023
+ return IDLE_STATUS_LABELS[hashString(seed) % IDLE_STATUS_LABELS.length];
18024
+ }
18025
+ function statusLabelForEvent(event) {
18026
+ switch (event.type) {
18027
+ case "task-note":
18028
+ return pickStatusLabel(event.message);
18029
+ case "task-started":
18030
+ return pickStatusLabel(event.task.title);
18031
+ case "tool-started":
18032
+ return pickStatusLabel(event.reason ?? event.toolId);
18033
+ case "model-usage":
18034
+ return "thinking";
18035
+ case "approval-requested":
18036
+ return "waiting";
18037
+ case "approval-resolved":
18038
+ return "continuing";
18039
+ case "task-finished":
18040
+ return event.status === "handoff" ? "rerouting" : "settling";
18041
+ default:
18042
+ return "thinking";
18043
+ }
18044
+ }
18045
+ function simplifyTaskNote(message) {
18046
+ const trimmed = message.trim();
18047
+ if (trimmed.startsWith("Using ") && trimmed.includes(" via ")) {
18048
+ return trimmed.replace(/^Using\s+/, "");
18049
+ }
18050
+ if (trimmed.startsWith("Reformatting model output")) {
18051
+ return trimmed;
18052
+ }
18053
+ return trimmed;
18054
+ }
17449
18055
  function renderTerminalMarkdown(markdown) {
17450
18056
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
17451
18057
  const output = [];
@@ -17658,7 +18264,7 @@ function renderLiveExecutionEvent(event) {
17658
18264
  ];
17659
18265
  case "task-note":
17660
18266
  return [
17661
- `${color(DIM, "note")} ${event.message}`
18267
+ `${color(AMBER, `[${pickStatusLabel(event.message)}]`)} ${simplifyTaskNote(event.message)}`
17662
18268
  ];
17663
18269
  case "approval-requested":
17664
18270
  return [
@@ -17791,6 +18397,8 @@ async function handleChatPrompt(cwd, prompt, runtime) {
17791
18397
  }
17792
18398
  ]);
17793
18399
  let result;
18400
+ const activity = new ShellActivityIndicator(pickIdleStatusLabel(prompt));
18401
+ activity.start();
17794
18402
  try {
17795
18403
  result = await brain.client.generateText({
17796
18404
  model: brain.model,
@@ -17806,9 +18414,11 @@ async function handleChatPrompt(cwd, prompt, runtime) {
17806
18414
  } : {}
17807
18415
  });
17808
18416
  } catch (error) {
18417
+ activity.stop();
17809
18418
  const message = error instanceof Error ? error.message : String(error);
17810
18419
  throw new Error(`Chat failed for ${brain.role} via ${brain.provider.id}/${brain.model}: ${message}`);
17811
18420
  }
18421
+ activity.stop();
17812
18422
  const nextConversation = trimConversation([
17813
18423
  ...messages,
17814
18424
  {
@@ -17833,13 +18443,29 @@ async function runGoalExecution(cwd, goal, runtime) {
17833
18443
  workspaceState: workspace.workspaceState,
17834
18444
  constraints: []
17835
18445
  };
17836
- const planResult = await generatePlanForRequest(request);
18446
+ const startedAt = Date.now();
18447
+ const telemetry = createExecutionTelemetry();
18448
+ const controller = new AbortController();
18449
+ runtime.activeExecution = {
18450
+ controller,
18451
+ label: goal
18452
+ };
18453
+ const planningSpinner = new ShellActivityIndicator("planning");
18454
+ console.log(color(DIM, `Working on: ${goal}`));
18455
+ for (const note of workspace.notes) {
18456
+ console.log(color(DIM, note));
18457
+ }
18458
+ planningSpinner.start();
18459
+ let planResult;
18460
+ try {
18461
+ planResult = await generatePlanForRequest(request);
18462
+ } finally {
18463
+ planningSpinner.stop();
18464
+ }
17837
18465
  const plan = planResult.plan;
17838
18466
  const planPath = await savePlan(plan, request.cwd);
17839
18467
  const envelope = orchestrator.buildEnvelope(request, plan);
17840
18468
  const initialSnapshot = orchestrator.createSessionSnapshot(envelope);
17841
- const startedAt = Date.now();
17842
- const telemetry = createExecutionTelemetry();
17843
18469
  const liveBoard = createLiveRunBoard(
17844
18470
  initialSnapshot.id,
17845
18471
  goal,
@@ -17849,21 +18475,14 @@ async function runGoalExecution(cwd, goal, runtime) {
17849
18475
  DEFAULT_MAX_AGENT_STEPS,
17850
18476
  DEFAULT_MAX_REPAIR_ATTEMPTS2
17851
18477
  );
17852
- const controller = new AbortController();
17853
- runtime.activeExecution = {
17854
- controller,
17855
- label: goal
17856
- };
17857
- console.log(color(DIM, `Working on: ${goal}`));
17858
- for (const note of workspace.notes) {
17859
- console.log(color(DIM, note));
17860
- }
17861
18478
  for (const line of renderPlanGenerationNotes(planResult)) {
17862
18479
  console.log(color(DIM, line));
17863
18480
  }
17864
18481
  console.log(renderShellPlanPreview(plan).join("\n"));
17865
18482
  console.log(renderRunStartCard(liveBoard));
17866
18483
  console.log("");
18484
+ const activity = new ShellActivityIndicator("starting");
18485
+ activity.start();
17867
18486
  let snapshot;
17868
18487
  try {
17869
18488
  snapshot = await orchestrator.continueSession(initialSnapshot, {
@@ -17881,6 +18500,8 @@ async function runGoalExecution(cwd, goal, runtime) {
17881
18500
  telemetry.inputTokens += event.usage?.inputTokens ?? 0;
17882
18501
  telemetry.outputTokens += event.usage?.outputTokens ?? 0;
17883
18502
  }
18503
+ activity.update(statusLabelForEvent(event));
18504
+ activity.suspend();
17884
18505
  for (const line of renderLiveExecutionEvent(event)) {
17885
18506
  console.log(line);
17886
18507
  }
@@ -17889,12 +18510,15 @@ async function runGoalExecution(cwd, goal, runtime) {
17889
18510
  console.log(line);
17890
18511
  }
17891
18512
  }
18513
+ activity.resume();
17892
18514
  }
17893
18515
  });
17894
18516
  } catch (error) {
18517
+ activity.stop();
17895
18518
  const message = error instanceof Error ? error.message : String(error);
17896
18519
  throw new Error(`Execution failed while working on "${goal}": ${message}`);
17897
18520
  } finally {
18521
+ activity.stop();
17898
18522
  runtime.activeExecution = null;
17899
18523
  }
17900
18524
  const sessionPath = await saveSession(snapshot, request.cwd);
@@ -17928,6 +18552,8 @@ async function resumeGoalExecution(cwd, runtime) {
17928
18552
  label: session.id
17929
18553
  };
17930
18554
  console.log(renderRunStartCard(liveBoard));
18555
+ const activity = new ShellActivityIndicator("resuming");
18556
+ activity.start();
17931
18557
  let snapshot;
17932
18558
  try {
17933
18559
  snapshot = await new ExecutionOrchestrator().continueSession(session, {
@@ -17945,6 +18571,8 @@ async function resumeGoalExecution(cwd, runtime) {
17945
18571
  telemetry.inputTokens += event.usage?.inputTokens ?? 0;
17946
18572
  telemetry.outputTokens += event.usage?.outputTokens ?? 0;
17947
18573
  }
18574
+ activity.update(statusLabelForEvent(event));
18575
+ activity.suspend();
17948
18576
  for (const line of renderLiveExecutionEvent(event)) {
17949
18577
  console.log(line);
17950
18578
  }
@@ -17953,12 +18581,15 @@ async function resumeGoalExecution(cwd, runtime) {
17953
18581
  console.log(line);
17954
18582
  }
17955
18583
  }
18584
+ activity.resume();
17956
18585
  }
17957
18586
  });
17958
18587
  } catch (error) {
18588
+ activity.stop();
17959
18589
  const message = error instanceof Error ? error.message : String(error);
17960
18590
  throw new Error(`Resume failed for ${session.id}: ${message}`);
17961
18591
  } finally {
18592
+ activity.stop();
17962
18593
  runtime.activeExecution = null;
17963
18594
  }
17964
18595
  const sessionPath = await saveSession(snapshot, cwd);
@@ -17967,29 +18598,42 @@ async function resumeGoalExecution(cwd, runtime) {
17967
18598
  console.log(color(DIM, renderExecutionTelemetry(telemetry, startedAt)));
17968
18599
  console.log(renderShellSessionSummary(snapshot, null, sessionPath));
17969
18600
  }
17970
- function resolveApprovalChoice(snapshot, requestedId) {
18601
+ function resolveApprovalChoices(snapshot, requestedId, options = {}) {
17971
18602
  if (snapshot.pendingApprovals.length === 0) {
17972
18603
  throw new Error("No pending approvals in the current session.");
17973
18604
  }
18605
+ if (requestedId === "all") {
18606
+ if (!options.allowAll) {
18607
+ throw new Error("Use /approve-all to approve every pending action.");
18608
+ }
18609
+ return [
18610
+ ...snapshot.pendingApprovals
18611
+ ];
18612
+ }
17974
18613
  if (requestedId) {
17975
18614
  const approval = snapshot.pendingApprovals.find((candidate) => candidate.id === requestedId);
17976
18615
  if (!approval) {
17977
18616
  throw new Error(`No pending approval found for "${requestedId}".`);
17978
18617
  }
17979
- return approval;
18618
+ return [
18619
+ approval
18620
+ ];
17980
18621
  }
17981
18622
  if (snapshot.pendingApprovals.length > 1) {
17982
- throw new Error("Multiple pending approvals exist. Use /approve <approval-id> or /deny <approval-id>.");
18623
+ throw new Error("Multiple pending approvals exist. Use /approve <approval-id>, /deny <approval-id>, or /approve-all.");
17983
18624
  }
17984
- return snapshot.pendingApprovals[0];
18625
+ return [
18626
+ snapshot.pendingApprovals[0]
18627
+ ];
17985
18628
  }
17986
- async function resolvePendingApproval(cwd, runtime, decision, approvalId) {
18629
+ async function resolvePendingApproval(cwd, runtime, decision, approvalId, options = {}) {
17987
18630
  const session = await loadLatestSession(cwd);
17988
18631
  if (!session) {
17989
18632
  throw new Error("No saved session found. Run a goal first.");
17990
18633
  }
17991
- const approval = resolveApprovalChoice(session, approvalId);
17992
- console.log(color(DIM, `${decision === "approve" ? "Approving" : "Denying"} ${approval.toolId} for ${approval.taskId}...`));
18634
+ const approvals = resolveApprovalChoices(session, approvalId, options);
18635
+ const label = approvals.length === 1 ? `${approvals[0].toolId} for ${approvals[0].taskId}` : `${approvals.length} pending actions`;
18636
+ console.log(color(DIM, `${decision === "approve" ? "Approving" : "Denying"} ${label}...`));
17993
18637
  const startedAt = Date.now();
17994
18638
  const telemetry = createExecutionTelemetry();
17995
18639
  const liveBoard = createLiveRunBoard(
@@ -18005,21 +18649,21 @@ async function resolvePendingApproval(cwd, runtime, decision, approvalId) {
18005
18649
  const controller = new AbortController();
18006
18650
  runtime.activeExecution = {
18007
18651
  controller,
18008
- label: `${session.id}:${approval.id}`
18652
+ label: approvals.length === 1 ? `${session.id}:${approvals[0].id}` : `${session.id}:batch-approval`
18009
18653
  };
18010
18654
  console.log(renderRunStartCard(liveBoard));
18655
+ const activity = new ShellActivityIndicator(decision === "approve" ? "approving" : "denying");
18656
+ activity.start();
18011
18657
  let snapshot;
18012
18658
  try {
18013
18659
  snapshot = await new ExecutionOrchestrator().continueSession(session, {
18014
18660
  maxAutoTasks: DEFAULT_MAX_AUTO_TASKS,
18015
18661
  maxAgentSteps: DEFAULT_MAX_AGENT_STEPS,
18016
18662
  maxRepairAttempts: DEFAULT_MAX_REPAIR_ATTEMPTS2,
18017
- approvalDecisions: [
18018
- {
18019
- approvalId: approval.id,
18020
- decision
18021
- }
18022
- ],
18663
+ approvalDecisions: approvals.map((approval) => ({
18664
+ approvalId: approval.id,
18665
+ decision
18666
+ })),
18023
18667
  signal: controller.signal,
18024
18668
  onProgress: async (event) => {
18025
18669
  updateLiveRunBoard(liveBoard, event);
@@ -18031,6 +18675,8 @@ async function resolvePendingApproval(cwd, runtime, decision, approvalId) {
18031
18675
  telemetry.inputTokens += event.usage?.inputTokens ?? 0;
18032
18676
  telemetry.outputTokens += event.usage?.outputTokens ?? 0;
18033
18677
  }
18678
+ activity.update(statusLabelForEvent(event));
18679
+ activity.suspend();
18034
18680
  for (const line of renderLiveExecutionEvent(event)) {
18035
18681
  console.log(line);
18036
18682
  }
@@ -18039,12 +18685,15 @@ async function resolvePendingApproval(cwd, runtime, decision, approvalId) {
18039
18685
  console.log(line);
18040
18686
  }
18041
18687
  }
18688
+ activity.resume();
18042
18689
  }
18043
18690
  });
18044
18691
  } catch (error) {
18692
+ activity.stop();
18045
18693
  const message = error instanceof Error ? error.message : String(error);
18046
- throw new Error(`Approval resolution failed for ${approval.id}: ${message}`);
18694
+ throw new Error(`Approval resolution failed for ${approvals.map((approval) => approval.id).join(", ")}: ${message}`);
18047
18695
  } finally {
18696
+ activity.stop();
18048
18697
  runtime.activeExecution = null;
18049
18698
  }
18050
18699
  const sessionPath = await saveSession(snapshot, cwd);
@@ -18060,6 +18709,28 @@ async function printLatestPlanSummary(cwd) {
18060
18709
  }
18061
18710
  console.log(renderShellPlanSummary(plan).join("\n"));
18062
18711
  }
18712
+ async function handleApprovalModeCommand(cwd, tokens) {
18713
+ const config = await loadConfig(cwd);
18714
+ if (!config) {
18715
+ throw new Error("No config found. Run /init or /providers add <template> first.");
18716
+ }
18717
+ const subcommand = tokens[1]?.trim().toLowerCase();
18718
+ if (!subcommand || subcommand === "status") {
18719
+ console.log(`approval mode: ${config.approvalMode}`);
18720
+ console.log("manual = ask before risky actions");
18721
+ console.log("auto = approve non-destructive actions automatically");
18722
+ return;
18723
+ }
18724
+ if (subcommand !== "manual" && subcommand !== "auto") {
18725
+ throw new Error("Usage: /approval [manual|auto|status]");
18726
+ }
18727
+ const outputPath = await saveConfig({
18728
+ ...config,
18729
+ approvalMode: subcommand
18730
+ }, cwd);
18731
+ console.log(`Updated ${outputPath}`);
18732
+ console.log(`approval mode: ${subcommand}`);
18733
+ }
18063
18734
  async function createPlanOnly(cwd, goal) {
18064
18735
  const request = {
18065
18736
  goal,
@@ -18068,7 +18739,14 @@ async function createPlanOnly(cwd, goal) {
18068
18739
  workspaceState: await inferPlanningWorkspaceState(cwd, goal),
18069
18740
  constraints: []
18070
18741
  };
18071
- const planResult = await generatePlanForRequest(request);
18742
+ const activity = new ShellActivityIndicator("planning");
18743
+ activity.start();
18744
+ let planResult;
18745
+ try {
18746
+ planResult = await generatePlanForRequest(request);
18747
+ } finally {
18748
+ activity.stop();
18749
+ }
18072
18750
  const plan = planResult.plan;
18073
18751
  const planPath = await savePlan(plan, cwd);
18074
18752
  for (const line of renderPlanGenerationNotes(planResult)) {
@@ -18647,13 +19325,35 @@ async function handleShellCommand(cwd, input, state, runtime, execute) {
18647
19325
  await resumeGoalExecution(cwd, runtime);
18648
19326
  return cwd;
18649
19327
  }
19328
+ if (head === "approval") {
19329
+ await handleApprovalModeCommand(cwd, [
19330
+ "approval",
19331
+ ...tokens.slice(1)
19332
+ ]);
19333
+ return cwd;
19334
+ }
19335
+ if (head === "approve-all") {
19336
+ await resolvePendingApproval(
19337
+ cwd,
19338
+ runtime,
19339
+ "approve",
19340
+ "all",
19341
+ {
19342
+ allowAll: true
19343
+ }
19344
+ );
19345
+ return cwd;
19346
+ }
18650
19347
  if (head === "approve" || head === "deny") {
18651
19348
  const approvalId = tokens[1]?.trim();
18652
19349
  await resolvePendingApproval(
18653
19350
  cwd,
18654
19351
  runtime,
18655
19352
  head === "approve" ? "approve" : "deny",
18656
- approvalId && approvalId.length > 0 ? approvalId : void 0
19353
+ approvalId && approvalId.length > 0 ? approvalId : void 0,
19354
+ {
19355
+ allowAll: false
19356
+ }
18657
19357
  );
18658
19358
  return cwd;
18659
19359
  }