@skein-code/cli 0.3.24 → 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.24",
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,17 @@ 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",
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",
246
+ "Content-free epoch handoffs preserve Task Contract criteria, unresolved failure circuits, changed files, and verification receipts across long runs",
247
+ "Intent Sufficiency routes clear requests to execution, repository-inferable gaps to inspection, and genuine product choices to one persisted clarification",
248
+ "TUI and headless output expose epoch, lifetime, and needs-input state; queued follow-ups pause and resume after keyboard clarification instead of becoming accidental answers",
249
+ "Successful identical tool calls clear matching historical failure signatures from later epoch handoffs",
239
250
  "Context compaction now rebuilds authoritative task, working-state, verification, permission, failure, and artifact facts outside generated narrative",
240
251
  "Automatic compaction runs only when three predicted prompt reuses produce positive net token savings while explicit compact commands remain available",
241
252
  "Compaction provider usage is included in session totals with separate content-free actual or estimated receipts",
@@ -1993,6 +2004,7 @@ var partialConfigSchema = z2.object({
1993
2004
  }).partial().optional(),
1994
2005
  agent: z2.object({
1995
2006
  maxTurns: z2.number().int().positive().optional(),
2007
+ maxEpochTokens: z2.number().int().positive().optional(),
1996
2008
  maxSessionTokens: z2.number().int().positive().optional(),
1997
2009
  autoVerify: z2.boolean().optional(),
1998
2010
  verifyCommands: z2.array(z2.string()).optional(),
@@ -2072,7 +2084,8 @@ function defaultConfig(workspace = process.cwd()) {
2072
2084
  hooks: {},
2073
2085
  agent: {
2074
2086
  maxTurns: 24,
2075
- maxSessionTokens: 25e4,
2087
+ maxEpochTokens: 25e4,
2088
+ maxSessionTokens: 1e6,
2076
2089
  autoVerify: true,
2077
2090
  verifyCommands: [],
2078
2091
  checkpointBeforeWrite: true
@@ -2436,6 +2449,7 @@ function configSummary(config) {
2436
2449
  workspaceRoots: config.workspaceRoots,
2437
2450
  permissions: config.permissions,
2438
2451
  maxTurns: config.agent.maxTurns,
2452
+ maxEpochTokens: config.agent.maxEpochTokens,
2439
2453
  maxSessionTokens: config.agent.maxSessionTokens,
2440
2454
  autoVerify: config.agent.autoVerify,
2441
2455
  skills: config.skills ? {
@@ -4720,10 +4734,127 @@ function formatContextHits(hits, roots) {
4720
4734
  }
4721
4735
 
4722
4736
  // src/agent/runner.ts
4723
- import { randomUUID as randomUUID13 } from "node:crypto";
4737
+ import { randomUUID as randomUUID15 } from "node:crypto";
4724
4738
 
4725
4739
  // src/context/manager.ts
4740
+ import { randomUUID as randomUUID5 } from "node:crypto";
4741
+
4742
+ // src/context/epochs.ts
4726
4743
  import { randomUUID as randomUUID4 } from "node:crypto";
4744
+ var MAX_EPOCHS = 64;
4745
+ var MAX_HANDOFF_FAILURES = 16;
4746
+ function activeContextEpoch(session) {
4747
+ return ensureContextEpoch(session);
4748
+ }
4749
+ function ensureContextEpoch(session) {
4750
+ const epochs = session.contextEpochs ?? (session.contextEpochs = []);
4751
+ const active = epochs.at(-1);
4752
+ if (active && active.finishedAt === void 0) return active;
4753
+ const next = createEpoch((active?.index ?? 0) + 1);
4754
+ epochs.push(next);
4755
+ trimEpochs(epochs);
4756
+ return next;
4757
+ }
4758
+ function recordContextEpochUsage(session, inputTokens, outputTokens) {
4759
+ const epoch = ensureContextEpoch(session);
4760
+ epoch.usage.inputTokens += boundedTokens(inputTokens);
4761
+ epoch.usage.outputTokens += boundedTokens(outputTokens);
4762
+ return epoch;
4763
+ }
4764
+ function contextEpochTokens(session) {
4765
+ const epoch = ensureContextEpoch(session);
4766
+ return epoch.usage.inputTokens + epoch.usage.outputTokens;
4767
+ }
4768
+ function rotateContextEpoch(session, reason, compaction) {
4769
+ const previous = ensureContextEpoch(session);
4770
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
4771
+ const handoff = buildEpochHandoff(session, reason, createdAt, compaction);
4772
+ previous.finishedAt = createdAt;
4773
+ previous.handoff = handoff;
4774
+ const current = createEpoch(previous.index + 1, createdAt);
4775
+ const epochs = session.contextEpochs ?? (session.contextEpochs = []);
4776
+ epochs.push(current);
4777
+ trimEpochs(epochs);
4778
+ return { previous, current, handoff };
4779
+ }
4780
+ function buildEpochHandoff(session, reason, createdAt, compaction) {
4781
+ const failures = unresolvedFailureReceipts(session);
4782
+ const contract = session.taskContract;
4783
+ return {
4784
+ reason,
4785
+ createdAt,
4786
+ ...compaction ? { compactionReceiptId: compaction.id } : {},
4787
+ ...session.compactedThroughMessageId ? { compactedThroughMessageId: session.compactedThroughMessageId } : {},
4788
+ ...contract ? {
4789
+ contract: {
4790
+ state: contract.state,
4791
+ required: contract.acceptanceCriteria.filter((criterion2) => criterion2.required).map((criterion2) => ({
4792
+ id: criterion2.id,
4793
+ status: criterion2.status,
4794
+ evidenceRefs: [...criterion2.evidenceRefs]
4795
+ }))
4796
+ }
4797
+ } : {},
4798
+ unresolvedFailures: failures,
4799
+ changedFiles: [...new Set(session.changedFiles)].slice(-256),
4800
+ checks: session.lastRun?.checks.map((check) => ({ ...check })) ?? []
4801
+ };
4802
+ }
4803
+ function unresolvedFailureReceipts(session) {
4804
+ const unresolved2 = /* @__PURE__ */ new Map();
4805
+ for (const event of session.audit ?? []) {
4806
+ if (event.type !== "tool") continue;
4807
+ if (event.outcome === "failure") {
4808
+ const receipt = failureReceipt(event.metadata?.failure);
4809
+ if (receipt) unresolved2.set(receipt.signature, receipt);
4810
+ continue;
4811
+ }
4812
+ const resolved = event.metadata?.resolvedFailureSignatures;
4813
+ if (!Array.isArray(resolved)) continue;
4814
+ for (const signature of resolved) {
4815
+ if (typeof signature === "string") unresolved2.delete(signature);
4816
+ }
4817
+ }
4818
+ return [...unresolved2.values()].slice(-MAX_HANDOFF_FAILURES);
4819
+ }
4820
+ function failureReceipt(value) {
4821
+ if (!value || typeof value !== "object") return;
4822
+ const candidate = value;
4823
+ const classes = /* @__PURE__ */ new Set([
4824
+ "schema_input",
4825
+ "unknown_tool",
4826
+ "permission_denied",
4827
+ "command_exit",
4828
+ "timeout",
4829
+ "cancelled",
4830
+ "hook",
4831
+ "execution",
4832
+ "no_progress",
4833
+ "contract_required"
4834
+ ]);
4835
+ if (typeof candidate.signature !== "string" || !candidate.signature || typeof candidate.class !== "string" || !classes.has(candidate.class) || typeof candidate.circuitOpen !== "boolean") return;
4836
+ return {
4837
+ signature: candidate.signature.slice(0, 256),
4838
+ class: candidate.class,
4839
+ circuitOpen: candidate.circuitOpen
4840
+ };
4841
+ }
4842
+ function createEpoch(index, startedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4843
+ return {
4844
+ id: randomUUID4(),
4845
+ index,
4846
+ startedAt,
4847
+ usage: { inputTokens: 0, outputTokens: 0 }
4848
+ };
4849
+ }
4850
+ function trimEpochs(epochs) {
4851
+ if (epochs.length > MAX_EPOCHS) epochs.splice(0, epochs.length - MAX_EPOCHS);
4852
+ }
4853
+ function boundedTokens(value) {
4854
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
4855
+ }
4856
+
4857
+ // src/context/manager.ts
4727
4858
  var RECENT_TURN_RESERVE = 3;
4728
4859
  var COMPACTION_HIGH_WATER = 0.78;
4729
4860
  var TOOL_PRESSURE_WATER = 0.28;
@@ -4776,13 +4907,21 @@ var ContextManager = class {
4776
4907
  modelContextTokens ?? Math.min(1e5, this.config.context.maxTokens * 3)
4777
4908
  );
4778
4909
  const compactedMessages = session.compactedThroughMessageId ? Math.max(0, session.messages.findIndex((message2) => message2.id === session.compactedThroughMessageId) + 1) : 0;
4910
+ const epoch = activeContextEpoch(session);
4911
+ const epochBudget = this.config.agent.maxEpochTokens ?? this.config.agent.maxSessionTokens;
4779
4912
  return {
4780
4913
  activeTokens,
4781
4914
  summaryTokens,
4782
4915
  toolTokens: toolTokenCount,
4783
4916
  messageCount: active.length,
4784
4917
  compactedMessages,
4785
- pressure: Math.min(1, (activeTokens + summaryTokens) / contextLimit)
4918
+ pressure: Math.min(1, (activeTokens + summaryTokens) / contextLimit),
4919
+ epochIndex: epoch.index,
4920
+ epochCount: epoch.index,
4921
+ epochTokens: epoch.usage.inputTokens + epoch.usage.outputTokens,
4922
+ epochBudget,
4923
+ lifetimeTokens: session.usage.inputTokens + session.usage.outputTokens,
4924
+ lifetimeBudget: this.config.agent.maxSessionTokens
4786
4925
  };
4787
4926
  }
4788
4927
  shouldCompact(session, tokenBudget) {
@@ -4891,7 +5030,7 @@ function compactionEstimate(session, older, facts, request) {
4891
5030
  }
4892
5031
  function skippedCompaction(session, mode, reason, estimate = emptyCompactionEstimate(session)) {
4893
5032
  const receipt = {
4894
- id: randomUUID4(),
5033
+ id: randomUUID5(),
4895
5034
  recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
4896
5035
  mode,
4897
5036
  status: "skipped",
@@ -4928,7 +5067,7 @@ function emptyCompactionEstimate(session) {
4928
5067
  function completedCompactionReceipt(mode, omittedMessages, compactedThroughMessageId, estimated, response, summary) {
4929
5068
  const actual = actualUsage(response.usage);
4930
5069
  return {
4931
- id: randomUUID4(),
5070
+ id: randomUUID5(),
4932
5071
  recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
4933
5072
  mode,
4934
5073
  status: "compacted",
@@ -4967,6 +5106,7 @@ function buildCompactionFactsEnvelope(session, throughMessageId) {
4967
5106
  const memory = session.workingMemory;
4968
5107
  const contract = session.taskContract;
4969
5108
  const lastRun = session.lastRun;
5109
+ const epoch = activeContextEpoch(session);
4970
5110
  const directives = olderUserDirectives(session, throughMessageId ?? session.compactedThroughMessageId);
4971
5111
  const permissions = recentPermissionFacts(session.audit ?? []);
4972
5112
  const failures = recentFailureFacts(session.audit ?? []);
@@ -5019,6 +5159,11 @@ ${factList(memory?.relevantFiles ?? [])}
5019
5159
  Session changed files:
5020
5160
  ${factList(session.changedFiles)}
5021
5161
 
5162
+ Context epoch ledger:
5163
+ - Current epoch: ${epoch.index}
5164
+ - Current epoch usage: input=${epoch.usage.inputTokens}; output=${epoch.usage.outputTokens}
5165
+ - Lifetime usage: input=${session.usage.inputTokens}; output=${session.usage.outputTokens}
5166
+
5022
5167
  Last-run verification and residual state:
5023
5168
  ${lastRunLines}
5024
5169
 
@@ -5462,7 +5607,7 @@ function normalizeForMatch(value) {
5462
5607
  }
5463
5608
 
5464
5609
  // src/providers/anthropic.ts
5465
- import { randomUUID as randomUUID5 } from "node:crypto";
5610
+ import { randomUUID as randomUUID6 } from "node:crypto";
5466
5611
 
5467
5612
  // src/providers/provider.ts
5468
5613
  var ProviderError = class extends Error {
@@ -5587,7 +5732,7 @@ var AnthropicProvider = class {
5587
5732
  return {
5588
5733
  content: blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n"),
5589
5734
  toolCalls: blocks.filter((block) => block.type === "tool_use").map((block) => ({
5590
- id: block.id ?? randomUUID5(),
5735
+ id: block.id ?? randomUUID6(),
5591
5736
  name: block.name ?? "unknown",
5592
5737
  arguments: safeJsonArguments(block.input)
5593
5738
  })),
@@ -5661,7 +5806,7 @@ var AnthropicProvider = class {
5661
5806
  }
5662
5807
  if (chunk.type === "content_block_start" && chunk.content_block?.type === "tool_use") {
5663
5808
  calls.set(index, {
5664
- id: chunk.content_block.id ?? randomUUID5(),
5809
+ id: chunk.content_block.id ?? randomUUID6(),
5665
5810
  name: chunk.content_block.name ?? "unknown",
5666
5811
  input: chunk.content_block.input,
5667
5812
  partialJson: ""
@@ -5672,7 +5817,7 @@ var AnthropicProvider = class {
5672
5817
  yield { type: "text_delta", content: chunk.delta.text };
5673
5818
  }
5674
5819
  if (chunk.type === "content_block_delta" && chunk.delta?.type === "input_json_delta") {
5675
- const call = calls.get(index) ?? { id: randomUUID5(), name: "unknown", input: void 0, partialJson: "" };
5820
+ const call = calls.get(index) ?? { id: randomUUID6(), name: "unknown", input: void 0, partialJson: "" };
5676
5821
  call.partialJson += chunk.delta.partial_json ?? "";
5677
5822
  calls.set(index, call);
5678
5823
  }
@@ -5702,7 +5847,7 @@ function normalizeAnthropicResponse(data) {
5702
5847
  return {
5703
5848
  content: blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n"),
5704
5849
  toolCalls: blocks.filter((block) => block.type === "tool_use").map((block) => ({
5705
- id: block.id ?? randomUUID5(),
5850
+ id: block.id ?? randomUUID6(),
5706
5851
  name: block.name ?? "unknown",
5707
5852
  arguments: safeJsonArguments(block.input)
5708
5853
  })),
@@ -5747,7 +5892,7 @@ function toAnthropicMessage(message2) {
5747
5892
  }
5748
5893
 
5749
5894
  // src/providers/gemini.ts
5750
- import { randomUUID as randomUUID6 } from "node:crypto";
5895
+ import { randomUUID as randomUUID7 } from "node:crypto";
5751
5896
  var GeminiProvider = class {
5752
5897
  constructor(config) {
5753
5898
  this.config = config;
@@ -5787,7 +5932,7 @@ var GeminiProvider = class {
5787
5932
  return {
5788
5933
  content: parts.map((part) => part.text ?? "").filter(Boolean).join("\n"),
5789
5934
  toolCalls: parts.filter((part) => part.functionCall).map((part) => ({
5790
- id: randomUUID6(),
5935
+ id: randomUUID7(),
5791
5936
  name: part.functionCall?.name ?? "unknown",
5792
5937
  arguments: safeJsonArguments(part.functionCall?.args)
5793
5938
  })),
@@ -5859,7 +6004,7 @@ var GeminiProvider = class {
5859
6004
  }
5860
6005
  if (part.functionCall) {
5861
6006
  toolCalls.push({
5862
- id: randomUUID6(),
6007
+ id: randomUUID7(),
5863
6008
  name: part.functionCall.name ?? "unknown",
5864
6009
  arguments: safeJsonArguments(part.functionCall.args)
5865
6010
  });
@@ -5888,7 +6033,7 @@ function normalizeGeminiResponse(data) {
5888
6033
  return {
5889
6034
  content: parts.map((part) => part.text ?? "").filter(Boolean).join("\n"),
5890
6035
  toolCalls: parts.filter((part) => part.functionCall).map((part) => ({
5891
- id: randomUUID6(),
6036
+ id: randomUUID7(),
5892
6037
  name: part.functionCall?.name ?? "unknown",
5893
6038
  arguments: safeJsonArguments(part.functionCall?.args)
5894
6039
  })),
@@ -5932,7 +6077,7 @@ function toGeminiMessage(message2) {
5932
6077
  }
5933
6078
 
5934
6079
  // src/providers/openai.ts
5935
- import { randomUUID as randomUUID7 } from "node:crypto";
6080
+ import { randomUUID as randomUUID8 } from "node:crypto";
5936
6081
  var OpenAIProvider = class {
5937
6082
  constructor(config) {
5938
6083
  this.config = config;
@@ -6055,7 +6200,7 @@ var OpenAIProvider = class {
6055
6200
  }
6056
6201
  for (const fragment of delta?.tool_calls ?? []) {
6057
6202
  const index = fragment.index ?? 0;
6058
- const current = calls.get(index) ?? { id: randomUUID7(), name: "", arguments: "" };
6203
+ const current = calls.get(index) ?? { id: randomUUID8(), name: "", arguments: "" };
6059
6204
  if (fragment.id) current.id = fragment.id;
6060
6205
  if (fragment.function?.name) current.name += fragment.function.name;
6061
6206
  if (fragment.function?.arguments) current.arguments += fragment.function.arguments;
@@ -6089,7 +6234,7 @@ function normalizeOpenAIResponse(data) {
6089
6234
  return {
6090
6235
  content: message2.content ?? "",
6091
6236
  toolCalls: (message2.tool_calls ?? []).map((call) => ({
6092
- id: call.id ?? randomUUID7(),
6237
+ id: call.id ?? randomUUID8(),
6093
6238
  name: call.function?.name ?? "unknown",
6094
6239
  arguments: safeJsonArguments(call.function?.arguments)
6095
6240
  })),
@@ -6145,7 +6290,7 @@ function createProvider(config) {
6145
6290
  }
6146
6291
 
6147
6292
  // src/checkpoint/store.ts
6148
- import { createHash as createHash8, randomUUID as randomUUID8 } from "node:crypto";
6293
+ import { createHash as createHash8, randomUUID as randomUUID9 } from "node:crypto";
6149
6294
  import { lstat as lstat10, mkdir as mkdir7, readFile as readFile5, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
6150
6295
  import { basename as basename6, dirname as dirname8, join as join9, relative as relative6, resolve as resolve12 } from "node:path";
6151
6296
  import { z as z4 } from "zod";
@@ -6181,7 +6326,7 @@ var CheckpointStore = class {
6181
6326
  return this.withManagedLease(() => this.captureUnlocked(sessionId, unique3, options));
6182
6327
  }
6183
6328
  async captureUnlocked(sessionId, unique3, options) {
6184
- const id = `${Date.now().toString(36)}-${randomUUID8()}`;
6329
+ const id = `${Date.now().toString(36)}-${randomUUID9()}`;
6185
6330
  const target = join9(this.directory, sessionId, id);
6186
6331
  const blobDirectory = join9(target, "blobs");
6187
6332
  await this.ensureDirectory();
@@ -6457,7 +6602,7 @@ var HookRunner = class {
6457
6602
  };
6458
6603
 
6459
6604
  // src/session/store.ts
6460
- import { randomUUID as randomUUID9 } from "node:crypto";
6605
+ import { randomUUID as randomUUID10 } from "node:crypto";
6461
6606
  import { constants as constants4 } from "node:fs";
6462
6607
  import {
6463
6608
  chmod as chmod6,
@@ -6683,6 +6828,76 @@ var contextCompactionReceiptSchema = z5.object({
6683
6828
  outputSource: z5.enum(["actual", "estimated", "none"]),
6684
6829
  narrative: z5.enum(["present", "empty", "not-requested"])
6685
6830
  }).strict();
6831
+ var contextEpochSchema = z5.object({
6832
+ id: z5.string().uuid(),
6833
+ index: z5.number().int().positive(),
6834
+ startedAt: z5.string().datetime(),
6835
+ finishedAt: z5.string().datetime().optional(),
6836
+ usage: z5.object({
6837
+ inputTokens: z5.number().int().nonnegative(),
6838
+ outputTokens: z5.number().int().nonnegative()
6839
+ }).strict(),
6840
+ handoff: z5.object({
6841
+ reason: z5.enum(["token_budget", "context_pressure", "manual"]),
6842
+ createdAt: z5.string().datetime(),
6843
+ compactionReceiptId: z5.string().uuid().optional(),
6844
+ compactedThroughMessageId: z5.string().optional(),
6845
+ contract: z5.object({
6846
+ state: z5.enum(["draft", "active", "satisfied", "blocked"]),
6847
+ required: z5.array(z5.object({
6848
+ id: z5.string().min(1).max(128),
6849
+ status: z5.enum(["pending", "satisfied", "blocked"]),
6850
+ evidenceRefs: z5.array(z5.string().min(1).max(256)).max(64)
6851
+ }).strict()).max(64)
6852
+ }).strict().optional(),
6853
+ unresolvedFailures: z5.array(z5.object({
6854
+ signature: z5.string().min(1).max(256),
6855
+ class: z5.enum([
6856
+ "schema_input",
6857
+ "unknown_tool",
6858
+ "permission_denied",
6859
+ "command_exit",
6860
+ "timeout",
6861
+ "cancelled",
6862
+ "hook",
6863
+ "execution",
6864
+ "no_progress",
6865
+ "contract_required"
6866
+ ]),
6867
+ circuitOpen: z5.boolean()
6868
+ }).strict()).max(16),
6869
+ changedFiles: z5.array(z5.string().max(4096)).max(256),
6870
+ checks: z5.array(verificationEvidenceSchema).max(64)
6871
+ }).strict().optional()
6872
+ }).strict();
6873
+ var intentAssessmentSchema = z5.object({
6874
+ version: z5.literal(1),
6875
+ route: z5.enum(["direct_execute", "inspect_then_execute", "needs_input", "permission_required"]),
6876
+ reasons: z5.array(z5.enum([
6877
+ "simple_explicit_request",
6878
+ "workspace_inference_available",
6879
+ "explicit_user_choice_missing",
6880
+ "public_api_compatibility_missing",
6881
+ "runtime_permission_separate",
6882
+ "clarification_resolved"
6883
+ ])).min(1).max(8),
6884
+ assessedAt: z5.string().datetime(),
6885
+ retrievalHits: z5.number().int().nonnegative()
6886
+ }).strict();
6887
+ var pendingInputSchema = z5.object({
6888
+ id: z5.string().uuid(),
6889
+ runId: z5.string().uuid(),
6890
+ createdAt: z5.string().datetime(),
6891
+ originalRequest: z5.string().min(1).max(12e4),
6892
+ question: z5.string().min(1).max(2e3),
6893
+ options: z5.array(z5.object({
6894
+ id: z5.string().regex(/^[a-z][a-z0-9_-]{0,63}$/u),
6895
+ label: z5.string().min(1).max(160),
6896
+ impact: z5.string().min(1).max(500),
6897
+ recommended: z5.boolean()
6898
+ }).strict()).min(2).max(3),
6899
+ reason: z5.enum(["explicit_user_choice_missing", "public_api_compatibility_missing"])
6900
+ }).strict();
6686
6901
  var sessionSchema = z5.object({
6687
6902
  id: sessionIdSchema,
6688
6903
  title: z5.string(),
@@ -6699,6 +6914,9 @@ var sessionSchema = z5.object({
6699
6914
  contextCompactions: z5.number().int().nonnegative().optional(),
6700
6915
  compactedThroughMessageId: z5.string().optional(),
6701
6916
  contextCompactionReceipts: z5.array(contextCompactionReceiptSchema).max(64).optional(),
6917
+ contextEpochs: z5.array(contextEpochSchema).max(64).optional(),
6918
+ intentAssessment: intentAssessmentSchema.optional(),
6919
+ pendingInput: pendingInputSchema.optional(),
6702
6920
  workingMemory: workingMemorySchema.optional(),
6703
6921
  contextSources: z5.array(contextSourceSchema).max(64).optional(),
6704
6922
  toolArtifacts: z5.array(toolArtifactSchema).max(200).optional(),
@@ -6849,7 +7067,7 @@ var SessionStore = class {
6849
7067
  const backup = this.backupPathFor(session.id);
6850
7068
  await this.assertManagedFile(target);
6851
7069
  await this.assertManagedFile(backup);
6852
- const temporary = join10(this.directory, `.${session.id}.${randomUUID9()}.tmp`);
7070
+ const temporary = join10(this.directory, `.${session.id}.${randomUUID10()}.tmp`);
6853
7071
  const data = `${JSON.stringify(session, null, 2)}
6854
7072
  `;
6855
7073
  const handle = await open2(temporary, "wx", 384);
@@ -6867,7 +7085,7 @@ var SessionStore = class {
6867
7085
  }
6868
7086
  }
6869
7087
  async copyBackup(source, target) {
6870
- const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID9()}.tmp`);
7088
+ const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID10()}.tmp`);
6871
7089
  try {
6872
7090
  await copyFile(source, temporary, constants4.COPYFILE_EXCL);
6873
7091
  await chmod6(temporary, 384);
@@ -6956,7 +7174,7 @@ var SessionStore = class {
6956
7174
  }
6957
7175
  };
6958
7176
  function createSession(options) {
6959
- const id = options.id ?? randomUUID9();
7177
+ const id = options.id ?? randomUUID10();
6960
7178
  validateId(id);
6961
7179
  const now = (/* @__PURE__ */ new Date()).toISOString();
6962
7180
  return {
@@ -6971,6 +7189,12 @@ function createSession(options) {
6971
7189
  tasks: [],
6972
7190
  changedFiles: [],
6973
7191
  audit: [],
7192
+ contextEpochs: [{
7193
+ id: randomUUID10(),
7194
+ index: 1,
7195
+ startedAt: now,
7196
+ usage: { inputTokens: 0, outputTokens: 0 }
7197
+ }],
6974
7198
  usage: { inputTokens: 0, outputTokens: 0 }
6975
7199
  };
6976
7200
  }
@@ -8063,7 +8287,7 @@ function isUnsafeGitOption(argument) {
8063
8287
  "--exclude-from",
8064
8288
  "--output",
8065
8289
  "--directory"
8066
- ].some((option) => argument === option || argument.startsWith(`${option}=`));
8290
+ ].some((option2) => argument === option2 || argument.startsWith(`${option2}=`));
8067
8291
  }
8068
8292
  function firstGitCommand(args) {
8069
8293
  for (const argument of args) {
@@ -8811,7 +9035,7 @@ async function discoverSnapshotFiles(root) {
8811
9035
  }
8812
9036
 
8813
9037
  // src/tools/task.ts
8814
- import { randomUUID as randomUUID10 } from "node:crypto";
9038
+ import { randomUUID as randomUUID11 } from "node:crypto";
8815
9039
  import { z as z14 } from "zod";
8816
9040
  var taskSchema2 = z14.object({
8817
9041
  id: z14.string().min(1).optional(),
@@ -8860,7 +9084,7 @@ var taskTool = {
8860
9084
  case "list":
8861
9085
  break;
8862
9086
  case "add":
8863
- tasks.push({ id: randomUUID10(), title: input2.title, status: input2.status ?? "pending" });
9087
+ tasks.push({ id: randomUUID11(), title: input2.title, status: input2.status ?? "pending" });
8864
9088
  break;
8865
9089
  case "update": {
8866
9090
  const task = tasks.find((item) => item.id === input2.id);
@@ -8877,7 +9101,7 @@ var taskTool = {
8877
9101
  }
8878
9102
  case "replace":
8879
9103
  tasks.splice(0, tasks.length, ...input2.tasks.map((task) => ({
8880
- id: task.id ?? randomUUID10(),
9104
+ id: task.id ?? randomUUID11(),
8881
9105
  title: task.title,
8882
9106
  status: task.status
8883
9107
  })));
@@ -8891,11 +9115,11 @@ var taskTool = {
8891
9115
  };
8892
9116
 
8893
9117
  // src/tools/task-contract.ts
8894
- import { randomUUID as randomUUID12 } from "node:crypto";
9118
+ import { randomUUID as randomUUID13 } from "node:crypto";
8895
9119
  import { z as z15 } from "zod";
8896
9120
 
8897
9121
  // src/agent/task-contract.ts
8898
- import { randomUUID as randomUUID11 } from "node:crypto";
9122
+ import { randomUUID as randomUUID12 } from "node:crypto";
8899
9123
  var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
8900
9124
  function shouldUseTaskContract(request, intent, existing) {
8901
9125
  if (existing && existing.state !== "satisfied") return true;
@@ -8961,7 +9185,7 @@ function refreshTaskContractState(contract) {
8961
9185
  }
8962
9186
  function criterion(id, description) {
8963
9187
  return {
8964
- id: `${id}-${randomUUID11().slice(0, 8)}`,
9188
+ id: `${id}-${randomUUID12().slice(0, 8)}`,
8965
9189
  description,
8966
9190
  required: true,
8967
9191
  status: "pending",
@@ -9515,7 +9739,7 @@ var taskContractTool = {
9515
9739
  }
9516
9740
  const now = (/* @__PURE__ */ new Date()).toISOString();
9517
9741
  const criteria = input2.acceptance_criteria.map((item) => ({
9518
- id: item.id ?? `criterion-${randomUUID12().slice(0, 8)}`,
9742
+ id: item.id ?? `criterion-${randomUUID13().slice(0, 8)}`,
9519
9743
  description: item.description,
9520
9744
  required: item.required ?? true,
9521
9745
  status: "pending",
@@ -9983,6 +10207,118 @@ function escapeAttribute3(value) {
9983
10207
  })[character] ?? character);
9984
10208
  }
9985
10209
 
10210
+ // src/agent/intent-sufficiency.ts
10211
+ import { randomUUID as randomUUID14 } from "node:crypto";
10212
+ function assessIntentSufficiency(request, intent, context) {
10213
+ const assessedAt = (/* @__PURE__ */ new Date()).toISOString();
10214
+ const uiChoice = explicitUiChoice(request);
10215
+ if (uiChoice) {
10216
+ const pending = clarification(
10217
+ request,
10218
+ assessedAt,
10219
+ "explicit_user_choice_missing",
10220
+ isChinese(request) ? "\u8FD9\u4E2A\u754C\u9762\u884C\u4E3A\u9700\u8981\u4F60\u7684\u4EA7\u54C1\u504F\u597D\uFF0C\u5E94\u91C7\u7528\u54EA\u4E00\u79CD\uFF1F" : "This UI behavior depends on your product preference. Which should be used?",
10221
+ uiChoice
10222
+ );
10223
+ return {
10224
+ assessment: assessment("needs_input", "explicit_user_choice_missing", assessedAt, context.retrievalHits),
10225
+ pending,
10226
+ directive: "Pause before mutation and ask the single recorded clarification question."
10227
+ };
10228
+ }
10229
+ if (publicApiCompatibilityMissing(request)) {
10230
+ const chinese = isChinese(request);
10231
+ const pending = clarification(
10232
+ request,
10233
+ assessedAt,
10234
+ "public_api_compatibility_missing",
10235
+ chinese ? "\u516C\u5171 API \u7684\u517C\u5BB9\u7B56\u7565\u5C1A\u672A\u6307\u5B9A\uFF0C\u5E94\u8BE5\u91C7\u7528\u54EA\u4E00\u79CD\uFF1F" : "The public API compatibility policy is unspecified. Which policy should be used?",
10236
+ chinese ? [
10237
+ option("backward_compatible", "\u4FDD\u6301\u5411\u540E\u517C\u5BB9", "\u4FDD\u7559\u65E7\u63A5\u53E3\u5E76\u63D0\u4F9B\u5F03\u7528\u6216\u8FC1\u79FB\u8DEF\u5F84\uFF1B\u6539\u52A8\u66F4\u7A33\u59A5\u4F46\u5B9E\u73B0\u8303\u56F4\u8F83\u5927\u3002", true),
10238
+ option("breaking_change", "\u5141\u8BB8\u7834\u574F\u6027\u53D8\u66F4", "\u76F4\u63A5\u91C7\u7528\u65B0\u63A5\u53E3\uFF1B\u8303\u56F4\u66F4\u5C0F\uFF0C\u4F46\u8C03\u7528\u65B9\u5FC5\u987B\u540C\u6B65\u8FC1\u79FB\u3002", false)
10239
+ ] : [
10240
+ option("backward_compatible", "Preserve compatibility", "Keep the old API with a deprecation or migration path; safer but broader.", true),
10241
+ option("breaking_change", "Allow breaking change", "Adopt the new API directly; smaller change but callers must migrate.", false)
10242
+ ]
10243
+ );
10244
+ return {
10245
+ assessment: assessment("needs_input", "public_api_compatibility_missing", assessedAt, context.retrievalHits),
10246
+ pending,
10247
+ directive: "Pause before mutation and ask the single recorded clarification question."
10248
+ };
10249
+ }
10250
+ if (requiresRuntimePermission(request)) {
10251
+ return {
10252
+ assessment: assessment("permission_required", "runtime_permission_separate", assessedAt, context.retrievalHits),
10253
+ directive: "The objective is sufficiently clear. Continue, but keep runtime permission approval separate from clarification."
10254
+ };
10255
+ }
10256
+ if (context.complex && shouldInspectWorkspace(request, intent)) {
10257
+ return {
10258
+ assessment: assessment("inspect_then_execute", "workspace_inference_available", assessedAt, context.retrievalHits),
10259
+ directive: "Do not ask the user for repository facts. Inspect the workspace and current evidence before the first mutation."
10260
+ };
10261
+ }
10262
+ return {
10263
+ assessment: assessment("direct_execute", "simple_explicit_request", assessedAt, context.retrievalHits),
10264
+ directive: "The request is sufficiently clear. Execute without an extra clarification turn."
10265
+ };
10266
+ }
10267
+ function resolvePendingInput(pending, answer) {
10268
+ const normalized = answer.trim();
10269
+ const byIndex = /^([1-3])(?:[.)、:]|$)/u.exec(normalized)?.[1];
10270
+ const selected = pending.options.find((candidate) => candidate.id.toLocaleLowerCase() === normalized.toLocaleLowerCase() || candidate.label.toLocaleLowerCase() === normalized.toLocaleLowerCase()) ?? (byIndex ? pending.options[Number(byIndex) - 1] : void 0);
10271
+ const decision = selected ? `${selected.label}: ${selected.impact}` : compact2(normalized, 2e3);
10272
+ return { answer: compact2(normalized, 2e3), decision };
10273
+ }
10274
+ function assessment(route, reason, assessedAt, retrievalHits) {
10275
+ return { version: 1, route, reasons: [reason], assessedAt, retrievalHits };
10276
+ }
10277
+ function clarification(originalRequest, createdAt, reason, question2, options) {
10278
+ return {
10279
+ id: randomUUID14(),
10280
+ runId: randomUUID14(),
10281
+ createdAt,
10282
+ originalRequest: originalRequest.slice(0, 12e4),
10283
+ question: question2,
10284
+ options,
10285
+ reason
10286
+ };
10287
+ }
10288
+ function explicitUiChoice(value) {
10289
+ const terms = "modal|dialog|drawer|popover|inline|new page|side panel|\u5F39\u7A97|\u6A21\u6001\u6846|\u62BD\u5C49|\u6D6E\u5C42|\u5185\u8054|\u9875\u9762\u5185|\u4FA7\u8FB9\u680F|\u65B0\u9875\u9762";
10290
+ const match = value.match(new RegExp(`\\b(${terms})\\b\\s*(?:or|versus|vs\\.?)\\s*\\b(${terms})\\b`, "iu")) ?? value.match(new RegExp(`(${terms})[\uFF0C\u3001\\s]*(?:\u8FD8\u662F|\u6216\u8005|\u6216)[\uFF0C\u3001\\s]*(${terms})`, "iu"));
10291
+ if (!match?.[1] || !match[2] || match[1].toLocaleLowerCase() === match[2].toLocaleLowerCase()) return;
10292
+ const chinese = isChinese(value);
10293
+ return [
10294
+ option("choice_1", compact2(match[1], 80), chinese ? "\u91C7\u7528\u8BF7\u6C42\u4E2D\u5217\u51FA\u7684\u7B2C\u4E00\u79CD\u4EA4\u4E92\u65B9\u5F0F\u3002" : "Use the first interaction named in the request.", true),
10295
+ option("choice_2", compact2(match[2], 80), chinese ? "\u91C7\u7528\u8BF7\u6C42\u4E2D\u5217\u51FA\u7684\u7B2C\u4E8C\u79CD\u4EA4\u4E92\u65B9\u5F0F\u3002" : "Use the second interaction named in the request.", false)
10296
+ ];
10297
+ }
10298
+ function publicApiCompatibilityMissing(value) {
10299
+ const api = /\b(?:public|external|exported)\s+(?:api|interface|contract)\b|公共\s*(?:api|接口)|公开\s*(?:api|接口)|对外接口/iu;
10300
+ const change = /\b(?:change|replace|remove|rename|migrate|redesign|refactor)\b|修改|替换|删除|移除|重命名|迁移|重构/iu;
10301
+ const policy = /\b(?:backward compatible|backwards compatible|breaking change|semver major|deprecat|compatibility shim|migration path)\b|向后兼容|保持兼容|允许破坏|破坏性变更|主版本|弃用|迁移路径/iu;
10302
+ return api.test(value) && change.test(value) && !policy.test(value);
10303
+ }
10304
+ function requiresRuntimePermission(value) {
10305
+ return /\b(?:push|publish|deploy|release|delete|drop|reset|force[- ]push|send|upload)\b|推送|发布|部署|删除|清空|重置|发送|上传/iu.test(value);
10306
+ }
10307
+ function shouldInspectWorkspace(value, intent) {
10308
+ if (intent === "debug" || intent === "refactor" || intent === "review") return true;
10309
+ const hasConcreteTarget = /(?:^|\s)(?:[\w.-]+\/)+[\w.-]+|`[^`]+`|\b[A-Za-z_$][\w$]*(?:\(\))?\b/u.test(value);
10310
+ return !hasConcreteTarget || /\b(?:existing|current|relevant|appropriate|best)\b|现有|当前|相关|合适|最适合|这个|那个/iu.test(value);
10311
+ }
10312
+ function option(id, label, impact, recommended) {
10313
+ return { id, label, impact, recommended };
10314
+ }
10315
+ function isChinese(value) {
10316
+ return /[\u3400-\u9fff]/u.test(value);
10317
+ }
10318
+ function compact2(value, limit) {
10319
+ return value.trim().replace(/\s+/gu, " ").slice(0, limit);
10320
+ }
10321
+
9986
10322
  // src/agent/tool-recovery.ts
9987
10323
  import { createHash as createHash13 } from "node:crypto";
9988
10324
  var RETRY_BUDGET = {
@@ -9997,6 +10333,7 @@ var RETRY_BUDGET = {
9997
10333
  no_progress: 0,
9998
10334
  contract_required: 2
9999
10335
  };
10336
+ var FAILURE_CLASSES = Object.keys(RETRY_BUDGET);
10000
10337
  var REPAIR_HINT = {
10001
10338
  schema_input: "Correct the arguments to match the tool schema.",
10002
10339
  unknown_tool: "Choose a tool exposed for this turn.",
@@ -10098,6 +10435,9 @@ function classifyToolFailure(result, fallback = "execution") {
10098
10435
  function formatFailureReceipt(receipt) {
10099
10436
  return `Failure: ${receipt.class}; attempt ${receipt.attempt}; ${receipt.remaining} retries remain; circuit ${receipt.circuitOpen ? "open" : "closed"}. Repair: ${receipt.repairHint}`;
10100
10437
  }
10438
+ function resolvableFailureSignatures(call) {
10439
+ return FAILURE_CLASSES.map((failureClass) => failureSignature(call, failureClass));
10440
+ }
10101
10441
  function isRetryable(failureClass) {
10102
10442
  return RETRY_BUDGET[failureClass] > 0;
10103
10443
  }
@@ -10915,16 +11255,35 @@ var AgentRunner = class {
10915
11255
  }
10916
11256
  async run(input2, options = {}) {
10917
11257
  if (this.running) throw new Error("This AgentRunner is already processing a turn.");
10918
- const request = input2.trim();
10919
- if (!request) throw new Error("User input cannot be empty.");
10920
- if (request.length > 12e4) {
11258
+ const submittedRequest = input2.trim();
11259
+ if (!submittedRequest) throw new Error("User input cannot be empty.");
11260
+ if (submittedRequest.length > 12e4) {
10921
11261
  throw new Error("User input is too large; pass a focused request or attach files with @path.");
10922
11262
  }
11263
+ let request = submittedRequest;
10923
11264
  this.running = true;
10924
11265
  this.contextEngine.resetDiagnostics?.();
10925
11266
  const emit = async (event) => {
10926
11267
  await options.onEvent?.(event);
10927
11268
  };
11269
+ const pendingAtStart = this.session.pendingInput;
11270
+ const resolvedInput = pendingAtStart ? resolvePendingInput(pendingAtStart, submittedRequest) : void 0;
11271
+ if (pendingAtStart && resolvedInput) {
11272
+ request = `${pendingAtStart.originalRequest}
11273
+
11274
+ <user-clarification-decision source="explicit-user-input">
11275
+ ${resolvedInput.decision}
11276
+ </user-clarification-decision>`;
11277
+ delete this.session.pendingInput;
11278
+ const constraint = `User clarification (${pendingAtStart.id}): ${resolvedInput.decision}`;
11279
+ if (this.session.taskContract && !this.session.taskContract.constraints.includes(constraint)) {
11280
+ this.session.taskContract.constraints.push(constraint);
11281
+ if (this.session.taskContract.constraints.length > 64) {
11282
+ this.session.taskContract.constraints.splice(0, this.session.taskContract.constraints.length - 64);
11283
+ }
11284
+ this.session.taskContract.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
11285
+ }
11286
+ }
10928
11287
  const changeSequenceAtStart = this.changeSequence;
10929
11288
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
10930
11289
  const loadedProgressiveTools = /* @__PURE__ */ new Set();
@@ -10992,11 +11351,12 @@ var AgentRunner = class {
10992
11351
  try {
10993
11352
  throwIfAborted(options.signal);
10994
11353
  await this.reconcileToolArtifacts();
11354
+ ensureContextEpoch(this.session);
10995
11355
  if (this.session.messages.length === 0 && this.session.title === "New session") {
10996
11356
  this.session.title = titleFromInput(request);
10997
11357
  }
10998
11358
  this.contextManager.startTurn(this.session, request);
10999
- const userMessage = message("user", request);
11359
+ const userMessage = message("user", submittedRequest);
11000
11360
  this.activeReuseGate = { requestId: userMessage.id, request, attempted: false };
11001
11361
  this.session.messages.push(userMessage);
11002
11362
  await this.persist();
@@ -11004,11 +11364,48 @@ var AgentRunner = class {
11004
11364
  const turnDirective = buildTurnDirective(request, {
11005
11365
  agents: Boolean(this.config.agents?.enabled)
11006
11366
  });
11367
+ const contractEnabled = shouldUseTaskContract(
11368
+ request,
11369
+ turnDirective.intent,
11370
+ this.session.taskContract
11371
+ );
11007
11372
  const packed = trivialTurn ? emptyPackedContext(selectContextBudget(request, this.config, {
11008
11373
  intent: turnDirective.intent,
11009
11374
  trivial: true
11010
11375
  })) : await this.packContext(request, { intent: turnDirective.intent });
11011
11376
  if (!trivialTurn) await emit({ type: "context", packed });
11377
+ if (contractEnabled && (!this.session.taskContract || this.session.taskContract.state === "satisfied")) {
11378
+ this.session.taskContract = createDraftTaskContract(request, this.session.audit?.at(-1)?.id);
11379
+ await this.persist();
11380
+ await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
11381
+ }
11382
+ let intentDirective;
11383
+ if (pendingAtStart && resolvedInput) {
11384
+ this.session.intentAssessment = {
11385
+ version: 1,
11386
+ route: "direct_execute",
11387
+ reasons: ["clarification_resolved"],
11388
+ assessedAt: (/* @__PURE__ */ new Date()).toISOString(),
11389
+ retrievalHits: packed.hits.length
11390
+ };
11391
+ intentDirective = "The user resolved the recorded clarification. Continue the same logical run and apply that decision without asking again.";
11392
+ await emit({ type: "input_resolved", pendingId: pendingAtStart.id, runId: pendingAtStart.runId, answer: resolvedInput.answer });
11393
+ await emit({ type: "intent", assessment: this.session.intentAssessment });
11394
+ } else {
11395
+ const intent = assessIntentSufficiency(request, turnDirective.intent, {
11396
+ retrievalHits: packed.hits.length,
11397
+ complex: contractEnabled
11398
+ });
11399
+ this.session.intentAssessment = intent.assessment;
11400
+ intentDirective = intent.directive;
11401
+ await emit({ type: "intent", assessment: intent.assessment });
11402
+ if (intent.pending) {
11403
+ this.session.pendingInput = intent.pending;
11404
+ await this.persist();
11405
+ await emit({ type: "needs_input", pending: intent.pending });
11406
+ return finishRun("needs_input");
11407
+ }
11408
+ }
11012
11409
  const mentions = await this.packMentions(request);
11013
11410
  const retrievedContext = trivialTurn && !mentions.length ? "" : buildRetrievedContext(
11014
11411
  packed,
@@ -11036,19 +11433,10 @@ var AgentRunner = class {
11036
11433
  scope: augmentation.memoryScope ?? "session"
11037
11434
  });
11038
11435
  }
11039
- const contractEnabled = shouldUseTaskContract(
11040
- request,
11041
- turnDirective.intent,
11042
- this.session.taskContract
11043
- );
11044
- if (contractEnabled && (!this.session.taskContract || this.session.taskContract.state === "satisfied")) {
11045
- this.session.taskContract = createDraftTaskContract(request, this.session.audit?.at(-1)?.id);
11046
- await this.persist();
11047
- await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
11048
- }
11049
11436
  activeRunContract = contractEnabled ? this.session.taskContract : void 0;
11050
11437
  let verificationAttempted = false;
11051
11438
  const maxTurns = options.maxTurns ?? this.config.agent.maxTurns;
11439
+ const epochTokenBudget = this.config.agent.maxEpochTokens ?? this.config.agent.maxSessionTokens;
11052
11440
  const contextBudget = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
11053
11441
  if (this.contextManager.shouldCompact(this.session, contextBudget)) {
11054
11442
  const compacted = await this.compactContext(void 0, options.signal, "automatic");
@@ -11074,11 +11462,15 @@ var AgentRunner = class {
11074
11462
  if (this.session.usage.inputTokens + this.session.usage.outputTokens >= this.config.agent.maxSessionTokens) {
11075
11463
  return finishRun("token_budget");
11076
11464
  }
11465
+ if (contextEpochTokens(this.session) >= epochTokenBudget) {
11466
+ await this.rollContextEpoch("token_budget", options.signal, emit);
11467
+ }
11077
11468
  this.applySteering();
11078
11469
  await emit({ type: "thinking", turn });
11079
11470
  const dynamicPrompt = [
11080
11471
  buildSessionStatePrompt(this.session),
11081
11472
  turnDirective.text,
11473
+ `<intent-sufficiency route="${this.session.intentAssessment?.route ?? "direct_execute"}">${intentDirective}</intent-sufficiency>`,
11082
11474
  this.contextManager.buildShortTermPrompt(this.session),
11083
11475
  pinnedContext,
11084
11476
  options.turnInstructions ?? "",
@@ -11091,7 +11483,9 @@ var AgentRunner = class {
11091
11483
  activeMessages(this.session),
11092
11484
  contextBudget
11093
11485
  );
11094
- const availableTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
11486
+ const availableLifetimeTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
11487
+ const availableEpochTokens = epochTokenBudget - contextEpochTokens(this.session);
11488
+ const availableTokens = Math.min(availableLifetimeTokens, availableEpochTokens);
11095
11489
  const visibleTools = visibleToolDefinitions(
11096
11490
  this.tools,
11097
11491
  options.askMode === true,
@@ -11105,9 +11499,15 @@ var AgentRunner = class {
11105
11499
  loadedProgressiveTools
11106
11500
  );
11107
11501
  const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(visibleTools);
11108
- if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
11502
+ if (estimatedInputTokens >= availableLifetimeTokens) {
11109
11503
  return finishRun("token_budget");
11110
11504
  }
11505
+ if (availableTokens <= 0 || estimatedInputTokens >= availableEpochTokens) {
11506
+ if (contextEpochTokens(this.session) === 0) return finishRun("token_budget");
11507
+ await this.rollContextEpoch("token_budget", options.signal, emit);
11508
+ turn -= 1;
11509
+ continue;
11510
+ }
11111
11511
  const maxOutputTokens = Math.max(1, Math.min(
11112
11512
  this.config.model.maxTokens ?? 8192,
11113
11513
  availableTokens - estimatedInputTokens
@@ -11129,7 +11529,7 @@ var AgentRunner = class {
11129
11529
  breakdown
11130
11530
  });
11131
11531
  }
11132
- const assistantId = randomUUID13();
11532
+ const assistantId = randomUUID15();
11133
11533
  const response = await this.completeModel(
11134
11534
  messages,
11135
11535
  visibleTools,
@@ -11509,6 +11909,9 @@ ${completeContent}`;
11509
11909
  metadata.failure = receipt;
11510
11910
  } else {
11511
11911
  recovery.recordSuccess(call);
11912
+ const historicalFailures = new Set((this.session.audit ?? []).filter((event) => event.type === "tool" && event.outcome === "failure").map((event) => failureSignatureFromMetadata(event.metadata?.failure)).filter((signature) => signature !== void 0));
11913
+ const resolvedFailureSignatures = resolvableFailureSignatures(call).filter((signature) => historicalFailures.has(signature));
11914
+ if (resolvedFailureSignatures.length) metadata.resolvedFailureSignatures = resolvedFailureSignatures;
11512
11915
  }
11513
11916
  const evidenceProgress = recovery.recordEvidence(call, {
11514
11917
  toolCallId: call.id,
@@ -11578,13 +11981,13 @@ ${completeContent}`;
11578
11981
  this.recordPermission(call, category, "allow", "Approved for this session.");
11579
11982
  return true;
11580
11983
  }
11581
- await emit({ type: "permission", call, category });
11984
+ await emit({ type: "permission", call, category, reason: decision.reason });
11582
11985
  if (!options.requestPermission) {
11583
11986
  this.recordPermission(call, category, "deny", "No permission handler was available.");
11584
11987
  return false;
11585
11988
  }
11586
11989
  try {
11587
- const grant = await options.requestPermission(call, category);
11990
+ const grant = await options.requestPermission(call, category, decision.reason);
11588
11991
  const allowed = grant === true || grant === "session";
11589
11992
  if (grant === "session") this.sessionApprovals.add(approvalKey);
11590
11993
  this.recordPermission(
@@ -11599,13 +12002,13 @@ ${completeContent}`;
11599
12002
  return false;
11600
12003
  }
11601
12004
  }
11602
- recordPermission(call, category, outcome, reason) {
12005
+ recordPermission(call, category, outcome2, reason) {
11603
12006
  this.appendAudit({
11604
12007
  type: "permission",
11605
12008
  toolCallId: call.id,
11606
12009
  tool: call.name,
11607
12010
  category,
11608
- outcome,
12011
+ outcome: outcome2,
11609
12012
  reason
11610
12013
  });
11611
12014
  }
@@ -11622,7 +12025,7 @@ ${completeContent}`;
11622
12025
  }
11623
12026
  appendAudit(event) {
11624
12027
  const audit = this.session.audit ?? (this.session.audit = []);
11625
- audit.push({ id: randomUUID13(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
12028
+ audit.push({ id: randomUUID15(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
11626
12029
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
11627
12030
  }
11628
12031
  async acceptChangedFiles(paths) {
@@ -11643,7 +12046,7 @@ ${completeContent}`;
11643
12046
  const results = [];
11644
12047
  for (const command2 of this.config.agent.verifyCommands) {
11645
12048
  const call = {
11646
- id: `verify-${randomUUID13()}`,
12049
+ id: `verify-${randomUUID15()}`,
11647
12050
  name: "shell",
11648
12051
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
11649
12052
  };
@@ -11691,6 +12094,30 @@ ${completeContent}`;
11691
12094
  await this.persist();
11692
12095
  return result;
11693
12096
  }
12097
+ async rollContextEpoch(reason, signal, emit) {
12098
+ let receipt;
12099
+ const contextBudget = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
12100
+ if (this.contextManager.shouldCompact(this.session, contextBudget)) {
12101
+ const compacted = await this.compactContext(
12102
+ "Create a boundary handoff for the next bounded context epoch.",
12103
+ signal,
12104
+ "automatic"
12105
+ );
12106
+ receipt = compacted.receipt;
12107
+ if (compacted.status === "compacted") await emit({ type: "context_compacted", ...compacted });
12108
+ }
12109
+ const rotated = rotateContextEpoch(this.session, reason, receipt);
12110
+ await this.persist();
12111
+ await emit({
12112
+ type: "context_epoch",
12113
+ index: rotated.current.index,
12114
+ previousIndex: rotated.previous.index,
12115
+ reason,
12116
+ inputTokens: rotated.previous.usage.inputTokens,
12117
+ outputTokens: rotated.previous.usage.outputTokens,
12118
+ handoff: rotated.handoff
12119
+ });
12120
+ }
11694
12121
  getContextStatus() {
11695
12122
  return this.contextManager.status(this.session);
11696
12123
  }
@@ -11722,8 +12149,14 @@ ${completeContent}`;
11722
12149
  toolOutputBudget() {
11723
12150
  const contextWindowTokens = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
11724
12151
  const activeContextTokens = this.contextManager.status(this.session, contextWindowTokens).activeTokens;
11725
- const remainingSessionTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
11726
- return dynamicToolOutputBudget(contextWindowTokens, activeContextTokens, remainingSessionTokens);
12152
+ const remainingLifetimeTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
12153
+ const epochBudget = this.config.agent.maxEpochTokens ?? this.config.agent.maxSessionTokens;
12154
+ const remainingEpochTokens = epochBudget - contextEpochTokens(this.session);
12155
+ return dynamicToolOutputBudget(
12156
+ contextWindowTokens,
12157
+ activeContextTokens,
12158
+ Math.min(remainingLifetimeTokens, remainingEpochTokens)
12159
+ );
11727
12160
  }
11728
12161
  async protectToolResult(result) {
11729
12162
  const metadata = { ...result.metadata ?? {} };
@@ -11779,7 +12212,7 @@ ${completeContent}`;
11779
12212
  };
11780
12213
  function message(role, content, extra = {}) {
11781
12214
  return {
11782
- id: randomUUID13(),
12215
+ id: randomUUID15(),
11783
12216
  role,
11784
12217
  content,
11785
12218
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11894,6 +12327,7 @@ function recordTokenUsage(session, providerUsage, estimatedInputTokens, estimate
11894
12327
  const outputTokens = outputActual ?? estimatedOutputTokens;
11895
12328
  session.usage.inputTokens += inputTokens;
11896
12329
  session.usage.outputTokens += outputTokens;
12330
+ recordContextEpochUsage(session, inputTokens, outputTokens);
11897
12331
  if (inputActual !== void 0) {
11898
12332
  session.usage.actualInputTokens = (session.usage.actualInputTokens ?? 0) + inputActual;
11899
12333
  } else {
@@ -11969,6 +12403,11 @@ ${content}` : content,
11969
12403
  ...failure ? { metadata: { failure } } : {}
11970
12404
  };
11971
12405
  }
12406
+ function failureSignatureFromMetadata(value) {
12407
+ if (!value || typeof value !== "object") return;
12408
+ const signature = value.signature;
12409
+ return typeof signature === "string" && signature ? signature : void 0;
12410
+ }
11972
12411
  function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable, request, loadedProgressiveTools) {
11973
12412
  const eligible = tools.definitions().filter(
11974
12413
  (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
@@ -12176,7 +12615,7 @@ function integer(value, fallback, min, max) {
12176
12615
  }
12177
12616
 
12178
12617
  // src/agent/delegation.ts
12179
- import { randomUUID as randomUUID15 } from "node:crypto";
12618
+ import { randomUUID as randomUUID17 } from "node:crypto";
12180
12619
  import { join as join18 } from "node:path";
12181
12620
  import { z as z19 } from "zod";
12182
12621
 
@@ -12305,7 +12744,7 @@ function numeric(...values) {
12305
12744
  }
12306
12745
 
12307
12746
  // src/agent/team-store.ts
12308
- import { createHash as createHash16, randomUUID as randomUUID14 } from "node:crypto";
12747
+ import { createHash as createHash16, randomUUID as randomUUID16 } from "node:crypto";
12309
12748
  import { lstat as lstat18, readFile as readFile17, readdir as readdir7, rm as rm3 } from "node:fs/promises";
12310
12749
  import { join as join16, resolve as resolve18 } from "node:path";
12311
12750
  import { z as z18 } from "zod";
@@ -12399,7 +12838,7 @@ var TeamRunStore = class {
12399
12838
  const now = (/* @__PURE__ */ new Date()).toISOString();
12400
12839
  const manifest = manifestSchema2.parse({
12401
12840
  version: 2,
12402
- id: randomUUID14(),
12841
+ id: randomUUID16(),
12403
12842
  workspace: this.workspace,
12404
12843
  objective: input2.objective,
12405
12844
  reviewer: input2.reviewer,
@@ -13205,7 +13644,7 @@ var DelegationManager = class {
13205
13644
  maxReviewRounds: 0
13206
13645
  });
13207
13646
  await emit?.({ type: "team_start", id: board.id, objective: task });
13208
- const writerId = randomUUID15();
13647
+ const writerId = randomUUID17();
13209
13648
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
13210
13649
  const draft = await this.writerLane.createDraft(
13211
13650
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -13216,18 +13655,18 @@ var DelegationManager = class {
13216
13655
  await this.recordAgent(board.id, writer, "write");
13217
13656
  const writerFailed = !writer.ok || !draft.patch || !draft.worktreeCleaned || signal?.aborted;
13218
13657
  if (writerFailed) {
13219
- const outcome = writer.termination === "cancelled" || signal?.aborted ? "cancelled" : "failed";
13658
+ const outcome2 = writer.termination === "cancelled" || signal?.aborted ? "cancelled" : "failed";
13220
13659
  await this.teamStore.recordWriterLane(board.id, {
13221
13660
  profile: profile.name,
13222
13661
  reviewer: reviewer.name,
13223
13662
  baseCommit: draft.baseCommit,
13224
- outcome,
13663
+ outcome: outcome2,
13225
13664
  patch: draft.patch,
13226
13665
  files: draft.files,
13227
13666
  worktreeCleaned: draft.worktreeCleaned
13228
13667
  });
13229
13668
  await this.teamStore.complete(board.id, { accepted: false, reviewRounds: 0, failed: true });
13230
- const status2 = outcome === "cancelled" ? "cancelled" : "failed";
13669
+ const status2 = outcome2 === "cancelled" ? "cancelled" : "failed";
13231
13670
  const detail2 = !draft.worktreeCleaned ? "Writer worktree cleanup could not be verified; integration is blocked." : !draft.patch ? "Writer returned no patch." : writer.summary;
13232
13671
  await emit?.({ type: "writer_lane", id: board.id, status: status2, detail: detail2, files: draft.files });
13233
13672
  await emit?.({ type: "team_done", id: board.id, accepted: false, reviewRounds: 0 });
@@ -13575,7 +14014,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
13575
14014
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
13576
14015
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
13577
14016
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
13578
- const runId = board?.id ?? randomUUID15();
14017
+ const runId = board?.id ?? randomUUID17();
13579
14018
  await emit?.({ type: "team_start", id: runId, objective });
13580
14019
  try {
13581
14020
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -13633,7 +14072,7 @@ ${review.summary}`,
13633
14072
  }
13634
14073
  }
13635
14074
  async runBatch(runId, tasks, phase, emit, signal) {
13636
- const scheduled = tasks.map((task) => ({ id: randomUUID15(), task }));
14075
+ const scheduled = tasks.map((task) => ({ id: randomUUID17(), task }));
13637
14076
  for (const item of scheduled) {
13638
14077
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
13639
14078
  }
@@ -13742,12 +14181,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
13742
14181
  });
13743
14182
  }
13744
14183
  async peerMessage(runId, from, to, content, emit) {
13745
- const id = randomUUID15();
14184
+ const id = randomUUID17();
13746
14185
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
13747
14186
  await emit?.({ type: "agent_message", id, from, to, content });
13748
14187
  }
13749
14188
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
13750
- const id = scheduledId ?? randomUUID15();
14189
+ const id = scheduledId ?? randomUUID17();
13751
14190
  const profile = this.options.profiles.get(task.profile);
13752
14191
  const configuredRoute = this.team.routes?.[task.profile];
13753
14192
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -14576,6 +15015,61 @@ function keyEnvironmentName(provider) {
14576
15015
  // src/cli/output.ts
14577
15016
  import { createInterface } from "node:readline/promises";
14578
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
14579
15073
  var HeadlessReporter = class {
14580
15074
  constructor(options) {
14581
15075
  this.options = options;
@@ -14585,7 +15079,6 @@ var HeadlessReporter = class {
14585
15079
  options;
14586
15080
  finalResponse = "";
14587
15081
  tools = [];
14588
- eventError;
14589
15082
  context;
14590
15083
  streamedAssistant = false;
14591
15084
  completion;
@@ -14595,7 +15088,6 @@ var HeadlessReporter = class {
14595
15088
  onEvent = (event) => {
14596
15089
  if (event.type === "assistant") this.finalResponse = event.content;
14597
15090
  if (event.type === "tool_result") this.tools.push(event.result);
14598
- if (event.type === "error") this.eventError = event.error.message;
14599
15091
  if (event.type === "done") {
14600
15092
  this.doneReason = event.reason;
14601
15093
  this.completion = event.completion;
@@ -14613,26 +15105,33 @@ var HeadlessReporter = class {
14613
15105
  this.printText(event);
14614
15106
  };
14615
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
+ });
14616
15114
  if (this.options.format === "stream-json") {
14617
15115
  process.stdout.write(`${JSON.stringify({
14618
15116
  type: "session",
15117
+ ...outcome2,
14619
15118
  session: sessionSummary(session)
14620
15119
  })}
14621
15120
  `);
14622
- return;
15121
+ return outcome2;
14623
15122
  }
14624
15123
  if (this.options.format === "json") {
14625
15124
  process.stdout.write(`${JSON.stringify({
14626
- ok: this.doneReason === void 0 || this.doneReason === "completed" && this.completion?.status !== "verification_failed",
15125
+ type: "result",
15126
+ ...outcome2,
14627
15127
  response: this.finalResponse,
14628
15128
  session: sessionSummary(session),
14629
- ...this.doneReason ? { reason: this.doneReason } : {},
14630
- ...this.completion ? { completion: this.completion } : {},
15129
+ ...completion ? { completion } : {},
14631
15130
  ...this.context ? { context: this.context } : {},
14632
15131
  tools: this.tools
14633
15132
  }, null, 2)}
14634
15133
  `);
14635
- return;
15134
+ return outcome2;
14636
15135
  }
14637
15136
  if (this.options.quiet && this.finalResponse.trim()) {
14638
15137
  process.stdout.write(`${this.finalResponse.trim()}
@@ -14647,24 +15146,41 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14647
15146
  `
14648
15147
  ));
14649
15148
  }
15149
+ return outcome2;
14650
15150
  }
14651
- fail(error) {
15151
+ fail(error, session) {
14652
15152
  const message2 = error instanceof Error ? error.message : String(error);
14653
- if (this.options.format === "stream-json" && this.eventError === message2) return;
14654
- if (this.options.format === "json" || this.options.format === "stream-json") {
14655
- 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
+ })}
14656
15161
  `);
14657
- 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;
14658
15173
  }
14659
15174
  process.stderr.write(`${this.paint.red(this.glyphs.error)} ${message2}
14660
15175
  `);
15176
+ return outcome2;
14661
15177
  }
14662
15178
  printText(event) {
14663
- const { quiet, compact: compact2 } = this.options;
14664
- if (quiet) return;
15179
+ const { quiet, compact: compact3 } = this.options;
15180
+ if (quiet && event.type !== "needs_input") return;
14665
15181
  switch (event.type) {
14666
15182
  case "thinking":
14667
- if (!compact2) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
15183
+ if (!compact3) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
14668
15184
  `));
14669
15185
  break;
14670
15186
  case "context": {
@@ -14676,7 +15192,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14676
15192
  break;
14677
15193
  }
14678
15194
  case "prompt":
14679
- if (!compact2) {
15195
+ if (!compact3) {
14680
15196
  const partition = event.breakdown ? ` ${this.glyphs.separator} stable ${event.breakdown.stableTokens} ${this.glyphs.separator} dynamic ${event.breakdown.dynamicTokens} ${this.glyphs.separator} history ${event.breakdown.conversationTokens} ${this.glyphs.separator} tool results ${event.breakdown.toolResultTokens} ${this.glyphs.separator} retrieved ${event.breakdown.retrievedTokens} ${this.glyphs.separator} tools ${event.breakdown.toolSchemaTokens} ${this.glyphs.separator} output cap ${event.breakdown.outputAllowanceTokens}` : "";
14681
15197
  process.stderr.write(this.paint.dim(
14682
15198
  `${this.glyphs.meta} prompt ${this.glyphs.separator} ${event.intent} ${this.glyphs.separator} ~${event.estimatedTokens} estimated tokens${partition}
@@ -14712,7 +15228,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14712
15228
  }
14713
15229
  break;
14714
15230
  case "tasks":
14715
- if (!compact2) {
15231
+ if (!compact3) {
14716
15232
  const completed = event.tasks.filter((task) => task.status === "completed").length;
14717
15233
  process.stderr.write(this.paint.dim(`${this.glyphs.meta} plan ${this.glyphs.separator} ${completed}/${event.tasks.length} complete
14718
15234
  `));
@@ -14733,6 +15249,22 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14733
15249
  `
14734
15250
  );
14735
15251
  break;
15252
+ case "needs_input":
15253
+ process.stderr.write(this.paint.yellow(`${this.glyphs.meta} ${event.pending.question}
15254
+ `));
15255
+ event.pending.options.forEach((option2, index) => {
15256
+ process.stderr.write(this.paint.dim(
15257
+ ` ${index + 1}. ${option2.label}${option2.recommended ? " (recommended)" : ""} ${this.glyphs.separator} ${option2.impact}
15258
+ `
15259
+ ));
15260
+ });
15261
+ break;
15262
+ case "input_resolved":
15263
+ process.stderr.write(this.paint.dim(
15264
+ `${this.glyphs.meta} clarification resolved ${this.glyphs.separator} ${event.answer}
15265
+ `
15266
+ ));
15267
+ break;
14736
15268
  case "usage":
14737
15269
  case "permission":
14738
15270
  case "skill":
@@ -14743,9 +15275,12 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14743
15275
  case "agent_done":
14744
15276
  case "workflow":
14745
15277
  case "context_compacted":
15278
+ case "context_epoch":
15279
+ case "intent":
14746
15280
  break;
14747
15281
  case "done":
14748
15282
  this.printCompletion(event.completion);
15283
+ this.printStopReason(event.reason);
14749
15284
  break;
14750
15285
  case "error":
14751
15286
  break;
@@ -14776,8 +15311,20 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
14776
15311
  `
14777
15312
  ));
14778
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
+ }
14779
15326
  };
14780
- 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.`) {
14781
15328
  if (!process.stdin.isTTY || !process.stderr.isTTY) return false;
14782
15329
  const paint = color ? chalk2 : new Chalk({ level: 0 });
14783
15330
  const glyphs = resolveCliGlyphs();
@@ -14785,6 +15332,10 @@ async function askConsolePermission(call, category, color = !process.env.NO_COLO
14785
15332
  ${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
14786
15333
  `);
14787
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)}`)}
14788
15339
  `);
14789
15340
  const readline = createInterface({ input: process.stdin, output: process.stderr });
14790
15341
  try {
@@ -14794,6 +15345,13 @@ ${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
14794
15345
  readline.close();
14795
15346
  }
14796
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
+ }
14797
15355
  function eventToJson(event) {
14798
15356
  if (event.type === "error") {
14799
15357
  return { type: event.type, error: event.error.message };
@@ -14815,6 +15373,9 @@ function sessionSummary(session) {
14815
15373
  ...session.lastRun ? { lastRun: session.lastRun } : {},
14816
15374
  ...session.tokenLedger?.length ? { tokenLedger: session.tokenLedger } : {},
14817
15375
  ...session.contextCompactionReceipts?.length ? { contextCompactionReceipts: session.contextCompactionReceipts } : {},
15376
+ ...session.contextEpochs?.length ? { contextEpochs: session.contextEpochs } : {},
15377
+ ...session.intentAssessment ? { intentAssessment: session.intentAssessment } : {},
15378
+ ...session.pendingInput ? { pendingInput: session.pendingInput } : {},
14818
15379
  usage: session.usage
14819
15380
  };
14820
15381
  }
@@ -14896,7 +15457,7 @@ function commandNames(command2) {
14896
15457
  // src/ui/tui.tsx
14897
15458
  import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
14898
15459
  import { Box as Box2, render as render2, Text as Text3, useApp, useInput as useInput2, useWindowSize } from "ink";
14899
- import { relative as relative9 } from "node:path";
15460
+ import { relative as relative10 } from "node:path";
14900
15461
 
14901
15462
  // src/ui/components.tsx
14902
15463
  import React2 from "react";
@@ -14920,6 +15481,8 @@ var commandDefinitions = [
14920
15481
  command("permissions", "Inspect the active permission policy"),
14921
15482
  command("changes", "List files changed in the active session"),
14922
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]"),
14923
15486
  command("checkpoints", "List recoverable pre-mutation snapshots"),
14924
15487
  command("audit", "Review the hash-chained tool and permission timeline"),
14925
15488
  command("rollback", "Restore workspace files from a checkpoint", "/rollback [checkpoint-id]"),
@@ -14976,6 +15539,28 @@ function commandSuggestions(input2, options = {}) {
14976
15539
  description: "Show the secure shared-connection setup command"
14977
15540
  }].filter((item) => item.label.includes(argument.trim().toLocaleLowerCase()));
14978
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
+ }
14979
15564
  if (firstSpace >= 0 && commandName === "memory") {
14980
15565
  const query = argument.trim().toLocaleLowerCase();
14981
15566
  return [
@@ -15507,7 +16092,7 @@ function ToolGlyph({ state, glyphs }) {
15507
16092
  if (state === "cancelled") return /* @__PURE__ */ jsx(Text, { color: theme.warning, children: glyphs.warning });
15508
16093
  return /* @__PURE__ */ jsx(Text, { color: theme.error, children: glyphs.error });
15509
16094
  }
15510
- function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact: compact2 = false }) {
16095
+ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact: compact3 = false }) {
15511
16096
  const theme = useTheme();
15512
16097
  const glyphs = resolveGlyphs(glyphMode);
15513
16098
  if (!items.length) {
@@ -15515,13 +16100,13 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
15515
16100
  }
15516
16101
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
15517
16102
  if (item.kind === "user") {
15518
- return /* @__PURE__ */ jsxs(Box, { marginBottom: compact2 || item.clipped ? 0 : 1, children: [
16103
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: compact3 || item.clipped ? 0 : 1, children: [
15519
16104
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
15520
16105
  /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, wrap: "wrap", children: sanitizeTerminalText(item.text) })
15521
16106
  ] }, item.id);
15522
16107
  }
15523
16108
  if (item.kind === "assistant") {
15524
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact2 || item.clipped ? 0 : 1, children: [
16109
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact3 || item.clipped ? 0 : 1, children: [
15525
16110
  /* @__PURE__ */ jsxs(Text, { bold: true, color: theme.accent, children: [
15526
16111
  glyphs.brand,
15527
16112
  " ",
@@ -15537,7 +16122,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
15537
16122
  }
15538
16123
  if (item.kind === "context") {
15539
16124
  const spans = item.spans ?? [];
15540
- const spanLimit = compact2 ? 2 : 3;
16125
+ const spanLimit = compact3 ? 2 : 3;
15541
16126
  const innerWidth = Math.max(1, safeWidth(width) - 2);
15542
16127
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
15543
16128
  /* @__PURE__ */ jsx(
@@ -15550,7 +16135,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
15550
16135
  labelColor: theme.accent
15551
16136
  }
15552
16137
  ),
15553
- !compact2 && item.budgetReason ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`${glyphs.branchLast} ${sanitizeInlineTerminalText(item.budgetReason)}`, innerWidth) }) : null,
16138
+ !compact3 && item.budgetReason ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`${glyphs.branchLast} ${sanitizeInlineTerminalText(item.budgetReason)}`, innerWidth) }) : null,
15554
16139
  spans.slice(0, spanLimit).map((span, spanIndex) => {
15555
16140
  const lines = span.startLine === span.endLine ? `${span.startLine}` : `${span.startLine}-${span.endLine}`;
15556
16141
  const location = `${compactDisplayPath(sanitizeInlineTerminalText(span.path), 44)}:${lines}`;
@@ -15590,7 +16175,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
15590
16175
  const duration = item.durationMs !== void 0 ? formatDuration(item.durationMs) : "";
15591
16176
  const detailText = [detail, duration].filter(Boolean).join(" ");
15592
16177
  const expanded = Boolean(item.output) && (showToolOutput || expandedToolId === item.id);
15593
- const verbose = expanded && item.output ? limitTerminalText(item.output, compact2 ? 24 : 80) : void 0;
16178
+ const verbose = expanded && item.output ? limitTerminalText(item.output, compact3 ? 24 : 80) : void 0;
15594
16179
  const disclosure = item.output ? expanded ? glyphs.expanded : glyphs.collapsed : "";
15595
16180
  const disclosureWidth = disclosure ? displayWidth(disclosure) + 1 : 0;
15596
16181
  const nameLimit = Math.max(1, Math.min(rowWidth - 2 - disclosureWidth, rowWidth < 64 ? rowWidth - 2 - disclosureWidth : 28));
@@ -15722,9 +16307,28 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
15722
16307
  item.id
15723
16308
  );
15724
16309
  }
16310
+ if (item.kind === "clarification") {
16311
+ const rowWidth = safeWidth(width);
16312
+ const chinese = /[\u3400-\u9fff]/u.test(item.pending.question);
16313
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [
16314
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: sanitizeTerminalText(item.pending.question) }),
16315
+ item.pending.options.map((option2, optionIndex) => {
16316
+ const label = `${optionIndex + 1}. ${sanitizeInlineTerminalText(option2.label)}${option2.recommended ? chinese ? "\uFF08\u63A8\u8350\uFF09" : " (recommended)" : ""}`;
16317
+ const impact = sanitizeInlineTerminalText(option2.impact);
16318
+ if (rowWidth < 48) {
16319
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
16320
+ /* @__PURE__ */ jsx(Text, { color: option2.recommended ? theme.textStrong : theme.muted, children: truncateDisplay(label, Math.max(1, rowWidth - 2)) }),
16321
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`${glyphs.branchLast} ${impact}`, Math.max(1, (rowWidth - 2) * 2)) })
16322
+ ] }, option2.id);
16323
+ }
16324
+ return /* @__PURE__ */ jsx(Text, { color: option2.recommended ? theme.textStrong : theme.muted, children: truncateDisplay(`${label} ${glyphs.separator} ${impact}`, Math.max(1, rowWidth - 2)) }, option2.id);
16325
+ }),
16326
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: chinese ? "\u56DE\u590D\u7F16\u53F7\u3001\u9009\u9879\u540D\u79F0\uFF0C\u6216\u7B80\u77ED\u8BF4\u660E\u4F60\u7684\u51B3\u5B9A\u3002" : "Reply with a number, option label, or a short custom decision." })
16327
+ ] }, item.id);
16328
+ }
15725
16329
  if (item.kind === "list") return /* @__PURE__ */ jsx(ListPanel, { title: item.title, entries: item.entries, width, glyphMode }, item.id);
15726
16330
  if (item.kind === "context-inspector") {
15727
- return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact: compact2, glyphMode }, item.id);
16331
+ return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact: compact3, glyphMode }, item.id);
15728
16332
  }
15729
16333
  if (item.kind === "theme") return /* @__PURE__ */ jsx(ThemePreview, { name: item.name, width, glyphs }, item.id);
15730
16334
  if (item.kind === "banner") {
@@ -15934,7 +16538,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
15934
16538
  ] }) : null
15935
16539
  ] });
15936
16540
  }
15937
- function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact2 = false }) {
16541
+ function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto", workspace, compact: compact3 = false }) {
15938
16542
  const theme = useTheme();
15939
16543
  const glyphs = resolveGlyphs(glyphMode);
15940
16544
  const summary = permissionSummary(call);
@@ -15942,7 +16546,9 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
15942
16546
  const innerWidth = Math.max(1, rowWidth - 2);
15943
16547
  const title = truncateDisplay(`Permission required ${glyphs.separator} ${category}`, innerWidth);
15944
16548
  const tool = truncateDisplay(`tool ${sanitizeInlineTerminalText(call.name)}`, innerWidth);
15945
- 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);
15946
16552
  const argumentCwd = typeof call.arguments.cwd === "string" ? call.arguments.cwd : void 0;
15947
16553
  const cwd = sanitizeInlineTerminalText(argumentCwd || workspace || "");
15948
16554
  const shortcuts = [
@@ -15967,11 +16573,13 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
15967
16573
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
15968
16574
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
15969
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 }) }),
15970
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,
15971
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: [
15972
16580
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
15973
16581
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
15974
- ] }) : compact2 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
16582
+ ] }) : compact3 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
15975
16583
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
15976
16584
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
15977
16585
  ] }) : /* @__PURE__ */ jsx(Box, { paddingLeft: 2, flexDirection: "column", children: shortcuts.map((part) => part.color ? /* @__PURE__ */ jsx(Text, { color: part.color, children: truncateDisplay(part.text, innerWidth) }, part.text) : /* @__PURE__ */ jsx(Text, { children: truncateDisplay(part.text, innerWidth) }, part.text)) })
@@ -15987,6 +16595,13 @@ function PermissionLine({ marker, children }) {
15987
16595
  children
15988
16596
  ] });
15989
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
+ }
15990
16605
  function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueCount = 0, queuePreview, attachments = [], glyphMode = "auto", children }) {
15991
16606
  const theme = useTheme();
15992
16607
  const glyphs = resolveGlyphs(glyphMode);
@@ -16232,7 +16847,7 @@ function MeterBar({ segments, total, width, glyphs }) {
16232
16847
  remainder ? /* @__PURE__ */ jsx(Text, { color: theme.border, children: glyphs.meterEmpty.repeat(remainder) }) : null
16233
16848
  ] });
16234
16849
  }
16235
- function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact: compact2 = false, minimal = false, glyphMode = "auto" }) {
16850
+ function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact: compact3 = false, minimal = false, glyphMode = "auto" }) {
16236
16851
  const theme = useTheme();
16237
16852
  const glyphs = resolveGlyphs(glyphMode);
16238
16853
  const hasCompactedContext = status.compactedMessages > 0 || Boolean(summary);
@@ -16253,10 +16868,16 @@ function ContextInspector({ status, working, summary, width, memory, connections
16253
16868
  { label: "summary", detail: hasCompactedContext ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted${summary ? "" : ` ${glyphs.separator} facts`}` : "not created" },
16254
16869
  { label: "long-term", detail: memory ?? `retrieved by relevance ${glyphs.separator} untrusted context` }
16255
16870
  ];
16256
- if (!compact2 && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
16257
- if (!compact2 && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
16258
- if (!compact2 && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
16259
- if (!compact2 && working?.relevantFiles.length) entries.push({ label: "relevant files", detail: working.relevantFiles.map((file) => compactDisplayPath(sanitizeInlineTerminalText(file), 28)).join(` ${glyphs.separator} `) });
16871
+ if (status.epochIndex !== void 0 && status.epochCount !== void 0 && status.epochTokens !== void 0 && status.epochBudget !== void 0 && status.lifetimeTokens !== void 0 && status.lifetimeBudget !== void 0) {
16872
+ entries.splice(1, 0, {
16873
+ label: "epoch",
16874
+ detail: `#${status.epochIndex} ${formatTokens(status.epochTokens)}/${formatTokens(status.epochBudget)} ${glyphs.separator} lifetime ${formatTokens(status.lifetimeTokens)}/${formatTokens(status.lifetimeBudget)}`
16875
+ });
16876
+ }
16877
+ if (!compact3 && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
16878
+ if (!compact3 && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
16879
+ if (!compact3 && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
16880
+ if (!compact3 && working?.relevantFiles.length) entries.push({ label: "relevant files", detail: working.relevantFiles.map((file) => compactDisplayPath(sanitizeInlineTerminalText(file), 28)).join(` ${glyphs.separator} `) });
16260
16881
  if (sources2?.length) {
16261
16882
  const pinned = sources2.filter((source) => source.state === "pinned");
16262
16883
  const muted = sources2.filter((source) => source.state === "muted");
@@ -16290,7 +16911,7 @@ function ContextInspector({ status, working, summary, width, memory, connections
16290
16911
  /* @__PURE__ */ jsx(MeterBar, { segments, total: meterTotal, width: meterWidth, glyphs })
16291
16912
  ] }) : null
16292
16913
  ] }),
16293
- /* @__PURE__ */ jsx(ListPanel, { title: "", hideTitle: true, entries, width, glyphMode })
16914
+ /* @__PURE__ */ jsx(ListPanel, { title: "", hideTitle: true, entries, width: Math.max(1, rowWidth - 2), glyphMode })
16294
16915
  ] });
