@skein-code/cli 0.3.16 → 0.3.18

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/README.md CHANGED
@@ -45,10 +45,12 @@ inspectable index for code retrieval and no retrieval service dependency.
45
45
  enforcement, changed-file telemetry, and persisted tool results.
46
46
  - **Repository reuse guard:** the first substantive implementation addition gets
47
47
  a warning-only, content-free receipt of current helper candidates and read
48
- evidence. After a successful TS/JS write, Skein also compares newly added or
48
+ evidence. After a successful TS/JS write, Skein compares newly added or
49
49
  significantly expanded functions with the pre-write index generation using
50
- normalized fingerprints. Docs, config, fixtures, generated files, deletions,
51
- small functions, and ordinary local edits stay quiet.
50
+ normalized fingerprints. Calibrated Type-1/2 matches block completion until
51
+ reuse, removal, or exact audited suppression; Type-3 remains warning-only.
52
+ Docs, config, fixtures, generated files, deletions, small functions, and
53
+ ordinary local edits stay quiet.
52
54
  - **Bounded tool context:** large tool results cannot crowd the task out of the
53
55
  model window. Skein keeps a token-budgeted head/tail receipt and, when the
54
56
  producer captured the complete result, retains a redacted session-scoped
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.16",
223
+ version: "0.3.18",
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
+ "Large MCP catalogs now disclose at most eight request-relevant schemas per run while preserving network permissions",
240
+ "Token Ledger receipts report deferred progressive schemas and selected definitions remain stable across tool turns",
241
+ "A deterministic Token Economy replay benchmark guards evidence, output-firewall, and no-progress invariants",
242
+ "Calibrated Type-1/2 duplication matches now block completion until reuse or exact audited suppression",
243
+ "Type-3 duplication remains warning-only and Type-4 semantic equivalence remains explicitly unsupported",
244
+ "Duplication benchmark fixtures report deterministic recall, precision, false-positive rate, and latency",
239
245
  "Duplication warnings now aggregate into the single completion receipt without changing verified status",
240
246
  "Exact match ids support bounded, reason-coded suppression with audit and credential/code-block safeguards",
241
247
  "Duplication audit tools are disclosed only when active warnings exist and repaired paths clear stale matches",
