@skein-code/cli 0.3.8 → 0.3.10

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.8",
223
+ version: "0.3.10",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,9 +236,9 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
- "Startup builds or reuses the local index and validates persisted workspace content before chat",
240
- "Wide fresh sessions add a factual workspace rail for model, context, permissions, tools, and extensions",
241
- "Completion requires current runtime evidence and conservatively tracks dynamic shell mutations"
239
+ "Complex executable requests now use a durable Task Contract with evidence-bound acceptance criteria",
240
+ "Completion stays unverified while required acceptance or final verification remains unresolved",
241
+ "Structured tool failure receipts add concise repair hints, retry budgets, cancellation handling, and identical-call circuit breaking"
242
242
  ]
243
243
  },
244
244
  bin: {
@@ -3279,7 +3279,7 @@ function formatContextHits(hits, roots) {
3279
3279
  }
3280
3280
 
3281
3281
  // src/agent/runner.ts
3282
- import { randomUUID as randomUUID10 } from "node:crypto";
3282
+ import { randomUUID as randomUUID12 } from "node:crypto";
3283
3283
 
3284
3284
  // src/context/manager.ts
3285
3285
  var RECENT_TURN_RESERVE = 3;
@@ -4974,6 +4974,19 @@ var lastRunSchema = z5.object({
4974
4974
  checks: z5.array(verificationEvidenceSchema),
4975
4975
  detail: z5.string(),
4976
4976
  mutationTracking: z5.enum(["complete", "unknown"]).optional(),
4977
+ acceptance: z5.object({
4978
+ state: z5.enum(["draft", "active", "satisfied", "blocked"]),
4979
+ total: z5.number().int().nonnegative(),
4980
+ satisfied: z5.number().int().nonnegative(),
4981
+ pending: z5.number().int().nonnegative(),
4982
+ blocked: z5.number().int().nonnegative(),
4983
+ missingVerification: z5.array(z5.string()),
4984
+ unresolved: z5.array(z5.object({
4985
+ id: z5.string(),
4986
+ description: z5.string(),
4987
+ status: z5.enum(["pending", "satisfied", "blocked"])
4988
+ }).strict())
4989
+ }).strict().optional(),
4977
4990
  reason: z5.string(),
4978
4991
  finishedAt: z5.string()
4979
4992
  }).strict();
@@ -4986,6 +4999,27 @@ var workingMemorySchema = z5.object({
4986
4999
  relevantFiles: z5.array(z5.string()),
4987
5000
  lastUpdatedAt: z5.string()
4988
5001
  }).strict();
5002
+ var taskContractCriterionSchema = z5.object({
5003
+ id: z5.string().min(1).max(128),
5004
+ description: z5.string().min(1).max(2e3),
5005
+ required: z5.boolean(),
5006
+ status: z5.enum(["pending", "satisfied", "blocked"]),
5007
+ evidenceRefs: z5.array(z5.string().min(1).max(256)).max(64),
5008
+ note: z5.string().max(2e3).optional()
5009
+ }).strict();
5010
+ var taskContractSchema = z5.object({
5011
+ version: z5.literal(1),
5012
+ state: z5.enum(["draft", "active", "satisfied", "blocked"]),
5013
+ objective: z5.string().min(1).max(2e4),
5014
+ scope: z5.array(z5.string().min(1).max(2e3)).max(64),
5015
+ constraints: z5.array(z5.string().min(1).max(2e3)).max(64),
5016
+ nonGoals: z5.array(z5.string().min(1).max(2e3)).max(64),
5017
+ acceptanceCriteria: z5.array(taskContractCriterionSchema).min(1).max(64),
5018
+ verificationRequirements: z5.array(z5.string().min(1).max(2e3)).max(64),
5019
+ createdAt: z5.string(),
5020
+ updatedAt: z5.string(),
5021
+ auditBoundaryId: z5.string().min(1).max(128).optional()
5022
+ }).strict();
4989
5023
  var sessionSchema = z5.object({
4990
5024
  id: sessionIdSchema,
4991
5025
  title: z5.string(),
@@ -5003,6 +5037,7 @@ var sessionSchema = z5.object({
5003
5037
  compactedThroughMessageId: z5.string().optional(),
5004
5038
  workingMemory: workingMemorySchema.optional(),
5005
5039
  contextSources: z5.array(contextSourceSchema).max(64).optional(),
5040
+ taskContract: taskContractSchema.optional(),
5006
5041
  lastRun: lastRunSchema.optional(),
5007
5042
  usage: z5.object({
5008
5043
  inputTokens: z5.number().nonnegative(),
@@ -6751,119 +6786,90 @@ var taskTool = {
6751
6786
  }
6752
6787
  };
6753
6788
 
6754
- // src/tools/working-memory.ts
6789
+ // src/tools/task-contract.ts
6790
+ import { randomUUID as randomUUID11 } from "node:crypto";
6755
6791
  import { z as z13 } from "zod";
6756
- var inputSchema9 = z13.discriminatedUnion("action", [
6757
- z13.object({ action: z13.literal("show") }).strict(),
6758
- z13.object({ action: z13.literal("set_goal"), value: z13.string().min(1).max(1e3) }).strict(),
6759
- z13.object({ action: z13.literal("set_focus"), value: z13.string().min(1).max(1e3) }).strict(),
6760
- z13.object({ action: z13.literal("add_constraint"), value: z13.string().min(1).max(1e3) }).strict(),
6761
- z13.object({ action: z13.literal("add_decision"), value: z13.string().min(1).max(1e3) }).strict(),
6762
- z13.object({ action: z13.literal("add_question"), value: z13.string().min(1).max(1e3) }).strict(),
6763
- z13.object({ action: z13.literal("resolve_question"), value: z13.string().min(1).max(1e3) }).strict(),
6764
- z13.object({ action: z13.literal("add_file"), path: z13.string().min(1).max(4e3) }).strict(),
6765
- z13.object({ action: z13.literal("clear"), field: z13.enum(["constraints", "decisions", "openQuestions", "relevantFiles"]) }).strict()
6766
- ]);
6767
- var workingMemoryTool = {
6768
- definition: {
6769
- name: "working_memory",
6770
- description: "Read or update short-term thread state: goal, focus, constraints, decisions, open questions, and relevant files. This state is temporary and is not durable memory.",
6771
- category: "read",
6772
- inputSchema: jsonSchema({
6773
- action: { type: "string", enum: [
6774
- "show",
6775
- "set_goal",
6776
- "set_focus",
6777
- "add_constraint",
6778
- "add_decision",
6779
- "add_question",
6780
- "resolve_question",
6781
- "add_file",
6782
- "clear"
6783
- ] },
6784
- value: { type: "string", description: "Text for a goal, focus, constraint, decision, or question." },
6785
- path: { type: "string", description: "Workspace-relative relevant file path." },
6786
- field: { type: "string", enum: ["constraints", "decisions", "openQuestions", "relevantFiles"] }
6787
- }, ["action"])
6788
- },
6789
- async execute(arguments_, context) {
6790
- const input2 = inputSchema9.parse(arguments_);
6791
- const memory = context.session.workingMemory ?? emptyWorkingMemory2();
6792
- switch (input2.action) {
6793
- case "show":
6794
- break;
6795
- case "set_goal":
6796
- memory.goal = clean(input2.value);
6797
- break;
6798
- case "set_focus":
6799
- memory.focus = clean(input2.value);
6800
- break;
6801
- case "add_constraint":
6802
- pushBounded2(memory.constraints, input2.value, 12);
6803
- break;
6804
- case "add_decision":
6805
- pushBounded2(memory.decisions, input2.value, 12);
6806
- break;
6807
- case "add_question":
6808
- pushBounded2(memory.openQuestions, input2.value, 12);
6809
- break;
6810
- case "resolve_question":
6811
- removeMatching(memory.openQuestions, input2.value);
6812
- break;
6813
- case "add_file": {
6814
- const safe = await context.workspace.resolvePath(input2.path, { expect: "any" });
6815
- pushBounded2(memory.relevantFiles, safe, 24);
6816
- break;
6817
- }
6818
- case "clear":
6819
- memory[input2.field] = [];
6820
- break;
6821
- }
6822
- memory.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
6823
- context.session.workingMemory = memory;
6824
- return { content: render(memory), metadata: { workingMemory: { ...memory } } };
6825
- }
6826
- };
6827
- function emptyWorkingMemory2() {
6792
+
6793
+ // src/agent/task-contract.ts
6794
+ import { randomUUID as randomUUID10 } from "node:crypto";
6795
+ var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
6796
+ function shouldUseTaskContract(request, intent, existing) {
6797
+ if (existing && existing.state !== "satisfied") return true;
6798
+ if (!EXECUTABLE_INTENTS.has(intent)) return false;
6799
+ const value = request.trim();
6800
+ if (value.length < 180) return false;
6801
+ const requirements = value.split(/(?:\n+|[;;。]\s*|\b(?:and|then|also|plus)\b|以及|并且|同时|还要|另外)/iu).filter((item) => item.trim().length >= 12);
6802
+ const complexitySignals = [
6803
+ /\b(?:refactor|migrate|redesign|architecture|end[- ]to[- ]end|across|multiple)\b/iu,
6804
+ /重构|迁移|架构|完整|全链路|多个|跨模块|兼容|同时/iu,
6805
+ /(?:^|\n)\s*(?:[-*]|\d+[.)、])\s+/mu
6806
+ ].filter((pattern) => pattern.test(value)).length;
6807
+ return requirements.length >= 3 || complexitySignals >= 2 || value.length >= 500;
6808
+ }
6809
+ function createDraftTaskContract(request, auditBoundaryId) {
6810
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6811
+ const objective = compact(request, 2e3);
6828
6812
  return {
6829
- goal: "",
6830
- focus: "",
6831
- constraints: [],
6832
- decisions: [],
6833
- openQuestions: [],
6834
- relevantFiles: [],
6835
- lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
6813
+ version: 1,
6814
+ state: "draft",
6815
+ objective,
6816
+ scope: ["Configured workspace roots and the files required by the objective."],
6817
+ constraints: [
6818
+ "Preserve unrelated user changes.",
6819
+ "Use successful tool evidence for acceptance claims."
6820
+ ],
6821
+ nonGoals: [],
6822
+ acceptanceCriteria: [
6823
+ criterion("requested-outcome", `Implement the requested outcome: ${compact(request, 500)}`),
6824
+ criterion("verification", "Run the smallest relevant deterministic verification after the final mutation.")
6825
+ ],
6826
+ verificationRequirements: [
6827
+ "Record at least one successful test, typecheck, lint, build, check, or diff check after the final mutation."
6828
+ ],
6829
+ createdAt: now,
6830
+ updatedAt: now,
6831
+ ...auditBoundaryId ? { auditBoundaryId } : {}
6836
6832
  };
6837
6833
  }
6838
- function pushBounded2(values, value, limit) {
6839
- const normalized = clean(value);
6840
- const existing = values.indexOf(normalized);
6841
- if (existing >= 0) values.splice(existing, 1);
6842
- values.push(normalized);
6843
- if (values.length > limit) values.splice(0, values.length - limit);
6834
+ function successfulContractEvidence(session) {
6835
+ const refs = /* @__PURE__ */ new Set();
6836
+ const contract = session.taskContract;
6837
+ if (!contract) return refs;
6838
+ for (const event of eventsSinceContract(session.audit ?? [], contract)) {
6839
+ if (event.type !== "tool" || event.outcome !== "success") continue;
6840
+ if (event.tool === "task_contract" || event.tool === "task" || event.tool === "working_memory") continue;
6841
+ refs.add(event.id);
6842
+ refs.add(event.toolCallId);
6843
+ }
6844
+ return refs;
6844
6845
  }
6845
- function removeMatching(values, value) {
6846
- const query = value.toLocaleLowerCase().trim();
6847
- for (let index = values.length - 1; index >= 0; index -= 1) {
6848
- const candidate = values[index];
6849
- if (candidate.toLocaleLowerCase() === query || candidate.toLocaleLowerCase().includes(query)) {
6850
- values.splice(index, 1);
6851
- }
6846
+ function eventsSinceContract(audit, contract) {
6847
+ if (contract.auditBoundaryId) {
6848
+ const boundary = audit.findIndex((event) => event.id === contract.auditBoundaryId);
6849
+ if (boundary >= 0) return audit.slice(boundary + 1);
6852
6850
  }
6851
+ return audit.filter((event) => event.createdAt >= contract.createdAt);
6853
6852
  }
6854
- function clean(value) {
6855
- return value.trim().replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/g, "[redacted-secret]").replace(/\b((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret))\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 1e3);
6853
+ function refreshTaskContractState(contract) {
6854
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
6855
+ contract.state = required.some((item) => item.status === "blocked") ? "blocked" : required.length > 0 && required.every((item) => item.status === "satisfied") ? "satisfied" : "active";
6856
+ contract.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6856
6857
  }
6857
- function render(memory) {
6858
- return [
6859
- `Goal: ${memory.goal || "(none)"}`,
6860
- `Focus: ${memory.focus || "(none)"}`,
6861
- `Constraints: ${memory.constraints.length ? memory.constraints.join(" \xB7 ") : "(none)"}`,
6862
- `Decisions: ${memory.decisions.length ? memory.decisions.join(" \xB7 ") : "(none)"}`,
6863
- `Open questions: ${memory.openQuestions.length ? memory.openQuestions.join(" \xB7 ") : "(none)"}`,
6864
- `Relevant files: ${memory.relevantFiles.length ? memory.relevantFiles.join(" \xB7 ") : "(none)"}`
6865
- ].join("\n");
6858
+ function criterion(id, description) {
6859
+ return {
6860
+ id: `${id}-${randomUUID10().slice(0, 8)}`,
6861
+ description,
6862
+ required: true,
6863
+ status: "pending",
6864
+ evidenceRefs: []
6865
+ };
6866
6866
  }
6867
+ function compact(value, limit) {
6868
+ return value.trim().replace(/\s+/gu, " ").slice(0, limit);
6869
+ }
6870
+
6871
+ // src/agent/completion-gate.ts
6872
+ import { createHash as createHash9 } from "node:crypto";
6867
6873
 
6868
6874
  // src/tools/permissions.ts
6869
6875
  import { createHash as createHash8, createHmac, randomBytes } from "node:crypto";
@@ -7025,27 +7031,529 @@ function normalizeFullUrl(value) {
7025
7031
  return normalized.slice(0, 8e3);
7026
7032
  }
7027
7033
  }
7028
- function scopeFingerprint(value) {
7029
- return createHmac("sha256", permissionScopeSecret).update(stableScopeValue(value)).digest("hex").slice(0, 24);
7034
+ function scopeFingerprint(value) {
7035
+ return createHmac("sha256", permissionScopeSecret).update(stableScopeValue(value)).digest("hex").slice(0, 24);
7036
+ }
7037
+ function stableScopeValue(value) {
7038
+ if (value === null) return "null";
7039
+ if (typeof value === "string") return `string:${JSON.stringify(value)}`;
7040
+ if (typeof value === "number") return `number:${String(value)}`;
7041
+ if (typeof value === "boolean") return `boolean:${String(value)}`;
7042
+ if (typeof value === "undefined") return "undefined";
7043
+ if (Array.isArray(value)) return `array:[${value.map(stableScopeValue).join(",")}]`;
7044
+ if (typeof value === "object") {
7045
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableScopeValue(item)}`);
7046
+ return `object:{${entries.join(",")}}`;
7047
+ }
7048
+ return `${typeof value}:${String(value)}`;
7049
+ }
7050
+ function containsShellControl(command2) {
7051
+ return /(?:[;&|<>`\n\r]|\$\(|\$\{)/.test(command2);
7052
+ }
7053
+ function escapeRegExp(value) {
7054
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7055
+ }
7056
+
7057
+ // src/agent/completion-gate.ts
7058
+ function captureVerification(call, result, changeSequence, configuredCommands) {
7059
+ if (call.name !== "shell" && call.name !== "git") return void 0;
7060
+ const command2 = commandForCall(call);
7061
+ if (!command2) return void 0;
7062
+ const normalized = normalizeCommand2(command2);
7063
+ const configured = new Set(configuredCommands.map(normalizeCommand2));
7064
+ const kind = configured.has(normalized) ? "configured" : classifyVerificationCommand(normalized);
7065
+ if (!kind) return void 0;
7066
+ return {
7067
+ toolCallId: call.id,
7068
+ tool: call.name,
7069
+ command: redactCommand(command2),
7070
+ kind,
7071
+ ok: result.ok,
7072
+ changeSequence,
7073
+ commandKey: createHash9("sha256").update(normalized).digest("hex")
7074
+ };
7075
+ }
7076
+ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutationTracking = "complete", taskContract, audit = []) {
7077
+ const files = [...new Set(changedFiles)];
7078
+ const acceptance = taskContract ? buildAcceptance(taskContract, audit, evidence, currentChangeSequence) : void 0;
7079
+ if (mutationTracking === "unknown") {
7080
+ return {
7081
+ status: "unverified",
7082
+ changedFiles: files,
7083
+ checks: [],
7084
+ detail: files.length ? `Workspace changes were observed, but a dynamic shell command prevented complete mutation tracking for ${fileCount(files.length)}.` : "A dynamic shell command may have changed workspace files, but reliable mutation tracking was unavailable.",
7085
+ mutationTracking,
7086
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
7087
+ };
7088
+ }
7089
+ if (!files.length) {
7090
+ if (acceptance && acceptanceUnresolved(acceptance)) {
7091
+ return {
7092
+ status: "unverified",
7093
+ changedFiles: [],
7094
+ checks: [],
7095
+ detail: acceptanceDetail(acceptance),
7096
+ acceptance: acceptanceForCompletion(acceptance, "unverified")
7097
+ };
7098
+ }
7099
+ return {
7100
+ status: "no_changes",
7101
+ changedFiles: [],
7102
+ checks: [],
7103
+ detail: "No workspace files changed in this run.",
7104
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "no_changes") } : {}
7105
+ };
7106
+ }
7107
+ const latestByCommand = /* @__PURE__ */ new Map();
7108
+ for (const item of evidence) {
7109
+ if (item.changeSequence === currentChangeSequence) {
7110
+ latestByCommand.set(item.commandKey, item);
7111
+ }
7112
+ }
7113
+ const checks = [...latestByCommand.values()].map(publicEvidence);
7114
+ if (!checks.length) {
7115
+ return {
7116
+ status: "unverified",
7117
+ changedFiles: files,
7118
+ checks,
7119
+ detail: `No successful verification was recorded after the last change to ${fileCount(files.length)}.`,
7120
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
7121
+ };
7122
+ }
7123
+ const failures = checks.filter((check) => !check.ok);
7124
+ if (failures.length) {
7125
+ return {
7126
+ status: "verification_failed",
7127
+ changedFiles: files,
7128
+ checks,
7129
+ detail: `${failures.length} of ${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} failed.`,
7130
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verification_failed") } : {}
7131
+ };
7132
+ }
7133
+ if (acceptance && acceptanceUnresolved(acceptance)) {
7134
+ return {
7135
+ status: "unverified",
7136
+ changedFiles: files,
7137
+ checks,
7138
+ detail: acceptanceDetail(acceptance),
7139
+ acceptance: acceptanceForCompletion(acceptance, "unverified")
7140
+ };
7141
+ }
7142
+ return {
7143
+ status: "verified",
7144
+ changedFiles: files,
7145
+ checks,
7146
+ detail: `${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} passed for ${fileCount(files.length)}.`,
7147
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verified") } : {}
7148
+ };
7149
+ }
7150
+ function completionRecoveryDirective(completion) {
7151
+ if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
7152
+ const unresolved = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
7153
+ const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
7154
+ return `<runtime-completion-gate status="acceptance_unresolved" authorization="none">
7155
+ The run cannot be marked complete while required Task Contract criteria remain unresolved:
7156
+ ${unresolved || "- Acceptance criteria are satisfied."}${completion.acceptance.missingVerification.length ? `
7157
+ Missing required verification:
7158
+ ${completion.acceptance.missingVerification.map((item) => `- ${item}`).join("\n")}` : ""}${failed ? `
7159
+ Current failed checks:
7160
+ ${failed}` : ""}
7161
+ Complete or explicitly block only these criteria. Mark a criterion satisfied through task_contract only with successful tool audit evidence or a successful tool-call id. Run the smallest missing verification after the final mutation. Do not repeat the final summary or claim acceptance from prose alone.
7162
+ </runtime-completion-gate>`;
7163
+ }
7164
+ if (completion.status === "verification_failed") {
7165
+ const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
7166
+ return `<runtime-completion-gate status="verification_failed" authorization="none">
7167
+ The run cannot be marked complete because current verification failed:
7168
+ ${failed}
7169
+ Inspect the recorded tool output, correct the underlying problem, and rerun the smallest relevant check. Do not repeat the final summary or claim success without a new successful tool result. If the failure cannot be resolved safely, state the exact blocker and leave the result unverified.
7170
+ </runtime-completion-gate>`;
7171
+ }
7172
+ const changeSummary = completion.mutationTracking === "unknown" ? "A dynamic shell command could not be mapped to a complete set of workspace changes." : `The run changed ${fileCount(completion.changedFiles.length)}, but no successful verification command was recorded after the last change.`;
7173
+ return `<runtime-completion-gate status="unverified" authorization="none">
7174
+ ${changeSummary}
7175
+ Run the smallest relevant test, typecheck, lint, build, or git diff --check now. Do not repeat the final summary or claim a check passed without a successful tool result. If verification cannot be run safely, state the exact reason and leave the result unverified.
7176
+ </runtime-completion-gate>`;
7177
+ }
7178
+ function buildAcceptance(contract, audit, evidence, currentChangeSequence) {
7179
+ const contractEvents = eventsSinceContract(audit, contract);
7180
+ const successfulEvents = contractEvents.filter(
7181
+ (event) => event.type === "tool" && event.outcome === "success" && event.tool !== "task_contract" && event.tool !== "task" && event.tool !== "working_memory"
7182
+ );
7183
+ const latestMutationIndex = contractEvents.reduce((latest, event, index) => {
7184
+ const changedFiles = event.metadata?.changedFiles;
7185
+ return Array.isArray(changedFiles) && changedFiles.length > 0 ? index : latest;
7186
+ }, -1);
7187
+ const successfulRefs = /* @__PURE__ */ new Map();
7188
+ for (const event of successfulEvents) {
7189
+ const index = contractEvents.indexOf(event);
7190
+ successfulRefs.set(event.id, { event, index });
7191
+ successfulRefs.set(event.toolCallId, { event, index });
7192
+ }
7193
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
7194
+ const normalized = required.map((item) => {
7195
+ const evidenceValid = item.evidenceRefs.some((ref) => {
7196
+ const event = successfulRefs.get(ref);
7197
+ return event !== void 0 && event.index >= latestMutationIndex;
7198
+ });
7199
+ const status = item.status === "satisfied" && !evidenceValid ? "pending" : item.status;
7200
+ return { id: item.id, description: item.description, status };
7201
+ });
7202
+ const satisfied = normalized.filter((item) => item.status === "satisfied").length;
7203
+ const pending = normalized.filter((item) => item.status === "pending").length;
7204
+ const blocked = normalized.filter((item) => item.status === "blocked").length;
7205
+ const currentChecks = evidence.filter((item) => item.changeSequence === currentChangeSequence && item.ok);
7206
+ const requirements = contract.verificationRequirements.length ? contract.verificationRequirements : ["Record at least one successful test, typecheck, lint, build, check, or diff check after the final mutation."];
7207
+ const missingVerification = requirements.filter(
7208
+ (requirement) => !verificationRequirementMet(requirement, currentChecks)
7209
+ );
7210
+ return {
7211
+ state: contract.state === "draft" ? "draft" : blocked > 0 ? "blocked" : pending > 0 || missingVerification.length > 0 ? "active" : "satisfied",
7212
+ total: normalized.length,
7213
+ satisfied,
7214
+ pending,
7215
+ blocked,
7216
+ missingVerification,
7217
+ unresolved: normalized.filter((item) => item.status !== "satisfied")
7218
+ };
7219
+ }
7220
+ function acceptanceDetail(acceptance) {
7221
+ const parts = [
7222
+ acceptance.pending ? `${acceptance.pending} pending` : "",
7223
+ acceptance.blocked ? `${acceptance.blocked} blocked` : "",
7224
+ acceptance.missingVerification.length ? `${acceptance.missingVerification.length} verification requirements missing` : ""
7225
+ ].filter(Boolean).join(" and ");
7226
+ const unresolved = acceptance.pending + acceptance.blocked;
7227
+ return `Task Contract acceptance is unresolved: ${parts} required ${unresolved === 1 ? "criterion" : "criteria"}.`;
7228
+ }
7229
+ function verificationRequirementMet(requirement, checks) {
7230
+ const normalized = normalizeCommand2(requirement);
7231
+ const broad = normalized.match(/^Record at least one successful (.+) after the final mutation\.?$/iu);
7232
+ if (broad) return checks.length > 0;
7233
+ const commandKey = createHash9("sha256").update(normalized).digest("hex");
7234
+ return checks.some((item) => item.commandKey === commandKey);
7235
+ }
7236
+ function acceptanceUnresolved(acceptance) {
7237
+ return acceptance.pending > 0 || acceptance.blocked > 0 || acceptance.missingVerification.length > 0;
7238
+ }
7239
+ function acceptanceForCompletion(acceptance, status) {
7240
+ if (acceptance.state === "draft" || acceptance.state === "blocked" || status === "verified" || status === "no_changes") return acceptance;
7241
+ return acceptance.state === "active" ? acceptance : { ...acceptance, state: "active" };
7242
+ }
7243
+ function classifyVerificationCommand(command2) {
7244
+ const normalized = normalizeCommand2(command2).toLocaleLowerCase();
7245
+ const segments = normalized.split(/\s*(?:&&|\|\||;)\s*/u).filter(Boolean);
7246
+ if (segments.length > 1) {
7247
+ const kinds = segments.map(classifySingleVerificationCommand);
7248
+ if (kinds.every((kind) => kind !== void 0)) {
7249
+ return kinds.every((kind) => kind === kinds[0]) ? kinds[0] : "check";
7250
+ }
7251
+ return void 0;
7252
+ }
7253
+ return classifySingleVerificationCommand(normalized);
7254
+ }
7255
+ function classifySingleVerificationCommand(command2) {
7256
+ const value = command2.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)+/u, "");
7257
+ if (/^git\s+diff\b.*(?:^|\s)--check(?:\s|$)/u.test(value)) return "diff";
7258
+ if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test(?::[^\s]+)?|test|vitest|jest)(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:vitest|jest)(?:\s|$)/u.test(value) || /^(?:python(?:\d+(?:\.\d+)*)?\s+-m\s+)?pytest(?:\s|$)/u.test(value) || /^(?:cargo|go|dotnet|mvn|mvnw|gradle|gradlew)\s+(?:test|verify)(?:\s|$)/u.test(value) || /^(?:make|just|task)\s+(?:test|verify)(?:\s|$)/u.test(value) || /^node\s+--test(?:\s|$)/u.test(value)) return "test";
7259
+ if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:typecheck|type-check|check:types)(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:tsc|pyright|mypy)(?:\s|$)/u.test(value) || /^(?:tsc|pyright|mypy)(?:\s|$)/u.test(value) || /^cargo\s+check(?:\s|$)/u.test(value)) return "typecheck";
7260
+ if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?lint(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:eslint|biome|ruff)(?:\s|$)/u.test(value) || /^(?:eslint|biome\s+check|ruff\s+check|cargo\s+clippy)(?:\s|$)/u.test(value)) return "lint";
7261
+ if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:build|compile)(?:\s|$)/u.test(value) || /^(?:cargo|go|dotnet|mvn|mvnw|gradle|gradlew)\s+build(?:\s|$)/u.test(value) || /^(?:make|just|task)\s+(?:build|compile)(?:\s|$)/u.test(value)) return "build";
7262
+ if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?check(?:\s|$)/u.test(value) || /^(?:make|just|task|gradle|gradlew)\s+check(?:\s|$)/u.test(value)) return "check";
7263
+ return void 0;
7264
+ }
7265
+ function publicEvidence(item) {
7266
+ return {
7267
+ toolCallId: item.toolCallId,
7268
+ tool: item.tool,
7269
+ command: item.command,
7270
+ kind: item.kind,
7271
+ ok: item.ok
7272
+ };
7273
+ }
7274
+ function normalizeCommand2(command2) {
7275
+ return command2.trim().replace(/[\t ]+/gu, " ");
7276
+ }
7277
+ function redactCommand(command2) {
7278
+ return command2.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\b((?:api[_-]?key|access[_-]?token|authorization|password|secret|token))\s*=\s*([^\s]+)/giu, "$1=[redacted]").replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/gu, "[redacted-secret]").trim().slice(0, 2e3);
7279
+ }
7280
+ function fileCount(count) {
7281
+ return `${count} workspace ${count === 1 ? "file" : "files"}`;
7282
+ }
7283
+
7284
+ // src/tools/task-contract.ts
7285
+ var criterionSchema = z13.object({
7286
+ id: z13.string().min(1).max(128).optional(),
7287
+ description: z13.string().min(1).max(2e3),
7288
+ required: z13.boolean().optional()
7289
+ }).strict();
7290
+ var inputSchema9 = z13.discriminatedUnion("action", [
7291
+ z13.object({ action: z13.literal("show") }).strict(),
7292
+ z13.object({
7293
+ action: z13.literal("replace"),
7294
+ objective: z13.string().min(1).max(2e4),
7295
+ scope: z13.array(z13.string().min(1).max(2e3)).max(64),
7296
+ constraints: z13.array(z13.string().min(1).max(2e3)).max(64),
7297
+ non_goals: z13.array(z13.string().min(1).max(2e3)).max(64),
7298
+ acceptance_criteria: z13.array(criterionSchema).min(1).max(64),
7299
+ verification_requirements: z13.array(z13.string().min(1).max(2e3)).min(1).max(64)
7300
+ }).strict(),
7301
+ z13.object({ action: z13.literal("activate") }).strict(),
7302
+ z13.object({
7303
+ action: z13.literal("update_criterion"),
7304
+ id: z13.string().min(1).max(128),
7305
+ status: z13.enum(["pending", "satisfied", "blocked"]),
7306
+ evidence_refs: z13.array(z13.string().min(1).max(256)).max(64).optional(),
7307
+ note: z13.string().max(2e3).optional()
7308
+ }).strict()
7309
+ ]);
7310
+ var taskContractTool = {
7311
+ definition: {
7312
+ name: "task_contract",
7313
+ description: "Define and update the durable acceptance contract for a complex executable request. Satisfied criteria require successful tool audit evidence IDs or tool-call IDs.",
7314
+ category: "read",
7315
+ inputSchema: {
7316
+ type: "object",
7317
+ properties: {
7318
+ action: { type: "string", enum: ["show", "replace", "activate", "update_criterion"] },
7319
+ objective: { type: "string" },
7320
+ scope: { type: "array", items: { type: "string" } },
7321
+ constraints: { type: "array", items: { type: "string" } },
7322
+ non_goals: { type: "array", items: { type: "string" } },
7323
+ acceptance_criteria: { type: "array", items: {
7324
+ type: "object",
7325
+ properties: {
7326
+ id: { type: "string" },
7327
+ description: { type: "string" },
7328
+ required: { type: "boolean" }
7329
+ },
7330
+ required: ["description"],
7331
+ additionalProperties: false
7332
+ } },
7333
+ verification_requirements: {
7334
+ type: "array",
7335
+ minItems: 1,
7336
+ items: { type: "string" },
7337
+ description: "Required verification commands or the standard any-successful-check requirement."
7338
+ },
7339
+ id: { type: "string" },
7340
+ status: { type: "string", enum: ["pending", "satisfied", "blocked"] },
7341
+ evidence_refs: { type: "array", items: { type: "string" } },
7342
+ note: { type: "string" }
7343
+ },
7344
+ required: ["action"],
7345
+ additionalProperties: false
7346
+ }
7347
+ },
7348
+ async execute(arguments_, context) {
7349
+ const input2 = inputSchema9.parse(arguments_);
7350
+ let contract = context.session.taskContract;
7351
+ if (!contract) throw new Error("No Task Contract is active for this session.");
7352
+ if (input2.action === "replace") {
7353
+ if (contract.state !== "draft") {
7354
+ throw new Error("Only a draft Task Contract can be replaced.");
7355
+ }
7356
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7357
+ const criteria = input2.acceptance_criteria.map((item) => ({
7358
+ id: item.id ?? `criterion-${randomUUID11().slice(0, 8)}`,
7359
+ description: item.description,
7360
+ required: item.required ?? true,
7361
+ status: "pending",
7362
+ evidenceRefs: []
7363
+ }));
7364
+ if (new Set(criteria.map((item) => item.id)).size !== criteria.length) {
7365
+ throw new Error("Task Contract criterion ids must be unique.");
7366
+ }
7367
+ if (!criteria.some((item) => item.required)) {
7368
+ throw new Error("Task Contract requires at least one required acceptance criterion.");
7369
+ }
7370
+ for (const existing of contract.acceptanceCriteria.filter((item) => item.required)) {
7371
+ const retained = criteria.find((item) => item.id === existing.id);
7372
+ if (!retained || !retained.required || retained.description !== existing.description) {
7373
+ throw new Error(`Required criterion cannot be removed or weakened: ${existing.id}`);
7374
+ }
7375
+ }
7376
+ for (const existing of contract.verificationRequirements) {
7377
+ if (!input2.verification_requirements.includes(existing)) {
7378
+ throw new Error(`Verification requirement cannot be removed: ${existing}`);
7379
+ }
7380
+ }
7381
+ for (const requirement of input2.verification_requirements) {
7382
+ validateVerificationRequirement(requirement);
7383
+ }
7384
+ Object.assign(contract, {
7385
+ version: 1,
7386
+ state: "active",
7387
+ objective: input2.objective,
7388
+ scope: input2.scope,
7389
+ constraints: input2.constraints,
7390
+ nonGoals: input2.non_goals,
7391
+ acceptanceCriteria: criteria,
7392
+ verificationRequirements: input2.verification_requirements,
7393
+ createdAt: contract.createdAt,
7394
+ updatedAt: now,
7395
+ ...contract.auditBoundaryId ? { auditBoundaryId: contract.auditBoundaryId } : {}
7396
+ });
7397
+ } else if (input2.action === "activate") {
7398
+ refreshTaskContractState(contract);
7399
+ } else if (input2.action === "update_criterion") {
7400
+ if (contract.state === "draft") {
7401
+ throw new Error("Activate the draft Task Contract before updating acceptance criteria.");
7402
+ }
7403
+ const criterion2 = contract.acceptanceCriteria.find((item) => item.id === input2.id);
7404
+ if (!criterion2) throw new Error(`Unknown Task Contract criterion id: ${input2.id}`);
7405
+ const refs = [...new Set(input2.evidence_refs ?? [])];
7406
+ if (input2.status === "satisfied") {
7407
+ if (!refs.length) throw new Error("Satisfied criteria require at least one evidence_refs entry.");
7408
+ const valid = successfulContractEvidence(context.session);
7409
+ const invalid = refs.filter((ref) => !valid.has(ref));
7410
+ if (invalid.length) {
7411
+ throw new Error(`Unknown or unsuccessful evidence refs: ${invalid.join(", ")}`);
7412
+ }
7413
+ }
7414
+ criterion2.status = input2.status;
7415
+ criterion2.evidenceRefs = input2.status === "satisfied" ? refs : [];
7416
+ if (input2.note === void 0) delete criterion2.note;
7417
+ else criterion2.note = input2.note;
7418
+ refreshTaskContractState(contract);
7419
+ }
7420
+ return {
7421
+ content: formatContract(contract),
7422
+ metadata: { taskContract: structuredClone(contract) }
7423
+ };
7424
+ }
7425
+ };
7426
+ function formatContract(contract) {
7427
+ const satisfied = contract.acceptanceCriteria.filter((item) => item.status === "satisfied").length;
7428
+ const criteria = contract.acceptanceCriteria.map(
7429
+ (item) => `- [${item.status}] ${item.id}: ${item.description}`
7430
+ ).join("\n");
7431
+ return `Task Contract: ${contract.state} (${satisfied}/${contract.acceptanceCriteria.length} satisfied)
7432
+ Objective: ${contract.objective}
7433
+ ${criteria}`;
7434
+ }
7435
+ function validateVerificationRequirement(requirement) {
7436
+ if (/^Record at least one successful (.+) after the final mutation\.?$/iu.test(requirement)) return;
7437
+ if (/\b(?:api[_-]?key|authorization|password|secret|token)\s*=/iu.test(requirement)) {
7438
+ throw new Error("Verification requirements cannot contain credentials.");
7439
+ }
7440
+ if (!classifyVerificationCommand(requirement)) {
7441
+ throw new Error(`Verification requirement is not a recognized deterministic check: ${requirement}`);
7442
+ }
7443
+ }
7444
+
7445
+ // src/tools/working-memory.ts
7446
+ import { z as z14 } from "zod";
7447
+ var inputSchema10 = z14.discriminatedUnion("action", [
7448
+ z14.object({ action: z14.literal("show") }).strict(),
7449
+ z14.object({ action: z14.literal("set_goal"), value: z14.string().min(1).max(1e3) }).strict(),
7450
+ z14.object({ action: z14.literal("set_focus"), value: z14.string().min(1).max(1e3) }).strict(),
7451
+ z14.object({ action: z14.literal("add_constraint"), value: z14.string().min(1).max(1e3) }).strict(),
7452
+ z14.object({ action: z14.literal("add_decision"), value: z14.string().min(1).max(1e3) }).strict(),
7453
+ z14.object({ action: z14.literal("add_question"), value: z14.string().min(1).max(1e3) }).strict(),
7454
+ z14.object({ action: z14.literal("resolve_question"), value: z14.string().min(1).max(1e3) }).strict(),
7455
+ z14.object({ action: z14.literal("add_file"), path: z14.string().min(1).max(4e3) }).strict(),
7456
+ z14.object({ action: z14.literal("clear"), field: z14.enum(["constraints", "decisions", "openQuestions", "relevantFiles"]) }).strict()
7457
+ ]);
7458
+ var workingMemoryTool = {
7459
+ definition: {
7460
+ name: "working_memory",
7461
+ description: "Read or update short-term thread state: goal, focus, constraints, decisions, open questions, and relevant files. This state is temporary and is not durable memory.",
7462
+ category: "read",
7463
+ inputSchema: jsonSchema({
7464
+ action: { type: "string", enum: [
7465
+ "show",
7466
+ "set_goal",
7467
+ "set_focus",
7468
+ "add_constraint",
7469
+ "add_decision",
7470
+ "add_question",
7471
+ "resolve_question",
7472
+ "add_file",
7473
+ "clear"
7474
+ ] },
7475
+ value: { type: "string", description: "Text for a goal, focus, constraint, decision, or question." },
7476
+ path: { type: "string", description: "Workspace-relative relevant file path." },
7477
+ field: { type: "string", enum: ["constraints", "decisions", "openQuestions", "relevantFiles"] }
7478
+ }, ["action"])
7479
+ },
7480
+ async execute(arguments_, context) {
7481
+ const input2 = inputSchema10.parse(arguments_);
7482
+ const memory = context.session.workingMemory ?? emptyWorkingMemory2();
7483
+ switch (input2.action) {
7484
+ case "show":
7485
+ break;
7486
+ case "set_goal":
7487
+ memory.goal = clean(input2.value);
7488
+ break;
7489
+ case "set_focus":
7490
+ memory.focus = clean(input2.value);
7491
+ break;
7492
+ case "add_constraint":
7493
+ pushBounded2(memory.constraints, input2.value, 12);
7494
+ break;
7495
+ case "add_decision":
7496
+ pushBounded2(memory.decisions, input2.value, 12);
7497
+ break;
7498
+ case "add_question":
7499
+ pushBounded2(memory.openQuestions, input2.value, 12);
7500
+ break;
7501
+ case "resolve_question":
7502
+ removeMatching(memory.openQuestions, input2.value);
7503
+ break;
7504
+ case "add_file": {
7505
+ const safe = await context.workspace.resolvePath(input2.path, { expect: "any" });
7506
+ pushBounded2(memory.relevantFiles, safe, 24);
7507
+ break;
7508
+ }
7509
+ case "clear":
7510
+ memory[input2.field] = [];
7511
+ break;
7512
+ }
7513
+ memory.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
7514
+ context.session.workingMemory = memory;
7515
+ return { content: render(memory), metadata: { workingMemory: { ...memory } } };
7516
+ }
7517
+ };
7518
+ function emptyWorkingMemory2() {
7519
+ return {
7520
+ goal: "",
7521
+ focus: "",
7522
+ constraints: [],
7523
+ decisions: [],
7524
+ openQuestions: [],
7525
+ relevantFiles: [],
7526
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
7527
+ };
7528
+ }
7529
+ function pushBounded2(values, value, limit) {
7530
+ const normalized = clean(value);
7531
+ const existing = values.indexOf(normalized);
7532
+ if (existing >= 0) values.splice(existing, 1);
7533
+ values.push(normalized);
7534
+ if (values.length > limit) values.splice(0, values.length - limit);
7030
7535
  }
