@skein-code/cli 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.14",
223
+ version: "0.3.16",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,12 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "Duplication warnings now aggregate into the single completion receipt without changing verified status",
240
+ "Exact match ids support bounded, reason-coded suppression with audit and credential/code-block safeguards",
241
+ "Duplication audit tools are disclosed only when active warnings exist and repaired paths clear stale matches",
242
+ "Warning-only TS/JS duplication audit compares new functions with the pre-write index generation",
243
+ "Deterministic Type-1/2 and Type-3 receipts retain only hashes, locations, scores, and bounded counts",
244
+ "Ordinary edits, renames, moves, deletions, small functions, tests, generated paths, and failed writes stay quiet",
239
245
  "Warning-only Reuse Gate records content-free candidate evidence before the first substantive write",
240
246
  "Reuse receipts surface unresolved index/read failures instead of claiming a safe new implementation",
241
247
  "Prompt and timeline diagnostics expose the repository-first reuse ladder without blocking legitimate writes",
@@ -993,16 +999,16 @@ async function hashRegularFile(path) {
993
999
  try {
994
1000
  const info = await handle.stat();
995
1001
  if (!info.isFile()) throw new Error(`Namespace entry is not a regular file: ${path}`);
996
- const hash3 = createHash2("sha256");
1002
+ const hash4 = createHash2("sha256");
997
1003
  const buffer = Buffer.allocUnsafe(64 * 1024);
998
1004
  let position = 0;
999
1005
  while (true) {
1000
1006
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
1001
1007
  if (!bytesRead) break;
1002
- hash3.update(buffer.subarray(0, bytesRead));
1008
+ hash4.update(buffer.subarray(0, bytesRead));
1003
1009
  position += bytesRead;
1004
1010
  }
1005
- return { sha256: hash3.digest("hex"), size: position };
1011
+ return { sha256: hash4.digest("hex"), size: position };
1006
1012
  } finally {
1007
1013
  await handle.close();
1008
1014
  }
@@ -1203,18 +1209,18 @@ var MemoryStore = class {
1203
1209
  input2.lastVerifiedAt ?? (explicit ? now : void 0),
1204
1210
  "Memory verification time"
1205
1211
  );
1206
- const hash3 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1212
+ const hash4 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1207
1213
  database.exec("BEGIN IMMEDIATE");
1208
1214
  try {
1209
1215
  const existing = database.prepare(
1210
1216
  "SELECT * FROM memories WHERE content_hash = ?"
1211
- ).get(hash3);
1217
+ ).get(hash4);
1212
1218
  const conflicting = conflictKey ? database.prepare(`
1213
1219
  SELECT * FROM memories
1214
1220
  WHERE scope = ? AND scope_key = ? AND conflict_key = ?
1215
1221
  AND status = 'active' AND content_hash <> ?
1216
1222
  ORDER BY updated_at DESC
1217
- `).all(input2.scope, scopeKey2, conflictKey, hash3) : [];
1223
+ `).all(input2.scope, scopeKey2, conflictKey, hash4) : [];
1218
1224
  const supersedesId = normalizeOptional(input2.supersedesId, 80) ?? conflicting[0]?.id;
1219
1225
  let id;
1220
1226
  if (existing) {
@@ -1259,7 +1265,7 @@ var MemoryStore = class {
1259
1265
  importance,
1260
1266
  confidence,
1261
1267
  source,
1262
- hash3,
1268
+ hash4,
1263
1269
  now,
1264
1270
  now,
1265
1271
  now,
@@ -1362,10 +1368,10 @@ var MemoryStore = class {
1362
1368
  const conflictKey = normalizeOptional(input2.conflictKey, 240);
1363
1369
  const candidateDefaultExpiry = !input2.expiresAt ? new Date(Date.now() + MEMORY_CANDIDATE_TTL_MS).toISOString() : void 0;
1364
1370
  const expiresAt = normalizeTimestamp(input2.expiresAt ?? candidateDefaultExpiry, "Memory expiration");
1365
- const hash3 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1371
+ const hash4 = createHash3("sha256").update(`${input2.scope}\0${scopeKey2}\0${content.toLocaleLowerCase()}`).digest("hex");
1366
1372
  const existing = database.prepare(
1367
1373
  "SELECT * FROM memory_candidates WHERE content_hash = ?"
1368
- ).get(hash3);
1374
+ ).get(hash4);
1369
1375
  const now = (/* @__PURE__ */ new Date()).toISOString();
1370
1376
  if (existing) {
1371
1377
  if (existing.status === "approved" && existing.approved_memory_id) {
@@ -1412,7 +1418,7 @@ var MemoryStore = class {
1412
1418
  confidence,
1413
1419
  source,
1414
1420
  rationale,
1415
- hash3,
1421
+ hash4,
1416
1422
  now,
1417
1423
  now,
1418
1424
  revision ?? null,
@@ -1699,8 +1705,8 @@ async function rejectSymlink(path, allowMissing = false) {
1699
1705
  throw error;
1700
1706
  }
1701
1707
  }
1702
- function clamp(value, minimum, maximum) {
1703
- return Math.min(maximum, Math.max(minimum, value));
1708
+ function clamp(value, minimum2, maximum) {
1709
+ return Math.min(maximum, Math.max(minimum2, value));
1704
1710
  }
1705
1711
 
1706
1712
  // src/tools/write.ts
@@ -2594,9 +2600,9 @@ function countExplicitPaths(value) {
2594
2600
  }
2595
2601
 
2596
2602
  // src/context/local-index.ts
2597
- import { createHash as createHash5 } from "node:crypto";
2603
+ import { createHash as createHash6 } from "node:crypto";
2598
2604
  import { lstat as lstat8, readFile as readFile3, stat as stat3 } from "node:fs/promises";
2599
- import { basename as basename5, dirname as dirname7, extname, isAbsolute as isAbsolute3, join as join7, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
2605
+ import { basename as basename5, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, join as join7, relative as relative5, resolve as resolve8, sep as sep4 } from "node:path";
2600
2606
  import fg from "fast-glob";
2601
2607
  import { z as z3 } from "zod";
2602
2608
 
@@ -2765,6 +2771,269 @@ function isEmojiOrSupplementarySymbol(character, codePoint) {
2765
2771
  return codePoint > 65535 || new RegExp("\\p{Extended_Pictographic}", "u").test(character);
2766
2772
  }
2767
2773
 
2774
+ // src/context/function-fingerprint.ts
2775
+ import { createHash as createHash5 } from "node:crypto";
2776
+ import { extname } from "node:path";
2777
+ var SUPPORTED = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
2778
+ var MIN_FUNCTION_TOKENS = 40;
2779
+ var SHINGLE_SIZE = 10;
2780
+ var WINDOW_SIZE = 6;
2781
+ var KEYWORDS = /* @__PURE__ */ new Set([
2782
+ "async",
2783
+ "await",
2784
+ "break",
2785
+ "case",
2786
+ "catch",
2787
+ "class",
2788
+ "const",
2789
+ "continue",
2790
+ "default",
2791
+ "delete",
2792
+ "do",
2793
+ "else",
2794
+ "extends",
2795
+ "false",
2796
+ "finally",
2797
+ "for",
2798
+ "function",
2799
+ "if",
2800
+ "in",
2801
+ "instanceof",
2802
+ "let",
2803
+ "new",
2804
+ "null",
2805
+ "of",
2806
+ "return",
2807
+ "static",
2808
+ "super",
2809
+ "switch",
2810
+ "this",
2811
+ "throw",
2812
+ "true",
2813
+ "try",
2814
+ "typeof",
2815
+ "undefined",
2816
+ "var",
2817
+ "while",
2818
+ "yield"
2819
+ ]);
2820
+ function supportsFunctionFingerprintPath(path) {
2821
+ return SUPPORTED.has(extname(path).toLocaleLowerCase()) && !isNoisePath(path);
2822
+ }
2823
+ function extractFunctionFingerprints(path, content) {
2824
+ return extractFunctionFingerprintReport(path, content).functions;
2825
+ }
2826
+ function extractFunctionFingerprintReport(path, content) {
2827
+ if (!supportsFunctionFingerprintPath(path)) return { functions: [], skippedSmallFunctions: 0 };
2828
+ const masked = maskCommentsAndStrings(content);
2829
+ const lineOffsets = lineStartOffsets(content);
2830
+ const declarations2 = findDeclarations(masked);
2831
+ const functions = [];
2832
+ let skippedSmallFunctions = 0;
2833
+ for (const declaration of declarations2) {
2834
+ const open3 = declaration.open;
2835
+ const close = matchingBrace(masked, open3);
2836
+ if (close < 0) continue;
2837
+ const body = content.slice(open3 + 1, close);
2838
+ const normalizedTokens = normalizeFunctionTokens(body);
2839
+ if (normalizedTokens.length < MIN_FUNCTION_TOKENS) {
2840
+ skippedSmallFunctions += 1;
2841
+ continue;
2842
+ }
2843
+ const startLine = lineAtOffset(lineOffsets, declaration.start);
2844
+ const endLine = lineAtOffset(lineOffsets, close);
2845
+ functions.push({
2846
+ path,
2847
+ symbol: declaration.symbol,
2848
+ startLine,
2849
+ endLine,
2850
+ tokenCount: normalizedTokens.length,
2851
+ exactHash: hash(normalizedTokens.join(" ")),
2852
+ fingerprints: winnow(normalizedTokens),
2853
+ normalizedTokens
2854
+ });
2855
+ }
2856
+ return { functions, skippedSmallFunctions };
2857
+ }
2858
+ function fingerprintSimilarity(left, right) {
2859
+ if (left.exactHash === right.exactHash) return 1;
2860
+ const a = new Set(left.fingerprints);
2861
+ const b = new Set(right.fingerprints);
2862
+ if (!a.size || !b.size) return 0;
2863
+ let intersection = 0;
2864
+ for (const value of a) if (b.has(value)) intersection += 1;
2865
+ return intersection / (a.size + b.size - intersection);
2866
+ }
2867
+ function findDeclarations(masked) {
2868
+ const output2 = [];
2869
+ const classBodies = findClassBodies(masked);
2870
+ const patterns = [
2871
+ {
2872
+ kind: "function",
2873
+ pattern: /(?:^|\n)\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)(?:\s*<[^>{}\n]+>)?\s*\([^)]*\)\s*(?::\s*[^{}\n=]+)?\s*\{/g
2874
+ },
2875
+ {
2876
+ kind: "arrow",
2877
+ pattern: /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=\n]+)?\s*=\s*(?:async\s*)?(?:<[^>{}\n]+>\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*(?::\s*[^={}\n]+)?=>\s*\{/g
2878
+ },
2879
+ {
2880
+ kind: "method",
2881
+ pattern: /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|abstract|override|get|set|async)\s+)*\*?\s*([A-Za-z_$][\w$]*)(?:\s*<[^>{}\n]+>)?\s*\([^)]*\)\s*(?::\s*[^{}\n=]+)?\s*\{/g
2882
+ }
2883
+ ];
2884
+ for (const { kind, pattern } of patterns) {
2885
+ for (const match of masked.matchAll(pattern)) {
2886
+ const symbol = match[1];
2887
+ if (!symbol || match.index === void 0) continue;
2888
+ if (KEYWORDS.has(symbol) || symbol === "constructor") continue;
2889
+ const start = match.index + match[0].indexOf(symbol);
2890
+ if (kind === "method" && !isDirectClassMember(masked, start, classBodies)) continue;
2891
+ output2.push({
2892
+ symbol,
2893
+ start,
2894
+ open: match.index + match[0].lastIndexOf("{")
2895
+ });
2896
+ }
2897
+ }
2898
+ return output2.sort((left, right) => left.start - right.start);
2899
+ }
2900
+ function findClassBodies(masked) {
2901
+ const output2 = [];
2902
+ const pattern = /(?:^|\n)\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class(?:\s+[A-Za-z_$][\w$]*)?[^\n{]*\{/g;
2903
+ for (const match of masked.matchAll(pattern)) {
2904
+ if (match.index === void 0) continue;
2905
+ const open3 = match.index + match[0].lastIndexOf("{");
2906
+ const close = matchingBrace(masked, open3);
2907
+ if (close >= 0) output2.push({ open: open3, close });
2908
+ }
2909
+ return output2.sort((left, right) => left.close - left.open - (right.close - right.open));
2910
+ }
2911
+ function isDirectClassMember(masked, start, classBodies) {
2912
+ for (const body of classBodies) {
2913
+ if (start <= body.open || start >= body.close) continue;
2914
+ let depth = 1;
2915
+ for (let index = body.open + 1; index < start; index += 1) {
2916
+ if (masked[index] === "{") depth += 1;
2917
+ else if (masked[index] === "}") depth -= 1;
2918
+ }
2919
+ if (depth === 1) return true;
2920
+ }
2921
+ return false;
2922
+ }
2923
+ function normalizeFunctionTokens(content) {
2924
+ const masked = maskCommentsAndStrings(content, true);
2925
+ const raw = masked.match(/[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|===|!==|=>|==|!=|<=|>=|&&|\|\||\+\+|--|\?\?|\?\.|[{}()[\].,;:+\-*/%<>=!?&|^~]/g) ?? [];
2926
+ return raw.map((token) => {
2927
+ if (token === "LIT") return token;
2928
+ if (/^[A-Za-z_$]/.test(token)) return KEYWORDS.has(token) ? token : "ID";
2929
+ if (/^\d/.test(token)) return "LIT";
2930
+ return token;
2931
+ });
2932
+ }
2933
+ function winnow(tokens2) {
2934
+ if (tokens2.length < SHINGLE_SIZE) return [];
2935
+ const shingles = Array.from({ length: tokens2.length - SHINGLE_SIZE + 1 }, (_, index) => hash(tokens2.slice(index, index + SHINGLE_SIZE).join(" ")).slice(0, 16));
2936
+ if (shingles.length <= WINDOW_SIZE) return [minimum(shingles)];
2937
+ const selected = /* @__PURE__ */ new Set();
2938
+ for (let index = 0; index <= shingles.length - WINDOW_SIZE; index += 1) {
2939
+ selected.add(minimum(shingles.slice(index, index + WINDOW_SIZE)));
2940
+ }
2941
+ return [...selected].sort();
2942
+ }
2943
+ function minimum(values) {
2944
+ return values.reduce((smallest, value) => value < smallest ? value : smallest);
2945
+ }
2946
+ function maskCommentsAndStrings(content, preserveLiterals = false) {
2947
+ let state = "code";
2948
+ let escaped = false;
2949
+ let output2 = "";
2950
+ for (let index = 0; index < content.length; index += 1) {
2951
+ const character = content[index] ?? "";
2952
+ const next = content[index + 1] ?? "";
2953
+ if (state === "code") {
2954
+ if (character === "/" && next === "/") {
2955
+ state = "line";
2956
+ output2 += " ";
2957
+ index += 1;
2958
+ continue;
2959
+ }
2960
+ if (character === "/" && next === "*") {
2961
+ state = "block";
2962
+ output2 += " ";
2963
+ index += 1;
2964
+ continue;
2965
+ }
2966
+ if (character === "'") state = "single";
2967
+ else if (character === '"') state = "double";
2968
+ else if (character === "`") state = "template";
2969
+ output2 += state === "code" ? character : preserveLiterals ? " LIT " : character === "\n" ? "\n" : " ";
2970
+ escaped = false;
2971
+ continue;
2972
+ }
2973
+ if (state === "line") {
2974
+ if (character === "\n") {
2975
+ state = "code";
2976
+ output2 += "\n";
2977
+ } else output2 += " ";
2978
+ continue;
2979
+ }
2980
+ if (state === "block") {
2981
+ if (character === "*" && next === "/") {
2982
+ state = "code";
2983
+ output2 += " ";
2984
+ index += 1;
2985
+ } else output2 += character === "\n" ? "\n" : " ";
2986
+ continue;
2987
+ }
2988
+ output2 += character === "\n" ? "\n" : " ";
2989
+ if (escaped) {
2990
+ escaped = false;
2991
+ continue;
2992
+ }
2993
+ if (character === "\\") {
2994
+ escaped = true;
2995
+ continue;
2996
+ }
2997
+ if (state === "single" && character === "'" || state === "double" && character === '"' || state === "template" && character === "`") state = "code";
2998
+ }
2999
+ return output2;
3000
+ }
3001
+ function matchingBrace(masked, open3) {
3002
+ let depth = 0;
3003
+ for (let index = open3; index < masked.length; index += 1) {
3004
+ if (masked[index] === "{") depth += 1;
3005
+ else if (masked[index] === "}") {
3006
+ depth -= 1;
3007
+ if (depth === 0) return index;
3008
+ }
3009
+ }
3010
+ return -1;
3011
+ }
3012
+ function lineStartOffsets(content) {
3013
+ const offsets = [0];
3014
+ for (let index = 0; index < content.length; index += 1) {
3015
+ if (content[index] === "\n") offsets.push(index + 1);
3016
+ }
3017
+ return offsets;
3018
+ }
3019
+ function lineAtOffset(offsets, offset) {
3020
+ let low = 0;
3021
+ let high = offsets.length;
3022
+ while (low < high) {
3023
+ const middle = Math.floor((low + high) / 2);
3024
+ if ((offsets[middle] ?? 0) <= offset) low = middle + 1;
3025
+ else high = middle;
3026
+ }
3027
+ return Math.max(1, low);
3028
+ }
3029
+ function isNoisePath(path) {
3030
+ const normalized = path.replaceAll("\\", "/").toLocaleLowerCase();
3031
+ return /(^|\/)(test|tests|__tests__|fixtures|fixture|generated|vendor|dist|build)(\/|$)/.test(normalized) || /(?:\.d|\.generated|\.min)\.[cm]?[jt]sx?$/.test(normalized);
3032
+ }
3033
+ function hash(value) {
3034
+ return createHash5("sha256").update(value).digest("hex");
3035
+ }
3036
+
2768
3037
  // src/context/local-index.ts
