@skein-code/cli 0.3.9 → 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.9",
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
- "First-run context preparation now shows the real inspect, build, persist, and verify stages",
240
- "Fresh sessions add a compact Skein identity and grouped workspace rail without filling the terminal",
241
- "Short and narrow terminals collapse the index handoff while preserving validation and recovery states"
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,120 +6786,91 @@ 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
+ }
6867
+ function compact(value, limit) {
6868
+ return value.trim().replace(/\s+/gu, " ").slice(0, limit);
6866
6869
  }
6867
6870
 
6871
+ // src/agent/completion-gate.ts
6872
+ import { createHash as createHash9 } from "node:crypto";
6873
+
6868
6874
  // src/tools/permissions.ts
6869
6875
  import { createHash as createHash8, createHmac, randomBytes } from "node:crypto";
6870
6876
  var permissionScopeSecret = randomBytes(32);
@@ -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") {
@@ -12069,7 +12686,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
12069
12686
  ] }) : null
12070
12687
  ] });
12071
12688
  }
12072
- function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact = false }) {
12689
+ function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact2 = false }) {
12073
12690
  const theme = useTheme();
12074
12691
  const glyphs = resolveGlyphs(glyphMode);
12075
12692
  const summary = permissionSummary(call);
@@ -12106,7 +12723,7 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
12106
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: [
12107
12724
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12108
12725
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12109
- ] }) : compact ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
12726
+ ] }) : compact2 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
12110
12727
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12111
12728
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12112
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)) })
@@ -12367,7 +12984,7 @@ function MeterBar({ segments, total, width, glyphs }) {
12367
12984
  remainder ? /* @__PURE__ */ jsx(Text, { color: theme.border, children: glyphs.meterEmpty.repeat(remainder) }) : null
12368
12985
  ] });
12369
12986
  }
12370
- 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" }) {
12371
12988
  const theme = useTheme();
12372
12989
  const glyphs = resolveGlyphs(glyphMode);
12373
12990
  if (minimal) {
@@ -12387,10 +13004,10 @@ function ContextInspector({ status, working, summary, width, memory, connections
12387
13004
  { label: "summary", detail: summary ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted` : "not created" },
12388
13005
  { label: "long-term", detail: memory ?? `retrieved by relevance ${glyphs.separator} untrusted context` }
12389
13006
  ];
12390
- if (!compact && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
12391
- if (!compact && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
12392
- if (!compact && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
12393
- 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} `) });
12394
13011
  if (sources2?.length) {
12395
13012
  const pinned = sources2.filter((source) => source.state === "pinned");
12396
13013
  const muted = sources2.filter((source) => source.state === "muted");
@@ -12511,8 +13128,8 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
12511
13128
  function UpdateNotice({ current, latest, command: command2, highlights, width, glyphs }) {
12512
13129
  const theme = useTheme();
12513
13130
  const availableWidth = safeWidth(width);
12514
- const compact = availableWidth < 48;
12515
- const parts = compact ? [
13131
+ const compact2 = availableWidth < 48;
13132
+ const parts = compact2 ? [
12516
13133
  { text: `${glyphs.up} `, color: theme.accent, bold: true },
12517
13134
  { text: `v${current}`, color: theme.dim, bold: false },
12518
13135
  { text: ` ${glyphs.arrow} `, color: theme.muted, bold: false },
@@ -12533,7 +13150,7 @@ function UpdateNotice({ current, latest, command: command2, highlights, width, g
12533
13150
  const bullets = bulletWidth > 0 ? (highlights ?? []).map((line) => truncateDisplay(sanitizeInlineTerminalText(line), bulletWidth)) : [];
12534
13151
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
12535
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)) }),
12536
- 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,
12537
13154
  bullets.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: `${bulletPrefix}${line}` }, index))
12538
13155
  ] });
12539
13156
  }
@@ -13441,9 +14058,9 @@ function tailText(value, width, maxRows) {
13441
14058
  return `${marker}
13442
14059
  ${selected.join("\n")}`;
13443
14060
  }