16295
16916
  }
16296
16917
  function ThemePreview({ name, width, glyphs }) {
@@ -16377,8 +16998,8 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
16377
16998
  function UpdateNotice({ current, latest, command: command2, highlights, width, glyphs }) {
16378
16999
  const theme = useTheme();
16379
17000
  const availableWidth = safeWidth(width);
16380
- const compact2 = availableWidth < 48;
16381
- const parts = compact2 ? [
17001
+ const compact3 = availableWidth < 48;
17002
+ const parts = compact3 ? [
16382
17003
  { text: `${glyphs.up} `, color: theme.accent, bold: true },
16383
17004
  { text: `v${current}`, color: theme.dim, bold: false },
16384
17005
  { text: ` ${glyphs.arrow} `, color: theme.muted, bold: false },
@@ -16399,7 +17020,7 @@ function UpdateNotice({ current, latest, command: command2, highlights, width, g
16399
17020
  const bullets = bulletWidth > 0 ? (highlights ?? []).map((line) => truncateDisplay(sanitizeInlineTerminalText(line), bulletWidth)) : [];
16400
17021
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
16401
17022
  /* @__PURE__ */ jsx(Box, { children: truncated ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: rendered }) : parts.map((part, index) => /* @__PURE__ */ jsx(Text, { color: part.color, bold: part.bold, children: part.text }, index)) }),
16402
- compact2 ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(` run ${command2}`, availableWidth) }) : null,
17023
+ compact3 ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(` run ${command2}`, availableWidth) }) : null,
16403
17024
  bullets.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: `${bulletPrefix}${line}` }, index))