@@ -274,6 +280,8 @@ var package_default = {
274
280
  "pretest:pty": "npm run build",
275
281
  "test:pty": "sh test/pty/run-visual.sh",
276
282
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
283
+ "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
284
+ "benchmark:token-economy": "tsx scripts/benchmark-token-economy.ts",
277
285
  "verify:package": "node scripts/verify-package.mjs",
278
286
  "release:verify": "npm run check && npm run verify:package --",
279
287
  typecheck: "tsc --noEmit",
@@ -5674,7 +5682,7 @@ var reuseReceiptSchema = z5.object({
5674
5682
  indexGeneration: z5.string().optional(),
5675
5683
  changeSequence: z5.number().int().nonnegative(),
5676
5684
  status: z5.enum(["warning", "skipped", "unresolved"]),
5677
- warningOnly: z5.literal(true)
5685
+ warningOnly: z5.boolean()
5678
5686
  }).strict();
5679
5687
  var duplicationMatchSchema = z5.object({
5680
5688
  matchId: z5.string().regex(/^[a-f0-9]{24}$/u).optional(),
@@ -5689,7 +5697,8 @@ var duplicationAuditSchema = z5.object({
5689
5697
  baselineGeneration: z5.string().min(1),
5690
5698
  changeSequence: z5.number().int().nonnegative(),
5691
5699
  status: z5.enum(["clear", "warning", "unresolved"]),
5692
- warningOnly: z5.literal(true),
5700
+ warningOnly: z5.boolean(),
5701
+ enforcement: z5.enum(["warning", "blocking"]).optional(),
5693
5702
  checkedFunctions: z5.number().int().nonnegative(),
5694
5703
  skippedSmallFunctions: z5.number().int().nonnegative(),
5695
5704
  matches: z5.array(duplicationMatchSchema).max(8),
@@ -5759,7 +5768,7 @@ var lastRunSchema = z5.object({
5759
5768
  detail: z5.string(),
5760
5769
  mutationTracking: z5.enum(["complete", "unknown"]).optional(),
5761
5770
  duplication: z5.object({
5762
- enforcement: z5.literal("warning"),
5771
+ enforcement: z5.enum(["warning", "blocking"]),
5763
5772
  status: z5.enum(["clear", "warning", "unresolved", "suppressed"]),
5764
5773
  warningCount: z5.number().int().nonnegative(),
5765
5774
  unresolvedCount: z5.number().int().nonnegative(),
@@ -8397,6 +8406,16 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8397
8406
  ...duplication ? { duplication } : {}
8398
8407
  };
8399
8408
  }
8409
+ if (duplication?.enforcement === "blocking" && duplication.warningCount > 0) {
8410
+ return {
8411
+ status: "unverified",
8412
+ changedFiles: files,
8413
+ checks: [],
8414
+ detail: "A high-confidence duplicate implementation requires reuse, extension, or an exact audited suppression before completion.",
8415
+ ...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {},
8416
+ duplication
8417
+ };
8418
+ }
8400
8419
  const latestByCommand = /* @__PURE__ */ new Map();
8401
8420
  for (const item of evidence) {
8402
8421
  if (item.changeSequence === currentChangeSequence) {
@@ -8445,6 +8464,13 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
8445
8464
  };
8446
8465
  }
8447
8466
  function completionRecoveryDirective(completion) {
8467
+ if (completion.duplication?.enforcement === "blocking" && completion.duplication.warningCount > 0) {
8468
+ const matches = completion.duplication.matches.filter((match) => match.kind === "type-1-or-2").map((match) => `- ${match.matchId}: ${match.changedPath} ${match.changedSymbol} duplicates ${match.candidatePath} ${match.candidateSymbol}`).join("\n");
8469
+ return `<runtime-completion-gate status="duplication_blocking" authorization="none">
8470
+ High-confidence Type-1/2 duplication was detected. Reuse or extend the existing implementation, remove the duplicate, or call the read-only duplication_audit tool to suppress an exact match with an audited reason. Suppression never waives verification, safety, accessibility, error handling, concurrency, or correctness requirements.
8471
+ ${matches || "- Active duplicate match details are unavailable."}
8472
+ </runtime-completion-gate>`;
8473
+ }
8448
8474
  if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
8449
8475
  const unresolved2 = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
8450
8476
  const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
@@ -8885,8 +8911,9 @@ function buildDuplicationCompletion(audit, suppressions = [], runStartedAt) {
8885
8911
  const suppressedIds = new Set(suppressions.filter((item) => activeIds.has(item.matchId)).map((item) => item.matchId));
8886
8912
  const warnings = [...active.values()].filter((match) => !suppressedIds.has(match.matchId));
8887
8913
  const suppressedCount = active.size - warnings.length;
8914
+ const enforcement = warnings.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
8888
8915
  return {
8889
- enforcement: "warning",
8916
+ enforcement,
8890
8917
  status: unresolved2.size ? "unresolved" : warnings.length ? "warning" : suppressedCount ? "suppressed" : "clear",
8891
8918
  warningCount: warnings.length,
8892
8919
  unresolvedCount: unresolved2.size,
@@ -9807,9 +9834,13 @@ function roundScore(value) {
9807
9834
  // src/agent/duplication-audit.ts
9808
9835
  import { createHash as createHash14 } from "node:crypto";
9809
9836
  import { readFile as readFile15 } from "node:fs/promises";
9810
- var NEAR_CLONE_THRESHOLD = 0.55;
9837
+ var DEFAULT_NEAR_CLONE_THRESHOLD = 0.55;
9811
9838
  var MAX_MATCHES = 8;
9812
9839
  async function auditChangedFunctions(input2) {
9840
+ const nearCloneThreshold = input2.nearCloneThreshold ?? DEFAULT_NEAR_CLONE_THRESHOLD;
9841
+ if (!Number.isFinite(nearCloneThreshold) || nearCloneThreshold < 0 || nearCloneThreshold > 1) {
9842
+ throw new Error("nearCloneThreshold must be a finite number between 0 and 1.");
9843
+ }
9813
9844
  const auditableFiles = input2.changedFiles.filter(supportsFunctionFingerprintPath);
9814
9845
  if (!auditableFiles.length) return void 0;
9815
9846
  const reports = [];
@@ -9836,7 +9867,7 @@ async function auditChangedFunctions(input2) {
9836
9867
  }
9837
9868
  if (!input2.baseline) return unresolved(input2.changeSequence);
9838
9869
  const lookup = createBaselineLookup(input2.baseline.functions);
9839
- const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions);
9870
+ const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions, nearCloneThreshold);
9840
9871
  const added = currentFunctions.flatMap((current) => {
9841
9872
  const previous = previousByCurrent.get(locationIdentity(current));
9842
9873
  const recheck = input2.recheckFunctions?.has(identity(current)) || previous !== void 0 && input2.recheckFunctions?.has(identity(previous));
@@ -9849,7 +9880,7 @@ async function auditChangedFunctions(input2) {
9849
9880
  for (const candidate of findCandidateFunctions(lookup, item)) {
9850
9881
  if (previous && locationIdentity(candidate) === locationIdentity(previous)) continue;
9851
9882
  const similarity = fingerprintSimilarity(item, candidate);
9852
- if (similarity < NEAR_CLONE_THRESHOLD || best && best.similarity >= similarity) continue;
9883
+ if (similarity < nearCloneThreshold || best && best.similarity >= similarity) continue;
9853
9884
  best = { candidate, similarity };
9854
9885
  }
9855
9886
  if (!best) continue;
@@ -9865,11 +9896,13 @@ async function auditChangedFunctions(input2) {
9865
9896
  }
9866
9897
  matches.sort((left, right) => right.similarity - left.similarity || left.changedPath.localeCompare(right.changedPath));
9867
9898
  const bounded = matches.slice(0, MAX_MATCHES);
9899
+ const enforcement = bounded.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
9868
9900
  return {
9869
9901
  baselineGeneration: input2.baseline.generation,
9870
9902
  changeSequence: input2.changeSequence,
9871
9903
  status: bounded.length ? "warning" : "clear",
9872
- warningOnly: true,
9904
+ warningOnly: enforcement === "warning",
9905
+ enforcement,
9873
9906
  checkedFunctions: added.length,
9874
9907
  skippedSmallFunctions,
9875
9908
  matches: bounded,
@@ -9882,6 +9915,7 @@ function clear(baselineGeneration, changeSequence, skippedSmallFunctions) {
9882
9915
  changeSequence,
9883
9916
  status: "clear",
9884
9917
  warningOnly: true,
9918
+ enforcement: "warning",
9885
9919
  checkedFunctions: 0,
9886
9920
  skippedSmallFunctions,
9887
9921
  matches: [],
@@ -9931,15 +9965,15 @@ function findCandidateFunctions(lookup, current) {
9931
9965
  }
9932
9966
  return [...candidates.values()];
9933
9967
  }
9934
- function matchCurrentFunctions(baseline, current) {
9968
+ function matchCurrentFunctions(baseline, current, nearCloneThreshold) {
9935
9969
  const matches = /* @__PURE__ */ new Map();
9936
9970
  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);
9971
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item) && linesOverlap(candidate, item)), false, nearCloneThreshold);
9972
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item)), true, nearCloneThreshold);
9973
+ assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => candidate.path === item.path && linesOverlap(candidate, item)), true, nearCloneThreshold);
9940
9974
  return matches;
9941
9975
  }
9942
- function assignMatches(current, matches, available, candidatesFor, requireSimilarity) {
9976
+ function assignMatches(current, matches, available, candidatesFor, requireSimilarity, nearCloneThreshold) {
9943
9977
  for (const item of current) {
9944
9978
  const currentId = locationIdentity(item);
9945
9979
  if (matches.has(currentId)) continue;
@@ -9948,7 +9982,7 @@ function assignMatches(current, matches, available, candidatesFor, requireSimila
9948
9982
  const similarity = fingerprintSimilarity(candidate, item);
9949
9983
  if (!best || similarity > best.similarity) best = { candidate, similarity };
9950
9984
  }
9951
- if (!best || requireSimilarity && best.similarity < NEAR_CLONE_THRESHOLD) continue;
9985
+ if (!best || requireSimilarity && best.similarity < nearCloneThreshold) continue;
9952
9986
  matches.set(currentId, best.candidate);
9953
9987
  available.delete(locationIdentity(best.candidate));
9954
9988
  }
@@ -9962,6 +9996,7 @@ function unresolved(changeSequence, generation = "unavailable") {
9962
9996
  changeSequence,
9963
9997
  status: "unresolved",
9964
9998
  warningOnly: true,
9999
+ enforcement: "warning",
9965
10000
  checkedFunctions: 0,
9966
10001
  skippedSmallFunctions: 0,
9967
10002
  matches: [],
@@ -10054,6 +10089,7 @@ var AgentRunner = class {
10054
10089
  };
10055
10090
  const changeSequenceAtStart = this.changeSequence;
10056
10091
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
10092
+ const loadedProgressiveTools = /* @__PURE__ */ new Set();
10057
10093
  const runChangedFiles = /* @__PURE__ */ new Set();
10058
10094
  const verificationEvidence = [];
10059
10095
  const toolRecovery = new ToolRecoveryController();
@@ -10217,7 +10253,9 @@ var AgentRunner = class {
10217
10253
  activeDuplicationMatchIds(
10218
10254
  this.session.audit ?? [],
10219
10255
  this.session.duplicationSuppressions ?? []
10220
- ).size > 0
10256
+ ).size > 0,
10257
+ request,
10258
+ loadedProgressiveTools
10221
10259
  );
10222
10260
  const estimatedInputTokens = estimateMessages2(messages) + estimateToolDefinitions(visibleTools);
10223
10261
  if (availableTokens <= 0 || estimatedInputTokens >= availableTokens) {
@@ -10282,7 +10320,10 @@ var AgentRunner = class {
10282
10320
  },
10283
10321
  inputSource: actualInputTokens === void 0 ? "estimated" : "actual",
10284
10322
  outputSource: actualOutputTokens === void 0 ? "estimated" : "actual",
10285
- tools: { loaded: visibleTools.map((tool) => tool.name), deferredCount: 0 },
10323
+ tools: {
10324
+ loaded: visibleTools.map((tool) => tool.name),
10325
+ deferredCount: Math.max(0, this.tools.definitions().filter((tool) => tool.progressive).length - visibleTools.filter((tool) => tool.progressive).length)
10326
+ },
10286
10327
  retrieval: tokenRetrievalReceipt(packed)
10287
10328
  });
10288
10329
  await emit({
@@ -10588,7 +10629,8 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
10588
10629
 
10589
10630
  ${completeContent}`;
10590
10631
  if (duplicationAudit?.status === "warning" || duplicationAudit?.status === "unresolved") {
10591
- completeContent = `Duplication audit (warning-only): ${duplicationAudit.rationale}
10632
+ const enforcement = duplicationAudit.enforcement === "blocking" ? "completion-blocking Type-1/2" : "warning-only";
10633
+ completeContent = `Duplication audit (${enforcement}): ${duplicationAudit.rationale}
10592
10634
 
10593
10635
  ${completeContent}`;
10594
10636
  }
@@ -11047,10 +11089,25 @@ ${content}` : content,
11047
11089
  ...failure ? { metadata: { failure } } : {}
11048
11090
  };
11049
11091
  }
11050
- function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable) {
11051
- return tools.definitions().filter(
11092
+ function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable, request, loadedProgressiveTools) {
11093
+ const eligible = tools.definitions().filter(
11052
11094
  (tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
11053
11095
  );
11096
+ const progressive = eligible.filter((tool) => tool.progressive);
11097
+ if (progressive.length <= 8) return eligible;
11098
+ for (const tool of selectProgressiveTools(progressive, request, 8)) {
11099
+ loadedProgressiveTools.add(tool.name);
11100
+ }
11101
+ return eligible.filter((tool) => !tool.progressive || loadedProgressiveTools.has(tool.name));
11102
+ }
11103
+ function selectProgressiveTools(tools, request, limit) {
11104
+ const terms = new Set(request.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
11105
+ return tools.map((tool) => {
11106
+ const searchable = `${tool.name.replaceAll("_", " ")} ${tool.description}`.toLocaleLowerCase();
11107
+ let score = 0;
11108
+ for (const term of terms) if (searchable.includes(term)) score += term.length;
11109
+ return { tool, score };
11110
+ }).sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name)).slice(0, limit).map(({ tool }) => tool);
11054
11111
  }
11055
11112
  function formatToolError(error) {
11056
11113
  const normalized = toError(error);
@@ -18963,7 +19020,8 @@ function createMcpToolAdapter(options) {
18963
19020
  // MCP servers are an external trust boundary. Read-only annotations are
18964
19021
  // hints from that server and must not lower the local permission level.
18965
19022
  category: "network",
18966
- inputSchema: inputSchema13
19023
+ inputSchema: inputSchema13,
19024
+ progressive: true
18967
19025
  },
18968
19026
  permissionCategories: () => ["network"],
18969
19027
  async execute(arguments_, context) {