@skein-code/cli 0.3.25 → 0.3.26

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/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.25",
223
+ version: "0.3.26",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,12 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "First-run setup now states that the primary agent needs API credentials and that provider subscriptions or signed-in coding CLIs are separate delegated tools",
240
+ "Permission prompts show the policy reason, redacted target, working directory, category risk, and explicit once, session, deny, and stop choices",
241
+ "The Recovery Center joins last-run state, failure repair hints, changed files, checkpoints, diff, audit, rollback, retry, and safe session resume",
242
+ "Read-only review commands pin working-tree, commit, or branch scope and inject a content-free redacted evidence bundle without storing it in the user transcript",
243
+ "JSON and JSONL terminal records now follow the published headless v1 schema with stable completed, verification, input, blocked, cancellation, turn, token, and error exit codes",
244
+ "Short terminal viewports clip long lists without overflow and pending clarifications cannot be consumed by recovery commands",
239
245
  "Durable sessions now separate a 250k-token context epoch from a 1m-token lifetime ceiling without changing the session id or deleting transcript history",
240
246
  "Content-free epoch handoffs preserve Task Contract criteria, unresolved failure circuits, changed files, and verification receipts across long runs",
241
247
  "Intent Sufficiency routes clear requests to execution, repository-inferable gaps to inspection, and genuine product choices to one persisted clarification",
@@ -11975,13 +11981,13 @@ ${completeContent}`;
11975
11981
  this.recordPermission(call, category, "allow", "Approved for this session.");
11976
11982
  return true;
11977
11983
  }
11978
- await emit({ type: "permission", call, category });
11984
+ await emit({ type: "permission", call, category, reason: decision.reason });
11979
11985
  if (!options.requestPermission) {
11980
11986
  this.recordPermission(call, category, "deny", "No permission handler was available.");
11981
11987
  return false;
11982
11988
  }
11983
11989
  try {
11984
- const grant = await options.requestPermission(call, category);
11990
+ const grant = await options.requestPermission(call, category, decision.reason);
11985
11991
  const allowed = grant === true || grant === "session";
11986
11992
  if (grant === "session") this.sessionApprovals.add(approvalKey);
11987
11993
  this.recordPermission(
@@ -11996,13 +12002,13 @@ ${completeContent}`;
11996
12002
  return false;
11997
12003
  }
11998
12004
  }