16404
17025
  ] });
16405
17026
  }
@@ -17265,6 +17886,40 @@ function clipTimelineItem(item, options) {
17265
17886
  if (item.kind === "notice") {
17266
17887
  return { ...item, text: tailText(item.text, width, options.rows) };
17267
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
+ }
17268
17923
  if (item.kind === "update") {
17269
17924
  const baseRows = width < 48 ? 3 : 2;
17270
17925
  if (options.rows < baseRows) {
@@ -17307,9 +17962,9 @@ function tailText(value, width, maxRows) {
17307
17962
  return `${marker}
17308
17963
  ${selected.join("\n")}`;
17309
17964
  }
17310
- function estimateTimelineItemRows(item, { width, compact: compact2 = false, showToolOutput = false, expandedToolId }) {
17965
+ function estimateTimelineItemRows(item, { width, compact: compact3 = false, showToolOutput = false, expandedToolId }) {
17311
17966
  const rowWidth = Math.max(1, Math.floor(width));
17312
- const gap = compact2 ? 0 : 1;
17967
+ const gap = compact3 ? 0 : 1;
17313
17968
  if (item.kind === "user") return wrappedRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
17314
17969
  if (item.kind === "assistant") {
17315
17970
  return 1 + richTextRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
@@ -17321,7 +17976,7 @@ function estimateTimelineItemRows(item, { width, compact: compact2 = false, show
17321
17976
  const detail = item.errorDetail || item.detail;
17322
17977
  const detailRows = narrow && detail ? 1 : 0;
17323
17978
  const metaRows = item.meta ? 1 : 0;
17324
- const outputRows = (showToolOutput || item.id === expandedToolId) && item.output ? Math.min(compact2 ? 25 : 81, richTextRows(item.output, Math.max(1, rowWidth - 2))) : 0;
17979
+ const outputRows = (showToolOutput || item.id === expandedToolId) && item.output ? Math.min(compact3 ? 25 : 81, richTextRows(item.output, Math.max(1, rowWidth - 2))) : 0;
17325
17980
  return 1 + detailRows + metaRows + outputRows;
17326
17981
  }
17327
17982
  if (item.kind === "list") {
@@ -17335,7 +17990,7 @@ function estimateTimelineItemRows(item, { width, compact: compact2 = false, show
17335
17990
  if (item.kind === "theme") return 3;
17336
17991
  if (item.kind === "context") {
17337
17992
  const metaRows = rowWidth < 64 ? 2 : 1;
17338
- const spanLimit = compact2 ? 2 : 3;
17993
+ const spanLimit = compact3 ? 2 : 3;
17339
17994
  const spanCount = Math.min(item.spans?.length ?? 0, spanLimit);
17340
17995
  const moreRow = (item.spans?.length ?? 0) > spanLimit ? 1 : 0;
17341
17996
  const degradationRows = item.degradation ? metaRows : 0;
@@ -17362,6 +18017,91 @@ function wrappedRows(value, width) {
17362
18017
  return sanitizeTerminalText(value).split("\n").reduce((rows, line) => rows + Math.max(1, Math.ceil(displayWidth(line) / safeWidth2)), 0);
17363
18018
  }
17364
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
+
17365
18105
  // src/ui/timeline-reducers.ts
17366
18106
  var itemCounter = 0;
17367
18107
  var nextId = () => `ui-${Date.now()}-${itemCounter++}`;
@@ -17565,7 +18305,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17565
18305
  const colorEnabled = config.ui.color && !process.env.NO_COLOR;
17566
18306
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
17567
18307
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
17568
- const [compact2, setCompact] = useState2(config.ui.compact);
18308
+ const [compact3, setCompact] = useState2(config.ui.compact);
17569
18309
  const [interactionMode, setInteractionMode] = useState2(planMode ? "plan" : askMode ? "ask" : "build");
17570
18310
  const [input2, setInput] = useState2("");
17571
18311
  const [busy, setBusy] = useState2(false);
@@ -17602,6 +18342,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17602
18342
  const controller = useRef2(void 0);
17603
18343
  const processing = useRef2(false);
17604
18344
  const queued = useRef2([]);
18345
+ const clarificationBacklog = useRef2([]);
17605
18346
  const stopRequested = useRef2(false);
17606
18347
  const startedInitial = useRef2(false);
17607
18348
  const lastSubmitted = useRef2(void 0);
@@ -17704,8 +18445,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17704
18445
  const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
17705
18446
  return () => clearInterval(timer);
17706
18447
  }, [busy]);
17707
- const requestPermission = useCallback((call, category) => {
17708
- 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
+ }));
17709
18455
  }, []);
17710
18456
  const onEvent = useCallback((event) => {
17711
18457
  switch (event.type) {
@@ -17725,7 +18471,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17725
18471
  ...event.packed.budgetReason ? { budgetReason: event.packed.budgetReason } : {},
17726
18472
  truncated: event.packed.truncated,
17727
18473
  spans: event.packed.hits.slice(0, 5).map((hit) => ({
17728
- path: relative9(runner.workspace.primaryRoot, hit.path) || hit.path,
18474
+ path: relative10(runner.workspace.primaryRoot, hit.path) || hit.path,
17729
18475
  startLine: hit.startLine,
17730
18476
  endLine: hit.endLine,
17731
18477
  score: hit.score,
@@ -17829,7 +18575,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17829
18575
  id: nextId(),
17830
18576
  kind: "notice",
17831
18577
  tone: event.status === "ready" || event.status === "integrated" ? "success" : "error",
17832
- 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.` : ""}`
17833
18579
  });
17834
18580
  break;
17835
18581
  case "agent_done":
@@ -17842,13 +18588,33 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17842
18588
  append({ id: nextId(), kind: "compaction", messages: event.omittedMessages, tokens: event.summaryTokens });
17843
18589
  refreshSession();
17844
18590
  break;
18591
+ case "context_epoch":
18592
+ append({
18593
+ id: nextId(),
18594
+ kind: "notice",
18595
+ tone: "info",
18596
+ text: `Context epoch ${event.previousIndex} \u2192 ${event.index}${separator}${event.reason.replace("_", " ")}${separator}${event.inputTokens + event.outputTokens} tokens preserved in the lifetime ledger.`
18597
+ });
18598
+ refreshSession();
18599
+ break;
18600
+ case "needs_input":
18601
+ append({ id: nextId(), kind: "clarification", pending: event.pending });
18602
+ refreshSession();
18603
+ break;
18604
+ case "input_resolved":
18605
+ append({ id: nextId(), kind: "notice", tone: "success", text: `Clarification resolved${separator}${event.answer}` });
18606
+ refreshSession();
18607
+ break;
18608
+ case "intent":
18609
+ refreshSession();
18610
+ break;
17845
18611
  case "usage":
17846
18612
  refreshSession();
17847
18613
  break;
17848
18614
  case "error":
17849
18615
  lastEventError.current = event.error.message;
17850
18616
  setTimeline(endStreamingAssistants);
17851
- 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.` });
17852
18618
  setActivity(void 0);
17853
18619
  break;
17854
18620
  case "done":
@@ -17872,7 +18638,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17872
18638
  id: nextId(),
17873
18639
  kind: "notice",
17874
18640
  tone: event.reason === "aborted" ? "info" : "error",
17875
- 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
17876
18642
  });
17877
18643
  }
17878
18644
  break;
@@ -17886,9 +18652,16 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17886
18652
  const runLocalCommand = useCallback(async (value) => {
17887
18653
  if (!value.startsWith("/")) return false;
17888
18654
  const [rawCommand = "", ...rest] = value.slice(1).trim().split(/\s+/);
17889
- const command2 = rawCommand.toLocaleLowerCase();
17890
- const argument = rest.join(" ").trim();
18655
+ let command2 = rawCommand.toLocaleLowerCase();
18656
+ let argument = rest.join(" ").trim();
17891
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
+ }
17892
18665
  if (command2 === "exit" || command2 === "quit") {
17893
18666
  exit();
17894
18667
  return true;
@@ -17970,10 +18743,77 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
17970
18743
  });
17971
18744
  return true;
17972
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
+ }
17973
18813
  if (command2 === "changes") {
17974
18814
  const changed = runner.getSession().changedFiles;
17975
18815
  appendList("Changed files", changed.length ? changed.map((path) => ({
17976
- label: relative9(runner.workspace.primaryRoot, path) || ".",
18816
+ label: relative10(runner.workspace.primaryRoot, path) || ".",
17977
18817
  detail: path
17978
18818
  })) : [{ label: "No recorded changes." }]);
17979
18819
  return true;
@@ -18126,7 +18966,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18126
18966
  const skills = extensions?.listSkills() ?? [];
18127
18967
  appendList("Skills", skills.length ? skills.map((skill) => ({
18128
18968
  label: `${skill.name} ${skill.scope}${skill.trusted ? "" : `${separator}untrusted`}`,
18129
- 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}`,
18130
18970
  tone: skill.trusted ? "normal" : "warning"
18131
18971
  })) : [{ label: "No skills discovered.", detail: "Add SKILL.md playbooks under .agents/skills, .claude/skills, or a configured directory." }]);
18132
18972
  return true;
@@ -18274,7 +19114,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18274
19114
  }
18275
19115
  if (command2 === "density") {
18276
19116
  const normalized = argument.toLocaleLowerCase();
18277
- const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact2 : void 0;
19117
+ const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact3 : void 0;
18278
19118
  if (next === void 0) throw new Error("Usage: /density [compact|comfortable]");
18279
19119
  setCompact(next);
18280
19120
  await saveUiPreference({ compact: next });
@@ -18367,7 +19207,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18367
19207
  })));
18368
19208
  return true;
18369
19209
  }
18370
- }, [append, appendList, compact2, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
19210
+ }, [append, appendList, compact3, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
18371
19211
  const submit = useCallback(async (raw, mode = "normal") => {
18372
19212
  const trimmed = raw.trim();
18373
19213
  if (!trimmed) return;
@@ -18382,6 +19222,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18382
19222
  if (processing.current && isExitCommand(trimmed)) {
18383
19223
  stopRequested.current = true;
18384
19224
  queued.current = [];
19225
+ clarificationBacklog.current = [];
18385
19226
  setQueue([]);
18386
19227
  controller.current?.abort();
18387
19228
  exit();
@@ -18455,7 +19296,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18455
19296
  lastEventError.current = void 0;
18456
19297
  try {
18457
19298
  const nextSession = await runner.run(current.runInput, {
18458
- askMode: interactionMode !== "build",
19299
+ askMode: current.readOnly === true || interactionMode !== "build",
18459
19300
  signal: abortController.signal,
18460
19301
  ...current.turnInstructions || interactionMode === "plan" ? {
18461
19302
  turnInstructions: [
@@ -18469,6 +19310,31 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18469
19310
  const snapshot = snapshotSession(nextSession);
18470
19311
  setSession(snapshot);
18471
19312
  setTasks(snapshot.tasks.map((task) => ({ ...task })));
19313
+ if (snapshot.pendingInput) {
19314
+ const deferred = queued.current.length;
19315
+ clarificationBacklog.current.push(...queued.current);
19316
+ queued.current = [];
19317
+ setQueue([]);
19318
+ if (deferred) append({
19319
+ id: nextId(),
19320
+ kind: "notice",
19321
+ tone: "info",
19322
+ text: `Paused ${deferred} queued follow-up${deferred === 1 ? "" : "s"} for clarification; they will resume after the answer.`
19323
+ });
19324
+ break;
19325
+ }
19326
+ if (clarificationBacklog.current.length) {
19327
+ const resumed = clarificationBacklog.current.length;
19328
+ queued.current = [...clarificationBacklog.current, ...queued.current];
19329
+ clarificationBacklog.current = [];
19330
+ setQueue([...queued.current]);
19331
+ append({
19332
+ id: nextId(),
19333
+ kind: "notice",
19334
+ tone: "success",
19335
+ text: `Clarification resolved; resuming ${resumed} queued follow-up${resumed === 1 ? "" : "s"}.`
19336
+ });
19337
+ }
18472
19338
  } catch (error) {
18473
19339
  const message2 = error instanceof Error ? error.message : String(error);
18474
19340
  if (!abortController.signal.aborted && message2 !== lastEventError.current) {
@@ -18481,8 +19347,9 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18481
19347
  }
18482
19348
  }
18483
19349
  if (abortController.signal.aborted || stopRequested.current) {
18484
- const discarded = queued.current.length;
19350
+ const discarded = queued.current.length + clarificationBacklog.current.length;
18485
19351
  queued.current = [];
19352
+ clarificationBacklog.current = [];
18486
19353
  setQueue([]);
18487
19354
  if (discarded) append({ id: nextId(), kind: "notice", text: `Discarded ${discarded} queued follow-up${discarded === 1 ? "" : "s"}.` });
18488
19355
  break;
@@ -18499,10 +19366,10 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18499
19366
  }, [append, exit, interactionMode, onEvent, requestPermission, runLocalCommand, runner]);
18500
19367
  const submitFromComposer = useCallback((raw, mode = "normal") => {
18501
19368
  if (historySearch) {
18502
- const selected = resolveHistorySearch(historySearch, "select");
19369
+ const selected2 = resolveHistorySearch(historySearch, "select");
18503
19370
  setHistorySearch(void 0);
18504
19371
  setHistoryIndex(-1);
18505
- void submit(selected, mode === "normal" && processing.current ? "steer" : mode);
19372
+ void submit(selected2, mode === "normal" && processing.current ? "steer" : mode);
18506
19373
  return;
18507
19374
  }
18508
19375
  if (suggestionMode === "mention" && selectedSuggestion) {
@@ -18513,7 +19380,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18513
19380
  return;
18514
19381
  }
18515
19382
  }
18516
- const suggestion = selectedSuggestion;
19383
+ const selected = selectedSuggestion;
19384
+ const suggestion = selected && !(raw.startsWith(selected.value) && raw.slice(selected.value.length).trim()) ? selected : void 0;
18517
19385
  const normalized = raw.trimEnd();
18518
19386
  if (suggestion && raw.startsWith("/") && suggestion.value !== raw && suggestion.value.endsWith(" ") && suggestion.label !== normalized) {
18519
19387
  setInput(suggestion.value);
@@ -18552,6 +19420,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18552
19420
  text: `Allowed ${call.name} for this exact ${category} target during the session.`
18553
19421
  });
18554
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
+ }
18555
19431
  if (stop) {
18556
19432
  requestRunStop();
18557
19433
  }
@@ -18731,7 +19607,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18731
19607
  const tokenTotal = session.usage.inputTokens + session.usage.outputTokens;
18732
19608
  const contextStatus = runner.getContextStatus();
18733
19609
  const frame = spinnerFrames()[frameIndex % spinnerFrames().length];
18734
- const compactUi = compact2 || terminalHeight < 28;
19610
+ const compactUi = compact3 || terminalHeight < 28;
18735
19611
  const constrainedHeight = terminalHeight < 18;
18736
19612
  const compactComposer = terminalHeight < 18;
18737
19613
  const minimalInspector = terminalHeight < 22;
@@ -18895,7 +19771,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18895
19771
  )
18896
19772
  }
18897
19773
  )
18898
- ] }) : /* @__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 }),
18899
19775
  showFooter ? /* @__PURE__ */ jsx3(
18900
19776
  Footer,
18901
19777
  {
@@ -18947,6 +19823,9 @@ function initialTimeline(session, banner, setupProblem) {
18947
19823
  text: `Contract ${session.taskContract.state} | ${satisfied}/${required.length} accepted`
18948
19824
  });
18949
19825
  }