7031
- function stableScopeValue(value) {
7032
- if (value === null) return "null";
7033
- if (typeof value === "string") return `string:${JSON.stringify(value)}`;
7034
- if (typeof value === "number") return `number:${String(value)}`;
7035
- if (typeof value === "boolean") return `boolean:${String(value)}`;
7036
- if (typeof value === "undefined") return "undefined";
7037
- if (Array.isArray(value)) return `array:[${value.map(stableScopeValue).join(",")}]`;
7038
- if (typeof value === "object") {
7039
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableScopeValue(item)}`);
7040
- return `object:{${entries.join(",")}}`;
7536
+ function removeMatching(values, value) {
7537
+ const query = value.toLocaleLowerCase().trim();
7538
+ for (let index = values.length - 1; index >= 0; index -= 1) {
7539
+ const candidate = values[index];
7540
+ if (candidate.toLocaleLowerCase() === query || candidate.toLocaleLowerCase().includes(query)) {
7541
+ values.splice(index, 1);
7542
+ }
7041
7543
  }
7042
- return `${typeof value}:${String(value)}`;
7043
7544
  }
7044
- function containsShellControl(command2) {
7045
- return /(?:[;&|<>`\n\r]|\$\(|\$\{)/.test(command2);
7545
+ function clean(value) {
7546
+ return value.trim().replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/g, "[redacted-secret]").replace(/\b((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret))\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 1e3);
7046
7547
  }