13444
- function estimateTimelineItemRows(item, { width, compact = false, showToolOutput = false, expandedToolId }) {
14061
+ function estimateTimelineItemRows(item, { width, compact: compact2 = false, showToolOutput = false, expandedToolId }) {
13445
14062
  const rowWidth = Math.max(1, Math.floor(width));
13446
- const gap = compact ? 0 : 1;
14063
+ const gap = compact2 ? 0 : 1;
13447
14064
  if (item.kind === "user") return wrappedRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
13448
14065
  if (item.kind === "assistant") {
13449
14066
  return 1 + richTextRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
@@ -13455,7 +14072,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13455
14072
  const detail = item.errorDetail || item.detail;
13456
14073
  const detailRows = narrow && detail ? 1 : 0;
13457
14074
  const metaRows = item.meta ? 1 : 0;
13458
- 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;
13459
14076
  return 1 + detailRows + metaRows + outputRows;
13460
14077
  }
13461
14078
  if (item.kind === "list") {
@@ -13469,7 +14086,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13469
14086
  if (item.kind === "theme") return 3;
13470
14087
  if (item.kind === "context") {
13471
14088
  const metaRows = rowWidth < 64 ? 2 : 1;
13472
- const spanLimit = compact ? 2 : 3;
14089
+ const spanLimit = compact2 ? 2 : 3;
13473
14090
  const spanCount = Math.min(item.spans?.length ?? 0, spanLimit);
13474
14091
  const moreRow = (item.spans?.length ?? 0) > spanLimit ? 1 : 0;
13475
14092
  const degradationRows = item.degradation ? metaRows : 0;
@@ -13567,6 +14184,21 @@ function finalizeAssistant(items, id, content) {
13567
14184
  function endStreamingAssistants(items) {
13568
14185
  return items.map((item) => item.kind === "assistant" && item.streaming ? { ...item, streaming: false } : item);
13569
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
+ }
13570
14202
  function updateAgent(items, event) {
13571
14203
  const found = items.some((item) => item.kind === "agent" && item.id === event.id);
13572
14204
  if (!found) {
@@ -13659,7 +14291,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13659
14291
  const colorEnabled = config.ui.color && !process.env.NO_COLOR;
13660
14292
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
13661
14293
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
13662
- const [compact, setCompact] = useState2(config.ui.compact);
14294
+ const [compact2, setCompact] = useState2(config.ui.compact);
13663
14295
  const [interactionMode, setInteractionMode] = useState2(planMode ? "plan" : askMode ? "ask" : "build");
13664
14296
  const [input2, setInput] = useState2("");
13665
14297
  const [busy, setBusy] = useState2(false);
@@ -13859,6 +14491,11 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13859
14491
  case "tasks":
13860
14492
  setTasks(event.tasks.map((task) => ({ ...task })));
13861
14493
  break;
14494
+ case "contract": {
14495
+ setTimeline((items) => updateContractProgress(items, event.contract, separator));
14496
+ refreshSession();
14497
+ break;
14498
+ }
13862
14499
  case "skill":
13863
14500
  append({ id: nextId(), kind: "skill", name: event.name, description: event.description });
13864
14501
  break;
@@ -14351,7 +14988,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14351
14988
  }
14352
14989
  if (command2 === "density") {
14353
14990
  const normalized = argument.toLocaleLowerCase();
14354
- 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;
14355
14992
  if (next === void 0) throw new Error("Usage: /density [compact|comfortable]");
14356
14993
  setCompact(next);
14357
14994
  await saveUiPreference({ compact: next });
@@ -14444,7 +15081,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14444
15081
  })));
14445
15082
  return true;
14446
15083
  }
14447
- }, [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]);
14448
15085
  const submit = useCallback(async (raw, mode = "normal") => {
14449
15086
  const trimmed = raw.trim();
14450
15087
  if (!trimmed) return;
@@ -14808,7 +15445,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14808
15445
  const tokenTotal = session.usage.inputTokens + session.usage.outputTokens;
14809
15446
  const contextStatus = runner.getContextStatus();
14810
15447
  const frame = spinnerFrames()[frameIndex % spinnerFrames().length];
14811
- const compactUi = compact || terminalHeight < 28;
15448
+ const compactUi = compact2 || terminalHeight < 28;
14812
15449
  const constrainedHeight = terminalHeight < 18;
14813
15450
  const compactComposer = terminalHeight < 18;
14814
15451
  const minimalInspector = terminalHeight < 22;
@@ -15014,6 +15651,16 @@ function initialTimeline(session, banner, setupProblem) {
15014
15651
  if (!items.length) {
15015
15652
  items.push({ id: nextId(), kind: "banner", model: banner.model, engine: banner.engine, workspace: banner.workspace, version: banner.version });
15016
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
+ }
15017
15664
  if (setupProblem && items.length <= 1) items.push({ id: nextId(), kind: "notice", tone: "error", text: setupProblem });
15018
15665
  return items;
15019
15666
  }
@@ -15105,17 +15752,17 @@ function composerAttachments(value) {
15105
15752
  const paths = [...value.matchAll(/(?:^|\s)@([^\s]+)/g)].map((match) => match[1]).filter((path) => Boolean(path));
15106
15753
  return [...new Set(paths)].slice(-3);
15107
15754
  }
15108
- function permissionRows(width, hasCwd, compact) {
15755
+ function permissionRows(width, hasCwd, compact2) {
15109
15756
  const content = 3 + (hasCwd ? 1 : 0);
15110
15757
  if (width >= 64) return content + 2;
15111
15758
  if (width >= 28) return content + 3;
15112
- if (compact) return content + 3;
15759
+ if (compact2) return content + 3;
15113
15760
  return content + 5;
15114
15761
  }
15115
- function contextInspectorRows(session, compact, width, minimal) {
15762
+ function contextInspectorRows(session, compact2, width, minimal) {
15116
15763
  if (minimal) return 2;
15117
15764
  const working = session.workingMemory;
15118
- 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));
15119
15766
  return 2 + entries * (width < 52 ? 2 : 1);
15120
15767
  }
15121
15768
  function contextInspectorStatus(status) {
@@ -15167,14 +15814,14 @@ function WorkspacePreparationView({
15167
15814
  const theme = useTheme();
15168
15815
  const safeWidth2 = Math.max(1, Math.floor(width));
15169
15816
  const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
15170
- const compact = safeWidth2 < 48;
15817
+ const compact2 = safeWidth2 < 48;
15171
15818
  const constrained = height < 14;
15172
15819
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15173
15820
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
15174
15821
  const separator = ascii ? "|" : "\xB7";
15175
15822
  const brand = ascii ? "*" : PRODUCT_MARK;
15176
15823
  const phase = readiness ? "ready" : error ? "error" : progress.phase;
15177
- const phaseLabel = preparationLabel(phase, progress, readiness, compact);
15824
+ const phaseLabel = preparationLabel(phase, progress, readiness, compact2);
15178
15825
  const detail = preparationDetail(phase, progress, readiness, error);
15179
15826
  const modelLine = `model ${sanitizeTerminalText(model)}`;
15180
15827
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
@@ -15186,14 +15833,14 @@ function WorkspacePreparationView({
15186
15833
  " ",
15187
15834
  PRODUCT_NAME.toUpperCase()
15188
15835
  ] }),
15189
- !compact && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
15836
+ !compact2 && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
15190
15837
  " ",
15191
15838
  separator,
15192
15839
  " LOCAL CONTEXT"
15193
15840
  ] }) : null
15194
15841
  ] }),