19826
+ if (session.pendingInput) {
19827
+ items.push({ id: `pending-input-${session.pendingInput.id}`, kind: "clarification", pending: session.pendingInput });
19828
+ }
18950
19829
  if (setupProblem && items.length <= 1) items.push({ id: nextId(), kind: "notice", tone: "error", text: setupProblem });
18951
19830
  return items;
18952
19831
  }
@@ -18992,6 +19871,35 @@ function snapshotSession(source) {
18992
19871
  relevantFiles: [...source.workingMemory.relevantFiles]
18993
19872
  }
18994
19873
  } : {},
19874
+ ...source.contextEpochs ? {
19875
+ contextEpochs: source.contextEpochs.map((epoch) => ({
19876
+ ...epoch,
19877
+ usage: { ...epoch.usage },
19878
+ ...epoch.handoff ? {
19879
+ handoff: {
19880
+ ...epoch.handoff,
19881
+ ...epoch.handoff.contract ? {
19882
+ contract: {
19883
+ ...epoch.handoff.contract,
19884
+ required: epoch.handoff.contract.required.map((criterion2) => ({
19885
+ ...criterion2,
19886
+ evidenceRefs: [...criterion2.evidenceRefs]
19887
+ }))
19888
+ }
19889
+ } : {},
19890
+ unresolvedFailures: epoch.handoff.unresolvedFailures.map((failure) => ({ ...failure })),
19891
+ changedFiles: [...epoch.handoff.changedFiles],
19892
+ checks: epoch.handoff.checks.map((check) => ({ ...check }))
19893
+ }
19894
+ } : {}
19895
+ }))
19896
+ } : {},
19897
+ ...source.intentAssessment ? {
19898
+ intentAssessment: { ...source.intentAssessment, reasons: [...source.intentAssessment.reasons] }
19899
+ } : {},
19900
+ ...source.pendingInput ? {
19901
+ pendingInput: { ...source.pendingInput, options: source.pendingInput.options.map((option2) => ({ ...option2 })) }
19902
+ } : {},
18995
19903
  usage: { ...source.usage }