2769
3038
  var contentHashSchema = z3.string().regex(/^[a-f\d]{64}$/u);
2770
3039
  var indexedChunkSchema = z3.object({
@@ -2835,6 +3104,7 @@ var LocalContextIndex = class {
2835
3104
  index;
2836
3105
  workspace;
2837
3106
  queryCache = /* @__PURE__ */ new Map();
3107
+ fingerprintCache;
2838
3108
  dirtyPaths = /* @__PURE__ */ new Set();
2839
3109
  refreshState = "current";
2840
3110
  refreshError;
@@ -2868,6 +3138,7 @@ var LocalContextIndex = class {
2868
3138
  }
2869
3139
  this.index = { ...parsed, files };
2870
3140
  this.queryCache.clear();
3141
+ this.fingerprintCache = void 0;
2871
3142
  this.refreshState = this.dirtyPaths.size ? "dirty" : "current";
2872
3143
  this.refreshError = void 0;
2873
3144
  return true;
@@ -2875,6 +3146,7 @@ var LocalContextIndex = class {
2875
3146
  if (error.code === "ENOENT") return false;
2876
3147
  delete this.index;
2877
3148
  this.queryCache.clear();
3149
+ this.fingerprintCache = void 0;
2878
3150
  return false;
2879
3151
  }
2880
3152
  }
@@ -2984,6 +3256,7 @@ var LocalContextIndex = class {
2984
3256
  files: nextFiles
2985
3257
  };
2986
3258
  this.queryCache.clear();
3259
+ this.fingerprintCache = void 0;
2987
3260
  await ensureWorkspaceStorageDirectory(workspace, dirname7(this.indexPath), {
2988
3261
  requireActiveNamespace: true
2989
3262
  });
@@ -3003,6 +3276,24 @@ var LocalContextIndex = class {
3003
3276
  const hits = await this.search(query, topK);
3004
3277
  return packContextHits(hits, this.roots, maxTokens, "local");
3005
3278
  }
3279
+ async functionFingerprints() {
3280
+ await this.flushDirty();
3281
+ await this.ensureCurrentIndex();
3282
+ const index = this.index;
3283
+ if (!index) throw new Error("The local context index is unavailable.");
3284
+ if (this.fingerprintCache?.generation === index.generation) {
3285
+ return cloneDuplicationBaseline(this.fingerprintCache);
3286
+ }
3287
+ const functions = index.files.flatMap((file) => {
3288
+ const content = reconstructIndexedContent(file.chunks);
3289
+ if (hashContent(content) !== file.contentHash) {
3290
+ throw new Error("The fingerprint baseline could not reconstruct current indexed files.");
3291
+ }
3292
+ return extractFunctionFingerprints(file.absolutePath, content).map(({ normalizedTokens: _tokens, ...fingerprint }) => fingerprint);
3293
+ });
3294
+ this.fingerprintCache = { generation: index.generation, functions };
3295
+ return cloneDuplicationBaseline(this.fingerprintCache);
3296
+ }
3006
3297
  status() {
3007
3298
  return {
3008
3299
  available: Boolean(this.index),
@@ -3104,6 +3395,7 @@ var LocalContextIndex = class {
3104
3395
  files
3105
3396
  };
3106
3397
  this.queryCache.clear();
3398
+ this.fingerprintCache = void 0;
3107
3399
  onProgress?.({ phase: "write", completed: files.length, total: files.length });
3108
3400
  await ensureWorkspaceStorageDirectory(
3109
3401
  this.roots[0] ?? process.cwd(),
@@ -3365,7 +3657,7 @@ function isIncludedSourcePath(path) {
3365
3657
  ".proto",
3366
3658
  ".tf",
3367
3659
  ".hcl"
3368
- ])).has(extname(name).toLowerCase());
3660
+ ])).has(extname2(name).toLowerCase());
3369
3661
  }