15195
15842
  !constrained ? /* @__PURE__ */ jsx4(Text4, { color: theme.muted, children: truncateDisplay("Ground the workspace before the first request.", innerWidth) }) : null,
15196
- !constrained && !compact ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
15843
+ !constrained && !compact2 ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
15197
15844
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
15198
15845
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
15199
15846
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
@@ -15207,10 +15854,10 @@ function WorkspacePreparationView({
15207
15854
  step2.glyph,
15208
15855
  " "
15209
15856
  ] }),
15210
- /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact ? 8 : 10) }),
15857
+ /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact2 ? 8 : 10) }),
15211
15858
  /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.muted : theme.dim, children: [
15212
15859
  " ",
15213
- truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact ? 12 : 14)))
15860
+ truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact2 ? 12 : 14)))
15214
15861
  ] })
15215
15862
  ] }, step2.id)) }),
15216
15863
  /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
@@ -15351,9 +15998,9 @@ async function runWorkspacePreparation(engine, config, options = {}) {
15351
15998
  await instance.waitUntilExit();
15352
15999
  return result ?? { status: "cancelled" };
15353
16000
  }
15354
- function preparationLabel(phase, progress, readiness, compact = false) {
16001
+ function preparationLabel(phase, progress, readiness, compact2 = false) {
15355
16002
  if (phase === "ready") {
15356
- if (compact) return "Index verified";
16003
+ if (compact2) return "Index verified";
15357
16004
  return readiness?.rebuilt ? "Workspace index created and verified" : "Workspace index verified";
15358
16005
  }
15359
16006
  if (phase === "error") return "Workspace preparation failed";
@@ -15757,7 +16404,7 @@ function OnboardingFlow({ initialConfig, saveConfig, onFinish }) {
15757
16404
  }, [finish, saveConfig, state]);
15758
16405
  return /* @__PURE__ */ jsx5(OnboardingScreen, { state, dispatch, width, compact: compactHeight });
15759
16406
  }