18996
19904
  };
18997
19905
  }
@@ -19003,6 +19911,14 @@ function cloneValue(value) {
19003
19911
  if (value && typeof value === "object") return cloneRecord(value);
19004
19912
  return value;
19005
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
+ }
19006
19922
  function isExitCommand(value) {
19007
19923
  const command2 = localCommandName(value);
19008
19924
  return command2 === "exit" || command2 === "quit";
@@ -19020,6 +19936,9 @@ function shouldDeferLocalCommand(value) {
19020
19936
  "remember",
19021
19937
  "diff",
19022
19938
  "checkpoints",
19939
+ "audit",
19940
+ "rollback",
19941
+ "recover",
19023
19942
  "workflow",
19024
19943
  "exit",
19025
19944
  "quit"
@@ -19038,17 +19957,17 @@ function composerAttachments(value) {
19038
19957
  const paths = [...value.matchAll(/(?:^|\s)@([^\s]+)/g)].map((match) => match[1]).filter((path) => Boolean(path));
19039
19958
  return [...new Set(paths)].slice(-3);
19040
19959
  }
19041
- function permissionRows(width, hasCwd, compact2) {
19042
- const content = 3 + (hasCwd ? 1 : 0);
19960
+ function permissionRows(width, hasCwd, compact3) {
19961
+ const content = 5 + (hasCwd ? 1 : 0);
19043
19962
  if (width >= 64) return content + 2;
19044
19963
  if (width >= 28) return content + 3;
19045
- if (compact2) return content + 3;
19964
+ if (compact3) return content + 3;
19046
19965
  return content + 5;
19047
19966
  }
19048
- function contextInspectorRows(session, compact2, width, minimal) {
19967
+ function contextInspectorRows(session, compact3, width, minimal) {
19049
19968
  if (minimal) return 2;
19050
19969
  const working = session.workingMemory;
19051
- const entries = 5 + (compact2 ? 0 : (working?.constraints.length ? 1 : 0) + (working?.decisions.length ? 1 : 0) + (working?.openQuestions.length ? 1 : 0) + (working?.relevantFiles.length ? 1 : 0));
19970
+ const entries = 6 + (compact3 ? 0 : (working?.constraints.length ? 1 : 0) + (working?.decisions.length ? 1 : 0) + (working?.openQuestions.length ? 1 : 0) + (working?.relevantFiles.length ? 1 : 0));
19052
19971
  return 2 + entries * (width < 52 ? 2 : 1);
19053
19972
  }
19054
19973
  function contextInspectorStatus(status) {
@@ -19058,7 +19977,13 @@ function contextInspectorStatus(status) {
19058
19977
  activeTokens: status.activeTokens,
19059
19978
  summaryTokens: status.summaryTokens,
19060
19979
  toolTokens: status.toolTokens,
19061
- compactedMessages: status.compactedMessages
19980
+ compactedMessages: status.compactedMessages,
19981
+ epochIndex: status.epochIndex,
19982
+ epochCount: status.epochCount,
19983
+ epochTokens: status.epochTokens,
19984
+ epochBudget: status.epochBudget,
19985
+ lifetimeTokens: status.lifetimeTokens,
19986
+ lifetimeBudget: status.lifetimeBudget
19062
19987
  };
19063
19988
  }
19064
19989
  function toolDetail2(call) {
@@ -19100,14 +20025,14 @@ function WorkspacePreparationView({
19100
20025
  const theme = useTheme();
19101
20026
  const safeWidth2 = Math.max(1, Math.floor(width));
19102
20027
  const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
19103
- const compact2 = safeWidth2 < 48;
20028
+ const compact3 = safeWidth2 < 48;
19104
20029
  const constrained = height < 14;
19105
20030
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
19106
20031
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
19107
20032
  const separator = ascii ? "|" : "\xB7";
19108
20033
  const brand = ascii ? "*" : PRODUCT_MARK;
19109
20034
  const phase = readiness ? "ready" : error ? "error" : progress.phase;
19110
- const phaseLabel = preparationLabel(phase, progress, readiness, compact2);
20035
+ const phaseLabel = preparationLabel(phase, progress, readiness, compact3);
19111
20036
  const detail = preparationDetail(phase, progress, readiness, error);
19112
20037
  const modelLine = `model ${sanitizeTerminalText(model)}`;
19113
20038
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
@@ -19119,14 +20044,14 @@ function WorkspacePreparationView({
19119
20044
  " ",
19120
20045
  PRODUCT_NAME.toUpperCase()
19121
20046
  ] }),
19122
- !compact2 && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
20047
+ !compact3 && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
19123
20048
  " ",
19124
20049
  separator,
19125
20050
  " LOCAL CONTEXT"
19126
20051
  ] }) : null
19127
20052
  ] }),
