@skein-code/cli 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { createInterface as createInterface2 } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { writeFile as writeFile3 } from "node:fs/promises";
7
- import { basename as basename12, resolve as resolve22 } from "node:path";
7
+ import { basename as basename12, resolve as resolve23 } from "node:path";
8
8
  import { Command, Option } from "commander";
9
9
  import chalk4 from "chalk";
10
10
 
@@ -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.11",
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
+ "Large tool results now use a language-aware dynamic token budget with status-preserving head and tail receipts",
240
+ "Complete captured output can be read back through redacted, expiring, session-scoped local artifacts",
241
+ "Shell and MCP source truncation is explicit, and session deletion removes retained output"
242
242
  ]
243
243
  },
244
244
  bin: {
@@ -990,16 +990,16 @@ async function hashRegularFile(path) {
990
990
  try {
991
991
  const info = await handle.stat();
992
992
  if (!info.isFile()) throw new Error(`Namespace entry is not a regular file: ${path}`);
993
- const hash = createHash2("sha256");
993
+ const hash2 = createHash2("sha256");
994
994
  const buffer = Buffer.allocUnsafe(64 * 1024);
995
995
  let position = 0;
996
996
  while (true) {
997
997
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
998
998
  if (!bytesRead) break;
999
- hash.update(buffer.subarray(0, bytesRead));
999
+ hash2.update(buffer.subarray(0, bytesRead));
1000
1000
  position += bytesRead;
1001
1001
  }
1002
- return { sha256: hash.digest("hex"), size: position };
1002
+ return { sha256: hash2.digest("hex"), size: position };
1003
1003
  } finally {
1004
1004
  await handle.close();
1005
1005
  }
@@ -1200,18 +1200,18 @@ var MemoryStore = class {
1200
1200
  input2.lastVerifiedAt ?? (explicit ? now : void 0),
1201
1201
  "Memory verification time"
1202
1202
  );
1203
- const hash = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1203
+ const hash2 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1204
1204
  database.exec("BEGIN IMMEDIATE");
1205
1205
  try {
1206
1206
  const existing = database.prepare(
1207
1207
  "SELECT * FROM memories WHERE content_hash = ?"
1208
- ).get(hash);
1208
+ ).get(hash2);
1209
1209
  const conflicting = conflictKey ? database.prepare(`
1210
1210
  SELECT * FROM memories
1211
1211
  WHERE scope = ? AND scope_key = ? AND conflict_key = ?
1212
1212
  AND status = 'active' AND content_hash <> ?
1213
1213
  ORDER BY updated_at DESC
1214
- `).all(input2.scope, scopeKey2, conflictKey, hash) : [];
1214
+ `).all(input2.scope, scopeKey2, conflictKey, hash2) : [];
1215
1215
  const supersedesId = normalizeOptional(input2.supersedesId, 80) ?? conflicting[0]?.id;
1216
1216
  let id;
1217
1217
  if (existing) {
@@ -1256,7 +1256,7 @@ var MemoryStore = class {
1256
1256
  importance,
1257
1257
  confidence,
1258
1258
  source,
1259
- hash,
1259
+ hash2,
1260
1260
  now,
1261
1261
  now,
1262
1262
  now,
@@ -1359,10 +1359,10 @@ var MemoryStore = class {
1359
1359
  const conflictKey = normalizeOptional(input2.conflictKey, 240);
1360
1360
  const candidateDefaultExpiry = !input2.expiresAt ? new Date(Date.now() + MEMORY_CANDIDATE_TTL_MS).toISOString() : void 0;
1361
1361
  const expiresAt = normalizeTimestamp(input2.expiresAt ?? candidateDefaultExpiry, "Memory expiration");
1362
- const hash = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1362
+ const hash2 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1363
1363
  const existing = database.prepare(
1364
1364
  "SELECT * FROM memory_candidates WHERE content_hash = ?"
1365
- ).get(hash);
1365
+ ).get(hash2);
1366
1366
  const now = (/* @__PURE__ */ new Date()).toISOString();
1367
1367
  if (existing) {
1368
1368
  if (existing.status === "approved" && existing.approved_memory_id) {
@@ -1409,7 +1409,7 @@ var MemoryStore = class {
1409
1409
  confidence,
1410
1410
  source,
1411
1411
  rationale,
1412
- hash,
1412
+ hash2,
1413
1413
  now,
1414
1414
  now,
1415
1415
  revision ?? null,
@@ -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;
@@ -4755,7 +4755,7 @@ function executableNames(command2) {
4755
4755
  return [command2, ...extensions.map((extension) => `${command2}${extension}`)];
4756
4756
  }
4757
4757
  function runProcess(command2, args, options) {
4758
- return new Promise((resolve23, reject) => {
4758
+ return new Promise((resolve24, reject) => {
4759
4759
  const started = Date.now();
4760
4760
  const environment = options.inheritEnv === false ? {} : { ...process.env };
4761
4761
  for (const name of options.unsetEnv ?? []) delete environment[name];
@@ -4776,6 +4776,8 @@ function runProcess(command2, args, options) {
4776
4776
  let stderr = "";
4777
4777
  let stdoutBytes = 0;
4778
4778
  let stderrBytes = 0;
4779
+ let stdoutObservedBytes = 0;
4780
+ let stderrObservedBytes = 0;
4779
4781
  let timedOut = false;
4780
4782
  let callbackError;
4781
4783
  const stdoutDecoder = new StringDecoder("utf8");
@@ -4798,12 +4800,14 @@ function runProcess(command2, args, options) {
4798
4800
  }
4799
4801
  };
4800
4802
  child.stdout.on("data", (chunk) => {
4803
+ stdoutObservedBytes += chunk.length;
4801
4804
  const appended = append(stdoutDecoder, chunk, stdoutBytes);
4802
4805
  stdout += appended.text;
4803
4806
  stdoutBytes = appended.usedBytes;
4804
4807
  notify(options.onStdout, stdoutCallbackDecoder, chunk);
4805
4808
  });
4806
4809
  child.stderr.on("data", (chunk) => {
4810
+ stderrObservedBytes += chunk.length;
4807
4811
  const appended = append(stderrDecoder, chunk, stderrBytes);
4808
4812
  stderr += appended.text;
4809
4813
  stderrBytes = appended.usedBytes;
@@ -4834,13 +4838,17 @@ function runProcess(command2, args, options) {
4834
4838
  reject(callbackError);
4835
4839
  return;
4836
4840
  }
4837
- resolve23({
4841
+ resolve24({
4838
4842
  command: [command2, ...args].join(" "),
4839
4843
  exitCode: code ?? (timedOut ? 124 : 1),
4840
4844
  stdout,
4841
4845
  stderr,
4842
4846
  timedOut,
4843
- durationMs: Date.now() - started
4847
+ durationMs: Date.now() - started,
4848
+ stdoutBytes: stdoutObservedBytes,
4849
+ stderrBytes: stderrObservedBytes,
4850
+ stdoutTruncated: stdoutObservedBytes > stdoutBytes,
4851
+ stderrTruncated: stderrObservedBytes > stderrBytes
4844
4852
  });
4845
4853
  });
4846
4854
  if (options.stdin) child.stdin.end(options.stdin);
@@ -4961,6 +4969,14 @@ var contextSourceSchema = z5.object({
4961
4969
  tokens: z5.number().int().nonnegative(),
4962
4970
  addedAt: z5.string()
4963
4971
  }).strict();
4972
+ var toolArtifactSchema = z5.object({
4973
+ toolCallId: z5.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f-\u009f]/u.test(value)),
4974
+ sha256: z5.string().regex(/^[a-f0-9]{64}$/u),
4975
+ bytes: z5.number().int().nonnegative().max(5 * 1024 * 1024),
4976
+ createdAt: z5.string().datetime(),
4977
+ expiresAt: z5.string().datetime(),
4978
+ redacted: z5.boolean()
4979
+ }).strict();
4964
4980
  var verificationEvidenceSchema = z5.object({
4965
4981
  toolCallId: z5.string(),
4966
4982
  tool: z5.enum(["shell", "git"]),
@@ -4974,6 +4990,19 @@ var lastRunSchema = z5.object({
4974
4990
  checks: z5.array(verificationEvidenceSchema),
4975
4991
  detail: z5.string(),
4976
4992
  mutationTracking: z5.enum(["complete", "unknown"]).optional(),
4993
+ acceptance: z5.object({
4994
+ state: z5.enum(["draft", "active", "satisfied", "blocked"]),
4995
+ total: z5.number().int().nonnegative(),
4996
+ satisfied: z5.number().int().nonnegative(),
4997
+ pending: z5.number().int().nonnegative(),
4998
+ blocked: z5.number().int().nonnegative(),
4999
+ missingVerification: z5.array(z5.string()),
5000
+ unresolved: z5.array(z5.object({
5001
+ id: z5.string(),
5002
+ description: z5.string(),
5003
+ status: z5.enum(["pending", "satisfied", "blocked"])
5004
+ }).strict())
5005
+ }).strict().optional(),
4977
5006
  reason: z5.string(),
4978
5007
  finishedAt: z5.string()
4979
5008
  }).strict();
@@ -4986,6 +5015,27 @@ var workingMemorySchema = z5.object({
4986
5015
  relevantFiles: z5.array(z5.string()),
4987
5016
  lastUpdatedAt: z5.string()
4988
5017
  }).strict();
5018
+ var taskContractCriterionSchema = z5.object({
5019
+ id: z5.string().min(1).max(128),
5020
+ description: z5.string().min(1).max(2e3),
5021
+ required: z5.boolean(),
5022
+ status: z5.enum(["pending", "satisfied", "blocked"]),
5023
+ evidenceRefs: z5.array(z5.string().min(1).max(256)).max(64),
5024
+ note: z5.string().max(2e3).optional()
5025
+ }).strict();
5026
+ var taskContractSchema = z5.object({
5027
+ version: z5.literal(1),
5028
+ state: z5.enum(["draft", "active", "satisfied", "blocked"]),
5029
+ objective: z5.string().min(1).max(2e4),
5030
+ scope: z5.array(z5.string().min(1).max(2e3)).max(64),
5031
+ constraints: z5.array(z5.string().min(1).max(2e3)).max(64),
5032
+ nonGoals: z5.array(z5.string().min(1).max(2e3)).max(64),
5033
+ acceptanceCriteria: z5.array(taskContractCriterionSchema).min(1).max(64),
5034
+ verificationRequirements: z5.array(z5.string().min(1).max(2e3)).max(64),
5035
+ createdAt: z5.string(),
5036
+ updatedAt: z5.string(),
5037
+ auditBoundaryId: z5.string().min(1).max(128).optional()
5038
+ }).strict();
4989
5039
  var sessionSchema = z5.object({
4990
5040
  id: sessionIdSchema,
4991
5041
  title: z5.string(),
@@ -5003,6 +5053,8 @@ var sessionSchema = z5.object({
5003
5053
  compactedThroughMessageId: z5.string().optional(),
5004
5054
  workingMemory: workingMemorySchema.optional(),
5005
5055
  contextSources: z5.array(contextSourceSchema).max(64).optional(),
5056
+ toolArtifacts: z5.array(toolArtifactSchema).max(200).optional(),
5057
+ taskContract: taskContractSchema.optional(),
5006
5058
  lastRun: lastRunSchema.optional(),
5007
5059
  usage: z5.object({
5008
5060
  inputTokens: z5.number().nonnegative(),
@@ -5263,13 +5315,390 @@ async function exists2(path) {
5263
5315
  }
5264
5316
  }
5265
5317
 
5318
+ // src/session/tool-artifacts.ts
5319
+ import { createHash as createHash7 } from "node:crypto";
5320
+ import { lstat as lstat12, readFile as readFile7, readdir as readdir4, rm as rm2 } from "node:fs/promises";
5321
+ import { join as join11, resolve as resolve13 } from "node:path";
5322
+ import { z as z6 } from "zod";
5323
+ var MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
5324
+ var MAX_TOTAL_BYTES = 20 * 1024 * 1024;
5325
+ var RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
5326
+ var sessionIdSchema2 = z6.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/);
5327
+ var toolCallIdSchema = z6.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f-\u009f]/u.test(value), {
5328
+ message: "Tool call id cannot contain control characters."
5329
+ });
5330
+ var artifactSchema = z6.object({
5331
+ version: z6.literal(1),
5332
+ sessionId: sessionIdSchema2,
5333
+ toolCallId: toolCallIdSchema,
5334
+ sha256: z6.string().regex(/^[a-f0-9]{64}$/u),
5335
+ bytes: z6.number().int().nonnegative().max(MAX_ARTIFACT_BYTES),
5336
+ createdAt: z6.string().datetime(),
5337
+ expiresAt: z6.string().datetime(),
5338
+ redacted: z6.boolean(),
5339
+ content: z6.string()
5340
+ }).strict();
5341
+ var ToolArtifactStore = class {
5342
+ workspace;
5343
+ directory;
5344
+ managedDirectory;
5345
+ now;
5346
+ maxArtifactBytes;
5347
+ maxTotalBytes;
5348
+ retentionMs;
5349
+ writes = Promise.resolve();
5350
+ constructor(workspace, options = {}) {
5351
+ this.workspace = resolve13(workspace);
5352
+ this.managedDirectory = options.directory === void 0;
5353
+ this.directory = options.directory ? resolve13(options.directory) : join11(resolveProjectNamespaceSync(this.workspace).active, "tool-artifacts");
5354
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
5355
+ this.maxArtifactBytes = options.maxArtifactBytes ?? MAX_ARTIFACT_BYTES;
5356
+ this.maxTotalBytes = options.maxTotalBytes ?? MAX_TOTAL_BYTES;
5357
+ this.retentionMs = options.retentionMs ?? RETENTION_MS;
5358
+ if (!Number.isSafeInteger(this.maxArtifactBytes) || this.maxArtifactBytes < 1 || this.maxArtifactBytes > MAX_ARTIFACT_BYTES) {
5359
+ throw new Error(`maxArtifactBytes must be between 1 and ${MAX_ARTIFACT_BYTES}.`);
5360
+ }
5361
+ if (!Number.isSafeInteger(this.maxTotalBytes) || this.maxTotalBytes < this.maxArtifactBytes) {
5362
+ throw new Error("maxTotalBytes must be at least maxArtifactBytes.");
5363
+ }
5364
+ if (!Number.isSafeInteger(this.retentionMs) || this.retentionMs < 1) {
5365
+ throw new Error("retentionMs must be a positive integer.");
5366
+ }
5367
+ }
5368
+ async archive(sessionId, toolCallId, content, options) {
5369
+ sessionIdSchema2.parse(sessionId);
5370
+ toolCallIdSchema.parse(toolCallId);
5371
+ const bytes = Buffer.byteLength(content);
5372
+ if (bytes > this.maxArtifactBytes) return { stored: false, reason: "too_large" };
5373
+ return this.enqueue(() => this.withManagedLease(async () => {
5374
+ await this.ensureDirectory();
5375
+ const now = this.now();
5376
+ const artifacts = await this.loadArtifacts();
5377
+ await this.removeExpired(artifacts, now);
5378
+ const active = artifacts.filter((artifact2) => artifact2.expiresAt > now.toISOString());
5379
+ const sha256 = hash(content);
5380
+ const existing = active.find((artifact2) => artifact2.sessionId === sessionId && artifact2.toolCallId === toolCallId);
5381
+ if (existing) {
5382
+ if (existing.sha256 === sha256 && existing.bytes === bytes && existing.redacted === options.redacted) {
5383
+ return { stored: true, artifact: reference(existing) };
5384
+ }
5385
+ return { stored: false, reason: "conflict" };
5386
+ }
5387
+ const artifact = {
5388
+ version: 1,
5389
+ sessionId,
5390
+ toolCallId,
5391
+ sha256,
5392
+ bytes,
5393
+ createdAt: now.toISOString(),
5394
+ expiresAt: new Date(now.getTime() + this.retentionMs).toISOString(),
5395
+ redacted: options.redacted,
5396
+ content
5397
+ };
5398
+ const storageBytes = storedBytes(artifact);
5399
+ let retainedBytes = active.reduce((total, retained) => total + storedBytes(retained), 0);
5400
+ const evictable = active.slice().sort(
5401
+ (left, right) => left.createdAt.localeCompare(right.createdAt)
5402
+ );
5403
+ while (retainedBytes + storageBytes > this.maxTotalBytes && evictable.length) {
5404
+ const oldest = evictable.shift();
5405
+ await this.removeArtifact(oldest);
5406
+ retainedBytes -= storedBytes(oldest);
5407
+ }
5408
+ if (retainedBytes + storageBytes > this.maxTotalBytes) return { stored: false, reason: "total_limit" };
5409
+ const path = this.pathFor(sessionId, toolCallId);
5410
+ await ensureWorkspaceStorageDirectory(this.workspace, resolve13(path, ".."), {
5411
+ requireActiveNamespace: this.managedDirectory
5412
+ });
5413
+ await atomicWrite(path, `${JSON.stringify(artifact)}
5414
+ `, 384);
5415
+ return { stored: true, artifact: reference(artifact) };
5416
+ }));
5417
+ }
5418
+ async read(sessionId, toolCallId, options = {}) {
5419
+ sessionIdSchema2.parse(sessionId);
5420
+ toolCallIdSchema.parse(toolCallId);
5421
+ const startLine = options.startLine ?? 1;
5422
+ const startByte = options.startByte;
5423
+ const maxLines = options.maxLines ?? 200;
5424
+ const maxBytes = options.maxBytes ?? 3e3;
5425
+ if (startByte !== void 0 && options.startLine !== void 0) {
5426
+ throw new Error("Use either startLine or startByte, not both.");
5427
+ }
5428
+ if (!Number.isInteger(startLine) || startLine < 1) throw new Error("startLine must be a positive integer.");
5429
+ if (startByte !== void 0 && (!Number.isInteger(startByte) || startByte < 0)) {
5430
+ throw new Error("startByte must be a non-negative integer.");
5431
+ }
5432
+ if (!Number.isInteger(maxLines) || maxLines < 1 || maxLines > 1e3) {
5433
+ throw new Error("maxLines must be between 1 and 1000.");
5434
+ }
5435
+ if (!Number.isInteger(maxBytes) || maxBytes < 256 || maxBytes > 32e3) {
5436
+ throw new Error("maxBytes must be between 256 and 32000.");
5437
+ }
5438
+ await this.writes;
5439
+ return this.withManagedLease(async () => {
5440
+ const artifact = await this.readArtifact(sessionId, toolCallId);
5441
+ const now = this.now();
5442
+ if (artifact.expiresAt <= now.toISOString()) {
5443
+ await this.removeArtifact(artifact);
5444
+ throw new Error("The retained tool output has expired. Re-run the tool if it is still needed.");
5445
+ }
5446
+ const page = startByte === void 0 ? readLinePage(artifact.content, startLine, maxLines, maxBytes) : readBytePage(artifact.content, startByte, maxBytes);
5447
+ return {
5448
+ ...reference(artifact),
5449
+ ...page
5450
+ };
5451
+ });
5452
+ }
5453
+ /** Remove expired output and return the still-valid receipts for one session. */
5454
+ async prune(sessionId) {
5455
+ sessionIdSchema2.parse(sessionId);
5456
+ return this.enqueue(() => this.withManagedLease(async () => {
5457
+ const artifacts = await this.loadArtifacts();
5458
+ const now = this.now();
5459
+ await this.removeExpired(artifacts, now);
5460
+ return artifacts.filter((artifact) => artifact.sessionId === sessionId && artifact.expiresAt > now.toISOString()).sort((left, right) => left.createdAt.localeCompare(right.createdAt)).map(reference);
5461
+ }));
5462
+ }
5463
+ /** Delete every retained result owned by a removed session. */
5464
+ async removeSession(sessionId) {
5465
+ sessionIdSchema2.parse(sessionId);
5466
+ await this.enqueue(() => this.withManagedLease(async () => {
5467
+ if (!await this.directoryAvailable()) return;
5468
+ const directory = this.sessionDirectoryFor(sessionId);
5469
+ try {
5470
+ const info = await lstat12(directory);
5471
+ if (info.isSymbolicLink() || !info.isDirectory()) {
5472
+ throw new Error(`Tool artifact session storage is not a regular directory: ${directory}`);
5473
+ }
5474
+ await assertNoSymlinkPath(this.workspace, directory);
5475
+ } catch (error) {
5476
+ if (error.code === "ENOENT") return;
5477
+ throw error;
5478
+ }
5479
+ await rm2(directory, { recursive: true, force: true });
5480
+ }));
5481
+ }
5482
+ async readArtifact(sessionId, toolCallId) {
5483
+ const path = this.pathFor(sessionId, toolCallId);
5484
+ await this.assertRegularFile(path);
5485
+ let artifact;
5486
+ try {
5487
+ artifact = artifactSchema.parse(JSON.parse(await readFile7(path, "utf8")));
5488
+ } catch {
5489
+ throw new Error("Retained tool output is unreadable or corrupt.");
5490
+ }
5491
+ if (artifact.sessionId !== sessionId || artifact.toolCallId !== toolCallId) {
5492
+ throw new Error("Retained tool output does not belong to this session.");
5493
+ }
5494
+ if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash(artifact.content)) {
5495
+ throw new Error("Retained tool output failed its integrity check.");
5496
+ }
5497
+ return artifact;
5498
+ }
5499
+ async loadArtifacts() {
5500
+ if (!await this.directoryAvailable()) return [];
5501
+ const artifacts = [];
5502
+ for (const entry of await readdir4(this.directory, { withFileTypes: true })) {
5503
+ const sessionDirectory = join11(this.directory, entry.name);
5504
+ if (entry.isSymbolicLink()) throw new Error(`Tool artifact storage contains a symbolic link: ${sessionDirectory}`);
5505
+ if (!entry.isDirectory() || !/^[a-f0-9]{64}$/u.test(entry.name)) continue;
5506
+ await assertNoSymlinkPath(this.workspace, sessionDirectory);
5507
+ for (const file of await readdir4(sessionDirectory, { withFileTypes: true })) {
5508
+ const path = join11(sessionDirectory, file.name);
5509
+ if (file.isSymbolicLink()) throw new Error(`Tool artifact storage contains a symbolic link: ${path}`);
5510
+ if (!file.isFile() || !/^[a-f0-9]{64}\.json$/u.test(file.name)) continue;
5511
+ await this.assertRegularFile(path);
5512
+ try {
5513
+ const artifact = artifactSchema.parse(JSON.parse(await readFile7(path, "utf8")));
5514
+ if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash(artifact.content)) {
5515
+ throw new Error("integrity");
5516
+ }
5517
+ if (sessionDirectory !== this.sessionDirectoryFor(artifact.sessionId) || path !== this.pathFor(artifact.sessionId, artifact.toolCallId)) {
5518
+ throw new Error("identity");
5519
+ }
5520
+ artifacts.push(artifact);
5521
+ } catch {
5522
+ throw new Error(`Tool artifact is unreadable or corrupt: ${path}`);
5523
+ }
5524
+ }
5525
+ }
5526
+ return artifacts;
5527
+ }
5528
+ async removeExpired(artifacts, now) {
5529
+ for (const artifact of artifacts) {
5530
+ if (artifact.expiresAt <= now.toISOString()) await this.removeArtifact(artifact);
5531
+ }
5532
+ }
5533
+ async removeArtifact(artifact) {
5534
+ await rm2(this.pathFor(artifact.sessionId, artifact.toolCallId), { force: true });
5535
+ }
5536
+ pathFor(sessionId, toolCallId) {
5537
+ return join11(this.sessionDirectoryFor(sessionId), `${hash(`call\0${toolCallId}`)}.json`);
5538
+ }
5539
+ sessionDirectoryFor(sessionId) {
5540
+ return join11(this.directory, hash(`session\0${sessionId}`));
5541
+ }
5542
+ async ensureDirectory() {
5543
+ await ensureWorkspaceStorageDirectory(this.workspace, this.directory, {
5544
+ requireActiveNamespace: this.managedDirectory
5545
+ });
5546
+ }
5547
+ async directoryAvailable() {
5548
+ if (this.managedDirectory) await assertNoSymlinkPath(this.workspace, this.directory);
5549
+ try {
5550
+ const info = await lstat12(this.directory);
5551
+ if (info.isSymbolicLink() || !info.isDirectory()) {
5552
+ throw new Error(`Tool artifact storage is not a regular directory: ${this.directory}`);
5553
+ }
5554
+ return true;
5555
+ } catch (error) {
5556
+ if (error.code === "ENOENT") return false;
5557
+ throw error;
5558
+ }
5559
+ }
5560
+ async assertRegularFile(path) {
5561
+ await assertNoSymlinkPath(this.workspace, resolve13(path, ".."));
5562
+ try {
5563
+ const info = await lstat12(path);
5564
+ if (info.isSymbolicLink() || !info.isFile()) {
5565
+ throw new Error(`Tool artifact cannot be a symbolic link or non-regular file: ${path}`);
5566
+ }
5567
+ } catch (error) {
5568
+ if (error.code === "ENOENT") {
5569
+ throw new Error("No retained tool output matches this tool call in the current session.");
5570
+ }
5571
+ throw error;
5572
+ }
5573
+ }
5574
+ async withManagedLease(operation) {
5575
+ if (!this.managedDirectory) return operation();
5576
+ return withNamespaceLease(projectNamespacePaths(this.workspace).canonical, "shared", async () => {
5577
+ assertActiveProjectNamespacePath(this.workspace, this.directory);
5578
+ return operation();
5579
+ });
5580
+ }
5581
+ async enqueue(operation) {
5582
+ const next = this.writes.then(operation);
5583
+ this.writes = next.then(() => void 0, () => void 0);
5584
+ return next;
5585
+ }
5586
+ };
5587
+ function reference(artifact) {
5588
+ return {
5589
+ toolCallId: artifact.toolCallId,
5590
+ sha256: artifact.sha256,
5591
+ bytes: artifact.bytes,
5592
+ createdAt: artifact.createdAt,
5593
+ expiresAt: artifact.expiresAt,
5594
+ redacted: artifact.redacted
5595
+ };
5596
+ }
5597
+ function hash(value) {
5598
+ return createHash7("sha256").update(value).digest("hex");
5599
+ }
5600
+ function storedBytes(artifact) {
5601
+ return Buffer.byteLength(JSON.stringify(artifact)) + 1;
5602
+ }
5603
+ function readLinePage(content, startLine, maxLines, maxBytes) {
5604
+ const ranges = lineRanges(content);
5605
+ const startIndex = startLine - 1;
5606
+ if (startIndex >= ranges.length) {
5607
+ const totalBytes = Buffer.byteLength(content);
5608
+ return {
5609
+ content: "",
5610
+ startLine,
5611
+ endLine: startLine - 1,
5612
+ totalLines: ranges.length,
5613
+ startByte: totalBytes,
5614
+ endByte: totalBytes,
5615
+ hasMore: false
5616
+ };
5617
+ }
5618
+ const endIndex = Math.min(ranges.length - 1, startIndex + maxLines - 1);
5619
+ const startCharacter = ranges[startIndex]?.start ?? content.length;
5620
+ const endCharacter = ranges[endIndex]?.end ?? content.length;
5621
+ const startByte = Buffer.byteLength(content.slice(0, startCharacter));
5622
+ const selected = Buffer.from(content.slice(startCharacter, endCharacter));
5623
+ if (selected.length > maxBytes) {
5624
+ const page = sliceUtf8Buffer(selected, 0, maxBytes);
5625
+ const pageContent = page.content;
5626
+ return {
5627
+ content: pageContent,
5628
+ startLine,
5629
+ endLine: startLine + countNewlines(pageContent),
5630
+ totalLines: ranges.length,
5631
+ startByte,
5632
+ endByte: startByte + page.bytes,
5633
+ hasMore: true,
5634
+ nextStartByte: startByte + page.bytes
5635
+ };
5636
+ }
5637
+ const endLine = endIndex + 1;
5638
+ const hasMore = endLine < ranges.length;
5639
+ return {
5640
+ content: selected.toString("utf8"),
5641
+ startLine,
5642
+ endLine,
5643
+ totalLines: ranges.length,
5644
+ startByte,
5645
+ endByte: startByte + selected.length,
5646
+ hasMore,
5647
+ ...hasMore ? { nextStartLine: endLine + 1 } : {}
5648
+ };
5649
+ }
5650
+ function readBytePage(content, requestedStart, maxBytes) {
5651
+ const buffer = Buffer.from(content);
5652
+ const startByte = Math.min(requestedStart, buffer.length);
5653
+ if (startByte < buffer.length && isUtf8Continuation(buffer[startByte] ?? 0)) {
5654
+ throw new Error("startByte must be an exact UTF-8 boundary shown by a previous page.");
5655
+ }
5656
+ const page = sliceUtf8Buffer(buffer, startByte, maxBytes);
5657
+ const prefix = buffer.subarray(0, startByte).toString("utf8");
5658
+ const startLine = countNewlines(prefix) + 1;
5659
+ const endByte = startByte + page.bytes;
5660
+ const hasMore = endByte < buffer.length;
5661
+ return {
5662
+ content: page.content,
5663
+ startLine,
5664
+ endLine: startLine + countNewlines(page.content),
5665
+ totalLines: countNewlines(content) + 1,
5666
+ startByte,
5667
+ endByte,
5668
+ hasMore,
5669
+ ...hasMore ? { nextStartByte: endByte } : {}
5670
+ };
5671
+ }
5672
+ function sliceUtf8Buffer(buffer, start, maxBytes) {
5673
+ let end = Math.min(buffer.length, start + maxBytes);
5674
+ while (end > start && isUtf8Continuation(buffer[end] ?? 0)) end -= 1;
5675
+ return { content: buffer.subarray(start, end).toString("utf8"), bytes: end - start };
5676
+ }
5677
+ function lineRanges(content) {
5678
+ const ranges = [];
5679
+ const expression = /\r?\n/gu;
5680
+ let start = 0;
5681
+ for (const match of content.matchAll(expression)) {
5682
+ ranges.push({ start, end: match.index });
5683
+ start = match.index + match[0].length;
5684
+ }
5685
+ ranges.push({ start, end: content.length });
5686
+ return ranges;
5687
+ }
5688
+ function countNewlines(value) {
5689
+ return value.match(/\n/gu)?.length ?? 0;
5690
+ }
5691
+ function isUtf8Continuation(byte) {
5692
+ return byte >= 128 && byte < 192;
5693
+ }
5694
+
5266
5695
  // src/tools/apply-patch.ts
5267
- import { lstat as lstat12, readFile as readFile7, unlink as unlink4 } from "node:fs/promises";
5696
+ import { lstat as lstat13, readFile as readFile8, unlink as unlink4 } from "node:fs/promises";
5268
5697
  import { applyPatch as applyUnifiedPatch, parsePatch } from "diff";
5269
- import { z as z6 } from "zod";
5270
- var inputSchema2 = z6.object({
5271
- patch: z6.string().min(1).max(1e7),
5272
- dry_run: z6.boolean().optional()
5698
+ import { z as z7 } from "zod";
5699
+ var inputSchema2 = z7.object({
5700
+ patch: z7.string().min(1).max(1e7),
5701
+ dry_run: z7.boolean().optional()
5273
5702
  }).strict();
5274
5703
  var applyPatchTool = {
5275
5704
  definition: {
@@ -5525,10 +5954,10 @@ function findSequence(haystack, needle, hint, minimum) {
5525
5954
  }
5526
5955
  async function readSnapshot2(path) {
5527
5956
  try {
5528
- const info = await lstat12(path);
5957
+ const info = await lstat13(path);
5529
5958
  if (info.isSymbolicLink()) throw new Error(`Refusing to patch a symbolic link: ${path}`);
5530
5959
  if (!info.isFile()) throw new Error(`Patch target is not a regular file: ${path}`);
5531
- return { before: await readFile7(path), mode: info.mode };
5960
+ return { before: await readFile8(path), mode: info.mode };
5532
5961
  } catch (error) {
5533
5962
  if (error.code === "ENOENT") return { before: null };
5534
5963
  throw error;
@@ -5577,13 +6006,13 @@ function buffersEqual(left, right) {
5577
6006
  }
5578
6007
 
5579
6008
  // src/tools/git.ts
5580
- import { join as join11 } from "node:path";
5581
- import { z as z7 } from "zod";
5582
- var inputSchema3 = z7.object({
5583
- args: z7.array(z7.string().max(1e4)).min(1).max(200),
5584
- cwd: z7.string().min(1).optional(),
5585
- timeout_ms: z7.number().int().min(100).max(6e5).optional(),
5586
- stdin: z7.string().max(5e6).optional()
6009
+ import { join as join12 } from "node:path";
6010
+ import { z as z8 } from "zod";
6011
+ var inputSchema3 = z8.object({
6012
+ args: z8.array(z8.string().max(1e4)).min(1).max(200),
6013
+ cwd: z8.string().min(1).optional(),
6014
+ timeout_ms: z8.number().int().min(100).max(6e5).optional(),
6015
+ stdin: z8.string().max(5e6).optional()
5587
6016
  }).strict();
5588
6017
  var networkCommands = /* @__PURE__ */ new Set([
5589
6018
  "clone",
@@ -5796,7 +6225,7 @@ var gitTool = {
5796
6225
  for (const candidate of explicit) {
5797
6226
  if (!candidate || candidate.startsWith("-")) continue;
5798
6227
  try {
5799
- paths.add(await context.workspace.resolvePath(join11(cwd, candidate), { allowMissing: true }));
6228
+ paths.add(await context.workspace.resolvePath(join12(cwd, candidate), { allowMissing: true }));
5800
6229
  } catch {
5801
6230
  }
5802
6231
  }
@@ -5817,7 +6246,7 @@ var gitTool = {
5817
6246
  const candidates = candidate.includes(" -> ") ? candidate.split(" -> ") : [candidate];
5818
6247
  for (const value of candidates) {
5819
6248
  try {
5820
- paths.add(await context.workspace.resolvePath(join11(cwd, value), { allowMissing: true }));
6249
+ paths.add(await context.workspace.resolvePath(join12(cwd, value), { allowMissing: true }));
5821
6250
  } catch {
5822
6251
  }
5823
6252
  }
@@ -5898,7 +6327,7 @@ async function collectGitChanges(before, after, runtime, cwd, context) {
5898
6327
  const paths = [];
5899
6328
  for (const candidate of [...candidates].slice(0, 2e3)) {
5900
6329
  try {
5901
- paths.push(await context.workspace.resolvePath(join11(cwd, candidate), { allowMissing: true }));
6330
+ paths.push(await context.workspace.resolvePath(join12(cwd, candidate), { allowMissing: true }));
5902
6331
  } catch {
5903
6332
  }
5904
6333
  }
@@ -6014,7 +6443,7 @@ async function validateGitWorkspaceArguments(args, command2, cwd, context) {
6014
6443
  if (/^(?:[a-z]+:\/\/|[a-z]+@[^:]+:)/i.test(candidate)) continue;
6015
6444
  if (command2 === "clone" && index === 0 && !candidate) continue;
6016
6445
  await context.workspace.resolvePath(
6017
- candidate.startsWith("/") ? candidate : join11(cwd, candidate),
6446
+ candidate.startsWith("/") ? candidate : join12(cwd, candidate),
6018
6447
  { allowMissing: true }
6019
6448
  );
6020
6449
  }
@@ -6067,17 +6496,17 @@ function positionalArguments(args, command2) {
6067
6496
  }
6068
6497
 
6069
6498
  // src/tools/list.ts
6070
- import { lstat as lstat13 } from "node:fs/promises";
6071
- import { relative as relative6, resolve as resolve13 } from "node:path";
6499
+ import { lstat as lstat14 } from "node:fs/promises";
6500
+ import { relative as relative6, resolve as resolve14 } from "node:path";
6072
6501
  import fg3 from "fast-glob";
6073
- import { z as z8 } from "zod";
6074
- var inputSchema4 = z8.object({
6075
- path: z8.string().min(1).optional(),
6076
- pattern: z8.string().min(1).optional(),
6077
- depth: z8.number().int().min(1).max(20).optional(),
6078
- include_hidden: z8.boolean().optional(),
6079
- include_directories: z8.boolean().optional(),
6080
- limit: z8.number().int().min(1).max(5e3).optional()
6502
+ import { z as z9 } from "zod";
6503
+ var inputSchema4 = z9.object({
6504
+ path: z9.string().min(1).optional(),
6505
+ pattern: z9.string().min(1).optional(),
6506
+ depth: z9.number().int().min(1).max(20).optional(),
6507
+ include_hidden: z9.boolean().optional(),
6508
+ include_directories: z9.boolean().optional(),
6509
+ limit: z9.number().int().min(1).max(5e3).optional()
6081
6510
  }).strict();
6082
6511
  var ignored = [
6083
6512
  "**/.git/**",
@@ -6126,11 +6555,11 @@ var listFilesTool = {
6126
6555
  for (const path of selected) {
6127
6556
  let safePath;
6128
6557
  try {
6129
- safePath = await context.workspace.resolvePath(resolve13(directory, path), { expect: "any" });
6558
+ safePath = await context.workspace.resolvePath(resolve14(directory, path), { expect: "any" });
6130
6559
  } catch {
6131
6560
  continue;
6132
6561
  }
6133
- const info = await lstat13(safePath);
6562
+ const info = await lstat14(safePath);
6134
6563
  rendered.push(`${info.isDirectory() ? "d" : "f"} ${relative6(directory, safePath)}${info.isDirectory() ? "/" : ""}`);
6135
6564
  }
6136
6565
  const base = workspaceAliasPath(directory, context.workspace.roots);
@@ -6147,14 +6576,14 @@ var listFilesTool = {
6147
6576
  };
6148
6577
 
6149
6578
  // src/tools/read.ts
6150
- import { readFile as readFile8, stat as stat7 } from "node:fs/promises";
6151
- import { z as z9 } from "zod";
6152
- var inputSchema5 = z9.object({
6153
- path: z9.string().min(1),
6154
- start_line: z9.number().int().positive().optional(),
6155
- end_line: z9.number().int().positive().optional(),
6156
- line_numbers: z9.boolean().optional(),
6157
- max_bytes: z9.number().int().positive().max(1e6).optional()
6579
+ import { readFile as readFile9, stat as stat7 } from "node:fs/promises";
6580
+ import { z as z10 } from "zod";
6581
+ var inputSchema5 = z10.object({
6582
+ path: z10.string().min(1),
6583
+ start_line: z10.number().int().positive().optional(),
6584
+ end_line: z10.number().int().positive().optional(),
6585
+ line_numbers: z10.boolean().optional(),
6586
+ max_bytes: z10.number().int().positive().max(1e6).optional()
6158
6587
  }).strict();
6159
6588
  var readFileTool = {
6160
6589
  definition: {
@@ -6177,7 +6606,7 @@ var readFileTool = {
6177
6606
  if (info.size > 1e7) {
6178
6607
  throw new Error(`File is too large to read safely (${info.size} bytes).`);
6179
6608
  }
6180
- const buffer = await readFile8(path);
6609
+ const buffer = await readFile9(path);
6181
6610
  if (looksBinary(buffer)) throw new Error("Binary files cannot be read with read_file.");
6182
6611
  const raw = buffer.toString("utf8");
6183
6612
  const lines = raw.split("\n");
@@ -6221,6 +6650,73 @@ function truncateUtf8(input2, maxBytes) {
6221
6650
  return buffer.subarray(0, end).toString("utf8");
6222
6651
  }
6223
6652
 
6653
+ // src/tools/read-artifact.ts
6654
+ import { z as z11 } from "zod";
6655
+ var inputSchema6 = z11.object({
6656
+ sha256: z11.string().regex(/^[a-f0-9]{64}$/u),
6657
+ start_line: z11.number().int().positive().optional(),
6658
+ start_byte: z11.number().int().nonnegative().optional(),
6659
+ max_lines: z11.number().int().min(1).max(1e3).optional(),
6660
+ max_bytes: z11.number().int().min(256).max(768).optional()
6661
+ }).strict().refine((input2) => input2.start_line === void 0 || input2.start_byte === void 0, {
6662
+ message: "Use either start_line or start_byte, not both."
6663
+ });
6664
+ var readToolArtifactTool = {
6665
+ definition: {
6666
+ name: "read_tool_artifact",
6667
+ description: "Read a bounded page of oversized tool output retained for this session. Use only the SHA-256 shown in a tool-output receipt.",
6668
+ category: "read",
6669
+ inputSchema: jsonSchema({
6670
+ sha256: { type: "string", description: "The exact 64-character SHA-256 from a retained-output receipt." },
6671
+ start_line: { type: "integer", minimum: 1, default: 1 },
6672
+ start_byte: { type: "integer", minimum: 0, description: "Exact UTF-8 continuation byte shown by a previous page." },
6673
+ max_lines: { type: "integer", minimum: 1, maximum: 1e3, default: 200 },
6674
+ max_bytes: { type: "integer", minimum: 256, maximum: 768, default: 768 }
6675
+ }, ["sha256"])
6676
+ },
6677
+ async execute(arguments_, context) {
6678
+ const input2 = inputSchema6.parse(arguments_);
6679
+ const artifact = context.session.toolArtifacts?.find(
6680
+ (candidate) => candidate.sha256 === input2.sha256 && Date.parse(candidate.expiresAt) > Date.now()
6681
+ );
6682
+ if (!artifact) {
6683
+ throw new Error("No retained tool output matches this tool call in the current session.");
6684
+ }
6685
+ const store = context.toolArtifactStore ?? new ToolArtifactStore(context.workspace.primaryRoot);
6686
+ const page = await store.read(context.session.id, artifact.toolCallId, {
6687
+ ...input2.start_line !== void 0 ? { startLine: input2.start_line } : {},
6688
+ ...input2.start_byte !== void 0 ? { startByte: input2.start_byte } : {},
6689
+ ...input2.max_lines !== void 0 ? { maxLines: input2.max_lines } : {},
6690
+ maxBytes: input2.max_bytes ?? 768
6691
+ });
6692
+ if (page.sha256 !== artifact.sha256 || page.bytes !== artifact.bytes) {
6693
+ throw new Error("Retained tool output no longer matches the session receipt.");
6694
+ }
6695
+ const heading = `Retained output page: lines ${page.startLine}-${page.endLine} of ${page.totalLines}; bytes ${page.startByte}-${page.endByte} of ${page.bytes}`;
6696
+ const continuation = page.nextStartByte !== void 0 ? `continue with start_byte=${page.nextStartByte}` : page.nextStartLine !== void 0 ? `continue with start_line=${page.nextStartLine}` : "";
6697
+ return {
6698
+ content: `${heading}
6699
+ ${page.content}${page.hasMore ? `
6700
+ \u2026 more retained output; ${continuation}` : ""}`,
6701
+ metadata: {
6702
+ artifact: {
6703
+ toolCallId: page.toolCallId,
6704
+ sha256: page.sha256,
6705
+ bytes: page.bytes,
6706
+ expiresAt: page.expiresAt,
6707
+ redacted: page.redacted
6708
+ },
6709
+ startLine: page.startLine,
6710
+ endLine: page.endLine,
6711
+ totalLines: page.totalLines,
6712
+ startByte: page.startByte,
6713
+ endByte: page.endByte,
6714
+ truncated: page.hasMore
6715
+ }
6716
+ };
6717
+ }
6718
+ };
6719
+
6224
6720
  // src/tools/registry.ts
6225
6721
  var ToolRegistry = class {
6226
6722
  tools = /* @__PURE__ */ new Map();
@@ -6268,19 +6764,19 @@ function assertToolName(name) {
6268
6764
  }
6269
6765
 
6270
6766
  // src/tools/search.ts
6271
- import { readFile as readFile9, stat as stat8 } from "node:fs/promises";
6272
- import { resolve as resolve14 } from "node:path";
6767
+ import { readFile as readFile10, stat as stat8 } from "node:fs/promises";
6768
+ import { resolve as resolve15 } from "node:path";
6273
6769
  import fg4 from "fast-glob";
6274
- import { z as z10 } from "zod";
6275
- var inputSchema6 = z10.object({
6276
- query: z10.string().min(1).max(1e4),
6277
- path: z10.string().min(1).optional(),
6278
- pattern: z10.string().min(1).optional(),
6279
- mode: z10.enum(["text", "ranked"]).optional(),
6280
- literal: z10.boolean().optional(),
6281
- case_sensitive: z10.boolean().optional(),
6282
- context_lines: z10.number().int().min(0).max(20).optional(),
6283
- max_results: z10.number().int().min(1).max(500).optional()
6770
+ import { z as z12 } from "zod";
6771
+ var inputSchema7 = z12.object({
6772
+ query: z12.string().min(1).max(1e4),
6773
+ path: z12.string().min(1).optional(),
6774
+ pattern: z12.string().min(1).optional(),
6775
+ mode: z12.enum(["text", "ranked"]).optional(),
6776
+ literal: z12.boolean().optional(),
6777
+ case_sensitive: z12.boolean().optional(),
6778
+ context_lines: z12.number().int().min(0).max(20).optional(),
6779
+ max_results: z12.number().int().min(1).max(500).optional()
6284
6780
  }).strict();
6285
6781
  var ignore = [
6286
6782
  "**/.git/**",
@@ -6315,7 +6811,7 @@ var searchCodeTool = {
6315
6811
  }, ["query"])
6316
6812
  },
6317
6813
  async execute(arguments_, context) {
6318
- const input2 = inputSchema6.parse(arguments_);
6814
+ const input2 = inputSchema7.parse(arguments_);
6319
6815
  const directory = await context.workspace.resolveDirectory(input2.path ?? ".");
6320
6816
  if ((input2.mode ?? "text") === "ranked") {
6321
6817
  if (!context.contextEngine) throw new Error("Local ranked retrieval is unavailable.");
@@ -6339,7 +6835,7 @@ var searchCodeTool = {
6339
6835
  if (results.length >= maxResults) break;
6340
6836
  let path;
6341
6837
  try {
6342
- path = await context.workspace.resolvePath(resolve14(directory, file), { expect: "file" });
6838
+ path = await context.workspace.resolvePath(resolve15(directory, file), { expect: "file" });
6343
6839
  } catch {
6344
6840
  skipped += 1;
6345
6841
  continue;
@@ -6349,7 +6845,7 @@ var searchCodeTool = {
6349
6845
  skipped += 1;
6350
6846
  continue;
6351
6847
  }
6352
- const buffer = await readFile9(path);
6848
+ const buffer = await readFile10(path);
6353
6849
  if (buffer.subarray(0, 8192).includes(0)) {
6354
6850
  skipped += 1;
6355
6851
  continue;
@@ -6421,17 +6917,17 @@ ${hit.content}`
6421
6917
  }
6422
6918
 
6423
6919
  // src/tools/shell.ts
6424
- import { createHash as createHash7 } from "node:crypto";
6425
- import { lstat as lstat14, readFile as readFile10, readdir as readdir4 } from "node:fs/promises";
6426
- import { join as join12 } from "node:path";
6427
- import { z as z11 } from "zod";
6428
- var inputSchema7 = z11.object({
6429
- command: z11.string().min(1).max(1e5),
6430
- cwd: z11.string().min(1).optional(),
6431
- timeout_ms: z11.number().int().min(100).max(6e5).optional(),
6432
- max_output_bytes: z11.number().int().min(1e3).max(5e6).optional(),
6433
- env: z11.record(z11.string(), z11.string()).optional(),
6434
- stdin: z11.string().max(5e6).optional()
6920
+ import { createHash as createHash8 } from "node:crypto";
6921
+ import { lstat as lstat15, readFile as readFile11, readdir as readdir5 } from "node:fs/promises";
6922
+ import { join as join13 } from "node:path";
6923
+ import { z as z13 } from "zod";
6924
+ var inputSchema8 = z13.object({
6925
+ command: z13.string().min(1).max(1e5),
6926
+ cwd: z13.string().min(1).optional(),
6927
+ timeout_ms: z13.number().int().min(100).max(6e5).optional(),
6928
+ max_output_bytes: z13.number().int().min(1e3).max(5e6).optional(),
6929
+ env: z13.record(z13.string(), z13.string()).optional(),
6930
+ stdin: z13.string().max(5e6).optional()
6435
6931
  }).strict();
6436
6932
  var shellTool = {
6437
6933
  definition: {
@@ -6448,7 +6944,7 @@ var shellTool = {
6448
6944
  }, ["command"])
6449
6945
  },
6450
6946
  permissionCategories(arguments_) {
6451
- const input2 = inputSchema7.parse(arguments_);
6947
+ const input2 = inputSchema8.parse(arguments_);
6452
6948
  validateEnvironment(input2.env);
6453
6949
  return [
6454
6950
  "shell",
@@ -6458,12 +6954,12 @@ var shellTool = {
6458
6954
  ];
6459
6955
  },
6460
6956
  async affectedPaths(arguments_, context) {
6461
- const input2 = inputSchema7.parse(arguments_);
6957
+ const input2 = inputSchema8.parse(arguments_);
6462
6958
  if (!appearsToModifyWorkspace(input2.command)) return [];
6463
6959
  return collectAffectedPaths(input2.command, input2.cwd ?? ".", context);
6464
6960
  },
6465
6961
  async execute(arguments_, context) {
6466
- const input2 = inputSchema7.parse(arguments_);
6962
+ const input2 = inputSchema8.parse(arguments_);
6467
6963
  validateEnvironment(input2.env);
6468
6964
  const cwd = await context.workspace.resolveDirectory(input2.cwd ?? ".");
6469
6965
  const candidates = appearsToModifyWorkspace(input2.command) ? await collectAffectedPaths(input2.command, input2.cwd ?? ".", context) : [];
@@ -6503,7 +6999,8 @@ Execution interrupted: ${error instanceof Error ? error.message : String(error)}
6503
6999
  result.stdout ? `stdout:
6504
7000
  ${result.stdout}` : "",
6505
7001
  result.stderr ? `stderr:
6506
- ${result.stderr}` : ""
7002
+ ${result.stderr}` : "",
7003
+ result.stdoutTruncated || result.stderrTruncated ? `Source output exceeded the command capture limit (${result.stdoutBytes} stdout bytes, ${result.stderrBytes} stderr bytes observed). Rerun with max_output_bytes up to 5000000 or narrow the command; omitted source bytes are not recoverable from this result.` : ""
6507
7004
  ].filter(Boolean);
6508
7005
  return {
6509
7006
  ok: result.exitCode === 0 && !result.timedOut,
@@ -6513,6 +7010,9 @@ ${result.stderr}` : ""
6513
7010
  exitCode: result.exitCode,
6514
7011
  timedOut: result.timedOut,
6515
7012
  durationMs: result.durationMs,
7013
+ stdoutBytes: result.stdoutBytes,
7014
+ stderrBytes: result.stderrBytes,
7015
+ sourceTruncated: result.stdoutTruncated || result.stderrTruncated,
6516
7016
  changeTracking: candidates.length ? "targeted" : beforeWorkspace && afterWorkspace && beforeWorkspace.complete && afterWorkspace.complete ? "workspace-snapshot" : beforeWorkspace ? "unresolved" : "read-only"
6517
7017
  },
6518
7018
  ...changedFiles.length ? { changedFiles } : {}
@@ -6576,7 +7076,7 @@ async function collectAffectedPaths(command2, cwdInput, context) {
6576
7076
  allowMissing: true
6577
7077
  });
6578
7078
  try {
6579
- if ((await lstat14(path)).isDirectory()) continue;
7079
+ if ((await lstat15(path)).isDirectory()) continue;
6580
7080
  } catch (error) {
6581
7081
  if (error.code !== "ENOENT") throw error;
6582
7082
  }
@@ -6604,7 +7104,7 @@ async function changedPaths(paths, before) {
6604
7104
  }
6605
7105
  async function snapshotPath(path) {
6606
7106
  try {
6607
- const info = await lstat14(path);
7107
+ const info = await lstat15(path);
6608
7108
  return { exists: true, size: info.size, mtimeMs: info.mtimeMs };
6609
7109
  } catch (error) {
6610
7110
  if (error.code === "ENOENT") return { exists: false };
@@ -6620,18 +7120,18 @@ async function captureWorkspaceSnapshot(roots) {
6620
7120
  if (!discovered.complete) complete = false;
6621
7121
  for (const path of discovered.files) {
6622
7122
  try {
6623
- const info = await lstat14(path);
7123
+ const info = await lstat15(path);
6624
7124
  if (!info.isFile() || info.isSymbolicLink()) continue;
6625
7125
  if (files.size >= MAX_SNAPSHOT_FILES || info.size > MAX_SNAPSHOT_FILE_BYTES || totalBytes + info.size > MAX_SNAPSHOT_TOTAL_BYTES) {
6626
7126
  complete = false;
6627
7127
  continue;
6628
7128
  }
6629
- const content = await readFile10(path);
7129
+ const content = await readFile11(path);
6630
7130
  totalBytes += content.length;
6631
7131
  files.set(path, {
6632
7132
  size: info.size,
6633
7133
  mtimeMs: info.mtimeMs,
6634
- hash: createHash7("sha256").update(content).digest("hex")
7134
+ hash: createHash8("sha256").update(content).digest("hex")
6635
7135
  });
6636
7136
  } catch (error) {
6637
7137
  if (error.code !== "ENOENT") complete = false;
@@ -6652,14 +7152,14 @@ async function discoverSnapshotFiles(root) {
6652
7152
  const directory = pending.pop();
6653
7153
  if (!directory) break;
6654
7154
  try {
6655
- const entries = await readdir4(directory, { withFileTypes: true, encoding: "utf8" });
7155
+ const entries = await readdir5(directory, { withFileTypes: true, encoding: "utf8" });
6656
7156
  for (const entry of entries) {
6657
7157
  if (ignored2.has(entry.name) || /^\.skein\.(?:migrating|rollback)-/u.test(entry.name)) continue;
6658
7158
  if (entry.isSymbolicLink()) {
6659
7159
  complete = false;
6660
7160
  continue;
6661
7161
  }
6662
- const path = join12(directory, entry.name);
7162
+ const path = join13(directory, entry.name);
6663
7163
  if (entry.isDirectory()) pending.push(path);
6664
7164
  else if (entry.isFile()) files.push(path);
6665
7165
  }
@@ -6673,18 +7173,18 @@ async function discoverSnapshotFiles(root) {
6673
7173
 
6674
7174
  // src/tools/task.ts
6675
7175
  import { randomUUID as randomUUID9 } from "node:crypto";
6676
- import { z as z12 } from "zod";
6677
- var taskSchema2 = z12.object({
6678
- id: z12.string().min(1).optional(),
6679
- title: z12.string().min(1).max(500),
6680
- status: z12.enum(["pending", "in_progress", "completed"])
7176
+ import { z as z14 } from "zod";
7177
+ var taskSchema2 = z14.object({
7178
+ id: z14.string().min(1).optional(),
7179
+ title: z14.string().min(1).max(500),
7180
+ status: z14.enum(["pending", "in_progress", "completed"])
6681
7181
  }).strict();
6682
- var inputSchema8 = z12.discriminatedUnion("action", [
6683
- z12.object({ action: z12.literal("list") }).strict(),
6684
- z12.object({ action: z12.literal("add"), title: z12.string().min(1).max(500), status: z12.enum(["pending", "in_progress", "completed"]).optional() }).strict(),
6685
- z12.object({ action: z12.literal("update"), id: z12.string().min(1), title: z12.string().min(1).max(500).optional(), status: z12.enum(["pending", "in_progress", "completed"]).optional() }).strict(),
6686
- z12.object({ action: z12.literal("remove"), id: z12.string().min(1) }).strict(),
6687
- z12.object({ action: z12.literal("replace"), tasks: z12.array(taskSchema2).max(100) }).strict()
7182
+ var inputSchema9 = z14.discriminatedUnion("action", [
7183
+ z14.object({ action: z14.literal("list") }).strict(),
7184
+ z14.object({ action: z14.literal("add"), title: z14.string().min(1).max(500), status: z14.enum(["pending", "in_progress", "completed"]).optional() }).strict(),
7185
+ z14.object({ action: z14.literal("update"), id: z14.string().min(1), title: z14.string().min(1).max(500).optional(), status: z14.enum(["pending", "in_progress", "completed"]).optional() }).strict(),
7186
+ z14.object({ action: z14.literal("remove"), id: z14.string().min(1) }).strict(),
7187
+ z14.object({ action: z14.literal("replace"), tasks: z14.array(taskSchema2).max(100) }).strict()
6688
7188
  ]);
6689
7189
  var taskTool = {
6690
7190
  definition: {
@@ -6715,7 +7215,7 @@ var taskTool = {
6715
7215
  }
6716
7216
  },
6717
7217
  async execute(arguments_, context) {
6718
- const input2 = inputSchema8.parse(arguments_);
7218
+ const input2 = inputSchema9.parse(arguments_);
6719
7219
  const tasks = context.session.tasks;
6720
7220
  switch (input2.action) {
6721
7221
  case "list":
@@ -6751,126 +7251,97 @@ var taskTool = {
6751
7251
  }
6752
7252
  };
6753
7253
 
6754
- // src/tools/working-memory.ts
6755
- 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() {
7254
+ // src/tools/task-contract.ts
7255
+ import { randomUUID as randomUUID11 } from "node:crypto";
7256
+ import { z as z15 } from "zod";
7257
+
7258
+ // src/agent/task-contract.ts
7259
+ import { randomUUID as randomUUID10 } from "node:crypto";
7260
+ var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
7261
+ function shouldUseTaskContract(request, intent, existing) {
7262
+ if (existing && existing.state !== "satisfied") return true;
7263
+ if (!EXECUTABLE_INTENTS.has(intent)) return false;
7264
+ const value = request.trim();
7265
+ if (value.length < 180) return false;
7266
+ const requirements = value.split(/(?:\n+|[;;。]\s*|\b(?:and|then|also|plus)\b|以及|并且|同时|还要|另外)/iu).filter((item) => item.trim().length >= 12);
7267
+ const complexitySignals = [
7268
+ /\b(?:refactor|migrate|redesign|architecture|end[- ]to[- ]end|across|multiple)\b/iu,
7269
+ /重构|迁移|架构|完整|全链路|多个|跨模块|兼容|同时/iu,
7270
+ /(?:^|\n)\s*(?:[-*]|\d+[.)、])\s+/mu
7271
+ ].filter((pattern) => pattern.test(value)).length;
7272
+ return requirements.length >= 3 || complexitySignals >= 2 || value.length >= 500;
7273
+ }
7274
+ function createDraftTaskContract(request, auditBoundaryId) {
7275
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7276
+ const objective = compact(request, 2e3);
6828
7277
  return {
6829
- goal: "",
6830
- focus: "",
6831
- constraints: [],
6832
- decisions: [],
6833
- openQuestions: [],
6834
- relevantFiles: [],
6835
- lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
7278
+ version: 1,
7279
+ state: "draft",
7280
+ objective,
7281
+ scope: ["Configured workspace roots and the files required by the objective."],
7282
+ constraints: [
7283
+ "Preserve unrelated user changes.",
7284
+ "Use successful tool evidence for acceptance claims."
7285
+ ],
7286
+ nonGoals: [],
7287
+ acceptanceCriteria: [
7288
+ criterion("requested-outcome", `Implement the requested outcome: ${compact(request, 500)}`),
7289
+ criterion("verification", "Run the smallest relevant deterministic verification after the final mutation.")
7290
+ ],
7291
+ verificationRequirements: [
7292
+ "Record at least one successful test, typecheck, lint, build, check, or diff check after the final mutation."
7293
+ ],
7294
+ createdAt: now,
7295
+ updatedAt: now,
7296
+ ...auditBoundaryId ? { auditBoundaryId } : {}
6836
7297
  };
6837
7298
  }
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);
7299
+ function successfulContractEvidence(session) {
7300
+ const refs = /* @__PURE__ */ new Set();
7301
+ const contract = session.taskContract;
7302
+ if (!contract) return refs;
7303
+ for (const event of eventsSinceContract(session.audit ?? [], contract)) {
7304
+ if (event.type !== "tool" || event.outcome !== "success") continue;
7305
+ if (event.tool === "task_contract" || event.tool === "task" || event.tool === "working_memory") continue;
7306
+ refs.add(event.id);
7307
+ refs.add(event.toolCallId);
7308
+ }
7309
+ return refs;
6844
7310
  }
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
- }
7311
+ function eventsSinceContract(audit, contract) {
7312
+ if (contract.auditBoundaryId) {
7313
+ const boundary = audit.findIndex((event) => event.id === contract.auditBoundaryId);
7314
+ if (boundary >= 0) return audit.slice(boundary + 1);
6852
7315
  }
7316
+ return audit.filter((event) => event.createdAt >= contract.createdAt);
6853
7317
  }
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);
7318
+ function refreshTaskContractState(contract) {
7319
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
7320
+ contract.state = required.some((item) => item.status === "blocked") ? "blocked" : required.length > 0 && required.every((item) => item.status === "satisfied") ? "satisfied" : "active";
7321
+ contract.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6856
7322
  }
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");
7323
+ function criterion(id, description) {
7324
+ return {
7325
+ id: `${id}-${randomUUID10().slice(0, 8)}`,
7326
+ description,
7327
+ required: true,
7328
+ status: "pending",
7329
+ evidenceRefs: []
7330
+ };
7331
+ }
7332
+ function compact(value, limit) {
7333
+ return value.trim().replace(/\s+/gu, " ").slice(0, limit);
6866
7334
  }
6867
7335
 
7336
+ // src/agent/completion-gate.ts
7337
+ import { createHash as createHash10 } from "node:crypto";
7338
+
6868
7339
  // src/tools/permissions.ts
6869
- import { createHash as createHash8, createHmac, randomBytes } from "node:crypto";
7340
+ import { createHash as createHash9, createHmac, randomBytes } from "node:crypto";
6870
7341
  var permissionScopeSecret = randomBytes(32);
6871
7342
  function permissionKey(call, category) {
6872
7343
  const categoryScope = category === "network" ? `\0network-request:${scopeFingerprint(normalizeRequestState(call.arguments))}` : "";
6873
- const digest = createHash8("sha256").update(`${category}\0${call.name}\0${permissionTarget(call)}${categoryScope}`).digest("hex").slice(0, 24);
7344
+ const digest = createHash9("sha256").update(`${category}\0${call.name}\0${permissionTarget(call)}${categoryScope}`).digest("hex").slice(0, 24);
6874
7345
  return `${category}:${call.name}:${digest}`;
6875
7346
  }
6876
7347
  function permissionTarget(call) {
@@ -7048,10 +7519,513 @@ function escapeRegExp(value) {
7048
7519
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7049
7520
  }
7050
7521
 
7522
+ // src/agent/completion-gate.ts
7523
+ function captureVerification(call, result, changeSequence, configuredCommands) {
7524
+ if (call.name !== "shell" && call.name !== "git") return void 0;
7525
+ const command2 = commandForCall(call);
7526
+ if (!command2) return void 0;
7527
+ const normalized = normalizeCommand2(command2);
7528
+ const configured = new Set(configuredCommands.map(normalizeCommand2));
7529
+ const kind = configured.has(normalized) ? "configured" : classifyVerificationCommand(normalized);
7530
+ if (!kind) return void 0;
7531
+ return {
7532
+ toolCallId: call.id,
7533
+ tool: call.name,
7534
+ command: redactCommand(command2),
7535
+ kind,
7536
+ ok: result.ok,
7537
+ changeSequence,
7538
+ commandKey: createHash10("sha256").update(normalized).digest("hex")
7539
+ };
7540
+ }
7541
+ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutationTracking = "complete", taskContract, audit = []) {
7542
+ const files = [...new Set(changedFiles)];
7543
+ const acceptance = taskContract ? buildAcceptance(taskContract, audit, evidence, currentChangeSequence) : void 0;
7544
+ if (mutationTracking === "unknown") {
7545
+ return {
7546
+ status: "unverified",
7547
+ changedFiles: files,
7548
+ checks: [],
7549
+ 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.",
7550
+ mutationTracking,
7551
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
7552
+ };
7553
+ }
7554
+ if (!files.length) {
7555
+ if (acceptance && acceptanceUnresolved(acceptance)) {
7556
+ return {
7557
+ status: "unverified",
7558
+ changedFiles: [],
7559
+ checks: [],
7560
+ detail: acceptanceDetail(acceptance),
7561
+ acceptance: acceptanceForCompletion(acceptance, "unverified")
7562
+ };
7563
+ }
7564
+ return {
7565
+ status: "no_changes",
7566
+ changedFiles: [],
7567
+ checks: [],
7568
+ detail: "No workspace files changed in this run.",
7569
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "no_changes") } : {}
7570
+ };
7571
+ }
7572
+ const latestByCommand = /* @__PURE__ */ new Map();
7573
+ for (const item of evidence) {
7574
+ if (item.changeSequence === currentChangeSequence) {
7575
+ latestByCommand.set(item.commandKey, item);
7576
+ }
7577
+ }
7578
+ const checks = [...latestByCommand.values()].map(publicEvidence);
7579
+ if (!checks.length) {
7580
+ return {
7581
+ status: "unverified",
7582
+ changedFiles: files,
7583
+ checks,
7584
+ detail: `No successful verification was recorded after the last change to ${fileCount(files.length)}.`,
7585
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
7586
+ };
7587
+ }
7588
+ const failures = checks.filter((check) => !check.ok);
7589
+ if (failures.length) {
7590
+ return {
7591
+ status: "verification_failed",
7592
+ changedFiles: files,
7593
+ checks,
7594
+ detail: `${failures.length} of ${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} failed.`,
7595
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verification_failed") } : {}
7596
+ };
7597
+ }
7598
+ if (acceptance && acceptanceUnresolved(acceptance)) {
7599
+ return {
7600
+ status: "unverified",
7601
+ changedFiles: files,
7602
+ checks,
7603
+ detail: acceptanceDetail(acceptance),
7604
+ acceptance: acceptanceForCompletion(acceptance, "unverified")
7605
+ };
7606
+ }
7607
+ return {
7608
+ status: "verified",
7609
+ changedFiles: files,
7610
+ checks,
7611
+ detail: `${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} passed for ${fileCount(files.length)}.`,
7612
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verified") } : {}
7613
+ };
7614
+ }
7615
+ function completionRecoveryDirective(completion) {
7616
+ if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
7617
+ const unresolved = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
7618
+ const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
7619
+ return `<runtime-completion-gate status="acceptance_unresolved" authorization="none">
7620
+ The run cannot be marked complete while required Task Contract criteria remain unresolved:
7621
+ ${unresolved || "- Acceptance criteria are satisfied."}${completion.acceptance.missingVerification.length ? `
7622
+ Missing required verification:
7623
+ ${completion.acceptance.missingVerification.map((item) => `- ${item}`).join("\n")}` : ""}${failed ? `
7624
+ Current failed checks:
7625
+ ${failed}` : ""}
7626
+ 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.
7627
+ </runtime-completion-gate>`;
7628
+ }
7629
+ if (completion.status === "verification_failed") {
7630
+ const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
7631
+ return `<runtime-completion-gate status="verification_failed" authorization="none">
7632
+ The run cannot be marked complete because current verification failed:
7633
+ ${failed}
7634
+ 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.
7635
+ </runtime-completion-gate>`;
7636
+ }
7637
+ 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.`;
7638
+ return `<runtime-completion-gate status="unverified" authorization="none">
7639
+ ${changeSummary}
7640
+ 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.
7641
+ </runtime-completion-gate>`;
7642
+ }
7643
+ function buildAcceptance(contract, audit, evidence, currentChangeSequence) {
7644
+ const contractEvents = eventsSinceContract(audit, contract);
7645
+ const successfulEvents = contractEvents.filter(
7646
+ (event) => event.type === "tool" && event.outcome === "success" && event.tool !== "task_contract" && event.tool !== "task" && event.tool !== "working_memory"
7647
+ );
7648
+ const latestMutationIndex = contractEvents.reduce((latest, event, index) => {
7649
+ const changedFiles = event.metadata?.changedFiles;
7650
+ return Array.isArray(changedFiles) && changedFiles.length > 0 ? index : latest;
7651
+ }, -1);
7652
+ const successfulRefs = /* @__PURE__ */ new Map();
7653
+ for (const event of successfulEvents) {
7654
+ const index = contractEvents.indexOf(event);
7655
+ successfulRefs.set(event.id, { event, index });
7656
+ successfulRefs.set(event.toolCallId, { event, index });
7657
+ }
7658
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
7659
+ const normalized = required.map((item) => {
7660
+ const evidenceValid = item.evidenceRefs.some((ref) => {
7661
+ const event = successfulRefs.get(ref);
7662
+ return event !== void 0 && event.index >= latestMutationIndex;
7663
+ });
7664
+ const status = item.status === "satisfied" && !evidenceValid ? "pending" : item.status;
7665
+ return { id: item.id, description: item.description, status };
7666
+ });
7667
+ const satisfied = normalized.filter((item) => item.status === "satisfied").length;
7668
+ const pending = normalized.filter((item) => item.status === "pending").length;
7669
+ const blocked = normalized.filter((item) => item.status === "blocked").length;
7670
+ const currentChecks = evidence.filter((item) => item.changeSequence === currentChangeSequence && item.ok);
7671
+ const requirements = contract.verificationRequirements.length ? contract.verificationRequirements : ["Record at least one successful test, typecheck, lint, build, check, or diff check after the final mutation."];
7672
+ const missingVerification = requirements.filter(
7673
+ (requirement) => !verificationRequirementMet(requirement, currentChecks)
7674
+ );
7675
+ return {
7676
+ state: contract.state === "draft" ? "draft" : blocked > 0 ? "blocked" : pending > 0 || missingVerification.length > 0 ? "active" : "satisfied",
7677
+ total: normalized.length,
7678
+ satisfied,
7679
+ pending,
7680
+ blocked,
7681
+ missingVerification,
7682
+ unresolved: normalized.filter((item) => item.status !== "satisfied")
7683
+ };
7684
+ }
7685
+ function acceptanceDetail(acceptance) {
7686
+ const parts = [
7687
+ acceptance.pending ? `${acceptance.pending} pending` : "",
7688
+ acceptance.blocked ? `${acceptance.blocked} blocked` : "",
7689
+ acceptance.missingVerification.length ? `${acceptance.missingVerification.length} verification requirements missing` : ""
7690
+ ].filter(Boolean).join(" and ");
7691
+ const unresolved = acceptance.pending + acceptance.blocked;
7692
+ return `Task Contract acceptance is unresolved: ${parts} required ${unresolved === 1 ? "criterion" : "criteria"}.`;
7693
+ }
7694
+ function verificationRequirementMet(requirement, checks) {
7695
+ const normalized = normalizeCommand2(requirement);
7696
+ const broad = normalized.match(/^Record at least one successful (.+) after the final mutation\.?$/iu);
7697
+ if (broad) return checks.length > 0;
7698
+ const commandKey = createHash10("sha256").update(normalized).digest("hex");
7699
+ return checks.some((item) => item.commandKey === commandKey);
7700
+ }
7701
+ function acceptanceUnresolved(acceptance) {
7702
+ return acceptance.pending > 0 || acceptance.blocked > 0 || acceptance.missingVerification.length > 0;
7703
+ }
7704
+ function acceptanceForCompletion(acceptance, status) {
7705
+ if (acceptance.state === "draft" || acceptance.state === "blocked" || status === "verified" || status === "no_changes") return acceptance;
7706
+ return acceptance.state === "active" ? acceptance : { ...acceptance, state: "active" };
7707
+ }
7708
+ function classifyVerificationCommand(command2) {
7709
+ const normalized = normalizeCommand2(command2).toLocaleLowerCase();
7710
+ const segments = normalized.split(/\s*(?:&&|\|\||;)\s*/u).filter(Boolean);
7711
+ if (segments.length > 1) {
7712
+ const kinds = segments.map(classifySingleVerificationCommand);
7713
+ if (kinds.every((kind) => kind !== void 0)) {
7714
+ return kinds.every((kind) => kind === kinds[0]) ? kinds[0] : "check";
7715
+ }
7716
+ return void 0;
7717
+ }
7718
+ return classifySingleVerificationCommand(normalized);
7719
+ }
7720
+ function classifySingleVerificationCommand(command2) {
7721
+ const value = command2.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+)\s+)+/u, "");
7722
+ if (/^git\s+diff\b.*(?:^|\s)--check(?:\s|$)/u.test(value)) return "diff";
7723
+ 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";
7724
+ 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";
7725
+ 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";
7726
+ 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";
7727
+ 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";
7728
+ return void 0;
7729
+ }
7730
+ function publicEvidence(item) {
7731
+ return {
7732
+ toolCallId: item.toolCallId,
7733
+ tool: item.tool,
7734
+ command: item.command,
7735
+ kind: item.kind,
7736
+ ok: item.ok
7737
+ };
7738
+ }
7739
+ function normalizeCommand2(command2) {
7740
+ return command2.trim().replace(/[\t ]+/gu, " ");
7741
+ }
7742
+ function redactCommand(command2) {
7743
+ 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);
7744
+ }
7745
+ function fileCount(count) {
7746
+ return `${count} workspace ${count === 1 ? "file" : "files"}`;
7747
+ }
7748
+
7749
+ // src/tools/task-contract.ts
7750
+ var criterionSchema = z15.object({
7751
+ id: z15.string().min(1).max(128).optional(),
7752
+ description: z15.string().min(1).max(2e3),
7753
+ required: z15.boolean().optional()
7754
+ }).strict();
7755
+ var inputSchema10 = z15.discriminatedUnion("action", [
7756
+ z15.object({ action: z15.literal("show") }).strict(),
7757
+ z15.object({
7758
+ action: z15.literal("replace"),
7759
+ objective: z15.string().min(1).max(2e4),
7760
+ scope: z15.array(z15.string().min(1).max(2e3)).max(64),
7761
+ constraints: z15.array(z15.string().min(1).max(2e3)).max(64),
7762
+ non_goals: z15.array(z15.string().min(1).max(2e3)).max(64),
7763
+ acceptance_criteria: z15.array(criterionSchema).min(1).max(64),
7764
+ verification_requirements: z15.array(z15.string().min(1).max(2e3)).min(1).max(64)
7765
+ }).strict(),
7766
+ z15.object({ action: z15.literal("activate") }).strict(),
7767
+ z15.object({
7768
+ action: z15.literal("update_criterion"),
7769
+ id: z15.string().min(1).max(128),
7770
+ status: z15.enum(["pending", "satisfied", "blocked"]),
7771
+ evidence_refs: z15.array(z15.string().min(1).max(256)).max(64).optional(),
7772
+ note: z15.string().max(2e3).optional()
7773
+ }).strict()
7774
+ ]);
7775
+ var taskContractTool = {
7776
+ definition: {
7777
+ name: "task_contract",
7778
+ 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.",
7779
+ category: "read",
7780
+ inputSchema: {
7781
+ type: "object",
7782
+ properties: {
7783
+ action: { type: "string", enum: ["show", "replace", "activate", "update_criterion"] },
7784
+ objective: { type: "string" },
7785
+ scope: { type: "array", items: { type: "string" } },
7786
+ constraints: { type: "array", items: { type: "string" } },
7787
+ non_goals: { type: "array", items: { type: "string" } },
7788
+ acceptance_criteria: { type: "array", items: {
7789
+ type: "object",
7790
+ properties: {
7791
+ id: { type: "string" },
7792
+ description: { type: "string" },
7793
+ required: { type: "boolean" }
7794
+ },
7795
+ required: ["description"],
7796
+ additionalProperties: false
7797
+ } },
7798
+ verification_requirements: {
7799
+ type: "array",
7800
+ minItems: 1,
7801
+ items: { type: "string" },
7802
+ description: "Required verification commands or the standard any-successful-check requirement."
7803
+ },
7804
+ id: { type: "string" },
7805
+ status: { type: "string", enum: ["pending", "satisfied", "blocked"] },
7806
+ evidence_refs: { type: "array", items: { type: "string" } },
7807
+ note: { type: "string" }
7808
+ },
7809
+ required: ["action"],
7810
+ additionalProperties: false
7811
+ }
7812
+ },
7813
+ async execute(arguments_, context) {
7814
+ const input2 = inputSchema10.parse(arguments_);
7815
+ let contract = context.session.taskContract;
7816
+ if (!contract) throw new Error("No Task Contract is active for this session.");
7817
+ if (input2.action === "replace") {
7818
+ if (contract.state !== "draft") {
7819
+ throw new Error("Only a draft Task Contract can be replaced.");
7820
+ }
7821
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7822
+ const criteria = input2.acceptance_criteria.map((item) => ({
7823
+ id: item.id ?? `criterion-${randomUUID11().slice(0, 8)}`,
7824
+ description: item.description,
7825
+ required: item.required ?? true,
7826
+ status: "pending",
7827
+ evidenceRefs: []
7828
+ }));
7829
+ if (new Set(criteria.map((item) => item.id)).size !== criteria.length) {
7830
+ throw new Error("Task Contract criterion ids must be unique.");
7831
+ }
7832
+ if (!criteria.some((item) => item.required)) {
7833
+ throw new Error("Task Contract requires at least one required acceptance criterion.");
7834
+ }
7835
+ for (const existing of contract.acceptanceCriteria.filter((item) => item.required)) {
7836
+ const retained = criteria.find((item) => item.id === existing.id);
7837
+ if (!retained || !retained.required || retained.description !== existing.description) {
7838
+ throw new Error(`Required criterion cannot be removed or weakened: ${existing.id}`);
7839
+ }
7840
+ }
7841
+ for (const existing of contract.verificationRequirements) {
7842
+ if (!input2.verification_requirements.includes(existing)) {
7843
+ throw new Error(`Verification requirement cannot be removed: ${existing}`);
7844
+ }
7845
+ }
7846
+ for (const requirement of input2.verification_requirements) {
7847
+ validateVerificationRequirement(requirement);
7848
+ }
7849
+ Object.assign(contract, {
7850
+ version: 1,
7851
+ state: "active",
7852
+ objective: input2.objective,
7853
+ scope: input2.scope,
7854
+ constraints: input2.constraints,
7855
+ nonGoals: input2.non_goals,
7856
+ acceptanceCriteria: criteria,
7857
+ verificationRequirements: input2.verification_requirements,
7858
+ createdAt: contract.createdAt,
7859
+ updatedAt: now,
7860
+ ...contract.auditBoundaryId ? { auditBoundaryId: contract.auditBoundaryId } : {}
7861
+ });
7862
+ } else if (input2.action === "activate") {
7863
+ refreshTaskContractState(contract);
7864
+ } else if (input2.action === "update_criterion") {
7865
+ if (contract.state === "draft") {
7866
+ throw new Error("Activate the draft Task Contract before updating acceptance criteria.");
7867
+ }
7868
+ const criterion2 = contract.acceptanceCriteria.find((item) => item.id === input2.id);
7869
+ if (!criterion2) throw new Error(`Unknown Task Contract criterion id: ${input2.id}`);
7870
+ const refs = [...new Set(input2.evidence_refs ?? [])];
7871
+ if (input2.status === "satisfied") {
7872
+ if (!refs.length) throw new Error("Satisfied criteria require at least one evidence_refs entry.");
7873
+ const valid = successfulContractEvidence(context.session);
7874
+ const invalid = refs.filter((ref) => !valid.has(ref));
7875
+ if (invalid.length) {
7876
+ throw new Error(`Unknown or unsuccessful evidence refs: ${invalid.join(", ")}`);
7877
+ }
7878
+ }
7879
+ criterion2.status = input2.status;
7880
+ criterion2.evidenceRefs = input2.status === "satisfied" ? refs : [];
7881
+ if (input2.note === void 0) delete criterion2.note;
7882
+ else criterion2.note = input2.note;
7883
+ refreshTaskContractState(contract);
7884
+ }
7885
+ return {
7886
+ content: formatContract(contract),
7887
+ metadata: { taskContract: structuredClone(contract) }
7888
+ };
7889
+ }
7890
+ };
7891
+ function formatContract(contract) {
7892
+ const satisfied = contract.acceptanceCriteria.filter((item) => item.status === "satisfied").length;
7893
+ const criteria = contract.acceptanceCriteria.map(
7894
+ (item) => `- [${item.status}] ${item.id}: ${item.description}`
7895
+ ).join("\n");
7896
+ return `Task Contract: ${contract.state} (${satisfied}/${contract.acceptanceCriteria.length} satisfied)
7897
+ Objective: ${contract.objective}
7898
+ ${criteria}`;
7899
+ }
7900
+ function validateVerificationRequirement(requirement) {
7901
+ if (/^Record at least one successful (.+) after the final mutation\.?$/iu.test(requirement)) return;
7902
+ if (/\b(?:api[_-]?key|authorization|password|secret|token)\s*=/iu.test(requirement)) {
7903
+ throw new Error("Verification requirements cannot contain credentials.");
7904
+ }
7905
+ if (!classifyVerificationCommand(requirement)) {
7906
+ throw new Error(`Verification requirement is not a recognized deterministic check: ${requirement}`);
7907
+ }
7908
+ }
7909
+
7910
+ // src/tools/working-memory.ts
7911
+ import { z as z16 } from "zod";
7912
+ var inputSchema11 = z16.discriminatedUnion("action", [
7913
+ z16.object({ action: z16.literal("show") }).strict(),
7914
+ z16.object({ action: z16.literal("set_goal"), value: z16.string().min(1).max(1e3) }).strict(),
7915
+ z16.object({ action: z16.literal("set_focus"), value: z16.string().min(1).max(1e3) }).strict(),
7916
+ z16.object({ action: z16.literal("add_constraint"), value: z16.string().min(1).max(1e3) }).strict(),
7917
+ z16.object({ action: z16.literal("add_decision"), value: z16.string().min(1).max(1e3) }).strict(),
7918
+ z16.object({ action: z16.literal("add_question"), value: z16.string().min(1).max(1e3) }).strict(),
7919
+ z16.object({ action: z16.literal("resolve_question"), value: z16.string().min(1).max(1e3) }).strict(),
7920
+ z16.object({ action: z16.literal("add_file"), path: z16.string().min(1).max(4e3) }).strict(),
7921
+ z16.object({ action: z16.literal("clear"), field: z16.enum(["constraints", "decisions", "openQuestions", "relevantFiles"]) }).strict()
7922
+ ]);
7923
+ var workingMemoryTool = {
7924
+ definition: {
7925
+ name: "working_memory",
7926
+ 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.",
7927
+ category: "read",
7928
+ inputSchema: jsonSchema({
7929
+ action: { type: "string", enum: [
7930
+ "show",
7931
+ "set_goal",
7932
+ "set_focus",
7933
+ "add_constraint",
7934
+ "add_decision",
7935
+ "add_question",
7936
+ "resolve_question",
7937
+ "add_file",
7938
+ "clear"
7939
+ ] },
7940
+ value: { type: "string", description: "Text for a goal, focus, constraint, decision, or question." },
7941
+ path: { type: "string", description: "Workspace-relative relevant file path." },
7942
+ field: { type: "string", enum: ["constraints", "decisions", "openQuestions", "relevantFiles"] }
7943
+ }, ["action"])
7944
+ },
7945
+ async execute(arguments_, context) {
7946
+ const input2 = inputSchema11.parse(arguments_);
7947
+ const memory = context.session.workingMemory ?? emptyWorkingMemory2();
7948
+ switch (input2.action) {
7949
+ case "show":
7950
+ break;
7951
+ case "set_goal":
7952
+ memory.goal = clean(input2.value);
7953
+ break;
7954
+ case "set_focus":
7955
+ memory.focus = clean(input2.value);
7956
+ break;
7957
+ case "add_constraint":
7958
+ pushBounded2(memory.constraints, input2.value, 12);
7959
+ break;
7960
+ case "add_decision":
7961
+ pushBounded2(memory.decisions, input2.value, 12);
7962
+ break;
7963
+ case "add_question":
7964
+ pushBounded2(memory.openQuestions, input2.value, 12);
7965
+ break;
7966
+ case "resolve_question":
7967
+ removeMatching(memory.openQuestions, input2.value);
7968
+ break;
7969
+ case "add_file": {
7970
+ const safe = await context.workspace.resolvePath(input2.path, { expect: "any" });
7971
+ pushBounded2(memory.relevantFiles, safe, 24);
7972
+ break;
7973
+ }
7974
+ case "clear":
7975
+ memory[input2.field] = [];
7976
+ break;
7977
+ }
7978
+ memory.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
7979
+ context.session.workingMemory = memory;
7980
+ return { content: render(memory), metadata: { workingMemory: { ...memory } } };
7981
+ }
7982
+ };
7983
+ function emptyWorkingMemory2() {
7984
+ return {
7985
+ goal: "",
7986
+ focus: "",
7987
+ constraints: [],
7988
+ decisions: [],
7989
+ openQuestions: [],
7990
+ relevantFiles: [],
7991
+ lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
7992
+ };
7993
+ }
7994
+ function pushBounded2(values, value, limit) {
7995
+ const normalized = clean(value);
7996
+ const existing = values.indexOf(normalized);
7997
+ if (existing >= 0) values.splice(existing, 1);
7998
+ values.push(normalized);
7999
+ if (values.length > limit) values.splice(0, values.length - limit);
8000
+ }
8001
+ function removeMatching(values, value) {
8002
+ const query = value.toLocaleLowerCase().trim();
8003
+ for (let index = values.length - 1; index >= 0; index -= 1) {
8004
+ const candidate = values[index];
8005
+ if (candidate.toLocaleLowerCase() === query || candidate.toLocaleLowerCase().includes(query)) {
8006
+ values.splice(index, 1);
8007
+ }
8008
+ }
8009
+ }
8010
+ function clean(value) {
8011
+ 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);
8012
+ }
8013
+ function render(memory) {
8014
+ return [
8015
+ `Goal: ${memory.goal || "(none)"}`,
8016
+ `Focus: ${memory.focus || "(none)"}`,
8017
+ `Constraints: ${memory.constraints.length ? memory.constraints.join(" \xB7 ") : "(none)"}`,
8018
+ `Decisions: ${memory.decisions.length ? memory.decisions.join(" \xB7 ") : "(none)"}`,
8019
+ `Open questions: ${memory.openQuestions.length ? memory.openQuestions.join(" \xB7 ") : "(none)"}`,
8020
+ `Relevant files: ${memory.relevantFiles.length ? memory.relevantFiles.join(" \xB7 ") : "(none)"}`
8021
+ ].join("\n");
8022
+ }
8023
+
7051
8024
  // src/tools/index.ts
7052
8025
  function createDefaultToolRegistry(_options = {}) {
7053
8026
  return new ToolRegistry([
7054
8027
  readFileTool,
8028
+ readToolArtifactTool,
7055
8029
  listFilesTool,
7056
8030
  searchCodeTool,
7057
8031
  writeFileTool,
@@ -7059,6 +8033,7 @@ function createDefaultToolRegistry(_options = {}) {
7059
8033
  shellTool,
7060
8034
  gitTool,
7061
8035
  taskTool,
8036
+ taskContractTool,
7062
8037
  workingMemoryTool
7063
8038
  ]);
7064
8039
  }
@@ -7098,11 +8073,19 @@ ${workspaceRules}` : ""}`;
7098
8073
  }
7099
8074
  function buildSessionStatePrompt(session) {
7100
8075
  const tasks = session.tasks.length ? session.tasks.map((task) => `- [${task.status}] ${task.title}`).join("\n") : "- No active plan.";
8076
+ const contract = session.taskContract && session.taskContract.state !== "satisfied" ? `
8077
+
8078
+ Task Contract (${session.taskContract.state}):
8079
+ Objective: ${session.taskContract.objective}
8080
+ ${session.taskContract.acceptanceCriteria.map(
8081
+ (item) => `- [${item.status}] ${item.id}: ${item.description}`
8082
+ ).join("\n")}
8083
+ Use task_contract to activate or update acceptance. A satisfied criterion requires successful tool evidence.` : "";
7101
8084
  return `<session-state scope="session" authorization="none">
7102
8085
  This is mutable execution state, not a permission grant or higher-priority instruction.
7103
8086
 
7104
8087
  Current saved plan:
7105
- ${tasks}
8088
+ ${tasks}${contract}
7106
8089
  </session-state>`;
7107
8090
  }
7108
8091
  function buildRetrievedContext(packed, mentions, primaryRoot, roots = [primaryRoot]) {
@@ -7170,136 +8153,328 @@ function escapeAttribute3(value) {
7170
8153
  })[character] ?? character);
7171
8154
  }
7172
8155
 
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
- };
8156
+ // src/agent/tool-recovery.ts
8157
+ import { createHash as createHash11 } from "node:crypto";
8158
+ var RETRY_BUDGET = {
8159
+ schema_input: 3,
8160
+ unknown_tool: 2,
8161
+ permission_denied: 0,
8162
+ command_exit: 3,
8163
+ timeout: 2,
8164
+ cancelled: 0,
8165
+ hook: 1,
8166
+ execution: 3,
8167
+ contract_required: 2
8168
+ };
8169
+ var REPAIR_HINT = {
8170
+ schema_input: "Correct the arguments to match the tool schema.",
8171
+ unknown_tool: "Choose a tool exposed for this turn.",
8172
+ permission_denied: "Do not retry unless the user or configuration changes permission.",
8173
+ command_exit: "Inspect the exit output, change the command or fix the cause, then retry once.",
8174
+ timeout: "Narrow the operation or increase its allowed timeout.",
8175
+ cancelled: "Stop work and preserve the current state.",
8176
+ hook: "Fix the hook failure before relying on the tool result.",
8177
+ execution: "Use the error detail to change the inputs or approach.",
8178
+ contract_required: "Activate the Task Contract before any workspace mutation."
8179
+ };
8180
+ var ToolRecoveryController = class {
8181
+ signatures = /* @__PURE__ */ new Map();
8182
+ classFailures = /* @__PURE__ */ new Map();
8183
+ toolClasses = /* @__PURE__ */ new Map();
8184
+ preflight(call) {
8185
+ const callKey = callSignature(call);
8186
+ const signatureState = this.signatures.get(callKey);
8187
+ if (signatureState && (signatureState.failures >= 2 || !isRetryable(signatureState.failureClass))) {
8188
+ return this.receipt(
8189
+ call,
8190
+ signatureState.failureClass,
8191
+ signatureState.failures + 1,
8192
+ true
8193
+ );
8194
+ }
8195
+ const lastClass = this.toolClasses.get(call.name);
8196
+ if (lastClass && (this.classFailures.get(lastClass) ?? 0) >= RETRY_BUDGET[lastClass] && RETRY_BUDGET[lastClass] > 0) {
8197
+ return this.receipt(call, lastClass, (signatureState?.failures ?? 0) + 1, true);
8198
+ }
8199
+ return void 0;
7203
8200
  }
7204
- if (!files.length) {
7205
- return {
7206
- status: "no_changes",
7207
- changedFiles: [],
7208
- checks: [],
7209
- detail: "No workspace files changed in this run."
7210
- };
8201
+ recordFailure(call, failureClass) {
8202
+ const callKey = callSignature(call);
8203
+ const current = this.signatures.get(callKey);
8204
+ const failures = current?.failureClass === failureClass ? current.failures + 1 : 1;
8205
+ this.signatures.set(callKey, { failureClass, failures });
8206
+ this.toolClasses.set(call.name, failureClass);
8207
+ this.classFailures.set(failureClass, (this.classFailures.get(failureClass) ?? 0) + 1);
8208
+ return this.receipt(
8209
+ call,
8210
+ failureClass,
8211
+ failures,
8212
+ failures >= 2 || !isRetryable(failureClass)
8213
+ );
7211
8214
  }
7212
- const latestByCommand = /* @__PURE__ */ new Map();
7213
- for (const item of evidence) {
7214
- if (item.changeSequence === currentChangeSequence) {
7215
- latestByCommand.set(item.commandKey, item);
7216
- }
8215
+ recordSuccess(call) {
8216
+ this.signatures.delete(callSignature(call));
8217
+ this.toolClasses.delete(call.name);
7217
8218
  }
7218
- const checks = [...latestByCommand.values()].map(publicEvidence);
7219
- if (!checks.length) {
8219
+ receipt(call, failureClass, attempt, circuitOpen) {
8220
+ const budget = RETRY_BUDGET[failureClass];
8221
+ const consumed = this.classFailures.get(failureClass) ?? 0;
7220
8222
  return {
7221
- status: "unverified",
7222
- changedFiles: files,
7223
- checks,
7224
- detail: `No successful verification was recorded after the last change to ${fileCount(files.length)}.`
8223
+ class: failureClass,
8224
+ retryable: isRetryable(failureClass) && consumed < budget && !circuitOpen,
8225
+ repairHint: REPAIR_HINT[failureClass],
8226
+ attempt,
8227
+ remaining: Math.max(0, budget - consumed),
8228
+ circuitOpen,
8229
+ signature: failureSignature(call, failureClass)
7225
8230
  };
7226
8231
  }
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
- };
8232
+ };
8233
+ function classifyToolFailure(result, fallback = "execution") {
8234
+ if (result.metadata?.failureClass && isFailureClass(result.metadata.failureClass)) {
8235
+ return result.metadata.failureClass;
8236
+ }
8237
+ if (result.metadata?.aborted === true) return "cancelled";
8238
+ if (result.metadata?.timedOut === true) return "timeout";
8239
+ if (result.metadata?.hookError) return "hook";
8240
+ if (typeof result.metadata?.exitCode === "number" && result.metadata.exitCode !== 0) {
8241
+ return "command_exit";
8242
+ }
8243
+ return fallback;
8244
+ }
8245
+ function formatFailureReceipt(receipt) {
8246
+ return `Failure: ${receipt.class}; attempt ${receipt.attempt}; ${receipt.remaining} retries remain; circuit ${receipt.circuitOpen ? "open" : "closed"}. Repair: ${receipt.repairHint}`;
8247
+ }
8248
+ function isRetryable(failureClass) {
8249
+ return RETRY_BUDGET[failureClass] > 0;
8250
+ }
8251
+ function callSignature(call) {
8252
+ return createHash11("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}`).digest("hex");
8253
+ }
8254
+ function failureSignature(call, failureClass) {
8255
+ return createHash11("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}\0${failureClass}`).digest("hex");
8256
+ }
8257
+ function redact(value, key = "") {
8258
+ if (/api[_-]?key|authorization|cookie|password|secret|token/i.test(key)) return "[redacted]";
8259
+ if (Array.isArray(value)) return value.map((item) => redact(item));
8260
+ if (value && typeof value === "object") {
8261
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([name, item]) => [name, redact(item, name)]));
8262
+ }
8263
+ return value;
8264
+ }
8265
+ function stableJson(value) {
8266
+ return JSON.stringify(value) ?? String(value);
8267
+ }
8268
+ function isFailureClass(value) {
8269
+ return typeof value === "string" && Object.hasOwn(RETRY_BUDGET, value);
8270
+ }
8271
+
8272
+ // src/agent/tool-output.ts
8273
+ import stripAnsi from "strip-ansi";
8274
+ var MIN_TOOL_OUTPUT_TOKENS = 1024;
8275
+ var MAX_TOOL_OUTPUT_TOKENS = 8192;
8276
+ async function protectToolOutput(options) {
8277
+ const safe = sanitizeToolOutput(options.content);
8278
+ const sanitized = redactToolOutput(safe.text);
8279
+ const estimatedTokens = estimateToolOutputTokens(sanitized.text);
8280
+ const budgetTokens = clamp2(options.budgetTokens, MIN_TOOL_OUTPUT_TOKENS, MAX_TOOL_OUTPUT_TOKENS);
8281
+ const base = {
8282
+ originalChars: options.content.length,
8283
+ originalBytes: Buffer.byteLength(options.content),
8284
+ estimatedTokens,
8285
+ budgetTokens,
8286
+ truncated: estimatedTokens > budgetTokens,
8287
+ redacted: sanitized.redacted,
8288
+ sanitized: safe.sanitized
8289
+ };
8290
+ if (estimatedTokens <= budgetTokens) return { content: sanitized.text, metadata: base };
8291
+ let archived;
8292
+ try {
8293
+ archived = await options.artifacts.archive(
8294
+ options.sessionId,
8295
+ options.toolCallId,
8296
+ sanitized.text,
8297
+ { redacted: sanitized.redacted }
8298
+ );
8299
+ } catch {
8300
+ archived = { stored: false, reason: "storage_error" };
7235
8301
  }
8302
+ const artifact = archived.stored ? archived.artifact : void 0;
8303
+ const artifactUnavailable = archived.stored ? void 0 : archived.reason;
8304
+ const metadata = {
8305
+ ...base,
8306
+ ...artifact ? {
8307
+ artifact: {
8308
+ toolCallId: artifact.toolCallId,
8309
+ sha256: artifact.sha256,
8310
+ bytes: artifact.bytes,
8311
+ createdAt: artifact.createdAt,
8312
+ expiresAt: artifact.expiresAt
8313
+ }
8314
+ } : artifactUnavailable ? { artifactUnavailable } : {}
8315
+ };
8316
+ const receipt = formatReceipt(options, metadata, "");
8317
+ const previewBudget = Math.max(0, budgetTokens - estimateToolOutputTokens(receipt));
8318
+ const preview = boundedPreview(sanitized.text, previewBudget);
7236
8319
  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)}.`
8320
+ content: formatReceipt(options, metadata, preview),
8321
+ metadata
7241
8322
  };
7242
8323
  }
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>`;
8324
+ function dynamicToolOutputBudget(contextWindowTokens, activeContextTokens, remainingSessionTokens) {
8325
+ const contextHeadroom = Math.max(0, contextWindowTokens - activeContextTokens);
8326
+ const sessionHeadroom = Math.max(0, remainingSessionTokens);
8327
+ const budget = Math.min(
8328
+ Math.floor(contextHeadroom * 0.35),
8329
+ Math.floor(sessionHeadroom * 0.12)
8330
+ );
8331
+ return clamp2(budget, MIN_TOOL_OUTPUT_TOKENS, MAX_TOOL_OUTPUT_TOKENS);
8332
+ }
8333
+ function estimateToolOutputTokens(value) {
8334
+ let tokens2 = 0;
8335
+ for (const character of value) {
8336
+ tokens2 += tokenCost(character);
7251
8337
  }
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>`;
8338
+ return Math.max(1, Math.ceil(tokens2));
7257
8339
  }
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;
8340
+ function formatReceipt(options, metadata, preview) {
8341
+ const lines = [
8342
+ "[Oversized tool output bounded for the model transcript]",
8343
+ `tool: ${boundedInlineByTokens(options.tool, 64)}`,
8344
+ `tool-call-id: ${boundedInlineByTokens(options.toolCallId, 96)}`,
8345
+ `status: ${options.ok ? "success" : "failure"}`,
8346
+ `original-output: ${metadata.originalChars} chars, ${metadata.originalBytes} bytes, ~${metadata.estimatedTokens} tokens`,
8347
+ `model-budget: ${metadata.budgetTokens} tokens (head + tail preview)`,
8348
+ ...metadata.redacted ? ["sensitive-values: redacted before display and retention"] : [],
8349
+ ...preservedSignals(options.metadata),
8350
+ ...metadata.artifact ? [
8351
+ `artifact: session-scoped; sha256 ${metadata.artifact.sha256}; ${metadata.artifact.bytes} bytes`,
8352
+ `read-back: read_tool_artifact({"sha256":"${metadata.artifact.sha256}","start_line":1,"max_lines":200}) before ${metadata.artifact.expiresAt}`
8353
+ ] : [`artifact: unavailable (${metadata.artifactUnavailable ?? "unknown"}); rerun the tool with a narrower result if more detail is needed`],
8354
+ "preview:",
8355
+ preview
8356
+ ];
8357
+ return lines.join("\n");
8358
+ }
8359
+ function preservedSignals(metadata) {
8360
+ const lines = [];
8361
+ if (typeof metadata.exitCode === "number") lines.push(`exit-code: ${metadata.exitCode}`);
8362
+ if (metadata.timedOut === true) lines.push("timed-out: true");
8363
+ if (metadata.sourceTruncated === true) {
8364
+ lines.push("source-truncated: true; the producing tool reached its capture limit before this firewall");
8365
+ }
8366
+ const changedFiles = metadata.changedFiles;
8367
+ if (Array.isArray(changedFiles)) {
8368
+ const allPaths = changedFiles.filter((path) => typeof path === "string");
8369
+ const paths = allPaths.slice(0, 3).map((path) => boundedInlineByTokens(path, 24));
8370
+ if (paths.length) {
8371
+ lines.push(`changed-files: ${paths.join(", ")}${allPaths.length > paths.length ? ` (+${allPaths.length - paths.length} more in metadata)` : ""}`);
8372
+ }
8373
+ }
8374
+ const failure = metadata.failure;
8375
+ if (isFailureReceipt(failure)) {
8376
+ lines.push(`failure-class: ${failure.class}; attempt ${failure.attempt}; ${failure.remaining} retries remain`);
8377
+ }
8378
+ return lines;
8379
+ }
8380
+ function isFailureReceipt(value) {
8381
+ return typeof value === "object" && value !== null && typeof value.class === "string" && typeof value.attempt === "number" && typeof value.remaining === "number";
8382
+ }
8383
+ function redactToolOutput(value) {
8384
+ let redacted = false;
8385
+ const replace = (expression, replacement) => {
8386
+ expression.lastIndex = 0;
8387
+ if (!expression.test(value)) return;
8388
+ redacted = true;
8389
+ expression.lastIndex = 0;
8390
+ value = value.replace(expression, replacement);
8391
+ };
8392
+ replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/gu, "[redacted-secret]");
8393
+ replace(/\b(authorization\s*:\s*(?:bearer\s+|basic\s+)?)\S+/giu, "$1[redacted]");
8394
+ replace(/\b((?:api[_-]?key|access[_-]?token|authorization|cookie|password|client[_-]?secret)\s*[:=]\s*)(?:"[A-Za-z0-9+/_=-]{8,}"|'[A-Za-z0-9+/_=-]{8,}'|[A-Za-z0-9+/_=-]{8,})/giu, "$1[redacted]");
8395
+ replace(/(--(?:api[_-]?key|access[_-]?token|password|client[_-]?secret)(?:=|\s+))[A-Za-z0-9+/_=-]{8,}/giu, "$1[redacted]");
8396
+ return { text: value, redacted };
8397
+ }
8398
+ function sanitizeToolOutput(value) {
8399
+ const text = stripAnsi(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "");
8400
+ return { text, sanitized: text !== value };
8401
+ }
8402
+ function sliceStartByTokens(value, budget) {
8403
+ let used = 0;
8404
+ let end = 0;
8405
+ for (const character of value) {
8406
+ const cost = tokenCost(character);
8407
+ if (used + cost > budget) break;
8408
+ used += cost;
8409
+ end += character.length;
7267
8410
  }
7268
- return classifySingleVerificationCommand(normalized);
8411
+ return value.slice(0, end);
7269
8412
  }
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;
8413
+ function sliceEndByTokens(value, budget) {
8414
+ let used = 0;
8415
+ let start = value.length;
8416
+ while (start > 0) {
8417
+ let next = start - 1;
8418
+ const code = value.charCodeAt(next);
8419
+ if (code >= 56320 && code <= 57343 && next > 0) {
8420
+ const previous = value.charCodeAt(next - 1);
8421
+ if (previous >= 55296 && previous <= 56319) next -= 1;
8422
+ }
8423
+ const character = value.slice(next, start);
8424
+ const cost = tokenCost(character);
8425
+ if (used + cost > budget) break;
8426
+ used += cost;
8427
+ start = next;
8428
+ }
8429
+ return value.slice(start);
8430
+ }
8431
+ function boundedPreview(value, budget) {
8432
+ if (budget <= 0) return "";
8433
+ if (estimateToolOutputTokens(value) <= budget) return value;
8434
+ let low = 0;
8435
+ let high = budget;
8436
+ let best = "";
8437
+ while (low <= high) {
8438
+ const contentBudget = Math.floor((low + high) / 2);
8439
+ const headBudget = Math.ceil(contentBudget * 0.6);
8440
+ const tailBudget = Math.max(0, contentBudget - headBudget);
8441
+ const head = sliceStartByTokens(value, headBudget);
8442
+ const tail = sliceEndByTokens(value, tailBudget);
8443
+ const preview = `${head}
8444
+ \u2026 ${Math.max(0, value.length - head.length - tail.length)} chars omitted \u2026
8445
+ ${tail}`;
8446
+ if (estimateToolOutputTokens(preview) <= budget) {
8447
+ best = preview;
8448
+ low = contentBudget + 1;
8449
+ } else {
8450
+ high = contentBudget - 1;
8451
+ }
8452
+ }
8453
+ return best || sliceStartByTokens(value, budget);
7279
8454
  }
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
- };
8455
+ function tokenCost(character) {
8456
+ const code = character.codePointAt(0) ?? 0;
8457
+ if (isCjkCharacter(character) || code > 127) return 2;
8458
+ return /[A-Z0-9\p{P}\p{S}]/u.test(character) ? 0.5 : 0.25;
7288
8459
  }
7289
- function normalizeCommand2(command2) {
7290
- return command2.trim().replace(/[\t ]+/gu, " ");
8460
+ function isCjkCharacter(character) {
8461
+ const code = character.codePointAt(0) ?? 0;
8462
+ return code >= 11904 && code <= 40959 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 131072 && code <= 195103;
7291
8463
  }
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);
8464
+ function clamp2(value, minimum, maximum) {
8465
+ return Math.max(minimum, Math.min(maximum, value));
7294
8466
  }
7295
- function fileCount(count) {
7296
- return `${count} workspace ${count === 1 ? "file" : "files"}`;
8467
+ function boundedInlineByTokens(value, maxTokens) {
8468
+ const normalized = value.replace(/[\r\n\t]+/gu, " ").trim();
8469
+ if (estimateToolOutputTokens(normalized) <= maxTokens) return normalized;
8470
+ const suffix = "\u2026";
8471
+ return `${sliceStartByTokens(normalized, Math.max(0, maxTokens - tokenCost(suffix)))}${suffix}`;
7297
8472
  }
7298
8473
 
7299
8474
  // src/agent/rules.ts
7300
8475
  import { existsSync as existsSync2 } from "node:fs";
7301
- import { join as join13 } from "node:path";
7302
- import { lstat as lstat15, readFile as readFile11 } from "node:fs/promises";
8476
+ import { join as join14 } from "node:path";
8477
+ import { lstat as lstat16, readFile as readFile12 } from "node:fs/promises";
7303
8478
  var workspaceRulePaths = [
7304
8479
  "AGENTS.md",
7305
8480
  "CLAUDE.md",
@@ -7308,14 +8483,14 @@ var workspaceRulePaths = [
7308
8483
  ];
7309
8484
  async function discoverWorkspaceRules(workspace, maxChars = 12e4) {
7310
8485
  const workspaceAccess = new WorkspaceAccess([workspace]);
7311
- const activeProjectRules = join13(resolveProjectNamespaceSync(workspace).active, "rules.md");
8486
+ const activeProjectRules = join14(resolveProjectNamespaceSync(workspace).active, "rules.md");
7312
8487
  const projectCandidates = [
7313
- ...workspaceRulePaths.slice(0, 3).map((path) => join13(workspace, path)),
8488
+ ...workspaceRulePaths.slice(0, 3).map((path) => join14(workspace, path)),
7314
8489
  activeProjectRules,
7315
- ...workspaceRulePaths.slice(3).map((path) => join13(workspace, path))
8490
+ ...workspaceRulePaths.slice(3).map((path) => join14(workspace, path))
7316
8491
  ];
7317
8492
  const candidates = [
7318
- { path: join13(resolveHomeNamespace(), "rules.md"), scope: "user" },
8493
+ { path: join14(resolveHomeNamespace(), "rules.md"), scope: "user" },
7319
8494
  ...projectCandidates.map((path) => ({
7320
8495
  path,
7321
8496
  scope: "workspace"
@@ -7326,13 +8501,13 @@ async function discoverWorkspaceRules(workspace, maxChars = 12e4) {
7326
8501
  for (const candidate of candidates) {
7327
8502
  if (remaining <= 0 || !existsSync2(candidate.path)) continue;
7328
8503
  try {
7329
- const info = await lstat15(candidate.path);
8504
+ const info = await lstat16(candidate.path);
7330
8505
  if (!info.isFile() || info.size > 1e6) continue;
7331
8506
  if (candidate.scope === "workspace") {
7332
8507
  const safePath = await workspaceAccess.resolvePath(candidate.path, { expect: "file" });
7333
8508
  if (safePath !== candidate.path) continue;
7334
8509
  }
7335
- const raw = await readFile11(candidate.path, "utf8");
8510
+ const raw = await readFile12(candidate.path, "utf8");
7336
8511
  if (raw.includes("\0")) continue;
7337
8512
  const content = raw.slice(0, remaining);
7338
8513
  rules.push({
@@ -7364,7 +8539,7 @@ function escapeAttribute4(value) {
7364
8539
  }
7365
8540
 
7366
8541
  // src/context/context-sources.ts
7367
- import { readFile as readFile12, stat as stat9 } from "node:fs/promises";
8542
+ import { readFile as readFile13, stat as stat9 } from "node:fs/promises";
7368
8543
  var MAX_SOURCE_CHARS = 6e4;
7369
8544
  var MAX_PINNED_CHARS = 16e4;
7370
8545
  var MAX_CONTEXT_SOURCES = 32;
@@ -7378,7 +8553,7 @@ async function pinContextSource(session, workspace, requested) {
7378
8553
  const resolved = await workspace.resolvePath(requested, { expect: "file" });
7379
8554
  const info = await stat9(resolved);
7380
8555
  const alias = workspace.display(resolved);
7381
- const tokens2 = estimateTokens3((await readFile12(resolved, "utf8")).slice(0, MAX_SOURCE_CHARS));
8556
+ const tokens2 = estimateTokens3((await readFile13(resolved, "utf8")).slice(0, MAX_SOURCE_CHARS));
7382
8557
  const list2 = sources(session);
7383
8558
  const existing = list2.find((source2) => source2.path === alias);
7384
8559
  if (existing) {
@@ -7428,7 +8603,7 @@ async function resolvePinnedContent(session, workspace) {
7428
8603
  if (remaining <= 0) break;
7429
8604
  try {
7430
8605
  const safe = await workspace.resolvePath(source.path, { expect: "file" });
7431
- const raw = await readFile12(safe, "utf8");
8606
+ const raw = await readFile13(safe, "utf8");
7432
8607
  const capped = raw.slice(0, Math.min(MAX_SOURCE_CHARS, remaining));
7433
8608
  source.tokens = estimateTokens3(capped);
7434
8609
  resolved.push({
@@ -7476,6 +8651,7 @@ var AgentRunner = class {
7476
8651
  contextEngine;
7477
8652
  tools;
7478
8653
  sessionStore;
8654
+ toolArtifactStore;
7479
8655
  checkpointStore;
7480
8656
  workspace;
7481
8657
  hooks;
@@ -7497,6 +8673,7 @@ var AgentRunner = class {
7497
8673
  contextEngine: this.contextEngine
7498
8674
  });
7499
8675
  this.sessionStore = options.sessionStore ?? new SessionStore(this.workspace.primaryRoot);
8676
+ this.toolArtifactStore = options.toolArtifactStore ?? new ToolArtifactStore(this.workspace.primaryRoot);
7500
8677
  this.checkpointStore = options.checkpointStore ?? new CheckpointStore(this.workspace);
7501
8678
  this.hooks = new HookRunner(options.config.hooks, this.workspace);
7502
8679
  this.contextManager = options.contextManager ?? new ContextManager(options.config);
@@ -7538,6 +8715,8 @@ var AgentRunner = class {
7538
8715
  const changeSequenceAtStart = this.changeSequence;
7539
8716
  const runChangedFiles = /* @__PURE__ */ new Set();
7540
8717
  const verificationEvidence = [];
8718
+ const toolRecovery = new ToolRecoveryController();
8719
+ let activeRunContract = this.session.taskContract;
7541
8720
  let mutationTracking = "complete";
7542
8721
  let completionRecoveryAttempted = false;
7543
8722
  const recordExecution = (call, result) => {
@@ -7556,12 +8735,21 @@ var AgentRunner = class {
7556
8735
  );
7557
8736
  if (evidence) verificationEvidence.push(evidence);
7558
8737
  };
7559
- const completionReport = () => buildRunCompletion(
7560
- runChangedFiles,
7561
- verificationEvidence,
7562
- this.changeSequence,
7563
- mutationTracking
7564
- );
8738
+ const completionReport = () => {
8739
+ const completion = buildRunCompletion(
8740
+ runChangedFiles,
8741
+ verificationEvidence,
8742
+ this.changeSequence,
8743
+ mutationTracking,
8744
+ activeRunContract,
8745
+ this.session.audit ?? []
8746
+ );
8747
+ if (completion.acceptance && activeRunContract && activeRunContract.state !== "draft") {
8748
+ activeRunContract.state = completion.acceptance.state;
8749
+ activeRunContract.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8750
+ }
8751
+ return completion;
8752
+ };
7565
8753
  const finishRun = async (reason, completion = completionReport()) => {
7566
8754
  this.session.lastRun = {
7567
8755
  ...completion,
@@ -7569,11 +8757,16 @@ var AgentRunner = class {
7569
8757
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
7570
8758
  };
7571
8759
  await this.persist();
8760
+ const finalContract = activeRunContract && structuredClone(activeRunContract);
8761
+ if (finalContract && finalContract.state !== "satisfied") {
8762
+ await emit({ type: "contract", contract: finalContract });
8763
+ }
7572
8764
  await emit({ type: "done", reason, completion });
7573
8765
  return this.session;
7574
8766
  };
7575
8767
  try {
7576
8768
  throwIfAborted(options.signal);
8769
+ await this.reconcileToolArtifacts();
7577
8770
  if (this.session.messages.length === 0 && this.session.title === "New session") {
7578
8771
  this.session.title = titleFromInput(request);
7579
8772
  }
@@ -7613,10 +8806,22 @@ var AgentRunner = class {
7613
8806
  const turnDirective = buildTurnDirective(request, {
7614
8807
  agents: Boolean(this.config.agents?.enabled)
7615
8808
  });
8809
+ const contractEnabled = shouldUseTaskContract(
8810
+ request,
8811
+ turnDirective.intent,
8812
+ this.session.taskContract
8813
+ );
8814
+ if (contractEnabled && (!this.session.taskContract || this.session.taskContract.state === "satisfied")) {
8815
+ this.session.taskContract = createDraftTaskContract(request, this.session.audit?.at(-1)?.id);
8816
+ await this.persist();
8817
+ await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
8818
+ }
8819
+ activeRunContract = contractEnabled ? this.session.taskContract : void 0;
7616
8820
  const promptSections = [
7617
8821
  `intent:${turnDirective.intent}`,
7618
8822
  ...workspaceRules ? ["rules"] : [],
7619
8823
  ...this.session.workingMemory ? ["working-memory"] : [],
8824
+ ...contractEnabled ? ["task-contract"] : [],
7620
8825
  ...this.session.contextSummary ? ["session-summary"] : [],
7621
8826
  ...!trivialTurn ? [`context:${packed.engine}`] : [],
7622
8827
  ...packed.text ? [`code:${packed.engine}`] : [],
@@ -7668,9 +8873,13 @@ var AgentRunner = class {
7668
8873
  contextBudget
7669
8874
  );
7670
8875
  const availableTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
7671
- const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(
7672
- options.askMode ? this.tools.definitions().filter((tool) => tool.category === "read") : this.tools.definitions()
8876
+ const visibleTools = visibleToolDefinitions(
8877
+ this.tools,
8878
+ options.askMode === true,
8879
+ contractEnabled,
8880
+ this.hasReadableToolArtifact()
7673
8881
  );
8882
+ const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(visibleTools);
7674
8883
  if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
7675
8884
  return finishRun("token_budget");
7676
8885
  }
@@ -7678,8 +8887,7 @@ var AgentRunner = class {
7678
8887
  this.config.model.maxTokens ?? 8192,
7679
8888
  availableTokens - estimatedInputTokens
7680
8889
  ));
7681
- const visibleTools = options.askMode ? this.tools.definitions().filter((tool) => tool.category === "read") : this.tools.definitions();
7682
- const assistantId = randomUUID10();
8890
+ const assistantId = randomUUID12();
7683
8891
  const response = await this.completeModel(
7684
8892
  messages,
7685
8893
  visibleTools,
@@ -7722,9 +8930,17 @@ var AgentRunner = class {
7722
8930
  return finishRun("token_budget");
7723
8931
  }
7724
8932
  if (response.toolCalls.length) {
8933
+ const visibleToolNames = new Set(visibleTools.map((tool) => tool.name));
7725
8934
  for (const call of response.toolCalls) {
7726
8935
  throwIfAborted(options.signal);
7727
- const result = await this.executeTool(call, options, emit);
8936
+ const result = await this.executeTool(
8937
+ call,
8938
+ options,
8939
+ emit,
8940
+ toolRecovery,
8941
+ visibleToolNames
8942
+ );
8943
+ if (call.name === "task_contract") activeRunContract = this.session.taskContract;
7728
8944
  recordExecution(call, result);
7729
8945
  this.session.messages.push(message("tool", result.content, {
7730
8946
  toolCallId: result.toolCallId,
@@ -7753,8 +8969,11 @@ Review these results, correct any failures if needed, then provide the final ans
7753
8969
  continue;
7754
8970
  }
7755
8971
  const completion = completionReport();
7756
- if (this.config.agent.autoVerify && (completion.status === "unverified" || completion.status === "verification_failed") && !completionRecoveryAttempted && turn < maxTurns) {
8972
+ if ((this.config.agent.autoVerify || Boolean(completion.acceptance)) && (completion.status === "unverified" || completion.status === "verification_failed") && !completionRecoveryAttempted && turn < maxTurns) {
7757
8973
  completionRecoveryAttempted = true;
8974
+ if (activeRunContract) {
8975
+ await emit({ type: "contract", contract: structuredClone(activeRunContract) });
8976
+ }
7758
8977
  this.session.messages.push(message("user", completionRecoveryDirective(completion)));
7759
8978
  await this.persist();
7760
8979
  await this.runAfterTurnHook(turn, [], options.signal);
@@ -7822,10 +9041,29 @@ ${input2}`
7822
9041
  if (final) return final;
7823
9042
  return { content, toolCalls: [] };
7824
9043
  }
7825
- async executeTool(call, options, emit) {
9044
+ async executeTool(call, options, emit, recovery = new ToolRecoveryController(), visibleToolNames) {
9045
+ const preflight = recovery.preflight(call);
9046
+ if (preflight) {
9047
+ const result = await this.protectToolResult(
9048
+ failedResult(call, "Tool call rejected by the recovery circuit.", preflight)
9049
+ );
9050
+ this.recordToolResult(result);
9051
+ await emit({ type: "tool_result", result });
9052
+ return result;
9053
+ }
9054
+ if (visibleToolNames && !visibleToolNames.has(call.name)) {
9055
+ const receipt = recovery.recordFailure(call, "unknown_tool");
9056
+ const result = await this.protectToolResult(
9057
+ failedResult(call, `Tool is not exposed for this turn: ${call.name}`, receipt)
9058
+ );
9059
+ this.recordToolResult(result);
9060
+ await emit({ type: "tool_result", result });
9061
+ return result;
9062
+ }
7826
9063
  const tool = this.tools.get(call.name);
7827
9064
  if (!tool) {
7828
- const result = failedResult(call, `Unknown tool: ${call.name}`);
9065
+ const receipt = recovery.recordFailure(call, "unknown_tool");
9066
+ const result = await this.protectToolResult(failedResult(call, `Unknown tool: ${call.name}`, receipt));
7829
9067
  this.recordToolResult(result);
7830
9068
  await emit({ type: "tool_result", result });
7831
9069
  return result;
@@ -7836,7 +9074,18 @@ ${input2}`
7836
9074
  tool.permissionCategories?.(call.arguments) ?? [tool.definition.category]
7837
9075
  );
7838
9076
  } catch (error) {
7839
- const result = failedResult(call, formatToolError(error));
9077
+ const failureClass = classifyThrownToolFailure(error, options.signal);
9078
+ const receipt = recovery.recordFailure(call, failureClass);
9079
+ const result = await this.protectToolResult(failedResult(call, formatToolError(error), receipt));
9080
+ this.recordToolResult(result, tool.definition.category);
9081
+ await emit({ type: "tool_result", result });
9082
+ return result;
9083
+ }
9084
+ if (categories.some((category) => category !== "read") && this.session.taskContract?.state === "draft") {
9085
+ const receipt = recovery.recordFailure(call, "contract_required");
9086
+ const result = await this.protectToolResult(
9087
+ failedResult(call, "Potentially mutating work is paused until the draft Task Contract is activated.", receipt)
9088
+ );
7840
9089
  this.recordToolResult(result, tool.definition.category);
7841
9090
  await emit({ type: "tool_result", result });
7842
9091
  return result;
@@ -7844,7 +9093,10 @@ ${input2}`
7844
9093
  for (const category of categories) {
7845
9094
  const allowed = await this.authorize(call, category, options, emit);
7846
9095
  if (!allowed) {
7847
- const result = failedResult(call, `Permission denied for ${category} operation.`);
9096
+ const receipt = recovery.recordFailure(call, "permission_denied");
9097
+ const result = await this.protectToolResult(
9098
+ failedResult(call, `Permission denied for ${category} operation.`, receipt)
9099
+ );
7848
9100
  this.recordToolResult(result, category);
7849
9101
  await emit({ type: "tool_result", result });
7850
9102
  return result;
@@ -7858,6 +9110,7 @@ ${input2}`
7858
9110
  workspace: this.workspace,
7859
9111
  session: this.session,
7860
9112
  contextEngine: this.contextEngine,
9113
+ toolArtifactStore: this.toolArtifactStore,
7861
9114
  emit,
7862
9115
  ...options.signal ? { signal: options.signal } : {}
7863
9116
  };
@@ -7891,30 +9144,48 @@ ${input2}`
7891
9144
  } catch (error) {
7892
9145
  afterHookError = toError(error);
7893
9146
  }
7894
- const result = {
7895
- toolCallId: call.id,
7896
- name: call.name,
7897
- ok: execution.ok !== false && !afterHookError,
7898
- content: truncateToolOutput(afterHookError ? `${execution.content}
9147
+ let completeContent = afterHookError ? `${execution.content}
7899
9148
 
7900
- Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : execution.content),
7901
- metadata: {
7902
- ...execution.metadata ?? {},
7903
- ...changedFiles.length ? { changedFiles } : {},
7904
- ...checkpointId ? { checkpointId } : {},
7905
- ...beforeHooks.length || afterHooks.length ? { hooks: { before: beforeHooks.length, after: afterHooks.length } } : {},
7906
- ...afterHookError ? { toolSucceeded: true, hookError: afterHookError.message } : {}
7907
- }
9149
+ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : execution.content;
9150
+ const metadata = {
9151
+ ...execution.metadata ?? {},
9152
+ ...changedFiles.length ? { changedFiles } : {},
9153
+ ...checkpointId ? { checkpointId } : {},
9154
+ ...beforeHooks.length || afterHooks.length ? { hooks: { before: beforeHooks.length, after: afterHooks.length } } : {},
9155
+ ...afterHookError ? { toolSucceeded: true, hookError: afterHookError.message } : {}
7908
9156
  };
9157
+ const ok = execution.ok !== false && !afterHookError;
9158
+ if (!ok) {
9159
+ const failureClass = options.signal?.aborted ? "cancelled" : classifyToolFailure({ toolCallId: call.id, name: call.name, ok, content: completeContent, metadata });
9160
+ const receipt = recovery.recordFailure(call, failureClass);
9161
+ completeContent = `${formatFailureReceipt(receipt)}
9162
+ ${completeContent}`;
9163
+ metadata.failure = receipt;
9164
+ } else {
9165
+ recovery.recordSuccess(call);
9166
+ }
9167
+ const result = await this.protectToolResult({
9168
+ toolCallId: call.id,
9169
+ name: call.name,
9170
+ ok,
9171
+ content: completeContent,
9172
+ metadata
9173
+ });
7909
9174
  this.contextManager.recordTool(this.session, call, result);
7910
9175
  if (JSON.stringify(this.session.tasks) !== tasksBefore || call.name === "task") {
7911
9176
  await emit({ type: "tasks", tasks: this.session.tasks.map((task) => ({ ...task })) });
7912
9177
  }
9178
+ if (call.name === "task_contract" && this.session.taskContract) {
9179
+ await emit({ type: "contract", contract: structuredClone(this.session.taskContract) });
9180
+ }
7913
9181
  this.recordToolResult(result, tool.definition.category);
7914
9182
  await emit({ type: "tool_result", result });
7915
9183
  return result;
7916
9184
  } catch (error) {
7917
- const result = failedResult(call, formatToolError(error));
9185
+ const normalized = toError(error);
9186
+ const failureClass = classifyThrownToolFailure(normalized, options.signal);
9187
+ const receipt = recovery.recordFailure(call, failureClass);
9188
+ const result = await this.protectToolResult(failedResult(call, formatToolError(error), receipt));
7918
9189
  this.recordToolResult(result, tool.definition.category);
7919
9190
  await emit({ type: "tool_result", result });
7920
9191
  return result;
@@ -7983,7 +9254,7 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
7983
9254
  }
7984
9255
  appendAudit(event) {
7985
9256
  const audit = this.session.audit ?? (this.session.audit = []);
7986
- audit.push({ id: randomUUID10(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
9257
+ audit.push({ id: randomUUID12(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
7987
9258
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
7988
9259
  }
7989
9260
  async acceptChangedFiles(paths) {
@@ -8004,7 +9275,7 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
8004
9275
  const results = [];
8005
9276
  for (const command2 of this.config.agent.verifyCommands) {
8006
9277
  const call = {
8007
- id: `verify-${randomUUID10()}`,
9278
+ id: `verify-${randomUUID12()}`,
8008
9279
  name: "shell",
8009
9280
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
8010
9281
  };
@@ -8068,10 +9339,67 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
8068
9339
  persist() {
8069
9340
  return this.persistSession ? this.sessionStore.save(this.session) : Promise.resolve();
8070
9341
  }
9342
+ toolOutputBudget() {
9343
+ const contextWindowTokens = Math.max(24e3, Math.min(1e5, this.config.context.maxTokens * 3));
9344
+ const activeContextTokens = this.contextManager.status(this.session, contextWindowTokens).activeTokens;
9345
+ const remainingSessionTokens = this.config.agent.maxSessionTokens - (this.session.usage.inputTokens + this.session.usage.outputTokens);
9346
+ return dynamicToolOutputBudget(contextWindowTokens, activeContextTokens, remainingSessionTokens);
9347
+ }
9348
+ async protectToolResult(result) {
9349
+ const metadata = { ...result.metadata ?? {} };
9350
+ const output2 = await protectToolOutput({
9351
+ content: result.content,
9352
+ sessionId: this.session.id,
9353
+ toolCallId: result.toolCallId,
9354
+ tool: result.name,
9355
+ ok: result.ok,
9356
+ budgetTokens: this.toolOutputBudget(),
9357
+ metadata,
9358
+ artifacts: this.toolArtifactStore
9359
+ });
9360
+ if (output2.metadata.artifact) this.registerToolArtifact({
9361
+ ...output2.metadata.artifact,
9362
+ redacted: output2.metadata.redacted
9363
+ });
9364
+ return {
9365
+ ...result,
9366
+ content: output2.content,
9367
+ ...output2.metadata.truncated || output2.metadata.redacted || output2.metadata.sanitized || output2.metadata.artifact || output2.metadata.artifactUnavailable ? { metadata: { ...metadata, toolOutput: output2.metadata } } : Object.keys(metadata).length ? { metadata } : {}
9368
+ };
9369
+ }
9370
+ hasReadableToolArtifact() {
9371
+ const now = Date.now();
9372
+ return (this.session.toolArtifacts ?? []).some(
9373
+ (artifact) => Date.parse(artifact.expiresAt) > now
9374
+ );
9375
+ }
9376
+ registerToolArtifact(artifact) {
9377
+ const now = Date.now();
9378
+ const artifacts = (this.session.toolArtifacts ?? []).filter((candidate) => Date.parse(candidate.expiresAt) > now && candidate.toolCallId !== artifact.toolCallId);
9379
+ artifacts.push(artifact);
9380
+ artifacts.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
9381
+ if (artifacts.length > 200) artifacts.splice(0, artifacts.length - 200);
9382
+ this.session.toolArtifacts = artifacts;
9383
+ }
9384
+ async reconcileToolArtifacts() {
9385
+ let stored;
9386
+ try {
9387
+ stored = await this.toolArtifactStore.prune(this.session.id);
9388
+ } catch {
9389
+ this.session.toolArtifacts = [];
9390
+ return;
9391
+ }
9392
+ const valid = new Map(stored.map((artifact) => [artifact.toolCallId, artifact]));
9393
+ const reconciled = (this.session.toolArtifacts ?? []).filter((receipt) => {
9394
+ const artifact = valid.get(receipt.toolCallId);
9395
+ return artifact !== void 0 && artifact.sha256 === receipt.sha256 && artifact.bytes === receipt.bytes && artifact.createdAt === receipt.createdAt && artifact.expiresAt === receipt.expiresAt && artifact.redacted === receipt.redacted;
9396
+ });
9397
+ this.session.toolArtifacts = reconciled;
9398
+ }
8071
9399
  };
8072
9400
  function message(role, content, extra = {}) {
8073
9401
  return {
8074
- id: randomUUID10(),
9402
+ id: randomUUID12(),
8075
9403
  role,
8076
9404
  content,
8077
9405
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8142,24 +9470,33 @@ function estimateResponseTokens(response) {
8142
9470
  function uniqueCategories(categories) {
8143
9471
  return [...new Set(categories)];
8144
9472
  }
8145
- function failedResult(call, content) {
9473
+ function failedResult(call, content, failure) {
8146
9474
  return {
8147
9475
  toolCallId: call.id,
8148
9476
  name: call.name,
8149
9477
  ok: false,
8150
- content: truncateToolOutput(content)
9478
+ content: failure ? `${formatFailureReceipt(failure)}
9479
+ ${content}` : content,
9480
+ ...failure ? { metadata: { failure } } : {}
8151
9481
  };
8152
9482
  }
8153
- function truncateToolOutput(content) {
8154
- const max = 8e4;
8155
- return content.length <= max ? content : `${content.slice(0, max)}
8156
- \u2026 tool output truncated`;
9483
+ function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable) {
9484
+ return tools.definitions().filter(
9485
+ (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact")
9486
+ );
8157
9487
  }
8158
9488
  function formatToolError(error) {
8159
9489
  const normalized = toError(error);
8160
9490
  if (normalized.name === "ZodError") return `Invalid tool arguments: ${normalized.message}`;
8161
9491
  return normalized.message;
8162
9492
  }
9493
+ function classifyThrownToolFailure(error, signal) {
9494
+ const normalized = toError(error);
9495
+ if (isAbortError(normalized) || signal?.aborted) return "cancelled";
9496
+ if (normalized.name === "HookError") return "hook";
9497
+ if (normalized.name === "ZodError" || normalized.name === "ToolInputError") return "schema_input";
9498
+ return "execution";
9499
+ }
8163
9500
  function titleFromInput(input2) {
8164
9501
  return input2.trim().replace(/\s+/g, " ").slice(0, 80) || "New session";
8165
9502
  }
@@ -8180,9 +9517,9 @@ async function safeEmit(emit, event) {
8180
9517
  }
8181
9518
 
8182
9519
  // src/agent/profiles.ts
8183
- import { lstat as lstat16, readFile as readFile13, readdir as readdir5 } from "node:fs/promises";
9520
+ import { lstat as lstat17, readFile as readFile14, readdir as readdir6 } from "node:fs/promises";
8184
9521
  import { homedir as homedir3 } from "node:os";
8185
- import { basename as basename8, join as join14 } from "node:path";
9522
+ import { basename as basename8, join as join15 } from "node:path";
8186
9523
  import { parse as parseYaml2 } from "yaml";
8187
9524
  var builtInProfiles = [
8188
9525
  {
@@ -8274,14 +9611,14 @@ var AgentProfileCatalog = class {
8274
9611
  profiles = new Map(builtInProfiles.map((profile) => [profile.name, profile]));
8275
9612
  async discover() {
8276
9613
  const locations = [
8277
- { path: join14(resolveHomeNamespace(), "agents"), source: "user" },
8278
- { path: join14(homedir3(), ".claude", "agents"), source: "user" },
8279
- { path: join14(resolveProjectNamespaceSync(this.workspace).active, "agents"), source: "workspace" },
8280
- { path: join14(this.workspace, ".claude", "agents"), source: "workspace" }
9614
+ { path: join15(resolveHomeNamespace(), "agents"), source: "user" },
9615
+ { path: join15(homedir3(), ".claude", "agents"), source: "user" },
9616
+ { path: join15(resolveProjectNamespaceSync(this.workspace).active, "agents"), source: "workspace" },
9617
+ { path: join15(this.workspace, ".claude", "agents"), source: "workspace" }
8281
9618
  ];
8282
9619
  for (const location of locations) {
8283
9620
  for (const file of await markdownFiles(location.path)) {
8284
- const profile = await readProfile(join14(location.path, file), location.source);
9621
+ const profile = await readProfile(join15(location.path, file), location.source);
8285
9622
  if (profile) this.profiles.set(profile.name, profile);
8286
9623
  }
8287
9624
  }
@@ -8297,18 +9634,18 @@ var AgentProfileCatalog = class {
8297
9634
  };
8298
9635
  async function markdownFiles(directory) {
8299
9636
  try {
8300
- const info = await lstat16(directory);
9637
+ const info = await lstat17(directory);
8301
9638
  if (!info.isDirectory() || info.isSymbolicLink()) return [];
8302
- return (await readdir5(directory, { withFileTypes: true })).filter((entry) => entry.isFile() && !entry.isSymbolicLink() && entry.name.endsWith(".md")).map((entry) => entry.name).slice(0, 200);
9639
+ return (await readdir6(directory, { withFileTypes: true })).filter((entry) => entry.isFile() && !entry.isSymbolicLink() && entry.name.endsWith(".md")).map((entry) => entry.name).slice(0, 200);
8303
9640
  } catch {
8304
9641
  return [];
8305
9642
  }
8306
9643
  }
8307
9644
  async function readProfile(path, source) {
8308
9645
  try {
8309
- const info = await lstat16(path);
9646
+ const info = await lstat17(path);
8310
9647
  if (!info.isFile() || info.isSymbolicLink() || info.size > 1e5) return void 0;
8311
- const raw = await readFile13(path, "utf8");
9648
+ const raw = await readFile14(path, "utf8");
8312
9649
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
8313
9650
  const frontmatter = match ? parseYaml2(match[1] ?? "") : {};
8314
9651
  const prompt = (match ? raw.slice(match[0].length) : raw).trim();
@@ -8335,9 +9672,9 @@ function integer(value, fallback, min, max) {
8335
9672
  }
8336
9673
 
8337
9674
  // src/agent/delegation.ts
8338
- import { randomUUID as randomUUID12 } from "node:crypto";
8339
- import { join as join17 } from "node:path";
8340
- import { z as z15 } from "zod";
9675
+ import { randomUUID as randomUUID14 } from "node:crypto";
9676
+ import { join as join18 } from "node:path";
9677
+ import { z as z18 } from "zod";
8341
9678
 
8342
9679
  // src/agent/external-runtime.ts
8343
9680
  async function runExternalAgent(request) {
@@ -8464,101 +9801,101 @@ function numeric(...values) {
8464
9801
  }
8465
9802
 
8466
9803
  // src/agent/team-store.ts
8467
- import { createHash as createHash10, randomUUID as randomUUID11 } from "node:crypto";
8468
- import { lstat as lstat17, readFile as readFile14, readdir as readdir6, rm as rm2 } from "node:fs/promises";
8469
- 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({
9804
+ import { createHash as createHash12, randomUUID as randomUUID13 } from "node:crypto";
9805
+ import { lstat as lstat18, readFile as readFile15, readdir as readdir7, rm as rm3 } from "node:fs/promises";
9806
+ import { join as join16, resolve as resolve16 } from "node:path";
9807
+ import { z as z17 } from "zod";
9808
+ var runIdSchema = z17.string().uuid();
9809
+ var hashSchema = z17.string().regex(/^[a-f0-9]{64}$/u);
9810
+ var artifactSchema2 = z17.object({
8474
9811
  sha256: hashSchema,
8475
- bytes: z14.number().int().nonnegative().max(5e5)
9812
+ bytes: z17.number().int().nonnegative().max(5e5)
8476
9813
  }).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(),
9814
+ var phaseSchema = z17.enum(["work", "review", "revision", "write"]);
9815
+ var agentRecordSchema = z17.object({
9816
+ id: z17.string().uuid(),
9817
+ profile: z17.string(),
9818
+ provider: z17.string(),
9819
+ model: z17.string(),
8483
9820
  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()
9821
+ ok: z17.boolean(),
9822
+ createdAt: z17.string(),
9823
+ startedAt: z17.string().optional(),
9824
+ endedAt: z17.string().optional(),
9825
+ durationMs: z17.number().int().nonnegative().optional(),
9826
+ toolCalls: z17.number().int().nonnegative().optional(),
9827
+ usage: z17.object({
9828
+ inputTokens: z17.number().int().nonnegative(),
9829
+ outputTokens: z17.number().int().nonnegative()
8493
9830
  }).strict().optional(),
8494
- report: artifactSchema
9831
+ report: artifactSchema2
8495
9832
  }).strict();
8496
- var messageRecordSchema = z14.object({
8497
- id: z14.string().uuid(),
8498
- from: z14.string(),
8499
- to: z14.string(),
8500
- createdAt: z14.string(),
8501
- content: artifactSchema
9833
+ var messageRecordSchema = z17.object({
9834
+ id: z17.string().uuid(),
9835
+ from: z17.string(),
9836
+ to: z17.string(),
9837
+ createdAt: z17.string(),
9838
+ content: artifactSchema2
8502
9839
  }).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()
9840
+ var writerIntegrationSchema = z17.object({
9841
+ status: z17.enum(["ready", "conflict", "integrated"]),
9842
+ checkedAt: z17.string(),
9843
+ detail: z17.string().max(2e4),
9844
+ checkpoint: z17.object({
9845
+ sessionId: z17.string(),
9846
+ checkpointId: z17.string()
8510
9847
  }).strict().optional(),
8511
- integratedAt: z14.string().optional()
9848
+ integratedAt: z17.string().optional()
8512
9849
  }).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"]),
8518
- patch: artifactSchema,
8519
- files: z14.array(z14.string().min(1).max(4e3)).max(2e3),
8520
- worktreeCleaned: z14.boolean(),
8521
- review: artifactSchema.optional(),
9850
+ var writerLaneSchema = z17.object({
9851
+ profile: z17.string(),
9852
+ reviewer: z17.string(),
9853
+ baseCommit: z17.string().regex(/^[a-f0-9]{40,64}$/u),
9854
+ outcome: z17.enum(["accepted", "rejected", "failed", "cancelled"]),
9855
+ patch: artifactSchema2,
9856
+ files: z17.array(z17.string().min(1).max(4e3)).max(2e3),
9857
+ worktreeCleaned: z17.boolean(),
9858
+ review: artifactSchema2.optional(),
8522
9859
  integration: writerIntegrationSchema.optional()
8523
9860
  }).strict();
8524
9861
  var manifestFields = {
8525
9862
  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)
9863
+ workspace: z17.string(),
9864
+ objective: z17.string().max(3e4),
9865
+ reviewer: z17.string(),
9866
+ createdAt: z17.string(),
9867
+ updatedAt: z17.string(),
9868
+ status: z17.enum(["running", "accepted", "rejected", "failed"]),
9869
+ maxReviewRounds: z17.number().int().min(0).max(3),
9870
+ reviewRounds: z17.number().int().min(0).max(3),
9871
+ agents: z17.array(agentRecordSchema).max(256),
9872
+ messages: z17.array(messageRecordSchema).max(512)
8536
9873
  };
8537
- var manifestV1Schema = z14.object({
8538
- version: z14.literal(1),
9874
+ var manifestV1Schema = z17.object({
9875
+ version: z17.literal(1),
8539
9876
  ...manifestFields
8540
9877
  }).strict();
8541
- var manifestV2Schema = z14.object({
8542
- version: z14.literal(2),
9878
+ var manifestV2Schema = z17.object({
9879
+ version: z17.literal(2),
8543
9880
  ...manifestFields,
8544
9881
  writer: writerLaneSchema.optional()
8545
9882
  }).strict();
8546
- var manifestSchema2 = z14.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
9883
+ var manifestSchema2 = z17.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
8547
9884
  var TeamRunStore = class {
8548
9885
  workspace;
8549
9886
  directory;
8550
9887
  managedDirectory;
8551
9888
  writes = Promise.resolve();
8552
9889
  constructor(workspace, directory) {
8553
- this.workspace = resolve15(workspace);
9890
+ this.workspace = resolve16(workspace);
8554
9891
  this.managedDirectory = directory === void 0;
8555
- this.directory = directory ? resolve15(directory) : join15(resolveProjectNamespaceSync(this.workspace).active, "team-runs");
9892
+ this.directory = directory ? resolve16(directory) : join16(resolveProjectNamespaceSync(this.workspace).active, "team-runs");
8556
9893
  }
8557
9894
  async create(input2) {
8558
9895
  const now = (/* @__PURE__ */ new Date()).toISOString();
8559
9896
  const manifest = manifestSchema2.parse({
8560
9897
  version: 2,
8561
- id: randomUUID11(),
9898
+ id: randomUUID13(),
8562
9899
  workspace: this.workspace,
8563
9900
  objective: input2.objective,
8564
9901
  reviewer: input2.reviewer,
@@ -8636,10 +9973,10 @@ var TeamRunStore = class {
8636
9973
  runIdSchema.parse(runId);
8637
9974
  await this.writes;
8638
9975
  await this.assertRunDirectory(runId);
8639
- const path = join15(this.runDirectory(runId), "manifest.json");
9976
+ const path = join16(this.runDirectory(runId), "manifest.json");
8640
9977
  await this.assertRegularFile(path);
8641
- const manifest = manifestSchema2.parse(JSON.parse(await readFile14(path, "utf8")));
8642
- if (manifest.id !== runId || resolve15(manifest.workspace) !== this.workspace) {
9978
+ const manifest = manifestSchema2.parse(JSON.parse(await readFile15(path, "utf8")));
9979
+ if (manifest.id !== runId || resolve16(manifest.workspace) !== this.workspace) {
8643
9980
  throw new Error("Team run manifest identity does not match its location.");
8644
9981
  }
8645
9982
  if (verify) {
@@ -8653,13 +9990,13 @@ var TeamRunStore = class {
8653
9990
  }
8654
9991
  async readArtifact(runId, artifact) {
8655
9992
  await this.verifyArtifact(runId, artifact);
8656
- return readFile14(this.artifactPath(runId, artifact.sha256), "utf8");
9993
+ return readFile15(this.artifactPath(runId, artifact.sha256), "utf8");
8657
9994
  }
8658
9995
  async list() {
8659
9996
  await this.writes;
8660
9997
  try {
8661
9998
  await assertNoSymlinkPath(this.workspace, this.directory);
8662
- const entries = await readdir6(this.directory, { withFileTypes: true });
9999
+ const entries = await readdir7(this.directory, { withFileTypes: true });
8663
10000
  const summaries = [];
8664
10001
  for (const entry of entries) {
8665
10002
  if (!entry.isDirectory() || entry.isSymbolicLink() || !runIdSchema.safeParse(entry.name).success) continue;
@@ -8682,9 +10019,9 @@ var TeamRunStore = class {
8682
10019
  const directory = this.runDirectory(runId);
8683
10020
  await assertNoSymlinkPath(this.workspace, directory);
8684
10021
  try {
8685
- const info = await lstat17(directory);
10022
+ const info = await lstat18(directory);
8686
10023
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Team run storage is not a regular directory.");
8687
- await rm2(directory, { recursive: true });
10024
+ await rm3(directory, { recursive: true });
8688
10025
  return true;
8689
10026
  } catch (error) {
8690
10027
  if (error.code === "ENOENT") return false;
@@ -8707,31 +10044,31 @@ var TeamRunStore = class {
8707
10044
  }
8708
10045
  async loadUnlocked(runId) {
8709
10046
  await this.assertRunDirectory(runId);
8710
- const path = join15(this.runDirectory(runId), "manifest.json");
10047
+ const path = join16(this.runDirectory(runId), "manifest.json");
8711
10048
  await this.assertRegularFile(path);
8712
- return manifestSchema2.parse(JSON.parse(await readFile14(path, "utf8")));
10049
+ return manifestSchema2.parse(JSON.parse(await readFile15(path, "utf8")));
8713
10050
  }
8714
10051
  async writeManifest(manifest) {
8715
10052
  const directory = this.runDirectory(manifest.id);
8716
10053
  await ensureWorkspaceStorageDirectory(this.workspace, directory, {
8717
10054
  requireActiveNamespace: this.managedDirectory
8718
10055
  });
8719
- await atomicWrite(join15(directory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
10056
+ await atomicWrite(join16(directory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
8720
10057
  `, 384);
8721
10058
  }
8722
10059
  async writeArtifact(runId, content, truncate = true) {
8723
10060
  const data = boundedArtifactText(content, 5e5, truncate);
8724
10061
  const bytes = Buffer.byteLength(data);
8725
- const sha256 = createHash10("sha256").update(data).digest("hex");
8726
- const directory = join15(this.runDirectory(runId), "blobs");
10062
+ const sha256 = createHash12("sha256").update(data).digest("hex");
10063
+ const directory = join16(this.runDirectory(runId), "blobs");
8727
10064
  await ensureWorkspaceStorageDirectory(this.workspace, directory, {
8728
10065
  requireActiveNamespace: this.managedDirectory
8729
10066
  });
8730
- const path = join15(directory, `${sha256}.txt`);
10067
+ const path = join16(directory, `${sha256}.txt`);
8731
10068
  try {
8732
10069
  await this.assertRegularFile(path);
8733
- const existing = await readFile14(path);
8734
- if (createHash10("sha256").update(existing).digest("hex") !== sha256) {
10070
+ const existing = await readFile15(path);
10071
+ if (createHash12("sha256").update(existing).digest("hex") !== sha256) {
8735
10072
  throw new Error(`Team artifact hash collision or corruption: ${sha256}`);
8736
10073
  }
8737
10074
  } catch (error) {
@@ -8751,27 +10088,27 @@ var TeamRunStore = class {
8751
10088
  hashSchema.parse(artifact.sha256);
8752
10089
  const path = this.artifactPath(runId, artifact.sha256);
8753
10090
  await this.assertRegularFile(path);
8754
- const data = await readFile14(path);
8755
- const hash = createHash10("sha256").update(data).digest("hex");
8756
- if (hash !== artifact.sha256 || data.byteLength !== artifact.bytes) {
10091
+ const data = await readFile15(path);
10092
+ const hash2 = createHash12("sha256").update(data).digest("hex");
10093
+ if (hash2 !== artifact.sha256 || data.byteLength !== artifact.bytes) {
8757
10094
  throw new Error(`Team artifact integrity check failed: ${artifact.sha256}`);
8758
10095
  }
8759
10096
  }
8760
10097
  artifactPath(runId, sha256) {
8761
- return join15(this.runDirectory(runId), "blobs", `${sha256}.txt`);
10098
+ return join16(this.runDirectory(runId), "blobs", `${sha256}.txt`);
8762
10099
  }
8763
10100
  runDirectory(runId) {
8764
- return join15(this.directory, runId);
10101
+ return join16(this.directory, runId);
8765
10102
  }
8766
10103
  async assertRunDirectory(runId) {
8767
10104
  const directory = this.runDirectory(runId);
8768
10105
  await assertNoSymlinkPath(this.workspace, directory);
8769
- const info = await lstat17(directory);
10106
+ const info = await lstat18(directory);
8770
10107
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Team run storage is not a regular directory.");
8771
10108
  }
8772
10109
  async assertRegularFile(path) {
8773
- await assertNoSymlinkPath(this.workspace, resolve15(path, ".."));
8774
- const info = await lstat17(path);
10110
+ await assertNoSymlinkPath(this.workspace, resolve16(path, ".."));
10111
+ const info = await lstat18(path);
8775
10112
  if (!info.isFile() || info.isSymbolicLink()) throw new Error(`Team run file is not a regular file: ${path}`);
8776
10113
  }
8777
10114
  };
@@ -8815,10 +10152,10 @@ function resolveAgentModelRoute(team, parent, profile) {
8815
10152
  }
8816
10153
 
8817
10154
  // src/agent/writer-lane.ts
8818
- import { createHash as createHash11 } from "node:crypto";
8819
- import { lstat as lstat18, mkdtemp, realpath as realpath7, rm as rm3 } from "node:fs/promises";
10155
+ import { createHash as createHash13 } from "node:crypto";
10156
+ import { lstat as lstat19, mkdtemp, realpath as realpath7, rm as rm4 } from "node:fs/promises";
8820
10157
  import { tmpdir as tmpdir2 } from "node:os";
8821
- import { isAbsolute as isAbsolute5, join as join16, resolve as resolve16 } from "node:path";
10158
+ import { isAbsolute as isAbsolute5, join as join17, resolve as resolve17 } from "node:path";
8822
10159
  var WriterLaneApplyError = class extends Error {
8823
10160
  constructor(message2, attempted, cause) {
8824
10161
  super(message2, cause === void 0 ? void 0 : { cause });
@@ -8831,18 +10168,18 @@ var WriterLane = class {
8831
10168
  workspace;
8832
10169
  constructor(workspace, workspaceRoots) {
8833
10170
  this.workspace = new WorkspaceAccess([
8834
- resolve16(workspace),
8835
- ...workspaceRoots.map((root) => resolve16(root))
10171
+ resolve17(workspace),
10172
+ ...workspaceRoots.map((root) => resolve17(root))
8836
10173
  ]);
8837
10174
  }
8838
10175
  async createDraft(maxPatchBytes, operation, signal) {
8839
10176
  const repository = await this.repository();
8840
10177
  const baseCommit = await this.head(repository);
8841
10178
  const lease = await acquireNamespaceLease(
8842
- join16(repository.commonDirectory, "skein-writer-lane"),
10179
+ join17(repository.commonDirectory, "skein-writer-lane"),
8843
10180
  "exclusive"
8844
10181
  );
8845
- const worktree = await mkdtemp(join16(tmpdir2(), "skein-writer-"));
10182
+ const worktree = await mkdtemp(join17(tmpdir2(), "skein-writer-"));
8846
10183
  let added = false;
8847
10184
  let value;
8848
10185
  let patch = "";
@@ -8913,7 +10250,7 @@ var WriterLane = class {
8913
10250
  return {
8914
10251
  baseCommit,
8915
10252
  patch,
8916
- patchSha256: createHash11("sha256").update(patch).digest("hex"),
10253
+ patchSha256: createHash13("sha256").update(patch).digest("hex"),
8917
10254
  files,
8918
10255
  worktreeCleaned,
8919
10256
  value
@@ -8932,7 +10269,7 @@ var WriterLane = class {
8932
10269
  try {
8933
10270
  const repository = await this.repository();
8934
10271
  const lease = await acquireNamespaceLease(
8935
- join16(repository.commonDirectory, "skein-writer-lane"),
10272
+ join17(repository.commonDirectory, "skein-writer-lane"),
8936
10273
  "exclusive"
8937
10274
  );
8938
10275
  try {
@@ -9040,7 +10377,7 @@ var WriterLane = class {
9040
10377
  if (isAbsolute5(file) || file === ".git" || file.startsWith(".git/") || file.includes("\0")) {
9041
10378
  throw new Error(`Writer patch contains an unsafe path: ${file}`);
9042
10379
  }
9043
- await this.workspace.resolvePath(join16(repository.root, file), { allowMissing: true });
10380
+ await this.workspace.resolvePath(join17(repository.root, file), { allowMissing: true });
9044
10381
  }
9045
10382
  if (expectedFiles && !sameSet(files, expectedFiles)) {
9046
10383
  throw new Error("Writer patch paths do not match the persisted file manifest.");
@@ -9055,7 +10392,7 @@ var WriterLane = class {
9055
10392
  if (topLevel.exitCode !== 0 || !topLevel.stdout.trim()) {
9056
10393
  throw new Error("The primary workspace must be a Git repository root for writer lanes.");
9057
10394
  }
9058
- const discoveredRoot = resolve16(topLevel.stdout.trim());
10395
+ const discoveredRoot = resolve17(topLevel.stdout.trim());
9059
10396
  if (await realpath7(discoveredRoot) !== await realpath7(root)) {
9060
10397
  throw new Error("The primary workspace must be the Git repository root for writer lanes.");
9061
10398
  }
@@ -9064,7 +10401,7 @@ var WriterLane = class {
9064
10401
  if (common.exitCode !== 0 || !common.stdout.trim()) {
9065
10402
  throw new Error(`Unable to resolve the Git common directory: ${processDetail(common)}`);
9066
10403
  }
9067
- const commonDirectory = await realpath7(isAbsolute5(common.stdout.trim()) ? common.stdout.trim() : resolve16(repositoryRoot, common.stdout.trim()));
10404
+ const commonDirectory = await realpath7(isAbsolute5(common.stdout.trim()) ? common.stdout.trim() : resolve17(repositoryRoot, common.stdout.trim()));
9068
10405
  return { root: repositoryRoot, commonDirectory, runtime };
9069
10406
  }
9070
10407
  async head(repository) {
@@ -9076,7 +10413,7 @@ var WriterLane = class {
9076
10413
  return value;
9077
10414
  }
9078
10415
  async cleanupWorktree(repository, worktree, added) {
9079
- const physicalWorktree = await realpath7(worktree).catch(() => resolve16(worktree));
10416
+ const physicalWorktree = await realpath7(worktree).catch(() => resolve17(worktree));
9080
10417
  if (added) {
9081
10418
  await runIsolatedGit(repository.runtime, [
9082
10419
  "worktree",
@@ -9085,7 +10422,7 @@ var WriterLane = class {
9085
10422
  worktree
9086
10423
  ], repository.root, { timeoutMs: 6e4 }).catch(() => void 0);
9087
10424
  }
9088
- await rm3(worktree, { recursive: true, force: true }).catch(() => void 0);
10425
+ await rm4(worktree, { recursive: true, force: true }).catch(() => void 0);
9089
10426
  await runIsolatedGit(repository.runtime, [
9090
10427
  "worktree",
9091
10428
  "prune",
@@ -9134,7 +10471,7 @@ function processDetail(result) {
9134
10471
  }
9135
10472
  async function pathExists2(path) {
9136
10473
  try {
9137
- await lstat18(path);
10474
+ await lstat19(path);
9138
10475
  return true;
9139
10476
  } catch (error) {
9140
10477
  if (error.code === "ENOENT") return false;
@@ -9143,14 +10480,14 @@ async function pathExists2(path) {
9143
10480
  }
9144
10481
 
9145
10482
  // 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()
10483
+ var writerRunInputSchema = z18.object({
10484
+ task: z18.string().min(1).max(2e4),
10485
+ profile: z18.string().max(64).optional(),
10486
+ reviewer: z18.string().max(64).optional()
9150
10487
  }).strict();
9151
- var writerIntegrateInputSchema = z15.object({
9152
- run_id: z15.string().uuid(),
9153
- patch_sha256: z15.string().regex(/^[a-f0-9]{64}$/u)
10488
+ var writerIntegrateInputSchema = z18.object({
10489
+ run_id: z18.string().uuid(),
10490
+ patch_sha256: z18.string().regex(/^[a-f0-9]{64}$/u)
9154
10491
  }).strict();
9155
10492
  var DelegationManager = class {
9156
10493
  constructor(options) {
@@ -9211,10 +10548,10 @@ var DelegationManager = class {
9211
10548
  },
9212
10549
  async execute(arguments_, context) {
9213
10550
  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)
10551
+ const input2 = z18.object({
10552
+ tasks: z18.array(z18.object({
10553
+ profile: z18.string().max(64).optional(),
10554
+ task: z18.string().min(1).max(2e4)
9218
10555
  })).min(1).max(manager.team.maxDelegations)
9219
10556
  }).parse(arguments_);
9220
10557
  const tasks = input2.tasks.map((task) => ({
@@ -9260,13 +10597,13 @@ var DelegationManager = class {
9260
10597
  },
9261
10598
  async execute(arguments_, context) {
9262
10599
  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)
10600
+ const input2 = z18.object({
10601
+ objective: z18.string().min(1).max(3e4),
10602
+ tasks: z18.array(z18.object({
10603
+ profile: z18.string().max(64).optional(),
10604
+ task: z18.string().min(1).max(2e4)
9268
10605
  })).min(1).max(manager.team.maxDelegations),
9269
- reviewer: z15.string().max(64).optional()
10606
+ reviewer: z18.string().max(64).optional()
9270
10607
  }).parse(arguments_);
9271
10608
  const tasks = input2.tasks.map((task) => ({
9272
10609
  profile: task.profile ?? manager.team.defaultProfile,
@@ -9329,7 +10666,7 @@ var DelegationManager = class {
9329
10666
  const plan = await manager.loadWriterPlan(input2.run_id, input2.patch_sha256);
9330
10667
  const files = await manager.writerLane.inspectPatch(plan.patch, plan.writer.files);
9331
10668
  return Promise.all(files.map(
9332
- (file) => context.workspace.resolvePath(join17(context.workspace.primaryRoot, file), { allowMissing: true })
10669
+ (file) => context.workspace.resolvePath(join18(context.workspace.primaryRoot, file), { allowMissing: true })
9333
10670
  ));
9334
10671
  },
9335
10672
  async execute(arguments_, context) {
@@ -9364,7 +10701,7 @@ var DelegationManager = class {
9364
10701
  maxReviewRounds: 0
9365
10702
  });
9366
10703
  await emit?.({ type: "team_start", id: board.id, objective: task });
9367
- const writerId = randomUUID12();
10704
+ const writerId = randomUUID14();
9368
10705
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
9369
10706
  const draft = await this.writerLane.createDraft(
9370
10707
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -9516,7 +10853,7 @@ ${review.summary}`,
9516
10853
  const plan = await this.loadWriterPlan(runId, patchSha256);
9517
10854
  const files = await this.writerLane.inspectPatch(plan.patch, plan.writer.files);
9518
10855
  const paths = await Promise.all(files.map(
9519
- (file) => context.workspace.resolvePath(join17(context.workspace.primaryRoot, file), { allowMissing: true })
10856
+ (file) => context.workspace.resolvePath(join18(context.workspace.primaryRoot, file), { allowMissing: true })
9520
10857
  ));
9521
10858
  const checkpointStore = new CheckpointStore(context.workspace);
9522
10859
  let checkpointId = context.checkpointId;
@@ -9734,7 +11071,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
9734
11071
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
9735
11072
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
9736
11073
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
9737
- const runId = board?.id ?? randomUUID12();
11074
+ const runId = board?.id ?? randomUUID14();
9738
11075
  await emit?.({ type: "team_start", id: runId, objective });
9739
11076
  try {
9740
11077
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -9792,7 +11129,7 @@ ${review.summary}`,
9792
11129
  }
9793
11130
  }
9794
11131
  async runBatch(runId, tasks, phase, emit, signal) {
9795
- const scheduled = tasks.map((task) => ({ id: randomUUID12(), task }));
11132
+ const scheduled = tasks.map((task) => ({ id: randomUUID14(), task }));
9796
11133
  for (const item of scheduled) {
9797
11134
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
9798
11135
  }
@@ -9901,12 +11238,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
9901
11238
  });
9902
11239
  }
9903
11240
  async peerMessage(runId, from, to, content, emit) {
9904
- const id = randomUUID12();
11241
+ const id = randomUUID14();
9905
11242
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
9906
11243
  await emit?.({ type: "agent_message", id, from, to, content });
9907
11244
  }
9908
11245
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
9909
- const id = scheduledId ?? randomUUID12();
11246
+ const id = scheduledId ?? randomUUID14();
9910
11247
  const profile = this.options.profiles.get(task.profile);
9911
11248
  const configuredRoute = this.team.routes?.[task.profile];
9912
11249
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -10818,11 +12155,11 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10818
12155
  `);
10819
12156
  }
10820
12157
  printText(event) {
10821
- const { quiet, compact } = this.options;
12158
+ const { quiet, compact: compact2 } = this.options;
10822
12159
  if (quiet) return;
10823
12160
  switch (event.type) {
10824
12161
  case "thinking":
10825
- if (!compact) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
12162
+ if (!compact2) process.stderr.write(this.paint.dim(`${this.glyphs.meta} reasoning ${this.glyphs.separator} turn ${event.turn}
10826
12163
  `));
10827
12164
  break;
10828
12165
  case "context":
@@ -10832,7 +12169,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10832
12169
  ));
10833
12170
  break;
10834
12171
  case "prompt":
10835
- if (!compact) {
12172
+ if (!compact2) {
10836
12173
  process.stderr.write(this.paint.dim(
10837
12174
  `${this.glyphs.meta} prompt ${this.glyphs.separator} ${event.intent} ${this.glyphs.separator} ${event.sections.join(" + ")} ${this.glyphs.separator} ~${event.estimatedTokens} tokens
10838
12175
  `
@@ -10867,12 +12204,21 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
10867
12204
  }
10868
12205
  break;
10869
12206
  case "tasks":
10870
- if (!compact) {
12207
+ if (!compact2) {
10871
12208
  const completed = event.tasks.filter((task) => task.status === "completed").length;
10872
12209
  process.stderr.write(this.paint.dim(`${this.glyphs.meta} plan ${this.glyphs.separator} ${completed}/${event.tasks.length} complete
10873
12210
  `));
10874
12211
  }
10875
12212
  break;
12213
+ case "contract": {
12214
+ const required = event.contract.acceptanceCriteria.filter((item) => item.required);
12215
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
12216
+ process.stderr.write(this.paint.dim(
12217
+ `${this.glyphs.meta} contract ${this.glyphs.separator} ${event.contract.state} ${this.glyphs.separator} ${satisfied}/${required.length} accepted
12218
+ `
12219
+ ));
12220
+ break;
12221
+ }
10876
12222
  case "writer_lane":
10877
12223
  process.stderr.write(
10878
12224
  `${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 +12300,7 @@ function sessionSummary(session) {
10954
12300
  createdAt: session.createdAt,
10955
12301
  updatedAt: session.updatedAt,
10956
12302
  tasks: session.tasks,
12303
+ ...session.taskContract ? { taskContract: session.taskContract } : {},
10957
12304
  changedFiles: session.changedFiles,
10958
12305
  ...session.lastRun ? { lastRun: session.lastRun } : {},
10959
12306
  usage: session.usage
@@ -10981,7 +12328,7 @@ function formatResultDetail(result, paint = chalk2, glyphs = resolveCliGlyphs())
10981
12328
  }
10982
12329
 
10983
12330
  // src/cli/namespace-leases.ts
10984
- import { resolve as resolve17 } from "node:path";
12331
+ import { resolve as resolve18 } from "node:path";
10985
12332
  function cliNamespaceLeaseScopes(actionCommand) {
10986
12333
  const names = commandNames(actionCommand);
10987
12334
  const topLevel = names[1];
@@ -11003,7 +12350,7 @@ async function acquireCliNamespaceLeases(actionCommand) {
11003
12350
  const scopes = cliNamespaceLeaseScopes(actionCommand);
11004
12351
  const localOptions = actionCommand.opts();
11005
12352
  const globalOptions = actionCommand.optsWithGlobals();
11006
- const workspace = resolve17(localOptions.workspace ?? globalOptions.workspace ?? process.cwd());
12353
+ const workspace = resolve18(localOptions.workspace ?? globalOptions.workspace ?? process.cwd());
11007
12354
  const targets = scopes.map((scope) => scope === "project" ? projectNamespacePaths(workspace).canonical : homeNamespacePaths().canonical);
11008
12355
  const leases = [];
11009
12356
  try {
@@ -11157,11 +12504,11 @@ function command(name, description, usage, aliases) {
11157
12504
 
11158
12505
  // src/ui/text.ts
11159
12506
  import stringWidth from "string-width";
11160
- import stripAnsi from "strip-ansi";
12507
+ import stripAnsi2 from "strip-ansi";
11161
12508
  var controlCharacters = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
11162
12509
  var escapeSequences = /[[\]][0-9;?=>!]*[ -/]*[@-~]|[[\]][?=>][0-9;?=>!]*[a-zA-Z~]/gu;
11163
12510
  function sanitizeTerminalText(value) {
11164
- return stripAnsi(value).replace(escapeSequences, "").replace(/\r\n?/g, "\n").replace(controlCharacters, "");
12511
+ return stripAnsi2(value).replace(escapeSequences, "").replace(/\r\n?/g, "\n").replace(controlCharacters, "");
11165
12512
  }
11166
12513
  function terminalEllipsis() {
11167
12514
  return process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii" ? "..." : "\u2026";
@@ -11254,8 +12601,8 @@ function graphemes(value) {
11254
12601
  }
11255
12602
 
11256
12603
  // src/ui/theme.ts
11257
- import { lstat as lstat19, readdir as readdir7, readFile as readFile15 } from "node:fs/promises";
11258
- import { basename as basename9, join as join18, resolve as resolve18 } from "node:path";
12604
+ import { lstat as lstat20, readdir as readdir8, readFile as readFile16 } from "node:fs/promises";
12605
+ import { basename as basename9, join as join19, resolve as resolve19 } from "node:path";
11259
12606
  import React, { createContext, useContext } from "react";
11260
12607
  function defineTheme(seed) {
11261
12608
  return {
@@ -11377,10 +12724,10 @@ async function reloadUserThemes(directory = userThemeDirectory()) {
11377
12724
  userThemeNames.clear();
11378
12725
  const loaded = [];
11379
12726
  const errors = [];
11380
- const resolvedDirectory = resolve18(directory);
12727
+ const resolvedDirectory = resolve19(directory);
11381
12728
  let entries;
11382
12729
  try {
11383
- entries = await readdir7(resolvedDirectory, { encoding: "utf8" });
12730
+ entries = await readdir8(resolvedDirectory, { encoding: "utf8" });
11384
12731
  } catch (error) {
11385
12732
  if (error.code === "ENOENT") {
11386
12733
  return { directory: resolvedDirectory, loaded, errors };
@@ -11389,13 +12736,13 @@ async function reloadUserThemes(directory = userThemeDirectory()) {
11389
12736
  }
11390
12737
  for (const entry of entries) {
11391
12738
  if (!entry.endsWith(".json")) continue;
11392
- const path = join18(resolvedDirectory, entry);
12739
+ const path = join19(resolvedDirectory, entry);
11393
12740
  try {
11394
- const info = await lstat19(path);
12741
+ const info = await lstat20(path);
11395
12742
  if (!info.isFile() || info.isSymbolicLink() || info.size > 64e3) {
11396
12743
  throw new Error("must be a regular JSON file smaller than 64 KB");
11397
12744
  }
11398
- const parsed = JSON.parse(await readFile15(path, "utf8"));
12745
+ const parsed = JSON.parse(await readFile16(path, "utf8"));
11399
12746
  const name = themeName(parsed, basename9(entry, ".json"));
11400
12747
  if (builtInThemeNames.has(name)) throw new Error(`cannot replace built-in theme \`${name}\``);
11401
12748
  themes[name] = defineTheme(themeSeed(parsed, name));
@@ -11408,7 +12755,7 @@ async function reloadUserThemes(directory = userThemeDirectory()) {
11408
12755
  return { directory: resolvedDirectory, loaded, errors };
11409
12756
  }
11410
12757
  function userThemeDirectory(environment = process.env) {
11411
- return environment.SKEIN_THEME_DIR ?? environment.MOSAIC_THEME_DIR ?? join18(resolveHomeNamespace(environment), "themes");
12758
+ return environment.SKEIN_THEME_DIR ?? environment.MOSAIC_THEME_DIR ?? join19(resolveHomeNamespace(environment), "themes");
11412
12759
  }
11413
12760
  var defaultTheme = themes.graphite;
11414
12761
  var palette = {
@@ -11644,7 +12991,7 @@ function ToolGlyph({ state, glyphs }) {
11644
12991
  if (state === "cancelled") return /* @__PURE__ */ jsx(Text, { color: theme.warning, children: glyphs.warning });
11645
12992
  return /* @__PURE__ */ jsx(Text, { color: theme.error, children: glyphs.error });
11646
12993
  }
11647
- function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact = false }) {
12994
+ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = false, expandedToolId, compact: compact2 = false }) {
11648
12995
  const theme = useTheme();
11649
12996
  const glyphs = resolveGlyphs(glyphMode);
11650
12997
  if (!items.length) {
@@ -11652,13 +12999,13 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11652
12999
  }
11653
13000
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
11654
13001
  if (item.kind === "user") {
11655
- return /* @__PURE__ */ jsxs(Box, { marginBottom: compact || item.clipped ? 0 : 1, children: [
13002
+ return /* @__PURE__ */ jsxs(Box, { marginBottom: compact2 || item.clipped ? 0 : 1, children: [
11656
13003
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
11657
13004
  /* @__PURE__ */ jsx(Text, { bold: true, color: theme.textStrong, wrap: "wrap", children: sanitizeTerminalText(item.text) })
11658
13005
  ] }, item.id);
11659
13006
  }
11660
13007
  if (item.kind === "assistant") {
11661
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact || item.clipped ? 0 : 1, children: [
13008
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: compact2 || item.clipped ? 0 : 1, children: [
11662
13009
  /* @__PURE__ */ jsxs(Text, { bold: true, color: theme.accent, children: [
11663
13010
  glyphs.brand,
11664
13011
  " ",
@@ -11674,7 +13021,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11674
13021
  }
11675
13022
  if (item.kind === "context") {
11676
13023
  const spans = item.spans ?? [];
11677
- const spanLimit = compact ? 2 : 3;
13024
+ const spanLimit = compact2 ? 2 : 3;
11678
13025
  const innerWidth = Math.max(1, safeWidth(width) - 2);
11679
13026
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
11680
13027
  /* @__PURE__ */ jsx(
@@ -11725,7 +13072,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11725
13072
  const duration = item.durationMs !== void 0 ? formatDuration(item.durationMs) : "";
11726
13073
  const detailText = [detail, duration].filter(Boolean).join(" ");
11727
13074
  const expanded = Boolean(item.output) && (showToolOutput || expandedToolId === item.id);
11728
- const verbose = expanded && item.output ? limitTerminalText(item.output, compact ? 24 : 80) : void 0;
13075
+ const verbose = expanded && item.output ? limitTerminalText(item.output, compact2 ? 24 : 80) : void 0;
11729
13076
  const disclosure = item.output ? expanded ? glyphs.expanded : glyphs.collapsed : "";
11730
13077
  const disclosureWidth = disclosure ? displayWidth(disclosure) + 1 : 0;
11731
13078
  const nameLimit = Math.max(1, Math.min(rowWidth - 2 - disclosureWidth, rowWidth < 64 ? rowWidth - 2 - disclosureWidth : 28));
@@ -11859,7 +13206,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11859
13206
  }
11860
13207
  if (item.kind === "list") return /* @__PURE__ */ jsx(ListPanel, { title: item.title, entries: item.entries, width, glyphMode }, item.id);
11861
13208
  if (item.kind === "context-inspector") {
11862
- return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact, glyphMode }, item.id);
13209
+ return /* @__PURE__ */ jsx(ContextInspector, { status: item.status, working: item.working, summary: item.summary, width, compact: compact2, glyphMode }, item.id);
11863
13210
  }
11864
13211
  if (item.kind === "theme") return /* @__PURE__ */ jsx(ThemePreview, { name: item.name, width, glyphs }, item.id);
11865
13212
  if (item.kind === "banner") {
@@ -12069,7 +13416,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
12069
13416
  ] }) : null
12070
13417
  ] });
12071
13418
  }
12072
- function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact = false }) {
13419
+ function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact2 = false }) {
12073
13420
  const theme = useTheme();
12074
13421
  const glyphs = resolveGlyphs(glyphMode);
12075
13422
  const summary = permissionSummary(call);
@@ -12106,7 +13453,7 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
12106
13453
  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
13454
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12108
13455
  /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12109
- ] }) : compact ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
13456
+ ] }) : compact2 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
12110
13457
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
12111
13458
  /* @__PURE__ */ jsx(InlineRow, { parts: compactNarrowShortcuts.slice(2), width: innerWidth, separator: " ", separatorColor: theme.border })
12112
13459
  ] }) : /* @__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 +13714,7 @@ function MeterBar({ segments, total, width, glyphs }) {
12367
13714
  remainder ? /* @__PURE__ */ jsx(Text, { color: theme.border, children: glyphs.meterEmpty.repeat(remainder) }) : null
12368
13715
  ] });
12369
13716
  }
12370
- function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact = false, minimal = false, glyphMode = "auto" }) {
13717
+ function ContextInspector({ status, working, summary, width, memory, connections, sources: sources2, compact: compact2 = false, minimal = false, glyphMode = "auto" }) {
12371
13718
  const theme = useTheme();
12372
13719
  const glyphs = resolveGlyphs(glyphMode);
12373
13720
  if (minimal) {
@@ -12387,10 +13734,10 @@ function ContextInspector({ status, working, summary, width, memory, connections
12387
13734
  { label: "summary", detail: summary ? `~${formatTokens(status.summaryTokens)} tokens ${glyphs.separator} ${status.compactedMessages} compacted` : "not created" },
12388
13735
  { label: "long-term", detail: memory ?? `retrieved by relevance ${glyphs.separator} untrusted context` }
12389
13736
  ];
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} `) });
13737
+ if (!compact2 && working?.constraints.length) entries.push({ label: `constraints ${working.constraints.length}`, detail: working.constraints.slice(0, 2).join(` ${glyphs.separator} `) });
13738
+ if (!compact2 && working?.decisions.length) entries.push({ label: `decisions ${working.decisions.length}`, detail: working.decisions.slice(0, 2).join(` ${glyphs.separator} `) });
13739
+ if (!compact2 && working?.openQuestions.length) entries.push({ label: `open ${working.openQuestions.length}`, detail: working.openQuestions.slice(0, 2).join(` ${glyphs.separator} `), tone: "warning" });
13740
+ if (!compact2 && working?.relevantFiles.length) entries.push({ label: "relevant files", detail: working.relevantFiles.map((file) => compactDisplayPath(sanitizeInlineTerminalText(file), 28)).join(` ${glyphs.separator} `) });
12394
13741
  if (sources2?.length) {
12395
13742
  const pinned = sources2.filter((source) => source.state === "pinned");
12396
13743
  const muted = sources2.filter((source) => source.state === "muted");
@@ -12511,8 +13858,8 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
12511
13858
  function UpdateNotice({ current, latest, command: command2, highlights, width, glyphs }) {
12512
13859
  const theme = useTheme();
12513
13860
  const availableWidth = safeWidth(width);
12514
- const compact = availableWidth < 48;
12515
- const parts = compact ? [
13861
+ const compact2 = availableWidth < 48;
13862
+ const parts = compact2 ? [
12516
13863
  { text: `${glyphs.up} `, color: theme.accent, bold: true },
12517
13864
  { text: `v${current}`, color: theme.dim, bold: false },
12518
13865
  { text: ` ${glyphs.arrow} `, color: theme.muted, bold: false },
@@ -12533,7 +13880,7 @@ function UpdateNotice({ current, latest, command: command2, highlights, width, g
12533
13880
  const bullets = bulletWidth > 0 ? (highlights ?? []).map((line) => truncateDisplay(sanitizeInlineTerminalText(line), bulletWidth)) : [];
12534
13881
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [
12535
13882
  /* @__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,
13883
+ compact2 ? /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(` run ${command2}`, availableWidth) }) : null,
12537
13884
  bullets.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: `${bulletPrefix}${line}` }, index))
12538
13885
  ] });
12539
13886
  }
@@ -12635,9 +13982,9 @@ function safeWidth(width) {
12635
13982
  }
12636
13983
 
12637
13984
  // src/utils/update-check.ts
12638
- import { mkdir as mkdir9, readFile as readFile16, writeFile as writeFile2 } from "node:fs/promises";
12639
- import { join as join19 } from "node:path";
12640
- import stripAnsi2 from "strip-ansi";
13985
+ import { mkdir as mkdir9, readFile as readFile17, writeFile as writeFile2 } from "node:fs/promises";
13986
+ import { join as join20 } from "node:path";
13987
+ import stripAnsi3 from "strip-ansi";
12641
13988
  var PACKAGE_NAME = "@skein-code/cli";
12642
13989
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
12643
13990
  var CACHE_FILE = "update-check.json";
@@ -12652,7 +13999,7 @@ function sanitizeHighlights(value) {
12652
13999
  const cleaned = [];
12653
14000
  for (const entry of value) {
12654
14001
  if (typeof entry !== "string") continue;
12655
- const flattened = stripAnsi2(entry).replace(BIDI_CONTROLS, "").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\s+/gu, " ").trim();
14002
+ const flattened = stripAnsi3(entry).replace(BIDI_CONTROLS, "").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\s+/gu, " ").trim();
12656
14003
  if (!flattened || flattened.length > MAX_HIGHLIGHT_LENGTH) continue;
12657
14004
  cleaned.push(flattened);
12658
14005
  if (cleaned.length >= MAX_HIGHLIGHTS) break;
@@ -12716,7 +14063,7 @@ function compareVersions(a, b) {
12716
14063
  return 0;
12717
14064
  }
12718
14065
  function updateCachePath(env = process.env) {
12719
- return join19(resolveHomeNamespace(env), CACHE_FILE);
14066
+ return join20(resolveHomeNamespace(env), CACHE_FILE);
12720
14067
  }
12721
14068
  function upgradeCommand() {
12722
14069
  return `npm i -g ${PACKAGE_NAME}`;
@@ -12733,7 +14080,7 @@ function noticeIfNewer(latest, current, highlights) {
12733
14080
  }
12734
14081
  async function readUpdateCache(env = process.env) {
12735
14082
  try {
12736
- const raw = await readFile16(updateCachePath(env), "utf8");
14083
+ const raw = await readFile17(updateCachePath(env), "utf8");
12737
14084
  const parsed = JSON.parse(raw);
12738
14085
  if (typeof parsed?.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && parsed.checkedAt >= 0 && (parsed.latest === null || typeof parsed.latest === "string" && parseVersion(parsed.latest))) {
12739
14086
  const latest = typeof parsed.latest === "string" ? parsed.latest : null;
@@ -13441,9 +14788,9 @@ function tailText(value, width, maxRows) {
13441
14788
  return `${marker}
13442
14789
  ${selected.join("\n")}`;
13443
14790
  }
13444
- function estimateTimelineItemRows(item, { width, compact = false, showToolOutput = false, expandedToolId }) {
14791
+ function estimateTimelineItemRows(item, { width, compact: compact2 = false, showToolOutput = false, expandedToolId }) {
13445
14792
  const rowWidth = Math.max(1, Math.floor(width));
13446
- const gap = compact ? 0 : 1;
14793
+ const gap = compact2 ? 0 : 1;
13447
14794
  if (item.kind === "user") return wrappedRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
13448
14795
  if (item.kind === "assistant") {
13449
14796
  return 1 + richTextRows(item.text, Math.max(1, rowWidth - 2)) + (item.clipped ? 0 : gap);
@@ -13455,7 +14802,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13455
14802
  const detail = item.errorDetail || item.detail;
13456
14803
  const detailRows = narrow && detail ? 1 : 0;
13457
14804
  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;
14805
+ const outputRows = (showToolOutput || item.id === expandedToolId) && item.output ? Math.min(compact2 ? 25 : 81, richTextRows(item.output, Math.max(1, rowWidth - 2))) : 0;
13459
14806
  return 1 + detailRows + metaRows + outputRows;
13460
14807
  }
13461
14808
  if (item.kind === "list") {
@@ -13469,7 +14816,7 @@ function estimateTimelineItemRows(item, { width, compact = false, showToolOutput
13469
14816
  if (item.kind === "theme") return 3;
13470
14817
  if (item.kind === "context") {
13471
14818
  const metaRows = rowWidth < 64 ? 2 : 1;
13472
- const spanLimit = compact ? 2 : 3;
14819
+ const spanLimit = compact2 ? 2 : 3;
13473
14820
  const spanCount = Math.min(item.spans?.length ?? 0, spanLimit);
13474
14821
  const moreRow = (item.spans?.length ?? 0) > spanLimit ? 1 : 0;
13475
14822
  const degradationRows = item.degradation ? metaRows : 0;
@@ -13567,6 +14914,21 @@ function finalizeAssistant(items, id, content) {
13567
14914
  function endStreamingAssistants(items) {
13568
14915
  return items.map((item) => item.kind === "assistant" && item.streaming ? { ...item, streaming: false } : item);
13569
14916
  }
14917
+ function updateContractProgress(items, contract, separator = " | ") {
14918
+ const id = "task-contract-progress";
14919
+ if (contract.state === "satisfied") return items.filter((item) => item.id !== id);
14920
+ const required = contract.acceptanceCriteria.filter((item) => item.required);
14921
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
14922
+ const next = {
14923
+ id,
14924
+ kind: "notice",
14925
+ tone: contract.state === "blocked" ? "error" : "info",
14926
+ text: `Contract ${contract.state}${separator}${satisfied}/${required.length} accepted`
14927
+ };
14928
+ const index = items.findIndex((item) => item.id === id);
14929
+ if (index < 0) return [...items, next].slice(-500);
14930
+ return items.map((item, itemIndex) => itemIndex === index ? next : item);
14931
+ }
13570
14932
  function updateAgent(items, event) {
13571
14933
  const found = items.some((item) => item.kind === "agent" && item.id === event.id);
13572
14934
  if (!found) {
@@ -13659,7 +15021,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13659
15021
  const colorEnabled = config.ui.color && !process.env.NO_COLOR;
13660
15022
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
13661
15023
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
13662
- const [compact, setCompact] = useState2(config.ui.compact);
15024
+ const [compact2, setCompact] = useState2(config.ui.compact);
13663
15025
  const [interactionMode, setInteractionMode] = useState2(planMode ? "plan" : askMode ? "ask" : "build");
13664
15026
  const [input2, setInput] = useState2("");
13665
15027
  const [busy, setBusy] = useState2(false);
@@ -13799,7 +15161,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13799
15161
  return () => clearInterval(timer);
13800
15162
  }, [busy]);
13801
15163
  const requestPermission = useCallback((call, category) => {
13802
- return new Promise((resolve23) => setPermission({ call, category, resolve: resolve23 }));
15164
+ return new Promise((resolve24) => setPermission({ call, category, resolve: resolve24 }));
13803
15165
  }, []);
13804
15166
  const onEvent = useCallback((event) => {
13805
15167
  switch (event.type) {
@@ -13859,6 +15221,11 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
13859
15221
  case "tasks":
13860
15222
  setTasks(event.tasks.map((task) => ({ ...task })));
13861
15223
  break;
15224
+ case "contract": {
15225
+ setTimeline((items) => updateContractProgress(items, event.contract, separator));
15226
+ refreshSession();
15227
+ break;
15228
+ }
13862
15229
  case "skill":
13863
15230
  append({ id: nextId(), kind: "skill", name: event.name, description: event.description });
13864
15231
  break;
@@ -14351,7 +15718,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14351
15718
  }
14352
15719
  if (command2 === "density") {
14353
15720
  const normalized = argument.toLocaleLowerCase();
14354
- const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact : void 0;
15721
+ const next = normalized === "compact" ? true : normalized === "comfortable" || normalized === "normal" ? false : normalized === "" || normalized === "toggle" ? !compact2 : void 0;
14355
15722
  if (next === void 0) throw new Error("Usage: /density [compact|comfortable]");
14356
15723
  setCompact(next);
14357
15724
  await saveUiPreference({ compact: next });
@@ -14444,7 +15811,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14444
15811
  })));
14445
15812
  return true;
14446
15813
  }
14447
- }, [append, appendList, compact, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
15814
+ }, [append, appendList, compact2, config, ellipsis, exit, extensions, interactionMode, refreshSession, requestPermission, runner, separator, showToolOutput, tasks, theme, workflows]);
14448
15815
  const submit = useCallback(async (raw, mode = "normal") => {
14449
15816
  const trimmed = raw.trim();
14450
15817
  if (!trimmed) return;
@@ -14618,8 +15985,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14618
15985
  }, [append]);
14619
15986
  function settlePermission(grant, stop = false) {
14620
15987
  if (!permission) return;
14621
- const { call, category, resolve: resolve23 } = permission;
14622
- resolve23(grant);
15988
+ const { call, category, resolve: resolve24 } = permission;
15989
+ resolve24(grant);
14623
15990
  setPermission(void 0);
14624
15991
  if (grant === "session") {
14625
15992
  append({
@@ -14808,7 +16175,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
14808
16175
  const tokenTotal = session.usage.inputTokens + session.usage.outputTokens;
14809
16176
  const contextStatus = runner.getContextStatus();
14810
16177
  const frame = spinnerFrames()[frameIndex % spinnerFrames().length];
14811
- const compactUi = compact || terminalHeight < 28;
16178
+ const compactUi = compact2 || terminalHeight < 28;
14812
16179
  const constrainedHeight = terminalHeight < 18;
14813
16180
  const compactComposer = terminalHeight < 18;
14814
16181
  const minimalInspector = terminalHeight < 22;
@@ -15014,6 +16381,16 @@ function initialTimeline(session, banner, setupProblem) {
15014
16381
  if (!items.length) {
15015
16382
  items.push({ id: nextId(), kind: "banner", model: banner.model, engine: banner.engine, workspace: banner.workspace, version: banner.version });
15016
16383
  }
16384
+ if (session.taskContract && session.taskContract.state !== "satisfied") {
16385
+ const required = session.taskContract.acceptanceCriteria.filter((item) => item.required);
16386
+ const satisfied = required.filter((item) => item.status === "satisfied").length;
16387
+ items.push({
16388
+ id: "task-contract-progress",
16389
+ kind: "notice",
16390
+ tone: session.taskContract.state === "blocked" ? "error" : "info",
16391
+ text: `Contract ${session.taskContract.state} | ${satisfied}/${required.length} accepted`
16392
+ });
16393
+ }
15017
16394
  if (setupProblem && items.length <= 1) items.push({ id: nextId(), kind: "notice", tone: "error", text: setupProblem });
15018
16395
  return items;
15019
16396
  }
@@ -15105,17 +16482,17 @@ function composerAttachments(value) {
15105
16482
  const paths = [...value.matchAll(/(?:^|\s)@([^\s]+)/g)].map((match) => match[1]).filter((path) => Boolean(path));
15106
16483
  return [...new Set(paths)].slice(-3);
15107
16484
  }
15108
- function permissionRows(width, hasCwd, compact) {
16485
+ function permissionRows(width, hasCwd, compact2) {
15109
16486
  const content = 3 + (hasCwd ? 1 : 0);
15110
16487
  if (width >= 64) return content + 2;
15111
16488
  if (width >= 28) return content + 3;
15112
- if (compact) return content + 3;
16489
+ if (compact2) return content + 3;
15113
16490
  return content + 5;
15114
16491
  }
15115
- function contextInspectorRows(session, compact, width, minimal) {
16492
+ function contextInspectorRows(session, compact2, width, minimal) {
15116
16493
  if (minimal) return 2;
15117
16494
  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));
16495
+ 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
16496
  return 2 + entries * (width < 52 ? 2 : 1);
15120
16497
  }
15121
16498
  function contextInspectorStatus(status) {
@@ -15167,14 +16544,14 @@ function WorkspacePreparationView({
15167
16544
  const theme = useTheme();
15168
16545
  const safeWidth2 = Math.max(1, Math.floor(width));
15169
16546
  const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
15170
- const compact = safeWidth2 < 48;
16547
+ const compact2 = safeWidth2 < 48;
15171
16548
  const constrained = height < 14;
15172
16549
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15173
16550
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
15174
16551
  const separator = ascii ? "|" : "\xB7";
15175
16552
  const brand = ascii ? "*" : PRODUCT_MARK;
15176
16553
  const phase = readiness ? "ready" : error ? "error" : progress.phase;
15177
- const phaseLabel = preparationLabel(phase, progress, readiness, compact);
16554
+ const phaseLabel = preparationLabel(phase, progress, readiness, compact2);
15178
16555
  const detail = preparationDetail(phase, progress, readiness, error);
15179
16556
  const modelLine = `model ${sanitizeTerminalText(model)}`;
15180
16557
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
@@ -15186,14 +16563,14 @@ function WorkspacePreparationView({
15186
16563
  " ",
15187
16564
  PRODUCT_NAME.toUpperCase()
15188
16565
  ] }),
15189
- !compact && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
16566
+ !compact2 && !constrained ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
15190
16567
  " ",
15191
16568
  separator,
15192
16569
  " LOCAL CONTEXT"
15193
16570
  ] }) : null
15194
16571
  ] }),
15195
16572
  !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: [
16573
+ !constrained && !compact2 ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(`${modelLine} ${separator} ${workspaceLine}`, innerWidth) }) : !constrained ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
15197
16574
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
15198
16575
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
15199
16576
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
@@ -15207,10 +16584,10 @@ function WorkspacePreparationView({
15207
16584
  step2.glyph,
15208
16585
  " "
15209
16586
  ] }),
15210
- /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact ? 8 : 10) }),
16587
+ /* @__PURE__ */ jsx4(Text4, { bold: step2.state === "active", color: step2.state === "pending" ? theme.dim : theme.textStrong, children: truncateDisplay(step2.label, compact2 ? 8 : 10) }),
15211
16588
  /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.muted : theme.dim, children: [
15212
16589
  " ",
15213
- truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact ? 12 : 14)))
16590
+ truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact2 ? 12 : 14)))
15214
16591
  ] })
15215
16592
  ] }, step2.id)) }),
15216
16593
  /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
@@ -15351,9 +16728,9 @@ async function runWorkspacePreparation(engine, config, options = {}) {
15351
16728
  await instance.waitUntilExit();
15352
16729
  return result ?? { status: "cancelled" };
15353
16730
  }
15354
- function preparationLabel(phase, progress, readiness, compact = false) {
16731
+ function preparationLabel(phase, progress, readiness, compact2 = false) {
15355
16732
  if (phase === "ready") {
15356
- if (compact) return "Index verified";
16733
+ if (compact2) return "Index verified";
15357
16734
  return readiness?.rebuilt ? "Workspace index created and verified" : "Workspace index verified";
15358
16735
  }
15359
16736
  if (phase === "error") return "Workspace preparation failed";
@@ -15757,7 +17134,7 @@ function OnboardingFlow({ initialConfig, saveConfig, onFinish }) {
15757
17134
  }, [finish, saveConfig, state]);
15758
17135
  return /* @__PURE__ */ jsx5(OnboardingScreen, { state, dispatch, width, compact: compactHeight });
15759
17136
  }
15760
- function OnboardingScreen({ state, dispatch, width, compact = false }) {
17137
+ function OnboardingScreen({ state, dispatch, width, compact: compact2 = false }) {
15761
17138
  const theme = useTheme();
15762
17139
  const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
15763
17140
  const marker = ascii ? ">" : "\u203A";
@@ -15774,14 +17151,14 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15774
17151
  ] }),
15775
17152
  /* @__PURE__ */ jsx5(Text6, { color: theme.border, children: truncateDisplay(stageDivider(ascii, headerWidth), headerWidth) }),
15776
17153
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(stage.name, headerWidth) }),
15777
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
17154
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15778
17155
  /* @__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,
17156
+ !compact2 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
15780
17157
  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,
17158
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
17159
+ state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
17160
+ state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
17161
+ state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact: compact2 }) : null,
15785
17162
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
15786
17163
  /* @__PURE__ */ jsxs5(Box4, { children: [
15787
17164
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -15809,18 +17186,18 @@ function OnboardingScreen({ state, dispatch, width, compact = false }) {
15809
17186
  "! ",
15810
17187
  truncateDisplay(state.error, Math.max(1, headerWidth - 2))
15811
17188
  ] }) : null,
15812
- !compact ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
17189
+ !compact2 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
15813
17190
  /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: footerForStep(state, headerWidth) })
15814
17191
  ] });
15815
17192
  }
15816
- function OptionList({ options, selected, marker, width, compact }) {
17193
+ function OptionList({ options, selected, marker, width, compact: compact2 }) {
15817
17194
  const theme = useTheme();
15818
17195
  return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", children: options.map((option, index) => {
15819
17196
  const active = index === selected;
15820
17197
  const prefix = active ? `${marker} ` : " ";
15821
17198
  const available = Math.max(1, width - displayWidth(prefix) - (active ? 2 : 0));
15822
17199
  const label = `${prefix}${truncateDisplay(option.label, available)}`;
15823
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact || index === options.length - 1 ? 0 : 1, children: [
17200
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", marginBottom: compact2 || index === options.length - 1 ? 0 : 1, children: [
15824
17201
  /* @__PURE__ */ jsx5(
15825
17202
  Text6,
15826
17203
  {
@@ -15830,7 +17207,7 @@ function OptionList({ options, selected, marker, width, compact }) {
15830
17207
  children: active ? padDisplay(label, width) : label
15831
17208
  }
15832
17209
  ),
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
17210
+ !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
17211
  ] }, option.label);
15835
17212
  }) });
15836
17213
  }
@@ -15944,24 +17321,24 @@ async function runFirstRunOnboarding(initialConfig, options = {}) {
15944
17321
  }
15945
17322
 
15946
17323
  // src/runtime/extensions.ts
15947
- import { resolve as resolve21 } from "node:path";
17324
+ import { resolve as resolve22 } from "node:path";
15948
17325
 
15949
17326
  // src/mcp/manager.ts
15950
17327
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
15951
17328
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
15952
17329
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15953
- import stripAnsi4 from "strip-ansi";
17330
+ import stripAnsi5 from "strip-ansi";
15954
17331
 
15955
17332
  // src/mcp/tool.ts
15956
- import { createHash as createHash12 } from "node:crypto";
15957
- import stripAnsi3 from "strip-ansi";
17333
+ import { createHash as createHash14 } from "node:crypto";
17334
+ import stripAnsi4 from "strip-ansi";
15958
17335
  var MAX_ARGUMENT_BYTES = 256e3;
15959
17336
  var MAX_DESCRIPTION_LENGTH = 4e3;
15960
- var MAX_RESULT_LENGTH = 8e4;
17337
+ var MAX_RESULT_BYTES = 5 * 1024 * 1024;
15961
17338
  var MAX_SCHEMA_BYTES = 1e5;
15962
17339
  function createMcpToolAdapter(options) {
15963
17340
  const { remoteTool } = options;
15964
- const inputSchema10 = copyInputSchema(remoteTool.inputSchema);
17341
+ const inputSchema12 = copyInputSchema(remoteTool.inputSchema);
15965
17342
  return {
15966
17343
  definition: {
15967
17344
  name: options.exposedName,
@@ -15969,7 +17346,7 @@ function createMcpToolAdapter(options) {
15969
17346
  // MCP servers are an external trust boundary. Read-only annotations are
15970
17347
  // hints from that server and must not lower the local permission level.
15971
17348
  category: "network",
15972
- inputSchema: inputSchema10
17349
+ inputSchema: inputSchema12
15973
17350
  },
15974
17351
  permissionCategories: () => ["network"],
15975
17352
  async execute(arguments_, context) {
@@ -15989,6 +17366,8 @@ function createMcpToolAdapter(options) {
15989
17366
  metadata: {
15990
17367
  mcpServer: options.serverName,
15991
17368
  mcpTool: remoteTool.name,
17369
+ sourceBytes: normalized.sourceBytes,
17370
+ sourceTruncated: normalized.sourceTruncated,
15992
17371
  ...normalized.isError ? { mcpError: true } : {}
15993
17372
  }
15994
17373
  };
@@ -16018,8 +17397,8 @@ function isUsableRemoteTool(tool) {
16018
17397
  }
16019
17398
  }
16020
17399
  function describeTool(serverName, tool) {
16021
- const label = stripAnsi3(tool.title?.trim() || tool.name).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 200);
16022
- const description = tool.description ? stripAnsi3(tool.description).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim().slice(0, MAX_DESCRIPTION_LENGTH) : void 0;
17400
+ const label = stripAnsi4(tool.title?.trim() || tool.name).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 200);
17401
+ const description = tool.description ? stripAnsi4(tool.description).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim().slice(0, MAX_DESCRIPTION_LENGTH) : void 0;
16023
17402
  return description ? `[MCP ${serverName}/${label}] ${description}` : `Call the ${label} tool provided by the ${serverName} MCP server.`;
16024
17403
  }
16025
17404
  function copyInputSchema(schema) {
@@ -16042,7 +17421,7 @@ function assertArguments(arguments_) {
16042
17421
  }
16043
17422
  function normalizeCallResult(result) {
16044
17423
  if (!isRecord2(result)) {
16045
- return { content: truncateResult(safeJson(result)), isError: false };
17424
+ return boundResult(safeJson(result), false);
16046
17425
  }
16047
17426
  const isError = result.isError === true;
16048
17427
  const sections = [];
@@ -16058,7 +17437,22 @@ ${safeJson(result.structuredContent)}`);
16058
17437
  ${safeJson(result.toolResult)}`);
16059
17438
  }
16060
17439
  const content = sections.filter(Boolean).join("\n\n") || (isError ? "The MCP tool reported an error." : "The MCP tool completed without output.");
16061
- return { content: truncateResult(content), isError };
17440
+ return boundResult(content, isError);
17441
+ }
17442
+ function boundResult(content, isError) {
17443
+ const sourceBytes = Buffer.byteLength(content);
17444
+ if (sourceBytes <= MAX_RESULT_BYTES) {
17445
+ return { content, isError, sourceBytes, sourceTruncated: false };
17446
+ }
17447
+ const marker = `
17448
+ ... ${sourceBytes - MAX_RESULT_BYTES} or more source bytes omitted by the MCP safety limit ...
17449
+ `;
17450
+ const markerBytes = Buffer.byteLength(marker);
17451
+ const available = MAX_RESULT_BYTES - markerBytes;
17452
+ const headBytes = Math.floor(available * 0.6);
17453
+ const tailBytes = available - headBytes;
17454
+ const bounded = `${sliceUtf8Start(content, headBytes)}${marker}${sliceUtf8End(content, tailBytes)}`;
17455
+ return { content: bounded, isError, sourceBytes, sourceTruncated: true };
16062
17456
  }
16063
17457
  function formatContentBlock(block) {
16064
17458
  if (!isRecord2(block) || typeof block.type !== "string") return safeJson(block);
@@ -16106,15 +17500,10 @@ function fitToolName(name, identity) {
16106
17500
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
16107
17501
  }
16108
17502
  function shortHash(value) {
16109
- return createHash12("sha256").update(value).digest("hex").slice(0, 8);
16110
- }
16111
- function truncateResult(content) {
16112
- if (content.length <= MAX_RESULT_LENGTH) return content;
16113
- return `${content.slice(0, MAX_RESULT_LENGTH)}
16114
- ... MCP result truncated`;
17503
+ return createHash14("sha256").update(value).digest("hex").slice(0, 8);
16115
17504
  }
16116
17505
  function sanitizeOutputText(value) {
16117
- return stripAnsi3(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
17506
+ return stripAnsi4(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
16118
17507
  }
16119
17508
  function sanitizeInlineText(value) {
16120
17509
  return sanitizeOutputText(value).replace(/[\r\n\t]+/g, " ").trim().slice(0, 2e3);
@@ -16126,13 +17515,25 @@ function safeJson(value) {
16126
17515
  return String(value);
16127
17516
  }
16128
17517
  }
17518
+ function sliceUtf8Start(value, maxBytes) {
17519
+ const buffer = Buffer.from(value);
17520
+ let end = Math.min(buffer.length, maxBytes);
17521
+ while (end > 0 && (buffer[end] ?? 0) >= 128 && (buffer[end] ?? 0) < 192) end -= 1;
17522
+ return buffer.subarray(0, end).toString("utf8");
17523
+ }
17524
+ function sliceUtf8End(value, maxBytes) {
17525
+ const buffer = Buffer.from(value);
17526
+ let start = Math.max(0, buffer.length - maxBytes);
17527
+ while (start < buffer.length && (buffer[start] ?? 0) >= 128 && (buffer[start] ?? 0) < 192) start += 1;
17528
+ return buffer.subarray(start).toString("utf8");
17529
+ }
16129
17530
  function isRecord2(value) {
16130
17531
  return typeof value === "object" && value !== null && !Array.isArray(value);
16131
17532
  }
16132
17533
 
16133
17534
  // src/mcp/validation.ts
16134
17535
  import { stat as stat10, realpath as realpath8 } from "node:fs/promises";
16135
- import { isAbsolute as isAbsolute6, resolve as resolve19 } from "node:path";
17536
+ import { isAbsolute as isAbsolute6, resolve as resolve20 } from "node:path";
16136
17537
  var SERVER_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
16137
17538
  var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16138
17539
  var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
@@ -16204,9 +17605,9 @@ async function validateCwd(configured, options) {
16204
17605
  if (configured !== void 0 && (configured.length > 4e3 || CONTROL_CHARACTERS.test(configured))) {
16205
17606
  throw new Error("MCP working directory is invalid or too long");
16206
17607
  }
16207
- const defaultCwd = resolve19(options.cwd ?? process.cwd());
16208
- const candidate = configured ? isAbsolute6(configured) ? resolve19(configured) : resolve19(defaultCwd, configured) : defaultCwd;
16209
- const roots = options.workspaceRoots?.length ? options.workspaceRoots.map((root) => resolve19(root)) : [defaultCwd];
17608
+ const defaultCwd = resolve20(options.cwd ?? process.cwd());
17609
+ const candidate = configured ? isAbsolute6(configured) ? resolve20(configured) : resolve20(defaultCwd, configured) : defaultCwd;
17610
+ const roots = options.workspaceRoots?.length ? options.workspaceRoots.map((root) => resolve20(root)) : [defaultCwd];
16210
17611
  const resolvedCandidate = await realpath8(candidate).catch(() => {
16211
17612
  throw new Error(`MCP working directory does not exist: ${candidate}`);
16212
17613
  });
@@ -16544,7 +17945,7 @@ var McpManager = class {
16544
17945
  stderr: "pipe"
16545
17946
  });
16546
17947
  transport.stderr?.on("data", (chunk) => {
16547
- const text = stripAnsi4(chunk.toString()).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
17948
+ const text = stripAnsi5(chunk.toString()).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
16548
17949
  if (text) this.options.logger?.(`MCP ${name} stderr`, { text: text.slice(0, 2e3) });
16549
17950
  });
16550
17951
  return transport;
@@ -16703,7 +18104,7 @@ function errorMessage2(error) {
16703
18104
  return sanitizeStatusText(message2) || "Unknown MCP error";
16704
18105
  }
16705
18106
  function sanitizeStatusText(value) {
16706
- return stripAnsi4(value).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 1e3);
18107
+ return stripAnsi5(value).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 1e3);
16707
18108
  }
16708
18109
  function timeoutError(timeoutMs) {
16709
18110
  const error = new Error(`MCP request timed out after ${timeoutMs} ms`);
@@ -16729,9 +18130,9 @@ async function closeTransportQuietly(transport) {
16729
18130
  }
16730
18131
 
16731
18132
  // 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"]);
18133
+ import { z as z19 } from "zod";
18134
+ var scopeSchema = z19.enum(["user", "workspace", "session", "agent"]);
18135
+ var kindSchema = z19.enum(["semantic", "episodic", "procedural"]);
16735
18136
  function createMemoryTools(store) {
16736
18137
  return [
16737
18138
  {
@@ -16746,10 +18147,10 @@ function createMemoryTools(store) {
16746
18147
  }, ["query"])
16747
18148
  },
16748
18149
  async execute(arguments_, context) {
16749
- const input2 = z16.object({
16750
- query: z16.string().max(4e3),
18150
+ const input2 = z19.object({
18151
+ query: z19.string().max(4e3),
16751
18152
  scope: scopeSchema.optional(),
16752
- limit: z16.number().int().min(1).max(20).optional()
18153
+ limit: z19.number().int().min(1).max(20).optional()
16753
18154
  }).parse(arguments_);
16754
18155
  const scopes = input2.scope ? [{ scope: input2.scope, scopeKey: scopeKey(input2.scope, context) }] : availableScopes(context);
16755
18156
  const records = store.search(input2.query, { scopes, limit: input2.limit ?? 8 });
@@ -16781,17 +18182,17 @@ ${record.content}`
16781
18182
  }, ["content", "rationale"])
16782
18183
  },
16783
18184
  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),
18185
+ const input2 = z19.object({
18186
+ content: z19.string().min(1).max(12e3),
18187
+ rationale: z19.string().min(1).max(1e3),
16787
18188
  scope: scopeSchema.optional(),
16788
18189
  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()
18190
+ tags: z19.array(z19.string()).max(24).optional(),
18191
+ importance: z19.number().min(0).max(1).optional(),
18192
+ confidence: z19.number().min(0).max(1).optional(),
18193
+ agent: z19.string().max(64).optional(),
18194
+ revision: z19.string().max(240).optional(),
18195
+ conflictKey: z19.string().max(240).optional()
16795
18196
  }).strict().parse(arguments_);
16796
18197
  const scope = input2.scope ?? "workspace";
16797
18198
  const candidate = store.propose({
@@ -16829,9 +18230,9 @@ ${record.content}`
16829
18230
  }, ["id"])
16830
18231
  },
16831
18232
  async execute(arguments_) {
16832
- const input2 = z16.object({
16833
- id: z16.string().uuid(),
16834
- permanent: z16.boolean().optional()
18233
+ const input2 = z19.object({
18234
+ id: z19.string().uuid(),
18235
+ permanent: z19.boolean().optional()
16835
18236
  }).parse(arguments_);
16836
18237
  const changed = input2.permanent ? store.remove(input2.id) : store.archive(input2.id);
16837
18238
  return {
@@ -16858,9 +18259,9 @@ function scopeKey(scope, context) {
16858
18259
  }
16859
18260
 
16860
18261
  // src/skills/catalog.ts
16861
- import { lstat as lstat20, readFile as readFile17, readdir as readdir8, realpath as realpath9 } from "node:fs/promises";
18262
+ import { lstat as lstat21, readFile as readFile18, readdir as readdir9, realpath as realpath9 } from "node:fs/promises";
16862
18263
  import { homedir as homedir4 } from "node:os";
16863
- import { basename as basename11, join as join20, resolve as resolve20 } from "node:path";
18264
+ import { basename as basename11, join as join21, resolve as resolve21 } from "node:path";
16864
18265
  import { parse as parseYaml3 } from "yaml";
16865
18266
  var SkillCatalog = class {
16866
18267
  constructor(workspace, config) {
@@ -16880,7 +18281,7 @@ var SkillCatalog = class {
16880
18281
  for (const location of locations) {
16881
18282
  const entries = await safeDirectories(location.path);
16882
18283
  for (const entry of entries) {
16883
- const skillPath = join20(location.path, entry, "SKILL.md");
18284
+ const skillPath = join21(location.path, entry, "SKILL.md");
16884
18285
  const metadata = await readMetadata(skillPath);
16885
18286
  if (!metadata) continue;
16886
18287
  const descriptor = {
@@ -16929,31 +18330,31 @@ ${skill.content}
16929
18330
  }
16930
18331
  function discoveryLocations(workspace, configured) {
16931
18332
  const home = homedir4();
16932
- const workspaceRoot = resolve20(workspace);
18333
+ const workspaceRoot = resolve21(workspace);
16933
18334
  return [
16934
- { path: join20(home, ".agents", "skills"), scope: "user", trusted: true },
16935
- { path: join20(home, ".claude", "skills"), scope: "user", trusted: true },
16936
- { path: join20(home, ".augment", "skills"), scope: "user", trusted: true },
16937
- { path: join20(resolveHomeNamespace(), "skills"), scope: "user", trusted: true },
18335
+ { path: join21(home, ".agents", "skills"), scope: "user", trusted: true },
18336
+ { path: join21(home, ".claude", "skills"), scope: "user", trusted: true },
18337
+ { path: join21(home, ".augment", "skills"), scope: "user", trusted: true },
18338
+ { path: join21(resolveHomeNamespace(), "skills"), scope: "user", trusted: true },
16938
18339
  ...configured.map((path) => {
16939
- const resolved = resolve20(workspaceRoot, path);
18340
+ const resolved = resolve21(workspaceRoot, path);
16940
18341
  return {
16941
18342
  path: resolved,
16942
18343
  scope: "configured",
16943
18344
  trusted: !isInside(workspaceRoot, resolved)
16944
18345
  };
16945
18346
  }),
16946
- { path: join20(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
16947
- { path: join20(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
16948
- { path: join20(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
16949
- { path: join20(resolveProjectNamespaceSync(workspaceRoot).active, "skills"), scope: "workspace", trusted: false }
18347
+ { path: join21(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
18348
+ { path: join21(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
18349
+ { path: join21(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
18350
+ { path: join21(resolveProjectNamespaceSync(workspaceRoot).active, "skills"), scope: "workspace", trusted: false }
16950
18351
  ];
16951
18352
  }
16952
18353
  async function safeDirectories(path) {
16953
18354
  try {
16954
- const info = await lstat20(path);
18355
+ const info = await lstat21(path);
16955
18356
  if (!info.isDirectory() || info.isSymbolicLink()) return [];
16956
- const entries = await readdir8(path, { withFileTypes: true });
18357
+ const entries = await readdir9(path, { withFileTypes: true });
16957
18358
  return entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).slice(0, 500);
16958
18359
  } catch {
16959
18360
  return [];
@@ -16965,7 +18366,7 @@ async function readMetadata(path) {
16965
18366
  const { frontmatter } = splitFrontmatter(raw);
16966
18367
  if (!frontmatter) return void 0;
16967
18368
  const parsed = parseYaml3(frontmatter);
16968
- const name = typeof parsed?.name === "string" ? parsed.name.trim() : basename11(resolve20(path, ".."));
18369
+ const name = typeof parsed?.name === "string" ? parsed.name.trim() : basename11(resolve21(path, ".."));
16969
18370
  const description = typeof parsed?.description === "string" ? parsed.description.trim() : "";
16970
18371
  if (!/^[a-z][a-z0-9_-]{0,63}$/.test(name) || !description || description.length > 1e3) {
16971
18372
  return void 0;
@@ -16984,13 +18385,13 @@ async function readSkill(path, maxChars) {
16984
18385
  }
16985
18386
  async function safeRead(path, maxBytes) {
16986
18387
  try {
16987
- const info = await lstat20(path);
18388
+ const info = await lstat21(path);
16988
18389
  if (!info.isFile() || info.isSymbolicLink() || info.size > maxBytes) return void 0;
16989
- const parent = resolve20(path, "..");
18390
+ const parent = resolve21(path, "..");
16990
18391
  const resolvedParent = await realpath9(parent);
16991
18392
  const resolvedPath = await realpath9(path);
16992
18393
  if (!resolvedPath.startsWith(`${resolvedParent}/`)) return void 0;
16993
- return await readFile17(path, "utf8");
18394
+ return await readFile18(path, "utf8");
16994
18395
  } catch {
16995
18396
  return void 0;
16996
18397
  }
@@ -17035,7 +18436,7 @@ function escapeAttribute6(value) {
17035
18436
  }
17036
18437
 
17037
18438
  // src/workflows/catalog.ts
17038
- import { z as z17 } from "zod";
18439
+ import { z as z20 } from "zod";
17039
18440
  var builtInWorkflows = [
17040
18441
  {
17041
18442
  name: "implement",
@@ -17126,7 +18527,7 @@ function createWorkflowTool(catalog) {
17126
18527
  }, ["name", "task"])
17127
18528
  },
17128
18529
  async execute(arguments_) {
17129
- const input2 = z17.object({ name: z17.string(), task: z17.string().min(1).max(2e4) }).parse(arguments_);
18530
+ const input2 = z20.object({ name: z20.string(), task: z20.string().min(1).max(2e4) }).parse(arguments_);
17130
18531
  const workflow = catalog.get(input2.name);
17131
18532
  if (!workflow) return { ok: false, content: `Unknown workflow: ${input2.name}` };
17132
18533
  return {
@@ -17171,7 +18572,7 @@ var ExtensionRuntime = class _ExtensionRuntime {
17171
18572
  delegation;
17172
18573
  initialized = false;
17173
18574
  static async create(config, registry, options = {}) {
17174
- const runtime = new _ExtensionRuntime(config, resolve21(config.workspaceRoots[0] ?? process.cwd()), options);
18575
+ const runtime = new _ExtensionRuntime(config, resolve22(config.workspaceRoots[0] ?? process.cwd()), options);
17175
18576
  try {
17176
18577
  await runtime.initialize(registry, options.signal);
17177
18578
  return runtime;
@@ -17376,15 +18777,15 @@ function upgradeCommandOverride(env = process.env) {
17376
18777
  return trimmed ? trimmed : void 0;
17377
18778
  }
17378
18779
  function runUpgrade(plan) {
17379
- return new Promise((resolve23) => {
18780
+ return new Promise((resolve24) => {
17380
18781
  const child = spawn2(plan.command, plan.args, {
17381
18782
  stdio: "inherit",
17382
18783
  shell: plan.shell ?? false
17383
18784
  });
17384
- child.on("error", () => resolve23({ ok: false, exitCode: 127, display: plan.display }));
18785
+ child.on("error", () => resolve24({ ok: false, exitCode: 127, display: plan.display }));
17385
18786
  child.on("close", (code) => {
17386
18787
  const exitCode = code ?? 1;
17387
- resolve23({ ok: exitCode === 0, exitCode, display: plan.display });
18788
+ resolve24({ ok: exitCode === 0, exitCode, display: plan.display });
17388
18789
  });
17389
18790
  });
17390
18791
  }
@@ -17633,9 +19034,11 @@ sessionCommand.command("show <id>").description("Show a saved session transcript
17633
19034
  else process.stdout.write(sessionMarkdown(session));
17634
19035
  });
17635
19036
  sessionCommand.command("delete <id>").description("Delete a saved session").option("-w, --workspace <path>", "workspace root").option("--yes", "skip confirmation").action(async (id, options) => {
17636
- const store = new SessionStore(workspaceOption(options.workspace));
19037
+ const workspace = workspaceOption(options.workspace);
19038
+ const store = new SessionStore(workspace);
17637
19039
  const session = await requireSessionSelector(store, id);
17638
19040
  if (!options.yes && !await confirm(`Delete session ${session.id.slice(0, 8)}?`)) return;
19041
+ await new ToolArtifactStore(workspace).removeSession(session.id);
17639
19042
  await store.remove(session.id);
17640
19043
  process.stdout.write(`Deleted ${session.id}
17641
19044
  `);
@@ -17644,7 +19047,7 @@ sessionCommand.command("export <id>").description("Export a session as Markdown"
17644
19047
  const store = new SessionStore(workspaceOption(options.workspace));
17645
19048
  const session = await requireSessionSelector(store, id);
17646
19049
  const markdown = sessionMarkdown(session);
17647
- if (options.output) await writeFile3(resolve22(options.output), markdown, "utf8");
19050
+ if (options.output) await writeFile3(resolve23(options.output), markdown, "utf8");
17648
19051
  else process.stdout.write(markdown);
17649
19052
  });
17650
19053
  var checkpointCommand = program.command("checkpoint").description("Inspect and restore pre-mutation snapshots");
@@ -18021,7 +19424,7 @@ async function runChat(prompts, options) {
18021
19424
  const stdinPrompt = !process.stdin.isTTY ? await readStdin() : "";
18022
19425
  const firstPrompt = [...prompts, stdinPrompt].filter(Boolean).join("\n\n").trim();
18023
19426
  if (shouldPrint && !firstPrompt) throw new Error("Provide a prompt argument or pipe input on stdin.");
18024
- const workspace = resolve22(options.workspace);
19427
+ const workspace = resolve23(options.workspace);
18025
19428
  let config = await runtimeConfig(workspace, options);
18026
19429
  let completedOnboarding = false;
18027
19430
  if (!shouldPrint && needsFirstRunOnboarding(config)) {
@@ -18246,14 +19649,14 @@ async function runAgentSetup(options) {
18246
19649
  `);
18247
19650
  }
18248
19651
  async function runtimeConfig(workspaceInput, options) {
18249
- const workspace = resolve22(workspaceInput);
19652
+ const workspace = resolve23(workspaceInput);
18250
19653
  const loaded = await loadConfig(workspace, options.config, {
18251
19654
  trustProjectConfig: options.trustProjectConfig === true
18252
19655
  });
18253
19656
  const roots = [
18254
19657
  workspace,
18255
19658
  ...loaded.workspaceRoots,
18256
- ...(options.addWorkspace ?? []).map((root) => resolve22(workspace, root))
19659
+ ...(options.addWorkspace ?? []).map((root) => resolve23(workspace, root))
18257
19660
  ];
18258
19661
  const provider = options.provider ? validateProvider(options.provider) : loaded.model.provider;
18259
19662
  return {
@@ -18409,7 +19812,7 @@ function collect(value, previous) {
18409
19812
  }
18410
19813
  function workspaceOption(value) {
18411
19814
  const rootOptions = program.opts();
18412
- return resolve22(value ?? rootOptions.workspace ?? process.cwd());
19815
+ return resolve23(value ?? rootOptions.workspace ?? process.cwd());
18413
19816
  }
18414
19817
  function runtimeOptions(options) {
18415
19818
  const root = program.opts();