15760
- function OnboardingScreen({ state, dispatch, width, compact = false }) {
16407
+ function OnboardingScreen({ state, dispatch, width, compact: compact2 = false }) {
15761
16408
  const theme = useTheme();
15762
16409
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15763
16410
  const marker = ascii ? ">" : "\u203A";
@@ -15774,14 +16421,14 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15774
16421
  ] }),
15775
16422
  /* @__PURE__ */ jsx5(Text6, { color: theme.border, children: truncateDisplay(stageDivider(ascii, headerWidth), headerWidth) }),
15776
16423
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(stage.name, headerWidth) }),
15777
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
16424
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15778
16425
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: truncateDisplay(titleForStep(state.step), headerWidth) }),
15779
- !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,
15780
16427
  summary ? /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(summary, headerWidth) }) : null,
15781
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15782
- state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact }) : null,
15783
- state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact }) : null,
15784
- 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,
15785
16432
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
15786
16433
  /* @__PURE__ */ jsxs5(Box4, { children: [
15787
16434
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -15809,18 +16456,18 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15809
16456
  "! ",
15810
16457
  truncateDisplay(state.error, Math.max(1, headerWidth - 2))
15811
16458
  ] }) : null,
15812
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
16459
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15813
16460
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: footerForStep(state, headerWidth) })
15814
16461
  ] });
15815
16462
  }
15816
- function OptionList({ options, selected, marker, width, compact }) {
16463
+ function OptionList({ options, selected, marker, width, compact: compact2 }) {
15817
16464
  const theme = useTheme();
15818
16465
  return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", children: options.map((option, index) => {
15819
16466
  const active = index === selected;
15820
16467
  const prefix = active ? `${marker} ` : " ";
15821
16468
  const available = Math.max(1, width - displayWidth(prefix) - (active ? 2 : 0));
15822
16469
  const label = `${prefix}${truncateDisplay(option.label, available)}`;
15823
- 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: [
15824
16471
  /* @__PURE__ */ jsx5(
15825
16472
  Text6,
15826
16473
  {
@@ -15830,7 +16477,7 @@ function OptionList({ options, selected, marker, width, compact }) {
15830
16477
  children: active ? padDisplay(label, width) : label
15831
16478
  }
15832
16479
  ),
15833
- !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
15834
16481
  ] }, option.label);
15835
16482
  }) });
15836
16483
  }
@@ -15953,7 +16600,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
15953
16600
  import stripAnsi4 from "strip-ansi";
15954
16601
 
15955
16602
  // src/mcp/tool.ts
15956
- import { createHash as createHash12 } from "node:crypto";
16603
+ import { createHash as createHash13 } from "node:crypto";
15957
16604
  import stripAnsi3 from "strip-ansi";
15958
16605
  var MAX_ARGUMENT_BYTES = 256e3;
15959
16606
  var MAX_DESCRIPTION_LENGTH = 4e3;
@@ -15961,7 +16608,7 @@ var MAX_RESULT_LENGTH = 8e4;
15961
16608
  var MAX_SCHEMA_BYTES = 1e5;