19128
20053
  !constrained ? /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: truncateDisplay("Ground the workspace before the first request.", innerWidth) }) : null,
19129
- !constrained && !compact2 ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
20054
+ !constrained && !compact3 ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
19130
20055
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
19131
20056
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
19132
20057
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
@@ -19140,10 +20065,10 @@ function WorkspacePreparationView({
19140
20065
  step2.glyph,
19141
20066
  " "
19142
20067
  ] }),
19143
- /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact2 ? 8 : 10) }),
20068
+ /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact3 ? 8 : 10) }),
19144
20069
  /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.muted : theme.dim, children: [
19145
20070
  " ",
19146
- truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact2 ? 12 : 14)))
20071
+ truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact3 ? 12 : 14)))
19147
20072
  ] })
19148
20073
  ] }, step2.id)) }),
19149
20074
  /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
@@ -19284,9 +20209,9 @@ async function runWorkspacePreparation(engine, config, options = {}) {
19284
20209
  await instance.waitUntilExit();
19285
20210
  return result ?? { status: "cancelled" };
19286
20211
  }
19287
- function preparationLabel(phase, progress, readiness, compact2 = false) {
20212
+ function preparationLabel(phase, progress, readiness, compact3 = false) {
19288
20213
  if (phase === "ready") {
19289
- if (compact2) return "Index verified";
20214
+ if (compact3) return "Index verified";
19290
20215
  return readiness?.rebuilt ? "Workspace index created and verified" : "Workspace index verified";
19291
20216
  }
19292
20217
  if (phase === "error") return "Workspace preparation failed";
@@ -19690,7 +20615,7 @@ function OnboardingFlow({ initialConfig, saveConfig, onFinish }) {
19690
20615
  }, [finish, saveConfig, state]);
19691
20616
  return /* @__PURE__ */ jsx5(OnboardingScreen, { state, dispatch, width, compact: compactHeight });
19692
20617
  }