7047
- function escapeRegExp(value) {
7048
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7548
+ function render(memory) {
7549
+ return [
7550
+ `Goal: ${memory.goal || "(none)"}`,
7551
+ `Focus: ${memory.focus || "(none)"}`,
7552
+ `Constraints: ${memory.constraints.length ? memory.constraints.join(" \xB7 ") : "(none)"}`,
7553
+ `Decisions: ${memory.decisions.length ? memory.decisions.join(" \xB7 ") : "(none)"}`,
7554
+ `Open questions: ${memory.openQuestions.length ? memory.openQuestions.join(" \xB7 ") : "(none)"}`,
7555
+ `Relevant files: ${memory.relevantFiles.length ? memory.relevantFiles.join(" \xB7 ") : "(none)"}`
7556
+ ].join("\n");
7049
7557
  }
7050
7558
 
7051
7559
  // src/tools/index.ts
@@ -7059,6 +7567,7 @@ function createDefaultToolRegistry(_options = {}) {
7059
7567
  shellTool,
7060
7568
  gitTool,
7061
7569
  taskTool,
7570
+ taskContractTool,
7062
7571
  workingMemoryTool
7063
7572
  ]);
7064
7573
  }
@@ -7098,11 +7607,19 @@ ${workspaceRules}` : ""}`;
7098
7607
  }
7099
7608
  function buildSessionStatePrompt(session) {
7100
7609
  const tasks = session.tasks.length ? session.tasks.map((task) => `- [${task.status}] ${task.title}`).join("\n") : "- No active plan.";
7610
+ const contract = session.taskContract && session.taskContract.state !== "satisfied" ? `
7611
+
7612
+ Task Contract (${session.taskContract.state}):
7613
+ Objective: ${session.taskContract.objective}
7614
+ ${session.taskContract.acceptanceCriteria.map(
7615
+ (item) => `- [${item.status}] ${item.id}: ${item.description}`
7616
+ ).join("\n")}
7617
+ Use task_contract to activate or update acceptance. A satisfied criterion requires successful tool evidence.` : "";
7101
7618
  return `<session-state scope="session" authorization="none">
7102
7619
  This is mutable execution state, not a permission grant or higher-priority instruction.
7103
7620
 
7104
7621
  Current saved plan:
7105
- ${tasks}
7622
+ ${tasks}${contract}
7106
7623
  </session-state>`;
7107
7624
  }
7108
7625
  function buildRetrievedContext(packed, mentions, primaryRoot, roots = [primaryRoot]) {
@@ -7170,130 +7687,120 @@ function escapeAttribute3(value) {
7170
7687
  })[character] ?? character);
7171
7688
  }
7172
7689
 