15962
16609
  function createMcpToolAdapter(options) {
15963
16610
  const { remoteTool } = options;
15964
- const inputSchema10 = copyInputSchema(remoteTool.inputSchema);
16611
+ const inputSchema11 = copyInputSchema(remoteTool.inputSchema);
15965
16612
  return {
15966
16613
  definition: {
15967
16614
  name: options.exposedName,
@@ -15969,7 +16616,7 @@ function createMcpToolAdapter(options) {
15969
16616
  // MCP servers are an external trust boundary. Read-only annotations are
15970
16617
  // hints from that server and must not lower the local permission level.
15971
16618
  category: "network",
15972
- inputSchema: inputSchema10
16619
+ inputSchema: inputSchema11
15973
16620
  },
15974
16621
  permissionCategories: () => ["network"],
15975
16622
  async execute(arguments_, context) {
@@ -16106,7 +16753,7 @@ function fitToolName(name, identity) {
16106
16753
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
16107
16754
  }
16108
16755
  function shortHash(value) {
16109
- return createHash12("sha256").update(value).digest("hex").slice(0, 8);
16756
+ return createHash13("sha256").update(value).digest("hex").slice(0, 8);
16110
16757
  }
16111
16758
  function truncateResult(content) {
16112
16759
  if (content.length <= MAX_RESULT_LENGTH) return content;
@@ -16729,9 +17376,9 @@ async function closeTransportQuietly(transport) {
16729
17376
  }
16730
17377
 
16731
17378
  // src/memory/tools.ts
16732
- import { z as z16 } from "zod";
16733
- var scopeSchema = z16.enum(["user", "workspace", "session", "agent"]);
16734
- 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"]);
16735
17382
  function createMemoryTools(store) {
16736
17383
  return [
16737
17384
  {
@@ -16746,10 +17393,10 @@ function createMemoryTools(store) {
16746
17393
  }, ["query"])
16747
17394
  },
16748
17395
  async execute(arguments_, context) {
16749
- const input2 = z16.object({
16750
- query: z16.string().max(4e3),
17396
+ const input2 = z17.object({
17397
+ query: z17.string().max(4e3),
16751
17398
  scope: scopeSchema.optional(),
16752
- limit: z16.number().int().min(1).max(20).optional()
17399
+ limit: z17.number().int().min(1).max(20).optional()
16753
17400
  }).parse(arguments_);
16754
17401
  const scopes = input2.scope ? [{ scope: input2.scope, scopeKey: scopeKey(input2.scope, context) }] : availableScopes(context);
16755
17402
  const records = store.search(input2.query, { scopes, limit: input2.limit ?? 8 });
@@ -16781,17 +17428,17 @@ ${record.content}`
16781
17428
  }, ["content", "rationale"])
16782
17429
  },
16783
17430
  async execute(arguments_, context) {
16784
- const input2 = z16.object({
16785
- content: z16.string().min(1).max(12e3),
16786
- 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),
16787
17434
  scope: scopeSchema.optional(),
16788
17435
  kind: kindSchema.optional(),
16789
- tags: z16.array(z16.string()).max(24).optional(),
16790
- importance: z16.number().min(0).max(1).optional(),
16791
- confidence: z16.number().min(0).max(1).optional(),
16792
- agent: z16.string().max(64).optional(),
16793
- revision: z16.string().max(240).optional(),
16794
- 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()
16795
17442
  }).strict().parse(arguments_);
16796
17443
  const scope = input2.scope ?? "workspace";
16797
17444
  const candidate = store.propose({
@@ -16829,9 +17476,9 @@ ${record.content}`
16829
17476
  }, ["id"])
16830
17477
  },
16831
17478
  async execute(arguments_) {
16832
- const input2 = z16.object({
16833
- id: z16.string().uuid(),
16834
- permanent: z16.boolean().optional()
17479
+ const input2 = z17.object({
17480
+ id: z17.string().uuid(),
17481
+ permanent: z17.boolean().optional()
16835
17482
  }).parse(arguments_);
16836
17483
  const changed = input2.permanent ? store.remove(input2.id) : store.archive(input2.id);
16837
17484
  return {
@@ -17035,7 +17682,7 @@ function escapeAttribute6(value) {
17035
17682
  }
17036
17683
 
17037
17684
  // src/workflows/catalog.ts
17038
- import { z as z17 } from "zod";
17685
+ import { z as z18 } from "zod";
17039
17686
  var builtInWorkflows = [
17040
17687
  {
17041
17688
  name: "implement",
@@ -17126,7 +17773,7 @@ function createWorkflowTool(catalog) {
17126
17773
  }, ["name", "task"])
17127
17774
  },
17128
17775
  async execute(arguments_) {
17129
- 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_);
17130
17777
  const workflow = catalog.get(input2.name);
17131
17778
  if (!workflow) return { ok: false, content: `Unknown workflow: ${input2.name}` };
17132
17779
  return {