19693
- function OnboardingScreen({ state, dispatch, width, compact: compact2 = false }) {
20618
+ function OnboardingScreen({ state, dispatch, width, compact: compact3 = false }) {
19694
20619
  const theme = useTheme();
19695
20620
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
19696
20621
  const marker = ascii ? ">" : "\u203A";
@@ -19707,14 +20632,14 @@ function OnboardingScreen({ state, dispatch, width, compact: compact2 = false })
19707
20632
  ] }),
19708
20633
  /* @__PURE__ */ jsx5(Text6, { color: theme.border, children: truncateDisplay(stageDivider(ascii, headerWidth), headerWidth) }),
19709
20634
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(stage.name, headerWidth) }),
19710
- !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
20635
+ !compact3 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
19711
20636
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: truncateDisplay(titleForStep(state.step), headerWidth) }),
19712
- !compact2 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
20637
+ !compact3 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
19713
20638
  summary ? /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(summary, headerWidth) }) : null,
19714
- !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
19715
- state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
19716
- state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
19717
- state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
20639
+ !compact3 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
20640
+ state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20641
+ state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20642
+ state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
19718
20643
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
19719
20644
  /* @__PURE__ */ jsxs5(Box4, { children: [
19720
20645
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -19742,18 +20667,18 @@ function OnboardingScreen({ state, dispatch, width, compact: compact2 = false })
19742
20667
  "! ",
19743
20668
  truncateDisplay(state.error, Math.max(1, headerWidth - 2))
19744
20669
  ] }) : null,