11999
- recordPermission(call, category, outcome, reason) {
12005
+ recordPermission(call, category, outcome2, reason) {
12000
12006
  this.appendAudit({
12001
12007
  type: "permission",
12002
12008
  toolCallId: call.id,
12003
12009
  tool: call.name,
12004
12010
  category,
12005
- outcome,
12011
+ outcome: outcome2,
12006
12012
  reason
12007
12013
  });
12008
12014
  }
@@ -13649,18 +13655,18 @@ var DelegationManager = class {
13649
13655
  await this.recordAgent(board.id, writer, "write");
13650
13656
  const writerFailed = !writer.ok || !draft.patch || !draft.worktreeCleaned || signal?.aborted;
13651
13657
  if (writerFailed) {
13652
- const outcome = writer.termination === "cancelled" || signal?.aborted ? "cancelled" : "failed";
13658
+ const outcome2 = writer.termination === "cancelled" || signal?.aborted ? "cancelled" : "failed";
13653
13659
  await this.teamStore.recordWriterLane(board.id, {
13654
13660
  profile: profile.name,
13655
13661
  reviewer: reviewer.name,
13656
13662
  baseCommit: draft.baseCommit,
13657
- outcome,
13663
+ outcome: outcome2,
13658
13664
  patch: draft.patch,
13659
13665
  files: draft.files,
13660
13666
  worktreeCleaned: draft.worktreeCleaned
13661
13667
  });
13662
13668
  await this.teamStore.complete(board.id, { accepted: false, reviewRounds: 0, failed: true });
13663
- const status2 = outcome === "cancelled" ? "cancelled" : "failed";
13669
+ const status2 = outcome2 === "cancelled" ? "cancelled" : "failed";
13664
13670
  const detail2 = !draft.worktreeCleaned ? "Writer worktree cleanup could not be verified; integration is blocked." : !draft.patch ? "Writer returned no patch." : writer.summary;
13665
13671
  await emit?.({ type: "writer_lane", id: board.id, status: status2, detail: detail2, files: draft.files });
13666
13672
  await emit?.({ type: "team_done", id: board.id, accepted: false, reviewRounds: 0 });
@@ -15009,6 +15015,61 @@ function keyEnvironmentName(provider) {
15009
15015
  // src/cli/output.ts
15010
15016
  import { createInterface } from "node:readline/promises";
15011
15017
  import chalk2, { Chalk } from "chalk";
15018
+
15019
+ // src/cli/headless-contract.ts
15020
+ var HEADLESS_SCHEMA_VERSION = 1;
15021
+ var HEADLESS_EXIT_CODES = {
15022
+ completed: 0,
15023
+ error: 1,
15024
+ needsInput: 2,
15025
+ unverified: 3,
15026
+ verificationFailed: 4,
15027
+ blocked: 5,
15028
+ cancelled: 6,
15029
+ maxTurns: 7,
15030
+ tokenBudget: 8
15031
+ };
15032
+ function resolveHeadlessOutcome(input2) {
15033
+ if (input2.error !== void 0 || input2.reason === "error") {
15034
+ return outcome("error", HEADLESS_EXIT_CODES.error, input2.reason ?? "error");
15035
+ }
15036
+ if (input2.reason === "needs_input") {
15037
+ return outcome("needs_input", HEADLESS_EXIT_CODES.needsInput, input2.reason);
15038
+ }
15039
+ if (input2.reason === "blocked" || input2.completion?.acceptance?.state === "blocked") {
15040
+ return outcome("blocked", HEADLESS_EXIT_CODES.blocked, input2.reason ?? "blocked");
15041
+ }
15042
+ if (input2.reason === "aborted" || input2.reason === "cancelled") {
15043
+ return outcome("cancelled", HEADLESS_EXIT_CODES.cancelled, input2.reason);
15044
+ }
15045
+ if (input2.reason === "max_turns") {
15046
+ return outcome("max_turns", HEADLESS_EXIT_CODES.maxTurns, input2.reason);
15047
+ }
15048
+ if (input2.reason === "token_budget") {
15049
+ return outcome("token_budget", HEADLESS_EXIT_CODES.tokenBudget, input2.reason);
15050
+ }
15051
+ if (input2.reason === "verification_failed" || input2.completion?.status === "verification_failed") {
15052
+ return outcome("verification_failed", HEADLESS_EXIT_CODES.verificationFailed, input2.reason ?? "verification_failed");
15053
+ }
15054
+ if (input2.reason === "unverified" || input2.completion?.status === "unverified") {
15055
+ return outcome("unverified", HEADLESS_EXIT_CODES.unverified, input2.reason ?? "unverified");
15056
+ }
15057
+ if (input2.completion?.status === "verified") {
15058
+ return outcome("verified", HEADLESS_EXIT_CODES.completed, input2.reason ?? "completed");
15059
+ }
15060
+ return outcome("completed", HEADLESS_EXIT_CODES.completed, input2.reason ?? "completed");
15061
+ }
15062
+ function outcome(status, exitCode, reason) {
15063
+ return {
15064
+ schemaVersion: HEADLESS_SCHEMA_VERSION,
15065
+ ok: exitCode === HEADLESS_EXIT_CODES.completed,
15066
+ status,
15067
+ exitCode,
15068
+ reason
15069
+ };
15070
+ }
15071
+
15072
+ // src/cli/output.ts
15012
15073
  var HeadlessReporter = class {
15013
15074
  constructor(options) {
15014
15075
  this.options = options;
@@ -15018,7 +15079,6 @@ var HeadlessReporter = class {
15018
15079
  options;
15019
15080
  finalResponse = "";
15020
15081
  tools = [];
15021
- eventError;
15022
15082
  context;
15023
15083
  streamedAssistant = false;
15024
15084
  completion;
@@ -15028,7 +15088,6 @@ var HeadlessReporter = class {
15028
15088
  onEvent = (event) => {
15029
15089
  if (event.type === "assistant") this.finalResponse = event.content;
15030
15090
  if (event.type === "tool_result") this.tools.push(event.result);
15031
- if (event.type === "error") this.eventError = event.error.message;
15032
15091
  if (event.type === "done") {
15033
15092
  this.doneReason = event.reason;
15034
15093
  this.completion = event.completion;
@@ -15046,26 +15105,33 @@ var HeadlessReporter = class {
15046
15105
  this.printText(event);
15047
15106
  };
15048
15107
  finish(session) {
15108
+ const completion = this.completion ?? session.lastRun;
15109
+ const reason = this.doneReason ?? session.lastRun?.reason;
15110
+ const outcome2 = resolveHeadlessOutcome({
15111
+ ...reason ? { reason } : {},
15112
+ ...completion ? { completion } : {}
15113
+ });
15049
15114
  if (this.options.format === "stream-json") {
15050
15115
  process.stdout.write(`${JSON.stringify({
15051
15116
  type: "session",
15117
+ ...outcome2,
15052
15118
  session: sessionSummary(session)
15053
15119
  })}
15054
15120
  `);
15055
- return;
15121
+ return outcome2;
15056
15122
  }
15057
15123
  if (this.options.format === "json") {
15058
15124
  process.stdout.write(`${JSON.stringify({
15059
- ok: this.doneReason === void 0 || this.doneReason === "completed" && this.completion?.status !== "verification_failed",
15125
+ type: "result",
15126
+ ...outcome2,
15060
15127
  response: this.finalResponse,
15061
15128
  session: sessionSummary(session),
15062
- ...this.doneReason ? { reason: this.doneReason } : {},
15063
- ...this.completion ? { completion: this.completion } : {},
15129
+ ...completion ? { completion } : {},
15064
15130
  ...this.context ? { context: this.context } : {},
15065
15131
  tools: this.tools
15066
15132
  }, null, 2)}
15067
15133
  `);
15068
- return;
15134
+ return outcome2;
15069
15135
  }
15070
15136
  if (this.options.quiet && this.finalResponse.trim()) {
15071
15137
  process.stdout.write(`${this.finalResponse.trim()}
@@ -15080,17 +15146,34 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15080
15146
  `
15081
15147
  ));
15082
15148
  }
15149
+ return outcome2;
15083
15150
  }
15084
- fail(error) {
15151
+ fail(error, session) {
15085
15152
  const message2 = error instanceof Error ? error.message : String(error);
15086
- if (this.options.format === "stream-json" && this.eventError === message2) return;
15087
- if (this.options.format === "json" || this.options.format === "stream-json") {
15088
- process.stdout.write(`${JSON.stringify({ type: "error", ok: false, error: message2 })}
15153
+ const outcome2 = resolveHeadlessOutcome({ reason: "error", error });
15154
+ if (this.options.format === "stream-json") {
15155
+ process.stdout.write(`${JSON.stringify({
15156
+ type: "final",
15157
+ ...outcome2,
15158
+ error: message2,
15159
+ ...session ? { session: sessionSummary(session) } : {}
15160
+ })}
15089
15161
  `);
15090
- return;
15162
+ return outcome2;
15163
+ }
15164
+ if (this.options.format === "json") {
15165
+ process.stdout.write(`${JSON.stringify({
15166
+ type: "result",
15167
+ ...outcome2,
15168
+ error: message2,
15169
+ ...session ? { session: sessionSummary(session) } : {}
15170
+ }, null, 2)}
15171
+ `);
15172
+ return outcome2;
15091
15173
  }
15092
15174
  process.stderr.write(`${this.paint.red(this.glyphs.error)} ${message2}
15093
15175
  `);
15176
+ return outcome2;
15094
15177
  }
15095
15178
  printText(event) {
15096
15179
  const { quiet, compact: compact3 } = this.options;
@@ -15197,6 +15280,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15197
15280
  break;
15198
15281
  case "done":
15199
15282
  this.printCompletion(event.completion);
15283
+ this.printStopReason(event.reason);
15200
15284
  break;
15201
15285
  case "error":
15202
15286
  break;
@@ -15227,8 +15311,20 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15227
15311
  `
15228
15312
  ));
15229
15313
  }
15314
+ printStopReason(reason) {
15315
+ if (reason === "aborted") {
15316
+ process.stderr.write(this.paint.yellow(`${this.glyphs.warning} cancelled ${this.glyphs.separator} resume with --resume after inspecting the saved session
15317
+ `));
15318
+ } else if (reason === "max_turns") {
15319
+ process.stderr.write(this.paint.yellow(`${this.glyphs.warning} turn limit reached ${this.glyphs.separator} resume with --resume and a larger --max-turns value
15320
+ `));
15321
+ } else if (reason === "token_budget") {
15322
+ process.stderr.write(this.paint.yellow(`${this.glyphs.warning} token budget reached ${this.glyphs.separator} inspect the saved session before resuming with a larger --token-budget
15323
+ `));
15324
+ }
15325
+ }
15230
15326
  };
15231
- async function askConsolePermission(call, category, color = !process.env.NO_COLOR) {
15327
+ async function askConsolePermission(call, category, color = !process.env.NO_COLOR, reason = `${category} tools require approval.`) {
15232
15328
  if (!process.stdin.isTTY || !process.stderr.isTTY) return false;
15233
15329
  const paint = color ? chalk2 : new Chalk({ level: 0 });
15234
15330
  const glyphs = resolveCliGlyphs();
@@ -15236,6 +15332,10 @@ async function askConsolePermission(call, category, color = !process.env.NO_COLO
15236
15332
  ${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
15237
15333
  `);
15238
15334
  process.stderr.write(`${paint.bold(call.name)}${formatToolDetail(call, paint)}
15335
+ `);
15336
+ process.stderr.write(`${paint.dim(`Reason: ${reason}`)}
15337
+ `);
15338
+ process.stderr.write(`${paint.yellow(`Risk: ${permissionRisk(category)}`)}
15239
15339
  `);
15240
15340
  const readline = createInterface({ input: process.stdin, output: process.stderr });
15241
15341
  try {
@@ -15245,6 +15345,13 @@ ${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
15245
15345
  readline.close();
15246
15346
  }
15247
15347
  }
15348
+ function permissionRisk(category) {
15349
+ if (category === "read") return "workspace content may enter model context";
15350
+ if (category === "write") return "workspace files may be created, replaced, or deleted";
15351
+ if (category === "shell") return "a local process may read or change workspace state";
15352
+ if (category === "git") return "repository state or remotes may change";
15353
+ return "data may leave this machine or remote state may change";
15354
+ }
15248
15355
  function eventToJson(event) {
15249
15356
  if (event.type === "error") {
15250
15357
  return { type: event.type, error: event.error.message };
@@ -15350,7 +15457,7 @@ function commandNames(command2) {
15350
15457
  // src/ui/tui.tsx
15351
15458
  import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
15352
15459
  import { Box as Box2, render as render2, Text as Text3, useApp, useInput as useInput2, useWindowSize } from "ink";
15353
- import { relative as relative9 } from "node:path";
15460
+ import { relative as relative10 } from "node:path";
15354
15461
 
15355
15462
  // src/ui/components.tsx
15356
15463
  import React2 from "react";
@@ -15374,6 +15481,8 @@ var commandDefinitions = [
15374
15481
  command("permissions", "Inspect the active permission policy"),
15375
15482
  command("changes", "List files changed in the active session"),
15376
15483
  command("diff", "Open the current workspace diff in the transcript"),
15484
+ command("review", "Run a read-only review with a fixed, redacted scope", "/review [working-tree|commit <ref>|branch <base-ref>]"),
15485
+ command("recover", "Open recovery actions for the last incomplete run", "/recover [retry|resume|diff|rollback|audit]"),
15377
15486
  command("checkpoints", "List recoverable pre-mutation snapshots"),
15378
15487
  command("audit", "Review the hash-chained tool and permission timeline"),
15379
15488
  command("rollback", "Restore workspace files from a checkpoint", "/rollback [checkpoint-id]"),
@@ -15430,6 +15539,28 @@ function commandSuggestions(input2, options = {}) {
15430
15539
  description: "Show the secure shared-connection setup command"
15431
15540
  }].filter((item) => item.label.includes(argument.trim().toLocaleLowerCase()));
15432
15541
  }
15542
+ if (firstSpace >= 0 && commandName === "review") {
15543
+ const query = argument.trim().toLocaleLowerCase();
15544
+ return [
15545
+ { value: "/review working-tree", label: "working-tree", description: "Review only the current working tree" },
15546
+ { value: "/review commit ", label: "commit", description: "Review exactly one Git commit" },
15547
+ { value: "/review branch ", label: "branch", description: "Review the current branch against one base ref" }
15548
+ ].filter((item) => item.label.includes(query) || item.value.slice("/review ".length).startsWith(query));
15549
+ }
15550
+ if (firstSpace >= 0 && commandName === "recover") {
15551
+ const query = argument.trim().toLocaleLowerCase();
15552
+ return [
15553
+ { name: "retry", description: "Repair and retry the latest failed operation once" },
15554
+ { name: "resume", description: "Continue the most recent incomplete logical run" },
15555
+ { name: "diff", description: "Inspect the current workspace patch" },
15556
+ { name: "audit", description: "Review permission and tool evidence" },
15557
+ { name: "rollback", description: "Choose a checkpoint to restore" }
15558
+ ].filter((item) => item.name.includes(query)).map((item) => ({
15559
+ value: `/recover ${item.name}`,
15560
+ label: item.name,
15561
+ description: item.description
15562
+ }));
15563
+ }
15433
15564
  if (firstSpace >= 0 && commandName === "memory") {
15434
15565
  const query = argument.trim().toLocaleLowerCase();
15435
15566
  return [
@@ -16407,7 +16538,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
16407
16538
  ] }) : null
16408
16539
  ] });
16409
16540
  }
16410
- function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact3 = false }) {
16541
+ function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto", workspace, compact: compact3 = false }) {
16411
16542
  const theme = useTheme();
16412
16543
  const glyphs = resolveGlyphs(glyphMode);
16413
16544
  const summary = permissionSummary(call);
@@ -16415,7 +16546,9 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
16415
16546
  const innerWidth = Math.max(1, rowWidth - 2);
16416
16547
  const title = truncateDisplay(`Permission required ${glyphs.separator} ${category}`, innerWidth);
16417
16548
  const tool = truncateDisplay(`tool ${sanitizeInlineTerminalText(call.name)}`, innerWidth);
16418
- const summaryLine = truncateDisplay(`${summary.label} ${summary.value}`, innerWidth);
16549
+ const summaryLine = truncateDisplay(`target ${summary.label} ${summary.value}`, innerWidth);
16550
+ const reasonLine = truncateDisplay(`reason ${redactPermissionText(reason ?? `${category} tools require approval.`)}`, innerWidth);
16551
+ const riskLine = truncateDisplay(`risk ${permissionRisk2(category)}`, innerWidth);
16419
16552
  const argumentCwd = typeof call.arguments.cwd === "string" ? call.arguments.cwd : void 0;
16420
16553
  const cwd = sanitizeInlineTerminalText(argumentCwd || workspace || "");
16421
16554
  const shortcuts = [
@@ -16440,6 +16573,8 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
16440
16573
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
16441
16574
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
16442
16575
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.text, children: summaryLine }) }),
16576
+ /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: reasonLine }) }),
16577
+ /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.warning, children: riskLine }) }),
16443
16578
  cwd ? /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`cwd ${compactDisplayPath(cwd, Math.max(1, innerWidth - 4))}`, innerWidth) }) }) : null,
16444
16579
  rowWidth >= 64 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts, width: innerWidth, separator: ` ${glyphs.separator} `, separatorColor: theme.border }) }) : rowWidth >= 28 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
16445
16580
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
@@ -16460,6 +16595,13 @@ function PermissionLine({ marker, children }) {
16460
16595
  children
16461
16596
  ] });
16462
16597
  }
16598
+ function permissionRisk2(category) {
16599
+ if (category === "read") return "workspace content may enter model context";
16600
+ if (category === "write") return "workspace files may be created, replaced, or deleted";
16601
+ if (category === "shell") return "a local process may read or change workspace state";
16602
+ if (category === "git") return "repository state or remotes may change";
16603
+ return "data may leave this machine or remote state may change";
16604
+ }
16463
16605
  function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueCount = 0, queuePreview, attachments = [], glyphMode = "auto", children }) {
16464
16606
  const theme = useTheme();
16465
16607
  const glyphs = resolveGlyphs(glyphMode);
@@ -17744,6 +17886,40 @@ function clipTimelineItem(item, options) {
17744
17886
  if (item.kind === "notice") {
17745
17887
  return { ...item, text: tailText(item.text, width, options.rows) };
17746
17888
  }
17889
+ if (item.kind === "list") {
17890
+ if (options.rows <= 1) return { id: item.id, kind: "notice", text: truncateDisplay(item.title, width) };
17891
+ const available = Math.max(1, options.rows - 2);
17892
+ const entries = [];
17893
+ let used = 0;
17894
+ let firstIncluded = item.entries.length;
17895
+ for (let index = item.entries.length - 1; index >= 0; index -= 1) {
17896
+ const entry = item.entries[index];
17897
+ const rows = 1 + (entry.detail ? 1 : 0);
17898
+ if (entries.length && used + rows > available) break;
17899
+ if (!entries.length && rows > available) {
17900
+ const { detail: _detail, ...compactEntry } = entry;
17901
+ entries.unshift(compactEntry);
17902
+ firstIncluded = index;
17903
+ used = 1;
17904
+ break;
17905
+ }
17906
+ entries.unshift(entry);
17907
+ firstIncluded = index;
17908
+ used += rows;
17909
+ }
17910
+ if (firstIncluded > 0) {
17911
+ while (entries.length > 1 && used >= available) {
17912
+ const removed = entries.shift();
17913
+ if (!removed) break;
17914
+ firstIncluded += 1;
17915
+ used -= 1 + (removed.detail ? 1 : 0);
17916
+ }
17917
+ if (used < available) {
17918
+ entries.unshift({ label: `${terminalEllipsis()} ${firstIncluded} earlier entries hidden` });
17919
+ }
17920
+ }
17921
+ return { ...item, entries };
17922
+ }
17747
17923
  if (item.kind === "update") {
17748
17924
  const baseRows = width < 48 ? 3 : 2;
17749
17925
  if (options.rows < baseRows) {
@@ -17841,6 +18017,91 @@ function wrappedRows(value, width) {
17841
18017
  return sanitizeTerminalText(value).split("\n").reduce((rows, line) => rows + Math.max(1, Math.ceil(displayWidth(line) / safeWidth2)), 0);
17842
18018
  }
17843
18019
 
18020
+ // src/ui/review-bundle.ts
18021
+ import { relative as relative9 } from "node:path";
18022
+ var failureClasses = /* @__PURE__ */ new Set([
18023
+ "schema_input",
18024
+ "unknown_tool",
18025
+ "permission_denied",
18026
+ "command_exit",
18027
+ "timeout",
18028
+ "cancelled",
18029
+ "hook",
18030
+ "execution",
18031
+ "no_progress",
18032
+ "contract_required"
18033
+ ]);
18034
+ function parseReviewScope(argument) {
18035
+ const value = argument.trim();
18036
+ if (!value || value === "working-tree") return { kind: "working-tree" };
18037
+ const [kind, ref, ...extra] = value.split(/\s+/u);
18038
+ if (kind !== "commit" && kind !== "branch" || !ref || extra.length) {
18039
+ throw new Error("Usage: /review [working-tree|commit <ref>|branch <base-ref>]");
18040
+ }
18041
+ if (!/^[A-Za-z0-9._/@{}~^+-]{1,200}$/u.test(ref) || ref.startsWith("-")) {
18042
+ throw new Error("Review refs must be a single Git revision without control characters or leading dashes.");
18043
+ }
18044
+ return { kind, ref };
18045
+ }
18046
+ function buildRedactedReviewBundle(session, workspaceRoot, scope) {
18047
+ const lastRun = session.lastRun;
18048
+ const failures = (session.audit ?? []).filter((event) => event.type === "tool" && event.outcome === "failure").slice(-8).map((event) => {
18049
+ const receipt = failureReceipt2(event.metadata?.failure);
18050
+ return {
18051
+ tool: event.tool,
18052
+ ...event.category ? { category: event.category } : {},
18053
+ ...receipt?.class ? { class: receipt.class } : {},
18054
+ ...receipt?.retryable !== void 0 ? { retryable: receipt.retryable } : {},
18055
+ ...receipt?.circuitOpen !== void 0 ? { circuitOpen: receipt.circuitOpen } : {}
18056
+ };
18057
+ });
18058
+ return {
18059
+ version: 1,
18060
+ redacted: true,
18061
+ sessionId: session.id,
18062
+ scope,
18063
+ changedFiles: [...new Set(session.changedFiles.map((path) => relative9(workspaceRoot, path) || "."))].sort(),
18064
+ ...lastRun ? {
18065
+ lastRun: {
18066
+ status: lastRun.status,
18067
+ reason: lastRun.reason,
18068
+ checks: lastRun.checks.map((check) => ({ tool: check.tool, kind: check.kind, ok: check.ok })),
18069
+ ...lastRun.acceptance ? { acceptance: {
18070
+ state: lastRun.acceptance.state,
18071
+ satisfied: lastRun.acceptance.satisfied,
18072
+ pending: lastRun.acceptance.pending,
18073
+ blocked: lastRun.acceptance.blocked
18074
+ } } : {}
18075
+ }
18076
+ } : {},
18077
+ failures
18078
+ };
18079
+ }
18080
+ function reviewRequest(scope) {
18081
+ if (scope.kind === "working-tree") return "Review the current working tree changes.";
18082
+ if (scope.kind === "commit") return `Review commit ${scope.ref}.`;
18083
+ return `Review the current branch against ${scope.ref}.`;
18084
+ }
18085
+ function reviewTurnInstructions(bundle) {
18086
+ return `A read-only review is active. Keep the review scope fixed to the bundle below. Do not mutate files, run write-capable tools, or widen the scope. Report actionable findings first with file and line evidence. Never reproduce credentials or secret values. The bundle is deliberately content-free and redacted.
18087
+
18088
+ <redacted-review-bundle>
18089
+ ${JSON.stringify(bundle, null, 2)}
18090
+ </redacted-review-bundle>`;
18091
+ }
18092
+ function failureReceipt2(value) {
18093
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
18094
+ const record = value;
18095
+ return {
18096
+ ...isToolFailureClass(record.class) ? { class: record.class } : {},
18097
+ ...typeof record.retryable === "boolean" ? { retryable: record.retryable } : {},
18098
+ ...typeof record.circuitOpen === "boolean" ? { circuitOpen: record.circuitOpen } : {}
18099
+ };
18100
+ }
18101
+ function isToolFailureClass(value) {
18102
+ return typeof value === "string" && failureClasses.has(value);
18103
+ }
18104
+
17844
18105
  // src/ui/timeline-reducers.ts
17845
18106
  var itemCounter = 0;
17846
18107
  var nextId = () => `ui-${Date.now()}-${itemCounter++}`;
@@ -18184,8 +18445,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18184
18445
  const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
18185
18446
  return () => clearInterval(timer);
18186
18447
  }, [busy]);
18187
- const requestPermission = useCallback((call, category) => {
18188
- return new Promise((resolve26) => setPermission({ call, category, resolve: resolve26 }));
18448
+ const requestPermission = useCallback((call, category, reason) => {
18449
+ return new Promise((resolve26) => setPermission({
18450
+ call,
18451
+ category,
18452
+ ...reason ? { reason } : {},
18453
+ resolve: resolve26
18454
+ }));
18189
18455
  }, []);
18190
18456
  const onEvent = useCallback((event) => {
18191
18457
  switch (event.type) {
@@ -18205,7 +18471,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18205
18471
  ...event.packed.budgetReason ? { budgetReason: event.packed.budgetReason } : {},
18206
18472
  truncated: event.packed.truncated,
18207
18473
  spans: event.packed.hits.slice(0, 5).map((hit) => ({
18208
- path: relative9(runner.workspace.primaryRoot, hit.path) || hit.path,
18474
+ path: relative10(runner.workspace.primaryRoot, hit.path) || hit.path,
18209
18475
  startLine: hit.startLine,
18210
18476
  endLine: hit.endLine,
18211
18477
  score: hit.score,
@@ -18309,7 +18575,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18309
18575
  id: nextId(),
18310
18576
  kind: "notice",
18311
18577
  tone: event.status === "ready" || event.status === "integrated" ? "success" : "error",
18312
- text: `Writer ${event.id.slice(0, 8)} ${event.status}${separator}${event.detail}`
18578
+ text: `Writer ${event.id.slice(0, 8)} ${event.status}${separator}${event.detail}${event.status === "conflict" || event.status === "failed" || event.status === "cancelled" ? `${separator}Run /recover before retrying or restoring.` : ""}`
18313
18579
  });
18314
18580
  break;
18315
18581
  case "agent_done":
@@ -18348,7 +18614,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18348
18614
  case "error":
18349
18615
  lastEventError.current = event.error.message;
18350
18616
  setTimeline(endStreamingAssistants);
18351
- append({ id: nextId(), kind: "notice", tone: "error", text: event.error.message });
18617
+ append({ id: nextId(), kind: "notice", tone: "error", text: `${event.error.message}${separator}Run /recover to inspect evidence, changes, and safe next actions.` });
18352
18618
  setActivity(void 0);
18353
18619
  break;
18354
18620
  case "done":
@@ -18372,7 +18638,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18372
18638
  id: nextId(),
18373
18639
  kind: "notice",
18374
18640
  tone: event.reason === "aborted" ? "info" : "error",
18375
- text: event.reason === "aborted" ? "Run interrupted." : event.reason === "max_turns" ? "Stopped at the configured turn limit." : event.reason === "token_budget" ? "Stopped at the configured token budget." : event.reason
18641
+ text: event.reason === "aborted" ? "Run interrupted. Use /recover to inspect changes or resume safely." : event.reason === "max_turns" ? "Stopped at the configured turn limit. Use /recover resume after adjusting the limit." : event.reason === "token_budget" ? "Stopped at the configured token budget. Use /recover to inspect state before resuming with a larger budget." : event.reason
18376
18642
  });
18377
18643
  }
18378
18644
  break;
@@ -18386,9 +18652,16 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18386
18652
  const runLocalCommand = useCallback(async (value) => {
18387
18653
  if (!value.startsWith("/")) return false;
18388
18654
  const [rawCommand = "", ...rest] = value.slice(1).trim().split(/\s+/);
18389
- const command2 = rawCommand.toLocaleLowerCase();
18390
- const argument = rest.join(" ").trim();
18655
+ let command2 = rawCommand.toLocaleLowerCase();
18656
+ let argument = rest.join(" ").trim();
18391
18657
  if (!command2) return true;
18658
+ if (command2 === "recover") {
18659
+ const [action = "", ...actionRest] = argument.split(/\s+/u);
18660
+ if (action === "diff" || action === "audit" || action === "rollback") {
18661
+ command2 = action;
18662
+ argument = actionRest.join(" ").trim();
18663
+ }
18664
+ }
18392
18665
  if (command2 === "exit" || command2 === "quit") {
18393
18666
  exit();
18394
18667
  return true;
@@ -18470,10 +18743,77 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18470
18743
  });
18471
18744
  return true;
18472
18745
  }
18746
+ if (command2 === "review") {
18747
+ const scope = parseReviewScope(argument);
18748
+ const bundle = buildRedactedReviewBundle(runner.getSession(), runner.workspace.primaryRoot, scope);
18749
+ return {
18750
+ kind: "agent",
18751
+ display: `/review ${scope.kind}${scope.kind === "working-tree" ? "" : ` ${scope.ref}`}`,
18752
+ runInput: reviewRequest(scope),
18753
+ turnInstructions: reviewTurnInstructions(bundle),
18754
+ readOnly: true
18755
+ };
18756
+ }
18757
+ if (command2 === "recover") {
18758
+ const action = argument.toLocaleLowerCase();
18759
+ const currentSession = runner.getSession();
18760
+ const lastRun = currentSession.lastRun;
18761
+ const lastFailure = (currentSession.audit ?? []).findLast((event) => event.type === "tool" && event.outcome === "failure");
18762
+ const failure = recoveryFailure(lastFailure?.metadata?.failure);
18763
+ if (action === "retry") {
18764
+ if (currentSession.pendingInput) throw new Error("Answer the pending clarification before retrying an operation.");
18765
+ if (!lastFailure) throw new Error("No failed operation is available to retry.");
18766
+ return {
18767
+ kind: "agent",
18768
+ display: "/recover retry",
18769
+ runInput: `Retry the most recent failed ${lastFailure.tool} operation after inspecting the current workspace state.`,
18770
+ turnInstructions: "Resume the existing session. Use the recorded failure receipt and current files as authority. Apply its repair hint before one targeted retry; do not replay a circuit-open or non-retryable operation unchanged."
18771
+ };
18772
+ }
18773
+ if (action === "resume") {
18774
+ if (currentSession.pendingInput) throw new Error("Answer the pending clarification in the composer to resume the same logical run.");
18775
+ if (!lastRun || lastRun.reason === "completed") throw new Error("The latest run is already complete.");
18776
+ return {
18777
+ kind: "agent",
18778
+ display: "/recover resume",
18779
+ runInput: "Resume the most recent incomplete run from its persisted state.",
18780
+ turnInstructions: "Resume the existing session from its Task Contract, changed-file set, last-run receipt, and unresolved failures. Inspect current state before acting, keep prior successful evidence, and run only missing verification."
18781
+ };
18782
+ }
18783
+ if (action) throw new Error("Usage: /recover [retry|resume|diff|rollback|audit]");
18784
+ const checkpoints = await runner.checkpointStore.list(currentSession.id);
18785
+ const latestCheckpoint = checkpoints[0];
18786
+ appendList("Recovery Center", [
18787
+ {
18788
+ label: `Last run ${lastRun?.status ?? "none"}${lastRun ? `${separator}${lastRun.reason}` : ""}`,
18789
+ detail: lastRun?.detail ?? "No completed or interrupted run has been recorded.",
18790
+ tone: lastRun?.status === "verified" || lastRun?.status === "no_changes" ? "success" : lastRun ? "warning" : "normal"
18791
+ },
18792
+ ...lastFailure ? [{
18793
+ label: `Failure ${lastFailure.tool}${failure?.class ? `${separator}${failure.class}` : ""}`,
18794
+ detail: failure?.repairHint ?? "Inspect the audit timeline before retrying.",
18795
+ tone: "error"
18796
+ }] : [],
18797
+ {
18798
+ label: `Workspace ${currentSession.changedFiles.length} changed file${currentSession.changedFiles.length === 1 ? "" : "s"}`,
18799
+ detail: currentSession.changedFiles.length ? "/diff inspects the current patch." : "No tracked session changes."
18800
+ },
18801
+ {
18802
+ label: `Checkpoint ${latestCheckpoint ? latestCheckpoint.id.slice(0, 12) : "none"}`,
18803
+ detail: latestCheckpoint ? `${latestCheckpoint.reason}${separator}${latestCheckpoint.entries.length} files${separator}/recover rollback` : "No pre-mutation snapshot is available for this session."
18804
+ },
18805
+ { label: "/recover retry", detail: lastFailure ? "Apply the repair hint, then retry the latest failure once." : "Unavailable until an operation fails." },
18806
+ { label: "/recover resume", detail: currentSession.pendingInput ? "Unavailable: answer the pending clarification in the composer." : lastRun && lastRun.reason !== "completed" ? "Continue the incomplete logical run." : "No incomplete run to resume." },
18807
+ { label: "/recover diff", detail: "Inspect the current workspace patch." },
18808
+ { label: "/recover audit", detail: "Review permission and tool evidence." },
18809
+ { label: "/recover rollback", detail: "Choose a checkpoint to restore." }
18810
+ ]);
18811
+ return true;
18812
+ }
18473
18813
  if (command2 === "changes") {
18474
18814
  const changed = runner.getSession().changedFiles;
18475
18815
  appendList("Changed files", changed.length ? changed.map((path) => ({
18476
- label: relative9(runner.workspace.primaryRoot, path) || ".",
18816
+ label: relative10(runner.workspace.primaryRoot, path) || ".",
18477
18817
  detail: path
18478
18818
  })) : [{ label: "No recorded changes." }]);
18479
18819
  return true;
@@ -18626,7 +18966,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18626
18966
  const skills = extensions?.listSkills() ?? [];
18627
18967
  appendList("Skills", skills.length ? skills.map((skill) => ({
18628
18968
  label: `${skill.name} ${skill.scope}${skill.trusted ? "" : `${separator}untrusted`}`,
18629
- detail: `${skill.description}${separator}${relative9(runner.workspace.primaryRoot, skill.path) || skill.path}`,
18969
+ detail: `${skill.description}${separator}${relative10(runner.workspace.primaryRoot, skill.path) || skill.path}`,
18630
18970
  tone: skill.trusted ? "normal" : "warning"
18631
18971
  })) : [{ label: "No skills discovered.", detail: "Add SKILL.md playbooks under .agents/skills, .claude/skills, or a configured directory." }]);
18632
18972
  return true;
@@ -18956,7 +19296,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18956
19296
  lastEventError.current = void 0;
18957
19297
  try {
18958
19298
  const nextSession = await runner.run(current.runInput, {
18959
- askMode: interactionMode !== "build",
19299
+ askMode: current.readOnly === true || interactionMode !== "build",
18960
19300
  signal: abortController.signal,
18961
19301
  ...current.turnInstructions || interactionMode === "plan" ? {
18962
19302
  turnInstructions: [
@@ -19026,10 +19366,10 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19026
19366
  }, [append, exit, interactionMode, onEvent, requestPermission, runLocalCommand, runner]);
19027
19367
  const submitFromComposer = useCallback((raw, mode = "normal") => {
19028
19368
  if (historySearch) {
19029
- const selected = resolveHistorySearch(historySearch, "select");
19369
+ const selected2 = resolveHistorySearch(historySearch, "select");
19030
19370
  setHistorySearch(void 0);
19031
19371
  setHistoryIndex(-1);
19032
- void submit(selected, mode === "normal" && processing.current ? "steer" : mode);
19372
+ void submit(selected2, mode === "normal" && processing.current ? "steer" : mode);
19033
19373
  return;
19034
19374
  }
19035
19375
  if (suggestionMode === "mention" && selectedSuggestion) {
@@ -19040,7 +19380,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19040
19380
  return;
19041
19381
  }
19042
19382
  }
19043
- const suggestion = selectedSuggestion;
19383
+ const selected = selectedSuggestion;
19384
+ const suggestion = selected && !(raw.startsWith(selected.value) && raw.slice(selected.value.length).trim()) ? selected : void 0;
19044
19385
  const normalized = raw.trimEnd();
19045
19386
  if (suggestion && raw.startsWith("/") && suggestion.value !== raw && suggestion.value.endsWith(" ") && suggestion.label !== normalized) {
19046
19387
  setInput(suggestion.value);
@@ -19079,6 +19420,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19079
19420
  text: `Allowed ${call.name} for this exact ${category} target during the session.`
19080
19421
  });
19081
19422
  }
19423
+ if (grant === false) {
19424
+ append({
19425
+ id: nextId(),
19426
+ kind: "notice",
19427
+ tone: "info",
19428
+ text: `Denied ${call.name}; the requested ${category} action was not run. Use /permissions to inspect policy or /recover to review the run.`
19429
+ });
19430
+ }
19082
19431
  if (stop) {
19083
19432
  requestRunStop();
19084
19433
  }
@@ -19422,7 +19771,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19422
19771
  )
19423
19772
  }
19424
19773
  )
19425
- ] }) : /* @__PURE__ */ jsx3(PermissionCard, { call: permission.call, category: permission.category, workspace: runner.workspace.primaryRoot, width: contentWidth, glyphMode, compact: constrainedHeight }),
19774
+ ] }) : /* @__PURE__ */ jsx3(PermissionCard, { call: permission.call, category: permission.category, ...permission.reason ? { reason: permission.reason } : {}, workspace: runner.workspace.primaryRoot, width: contentWidth, glyphMode, compact: constrainedHeight }),
19426
19775
  showFooter ? /* @__PURE__ */ jsx3(
19427
19776
  Footer,
19428
19777
  {
@@ -19562,6 +19911,14 @@ function cloneValue(value) {
19562
19911
  if (value && typeof value === "object") return cloneRecord(value);
19563
19912
  return value;
19564
19913
  }
19914
+ function recoveryFailure(value) {
19915
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
19916
+ const receipt = value;
19917
+ return {
19918
+ ...typeof receipt.class === "string" ? { class: sanitizeTerminalText(receipt.class).slice(0, 40) } : {},
19919
+ ...typeof receipt.repairHint === "string" ? { repairHint: sanitizeTerminalText(receipt.repairHint).replace(/\s+/gu, " ").slice(0, 240) } : {}
19920
+ };
19921
+ }
19565
19922
  function isExitCommand(value) {
19566
19923
  const command2 = localCommandName(value);
19567
19924
  return command2 === "exit" || command2 === "quit";
@@ -19579,6 +19936,9 @@ function shouldDeferLocalCommand(value) {
19579
19936
  "remember",
19580
19937
  "diff",
19581
19938
  "checkpoints",
19939
+ "audit",
19940
+ "rollback",
19941
+ "recover",
19582
19942
  "workflow",
19583
19943
  "exit",
19584
19944
  "quit"
@@ -19598,7 +19958,7 @@ function composerAttachments(value) {
19598
19958
  return [...new Set(paths)].slice(-3);
19599
19959
  }
19600
19960
  function permissionRows(width, hasCwd, compact3) {
19601
- const content = 3 + (hasCwd ? 1 : 0);
19961
+ const content = 5 + (hasCwd ? 1 : 0);
19602
19962
  if (width >= 64) return content + 2;
19603
19963
  if (width >= 28) return content + 3;
19604
19964
  if (compact3) return content + 3;
@@ -20392,7 +20752,7 @@ function titleForStep(step2) {
20392
20752
  return "Saving configuration";
20393
20753
  }
20394
20754
  function descriptionForStep(state) {
20395
- if (state.step === "method") return "Pick one connection path. You can change it later with configuration.";
20755
+ if (state.step === "method") return "Skein's primary agent needs an API credential. OpenAI, Anthropic, and Gemini subscription logins are not API keys; signed-in coding CLIs are separate delegated tools.";
20396
20756
  if (state.step === "official-provider") return "Use the API credential issued by the selected provider.";
20397
20757
  if (state.step === "relay-protocol") return "The protocol is explicit so requests and credentials are never guessed.";
20398
20758
  if (state.step === "endpoint") return "Remote endpoints require HTTPS. Loopback development servers may use HTTP.";
@@ -22636,14 +22996,26 @@ async function runCli() {
22636
22996
  try {
22637
22997
  await program.parseAsync(process.argv);
22638
22998
  } catch (error) {
22639
- const message2 = error instanceof Error ? error.message : String(error);
22640
- process.stderr.write(`${chalk4.red(cliGlyphs.error)} ${message2}
22999
+ const format = structuredOutputFormat(process.argv);
23000
+ if (format) {
23001
+ const reporter = new HeadlessReporter({ format, color: false });
23002
+ process.exitCode = reporter.fail(error).exitCode;
23003
+ } else {
23004
+ const message2 = error instanceof Error ? error.message : String(error);
23005
+ process.stderr.write(`${chalk4.red(cliGlyphs.error)} ${message2}
22641
23006
  `);
22642
- process.exitCode = 1;
23007
+ process.exitCode = 1;
23008
+ }
22643
23009
  } finally {
22644
23010
  releaseCliNamespaceLeases(cliNamespaceLeases);
22645
23011
  }
22646
23012
  }
23013
+ function structuredOutputFormat(argv) {
23014
+ const inline = argv.find((argument) => argument.startsWith("--output-format="))?.slice("--output-format=".length);
23015
+ const index = argv.indexOf("--output-format");
23016
+ const value = inline ?? (index >= 0 ? argv[index + 1] : void 0);
23017
+ return value === "json" || value === "stream-json" ? value : void 0;
23018
+ }
22647
23019
  async function runChat(prompts, options) {
22648
23020
  const shouldPrint = options.print === true || !process.stdin.isTTY || !process.stdout.isTTY;
22649
23021
  if (options.ask && options.plan) throw new Error("--ask and --plan cannot be used together.");
@@ -22711,7 +23083,8 @@ async function runChat(prompts, options) {
22711
23083
  color: (options.color ?? config.ui.color) && !process.env.NO_COLOR
22712
23084
  });
22713
23085
  const colorOutput = (options.color ?? config.ui.color) && !process.env.NO_COLOR;
22714
- const requestPermission = options.yes ? async () => true : options.autoEdit ? async (_call, category) => category === "read" || category === "write" ? true : askConsolePermission(_call, category, colorOutput) : async (call, category) => askConsolePermission(call, category, colorOutput);
23086
+ const requestPermission = options.yes ? async () => true : options.autoEdit ? async (_call, category, reason) => category === "read" || category === "write" ? true : askConsolePermission(_call, category, colorOutput, reason) : async (call, category, reason) => askConsolePermission(call, category, colorOutput, reason);
23087
+ let extensionsClosed = false;
22715
23088
  try {
22716
23089
  let session = await runner.run(firstPrompt, {
22717
23090
  askMode: options.ask === true || options.plan === true,
@@ -22730,13 +23103,15 @@ async function runChat(prompts, options) {
22730
23103
  requestPermission
22731
23104
  });
22732
23105
  }
22733
- reporter.finish(session);
22734
- if (session.pendingInput) process.exitCode = 2;
23106
+ await extensions.close();
23107
+ extensionsClosed = true;
23108
+ const outcome2 = reporter.finish(session);
23109
+ process.exitCode = outcome2.exitCode;
22735
23110
  } catch (error) {
22736
- reporter.fail(error);
22737
- process.exitCode = 1;
23111
+ const outcome2 = reporter.fail(error, runner.getSession());
23112
+ process.exitCode = outcome2.exitCode;
22738
23113
  } finally {
22739
- await extensions.close();
23114
+ if (!extensionsClosed) await extensions.close().catch(() => void 0);
22740
23115
  }
22741
23116
  }
22742
23117
  async function openMemoryStore(config) {