3370
3662
  function chunksMatch(expected, actual) {
3371
3663
  if (expected.length !== actual.length) return false;
@@ -3446,11 +3738,29 @@ ${hit.content}
3446
3738
  function cloneHits(hits) {
3447
3739
  return hits.map((hit) => ({ ...hit }));
3448
3740
  }
3741
+ function cloneDuplicationBaseline(baseline) {
3742
+ return {
3743
+ generation: baseline.generation,
3744
+ functions: baseline.functions.map((item) => ({ ...item, fingerprints: [...item.fingerprints] }))
3745
+ };
3746
+ }
3747
+ function reconstructIndexedContent(chunks) {
3748
+ const ordered = chunks.slice().sort((left, right) => left.startLine - right.startLine);
3749
+ const totalLines = ordered.reduce((maximum, chunk) => Math.max(maximum, chunk.endLine), 0);
3750
+ const lines = Array.from({ length: totalLines });
3751
+ for (const chunk of ordered) {
3752
+ for (const [offset, line] of chunk.content.split("\n").entries()) {
3753
+ const index = chunk.startLine - 1 + offset;
3754
+ if (lines[index] === void 0) lines[index] = line;
3755
+ }
3756
+ }
3757
+ return lines.map((line) => line ?? "").join("\n");
3758
+ }
3449
3759
  function hashContent(content) {
3450
- return createHash5("sha256").update(content, "utf8").digest("hex");
3760
+ return createHash6("sha256").update(content, "utf8").digest("hex");
3451
3761
  }
3452
3762
  function createGeneration(files) {
3453
- return createHash5("sha256").update(files.slice().sort((left, right) => left.absolutePath.localeCompare(right.absolutePath)).map((file) => `${file.absolutePath}\0${file.contentHash}`).join("\n"), "utf8").digest("hex").slice(0, 16);
3763
+ return createHash6("sha256").update(files.slice().sort((left, right) => left.absolutePath.localeCompare(right.absolutePath)).map((file) => `${file.absolutePath}\0${file.contentHash}`).join("\n"), "utf8").digest("hex").slice(0, 16);
3454
3764
  }
3455
3765
  function hasSubstantialOverlap(left, right) {
3456
3766
  if (left.path !== right.path) return false;
@@ -3497,7 +3807,7 @@ function appendChunkRange(chunks, file, lines, rangeStart, rangeEnd) {
3497
3807
  }
3498
3808
  }
3499
3809
  function structuralStarts(path, lines) {
3500
- const extension = extname(path).toLocaleLowerCase();
3810
+ const extension = extname2(path).toLocaleLowerCase();
3501
3811
  return lines.flatMap((line, index) => isStructuralLine(line, extension) ? [index] : []);
3502
3812
  }
3503
3813
  function isStructuralLine(line, extension) {
@@ -3663,6 +3973,9 @@ var ContextEngine = class {
3663
3973
  lastDegradation() {
3664
3974
  return this.degradation ? { ...this.degradation } : void 0;
3665
3975
  }
3976
+ async functionFingerprints() {
3977
+ return this.local.functionFingerprints();
3978
+ }
3666
3979
  };
3667
3980
  function formatContextHits(hits, roots) {
3668
3981
  return hits.map((hit) => {
@@ -4827,7 +5140,7 @@ function createProvider(config) {
4827
5140
  }
4828
5141
 
4829
5142
  // src/checkpoint/store.ts
4830
- import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
5143
+ import { createHash as createHash7, randomUUID as randomUUID7 } from "node:crypto";
4831
5144
  import { lstat as lstat9, mkdir as mkdir7, readFile as readFile5, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
4832
5145
  import { basename as basename6, dirname as dirname8, join as join8, relative as relative6, resolve as resolve10 } from "node:path";
4833
5146
  import { z as z4 } from "zod";
@@ -4880,7 +5193,7 @@ var CheckpointStore = class {
4880
5193
  if (info.size > 25e6) {
4881
5194
  throw new Error(`File is too large to checkpoint (${info.size} bytes): ${input2}`);
4882
5195
  }
4883
- const blob = `${createHash6("sha256").update(path).digest("hex")}.bin`;
5196
+ const blob = `${createHash7("sha256").update(path).digest("hex")}.bin`;
4884
5197
  await atomicWrite(join8(blobDirectory, blob), await readFile5(path), 384);
4885
5198
  entries.push({
4886
5199
  path,
@@ -5363,11 +5676,49 @@ var reuseReceiptSchema = z5.object({
5363
5676
  status: z5.enum(["warning", "skipped", "unresolved"]),
5364
5677
  warningOnly: z5.literal(true)
5365
5678
  }).strict();
5679
+ var duplicationMatchSchema = z5.object({
5680
+ matchId: z5.string().regex(/^[a-f0-9]{24}$/u).optional(),
5681
+ changedPath: z5.string(),
5682
+ changedSymbol: z5.string(),
5683
+ candidatePath: z5.string(),
5684
+ candidateSymbol: z5.string(),
5685
+ kind: z5.enum(["type-1-or-2", "type-3"]),
5686
+ similarity: z5.number().min(0).max(1)
5687
+ }).strict();
5688
+ var duplicationAuditSchema = z5.object({
5689
+ baselineGeneration: z5.string().min(1),
5690
+ changeSequence: z5.number().int().nonnegative(),
5691
+ status: z5.enum(["clear", "warning", "unresolved"]),
5692
+ warningOnly: z5.literal(true),
5693
+ checkedFunctions: z5.number().int().nonnegative(),
5694
+ skippedSmallFunctions: z5.number().int().nonnegative(),
5695
+ matches: z5.array(duplicationMatchSchema).max(8),
5696
+ rationale: z5.string().max(500)
5697
+ }).strict();
5698
+ var duplicationSuppressionSchema = z5.object({
5699
+ matchId: z5.string().regex(/^[a-f0-9]{24}$/u),
5700
+ reasonCode: z5.enum(["separate-boundary", "protocol-required", "generated-contract", "false-positive", "other"]),
5701
+ reason: z5.string().min(12).max(240),
5702
+ createdAt: z5.string().datetime(),
5703
+ toolCallId: z5.string().min(1).max(512)
5704
+ }).strict();
5366
5705
  var auditMetadataSchema = z5.record(z5.string(), z5.unknown()).superRefine((metadata, ctx) => {
5367
5706
  const receipt = metadata.reuseReceipt;
5368
5707
  if (receipt !== void 0 && !reuseReceiptSchema.safeParse(receipt).success) {
5369
5708
  ctx.addIssue({ code: "custom", message: "Invalid reuse receipt" });
5370
5709
  }
5710
+ const duplication = metadata.duplicationAudit;
5711
+ if (duplication !== void 0 && !duplicationAuditSchema.safeParse(duplication).success) {
5712
+ ctx.addIssue({ code: "custom", message: "Invalid duplication audit receipt" });
5713
+ }
5714
+ const suppression = metadata.duplicationSuppression;
5715
+ if (suppression !== void 0 && !duplicationSuppressionSchema.safeParse(suppression).success) {
5716
+ ctx.addIssue({ code: "custom", message: "Invalid duplication suppression receipt" });
5717
+ }
5718
+ const activeMatches = metadata.activeDuplicationMatches;
5719
+ if (activeMatches !== void 0 && !z5.array(z5.string().regex(/^[a-f0-9]{24}$/u)).max(8).safeParse(activeMatches).success) {
5720
+ ctx.addIssue({ code: "custom", message: "Invalid active duplication matches" });
5721
+ }
5371
5722
  });
5372
5723
  var auditSchema = z5.object({
5373
5724
  id: z5.string(),
@@ -5407,6 +5758,16 @@ var lastRunSchema = z5.object({
5407
5758
  checks: z5.array(verificationEvidenceSchema),
5408
5759
  detail: z5.string(),
5409
5760
  mutationTracking: z5.enum(["complete", "unknown"]).optional(),
5761
+ duplication: z5.object({
5762
+ enforcement: z5.literal("warning"),
5763
+ status: z5.enum(["clear", "warning", "unresolved", "suppressed"]),
5764
+ warningCount: z5.number().int().nonnegative(),
5765
+ unresolvedCount: z5.number().int().nonnegative(),
5766
+ suppressedCount: z5.number().int().nonnegative(),
5767
+ matches: z5.array(duplicationMatchSchema.extend({
5768
+ matchId: z5.string().regex(/^[a-f0-9]{24}$/u)
5769
+ })).max(8)
5770
+ }).strict().optional(),
5410
5771
  acceptance: z5.object({
5411
5772
  state: z5.enum(["draft", "active", "satisfied", "blocked"]),
5412
5773
  total: z5.number().int().nonnegative(),
@@ -5472,6 +5833,7 @@ var sessionSchema = z5.object({
5472
5833
  contextSources: z5.array(contextSourceSchema).max(64).optional(),
5473
5834
  toolArtifacts: z5.array(toolArtifactSchema).max(200).optional(),
5474
5835
  taskContract: taskContractSchema.optional(),
5836
+ duplicationSuppressions: z5.array(duplicationSuppressionSchema).max(64).optional(),
5475
5837
  lastRun: lastRunSchema.optional(),
5476
5838
  usage: z5.object({
5477
5839
  inputTokens: z5.number().nonnegative(),
@@ -5779,7 +6141,7 @@ async function exists2(path) {
5779
6141
  }
5780
6142
 
5781
6143
  // src/session/tool-artifacts.ts
5782
- import { createHash as createHash7 } from "node:crypto";
6144
+ import { createHash as createHash8 } from "node:crypto";
5783
6145
  import { lstat as lstat12, readFile as readFile7, readdir as readdir4, rm as rm2 } from "node:fs/promises";
5784
6146
  import { join as join11, resolve as resolve13 } from "node:path";
5785
6147
  import { z as z6 } from "zod";
@@ -5839,7 +6201,7 @@ var ToolArtifactStore = class {
5839
6201
  const artifacts = await this.loadArtifacts();
5840
6202
  await this.removeExpired(artifacts, now);
5841
6203
  const active = artifacts.filter((artifact2) => artifact2.expiresAt > now.toISOString());
5842
- const sha256 = hash(content);
6204
+ const sha256 = hash2(content);
5843
6205
  const existing = active.find((artifact2) => artifact2.sessionId === sessionId && artifact2.toolCallId === toolCallId);
5844
6206
  if (existing) {
5845
6207
  if (existing.sha256 === sha256 && existing.bytes === bytes && existing.redacted === options.redacted) {
@@ -5954,7 +6316,7 @@ var ToolArtifactStore = class {
5954
6316
  if (artifact.sessionId !== sessionId || artifact.toolCallId !== toolCallId) {
5955
6317
  throw new Error("Retained tool output does not belong to this session.");
5956
6318
  }
5957
- if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash(artifact.content)) {
6319
+ if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash2(artifact.content)) {
5958
6320
  throw new Error("Retained tool output failed its integrity check.");
5959
6321
  }
5960
6322
  return artifact;
@@ -5974,7 +6336,7 @@ var ToolArtifactStore = class {
5974
6336
  await this.assertRegularFile(path);
5975
6337
  try {
5976
6338
  const artifact = artifactSchema.parse(JSON.parse(await readFile7(path, "utf8")));
5977
- if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash(artifact.content)) {
6339
+ if (artifact.bytes !== Buffer.byteLength(artifact.content) || artifact.sha256 !== hash2(artifact.content)) {
5978
6340
  throw new Error("integrity");
5979
6341
  }
5980
6342
  if (sessionDirectory !== this.sessionDirectoryFor(artifact.sessionId) || path !== this.pathFor(artifact.sessionId, artifact.toolCallId)) {
@@ -5997,10 +6359,10 @@ var ToolArtifactStore = class {
5997
6359
  await rm2(this.pathFor(artifact.sessionId, artifact.toolCallId), { force: true });
5998
6360
  }
5999
6361
  pathFor(sessionId, toolCallId) {
6000
- return join11(this.sessionDirectoryFor(sessionId), `${hash(`call\0${toolCallId}`)}.json`);
6362
+ return join11(this.sessionDirectoryFor(sessionId), `${hash2(`call\0${toolCallId}`)}.json`);
6001
6363
  }
6002
6364
  sessionDirectoryFor(sessionId) {
6003
- return join11(this.directory, hash(`session\0${sessionId}`));
6365
+ return join11(this.directory, hash2(`session\0${sessionId}`));
6004
6366
  }
6005
6367
  async ensureDirectory() {
6006
6368
  await ensureWorkspaceStorageDirectory(this.workspace, this.directory, {
@@ -6057,8 +6419,8 @@ function reference(artifact) {
6057
6419
  redacted: artifact.redacted
6058
6420
  };
6059
6421
  }
6060
- function hash(value) {
6061
- return createHash7("sha256").update(value).digest("hex");
6422
+ function hash2(value) {
6423
+ return createHash8("sha256").update(value).digest("hex");
6062
6424
  }
6063
6425
  function storedBytes(artifact) {
6064
6426
  return Buffer.byteLength(JSON.stringify(artifact)) + 1;
@@ -6403,13 +6765,13 @@ ${preview}`);
6403
6765
  }
6404
6766
  return `${lines.join("\n")}${forceFinalNewline && lines.length ? "\n" : ""}`;
6405
6767
  }
6406
- function findSequence(haystack, needle, hint, minimum) {
6768
+ function findSequence(haystack, needle, hint, minimum2) {
6407
6769
  const matchesAt = (start, relaxed) => needle.every((line, offset) => {
6408
6770
  const candidate = haystack[start + offset];
6409
6771
  return relaxed ? candidate?.trimEnd() === line.trimEnd() : candidate === line;
6410
6772
  });
6411
6773
  const starts = [];
6412
- for (let index = Math.max(0, minimum); index <= haystack.length - needle.length; index += 1) {
6774
+ for (let index = Math.max(0, minimum2); index <= haystack.length - needle.length; index += 1) {
6413
6775
  starts.push(index);
6414
6776
  }
6415
6777
  starts.sort((a, b) => Math.abs(a - hint) - Math.abs(b - hint));
@@ -7380,7 +7742,7 @@ ${hit.content}`
7380
7742
  }
7381
7743
 
7382
7744
  // src/tools/shell.ts
7383
- import { createHash as createHash8 } from "node:crypto";
7745
+ import { createHash as createHash9 } from "node:crypto";
7384
7746
  import { lstat as lstat15, readFile as readFile11, readdir as readdir5 } from "node:fs/promises";
7385
7747
  import { join as join13 } from "node:path";
7386
7748
  import { z as z13 } from "zod";
@@ -7426,10 +7788,10 @@ var shellTool = {
7426
7788
  validateEnvironment(input2.env);
7427
7789
  const cwd = await context.workspace.resolveDirectory(input2.cwd ?? ".");
7428
7790
  const candidates = appearsToModifyWorkspace(input2.command) ? await collectAffectedPaths(input2.command, input2.cwd ?? ".", context) : [];
7429
- const unresolved = appearsToModifyWorkspace(input2.command) && candidates.length === 0;
7430
- const roots = unresolved ? context.workspace.roots : [];
7791
+ const unresolved2 = appearsToModifyWorkspace(input2.command) && candidates.length === 0;
7792
+ const roots = unresolved2 ? context.workspace.roots : [];
7431
7793
  const before = await snapshotPaths(candidates);
7432
- const beforeWorkspace = unresolved ? await captureWorkspaceSnapshot(roots) : void 0;
7794
+ const beforeWorkspace = unresolved2 ? await captureWorkspaceSnapshot(roots) : void 0;
7433
7795
  let result;
7434
7796
  try {
7435
7797
  result = await runShell(input2.command, cwd, {
@@ -7594,7 +7956,7 @@ async function captureWorkspaceSnapshot(roots) {
7594
7956
  files.set(path, {
7595
7957
  size: info.size,
7596
7958
  mtimeMs: info.mtimeMs,
7597
- hash: createHash8("sha256").update(content).digest("hex")
7959
+ hash: createHash9("sha256").update(content).digest("hex")
7598
7960
  });
7599
7961
  } catch (error) {
7600
7962
  if (error.code !== "ENOENT") complete = false;
@@ -7797,14 +8159,14 @@ function compact(value, limit) {
7797
8159
  }
7798
8160
 
7799
8161
  // src/agent/completion-gate.ts
7800
- import { createHash as createHash10 } from "node:crypto";
8162
+ import { createHash as createHash11 } from "node:crypto";
7801
8163
 
7802
8164
  // src/tools/permissions.ts
7803
- import { createHash as createHash9, createHmac, randomBytes } from "node:crypto";
8165
+ import { createHash as createHash10, createHmac, randomBytes } from "node:crypto";
7804
8166
  var permissionScopeSecret = randomBytes(32);
7805
8167
  function permissionKey(call, category) {
7806
8168
  const categoryScope = category === "network" ? `\0network-request:${scopeFingerprint(normalizeRequestState(call.arguments))}` : "";
7807
- const digest = createHash9("sha256").update(`${category}\0${call.name}\0${permissionTarget(call)}${categoryScope}`).digest("hex").slice(0, 24);
8169
+ const digest = createHash10("sha256").update(`${category}\0${call.name}\0${permissionTarget(call)}${categoryScope}`).digest("hex").slice(0, 24);
7808
8170
  return `${category}:${call.name}:${digest}`;
7809
8171
  }
7810
8172
  function permissionTarget(call) {
@@ -7998,10 +8360,10 @@ function captureVerification(call, result, changeSequence, configuredCommands) {
7998
8360
  kind,
7999
8361
  ok: result.ok,
8000
8362
  changeSequence,
8001
- commandKey: createHash10("sha256").update(normalized).digest("hex")
8363
+ commandKey: createHash11("sha256").update(normalized).digest("hex")
8002
8364
  };
8003
8365
  }
8004
- function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutationTracking = "complete", taskContract, audit = []) {
8366
+ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutationTracking = "complete", taskContract, audit = [], duplication) {
8005
8367
  const files = [...new Set(changedFiles)];
8006
8368
  const acceptance = taskContract ? buildAcceptance(taskContract, audit, evidence, currentChangeSequence) : void 0;
8007
8369
  if (mutationTracking === "unknown") {
@@ -8011,6 +8373,7 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8011
8373
  checks: [],
8012
8374
  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.",
8013
8375
  mutationTracking,
8376
+ ...duplication ? { duplication } : {},
8014
8377
  ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
8015
8378
  };
8016
8379
  }
@@ -8021,7 +8384,8 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8021
8384
  changedFiles: [],
8022
8385
  checks: [],
8023
8386
  detail: acceptanceDetail(acceptance),
8024
- acceptance: acceptanceForCompletion(acceptance, "unverified")
8387
+ acceptance: acceptanceForCompletion(acceptance, "unverified"),
8388
+ ...duplication ? { duplication } : {}
8025
8389
  };
8026
8390
  }
8027
8391
  return {
@@ -8029,7 +8393,8 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8029
8393
  changedFiles: [],
8030
8394
  checks: [],
8031
8395
  detail: "No workspace files changed in this run.",
8032
- ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "no_changes") } : {}
8396
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "no_changes") } : {},
8397
+ ...duplication ? { duplication } : {}
8033
8398
  };
8034
8399
  }
8035
8400
  const latestByCommand = /* @__PURE__ */ new Map();
@@ -8045,7 +8410,8 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8045
8410
  changedFiles: files,
8046
8411
  checks,
8047
8412
  detail: `No successful verification was recorded after the last change to ${fileCount(files.length)}.`,
8048
- ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {}
8413
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {},
8414
+ ...duplication ? { duplication } : {}
8049
8415
  };
8050
8416
  }
8051
8417
  const failures = checks.filter((check) => !check.ok);
@@ -8055,7 +8421,8 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8055
8421
  changedFiles: files,
8056
8422
  checks,
8057
8423
  detail: `${failures.length} of ${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} failed.`,
8058
- ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verification_failed") } : {}
8424
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verification_failed") } : {},
8425
+ ...duplication ? { duplication } : {}
8059
8426
  };
8060
8427
  }
8061
8428
  if (acceptance && acceptanceUnresolved(acceptance)) {
@@ -8064,7 +8431,8 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8064
8431
  changedFiles: files,
8065
8432
  checks,
8066
8433
  detail: acceptanceDetail(acceptance),
8067
- acceptance: acceptanceForCompletion(acceptance, "unverified")
8434
+ acceptance: acceptanceForCompletion(acceptance, "unverified"),
8435
+ ...duplication ? { duplication } : {}
8068
8436
  };
8069
8437
  }
8070
8438
  return {
@@ -8072,16 +8440,17 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8072
8440
  changedFiles: files,
8073
8441
  checks,
8074
8442
  detail: `${checks.length} current verification ${checks.length === 1 ? "check" : "checks"} passed for ${fileCount(files.length)}.`,
8075
- ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verified") } : {}
8443
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "verified") } : {},
8444
+ ...duplication ? { duplication } : {}
8076
8445
  };
8077
8446
  }
8078
8447
  function completionRecoveryDirective(completion) {
8079
8448
  if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
8080
- const unresolved = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
8449
+ const unresolved2 = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
8081
8450
  const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
8082
8451
  return `<runtime-completion-gate status="acceptance_unresolved" authorization="none">
8083
8452
  The run cannot be marked complete while required Task Contract criteria remain unresolved:
8084
- ${unresolved || "- Acceptance criteria are satisfied."}${completion.acceptance.missingVerification.length ? `
8453
+ ${unresolved2 || "- Acceptance criteria are satisfied."}${completion.acceptance.missingVerification.length ? `
8085
8454
  Missing required verification:
8086
8455
  ${completion.acceptance.missingVerification.map((item) => `- ${item}`).join("\n")}` : ""}${failed ? `
8087
8456
  Current failed checks:
@@ -8151,14 +8520,14 @@ function acceptanceDetail(acceptance) {
8151
8520
  acceptance.blocked ? `${acceptance.blocked} blocked` : "",
8152
8521
  acceptance.missingVerification.length ? `${acceptance.missingVerification.length} verification requirements missing` : ""
8153
8522
  ].filter(Boolean).join(" and ");
8154
- const unresolved = acceptance.pending + acceptance.blocked;
8155
- return `Task Contract acceptance is unresolved: ${parts} required ${unresolved === 1 ? "criterion" : "criteria"}.`;
8523
+ const unresolved2 = acceptance.pending + acceptance.blocked;
8524
+ return `Task Contract acceptance is unresolved: ${parts} required ${unresolved2 === 1 ? "criterion" : "criteria"}.`;
8156
8525
  }
8157
8526
  function verificationRequirementMet(requirement, checks) {
8158
8527
  const normalized = normalizeCommand2(requirement);
8159
8528
  const broad = normalized.match(/^Record at least one successful (.+) after the final mutation\.?$/iu);
8160
8529
  if (broad) return checks.length > 0;
8161
- const commandKey = createHash10("sha256").update(normalized).digest("hex");
8530
+ const commandKey = createHash11("sha256").update(normalized).digest("hex");
8162
8531
  return checks.some((item) => item.commandKey === commandKey);
8163
8532
  }
8164
8533
  function acceptanceUnresolved(acceptance) {
@@ -8484,6 +8853,139 @@ function render(memory) {
8484
8853
  ].join("\n");
8485
8854
  }
8486
8855
 
8856
+ // src/tools/duplication.ts
8857
+ import { z as z17 } from "zod";
8858
+
8859
+ // src/agent/duplication-state.ts
8860
+ function buildDuplicationCompletion(audit, suppressions = [], runStartedAt) {
8861
+ const active = /* @__PURE__ */ new Map();
8862
+ const unresolved2 = /* @__PURE__ */ new Map();
8863
+ let sawCurrentReceipt = false;
8864
+ for (const event of audit) {
8865
+ if (runStartedAt && event.createdAt < runStartedAt) continue;
8866
+ const receipt = duplicationReceipt(event.metadata?.duplicationAudit);
8867
+ if (!receipt) continue;
8868
+ if (receipt.status === "clear" || receipt.status === "unresolved" || receipt.matches.some((match) => typeof match.matchId === "string")) sawCurrentReceipt = true;
8869
+ const changedPaths2 = changedPathsFor(event, receipt);
8870
+ for (const path of changedPaths2) {
8871
+ for (const [id, match] of active) if (match.changedPath === path) active.delete(id);
8872
+ unresolved2.delete(path);
8873
+ }
8874
+ if (receipt.status === "unresolved") {
8875
+ for (const path of changedPaths2) unresolved2.set(path, receipt.changeSequence);
8876
+ }
8877
+ for (const match of receipt.matches) {
8878
+ if (typeof match.matchId === "string") {
8879
+ active.set(match.matchId, match);
8880
+ }
8881
+ }
8882
+ }
8883
+ if (!active.size && !unresolved2.size && !sawCurrentReceipt) return void 0;
8884
+ const activeIds = new Set(active.keys());
8885
+ const suppressedIds = new Set(suppressions.filter((item) => activeIds.has(item.matchId)).map((item) => item.matchId));
8886
+ const warnings = [...active.values()].filter((match) => !suppressedIds.has(match.matchId));
8887
+ const suppressedCount = active.size - warnings.length;
8888
+ return {
8889
+ enforcement: "warning",
8890
+ status: unresolved2.size ? "unresolved" : warnings.length ? "warning" : suppressedCount ? "suppressed" : "clear",
8891
+ warningCount: warnings.length,
8892
+ unresolvedCount: unresolved2.size,
8893
+ suppressedCount,
8894
+ matches: warnings.slice(0, 8)
8895
+ };
8896
+ }
8897
+ function activeDuplicationMatchIds(audit, suppressions = []) {
8898
+ const completion = buildDuplicationCompletion(audit, suppressions);
8899
+ return new Set(completion?.matches.map((match) => match.matchId) ?? []);
8900
+ }
8901
+ function pruneDuplicationSuppressions(audit, suppressions = []) {
8902
+ if (!suppressions.length) return [];
8903
+ const active = new Set(buildDuplicationCompletion(audit, [])?.matches.map((match) => match.matchId) ?? []);
8904
+ return suppressions.filter((item) => active.has(item.matchId)).slice(-64);
8905
+ }
8906
+ function findActiveDuplicationMatches(audit, suppressions = []) {
8907
+ return buildDuplicationCompletion(audit, suppressions)?.matches ?? [];
8908
+ }
8909
+ function hasDuplicationActivity(audit, runStartedAt) {
8910
+ return audit.some(
8911
+ (event) => (!runStartedAt || event.createdAt >= runStartedAt) && (duplicationReceipt(event.metadata?.duplicationAudit) !== void 0 || event.metadata?.duplicationSuppression !== void 0 || event.metadata?.activeDuplicationMatches !== void 0)
8912
+ );
8913
+ }
8914
+ function duplicationReceipt(value) {
8915
+ if (!value || typeof value !== "object") return void 0;
8916
+ const receipt = value;
8917
+ if (!Array.isArray(receipt.matches) || typeof receipt.changeSequence !== "number" || !["clear", "warning", "unresolved"].includes(String(receipt.status))) return void 0;
8918
+ return receipt;
8919
+ }
8920
+ function changedPathsFor(event, receipt) {
8921
+ const metadataPaths = event.metadata?.changedFiles;
8922
+ const fromMetadata = Array.isArray(metadataPaths) ? metadataPaths.filter((path) => typeof path === "string") : [];
8923
+ return [.../* @__PURE__ */ new Set([...fromMetadata, ...receipt.matches.map((match) => match.changedPath)])];
8924
+ }
8925
+
8926
+ // src/tools/duplication.ts
8927
+ var inputSchema12 = z17.discriminatedUnion("action", [
8928
+ z17.object({ action: z17.literal("show") }).strict(),
8929
+ z17.object({
8930
+ action: z17.literal("suppress"),
8931
+ match_id: z17.string().regex(/^[a-f0-9]{24}$/u),
8932
+ reason_code: z17.enum(["separate-boundary", "protocol-required", "generated-contract", "false-positive", "other"]),
8933
+ reason: z17.string().trim().min(12).max(240)
8934
+ }).strict()
8935
+ ]);
8936
+ var duplicationTool = {
8937
+ definition: {
8938
+ name: "duplication_audit",
8939
+ description: "Inspect active duplicate-function warnings or suppress one exact match with an auditable reason. Suppression does not bypass correctness, security, accessibility, concurrency, or verification requirements.",
8940
+ category: "read",
8941
+ inputSchema: jsonSchema({
8942
+ action: { type: "string", enum: ["show", "suppress"] },
8943
+ match_id: { type: "string", description: "Exact 24-character match id from the current audit." },
8944
+ reason_code: { type: "string", enum: ["separate-boundary", "protocol-required", "generated-contract", "false-positive", "other"] },
8945
+ reason: { type: "string", description: "Specific 12-240 character explanation; code blocks and credentials are rejected." }
8946
+ }, ["action"])
8947
+ },
8948
+ async execute(arguments_, context) {
8949
+ const input2 = inputSchema12.parse(arguments_);
8950
+ const suppressions = pruneDuplicationSuppressions(
8951
+ context.session.audit ?? [],
8952
+ context.session.duplicationSuppressions ?? []
8953
+ );
8954
+ context.session.duplicationSuppressions = suppressions;
8955
+ const active = activeDuplicationMatchIds(context.session.audit ?? [], suppressions);
8956
+ if (input2.action === "suppress") {
8957
+ if (!active.has(input2.match_id)) {
8958
+ throw new Error(`Unknown, stale, or already suppressed duplication match: ${input2.match_id}`);
8959
+ }
8960
+ suppressions.push({
8961
+ matchId: input2.match_id,
8962
+ reasonCode: input2.reason_code,
8963
+ reason: cleanReason(input2.reason),
8964
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8965
+ toolCallId: context.toolCallId ?? "unavailable"
8966
+ });
8967
+ context.session.duplicationSuppressions = suppressions.slice(-64);
8968
+ return {
8969
+ content: `Suppressed duplication match ${input2.match_id}. This does not waive verification or safety requirements.`,
8970
+ metadata: { duplicationSuppression: context.session.duplicationSuppressions.at(-1) }
8971
+ };
8972
+ }
8973
+ const matches = findActiveDuplicationMatches(context.session.audit ?? [], suppressions);
8974
+ return {
8975
+ content: matches.length ? matches.map(
8976
+ (match) => `- ${match.matchId}: ${match.changedPath}#${match.changedSymbol} -> ${match.candidatePath}#${match.candidateSymbol} (${match.kind}, ${match.similarity.toFixed(3)})`
8977
+ ).join("\n") : "No unsuppressed duplication matches.",
8978
+ metadata: { activeDuplicationMatches: matches.map((match) => match.matchId) }
8979
+ };
8980
+ }
8981
+ };
8982
+ function cleanReason(value) {
8983
+ if (/```|~~~|\b(?:api[_-]?key|access[_-]?token|authorization|cookie|password|secret)\s*[:=]/iu.test(value) || /\b(?:sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/u.test(value)) {
8984
+ throw new Error("Suppression reasons cannot contain code blocks or credentials.");
8985
+ }
8986
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\s+/gu, " ").trim();
8987
+ }
8988
+
8487
8989
  // src/tools/index.ts
8488
8990
  function createDefaultToolRegistry(_options = {}) {
8489
8991
  return new ToolRegistry([
@@ -8497,6 +8999,7 @@ function createDefaultToolRegistry(_options = {}) {
8497
8999
  gitTool,
8498
9000
  taskTool,
8499
9001
  taskContractTool,
9002
+ duplicationTool,
8500
9003
  workingMemoryTool
8501
9004
  ]);
8502
9005
  }
@@ -8617,7 +9120,7 @@ function escapeAttribute3(value) {
8617
9120
  }
8618
9121
 
8619
9122
  // src/agent/tool-recovery.ts
8620
- import { createHash as createHash11 } from "node:crypto";
9123
+ import { createHash as createHash12 } from "node:crypto";
8621
9124
  var RETRY_BUDGET = {
8622
9125
  schema_input: 3,
8623
9126
  unknown_tool: 2,
@@ -8689,7 +9192,7 @@ var ToolRecoveryController = class {
8689
9192
  recordEvidence(call, result) {
8690
9193
  if (call.name !== "search_code" || !result.ok) return void 0;
8691
9194
  const callKey = callSignature(call);
8692
- const fingerprint = createHash11("sha256").update(`${result.content}\0${stableJson(result.metadata ?? {})}`).digest("hex");
9195
+ const fingerprint = createHash12("sha256").update(`${result.content}\0${stableJson(result.metadata ?? {})}`).digest("hex");
8693
9196
  const count = typeof result.metadata?.count === "number" ? result.metadata.count : void 0;
8694
9197
  const current = this.evidence.get(callKey);
8695
9198
  const repeated = current?.fingerprint === fingerprint;
@@ -8699,7 +9202,7 @@ var ToolRecoveryController = class {
8699
9202
  status: count === 0 ? "empty" : repeated ? "repeated" : "new",
8700
9203
  repeatCount: repeats,
8701
9204
  stop: repeats >= 2,
8702
- signature: createHash11("sha256").update(`${callKey}\0${fingerprint}`).digest("hex")
9205
+ signature: createHash12("sha256").update(`${callKey}\0${fingerprint}`).digest("hex")
8703
9206
  };
8704
9207
  }
8705
9208
  receipt(call, failureClass, attempt, circuitOpen) {
@@ -8735,10 +9238,10 @@ function isRetryable(failureClass) {
8735
9238
  return RETRY_BUDGET[failureClass] > 0;
8736
9239
  }
8737
9240
  function callSignature(call) {
8738
- return createHash11("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}`).digest("hex");
9241
+ return createHash12("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}`).digest("hex");
8739
9242
  }
8740
9243
  function failureSignature(call, failureClass) {
8741
- return createHash11("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}\0${failureClass}`).digest("hex");
9244
+ return createHash12("sha256").update(`${call.name}\0${stableJson(redact(call.arguments))}\0${failureClass}`).digest("hex");
8742
9245
  }
8743
9246
  function redact(value, key = "") {
8744
9247
  if (/api[_-]?key|authorization|cookie|password|secret|token/i.test(key)) return "[redacted]";
@@ -8905,8 +9408,8 @@ ${tail}`;
8905
9408
  }
8906
9409
  return best || sliceStartByTokens(value, budget);
8907
9410
  }
8908
- function clamp2(value, minimum, maximum) {
8909
- return Math.max(minimum, Math.min(maximum, value));
9411
+ function clamp2(value, minimum2, maximum) {
9412
+ return Math.max(minimum2, Math.min(maximum, value));
8910
9413
  }
8911
9414
  function boundedInlineByTokens(value, maxTokens) {
8912
9415
  const normalized = value.replace(/[\r\n\t]+/gu, " ").trim();
@@ -9086,9 +9589,9 @@ function escapeAttribute5(value) {
9086
9589
  }
9087
9590
 
9088
9591
  // src/agent/reuse-gate.ts
9089
- import { createHash as createHash12 } from "node:crypto";
9592
+ import { createHash as createHash13 } from "node:crypto";
9090
9593
  import { readFile as readFile14, stat as stat10 } from "node:fs/promises";
9091
- import { basename as basename8, extname as extname2 } from "node:path";
9594
+ import { basename as basename8, extname as extname3 } from "node:path";
9092
9595
  var MAX_CANDIDATES = 5;
9093
9596
  var SKIPPED_EXTENSIONS = /* @__PURE__ */ new Set([
9094
9597
  ".md",
@@ -9153,7 +9656,7 @@ async function evaluateReuseGate(input2) {
9153
9656
  if (preview.paths.every(isExemptPath)) return { triggered: false };
9154
9657
  const targetPaths = preview.paths.slice(0, MAX_CANDIDATES);
9155
9658
  const query = [input2.request, ...preview.symbols, ...targetPaths.map((path) => basename8(path))].join(" ").slice(0, 8e3);
9156
- const queryHash = hash2(query);
9659
+ const queryHash = hash3(query);
9157
9660
  let refresh;
9158
9661
  try {
9159
9662
  refresh = input2.context.flushDirty ? await input2.context.flushDirty() : { status: "current", paths: 0 };
@@ -9276,7 +9779,7 @@ function isExemptPath(path) {
9276
9779
  const normalized = path.replaceAll("\\", "/").toLocaleLowerCase();
9277
9780
  if (/(^|\/)(test|tests|__tests__|fixtures|fixture|generated|vendor|dist|build)(\/|$)/.test(normalized)) return true;
9278
9781
  if (/(\.generated|\.min)\.[^.]+$/.test(normalized)) return true;
9279
- const extension = extname2(normalized);
9782
+ const extension = extname3(normalized);
9280
9783
  if (SKIPPED_EXTENSIONS.has(extension)) return true;
9281
9784
  return !SOURCE_EXTENSIONS.has(extension);
9282
9785
  }
@@ -9294,13 +9797,192 @@ function unresolvedReceipt(input2, queryHash, targetPaths, trigger, detail) {
9294
9797
  warningOnly: true
9295
9798
  };
9296
9799
  }
9297
- function hash2(value) {
9298
- return createHash12("sha256").update(value).digest("hex");
9800
+ function hash3(value) {
9801
+ return createHash13("sha256").update(value).digest("hex");
9299
9802
  }
9300
9803
  function roundScore(value) {
9301
9804
  return Number.isFinite(value) ? Math.round(value * 1e3) / 1e3 : 0;
9302
9805
  }
9303
9806
 
9807
+ // src/agent/duplication-audit.ts
9808
+ import { createHash as createHash14 } from "node:crypto";
9809
+ import { readFile as readFile15 } from "node:fs/promises";
9810
+ var NEAR_CLONE_THRESHOLD = 0.55;
9811
+ var MAX_MATCHES = 8;
9812
+ async function auditChangedFunctions(input2) {
9813
+ const auditableFiles = input2.changedFiles.filter(supportsFunctionFingerprintPath);
9814
+ if (!auditableFiles.length) return void 0;
9815
+ const reports = [];
9816
+ let skippedSmallFunctions = 0;
9817
+ try {
9818
+ for (const path of auditableFiles) {
9819
+ let content;
9820
+ try {
9821
+ content = await readFile15(path, "utf8");
9822
+ } catch (error) {
9823
+ if (error.code === "ENOENT") continue;
9824
+ throw error;
9825
+ }
9826
+ const report = extractFunctionFingerprintReport(path, content);
9827
+ skippedSmallFunctions += report.skippedSmallFunctions;
9828
+ reports.push(report);
9829
+ }
9830
+ } catch {
9831
+ return unresolved(input2.changeSequence, input2.baseline?.generation);
9832
+ }
9833
+ const currentFunctions = reports.flatMap((report) => report.functions.map(withoutTokens));
9834
+ if (!currentFunctions.length) {
9835
+ return auditableFiles.some((path) => input2.recheckPaths?.has(path)) && input2.baseline ? clear(input2.baseline.generation, input2.changeSequence, skippedSmallFunctions) : void 0;
9836
+ }
9837
+ if (!input2.baseline) return unresolved(input2.changeSequence);
9838
+ const lookup = createBaselineLookup(input2.baseline.functions);
9839
+ const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions);
9840
+ const added = currentFunctions.flatMap((current) => {
9841
+ const previous = previousByCurrent.get(locationIdentity(current));
9842
+ const recheck = input2.recheckFunctions?.has(identity(current)) || previous !== void 0 && input2.recheckFunctions?.has(identity(previous));
9843
+ return !previous || current.tokenCount >= previous.tokenCount * 1.5 || recheck ? [{ current, previous }] : [];
9844
+ });
9845
+ if (!added.length) return void 0;
9846
+ const matches = [];
9847
+ for (const { current: item, previous } of added) {
9848
+ let best;
9849
+ for (const candidate of findCandidateFunctions(lookup, item)) {
9850
+ if (previous && locationIdentity(candidate) === locationIdentity(previous)) continue;
9851
+ const similarity = fingerprintSimilarity(item, candidate);
9852
+ if (similarity < NEAR_CLONE_THRESHOLD || best && best.similarity >= similarity) continue;
9853
+ best = { candidate, similarity };
9854
+ }
9855
+ if (!best) continue;
9856
+ matches.push({
9857
+ matchId: matchId(input2.baseline.generation, input2.changeSequence, item, best.candidate, best.similarity),
9858
+ changedPath: item.path,
9859
+ changedSymbol: item.symbol,
9860
+ candidatePath: best.candidate.path,
9861
+ candidateSymbol: best.candidate.symbol,
9862
+ kind: best.similarity === 1 ? "type-1-or-2" : "type-3",
9863
+ similarity: round(best.similarity)
9864
+ });
9865
+ }
9866
+ matches.sort((left, right) => right.similarity - left.similarity || left.changedPath.localeCompare(right.changedPath));
9867
+ const bounded = matches.slice(0, MAX_MATCHES);
9868
+ return {
9869
+ baselineGeneration: input2.baseline.generation,
9870
+ changeSequence: input2.changeSequence,
9871
+ status: bounded.length ? "warning" : "clear",
9872
+ warningOnly: true,
9873
+ checkedFunctions: added.length,
9874
+ skippedSmallFunctions,
9875
+ matches: bounded,
9876
+ rationale: bounded.length ? `${bounded.length} deterministic duplicate candidate${bounded.length === 1 ? "" : "s"} found; review for reuse.` : "No deterministic duplicate candidate exceeded the warning threshold."
9877
+ };
9878
+ }
9879
+ function clear(baselineGeneration, changeSequence, skippedSmallFunctions) {
9880
+ return {
9881
+ baselineGeneration,
9882
+ changeSequence,
9883
+ status: "clear",
9884
+ warningOnly: true,
9885
+ checkedFunctions: 0,
9886
+ skippedSmallFunctions,
9887
+ matches: [],
9888
+ rationale: "No active deterministic duplicate candidate remains on the rechecked path."
9889
+ };
9890
+ }
9891
+ function withoutTokens(value) {
9892
+ const { normalizedTokens: _tokens, ...fingerprint } = value;
9893
+ return fingerprint;
9894
+ }
9895
+ function identity(value) {
9896
+ return `${value.path}\0${value.symbol}`;
9897
+ }
9898
+ function locationIdentity(value) {
9899
+ return `${identity(value)}\0${value.startLine}\0${value.endLine}`;
9900
+ }
9901
+ function createBaselineLookup(functions) {
9902
+ const lookup = {
9903
+ byIdentity: /* @__PURE__ */ new Map(),
9904
+ byPath: /* @__PURE__ */ new Map(),
9905
+ byExactHash: /* @__PURE__ */ new Map(),
9906
+ byFingerprint: /* @__PURE__ */ new Map()
9907
+ };
9908
+ for (const item of functions) {
9909
+ appendLookup(lookup.byIdentity, identity(item), item);
9910
+ appendLookup(lookup.byPath, item.path, item);
9911
+ appendLookup(lookup.byExactHash, item.exactHash, item);
9912
+ for (const fingerprint of new Set(item.fingerprints)) {
9913
+ appendLookup(lookup.byFingerprint, fingerprint, item);
9914
+ }
9915
+ }
9916
+ return lookup;
9917
+ }
9918
+ function appendLookup(lookup, key, item) {
9919
+ const values = lookup.get(key);
9920
+ if (values) values.push(item);
9921
+ else lookup.set(key, [item]);
9922
+ }
9923
+ function findCandidateFunctions(lookup, current) {
9924
+ const exact = lookup.byExactHash.get(current.exactHash);
9925
+ if (exact?.length) return exact;
9926
+ const candidates = /* @__PURE__ */ new Map();
9927
+ for (const fingerprint of new Set(current.fingerprints)) {
9928
+ for (const candidate of lookup.byFingerprint.get(fingerprint) ?? []) {
9929
+ candidates.set(locationIdentity(candidate), candidate);
9930
+ }
9931
+ }
9932
+ return [...candidates.values()];
9933
+ }
9934
+ function matchCurrentFunctions(baseline, current) {
9935
+ const matches = /* @__PURE__ */ new Map();
9936
+ const available = new Map(baseline.map((item) => [locationIdentity(item), item]));
9937
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item) && linesOverlap(candidate, item)), false);
9938
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item)), true);
9939
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => candidate.path === item.path && linesOverlap(candidate, item)), true);
9940
+ return matches;
9941
+ }
9942
+ function assignMatches(current, matches, available, candidatesFor, requireSimilarity) {
9943
+ for (const item of current) {
9944
+ const currentId = locationIdentity(item);
9945
+ if (matches.has(currentId)) continue;
9946
+ let best;
9947
+ for (const candidate of candidatesFor(item)) {
9948
+ const similarity = fingerprintSimilarity(candidate, item);
9949
+ if (!best || similarity > best.similarity) best = { candidate, similarity };
9950
+ }
9951
+ if (!best || requireSimilarity && best.similarity < NEAR_CLONE_THRESHOLD) continue;
9952
+ matches.set(currentId, best.candidate);
9953
+ available.delete(locationIdentity(best.candidate));
9954
+ }
9955
+ }
9956
+ function linesOverlap(left, right) {
9957
+ return Math.max(left.startLine, right.startLine) <= Math.min(left.endLine, right.endLine);
9958
+ }
9959
+ function unresolved(changeSequence, generation = "unavailable") {
9960
+ return {
9961
+ baselineGeneration: generation,
9962
+ changeSequence,
9963
+ status: "unresolved",
9964
+ warningOnly: true,
9965
+ checkedFunctions: 0,
9966
+ skippedSmallFunctions: 0,
9967
+ matches: [],
9968
+ rationale: "Duplication evidence is incomplete; the write remains allowed in warning-only mode."
9969
+ };
9970
+ }
9971
+ function round(value) {
9972
+ return Math.round(value * 1e3) / 1e3;
9973
+ }
9974
+ function matchId(generation, changeSequence, changed, candidate, similarity) {
9975
+ return createHash14("sha256").update([
9976
+ generation,
9977
+ String(changeSequence),
9978
+ changed.path,
9979
+ changed.symbol,
9980
+ candidate.path,
9981
+ candidate.symbol,
9982
+ similarity === 1 ? "type-1-or-2" : "type-3"
9983
+ ].join("\0")).digest("hex").slice(0, 24);
9984
+ }
9985
+
9304
9986
  // src/agent/runner.ts
9305
9987
  var AgentRunner = class {
9306
9988
  config;
@@ -9371,6 +10053,7 @@ var AgentRunner = class {
9371
10053
  await options.onEvent?.(event);
9372
10054
  };
9373
10055
  const changeSequenceAtStart = this.changeSequence;
10056
+ const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
9374
10057
  const runChangedFiles = /* @__PURE__ */ new Set();
9375
10058
  const verificationEvidence = [];
9376
10059
  const toolRecovery = new ToolRecoveryController();
@@ -9400,7 +10083,11 @@ var AgentRunner = class {
9400
10083
  this.changeSequence,
9401
10084
  mutationTracking,
9402
10085
  activeRunContract,
9403
- this.session.audit ?? []
10086
+ this.session.audit ?? [],
10087
+ hasDuplicationActivity(this.session.audit ?? [], runStartedAt) ? buildDuplicationCompletion(
10088
+ this.session.audit ?? [],
10089
+ this.session.duplicationSuppressions ?? []
10090
+ ) : void 0
9404
10091
  );
9405
10092
  if (completion.acceptance && activeRunContract && activeRunContract.state !== "draft") {
9406
10093
  activeRunContract.state = completion.acceptance.state;
@@ -9526,7 +10213,11 @@ var AgentRunner = class {
9526
10213
  this.tools,
9527
10214
  options.askMode === true,
9528
10215
  contractEnabled,
9529
- this.hasReadableToolArtifact()
10216
+ this.hasReadableToolArtifact(),
10217
+ activeDuplicationMatchIds(
10218
+ this.session.audit ?? [],
10219
+ this.session.duplicationSuppressions ?? []
10220
+ ).size > 0
9530
10221
  );
9531
10222
  const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(visibleTools);
9532
10223
  if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
@@ -9811,11 +10502,14 @@ ${input2}`
9811
10502
  contextEngine: this.contextEngine,
9812
10503
  toolArtifactStore: this.toolArtifactStore,
9813
10504
  emit,
9814
- ...options.signal ? { signal: options.signal } : {}
10505
+ ...options.signal ? { signal: options.signal } : {},
10506
+ toolCallId: call.id
9815
10507
  };
9816
10508
  try {
9817
10509
  let reuseReceipt;
9818
10510
  let reuseWarning;
10511
+ let duplicationBaseline;
10512
+ const duplicationAuditEnabled = categories.includes("write") && (call.name === "write_file" || call.name === "apply_patch") && Boolean(this.contextEngine.functionFingerprints);
9819
10513
  if (categories.includes("write") && this.activeReuseGate && !this.activeReuseGate.attempted && (call.name === "write_file" || call.name === "apply_patch")) {
9820
10514
  this.activeReuseGate.attempted = true;
9821
10515
  try {
@@ -9835,6 +10529,13 @@ ${input2}`
9835
10529
  reuseWarning = "Reuse check (warning-only) was inconclusive.";
9836
10530
  }
9837
10531
  }
10532
+ if (duplicationAuditEnabled) {
10533
+ try {
10534
+ duplicationBaseline = await this.contextEngine.functionFingerprints?.();
10535
+ } catch {
10536
+ duplicationBaseline = void 0;
10537
+ }
10538
+ }
9838
10539
  let checkpointId;
9839
10540
  if (this.config.agent.checkpointBeforeWrite && categories.includes("write") && tool.affectedPaths) {
9840
10541
  const paths = await tool.affectedPaths(call.arguments, executionContext);
@@ -9852,6 +10553,22 @@ ${input2}`
9852
10553
  throwIfAborted(options.signal);
9853
10554
  const execution = await tool.execute(call.arguments, toolExecutionContext);
9854
10555
  const changedFiles = await this.acceptChangedFiles(execution.changedFiles ?? []);
10556
+ const activeDuplicateFunctions = new Set(
10557
+ buildDuplicationCompletion(
10558
+ this.session.audit ?? [],
10559
+ []
10560
+ )?.matches.map((match) => `${match.changedPath}\0${match.changedSymbol}`) ?? []
10561
+ );
10562
+ const activeDuplicatePaths = new Set(
10563
+ buildDuplicationCompletion(this.session.audit ?? [], [])?.matches.map((match) => match.changedPath) ?? []
10564
+ );
10565
+ const duplicationAudit = duplicationAuditEnabled ? await auditChangedFunctions({
10566
+ ...duplicationBaseline ? { baseline: duplicationBaseline } : {},
10567
+ changedFiles,
10568
+ changeSequence: this.changeSequence,
10569
+ recheckFunctions: activeDuplicateFunctions,
10570
+ recheckPaths: activeDuplicatePaths
10571
+ }) : void 0;
9855
10572
  const tasksBefore = JSON.stringify(this.session.tasks);
9856
10573
  let afterHookError;
9857
10574
  let afterHooks = [];
@@ -9870,11 +10587,17 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
9870
10587
  if (reuseWarning) completeContent = `${reuseWarning}
9871
10588
 
9872
10589
  ${completeContent}`;
10590
+ if (duplicationAudit?.status === "warning" || duplicationAudit?.status === "unresolved") {
10591
+ completeContent = `Duplication audit (warning-only): ${duplicationAudit.rationale}
10592
+
10593
+ ${completeContent}`;
10594
+ }
9873
10595
  const metadata = {
9874
10596
  ...execution.metadata ?? {},
9875
10597
  ...changedFiles.length ? { changedFiles } : {},
9876
10598
  ...checkpointId ? { checkpointId } : {},
9877
10599
  ...reuseReceipt ? { reuseReceipt } : {},
10600
+ ...duplicationAudit ? { duplicationAudit } : {},
9878
10601
  ...beforeHooks.length || afterHooks.length ? { hooks: { before: beforeHooks.length, after: afterHooks.length } } : {},
9879
10602
  ...afterHookError ? { toolSucceeded: true, hookError: afterHookError.message } : {}
9880
10603
  };
@@ -10324,9 +11047,9 @@ ${content}` : content,
10324
11047
  ...failure ? { metadata: { failure } } : {}
10325
11048
  };
10326
11049
  }
10327
- function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable) {
11050
+ function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable) {
10328
11051
  return tools.definitions().filter(
10329
- (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact")
11052
+ (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
10330
11053
  );
10331
11054
  }
10332
11055
  function formatToolError(error) {
@@ -10361,7 +11084,7 @@ async function safeEmit(emit, event) {
10361
11084
  }
10362
11085
 
10363
11086
  // src/agent/profiles.ts
10364
- import { lstat as lstat17, readFile as readFile15, readdir as readdir6 } from "node:fs/promises";
11087
+ import { lstat as lstat17, readFile as readFile16, readdir as readdir6 } from "node:fs/promises";
10365
11088
  import { homedir as homedir3 } from "node:os";
10366
11089
  import { basename as basename9, join as join15 } from "node:path";
10367
11090
  import { parse as parseYaml2 } from "yaml";
@@ -10489,7 +11212,7 @@ async function readProfile(path, source) {
10489
11212
  try {
10490
11213
  const info = await lstat17(path);
10491
11214
  if (!info.isFile() || info.isSymbolicLink() || info.size > 1e5) return void 0;
10492
- const raw = await readFile15(path, "utf8");
11215
+ const raw = await readFile16(path, "utf8");
10493
11216
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
10494
11217
  const frontmatter = match ? parseYaml2(match[1] ?? "") : {};
10495
11218
  const prompt = (match ? raw.slice(match[0].length) : raw).trim();
@@ -10518,7 +11241,7 @@ function integer(value, fallback, min, max) {
10518
11241
  // src/agent/delegation.ts
10519
11242
  import { randomUUID as randomUUID14 } from "node:crypto";
10520
11243
  import { join as join18 } from "node:path";
10521
- import { z as z18 } from "zod";
11244
+ import { z as z19 } from "zod";
10522
11245
 
10523
11246
  // src/agent/external-runtime.ts
10524
11247
  async function runExternalAgent(request) {
@@ -10645,86 +11368,86 @@ function numeric(...values) {
10645
11368
  }
10646
11369
 
10647
11370
  // src/agent/team-store.ts
10648
- import { createHash as createHash13, randomUUID as randomUUID13 } from "node:crypto";
10649
- import { lstat as lstat18, readFile as readFile16, readdir as readdir7, rm as rm3 } from "node:fs/promises";
11371
+ import { createHash as createHash15, randomUUID as randomUUID13 } from "node:crypto";
11372
+ import { lstat as lstat18, readFile as readFile17, readdir as readdir7, rm as rm3 } from "node:fs/promises";
10650
11373
  import { join as join16, resolve as resolve16 } from "node:path";
10651
- import { z as z17 } from "zod";
10652
- var runIdSchema = z17.string().uuid();
10653
- var hashSchema = z17.string().regex(/^[a-f0-9]{64}$/u);
10654
- var artifactSchema2 = z17.object({
11374
+ import { z as z18 } from "zod";
11375
+ var runIdSchema = z18.string().uuid();
11376
+ var hashSchema = z18.string().regex(/^[a-f0-9]{64}$/u);
11377
+ var artifactSchema2 = z18.object({
10655
11378
  sha256: hashSchema,
10656
- bytes: z17.number().int().nonnegative().max(5e5)
11379
+ bytes: z18.number().int().nonnegative().max(5e5)
10657
11380
  }).strict();
10658
- var phaseSchema = z17.enum(["work", "review", "revision", "write"]);
10659
- var agentRecordSchema = z17.object({
10660
- id: z17.string().uuid(),
10661
- profile: z17.string(),
10662
- provider: z17.string(),
10663
- model: z17.string(),
11381
+ var phaseSchema = z18.enum(["work", "review", "revision", "write"]);
11382
+ var agentRecordSchema = z18.object({
11383
+ id: z18.string().uuid(),
11384
+ profile: z18.string(),
11385
+ provider: z18.string(),
11386
+ model: z18.string(),
10664
11387
  phase: phaseSchema,
10665
- ok: z17.boolean(),
10666
- createdAt: z17.string(),
10667
- startedAt: z17.string().optional(),
10668
- endedAt: z17.string().optional(),
10669
- durationMs: z17.number().int().nonnegative().optional(),
10670
- toolCalls: z17.number().int().nonnegative().optional(),
10671
- usage: z17.object({
10672
- inputTokens: z17.number().int().nonnegative(),
10673
- outputTokens: z17.number().int().nonnegative()
11388
+ ok: z18.boolean(),
11389
+ createdAt: z18.string(),
11390
+ startedAt: z18.string().optional(),
11391
+ endedAt: z18.string().optional(),
11392
+ durationMs: z18.number().int().nonnegative().optional(),
11393
+ toolCalls: z18.number().int().nonnegative().optional(),
11394
+ usage: z18.object({
11395
+ inputTokens: z18.number().int().nonnegative(),
11396
+ outputTokens: z18.number().int().nonnegative()
10674
11397
  }).strict().optional(),
10675
11398
  report: artifactSchema2
10676
11399
  }).strict();
10677
- var messageRecordSchema = z17.object({
10678
- id: z17.string().uuid(),
10679
- from: z17.string(),
10680
- to: z17.string(),
10681
- createdAt: z17.string(),
11400
+ var messageRecordSchema = z18.object({
11401
+ id: z18.string().uuid(),
11402
+ from: z18.string(),
11403
+ to: z18.string(),
11404
+ createdAt: z18.string(),
10682
11405
  content: artifactSchema2
10683
11406
  }).strict();
10684
- var writerIntegrationSchema = z17.object({
10685
- status: z17.enum(["ready", "conflict", "integrated"]),
10686
- checkedAt: z17.string(),
10687
- detail: z17.string().max(2e4),
10688
- checkpoint: z17.object({
10689
- sessionId: z17.string(),
10690
- checkpointId: z17.string()
11407
+ var writerIntegrationSchema = z18.object({
11408
+ status: z18.enum(["ready", "conflict", "integrated"]),
11409
+ checkedAt: z18.string(),
11410
+ detail: z18.string().max(2e4),
11411
+ checkpoint: z18.object({
11412
+ sessionId: z18.string(),
11413
+ checkpointId: z18.string()
10691
11414
  }).strict().optional(),
10692
- integratedAt: z17.string().optional()
11415
+ integratedAt: z18.string().optional()
10693
11416
  }).strict();
10694
- var writerLaneSchema = z17.object({
10695
- profile: z17.string(),
10696
- reviewer: z17.string(),
10697
- baseCommit: z17.string().regex(/^[a-f0-9]{40,64}$/u),
10698
- outcome: z17.enum(["accepted", "rejected", "failed", "cancelled"]),
11417
+ var writerLaneSchema = z18.object({
11418
+ profile: z18.string(),
11419
+ reviewer: z18.string(),
11420
+ baseCommit: z18.string().regex(/^[a-f0-9]{40,64}$/u),
11421
+ outcome: z18.enum(["accepted", "rejected", "failed", "cancelled"]),
10699
11422
  patch: artifactSchema2,
10700
- files: z17.array(z17.string().min(1).max(4e3)).max(2e3),
10701
- worktreeCleaned: z17.boolean(),
11423
+ files: z18.array(z18.string().min(1).max(4e3)).max(2e3),
11424
+ worktreeCleaned: z18.boolean(),
10702
11425
  review: artifactSchema2.optional(),
10703
11426
  integration: writerIntegrationSchema.optional()
10704
11427
  }).strict();
10705
11428
  var manifestFields = {
10706
11429
  id: runIdSchema,
10707
- workspace: z17.string(),
10708
- objective: z17.string().max(3e4),
10709
- reviewer: z17.string(),
10710
- createdAt: z17.string(),
10711
- updatedAt: z17.string(),
10712
- status: z17.enum(["running", "accepted", "rejected", "failed"]),
10713
- maxReviewRounds: z17.number().int().min(0).max(3),
10714
- reviewRounds: z17.number().int().min(0).max(3),
10715
- agents: z17.array(agentRecordSchema).max(256),
10716
- messages: z17.array(messageRecordSchema).max(512)
11430
+ workspace: z18.string(),
11431
+ objective: z18.string().max(3e4),
11432
+ reviewer: z18.string(),
11433
+ createdAt: z18.string(),
11434
+ updatedAt: z18.string(),
11435
+ status: z18.enum(["running", "accepted", "rejected", "failed"]),
11436
+ maxReviewRounds: z18.number().int().min(0).max(3),
11437
+ reviewRounds: z18.number().int().min(0).max(3),
11438
+ agents: z18.array(agentRecordSchema).max(256),
11439
+ messages: z18.array(messageRecordSchema).max(512)
10717
11440
  };
10718
- var manifestV1Schema = z17.object({
10719
- version: z17.literal(1),
11441
+ var manifestV1Schema = z18.object({
11442
+ version: z18.literal(1),
10720
11443
  ...manifestFields
10721
11444
  }).strict();
10722
- var manifestV2Schema = z17.object({
10723
- version: z17.literal(2),
11445
+ var manifestV2Schema = z18.object({
11446
+ version: z18.literal(2),
10724
11447
  ...manifestFields,
10725
11448
  writer: writerLaneSchema.optional()
10726
11449
  }).strict();
10727
- var manifestSchema2 = z17.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
11450
+ var manifestSchema2 = z18.discriminatedUnion("version", [manifestV1Schema, manifestV2Schema]);
10728
11451
  var TeamRunStore = class {
10729
11452
  workspace;
10730
11453
  directory;
@@ -10819,7 +11542,7 @@ var TeamRunStore = class {
10819
11542
  await this.assertRunDirectory(runId);
10820
11543
  const path = join16(this.runDirectory(runId), "manifest.json");
10821
11544
  await this.assertRegularFile(path);
10822
- const manifest = manifestSchema2.parse(JSON.parse(await readFile16(path, "utf8")));
11545
+ const manifest = manifestSchema2.parse(JSON.parse(await readFile17(path, "utf8")));
10823
11546
  if (manifest.id !== runId || resolve16(manifest.workspace) !== this.workspace) {
10824
11547
  throw new Error("Team run manifest identity does not match its location.");
10825
11548
  }
@@ -10834,7 +11557,7 @@ var TeamRunStore = class {
10834
11557
  }
10835
11558
  async readArtifact(runId, artifact) {
10836
11559
  await this.verifyArtifact(runId, artifact);
10837
- return readFile16(this.artifactPath(runId, artifact.sha256), "utf8");
11560
+ return readFile17(this.artifactPath(runId, artifact.sha256), "utf8");
10838
11561
  }
10839
11562
  async list() {
10840
11563
  await this.writes;
@@ -10890,7 +11613,7 @@ var TeamRunStore = class {
10890
11613
  await this.assertRunDirectory(runId);
10891
11614
  const path = join16(this.runDirectory(runId), "manifest.json");
10892
11615
  await this.assertRegularFile(path);
10893
- return manifestSchema2.parse(JSON.parse(await readFile16(path, "utf8")));
11616
+ return manifestSchema2.parse(JSON.parse(await readFile17(path, "utf8")));
10894
11617
  }
10895
11618
  async writeManifest(manifest) {
10896
11619
  const directory = this.runDirectory(manifest.id);
@@ -10903,7 +11626,7 @@ var TeamRunStore = class {
10903
11626
  async writeArtifact(runId, content, truncate = true) {
10904
11627
  const data = boundedArtifactText(content, 5e5, truncate);
10905
11628
  const bytes = Buffer.byteLength(data);
10906
- const sha256 = createHash13("sha256").update(data).digest("hex");
11629
+ const sha256 = createHash15("sha256").update(data).digest("hex");
10907
11630
  const directory = join16(this.runDirectory(runId), "blobs");
10908
11631
  await ensureWorkspaceStorageDirectory(this.workspace, directory, {
10909
11632
  requireActiveNamespace: this.managedDirectory
@@ -10911,8 +11634,8 @@ var TeamRunStore = class {
10911
11634
  const path = join16(directory, `${sha256}.txt`);
10912
11635
  try {
10913
11636
  await this.assertRegularFile(path);
10914
- const existing = await readFile16(path);
10915
- if (createHash13("sha256").update(existing).digest("hex") !== sha256) {
11637
+ const existing = await readFile17(path);
11638
+ if (createHash15("sha256").update(existing).digest("hex") !== sha256) {
10916
11639
  throw new Error(`Team artifact hash collision or corruption: ${sha256}`);
10917
11640
  }
10918
11641
  } catch (error) {
@@ -10932,9 +11655,9 @@ var TeamRunStore = class {
10932
11655
  hashSchema.parse(artifact.sha256);
10933
11656
  const path = this.artifactPath(runId, artifact.sha256);
10934
11657
  await this.assertRegularFile(path);
10935
- const data = await readFile16(path);
10936
- const hash3 = createHash13("sha256").update(data).digest("hex");
10937
- if (hash3 !== artifact.sha256 || data.byteLength !== artifact.bytes) {
11658
+ const data = await readFile17(path);
11659
+ const hash4 = createHash15("sha256").update(data).digest("hex");
11660
+ if (hash4 !== artifact.sha256 || data.byteLength !== artifact.bytes) {
10938
11661
  throw new Error(`Team artifact integrity check failed: ${artifact.sha256}`);
10939
11662
  }
10940
11663
  }
@@ -10996,7 +11719,7 @@ function resolveAgentModelRoute(team, parent, profile) {
10996
11719
  }
10997
11720
 
10998
11721
  // src/agent/writer-lane.ts
10999
- import { createHash as createHash14 } from "node:crypto";
11722
+ import { createHash as createHash16 } from "node:crypto";
11000
11723
  import { lstat as lstat19, mkdtemp, realpath as realpath7, rm as rm4 } from "node:fs/promises";
11001
11724
  import { tmpdir as tmpdir2 } from "node:os";
11002
11725
  import { isAbsolute as isAbsolute6, join as join17, resolve as resolve17 } from "node:path";
@@ -11094,7 +11817,7 @@ var WriterLane = class {
11094
11817
  return {
11095
11818
  baseCommit,
11096
11819
  patch,
11097
- patchSha256: createHash14("sha256").update(patch).digest("hex"),
11820
+ patchSha256: createHash16("sha256").update(patch).digest("hex"),
11098
11821
  files,
11099
11822
  worktreeCleaned,
11100
11823
  value
@@ -11324,14 +12047,14 @@ async function pathExists2(path) {
11324
12047
  }
11325
12048
 
11326
12049
  // src/agent/delegation.ts
11327
- var writerRunInputSchema = z18.object({
11328
- task: z18.string().min(1).max(2e4),
11329
- profile: z18.string().max(64).optional(),
11330
- reviewer: z18.string().max(64).optional()
12050
+ var writerRunInputSchema = z19.object({
12051
+ task: z19.string().min(1).max(2e4),
12052
+ profile: z19.string().max(64).optional(),
12053
+ reviewer: z19.string().max(64).optional()
11331
12054
  }).strict();
11332
- var writerIntegrateInputSchema = z18.object({
11333
- run_id: z18.string().uuid(),
11334
- patch_sha256: z18.string().regex(/^[a-f0-9]{64}$/u)
12055
+ var writerIntegrateInputSchema = z19.object({
12056
+ run_id: z19.string().uuid(),
12057
+ patch_sha256: z19.string().regex(/^[a-f0-9]{64}$/u)
11335
12058
  }).strict();
11336
12059
  var DelegationManager = class {
11337
12060
  constructor(options) {
@@ -11392,10 +12115,10 @@ var DelegationManager = class {
11392
12115
  },
11393
12116
  async execute(arguments_, context) {
11394
12117
  if (!manager.team.enabled) return { ok: false, content: "Multi-agent delegation is disabled." };
11395
- const input2 = z18.object({
11396
- tasks: z18.array(z18.object({
11397
- profile: z18.string().max(64).optional(),
11398
- task: z18.string().min(1).max(2e4)
12118
+ const input2 = z19.object({
12119
+ tasks: z19.array(z19.object({
12120
+ profile: z19.string().max(64).optional(),
12121
+ task: z19.string().min(1).max(2e4)
11399
12122
  })).min(1).max(manager.team.maxDelegations)
11400
12123
  }).parse(arguments_);
11401
12124
  const tasks = input2.tasks.map((task) => ({
@@ -11441,13 +12164,13 @@ var DelegationManager = class {
11441
12164
  },
11442
12165
  async execute(arguments_, context) {
11443
12166
  if (!manager.team.enabled) return { ok: false, content: "Multi-agent teams are disabled." };
11444
- const input2 = z18.object({
11445
- objective: z18.string().min(1).max(3e4),
11446
- tasks: z18.array(z18.object({
11447
- profile: z18.string().max(64).optional(),
11448
- task: z18.string().min(1).max(2e4)
12167
+ const input2 = z19.object({
12168
+ objective: z19.string().min(1).max(3e4),
12169
+ tasks: z19.array(z19.object({
12170
+ profile: z19.string().max(64).optional(),
12171
+ task: z19.string().min(1).max(2e4)
11449
12172
  })).min(1).max(manager.team.maxDelegations),
11450
- reviewer: z18.string().max(64).optional()
12173
+ reviewer: z19.string().max(64).optional()
11451
12174
  }).parse(arguments_);
11452
12175
  const tasks = input2.tasks.map((task) => ({
11453
12176
  profile: task.profile ?? manager.team.defaultProfile,
@@ -12811,10 +13534,10 @@ function supportsNodeVersion(version) {
12811
13534
  const match = version.trim().replace(/^v/u, "").match(/^(\d+)\.(\d+)\.(\d+)/u);
12812
13535
  if (!match) return false;
12813
13536
  const current = match.slice(1).map(Number);
12814
- const minimum = MINIMUM_NODE_VERSION.split(".").map(Number);
12815
- for (let index = 0; index < minimum.length; index += 1) {
12816
- if ((current[index] ?? 0) > (minimum[index] ?? 0)) return true;
12817
- if ((current[index] ?? 0) < (minimum[index] ?? 0)) return false;
13537
+ const minimum2 = MINIMUM_NODE_VERSION.split(".").map(Number);
13538
+ for (let index = 0; index < minimum2.length; index += 1) {
13539
+ if ((current[index] ?? 0) > (minimum2[index] ?? 0)) return true;
13540
+ if ((current[index] ?? 0) < (minimum2[index] ?? 0)) return false;
12818
13541
  }
12819
13542
  return true;
12820
13543
  }
@@ -13095,22 +13818,24 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
13095
13818
  if (!completion || completion.status === "no_changes") return;
13096
13819
  const checks = completion.checks.map((check) => check.command).join(", ");
13097
13820
  const suffix = checks ? ` ${this.glyphs.separator} ${checks}` : "";
13821
+ const duplication = completion.duplication;
13822
+ const duplicateSuffix = duplication ? ` ${this.glyphs.separator} duplication ${duplication.status} (${duplication.warningCount} warning, ${duplication.unresolvedCount} incomplete, ${duplication.suppressedCount} suppressed)` : "";
13098
13823
  if (completion.status === "verified") {
13099
13824
  process.stderr.write(this.paint.green(
13100
- `${this.glyphs.success} verified ${this.glyphs.separator} ${completion.detail}${suffix}
13825
+ `${this.glyphs.success} verified ${this.glyphs.separator} ${completion.detail}${suffix}${duplicateSuffix}
13101
13826
  `
13102
13827
  ));
13103
13828
  return;
13104
13829
  }
13105
13830
  if (completion.status === "verification_failed") {
13106
13831
  process.stderr.write(this.paint.red(
13107
- `${this.glyphs.error} verification failed ${this.glyphs.separator} ${completion.detail}${suffix}
13832
+ `${this.glyphs.error} verification failed ${this.glyphs.separator} ${completion.detail}${suffix}${duplicateSuffix}
13108
13833
  `
13109
13834
  ));
13110
13835
  return;
13111
13836
  }
13112
13837
  process.stderr.write(this.paint.yellow(
13113
- `${this.glyphs.warning} unverified ${this.glyphs.separator} ${completion.detail}
13838
+ `${this.glyphs.warning} unverified ${this.glyphs.separator} ${completion.detail}${duplicateSuffix}
13114
13839
  `
13115
13840
  ));
13116
13841
  }
@@ -13454,7 +14179,7 @@ function graphemes(value) {
13454
14179
  }
13455
14180
 
13456
14181
  // src/ui/theme.ts
13457
- import { lstat as lstat20, readdir as readdir8, readFile as readFile17 } from "node:fs/promises";
14182
+ import { lstat as lstat20, readdir as readdir8, readFile as readFile18 } from "node:fs/promises";
13458
14183
  import { basename as basename10, join as join19, resolve as resolve19 } from "node:path";
13459
14184
  import React, { createContext, useContext } from "react";
13460
14185
  function defineTheme(seed) {
@@ -13595,7 +14320,7 @@ async function reloadUserThemes(directory = userThemeDirectory()) {
13595
14320
  if (!info.isFile() || info.isSymbolicLink() || info.size > 64e3) {
13596
14321
  throw new Error("must be a regular JSON file smaller than 64 KB");
13597
14322
  }
13598
- const parsed = JSON.parse(await readFile17(path, "utf8"));
14323
+ const parsed = JSON.parse(await readFile18(path, "utf8"));
13599
14324
  const name = themeName(parsed, basename10(entry, ".json"));
13600
14325
  if (builtInThemeNames.has(name)) throw new Error(`cannot replace built-in theme \`${name}\``);
13601
14326
  themes[name] = defineTheme(themeSeed(parsed, name));
@@ -13816,10 +14541,10 @@ function Header({ config, askMode, planMode = false, width = 80, glyphMode = "au
13816
14541
  const separator = ` ${glyphs.separator} `;
13817
14542
  const model = sanitizeInlineTerminalText(`${config.model.provider}/${config.model.model}`);
13818
14543
  const repository = sanitizeInlineTerminalText(basename11(root) || root);
13819
- const minimum = `${brand} ${modeLabel}`;
14544
+ const minimum2 = `${brand} ${modeLabel}`;
13820
14545
  const withRepository = `${brand}${separator}${repository}${separator}${modeLabel}`;
13821
14546
  const showRepository = terminalWidth >= 32 && displayWidth(withRepository) <= terminalWidth;
13822
- const leftWidth = displayWidth(showRepository ? withRepository : minimum);
14547
+ const leftWidth = displayWidth(showRepository ? withRepository : minimum2);
13823
14548
  const modelSpace = terminalWidth - leftWidth - 2;
13824
14549
  const showModel = terminalWidth >= 72 && modelSpace >= 12;
13825
14550
  return /* @__PURE__ */ jsxs(Box, { marginBottom: 1, height: 1, overflowY: "hidden", children: [
@@ -14837,7 +15562,7 @@ function safeWidth(width) {
14837
15562
  }
14838
15563
 
14839
15564
  // src/utils/update-check.ts
14840
- import { mkdir as mkdir9, readFile as readFile18, writeFile as writeFile2 } from "node:fs/promises";
15565
+ import { mkdir as mkdir9, readFile as readFile19, writeFile as writeFile2 } from "node:fs/promises";
14841
15566
  import { join as join20 } from "node:path";
14842
15567
  import stripAnsi3 from "strip-ansi";
14843
15568
  var PACKAGE_NAME = "@skein-code/cli";
@@ -14935,7 +15660,7 @@ function noticeIfNewer(latest, current, highlights) {
14935
15660
  }
14936
15661
  async function readUpdateCache(env = process.env) {
14937
15662
  try {
14938
- const raw = await readFile18(updateCachePath(env), "utf8");
15663
+ const raw = await readFile19(updateCachePath(env), "utf8");
14939
15664
  const parsed = JSON.parse(raw);
14940
15665
  if (typeof parsed?.checkedAt === "number" && Number.isFinite(parsed.checkedAt) && parsed.checkedAt >= 0 && (parsed.latest === null || typeof parsed.latest === "string" && parseVersion(parsed.latest))) {
14941
15666
  const latest = typeof parsed.latest === "string" ? parsed.latest : null;
@@ -15535,9 +16260,9 @@ function uniqueNewestFirst(history) {
15535
16260
  for (let index = history.length - 1; index >= 0; index -= 1) {
15536
16261
  const entry = history[index];
15537
16262
  if (!entry.trim()) continue;
15538
- const identity = entry.normalize("NFC");
15539
- if (seen.has(identity)) continue;
15540
- seen.add(identity);
16263
+ const identity2 = entry.normalize("NFC");
16264
+ if (seen.has(identity2)) continue;
16265
+ seen.add(identity2);
15541
16266
  entries.push(entry);
15542
16267
  }
15543
16268
  return entries;
@@ -15722,6 +16447,18 @@ function toolMetaSummary(metadata) {
15722
16447
  parts.push(`reuse ${decision}${status === "unresolved" ? " (incomplete)" : " (warning)"}`);
15723
16448
  }
15724
16449
  }
16450
+ const duplication = metadata.duplicationAudit;
16451
+ if (duplication && typeof duplication === "object") {
16452
+ const status = duplication.status;
16453
+ const matches = Number(duplication.matches?.length ?? 0);
16454
+ if (status === "warning") parts.push(`duplicates ${matches} (warning)`);
16455
+ else if (status === "unresolved") parts.push("duplicates incomplete");
16456
+ }
16457
+ const suppression = metadata.duplicationSuppression;
16458
+ if (suppression && typeof suppression === "object") {
16459
+ const matchId2 = suppression.matchId;
16460
+ if (typeof matchId2 === "string") parts.push(`duplicate ${matchId2.slice(0, 8)} suppressed`);
16461
+ }
15725
16462
  const hooks = metadata.hooks;
15726
16463
  if (hooks && typeof hooks === "object") {
15727
16464
  const before = Number(hooks.before ?? 0);
@@ -16181,12 +16918,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
16181
16918
  refreshSession();
16182
16919
  if (event.completion && event.completion.status !== "no_changes") {
16183
16920
  const checks = event.completion.checks.map((check) => check.command).join(` ${separator} `);
16921
+ const duplication = event.completion.duplication;
16922
+ const duplicateDetail = duplication ? `${separator} duplication ${duplication.status} (${duplication.warningCount} warning, ${duplication.unresolvedCount} incomplete, ${duplication.suppressedCount} suppressed)` : "";
16184
16923
  append({
16185
16924
  id: nextId(),
16186
16925
  kind: "notice",
16187
16926
  wrapWidth: contentWidth,
16188
16927
  tone: event.completion.status === "verified" ? "success" : event.completion.status === "unverified" ? "warning" : "error",
16189
- text: event.completion.status === "verified" ? `Verified${separator}${event.completion.detail}${checks ? `${separator}${checks}` : ""}` : event.completion.status === "verification_failed" ? `Verification failed${separator}${event.completion.detail}${checks ? `${separator}${checks}` : ""}` : `Unverified${separator}${event.completion.detail}`
16928
+ text: event.completion.status === "verified" ? `Verified${separator}${event.completion.detail}${checks ? `${separator}${checks}` : ""}${duplicateDetail}` : event.completion.status === "verification_failed" ? `Verification failed${separator}${event.completion.detail}${checks ? `${separator}${checks}` : ""}${duplicateDetail}` : `Unverified${separator}${event.completion.detail}${duplicateDetail}`
16190
16929
  });
16191
16930
  }
16192
16931
  if (event.reason !== "completed" && event.reason !== "unverified" && event.reason !== "verification_failed") {
@@ -18208,7 +18947,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
18208
18947
  import stripAnsi5 from "strip-ansi";
18209
18948
 
18210
18949
  // src/mcp/tool.ts
18211
- import { createHash as createHash15 } from "node:crypto";
18950
+ import { createHash as createHash17 } from "node:crypto";
18212
18951
  import stripAnsi4 from "strip-ansi";
18213
18952
  var MAX_ARGUMENT_BYTES = 256e3;
18214
18953
  var MAX_DESCRIPTION_LENGTH = 4e3;
@@ -18216,7 +18955,7 @@ var MAX_RESULT_BYTES = 5 * 1024 * 1024;
18216
18955
  var MAX_SCHEMA_BYTES = 1e5;
18217
18956
  function createMcpToolAdapter(options) {
18218
18957
  const { remoteTool } = options;
18219
- const inputSchema12 = copyInputSchema(remoteTool.inputSchema);
18958
+ const inputSchema13 = copyInputSchema(remoteTool.inputSchema);
18220
18959
  return {
18221
18960
  definition: {
18222
18961
  name: options.exposedName,
@@ -18224,7 +18963,7 @@ function createMcpToolAdapter(options) {
18224
18963
  // MCP servers are an external trust boundary. Read-only annotations are
18225
18964
  // hints from that server and must not lower the local permission level.
18226
18965
  category: "network",
18227
- inputSchema: inputSchema12
18966
+ inputSchema: inputSchema13
18228
18967
  },
18229
18968
  permissionCategories: () => ["network"],
18230
18969
  async execute(arguments_, context) {
@@ -18372,13 +19111,13 @@ function normalizeToolSegment(value, fallback) {
18372
19111
  if (!normalized) return fallback;
18373
19112
  return /^[a-z]/.test(normalized) ? normalized : `${fallback}_${normalized}`;
18374
19113
  }
18375
- function fitToolName(name, identity) {
19114
+ function fitToolName(name, identity2) {
18376
19115
  if (name.length <= 64) return name;
18377
- const suffix = `_${shortHash(identity)}`;
19116
+ const suffix = `_${shortHash(identity2)}`;
18378
19117
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
18379
19118
  }
18380
19119
  function shortHash(value) {
18381
- return createHash15("sha256").update(value).digest("hex").slice(0, 8);
19120
+ return createHash17("sha256").update(value).digest("hex").slice(0, 8);
18382
19121
  }
18383
19122
  function sanitizeOutputText(value) {
18384
19123
  return stripAnsi4(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
@@ -18874,12 +19613,12 @@ var McpManager = class {
18874
19613
  );
18875
19614
  for (const remoteTool of remoteTools) {
18876
19615
  let exposedName = makeMcpToolName(namespace, remoteTool.name);
18877
- const identity = `${serverName}\0${remoteTool.name}`;
18878
- if (seen.has(exposedName) || this.isToolNameOwnedByAnother(exposedName, identity)) {
19616
+ const identity2 = `${serverName}\0${remoteTool.name}`;
19617
+ if (seen.has(exposedName) || this.isToolNameOwnedByAnother(exposedName, identity2)) {
18879
19618
  exposedName = disambiguateMcpToolName(exposedName, serverName, remoteTool.name);
18880
19619
  }
18881
19620
  let collision = 0;
18882
- while (seen.has(exposedName) || this.isToolNameOwnedByAnother(exposedName, identity)) {
19621
+ while (seen.has(exposedName) || this.isToolNameOwnedByAnother(exposedName, identity2)) {
18883
19622
  collision += 1;
18884
19623
  exposedName = disambiguateMcpToolName(
18885
19624
  `${makeMcpToolName(namespace, remoteTool.name)}_${collision}`,
@@ -18895,7 +19634,7 @@ var McpManager = class {
18895
19634
  }
18896
19635
  return active.client.callTool(params, void 0, options);
18897
19636
  };
18898
- let adapter = this.stableAdapters.get(identity);
19637
+ let adapter = this.stableAdapters.get(identity2);
18899
19638
  if (!adapter) {
18900
19639
  adapter = createMcpToolAdapter({
18901
19640
  serverName,
@@ -18904,17 +19643,17 @@ var McpManager = class {
18904
19643
  timeoutMs,
18905
19644
  callTool
18906
19645
  });
18907
- this.stableAdapters.set(identity, adapter);
19646
+ this.stableAdapters.set(identity2, adapter);
18908
19647
  }
18909
19648
  result.set(exposedName, adapter);
18910
19649
  seen.add(exposedName);
18911
- this.toolOwners.set(exposedName, identity);
19650
+ this.toolOwners.set(exposedName, identity2);
18912
19651
  }
18913
19652
  return result;
18914
19653
  }
18915
- isToolNameOwnedByAnother(toolName, identity) {
19654
+ isToolNameOwnedByAnother(toolName, identity2) {
18916
19655
  const owner = this.toolOwners.get(toolName);
18917
- return owner !== void 0 && owner !== identity;
19656
+ return owner !== void 0 && owner !== identity2;
18918
19657
  }
18919
19658
  handleUnexpectedClose(name) {
18920
19659
  const connection = this.connections.get(name);
@@ -19008,9 +19747,9 @@ async function closeTransportQuietly(transport) {
19008
19747
  }
19009
19748
 
19010
19749
  // src/memory/tools.ts
19011
- import { z as z19 } from "zod";
19012
- var scopeSchema = z19.enum(["user", "workspace", "session", "agent"]);
19013
- var kindSchema = z19.enum(["semantic", "episodic", "procedural"]);
19750
+ import { z as z20 } from "zod";
19751
+ var scopeSchema = z20.enum(["user", "workspace", "session", "agent"]);
19752
+ var kindSchema = z20.enum(["semantic", "episodic", "procedural"]);
19014
19753
  function createMemoryTools(store) {
19015
19754
  return [
19016
19755
  {
@@ -19025,10 +19764,10 @@ function createMemoryTools(store) {
19025
19764
  }, ["query"])
19026
19765
  },
19027
19766
  async execute(arguments_, context) {
19028
- const input2 = z19.object({
19029
- query: z19.string().max(4e3),
19767
+ const input2 = z20.object({
19768
+ query: z20.string().max(4e3),
19030
19769
  scope: scopeSchema.optional(),
19031
- limit: z19.number().int().min(1).max(20).optional()
19770
+ limit: z20.number().int().min(1).max(20).optional()
19032
19771
  }).parse(arguments_);
19033
19772
  const scopes = input2.scope ? [{ scope: input2.scope, scopeKey: scopeKey(input2.scope, context) }] : availableScopes(context);
19034
19773
  const records = store.search(input2.query, { scopes, limit: input2.limit ?? 8 });
@@ -19060,17 +19799,17 @@ ${record.content}`
19060
19799
  }, ["content", "rationale"])
19061
19800
  },
19062
19801
  async execute(arguments_, context) {
19063
- const input2 = z19.object({
19064
- content: z19.string().min(1).max(12e3),
19065
- rationale: z19.string().min(1).max(1e3),
19802
+ const input2 = z20.object({
19803
+ content: z20.string().min(1).max(12e3),
19804
+ rationale: z20.string().min(1).max(1e3),
19066
19805
  scope: scopeSchema.optional(),
19067
19806
  kind: kindSchema.optional(),
19068
- tags: z19.array(z19.string()).max(24).optional(),
19069
- importance: z19.number().min(0).max(1).optional(),
19070
- confidence: z19.number().min(0).max(1).optional(),
19071
- agent: z19.string().max(64).optional(),
19072
- revision: z19.string().max(240).optional(),
19073
- conflictKey: z19.string().max(240).optional()
19807
+ tags: z20.array(z20.string()).max(24).optional(),
19808
+ importance: z20.number().min(0).max(1).optional(),
19809
+ confidence: z20.number().min(0).max(1).optional(),
19810
+ agent: z20.string().max(64).optional(),
19811
+ revision: z20.string().max(240).optional(),
19812
+ conflictKey: z20.string().max(240).optional()
19074
19813
  }).strict().parse(arguments_);
19075
19814
  const scope = input2.scope ?? "workspace";
19076
19815
  const candidate = store.propose({
@@ -19108,9 +19847,9 @@ ${record.content}`
19108
19847
  }, ["id"])
19109
19848
  },
19110
19849
  async execute(arguments_) {
19111
- const input2 = z19.object({
19112
- id: z19.string().uuid(),
19113
- permanent: z19.boolean().optional()
19850
+ const input2 = z20.object({
19851
+ id: z20.string().uuid(),
19852
+ permanent: z20.boolean().optional()
19114
19853
  }).parse(arguments_);
19115
19854
  const changed = input2.permanent ? store.remove(input2.id) : store.archive(input2.id);
19116
19855
  return {
@@ -19137,7 +19876,7 @@ function scopeKey(scope, context) {
19137
19876
  }
19138
19877
 
19139
19878
  // src/skills/catalog.ts
19140
- import { lstat as lstat21, readFile as readFile19, readdir as readdir9, realpath as realpath9 } from "node:fs/promises";
19879
+ import { lstat as lstat21, readFile as readFile20, readdir as readdir9, realpath as realpath9 } from "node:fs/promises";
19141
19880
  import { homedir as homedir4 } from "node:os";
19142
19881
  import { basename as basename12, join as join21, resolve as resolve21 } from "node:path";
19143
19882
  import { parse as parseYaml3 } from "yaml";
@@ -19269,7 +20008,7 @@ async function safeRead(path, maxBytes) {
19269
20008
  const resolvedParent = await realpath9(parent);
19270
20009
  const resolvedPath = await realpath9(path);
19271
20010
  if (!resolvedPath.startsWith(`${resolvedParent}/`)) return void 0;
19272
- return await readFile19(path, "utf8");
20011
+ return await readFile20(path, "utf8");
19273
20012
  } catch {
19274
20013
  return void 0;
19275
20014
  }
@@ -19314,7 +20053,7 @@ function escapeAttribute6(value) {
19314
20053
  }
19315
20054
 
19316
20055
  // src/workflows/catalog.ts
19317
- import { z as z20 } from "zod";
20056
+ import { z as z21 } from "zod";
19318
20057
  var builtInWorkflows = [
19319
20058
  {
19320
20059
  name: "implement",
@@ -19405,7 +20144,7 @@ function createWorkflowTool(catalog) {
19405
20144
  }, ["name", "task"])
19406
20145
  },
19407
20146
  async execute(arguments_) {
19408
- const input2 = z20.object({ name: z20.string(), task: z20.string().min(1).max(2e4) }).parse(arguments_);
20147
+ const input2 = z21.object({ name: z21.string(), task: z21.string().min(1).max(2e4) }).parse(arguments_);
19409
20148
  const workflow = catalog.get(input2.name);
19410
20149
  if (!workflow) return { ok: false, content: `Unknown workflow: ${input2.name}` };
19411
20150
  return {