19745
- !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
20670
+ !compact3 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
19746
20671
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: footerForStep(state, headerWidth) })
19747
20672
  ] });
19748
20673
  }
19749
- function OptionList({ options, selected, marker, width, compact: compact2 }) {
20674
+ function OptionList({ options, selected, marker, width, compact: compact3 }) {
19750
20675
  const theme = useTheme();
19751
- return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", children: options.map((option, index) => {
20676
+ return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", children: options.map((option2, index) => {
19752
20677
  const active = index === selected;
19753
20678
  const prefix = active ? `${marker} ` : " ";
19754
20679
  const available = Math.max(1, width - displayWidth(prefix) - (active ? 2 : 0));
19755
- const label = `${prefix}${truncateDisplay(option.label, available)}`;
19756
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact2 || index === options.length - 1 ? 0 : 1, children: [
20680
+ const label = `${prefix}${truncateDisplay(option2.label, available)}`;
20681
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact3 || index === options.length - 1 ? 0 : 1, children: [
19757
20682
  /* @__PURE__ */ jsx5(
19758
20683
  Text6,
19759
20684
  {
@@ -19763,8 +20688,8 @@ function OptionList({ options, selected, marker, width, compact: compact2 }) {
19763
20688
  children: active ? padDisplay(label, width) : label
19764
20689
  }
19765
20690
  ),
19766
- !compact2 && width >= 36 || active ? /* @__PURE__ */ jsx5(Box4, { marginLeft: 2, width: Math.max(1, width - 2), children: /* @__PURE__ */ jsx5(Text6, { color: active ? theme.muted : theme.dim, wrap: "wrap", children: option.detail }) }) : null
19767
- ] }, option.label);
20691
+ !compact3 && width >= 36 || active ? /* @__PURE__ */ jsx5(Box4, { marginLeft: 2, width: Math.max(1, width - 2), children: /* @__PURE__ */ jsx5(Text6, { color: active ? theme.muted : theme.dim, wrap: "wrap", children: option2.detail }) }) : null
20692
+ ] }, option2.label);
19768
20693
  }) });
19769
20694
  }
19770
20695
  function Confirmation({ state, width }) {
@@ -19827,7 +20752,7 @@ function titleForStep(step2) {
19827
20752
  return "Saving configuration";
19828
20753
  }
19829
20754
  function descriptionForStep(state) {
19830
- 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.";
19831
20756
  if (state.step === "official-provider") return "Use the API credential issued by the selected provider.";
19832
20757
  if (state.step === "relay-protocol") return "The protocol is explicit so requests and credentials are never guessed.";
19833
20758
  if (state.step === "endpoint") return "Remote endpoints require HTTPS. Loopback development servers may use HTTP.";
@@ -21465,7 +22390,7 @@ process.on("warning", (warning) => {
21465
22390
  var program = new Command();
21466
22391
  program.enablePositionalOptions();
21467
22392
  program.name(PRODUCT_COMMAND).description("A context-first, model-agnostic coding agent with an auditable workspace.").version(package_default.version).showSuggestionAfterError();
21468
- program.argument("[prompt...]", "instruction for the agent").option("-p, --print", "run once and print the result").option("-a, --ask", "retrieval and inspection mode; mutation tools are denied").option("--plan", "read-only planning mode; propose changes without mutating the workspace").option("-q, --quiet", "print only the final response in text mode").addOption(new Option("--output-format <format>", "text, json, or stream-json").choices(["text", "json", "stream-json"]).default("text")).option("--compact", "reduce progress output in print mode").option("--yes", "approve all non-denied tool requests for this run").option("--auto-edit", "approve read/write requests and ask before shell/Git/network").option("--trust-project-config", "allow executable and security-sensitive settings from project config").option("--queue <prompt>", "run an additional prompt after the first one", collect, []).option("-w, --workspace <path>", "primary workspace root", process.cwd()).option("--add-workspace <path>", "additional workspace root", collect, []).option("--config <path>", "explicit config file").option("--provider <provider>", "model provider").option("--model <model>", "model identifier").option("--base-url <url>", "OpenAI-compatible or provider base URL").option("--max-turns <n>", "maximum agent turns").option("--token-budget <n>", "maximum cumulative session tokens").option("--resume [session]", "resume a session by id or prefix").option("-c, --continue", "resume the latest session").option("--no-color", "disable color output").option("--no-checkpoint", "disable pre-mutation checkpoints for this run").action(async (prompts, options) => {
22393
+ program.argument("[prompt...]", "instruction for the agent").option("-p, --print", "run once and print the result").option("-a, --ask", "retrieval and inspection mode; mutation tools are denied").option("--plan", "read-only planning mode; propose changes without mutating the workspace").option("-q, --quiet", "print only the final response in text mode").addOption(new Option("--output-format <format>", "text, json, or stream-json").choices(["text", "json", "stream-json"]).default("text")).option("--compact", "reduce progress output in print mode").option("--yes", "approve all non-denied tool requests for this run").option("--auto-edit", "approve read/write requests and ask before shell/Git/network").option("--trust-project-config", "allow executable and security-sensitive settings from project config").option("--queue <prompt>", "run an additional prompt after the first one", collect, []).option("-w, --workspace <path>", "primary workspace root", process.cwd()).option("--add-workspace <path>", "additional workspace root", collect, []).option("--config <path>", "explicit config file").option("--provider <provider>", "model provider").option("--model <model>", "model identifier").option("--base-url <url>", "OpenAI-compatible or provider base URL").option("--max-turns <n>", "maximum agent turns").option("--epoch-token-budget <n>", "maximum tokens before an internal context handoff").option("--token-budget <n>", "maximum lifetime tokens across the resumed session").option("--resume [session]", "resume a session by id or prefix").option("-c, --continue", "resume the latest session").option("--no-color", "disable color output").option("--no-checkpoint", "disable pre-mutation checkpoints for this run").action(async (prompts, options) => {
21469
22394
  await runChat(prompts, options);
21470
22395
  });
21471
22396
  program.command("init").description("Create a project-local config (preserving an existing .mosaic namespace)").option("-w, --workspace <path>", "workspace root").option("--provider <provider>", "openai, anthropic, gemini, or compatible", "openai").option("--model <model>", "model identifier").option("--base-url <url>", "provider base URL").option("--api-key <key>", "store a provider key in project config (prefer env vars)").option("--index", "build the index after writing config").option("--yes", "use defaults without prompting").action(async (options) => {
@@ -22071,14 +22996,26 @@ async function runCli() {
22071
22996
  try {
22072
22997
  await program.parseAsync(process.argv);
22073
22998
  } catch (error) {
22074
- const message2 = error instanceof Error ? error.message : String(error);
22075
- 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}
22076
23006
  `);
22077
- process.exitCode = 1;
23007
+ process.exitCode = 1;
23008
+ }
22078
23009
  } finally {
22079
23010
  releaseCliNamespaceLeases(cliNamespaceLeases);
22080
23011
  }
22081
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
+ }
22082
23019
  async function runChat(prompts, options) {
22083
23020
  const shouldPrint = options.print === true || !process.stdin.isTTY || !process.stdout.isTTY;
22084
23021
  if (options.ask && options.plan) throw new Error("--ask and --plan cannot be used together.");
@@ -22146,7 +23083,8 @@ async function runChat(prompts, options) {
22146
23083
  color: (options.color ?? config.ui.color) && !process.env.NO_COLOR
22147
23084
  });
22148
23085
  const colorOutput = (options.color ?? config.ui.color) && !process.env.NO_COLOR;
22149
- 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;
22150
23088
  try {
22151
23089
  let session = await runner.run(firstPrompt, {
22152
23090
  askMode: options.ask === true || options.plan === true,
@@ -22156,6 +23094,7 @@ async function runChat(prompts, options) {
22156
23094
  requestPermission
22157
23095
  });
22158
23096
  for (const queued of options.queue) {
23097
+ if (session.pendingInput) break;
22159
23098
  session = await runner.run(queued, {
22160
23099
  askMode: options.ask === true || options.plan === true,
22161
23100
  ...options.plan ? { turnInstructions: PLAN_MODE_INSTRUCTIONS } : {},
@@ -22164,12 +23103,15 @@ async function runChat(prompts, options) {
22164
23103
  requestPermission
22165
23104
  });
22166
23105
  }
22167
- reporter.finish(session);
23106
+ await extensions.close();
23107
+ extensionsClosed = true;
23108
+ const outcome2 = reporter.finish(session);
23109
+ process.exitCode = outcome2.exitCode;
22168
23110
  } catch (error) {
22169
- reporter.fail(error);
22170
- process.exitCode = 1;
23111
+ const outcome2 = reporter.fail(error, runner.getSession());
23112
+ process.exitCode = outcome2.exitCode;
22171
23113
  } finally {
22172
- await extensions.close();
23114
+ if (!extensionsClosed) await extensions.close().catch(() => void 0);
22173
23115
  }
22174
23116
  }
22175
23117
  async function openMemoryStore(config) {
@@ -22333,6 +23275,7 @@ async function runtimeConfig(workspaceInput, options) {
22333
23275
  agent: {
22334
23276
  ...loaded.agent,
22335
23277
  ...options.checkpoint === false ? { checkpointBeforeWrite: false } : {},
23278
+ ...options.epochTokenBudget ? { maxEpochTokens: positiveInt(options.epochTokenBudget, loaded.agent.maxEpochTokens ?? loaded.agent.maxSessionTokens) } : {},
22336
23279
  ...options.tokenBudget ? { maxSessionTokens: positiveInt(options.tokenBudget, loaded.agent.maxSessionTokens) } : {}
22337
23280
  },
22338
23281
  ui: { ...loaded.ui, ...options.color === false ? { color: false } : {} }
@@ -22482,6 +23425,7 @@ function runtimeOptions(options) {
22482
23425
  const provider = options.provider ?? root.provider;
22483
23426
  const model = options.model ?? root.model;
22484
23427
  const baseUrl = options.baseUrl ?? root.baseUrl;
23428
+ const epochTokenBudget = options.epochTokenBudget ?? root.epochTokenBudget;
22485
23429
  const tokenBudget = options.tokenBudget ?? root.tokenBudget;
22486
23430
  return {
22487
23431
  addWorkspace: [...root.addWorkspace ?? [], ...options.addWorkspace ?? []],
@@ -22490,6 +23434,7 @@ function runtimeOptions(options) {
22490
23434
  ...model ? { model } : {},
22491
23435
  ...baseUrl ? { baseUrl } : {},
22492
23436
  ...options.maxTokens ? { maxTokens: options.maxTokens } : {},
23437
+ ...epochTokenBudget ? { epochTokenBudget } : {},
22493
23438
  ...tokenBudget ? { tokenBudget } : {},
22494
23439
  ...root.color !== void 0 ? { color: root.color } : {},
22495
23440
  ...root.checkpoint !== void 0 ? { checkpoint: root.checkpoint } : {},