7173
- // src/agent/completion-gate.ts
7174
- import { createHash as createHash9 } from "node:crypto";
7175
- function captureVerification(call, result, changeSequence, configuredCommands) {
7176
- if (call.name !== "shell" && call.name !== "git") return void 0;
7177
- const command2 = commandForCall(call);
7178
- if (!command2) return void 0;
7179
- const normalized = normalizeCommand2(command2);
7180
- const configured = new Set(configuredCommands.map(normalizeCommand2));
7181
- const kind = configured.has(normalized) ? "configured" : classifyVerificationCommand(normalized);
7182
- if (!kind) return void 0;
7183
- return {
7184
- toolCallId: call.id,
7185
- tool: call.name,
7186
- command: redactCommand(command2),
7187
- kind,
7188
- ok: result.ok,
7189
- changeSequence,
7190
- commandKey: createHash9("sha256").update(normalized).digest("hex")
7191
- };
7192
- }
7193
- function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutationTracking = "complete") {
7194
- const files = [...new Set(changedFiles)];
7195
- if (mutationTracking === "unknown") {
7196
- return {
7197
- status: "unverified",
7198
- changedFiles: files,
7199
- checks: [],
7200
- detail: files.length ? `Workspace changes were observed, but a dynamic shell command prevented complete mutation tracking for ${fileCount(files.length)}.` : "A dynamic shell command may have changed workspace files, but reliable mutation tracking was unavailable.",
7201
- mutationTracking
7202
- };
7690
+ // src/agent/tool-recovery.ts
7691
+ import { createHash as createHash10 } from "node:crypto";
7692
+ var RETRY_BUDGET = {
7693
+ schema_input: 3,
7694
+ unknown_tool: 2,
7695
+ permission_denied: 0,
7696
+ command_exit: 3,
7697
+ timeout: 2,
7698
+ cancelled: 0,
7699
+ hook: 1,
7700
+ execution: 3,
7701
+ contract_required: 2
7702
+ };
7703
+ var REPAIR_HINT = {
7704
+ schema_input: "Correct the arguments to match the tool schema.",
7705
+ unknown_tool: "Choose a tool exposed for this turn.",
7706
+ permission_denied: "Do not retry unless the user or configuration changes permission.",
7707
+ command_exit: "Inspect the exit output, change the command or fix the cause, then retry once.",
7708
+ timeout: "Narrow the operation or increase its allowed timeout.",
7709
+ cancelled: "Stop work and preserve the current state.",
7710
+ hook: "Fix the hook failure before relying on the tool result.",
7711
+ execution: "Use the error detail to change the inputs or approach.",
7712
+ contract_required: "Activate the Task Contract before any workspace mutation."
7713
+ };
7714
+ var ToolRecoveryController = class {
7715
+ signatures = /* @__PURE__ */ new Map();
7716
+ classFailures = /* @__PURE__ */ new Map();
7717
+ toolClasses = /* @__PURE__ */ new Map();
7718
+ preflight(call) {
7719
+ const callKey = callSignature(call);
7720
+ const signatureState = this.signatures.get(callKey);
7721
+ if (signatureState && (signatureState.failures >= 2 || !isRetryable(signatureState.failureClass))) {
7722
+ return this.receipt(
7723
+ call,
7724
+ signatureState.failureClass,
7725
+ signatureState.failures + 1,
7726
+ true
7727
+ );
7728
+ }
7729
+ const lastClass = this.toolClasses.get(call.name);
7730
+ if (lastClass && (this.classFailures.get(lastClass) ?? 0) >= RETRY_BUDGET[lastClass] && RETRY_BUDGET[lastClass] > 0) {
7731
+ return this.receipt(call, lastClass, (signatureState?.failures ?? 0) + 1, true);
7732
+ }
7733
+ return void 0;
7203
7734
  }
7204
- if (!files.length) {
7205
- return {
7206
- status: "no_changes",
7207
- changedFiles: [],
7208
- checks: [],
7209
- detail: "No workspace files changed in this run."
7210
- };
7735
+ recordFailure(call, failureClass) {
7736
+ const callKey = callSignature(call);
7737
+ const current = this.signatures.get(callKey);
7738
+ const failures = current?.failureClass === failureClass ? current.failures + 1 : 1;
7739
+ this.signatures.set(callKey, { failureClass, failures });
7740
+ this.toolClasses.set(call.name, failureClass);
7741
+ this.classFailures.set(failureClass, (this.classFailures.get(failureClass) ?? 0) + 1);
7742
+ return this.receipt(
7743
+ call,
7744
+ failureClass,
7745
+ failures,
7746
+ failures >= 2 || !isRetryable(failureClass)
7747
+ );
7211
7748
  }
7212
- const latestByCommand = /* @__PURE__ */ new Map();
7213
- for (const item of evidence) {
7214
- if (item.changeSequence === currentChangeSequence) {
7215
- latestByCommand.set(item.commandKey, item);
7216
- }
7749
+ recordSuccess(call) {
7750
+ this.signatures.delete(callSignature(call));
7751
+ this.toolClasses.delete(call.name);
7217
7752
  }
7218
- const checks = [...latestByCommand.values()].map(publicEvidence);
7219
- if (!checks.length) {
7753
+ receipt(call, failureClass, attempt, circuitOpen) {
7754
+ const budget = RETRY_BUDGET[failureClass];
7755
+ const consumed = this.classFailures.get(failureClass) ?? 0;
7220
7756
  return {
7221
- status: "unverified",
7222
- changedFiles: files,
7223
- checks,
7224
- detail: `No successful verification was recorded after the last change to ${fileCount(files.length)}.`
7757
+ class: failureClass,
7758
+ retryable: isRetryable(failureClass) && consumed < budget && !circuitOpen,
7759
+ repairHint: REPAIR_HINT[failureClass],
7760
+ attempt,
7761
+ remaining: Math.max(0, budget - consumed),
7762
+ circuitOpen,
7763
+ signature: failureSignature(call, failureClass)
7225
7764
  };
7226
7765
  }
7227
- const failures = checks.filter((check) => !check.ok);
7228
- if (failures.length) {
7229
- return {
7230
- status: "verification_failed",
7231
- changedFiles: files,
7232
- checks,
7233
- detail: `${failures.length} of ${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} failed.`
7234
- };
7766
+ };
7767
+ function classifyToolFailure(result, fallback = "execution") {
7768
+ if (result.metadata?.failureClass && isFailureClass(result.metadata.failureClass)) {
7769
+ return result.metadata.failureClass;
7235
7770
  }
7236
- return {
7237
- status: "verified",
7238
- changedFiles: files,
7239
- checks,
7240
- detail: `${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} passed for ${fileCount(files.length)}.`
7241
- };
7242
- }
7243
- function completionRecoveryDirective(completion) {
7244
- if (completion.status === "verification_failed") {
7245
- const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
7246
- return `<runtime-completion-gate status="verification_failed" authorization="none">
7247
- The run cannot be marked complete because current verification failed:
7248
- ${failed}
7249
- Inspect the recorded tool output, correct the underlying problem, and rerun the smallest relevant check. Do not repeat the final summary or claim success without a new successful tool result. If the failure cannot be resolved safely, state the exact blocker and leave the result unverified.
7250
- </runtime-completion-gate>`;
7771
+ if (result.metadata?.aborted === true) return "cancelled";
7772
+ if (result.metadata?.timedOut === true) return "timeout";
7773
+ if (result.metadata?.hookError) return "hook";
7774
+ if (typeof result.metadata?.exitCode === "number" && result.metadata.exitCode !== 0) {
7775
+ return "command_exit";
7251
7776
  }
7252
- const changeSummary = completion.mutationTracking === "unknown" ? "A dynamic shell command could not be mapped to a complete set of workspace changes." : `The run changed ${fileCount(completion.changedFiles.length)}, but no successful verification command was recorded after the last change.`;
7253
- return `<runtime-completion-gate status="unverified" authorization="none">
7254
- ${changeSummary}
7255
- Run the smallest relevant test, typecheck, lint, build, or git diff --check now. Do not repeat the final summary or claim a check passed without a successful tool result. If verification cannot be run safely, state the exact reason and leave the result unverified.
7256
- </runtime-completion-gate>`;
7777
+ return fallback;
7257
7778
  }
7258
- function classifyVerificationCommand(command2) {
7259
- const normalized = normalizeCommand2(command2).toLocaleLowerCase();
7260
- const segments = normalized.split(/\s*(?:&&|\|\||;)\s*/u).filter(Boolean);
7261
- if (segments.length > 1) {
7262
- const kinds = segments.map(classifySingleVerificationCommand);
7263
- if (kinds.every((kind) => kind !== void 0)) {
7264
- return kinds.every((kind) => kind === kinds[0]) ? kinds[0] : "check";
7265
- }
7266
- return void 0;
7267
- }
7268
- return classifySingleVerificationCommand(normalized);
7779
+ function formatFailureReceipt(receipt) {
7780
+ return `Failure: ${receipt.class}; attempt ${receipt.attempt}; ${receipt.remaining} retries remain; circuit ${receipt.circuitOpen ? "open" : "closed"}. Repair: ${receipt.repairHint}`;
7269
7781
  }
7270
- function classifySingleVerificationCommand(command2) {
7271
- const value = command2.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)+/u, "");
7272
- if (/^git\s+diff\b.*(?:^|\s)--check(?:\s|$)/u.test(value)) return "diff";
7273
- if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test(?::[^\s]+)?|test|vitest|jest)(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:vitest|jest)(?:\s|$)/u.test(value) || /^(?:python(?:\d+(?:\.\d+)*)?\s+-m\s+)?pytest(?:\s|$)/u.test(value) || /^(?:cargo|go|dotnet|mvn|mvnw|gradle|gradlew)\s+(?:test|verify)(?:\s|$)/u.test(value) || /^(?:make|just|task)\s+(?:test|verify)(?:\s|$)/u.test(value) || /^node\s+--test(?:\s|$)/u.test(value)) return "test";
7274
- if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:typecheck|type-check|check:types)(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:tsc|pyright|mypy)(?:\s|$)/u.test(value) || /^(?:tsc|pyright|mypy)(?:\s|$)/u.test(value) || /^cargo\s+check(?:\s|$)/u.test(value)) return "typecheck";
7275
- if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?lint(?:\s|$)/u.test(value) || /^(?:npx|pnpx|bunx)\s+(?:eslint|biome|ruff)(?:\s|$)/u.test(value) || /^(?:eslint|biome\s+check|ruff\s+check|cargo\s+clippy)(?:\s|$)/u.test(value)) return "lint";
7276
- if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:build|compile)(?:\s|$)/u.test(value) || /^(?:cargo|go|dotnet|mvn|mvnw|gradle|gradlew)\s+build(?:\s|$)/u.test(value) || /^(?:make|just|task)\s+(?:build|compile)(?:\s|$)/u.test(value)) return "build";
7277
- if (/^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?check(?:\s|$)/u.test(value) || /^(?:make|just|task|gradle|gradlew)\s+check(?:\s|$)/u.test(value)) return "check";
7278
- return void 0;
7782
+ function isRetryable(failureClass) {
7783
+ return RETRY_BUDGET[failureClass] > 0;
7279
7784
  }
