@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 +5 -3
- package/dist/cli.js +79 -21
- package/dist/cli.js.map +1 -1
- package/docs/ARCHITECTURE.md +5 -3
- package/docs/NEXT_STEPS.md +32 -12
- package/package.json +9 -1
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
|
|
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.
|
|
51
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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 <
|
|
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:
|
|
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 <
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|