7280
- function publicEvidence(item) {
7281
- return {
7282
- toolCallId: item.toolCallId,
7283
- tool: item.tool,
7284
- command: item.command,
7285
- kind: item.kind,
7286
- ok: item.ok
7287
- };
7785
+ function callSignature(call) {
7786
+ return createHash10("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}`).digest("hex");
7288
7787
  }
7289
- function normalizeCommand2(command2) {
7290
- return command2.trim().replace(/[\t ]+/gu, " ");
7788
+ function failureSignature(call, failureClass) {
7789
+ return createHash10("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}\0${failureClass}`).digest("hex");
7291
7790
  }
7292
- function redactCommand(command2) {
7293
- return command2.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\b((?:api[_-]?key|access[_-]?token|authorization|password|secret|token))\s*=\s*([^\s]+)/giu, "$1=[redacted]").replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/gu, "[redacted-secret]").trim().slice(0, 2e3);
7791
+ function redact(value, key = "") {
7792
+ if (/api[_-]?key|authorization|cookie|password|secret|token/i.test(key)) return "[redacted]";
7793
+ if (Array.isArray(value)) return value.map((item) => redact(item));
7794
+ if (value && typeof value === "object") {
7795
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([name, item]) => [name, redact(item, name)]));
7796
+ }
7797
+ return value;
7294
7798
  }
7295
- function fileCount(count) {
7296
- return `${count} workspace ${count === 1 ? "file" : "files"}`;
7799
+ function stableJson(value) {
7800
+ return JSON.stringify(value) ?? String(value);
7801
+ }
7802
+ function isFailureClass(value) {
7803
+ return typeof value === "string" && Object.hasOwn(RETRY_BUDGET, value);
7297
7804
  }
7298
7805
 
7299
7806
  // src/agent/rules.ts
@@ -7538,6 +8045,8 @@ var AgentRunner = class {
7538
8045
  const changeSequenceAtStart = this.changeSequence;
7539
8046
  const runChangedFiles = /* @__PURE__ */ new Set();
7540
8047
  const verificationEvidence = [];
8048
+ const toolRecovery = new ToolRecoveryController();
8049
+ let activeRunContract = this.session.taskContract;
7541
8050
  let mutationTracking = "complete";
7542
8051
  let completionRecoveryAttempted = false;
7543
8052
  const recordExecution = (call, result) => {
@@ -7556,12 +8065,21 @@ var AgentRunner = class {
7556
8065
  );
7557
8066
  if (evidence) verificationEvidence.push(evidence);
7558
8067
  };
7559
- const completionReport = () => buildRunCompletion(
7560
- runChangedFiles,
7561
- verificationEvidence,
7562
- this.changeSequence,
7563
- mutationTracking
7564
- );
8068
+ const completionReport = () => {
8069
+ const completion = buildRunCompletion(
8070
+ runChangedFiles,
8071
+ verificationEvidence,
8072
+ this.changeSequence,
8073
+ mutationTracking,
8074
+ activeRunContract,
8075
+ this.session.audit ?? []
8076
+ );
8077
+ if (completion.acceptance && activeRunContract && activeRunContract.state !== "draft") {
8078
+ activeRunContract.state = completion.acceptance.state;
8079
+ activeRunContract.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8080
+ }
8081
+ return completion;
8082
+ };
7565
8083
  const finishRun = async (reason, completion = completionReport()) => {
7566
8084
  this.session.lastRun = {
7567
8085
  ...completion,
@@ -7569,6 +8087,10 @@ var AgentRunner = class {
7569
8087
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
7570
8088
  };
7571
8089
  await this.persist();
8090
+ const finalContract = activeRunContract && structuredClone(activeRunContract);
8091
+ if (finalContract && finalContract.state !== "satisfied") {
8092
+ await emit({ type: "contract", contract: finalContract });
8093
+ }
7572
8094
  await emit({ type: "done", reason, completion });
7573
8095
  return this.session;
7574
8096
  };
@@ -7613,10 +8135,22 @@ var AgentRunner = class {
7613
8135
  const turnDirective = buildTurnDirective(request, {
7614
8136
  agents: Boolean(this.config.agents?.enabled)
7615
8137
  });
8138
+ const contractEnabled = shouldUseTaskContract(
8139
+ request,
8140
+ turnDirective.intent,
8141
+ this.session.taskContract
8142
+ );
8143
+ if (contractEnabled && (!this.session.taskContract || this.session.taskContract.state === "satisfied")) {
8144
+ this.session.taskContract = createDraftTaskContract(request, this.session.audit?.at(-1)?.id);
8145
+ await this.persist();
8146
+ await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
8147
+ }
8148
+ activeRunContract = contractEnabled ? this.session.taskContract : void 0;
7616
8149
  const promptSections = [
7617
8150
  `intent:${turnDirective.intent}`,
7618
8151
  ...workspaceRules ? ["rules"] : [],
7619
8152
  ...this.session.workingMemory ? ["working-memory"] : [],
8153
+ ...contractEnabled ? ["task-contract"] : [],
7620
8154
  ...this.session.contextSummary ? ["session-summary"] : [],
7621
8155
  ...!trivialTurn ? [`context:${packed.engine}`] : [],
7622
8156
  ...packed.text ? [`code:${packed.engine}`] : [],
@@ -7669,7 +8203,7 @@ var AgentRunner = class {
7669
8203
  );
7670
8204
  const availableTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
7671
8205
  const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(
7672
- options.askMode ? this.tools.definitions().filter((tool) => tool.category === "read") : this.tools.definitions()
8206
+ visibleToolDefinitions(this.tools, options.askMode === true, contractEnabled)
7673
8207
  );
7674
8208
  if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
7675
8209
  return finishRun("token_budget");
@@ -7678,8 +8212,8 @@ var AgentRunner = class {
7678
8212
  this.config.model.maxTokens ?? 8192,
7679
8213
  availableTokens - estimatedInputTokens
7680
8214
  ));
7681
- const visibleTools = options.askMode ? this.tools.definitions().filter((tool) => tool.category === "read") : this.tools.definitions();
7682
- const assistantId = randomUUID10();
8215
+ const visibleTools = visibleToolDefinitions(this.tools, options.askMode === true, contractEnabled);
8216
+ const assistantId = randomUUID12();
7683
8217
  const response = await this.completeModel(
7684
8218
  messages,
7685
8219
  visibleTools,
@@ -7722,9 +8256,17 @@ var AgentRunner = class {
7722
8256
  return finishRun("token_budget");
7723
8257
  }
7724
8258
  if (response.toolCalls.length) {
8259
+ const visibleToolNames = new Set(visibleTools.map((tool) => tool.name));
7725
8260
  for (const call of response.toolCalls) {
7726
8261
  throwIfAborted(options.signal);
7727
- const result = await this.executeTool(call, options, emit);
8262
+ const result = await this.executeTool(
8263
+ call,
8264
+ options,
8265
+ emit,
8266
+ toolRecovery,
8267
+ visibleToolNames
8268
+ );
8269
+ if (call.name === "task_contract") activeRunContract = this.session.taskContract;
7728
8270
  recordExecution(call, result);
7729
8271
  this.session.messages.push(message("tool", result.content, {
7730
8272
  toolCallId: result.toolCallId,
@@ -7753,8 +8295,11 @@ Review these results, correct any failures if needed, then provide the final ans
7753
8295
  continue;
7754
8296
  }
7755
8297
  const completion = completionReport();
7756
- if (this.config.agent.autoVerify && (completion.status === "unverified" || completion.status === "verification_failed") && !completionRecoveryAttempted && turn < maxTurns) {
8298
+ if ((this.config.agent.autoVerify || Boolean(completion.acceptance)) && (completion.status === "unverified" || completion.status === "verification_failed") && !completionRecoveryAttempted && turn < maxTurns) {
7757
8299
  completionRecoveryAttempted = true;
8300
+ if (activeRunContract) {
8301
+ await emit({ type: "contract", contract: structuredClone(activeRunContract) });
8302
+ }
7758
8303
  this.session.messages.push(message("user", completionRecoveryDirective(completion)));
7759
8304
  await this.persist();
7760
8305
  await this.runAfterTurnHook(turn, [], options.signal);
@@ -7822,10 +8367,29 @@ ${input2}`
7822
8367
  if (final) return final;
7823
8368
  return { content, toolCalls: [] };
7824
8369
  }
7825
- async executeTool(call, options, emit) {
8370
+ async executeTool(call, options, emit, recovery = new ToolRecoveryController(), visibleToolNames) {
8371
+ const preflight = recovery.preflight(call);
8372
+ if (preflight) {
8373
+ const result = failedResult(call, "Tool call rejected by the recovery circuit.", preflight);
8374
+ this.recordToolResult(result);
8375
+ await emit({ type: "tool_result", result });
8376
+ return result;
8377
+ }
8378
+ if (visibleToolNames && !visibleToolNames.has(call.name)) {
8379
+ const receipt = recovery.recordFailure(call, "unknown_tool");
8380
+ const result = failedResult(
8381
+ call,
8382
+ `Tool is not exposed for this turn: ${call.name}`,
8383
+ receipt
8384
+ );
8385
+ this.recordToolResult(result);
8386
+ await emit({ type: "tool_result", result });
8387
+ return result;
8388
+ }
7826
8389
  const tool = this.tools.get(call.name);
7827
8390
  if (!tool) {
7828
- const result = failedResult(call, `Unknown tool: ${call.name}`);
8391
+ const receipt = recovery.recordFailure(call, "unknown_tool");
8392
+ const result = failedResult(call, `Unknown tool: ${call.name}`, receipt);
7829
8393
  this.recordToolResult(result);
7830
8394
  await emit({ type: "tool_result", result });
7831
8395
  return result;
@@ -7836,7 +8400,20 @@ ${input2}`
7836
8400
  tool.permissionCategories?.(call.arguments) ?? [tool.definition.category]
7837
8401
  );
7838
8402
  } catch (error) {
7839
- const result = failedResult(call, formatToolError(error));
8403
+ const failureClass = classifyThrownToolFailure(error, options.signal);
8404
+ const receipt = recovery.recordFailure(call, failureClass);
8405
+ const result = failedResult(call, formatToolError(error), receipt);
8406
+ this.recordToolResult(result, tool.definition.category);
8407
+ await emit({ type: "tool_result", result });
8408
+ return result;
8409
+ }
8410
+ if (categories.some((category) => category !== "read") && this.session.taskContract?.state === "draft") {
8411
+ const receipt = recovery.recordFailure(call, "contract_required");
8412
+ const result = failedResult(
8413
+ call,
8414
+ "Potentially mutating work is paused until the draft Task Contract is activated.",
8415
+ receipt
8416
+ );
7840
8417
  this.recordToolResult(result, tool.definition.category);
7841
8418
  await emit({ type: "tool_result", result });
7842
8419
  return result;
@@ -7844,7 +8421,8 @@ ${input2}`
7844
8421
  for (const category of categories) {
7845
8422
  const allowed = await this.authorize(call, category, options, emit);
7846
8423
  if (!allowed) {
7847
- const result = failedResult(call, `Permission denied for ${category} operation.`);
8424
+ const receipt = recovery.recordFailure(call, "permission_denied");
8425
+ const result = failedResult(call, `Permission denied for ${category} operation.`, receipt);
7848
8426
  this.recordToolResult(result, category);
7849
8427
  await emit({ type: "tool_result", result });
7850
8428
  return result;
@@ -7906,15 +8484,30 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
7906
8484
  ...afterHookError ? { toolSucceeded: true, hookError: afterHookError.message } : {}
7907
8485
  }
7908
8486
  };
8487
+ if (!result.ok) {
8488
+ const failureClass = options.signal?.aborted ? "cancelled" : classifyToolFailure(result);
8489
+ const receipt = recovery.recordFailure(call, failureClass);
8490
+ result.content = truncateToolOutput(`${formatFailureReceipt(receipt)}
8491
+ ${result.content}`);
8492
+ result.metadata = { ...result.metadata, failure: receipt };
8493
+ } else {
8494
+ recovery.recordSuccess(call);
8495
+ }
7909
8496
  this.contextManager.recordTool(this.session, call, result);
7910
8497
  if (JSON.stringify(this.session.tasks) !== tasksBefore || call.name === "task") {
7911
8498
  await emit({ type: "tasks", tasks: this.session.tasks.map((task) => ({ ...task })) });
7912
8499
  }
8500
+ if (call.name === "task_contract" && this.session.taskContract) {
8501
+ await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
8502
+ }
7913
8503
  this.recordToolResult(result, tool.definition.category);
7914
8504
  await emit({ type: "tool_result", result });
7915
8505
  return result;
7916
8506
  } catch (error) {
7917
- const result = failedResult(call, formatToolError(error));
8507
+ const normalized = toError(error);
8508
+ const failureClass = classifyThrownToolFailure(normalized, options.signal);
8509
+ const receipt = recovery.recordFailure(call, failureClass);
8510
+ const result = failedResult(call, formatToolError(error), receipt);
7918
8511
  this.recordToolResult(result, tool.definition.category);
7919
8512
  await emit({ type: "tool_result", result });
7920
8513
  return result;
@@ -7983,7 +8576,7 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
7983
8576
  }
7984
8577
  appendAudit(event) {
7985
8578
  const audit = this.session.audit ?? (this.session.audit = []);
7986
- audit.push({ id: randomUUID10(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
8579
+ audit.push({ id: randomUUID12(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
7987
8580
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
7988
8581
  }
7989
8582
  async acceptChangedFiles(paths) {
@@ -8004,7 +8597,7 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
8004
8597
  const results = [];
8005
8598
  for (const command2 of this.config.agent.verifyCommands) {
8006
8599
  const call = {
8007
- id: `verify-${randomUUID10()}`,
8600
+ id: `verify-${randomUUID12()}`,
8008
8601
  name: "shell",
8009
8602
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
8010
8603
  };
@@ -8071,7 +8664,7 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
8071
8664
  };
8072
8665
  function message(role, content, extra = {}) {
8073
8666
  return {
8074
- id: randomUUID10(),
8667
+ id: randomUUID12(),
8075
8668
  role,
8076
8669
  content,
8077
8670
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8142,14 +8735,21 @@ function estimateResponseTokens(response) {
8142
8735
  function uniqueCategories(categories) {
8143
8736
  return [...new Set(categories)];
8144
8737
  }
8145
- function failedResult(call, content) {
8738
+ function failedResult(call, content, failure) {
8146
8739
  return {
8147
8740
  toolCallId: call.id,
8148
8741
  name: call.name,
8149
8742
  ok: false,
8150
- content: truncateToolOutput(content)
8743
+ content: truncateToolOutput(failure ? `${formatFailureReceipt(failure)}
8744
+ ${content}` : content),
8745
+ ...failure ? { metadata: { failure } } : {}
8151
8746
  };
8152
8747
  }
8748
+ function visibleToolDefinitions(tools, askMode, contractEnabled) {
8749
+ return tools.definitions().filter(
8750
+ (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract")
8751
+ );
8752
+ }
8153
8753
  function truncateToolOutput(content) {
8154
8754
  const max = 8e4;
8155
8755
  return content.length <= max ? content : `${content.slice(0, max)}
@@ -8160,6 +8760,13 @@ function formatToolError(error) {
8160
8760
  if (normalized.name === "ZodError") return `Invalid tool arguments: ${normalized.message}`;
8161
8761
  return normalized.message;
8162
8762
  }
8763
+ function classifyThrownToolFailure(error, signal) {
8764
+ const normalized = toError(error);
8765
+ if (isAbortError(normalized) || signal?.aborted) return "cancelled";
8766
+ if (normalized.name === "HookError") return "hook";
8767
+ if (normalized.name === "ZodError" || normalized.name === "ToolInputError") return "schema_input";
8768
+ return "execution";
8769
+ }
8163
8770
  function titleFromInput(input2) {
8164
8771
  return input2.trim().replace(/\s+/g, " ").slice(0, 80) || "New session";
8165
8772
  }
@@ -8335,9 +8942,9 @@ function integer(value, fallback, min, max) {
8335
8942
  }
8336
8943
 
8337
8944
  // src/agent/delegation.ts
8338
- import { randomUUID as randomUUID12 } from "node:crypto";
8945
+ import { randomUUID as randomUUID14 } from "node:crypto";
8339
8946
  import { join as join17 } from "node:path";
8340
- import { z as z15 } from "zod";
8947
+ import { z as z16 } from "zod";
8341
8948
 
8342
8949
  // src/agent/external-runtime.ts
8343
8950
  async function runExternalAgent(request) {
@@ -8464,86 +9071,86 @@ function numeric(...values) {
8464
9071
  }
8465
9072
 
8466
9073
  // src/agent/team-store.ts
8467
- import { createHash as createHash10, randomUUID as randomUUID11 } from "node:crypto";
9074
+ import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
8468
9075
  import { lstat as lstat17, readFile as readFile14, readdir as readdir6, rm as rm2 } from "node:fs/promises";
8469
9076
  import { join as join15, resolve as resolve15 } from "node:path";
8470
- import { z as z14 } from "zod";
8471
- var runIdSchema = z14.string().uuid();
8472
- var hashSchema = z14.string().regex(/^[a-f0-9]{64}$/u);
8473
- var artifactSchema = z14.object({
9077
+ import { z as z15 } from "zod";
9078
+ var runIdSchema = z15.string().uuid();
9079
+ var hashSchema = z15.string().regex(/^[a-f0-9]{64}$/u);
9080
+ var artifactSchema = z15.object({
8474
9081
  sha256: hashSchema,
8475
- bytes: z14.number().int().nonnegative().max(5e5)
9082
+ bytes: z15.number().int().nonnegative().max(5e5)
8476
9083
  }).strict();
8477
- var phaseSchema = z14.enum(["work", "review", "revision", "write"]);
8478
- var agentRecordSchema = z14.object({
8479
- id: z14.string().uuid(),
8480
- profile: z14.string(),
8481
- provider: z14.string(),
8482
- model: z14.string(),
9084
+ var phaseSchema = z15.enum(["work", "review", "revision", "write"]);
9085
+ var agentRecordSchema = z15.object({
9086
+ id: z15.string().uuid(),
9087
+ profile: z15.string(),
9088
+ provider: z15.string(),
9089
+ model: z15.string(),
8483
9090
  phase: phaseSchema,
8484
- ok: z14.boolean(),
8485
- createdAt: z14.string(),
8486
- startedAt: z14.string().optional(),
8487
- endedAt: z14.string().optional(),
8488
- durationMs: z14.number().int().nonnegative().optional(),
8489
- toolCalls: z14.number().int().nonnegative().optional(),
8490
- usage: z14.object({
8491
- inputTokens: z14.number().int().nonnegative(),
8492
- outputTokens: z14.number().int().nonnegative()
9091
+ ok: z15.boolean(),
9092
+ createdAt: z15.string(),
9093
+ startedAt: z15.string().optional(),
9094
+ endedAt: z15.string().optional(),
9095
+ durationMs: z15.number().int().nonnegative().optional(),
9096
+ toolCalls: z15.number().int().nonnegative().optional(),
9097
+ usage: z15.object({
9098
+ inputTokens: z15.number().int().nonnegative(),
9099
+ outputTokens: z15.number().int().nonnegative()
8493
9100
  }).strict().optional(),
8494
9101
  report: artifactSchema
8495
9102
  }).strict();
8496
- var messageRecordSchema = z14.object({
8497
- id: z14.string().uuid(),
8498
- from: z14.string(),
8499
- to: z14.string(),
8500
- createdAt: z14.string(),
9103
+ var messageRecordSchema = z15.object({
9104
+ id: z15.string().uuid(),
9105
+ from: z15.string(),
9106
+ to: z15.string(),
9107
+ createdAt: z15.string(),
8501
9108
  content: artifactSchema
8502
9109
  }).strict();
8503
- var writerIntegrationSchema = z14.object({
8504
- status: z14.enum(["ready", "conflict", "integrated"]),
8505
- checkedAt: z14.string(),
8506
- detail: z14.string().max(2e4),
8507
- checkpoint: z14.object({
8508
- sessionId: z14.string(),
8509
- checkpointId: z14.string()
9110
+ var writerIntegrationSchema = z15.object({
9111
+ status: z15.enum(["ready", "conflict", "integrated"]),
9112
+ checkedAt: z15.string(),
9113
+ detail: z15.string().max(2e4),
9114
+ checkpoint: z15.object({
9115
+ sessionId: z15.string(),
9116
+ checkpointId: z15.string()
8510
9117
  }).strict().optional(),
8511
- integratedAt: z14.string().optional()
9118
+ integratedAt: z15.string().optional()
8512
9119
  }).strict();
8513
- var writerLaneSchema = z14.object({
8514
- profile: z14.string(),
8515
- reviewer: z14.string(),
8516
- baseCommit: z14.string().regex(/^[a-f0-9]{40,64}$/u),
8517
- outcome: z14.enum(["accepted", "rejected", "failed", "cancelled"]),
9120
+ var writerLaneSchema = z15.object({
9121
+ profile: z15.string(),
9122
+ reviewer: z15.string(),
9123
+ baseCommit: z15.string().regex(/^[a-f0-9]{40,64}$/u),
9124
+ outcome: z15.enum(["accepted", "rejected", "failed", "cancelled"]),
8518
9125
  patch: artifactSchema,
8519
- files: z14.array(z14.string().min(1).max(4e3)).max(2e3),
8520
- worktreeCleaned: z14.boolean(),
9126
+ files: z15.array(z15.string().min(1).max(4e3)).max(2e3),
9127
+ worktreeCleaned: z15.boolean(),
8521
9128
  review: artifactSchema.optional(),
8522
9129
  integration: writerIntegrationSchema.optional()
8523
9130
  }).strict();
8524
9131
  var manifestFields = {
8525
9132
  id: runIdSchema,
8526
- workspace: z14.string(),
8527
- objective: z14.string().max(3e4),
8528
- reviewer: z14.string(),
8529
- createdAt: z14.string(),
8530
- updatedAt: z14.string(),
8531
- status: z14.enum(["running", "accepted", "rejected", "failed"]),
8532
- maxReviewRounds: z14.number().int().min(0).max(3),
8533
- reviewRounds: z14.number().int().min(0).max(3),
8534
- agents: z14.array(agentRecordSchema).max(256),
8535
- messages: z14.array(messageRecordSchema).max(512)
9133
+ workspace: z15.string(),
9134
+ objective: z15.string().max(3e4),
9135
+ reviewer: z15.string(),
9136
+ createdAt: z15.string(),
9137
+ updatedAt: z15.string(),
9138
+ status: z15.enum(["running", "accepted", "rejected", "failed"]),
9139
+ maxReviewRounds: z15.number().int().min(0).max(3),
9140
+ reviewRounds: z15.number().int().min(0).max(3),
9141
+ agents: z15.array(agentRecordSchema).max(256),
9142
+ messages: z15.array(messageRecordSchema).max(512)
8536
9143
  };
8537
- var manifestV1Schema = z14.object({
8538
- version: z14.literal(1),
9144
+ var manifestV1Schema = z15.object({
9145
+ version: z15.literal(1),
8539
9146
  ...manifestFields
8540
9147
  }).strict();
8541
- var manifestV2Schema = z14.object({
8542
- version: z14.literal(2),
9148
+ var manifestV2Schema = z15.object({
9149
+ version: z15.literal(2),
8543
9150
  ...manifestFields,
8544
9151
  writer: writerLaneSchema.optional()
8545
9152
  }).strict();
8546
- var manifestSchema2 = z14.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
9153
+ var manifestSchema2 = z15.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
8547
9154
  var TeamRunStore = class {
8548
9155
  workspace;
8549
9156
  directory;
@@ -8558,7 +9165,7 @@ var TeamRunStore = class {
8558
9165
  const now = (/* @__PURE__ */ new Date()).toISOString();
8559
9166
  const manifest = manifestSchema2.parse({
8560
9167
  version: 2,
8561
- id: randomUUID11(),
9168
+ id: randomUUID13(),
8562
9169
  workspace: this.workspace,
8563
9170
  objective: input2.objective,
8564
9171
  reviewer: input2.reviewer,
@@ -8722,7 +9329,7 @@ var TeamRunStore = class {
8722
9329
  async writeArtifact(runId, content, truncate = true) {
8723
9330
  const data = boundedArtifactText(content, 5e5, truncate);
8724
9331
  const bytes = Buffer.byteLength(data);
8725
- const sha256 = createHash10("sha256").update(data).digest("hex");
9332
+ const sha256 = createHash11("sha256").update(data).digest("hex");
8726
9333
  const directory = join15(this.runDirectory(runId), "blobs");
8727
9334
  await ensureWorkspaceStorageDirectory(this.workspace, directory, {
8728
9335
  requireActiveNamespace: this.managedDirectory
@@ -8731,7 +9338,7 @@ var TeamRunStore = class {
8731
9338
  try {
8732
9339
  await this.assertRegularFile(path);
8733
9340
  const existing = await readFile14(path);
8734
- if (createHash10("sha256").update(existing).digest("hex") !== sha256) {
9341
+ if (createHash11("sha256").update(existing).digest("hex") !== sha256) {
8735
9342
  throw new Error(`Team artifact hash collision or corruption: ${sha256}`);
8736
9343
  }
8737
9344
  } catch (error) {
@@ -8752,7 +9359,7 @@ var TeamRunStore = class {
8752
9359
  const path = this.artifactPath(runId, artifact.sha256);
8753
9360
  await this.assertRegularFile(path);
8754
9361
  const data = await readFile14(path);
8755
- const hash = createHash10("sha256").update(data).digest("hex");
9362
+ const hash = createHash11("sha256").update(data).digest("hex");
8756
9363
  if (hash !== artifact.sha256 || data.byteLength !== artifact.bytes) {
8757
9364
  throw new Error(`Team artifact integrity check failed: ${artifact.sha256}`);
8758
9365
  }
@@ -8815,7 +9422,7 @@ function resolveAgentModelRoute(team, parent, profile) {
8815
9422
  }
8816
9423
 
8817
9424
  // src/agent/writer-lane.ts
8818
- import { createHash as createHash11 } from "node:crypto";
9425
+ import { createHash as createHash12 } from "node:crypto";
8819
9426
  import { lstat as lstat18, mkdtemp, realpath as realpath7, rm as rm3 } from "node:fs/promises";
8820
9427
  import { tmpdir as tmpdir2 } from "node:os";
8821
9428
  import { isAbsolute as isAbsolute5, join as join16, resolve as resolve16 } from "node:path";
@@ -8913,7 +9520,7 @@ var WriterLane = class {
8913
9520
  return {
8914
9521
  baseCommit,
8915
9522
  patch,
8916
- patchSha256: createHash11("sha256").update(patch).digest("hex"),
9523
+ patchSha256: createHash12("sha256").update(patch).digest("hex"),
8917
9524
  files,
8918
9525
  worktreeCleaned,
8919
9526
  value
@@ -9143,14 +9750,14 @@ async function pathExists2(path) {
9143
9750
  }
9144
9751
 
9145
9752
  // src/agent/delegation.ts
9146
- var writerRunInputSchema = z15.object({
9147
- task: z15.string().min(1).max(2e4),
9148
- profile: z15.string().max(64).optional(),
9149
- reviewer: z15.string().max(64).optional()
9753
+ var writerRunInputSchema = z16.object({
9754
+ task: z16.string().min(1).max(2e4),
9755
+ profile: z16.string().max(64).optional(),
9756
+ reviewer: z16.string().max(64).optional()
9150
9757
  }).strict();
9151
- var writerIntegrateInputSchema = z15.object({
9152
- run_id: z15.string().uuid(),
9153
- patch_sha256: z15.string().regex(/^[a-f0-9]{64}$/u)
9758
+ var writerIntegrateInputSchema = z16.object({
9759
+ run_id: z16.string().uuid(),
9760
+ patch_sha256: z16.string().regex(/^[a-f0-9]{64}$/u)
9154
9761
  }).strict();
9155
9762
  var DelegationManager = class {
9156
9763
  constructor(options) {
@@ -9211,10 +9818,10 @@ var DelegationManager = class {
9211
9818
  },
9212
9819
  async execute(arguments_, context) {
9213
9820
  if (!manager.team.enabled) return { ok: false, content: "Multi-agent delegation is disabled." };
9214
- const input2 = z15.object({
9215
- tasks: z15.array(z15.object({
9216
- profile: z15.string().max(64).optional(),
9217
- task: z15.string().min(1).max(2e4)
9821
+ const input2 = z16.object({
9822
+ tasks: z16.array(z16.object({
9823
+ profile: z16.string().max(64).optional(),
9824
+ task: z16.string().min(1).max(2e4)
9218
9825
  })).min(1).max(manager.team.maxDelegations)
9219
9826
  }).parse(arguments_);
9220
9827
  const tasks = input2.tasks.map((task) => ({
@@ -9260,13 +9867,13 @@ var DelegationManager = class {
9260
9867
  },
9261
9868
  async execute(arguments_, context) {
9262
9869
  if (!manager.team.enabled) return { ok: false, content: "Multi-agent teams are disabled." };
9263
- const input2 = z15.object({
9264
- objective: z15.string().min(1).max(3e4),
9265
- tasks: z15.array(z15.object({
9266
- profile: z15.string().max(64).optional(),
9267
- task: z15.string().min(1).max(2e4)
9870
+ const input2 = z16.object({
9871
+ objective: z16.string().min(1).max(3e4),
9872
+ tasks: z16.array(z16.object({
9873
+ profile: z16.string().max(64).optional(),
9874
+ task: z16.string().min(1).max(2e4)
9268
9875
  })).min(1).max(manager.team.maxDelegations),
9269
- reviewer: z15.string().max(64).optional()
9876
+ reviewer: z16.string().max(64).optional()
9270
9877
  }).parse(arguments_);
9271
9878
  const tasks = input2.tasks.map((task) => ({
9272
9879
  profile: task.profile ?? manager.team.defaultProfile,
@@ -9364,7 +9971,7 @@ var DelegationManager = class {
9364
9971
  maxReviewRounds: 0
9365
9972
  });
9366
9973
  await emit?.({ type: "team_start", id: board.id, objective: task });
9367
- const writerId = randomUUID12();
9974
+ const writerId = randomUUID14();
9368
9975
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
9369
9976
  const draft = await this.writerLane.createDraft(
9370
9977
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -9734,7 +10341,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
9734
10341
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
9735
10342
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
9736
10343
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
9737
- const runId = board?.id ?? randomUUID12();
10344
+ const runId = board?.id ?? randomUUID14();
9738
10345
  await emit?.({ type: "team_start", id: runId, objective });
9739
10346
  try {
9740
10347
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -9792,7 +10399,7 @@ ${review.summary}`,
9792
10399
  }
9793
10400
  }
9794
10401
  async runBatch(runId, tasks, phase, emit, signal) {
9795
- const scheduled = tasks.map((task) => ({ id: randomUUID12(), task }));
10402
+ const scheduled = tasks.map((task) => ({ id: randomUUID14(), task }));
9796
10403
  for (const item of scheduled) {
9797
10404
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
9798
10405
  }
@@ -9901,12 +10508,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
9901
10508
  });
9902
10509
  }
9903
10510
  async peerMessage(runId, from, to, content, emit) {
9904
- const id = randomUUID12();
10511
+ const id = randomUUID14();
9905
10512
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
9906
10513
  await emit?.({ type: "agent_message", id, from, to, content });
9907
10514
  }
9908
10515
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
9909
- const id = scheduledId ?? randomUUID12();
10516
+ const id = scheduledId ?? randomUUID14();
9910
10517
  const profile = this.options.profiles.get(task.profile);
9911
10518
  const configuredRoute = this.team.routes?.[task.profile];
9912
10519
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -10818,11 +11425,11 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10818
11425
  `);
10819
11426
  }
10820
11427
  printText(event) {
10821
- const { quiet, compact } = this.options;
11428
+ const { quiet, compact: compact2 } = this.options;
10822
11429
  if (quiet) return;
10823
11430
  switch (event.type) {
10824
11431
  case "thinking":
10825
- if (!compact) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
11432
+ if (!compact2) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
10826
11433
  `));
10827
11434
  break;
10828
11435
  case "context":
@@ -10832,7 +11439,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10832
11439
  ));
10833
11440
  break;
10834
11441
  case "prompt":
10835
- if (!compact) {
11442
+ if (!compact2) {
10836
11443
  process.stderr.write(this.paint.dim(
10837
11444
  `${this.glyphs.meta} prompt ${this.glyphs.separator} ${event.intent} ${this.glyphs.separator} ${event.sections.join(" + ")} ${this.glyphs.separator} ~${event.estimatedTokens} tokens
10838
11445
  `
@@ -10867,12 +11474,21 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10867
11474
  }
10868
11475
  break;
10869
11476
  case "tasks":
10870
- if (!compact) {
11477
+ if (!compact2) {
10871
11478
  const completed = event.tasks.filter((task) => task.status === "completed").length;
10872
11479
  process.stderr.write(this.paint.dim(`${this.glyphs.meta} plan ${this.glyphs.separator} ${completed}/${event.tasks.length} complete
10873
11480
  `));
10874
11481
  }
10875
11482
  break;
11483
+ case "contract": {
11484
+ const required = event.contract.acceptanceCriteria.filter((item) => item.required);
11485
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
11486
+ process.stderr.write(this.paint.dim(
11487
+ `${this.glyphs.meta} contract ${this.glyphs.separator} ${event.contract.state} ${this.glyphs.separator} ${satisfied}/${required.length} accepted
11488
+ `
11489
+ ));
11490
+ break;
11491
+ }
10876
11492
  case "writer_lane":
10877
11493
  process.stderr.write(
10878
11494
  `${event.status === "ready" || event.status === "integrated" ? this.paint.green(this.glyphs.success) : this.paint.red(this.glyphs.error)} writer ${event.id.slice(0, 8)} ${this.glyphs.separator} ${event.status} ${this.glyphs.separator} ${event.detail}
@@ -10954,6 +11570,7 @@ function sessionSummary(session) {
10954
11570
  createdAt: session.createdAt,
10955
11571
  updatedAt: session.updatedAt,
10956
11572
  tasks: session.tasks,
11573
+ ...session.taskContract ? { taskContract: session.taskContract } : {},
10957
11574
  changedFiles: session.changedFiles,
10958
11575
  ...session.lastRun ? { lastRun: session.lastRun } : {},
10959
11576
  usage: session.usage
@@ -11644,7 +12261,7 @@ function ToolGlyph({ state, glyphs }) {
11644
12261
  if (state === "cancelled") return /* @__PURE__ */ jsx(Text, { color: theme.warning, children: glyphs.warning });
11645
12262
  return /* @__PURE__ */ jsx(Text, { color: theme.error, children: glyphs.error });
11646
12263
  }
11647
- function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact = false }) {
12264
+ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact: compact2 = false }) {
11648
12265
  const theme = useTheme();
11649
12266
  const glyphs = resolveGlyphs(glyphMode);
11650
12267
  if (!items.length) {
@@ -11652,13 +12269,13 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11652
12269
  }
11653
12270
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
11654
12271
  if (item.kind === "user") {
11655
- return /* @__PURE__ */ jsxs(Box, { marginBottom: compact || item.clipped ? 0 : 1, children: [
12272
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: compact2 || item.clipped ? 0 : 1, children: [
11656
12273
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
11657
12274
  /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, wrap: "wrap", children: sanitizeTerminalText(item.text) })
11658
12275
  ] }, item.id);
11659
12276
  }
11660
12277
  if (item.kind === "assistant") {
11661
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact || item.clipped ? 0 : 1, children: [
12278
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact2 || item.clipped ? 0 : 1, children: [
11662
12279
  /* @__PURE__ */ jsxs(Text, { bold: true, color: theme.accent, children: [
11663
12280
  glyphs.brand,
11664
12281
  " ",
@@ -11674,7 +12291,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11674
12291
  }
11675
12292
  if (item.kind === "context") {
11676
12293
  const spans = item.spans ?? [];
11677
- const spanLimit = compact ? 2 : 3;
12294
+ const spanLimit = compact2 ? 2 : 3;
11678
12295
  const innerWidth = Math.max(1, safeWidth(width) - 2);
11679
12296
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
11680
12297
  /* @__PURE__ */ jsx(
@@ -11725,7 +12342,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11725
12342
  const duration = item.durationMs !== void 0 ? formatDuration(item.durationMs) : "";
11726
12343
  const detailText = [detail, duration].filter(Boolean).join(" ");
11727
12344
  const expanded = Boolean(item.output) && (showToolOutput || expandedToolId === item.id);
11728
- const verbose = expanded && item.output ? limitTerminalText(item.output, compact ? 24 : 80) : void 0;
12345
+ const verbose = expanded && item.output ? limitTerminalText(item.output, compact2 ? 24 : 80) : void 0;
11729
12346
  const disclosure = item.output ? expanded ? glyphs.expanded : glyphs.collapsed : "";
11730
12347
  const disclosureWidth = disclosure ? displayWidth(disclosure) + 1 : 0;
11731
12348
  const nameLimit = Math.max(1, Math.min(rowWidth - 2 - disclosureWidth, rowWidth < 64 ? rowWidth - 2 - disclosureWidth : 28));
@@ -11859,7 +12476,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11859
12476
  }
11860
12477
  if (item.kind === "list") return /* @__PURE__ */ jsx(ListPanel, { title: item.title, entries: item.entries, width, glyphMode }, item.id);
11861
12478
  if (item.kind === "context-inspector") {
11862
- return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact, glyphMode }, item.id);
12479
+ return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact: compact2, glyphMode }, item.id);
11863
12480
  }
11864
12481
  if (item.kind === "theme") return /* @__PURE__ */ jsx(ThemePreview, { name: item.name, width, glyphs }, item.id);
11865
12482
  if (item.kind === "banner") {
@@ -11917,15 +12534,18 @@ function WorkspacePanel({ status, width = 36, glyphMode = "auto" }) {
11917
12534
  const inner = Math.max(8, safeWidth(width) - 4);
11918
12535
  const contextLabel = status.context === "empty" ? "ready \xB7 empty workspace" : "ready";
11919
12536
  const mcpLabel = status.mcpTotal ? `${status.mcpConnected}/${status.mcpTotal} connected` : "off";
11920
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, borderStyle: glyphs.borderStyle, borderColor: theme.border, paddingX: 1, children: [
11921
- /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: truncateDisplay(`${glyphs.context} WORKSPACE`, inner) }),
11922
- /* @__PURE__ */ jsx(Text, { color: status.context === "empty" ? theme.warning : theme.success, children: truncateDisplay(`${glyphs.success} context ${contextLabel}`, inner) }),
11923
- status.context === "ready" ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`${status.files} files ${glyphs.separator} ${status.chunks} chunks`, inner) }) : null,
12537
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height: 13, borderStyle: glyphs.borderStyle, borderColor: theme.border, paddingX: 1, children: [
12538
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: truncateDisplay(`${glyphs.brand} WORKSPACE`, inner) }),
12539
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay("CONTEXT", inner) }),
12540
+ /* @__PURE__ */ jsx(Text, { color: status.context === "empty" ? theme.warning : theme.success, children: truncateDisplay(`${glyphs.success} local index ${contextLabel}`, inner) }),
12541
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`${status.files} files ${glyphs.separator} ${status.chunks} chunks`, inner) }),
12542
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay("RUNTIME", inner) }),
11924
12543
  /* @__PURE__ */ jsx(Text, { color: theme.text, children: truncateDisplay(status.model, inner) }),
11925
12544
  /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`mode ${status.mode.toUpperCase()} ${glyphs.separator} ${status.permissions}`, inner) }),
12545
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay("EXTENSIONS", inner) }),
11926
12546
  /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`${status.tools} tools ${glyphs.separator} ${status.skills} skills`, inner) }),
11927
12547
  /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`MCP ${mcpLabel} ${glyphs.separator} memory ${status.memory}`, inner) }),
11928
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay("@file pin \xB7 /status inspect", inner) })
12548
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`@file pin ${glyphs.separator} /status inspect`, inner) })
11929
12549
  ] });
11930
12550
  }
11931
12551
  function TeamWorkbench({ items, tasks, width = 80, glyphMode = "auto", view = "agents", selectedIndex = 0, expanded = false, run, notice }) {
@@ -12066,7 +12686,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
12066
12686
  ] }) : null
12067
12687
  ] });
12068
12688
  }
12069
- function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact = false }) {
12689
+ function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact2 = false }) {
12070
12690
  const theme = useTheme();
12071
12691
  const glyphs = resolveGlyphs(glyphMode);
12072
12692
  const summary = permissionSummary(call);
@@ -12103,7 +12723,7 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
12103
12723
  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: [
12104
12724
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12105
12725
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12106
- ] }) : compact ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
12726
+ ] }) : compact2 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
12107
12727
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12108
12728
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12109
12729
  ] }) : /* @__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)) })
@@ -12364,7 +12984,7 @@ function MeterBar({ segments, total, width, glyphs }) {
12364
12984
  remainder ? /* @__PURE__ */ jsx(Text, { color: theme.border, children: glyphs.meterEmpty.repeat(remainder) }) : null
12365
12985
  ] });
12366
12986
  }
12367
- function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact = false, minimal = false, glyphMode = "auto" }) {
12987
+ function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact: compact2 = false, minimal = false, glyphMode = "auto" }) {
12368
12988
  const theme = useTheme();
12369
12989
  const glyphs = resolveGlyphs(glyphMode);
12370
12990
  if (minimal) {
@@ -12384,10 +13004,10 @@ function ContextInspector({ status, working, summary, width, memory, connections
12384
13004
  { label: "summary", detail: summary ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted` : "not created" },
12385
13005
  { label: "long-term", detail: memory ?? `retrieved by relevance ${glyphs.separator} untrusted context` }
12386
13006
  ];
12387
- if (!compact && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
12388
- if (!compact && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
12389
- if (!compact && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
12390
- if (!compact && working?.relevantFiles.length) entries.push({ label: "relevant files", detail: working.relevantFiles.map((file) => compactDisplayPath(sanitizeInlineTerminalText(file), 28)).join(` ${glyphs.separator} `) });
13007
+ if (!compact2 && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
13008
+ if (!compact2 && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
13009
+ if (!compact2 && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
13010
+ if (!compact2 && working?.relevantFiles.length) entries.push({ label: "relevant files", detail: working.relevantFiles.map((file) => compactDisplayPath(sanitizeInlineTerminalText(file), 28)).join(` ${glyphs.separator} `) });
12391
13011
  if (sources2?.length) {
12392
13012
  const pinned = sources2.filter((source) => source.state === "pinned");
12393
13013
  const muted = sources2.filter((source) => source.state === "muted");
@@ -12466,10 +13086,13 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
12466
13086
  const padding = rowWidth >= 24 ? 2 : 0;
12467
13087
  const innerWidth = Math.max(1, rowWidth - padding);
12468
13088
  const safeEngine = sanitizeInlineTerminalText(engine);
12469
- const meta = rowWidth >= 48 ? `${PRODUCT_NAME.toUpperCase()} ${glyphs.separator} local-first coding workspace` : rowWidth >= 28 ? `New session ${glyphs.separator} v${version}` : `New ${glyphs.separator} v${version}`;
12470
- const cwd = rowWidth >= 48 ? `${glyphs.success} Ready ${glyphs.separator} ${safeEngine} context ${glyphs.separator} ${sanitizeInlineTerminalText(model)} ${glyphs.separator} v${version}` : `cwd ${compactDisplayPath(sanitizeInlineTerminalText(workspace), Math.max(1, innerWidth - 4))}`;
13089
+ const expanded = rowWidth >= 48;
13090
+ const ascii = glyphs.brand === "*";
13091
+ const weaveTop = ascii ? "/\\/\\" : "\u2572\u2571\u2572\u2571";
13092
+ const weaveBottom = ascii ? "\\/\\/" : "\u2571\u2572\u2571\u2572";
13093
+ const meta = expanded ? "grounded coding workspace" : rowWidth >= 28 ? `New session ${glyphs.separator} v${version}` : `New ${glyphs.separator} v${version}`;
13094
+ const status = expanded ? `${glyphs.success} ${safeEngine} index verified ${glyphs.separator} ${sanitizeInlineTerminalText(model)} ${glyphs.separator} v${version}` : `cwd ${compactDisplayPath(sanitizeInlineTerminalText(workspace), Math.max(1, innerWidth - 4))}`;
12471
13095
  const hint = `context runs automatically ${glyphs.separator} @file pins ${glyphs.separator} /help commands`;
12472
- const statusWidth = displayWidth(glyphs.success) + 1;
12473
13096
  return /* @__PURE__ */ jsxs(
12474
13097
  Box,
12475
13098
  {
@@ -12477,15 +13100,27 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
12477
13100
  flexDirection: "column",
12478
13101
  paddingLeft: padding,
12479
13102
  children: [
12480
- /* @__PURE__ */ jsxs(Box, { height: 1, overflowY: "hidden", children: [
13103
+ expanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
13104
+ /* @__PURE__ */ jsxs(Box, { height: 1, overflowY: "hidden", children: [
13105
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: weaveTop }),
13106
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, children: " S K E I N" })
13107
+ ] }),
13108
+ /* @__PURE__ */ jsxs(Box, { height: 1, overflowY: "hidden", children: [
13109
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: weaveBottom }),
13110
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
13111
+ " ",
13112
+ truncateDisplay(meta, Math.max(1, innerWidth - displayWidth(weaveBottom) - 2))
13113
+ ] })
13114
+ ] })
13115
+ ] }) : /* @__PURE__ */ jsxs(Box, { height: 1, overflowY: "hidden", children: [
12481
13116
  /* @__PURE__ */ jsxs(Text, { bold: true, color: theme.accent, children: [
12482
13117
  glyphs.brand,
12483
13118
  " "
12484
13119
  ] }),
12485
- /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, wrap: "truncate", children: truncateDisplay(meta, Math.max(1, innerWidth - statusWidth)) })
13120
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, children: truncateDisplay(meta, Math.max(1, innerWidth - displayWidth(glyphs.brand) - 1)) })
12486
13121
  ] }),
12487
- /* @__PURE__ */ jsx(Text, { color: rowWidth >= 48 ? theme.success : theme.muted, children: truncateDisplay(cwd, innerWidth) }),
12488
- rowWidth >= 48 ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`${hint} ${glyphs.separator} cwd ${compactDisplayPath(sanitizeInlineTerminalText(workspace), 24)}`, innerWidth) }) : null
13122
+ /* @__PURE__ */ jsx(Text, { color: expanded ? theme.success : theme.muted, children: truncateDisplay(status, innerWidth) }),
13123
+ expanded ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`${hint} ${glyphs.separator} cwd ${compactDisplayPath(sanitizeInlineTerminalText(workspace), 24)}`, innerWidth) }) : null
12489
13124
  ]
12490
13125
  }
12491
13126
  );
@@ -12493,8 +13128,8 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
12493
13128
  function UpdateNotice({ current, latest, command: command2, highlights, width, glyphs }) {
12494
13129
  const theme = useTheme();
12495
13130
  const availableWidth = safeWidth(width);
12496
- const compact = availableWidth < 48;
12497
- const parts = compact ? [
13131
+ const compact2 = availableWidth < 48;
13132
+ const parts = compact2 ? [
12498
13133
  { text: `${glyphs.up} `, color: theme.accent, bold: true },
12499
13134
  { text: `v${current}`, color: theme.dim, bold: false },
12500
13135
  { text: ` ${glyphs.arrow} `, color: theme.muted, bold: false },
@@ -12515,7 +13150,7 @@ function UpdateNotice({ current, latest, command: command2, highlights, width, g
12515
13150
  const bullets = bulletWidth > 0 ? (highlights ?? []).map((line) => truncateDisplay(sanitizeInlineTerminalText(line), bulletWidth)) : [];
12516
13151
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
12517
13152
  /* @__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)) }),
12518
- compact ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(` run ${command2}`, availableWidth) }) : null,
13153
+ compact2 ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(` run ${command2}`, availableWidth) }) : null,
12519
13154
  bullets.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: `${bulletPrefix}${line}` }, index))
12520
13155
  ] });
12521
13156
  }
@@ -13423,9 +14058,9 @@ function tailText(value, width, maxRows) {
13423
14058
  return `${marker}
13424
14059
  ${selected.join("\n")}`;
13425
14060
  }
13426
- function estimateTimelineItemRows(item, { width, compact = false, showToolOutput = false, expandedToolId }) {
14061
+ function estimateTimelineItemRows(item, { width, compact: compact2 = false, showToolOutput = false, expandedToolId }) {
13427
14062
  const rowWidth = Math.max(1, Math.floor(width));
13428
- const gap = compact ? 0 : 1;
14063
+ const gap = compact2 ? 0 : 1;
13429
14064
  if (item.kind === "user") return wrappedRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
13430
14065
  if (item.kind === "assistant") {
13431
14066
  return 1 + richTextRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
@@ -13437,7 +14072,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13437
14072
  const detail = item.errorDetail || item.detail;
13438
14073
  const detailRows = narrow && detail ? 1 : 0;
13439
14074
  const metaRows = item.meta ? 1 : 0;
13440
- const outputRows = (showToolOutput || item.id === expandedToolId) && item.output ? Math.min(compact ? 25 : 81, richTextRows(item.output, Math.max(1, rowWidth - 2))) : 0;
14075
+ const outputRows = (showToolOutput || item.id === expandedToolId) && item.output ? Math.min(compact2 ? 25 : 81, richTextRows(item.output, Math.max(1, rowWidth - 2))) : 0;
13441
14076
  return 1 + detailRows + metaRows + outputRows;
13442
14077
  }
13443
14078
  if (item.kind === "list") {
@@ -13451,7 +14086,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13451
14086
  if (item.kind === "theme") return 3;
13452
14087
  if (item.kind === "context") {
13453
14088
  const metaRows = rowWidth < 64 ? 2 : 1;
13454
- const spanLimit = compact ? 2 : 3;
14089
+ const spanLimit = compact2 ? 2 : 3;
13455
14090
  const spanCount = Math.min(item.spans?.length ?? 0, spanLimit);
13456
14091
  const moreRow = (item.spans?.length ?? 0) > spanLimit ? 1 : 0;
13457
14092
  const degradationRows = item.degradation ? metaRows : 0;
@@ -13466,7 +14101,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13466
14101
  if (item.kind === "agent" || item.kind === "agent-message") return rowWidth < 64 ? 2 : 1;
13467
14102
  if (item.kind === "workflow") return rowWidth < 64 ? 2 : 1;
13468
14103
  if (item.kind === "banner") {
13469
- return rowWidth >= 48 ? 4 : 3;
14104
+ return rowWidth >= 48 ? 5 : 3;
13470
14105
  }
13471
14106
  return 1;
13472
14107
  }
@@ -13549,6 +14184,21 @@ function finalizeAssistant(items, id, content) {
13549
14184
  function endStreamingAssistants(items) {
13550
14185
  return items.map((item) => item.kind === "assistant" && item.streaming ? { ...item, streaming: false } : item);
13551
14186
  }
14187
+ function updateContractProgress(items, contract, separator = " | ") {
14188
+ const id = "task-contract-progress";
14189
+ if (contract.state === "satisfied") return items.filter((item) => item.id !== id);
14190
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
14191
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
14192
+ const next = {
14193
+ id,
14194
+ kind: "notice",
14195
+ tone: contract.state === "blocked" ? "error" : "info",
14196
+ text: `Contract ${contract.state}${separator}${satisfied}/${required.length} accepted`
14197
+ };
14198
+ const index = items.findIndex((item) => item.id === id);
14199
+ if (index < 0) return [...items, next].slice(-500);
14200
+ return items.map((item, itemIndex) => itemIndex === index ? next : item);
14201
+ }
13552
14202
  function updateAgent(items, event) {
13553
14203
  const found = items.some((item) => item.kind === "agent" && item.id === event.id);
13554
14204
  if (!found) {
@@ -13641,7 +14291,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13641
14291
  const colorEnabled = config.ui.color && !process.env.NO_COLOR;
13642
14292
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
13643
14293
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
13644
- const [compact, setCompact] = useState2(config.ui.compact);
14294
+ const [compact2, setCompact] = useState2(config.ui.compact);
13645
14295
  const [interactionMode, setInteractionMode] = useState2(planMode ? "plan" : askMode ? "ask" : "build");
13646
14296
  const [input2, setInput] = useState2("");
13647
14297
  const [busy, setBusy] = useState2(false);
@@ -13841,6 +14491,11 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13841
14491
  case "tasks":
13842
14492
  setTasks(event.tasks.map((task) => ({ ...task })));
13843
14493
  break;
14494
+ case "contract": {
14495
+ setTimeline((items) => updateContractProgress(items, event.contract, separator));
14496
+ refreshSession();
14497
+ break;
14498
+ }
13844
14499
  case "skill":
13845
14500
  append({ id: nextId(), kind: "skill", name: event.name, description: event.description });
13846
14501
  break;
@@ -14333,7 +14988,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14333
14988
  }
14334
14989
  if (command2 === "density") {
14335
14990
  const normalized = argument.toLocaleLowerCase();
14336
- const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact : void 0;
14991
+ const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact2 : void 0;
14337
14992
  if (next === void 0) throw new Error("Usage: /density [compact|comfortable]");
14338
14993
  setCompact(next);
14339
14994
  await saveUiPreference({ compact: next });
@@ -14426,7 +15081,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14426
15081
  })));
14427
15082
  return true;
14428
15083
  }
14429
- }, [append, appendList, compact, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
15084
+ }, [append, appendList, compact2, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
14430
15085
  const submit = useCallback(async (raw, mode = "normal") => {
14431
15086
  const trimmed = raw.trim();
14432
15087
  if (!trimmed) return;
@@ -14790,7 +15445,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14790
15445
  const tokenTotal = session.usage.inputTokens + session.usage.outputTokens;
14791
15446
  const contextStatus = runner.getContextStatus();
14792
15447
  const frame = spinnerFrames()[frameIndex % spinnerFrames().length];
14793
- const compactUi = compact || terminalHeight < 28;
15448
+ const compactUi = compact2 || terminalHeight < 28;
14794
15449
  const constrainedHeight = terminalHeight < 18;
14795
15450
  const compactComposer = terminalHeight < 18;
14796
15451
  const minimalInspector = terminalHeight < 22;
@@ -14818,7 +15473,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14818
15473
  const chromeRows = headerRows + composerRows + footerRows + taskRows + paletteRows + inspectorRows + activityRows;
14819
15474
  const availableTimelineRows = Math.max(0, terminalHeight - chromeRows);
14820
15475
  const teamItems = timeline.filter((item) => item.kind === "agent" || item.kind === "agent-message");
14821
- const showWorkspacePanel = Boolean(workspaceReadiness) && contentWidth >= 96 && !teamWorkbenchOpen && !teamItems.some((item) => item.kind === "agent");
15476
+ const showWorkspacePanel = Boolean(workspaceReadiness) && contentWidth >= 88 && terminalHeight >= 20 && !teamWorkbenchOpen && !teamItems.some((item) => item.kind === "agent");
14822
15477
  const workspacePanelWidth = showWorkspacePanel ? Math.min(38, Math.max(32, Math.floor(contentWidth * 0.34))) : 0;
14823
15478
  const workspaceTimelineWidth = Math.max(1, contentWidth - workspacePanelWidth - (showWorkspacePanel ? 1 : 0));
14824
15479
  const timelineContentRows = timeline.reduce((rows2, item) => rows2 + estimateTimelineItemRows(item, {
@@ -14828,7 +15483,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14828
15483
  showToolOutput,
14829
15484
  ...expandedToolId ? { expandedToolId } : {}
14830
15485
  }), 0);
14831
- const timelineRows = teamWorkbenchOpen ? availableTimelineRows : Math.min(availableTimelineRows, Math.max(timelineContentRows, showWorkspacePanel ? 10 : 0));
15486
+ const timelineRows = teamWorkbenchOpen ? availableTimelineRows : Math.min(availableTimelineRows, Math.max(timelineContentRows, showWorkspacePanel ? 13 : 0));
14832
15487
  const showTeamCockpit = config.agents?.cockpit !== false && contentWidth >= 100 && timelineRows >= 7 && teamItems.some((item) => item.kind === "agent");
14833
15488
  const cockpitWidth = showTeamCockpit ? Math.min(38, Math.max(30, Math.floor(contentWidth * 0.32))) : workspacePanelWidth;
14834
15489
  const hasSidePanel = showTeamCockpit || showWorkspacePanel;
@@ -14996,6 +15651,16 @@ function initialTimeline(session, banner, setupProblem) {
14996
15651
  if (!items.length) {
14997
15652
  items.push({ id: nextId(), kind: "banner", model: banner.model, engine: banner.engine, workspace: banner.workspace, version: banner.version });
14998
15653
  }
15654
+ if (session.taskContract && session.taskContract.state !== "satisfied") {
15655
+ const required = session.taskContract.acceptanceCriteria.filter((item) => item.required);
15656
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
15657
+ items.push({
15658
+ id: "task-contract-progress",
15659
+ kind: "notice",
15660
+ tone: session.taskContract.state === "blocked" ? "error" : "info",
15661
+ text: `Contract ${session.taskContract.state} | ${satisfied}/${required.length} accepted`
15662
+ });
15663
+ }
14999
15664
  if (setupProblem && items.length <= 1) items.push({ id: nextId(), kind: "notice", tone: "error", text: setupProblem });
15000
15665
  return items;
15001
15666
  }
@@ -15087,17 +15752,17 @@ function composerAttachments(value) {
15087
15752
  const paths = [...value.matchAll(/(?:^|\s)@([^\s]+)/g)].map((match) => match[1]).filter((path) => Boolean(path));
15088
15753
  return [...new Set(paths)].slice(-3);
15089
15754
  }
15090
- function permissionRows(width, hasCwd, compact) {
15755
+ function permissionRows(width, hasCwd, compact2) {
15091
15756
  const content = 3 + (hasCwd ? 1 : 0);
15092
15757
  if (width >= 64) return content + 2;
15093
15758
  if (width >= 28) return content + 3;
15094
- if (compact) return content + 3;
15759
+ if (compact2) return content + 3;
15095
15760
  return content + 5;
15096
15761
  }
15097
- function contextInspectorRows(session, compact, width, minimal) {
15762
+ function contextInspectorRows(session, compact2, width, minimal) {
15098
15763
  if (minimal) return 2;
15099
15764
  const working = session.workingMemory;
15100
- const entries = 5 + (compact ? 0 : (working?.constraints.length ? 1 : 0) + (working?.decisions.length ? 1 : 0) + (working?.openQuestions.length ? 1 : 0) + (working?.relevantFiles.length ? 1 : 0));
15765
+ 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));
15101
15766
  return 2 + entries * (width < 52 ? 2 : 1);
15102
15767
  }
15103
15768
  function contextInspectorStatus(status) {
@@ -15143,46 +15808,99 @@ function WorkspacePreparationView({
15143
15808
  workspace,
15144
15809
  model,
15145
15810
  width,
15811
+ height = 24,
15146
15812
  frame = 0
15147
15813
  }) {
15148
15814
  const theme = useTheme();
15149
15815
  const safeWidth2 = Math.max(1, Math.floor(width));
15150
15816
  const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
15151
- const compact = safeWidth2 < 48;
15817
+ const compact2 = safeWidth2 < 48;
15818
+ const constrained = height < 14;
15152
15819
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15153
15820
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
15154
15821
  const separator = ascii ? "|" : "\xB7";
15155
15822
  const brand = ascii ? "*" : PRODUCT_MARK;
15156
15823
  const phase = readiness ? "ready" : error ? "error" : progress.phase;
15157
- const activeGlyph = readiness ? ascii ? "[ok]" : "\u2713" : error ? ascii ? "[x]" : "\xD7" : spinner;
15158
- const phaseLabel = preparationLabel(phase, progress, readiness, compact);
15824
+ const phaseLabel = preparationLabel(phase, progress, readiness, compact2);
15159
15825
  const detail = preparationDetail(phase, progress, readiness, error);
15160
15826
  const modelLine = `model ${sanitizeTerminalText(model)}`;
15161
15827
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
15162
- const steps = ["inspect", "index", "validate"];
15163
- const currentIndex = readiness ? steps.length : phase === "validate" ? 2 : phase === "scan" || phase === "index" || phase === "write" ? 1 : 0;
15164
- const tracker = steps.map((step2, index) => {
15165
- const marker = index < currentIndex ? ascii ? "[x]" : "\u25CF" : index === currentIndex && !error ? ascii ? "[>]" : "\u25C6" : ascii ? "[ ]" : "\u25CB";
15166
- return `${marker} ${step2}`;
15167
- }).join(compact ? " " : " ");
15828
+ const steps = preparationSteps(phase, progress, readiness, error, { ascii, spinner });
15168
15829
  return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: safeWidth2 >= 24 ? 2 : 0, children: [
15169
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.accent, children: truncateDisplay(`${brand} ${PRODUCT_NAME.toUpperCase()} WORKSPACE PREP`, innerWidth) }),
15170
- !compact ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
15830
+ /* @__PURE__ */ jsxs4(Box3, { children: [
15831
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.accent, children: [
15832
+ brand,
15833
+ " ",
15834
+ PRODUCT_NAME.toUpperCase()
15835
+ ] }),
15836
+ !compact2 && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
15837
+ " ",
15838
+ separator,
15839
+ " LOCAL CONTEXT"
15840
+ ] }) : null
15841
+ ] }),
15842
+ !constrained ? /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: truncateDisplay("Ground the workspace before the first request.", innerWidth) }) : null,
15843
+ !constrained && !compact2 ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
15171
15844
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
15172
15845
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
15173
- ] }),
15174
- /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: theme.border, children: truncateDisplay(tracker, innerWidth) }) }),
15846
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
15847
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, flexDirection: constrained ? "row" : "column", children: steps.map((step2) => constrained ? /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.dim, children: [
15848
+ step2.glyph,
15849
+ " ",
15850
+ step2.id === "persist" ? "save" : step2.id,
15851
+ step2.id === "verify" ? "" : " "
15852
+ ] }, step2.id) : /* @__PURE__ */ jsxs4(Box3, { height: 1, overflowY: "hidden", children: [
15853
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.border, children: [
15854
+ step2.glyph,
15855
+ " "
15856
+ ] }),
15857
+ /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact2 ? 8 : 10) }),
15858
+ /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.muted : theme.dim, children: [
15859
+ " ",
15860
+ truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact2 ? 12 : 14)))
15861
+ ] })
15862
+ ] }, step2.id)) }),
15175
15863
  /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
15176
15864
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: error ? theme.error : readiness ? theme.success : theme.accent, children: [
15177
- activeGlyph,
15865
+ readiness ? ascii ? "[ok]" : "\u2713" : error ? ascii ? "[x]" : "\xD7" : spinner,
15178
15866
  " "
15179
15867
  ] }),
15180
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.textStrong, children: truncateDisplay(phaseLabel, Math.max(1, innerWidth - displayWidth(activeGlyph) - 1)) })
15868
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.textStrong, children: truncateDisplay(phaseLabel, Math.max(1, innerWidth - 5)) })
15181
15869
  ] }),
15182
15870
  /* @__PURE__ */ jsx4(Text4, { color: error ? theme.error : theme.muted, wrap: "truncate", children: truncateDisplay(detail, innerWidth) }),
15183
- error ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`Enter retry ${separator} Esc exit`, innerWidth) }) : null
15871
+ error ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`Enter retry ${separator} Esc exit`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`local only ${separator} no source code uploaded`, innerWidth) }) : null
15184
15872
  ] });
15185
15873
  }
15874
+ function preparationSteps(phase, progress, readiness, error, glyphs) {
15875
+ const runtimePhase = phase === "error" ? progress.phase : phase;
15876
+ const current = runtimePhase === "scan" || runtimePhase === "index" ? "build" : runtimePhase === "write" ? "persist" : runtimePhase === "validate" ? "verify" : "inspect";
15877
+ const order = ["inspect", "build", "persist", "verify"];
15878
+ const currentIndex = phase === "ready" ? order.length : order.indexOf(current);
15879
+ const activeDetail = runtimePhase === "inspect" ? "manifest and workspace boundaries" : runtimePhase === "scan" ? `${progress.total} source files discovered` : runtimePhase === "index" ? `${progress.completed}/${progress.total} source files` : runtimePhase === "write" ? "atomic local index write" : runtimePhase === "validate" ? progress.total ? `${progress.completed}/${progress.total} content hashes` : "generation and content hashes" : "";
15880
+ const readyDetails = {
15881
+ inspect: "workspace boundaries checked",
15882
+ build: readiness?.rebuilt ? `${readiness.files} files \xB7 ${readiness.chunks} chunks` : "existing index is current",
15883
+ persist: readiness?.rebuilt ? "local artifact committed" : "local artifact loaded",
15884
+ verify: "content and generation matched"
15885
+ };
15886
+ const pendingDetails = {
15887
+ inspect: "manifest and workspace boundaries",
15888
+ build: "source files and code chunks",
15889
+ persist: "atomic local index write",
15890
+ verify: "generation and content hashes"
15891
+ };
15892
+ return order.map((id, index) => {
15893
+ const state = error && index === currentIndex ? "error" : index < currentIndex || phase === "ready" ? "complete" : index === currentIndex ? "active" : "pending";
15894
+ const glyph = state === "complete" ? glyphs.ascii ? "[x]" : "\u25CF" : state === "error" ? glyphs.ascii ? "[!]" : "\xD7" : state === "active" ? glyphs.spinner : glyphs.ascii ? "[ ]" : "\u25CB";
15895
+ return {
15896
+ id,
15897
+ label: id === "persist" ? "Persist" : `${id[0]?.toUpperCase()}${id.slice(1)}`,
15898
+ detail: phase === "ready" ? readyDetails[id] : state === "active" || state === "error" ? activeDetail : pendingDetails[id],
15899
+ state,
15900
+ glyph
15901
+ };
15902
+ });
15903
+ }
15186
15904
  function WorkspacePreparationApp({
15187
15905
  engine,
15188
15906
  config,
@@ -15192,7 +15910,7 @@ function WorkspacePreparationApp({
15192
15910
  onFinish
15193
15911
  }) {
15194
15912
  const { exit } = useApp2();
15195
- const { columns } = useWindowSize2();
15913
+ const { columns, rows } = useWindowSize2();
15196
15914
  const [attempt, setAttempt] = useState3(0);
15197
15915
  const [frame, setFrame] = useState3(0);
15198
15916
  const [progress, setProgress] = useState3({ phase: "inspect", completed: 0, total: 0 });
@@ -15244,6 +15962,7 @@ function WorkspacePreparationApp({
15244
15962
  workspace,
15245
15963
  model: `${config.model.provider}/${config.model.model}`,
15246
15964
  width: Math.max(1, columns || 80),
15965
+ height: Math.max(1, rows || 24),
15247
15966
  frame
15248
15967
  }
15249
15968
  );
@@ -15279,9 +15998,9 @@ async function runWorkspacePreparation(engine, config, options = {}) {
15279
15998
  await instance.waitUntilExit();
15280
15999
  return result ?? { status: "cancelled" };
15281
16000
  }
15282
- function preparationLabel(phase, progress, readiness, compact = false) {
16001
+ function preparationLabel(phase, progress, readiness, compact2 = false) {
15283
16002
  if (phase === "ready") {
15284
- if (compact) return "Index verified";
16003
+ if (compact2) return "Index verified";
15285
16004
  return readiness?.rebuilt ? "Workspace index created and verified" : "Workspace index verified";
15286
16005
  }
15287
16006
  if (phase === "error") return "Workspace preparation failed";
@@ -15685,7 +16404,7 @@ function OnboardingFlow({ initialConfig, saveConfig, onFinish }) {
15685
16404
  }, [finish, saveConfig, state]);
15686
16405
  return /* @__PURE__ */ jsx5(OnboardingScreen, { state, dispatch, width, compact: compactHeight });
15687
16406
  }
15688
- function OnboardingScreen({ state, dispatch, width, compact = false }) {
16407
+ function OnboardingScreen({ state, dispatch, width, compact: compact2 = false }) {
15689
16408
  const theme = useTheme();
15690
16409
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15691
16410
  const marker = ascii ? ">" : "\u203A";
@@ -15702,14 +16421,14 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15702
16421
  ] }),
15703
16422
  /* @__PURE__ */ jsx5(Text6, { color: theme.border, children: truncateDisplay(stageDivider(ascii, headerWidth), headerWidth) }),
15704
16423
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(stage.name, headerWidth) }),
15705
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
16424
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15706
16425
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: truncateDisplay(titleForStep(state.step), headerWidth) }),
15707
- !compact ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
16426
+ !compact2 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
15708
16427
  summary ? /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(summary, headerWidth) }) : null,
15709
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15710
- state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact }) : null,
15711
- state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact }) : null,
15712
- state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact }) : null,
16428
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
16429
+ state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
16430
+ state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
16431
+ state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
15713
16432
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
15714
16433
  /* @__PURE__ */ jsxs5(Box4, { children: [
15715
16434
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -15737,18 +16456,18 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15737
16456
  "! ",
15738
16457
  truncateDisplay(state.error, Math.max(1, headerWidth - 2))
15739
16458
  ] }) : null,
15740
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
16459
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15741
16460
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: footerForStep(state, headerWidth) })
15742
16461
  ] });
15743
16462
  }
15744
- function OptionList({ options, selected, marker, width, compact }) {
16463
+ function OptionList({ options, selected, marker, width, compact: compact2 }) {
15745
16464
  const theme = useTheme();
15746
16465
  return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", children: options.map((option, index) => {
15747
16466
  const active = index === selected;
15748
16467
  const prefix = active ? `${marker} ` : " ";
15749
16468
  const available = Math.max(1, width - displayWidth(prefix) - (active ? 2 : 0));
15750
16469
  const label = `${prefix}${truncateDisplay(option.label, available)}`;
15751
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact || index === options.length - 1 ? 0 : 1, children: [
16470
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact2 || index === options.length - 1 ? 0 : 1, children: [
15752
16471
  /* @__PURE__ */ jsx5(
15753
16472
  Text6,
15754
16473
  {
@@ -15758,7 +16477,7 @@ function OptionList({ options, selected, marker, width, compact }) {
15758
16477
  children: active ? padDisplay(label, width) : label
15759
16478
  }
15760
16479
  ),
15761
- !compact && 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
16480
+ !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
15762
16481
  ] }, option.label);
15763
16482
  }) });
15764
16483
  }
@@ -15881,7 +16600,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
15881
16600
  import stripAnsi4 from "strip-ansi";
15882
16601
 
15883
16602
  // src/mcp/tool.ts
15884
- import { createHash as createHash12 } from "node:crypto";
16603
+ import { createHash as createHash13 } from "node:crypto";
15885
16604
  import stripAnsi3 from "strip-ansi";
15886
16605
  var MAX_ARGUMENT_BYTES = 256e3;
15887
16606
  var MAX_DESCRIPTION_LENGTH = 4e3;
@@ -15889,7 +16608,7 @@ var MAX_RESULT_LENGTH = 8e4;
15889
16608
  var MAX_SCHEMA_BYTES = 1e5;
15890
16609
  function createMcpToolAdapter(options) {
15891
16610
  const { remoteTool } = options;
15892
- const inputSchema10 = copyInputSchema(remoteTool.inputSchema);
16611
+ const inputSchema11 = copyInputSchema(remoteTool.inputSchema);
15893
16612
  return {
15894
16613
  definition: {
15895
16614
  name: options.exposedName,
@@ -15897,7 +16616,7 @@ function createMcpToolAdapter(options) {
15897
16616
  // MCP servers are an external trust boundary. Read-only annotations are
15898
16617
  // hints from that server and must not lower the local permission level.
15899
16618
  category: "network",
15900
- inputSchema: inputSchema10
16619
+ inputSchema: inputSchema11
15901
16620
  },
15902
16621
  permissionCategories: () => ["network"],
15903
16622
  async execute(arguments_, context) {
@@ -16034,7 +16753,7 @@ function fitToolName(name, identity) {
16034
16753
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
16035
16754
  }
16036
16755
  function shortHash(value) {
16037
- return createHash12("sha256").update(value).digest("hex").slice(0, 8);
16756
+ return createHash13("sha256").update(value).digest("hex").slice(0, 8);
16038
16757
  }
16039
16758
  function truncateResult(content) {
16040
16759
  if (content.length <= MAX_RESULT_LENGTH) return content;
@@ -16657,9 +17376,9 @@ async function closeTransportQuietly(transport) {
16657
17376
  }
16658
17377
 
16659
17378
  // src/memory/tools.ts
16660
- import { z as z16 } from "zod";
16661
- var scopeSchema = z16.enum(["user", "workspace", "session", "agent"]);
16662
- var kindSchema = z16.enum(["semantic", "episodic", "procedural"]);
17379
+ import { z as z17 } from "zod";
17380
+ var scopeSchema = z17.enum(["user", "workspace", "session", "agent"]);
17381
+ var kindSchema = z17.enum(["semantic", "episodic", "procedural"]);
16663
17382
  function createMemoryTools(store) {
16664
17383
  return [
16665
17384
  {
@@ -16674,10 +17393,10 @@ function createMemoryTools(store) {
16674
17393
  }, ["query"])
16675
17394
  },
16676
17395
  async execute(arguments_, context) {
16677
- const input2 = z16.object({
16678
- query: z16.string().max(4e3),
17396
+ const input2 = z17.object({
17397
+ query: z17.string().max(4e3),
16679
17398
  scope: scopeSchema.optional(),
16680
- limit: z16.number().int().min(1).max(20).optional()
17399
+ limit: z17.number().int().min(1).max(20).optional()
16681
17400
  }).parse(arguments_);
16682
17401
  const scopes = input2.scope ? [{ scope: input2.scope, scopeKey: scopeKey(input2.scope, context) }] : availableScopes(context);
16683
17402
  const records = store.search(input2.query, { scopes, limit: input2.limit ?? 8 });
@@ -16709,17 +17428,17 @@ ${record.content}`
16709
17428
  }, ["content", "rationale"])
16710
17429
  },
16711
17430
  async execute(arguments_, context) {
16712
- const input2 = z16.object({
16713
- content: z16.string().min(1).max(12e3),
16714
- rationale: z16.string().min(1).max(1e3),
17431
+ const input2 = z17.object({
17432
+ content: z17.string().min(1).max(12e3),
17433
+ rationale: z17.string().min(1).max(1e3),
16715
17434
  scope: scopeSchema.optional(),
16716
17435
  kind: kindSchema.optional(),
16717
- tags: z16.array(z16.string()).max(24).optional(),
16718
- importance: z16.number().min(0).max(1).optional(),
16719
- confidence: z16.number().min(0).max(1).optional(),
16720
- agent: z16.string().max(64).optional(),
16721
- revision: z16.string().max(240).optional(),
16722
- conflictKey: z16.string().max(240).optional()
17436
+ tags: z17.array(z17.string()).max(24).optional(),
17437
+ importance: z17.number().min(0).max(1).optional(),
17438
+ confidence: z17.number().min(0).max(1).optional(),
17439
+ agent: z17.string().max(64).optional(),
17440
+ revision: z17.string().max(240).optional(),
17441
+ conflictKey: z17.string().max(240).optional()
16723
17442
  }).strict().parse(arguments_);
16724
17443
  const scope = input2.scope ?? "workspace";
16725
17444
  const candidate = store.propose({
@@ -16757,9 +17476,9 @@ ${record.content}`
16757
17476
  }, ["id"])
16758
17477
  },
16759
17478
  async execute(arguments_) {
16760
- const input2 = z16.object({
16761
- id: z16.string().uuid(),
16762
- permanent: z16.boolean().optional()
17479
+ const input2 = z17.object({
17480
+ id: z17.string().uuid(),
17481
+ permanent: z17.boolean().optional()
16763
17482
  }).parse(arguments_);
16764
17483
  const changed = input2.permanent ? store.remove(input2.id) : store.archive(input2.id);
16765
17484
  return {
@@ -16963,7 +17682,7 @@ function escapeAttribute6(value) {
16963
17682
  }
16964
17683
 
16965
17684
  // src/workflows/catalog.ts
16966
- import { z as z17 } from "zod";
17685
+ import { z as z18 } from "zod";
16967
17686
  var builtInWorkflows = [
16968
17687
  {
16969
17688
  name: "implement",
@@ -17054,7 +17773,7 @@ function createWorkflowTool(catalog) {
17054
17773
  }, ["name", "task"])
17055
17774
  },
17056
17775
  async execute(arguments_) {
17057
- const input2 = z17.object({ name: z17.string(), task: z17.string().min(1).max(2e4) }).parse(arguments_);
17776
+ const input2 = z18.object({ name: z18.string(), task: z18.string().min(1).max(2e4) }).parse(arguments_);
17058
17777
  const workflow = catalog.get(input2.name);
17059
17778
  if (!workflow) return { ok: false, content: `Unknown workflow: ${input2.name}` };
17060
17779
  return {