@skein-code/cli 0.3.16 → 0.3.17
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 +48 -16
- package/dist/cli.js.map +1 -1
- package/docs/ARCHITECTURE.md +5 -3
- package/docs/NEXT_STEPS.md +11 -9
- package/package.json +5 -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.17",
|
|
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,9 @@ var package_default = {
|
|
|
236
236
|
},
|
|
237
237
|
skein: {
|
|
238
238
|
releaseNotes: [
|
|
239
|
+
"Calibrated Type-1/2 duplication matches now block completion until reuse or exact audited suppression",
|
|
240
|
+
"Type-3 duplication remains warning-only and Type-4 semantic equivalence remains explicitly unsupported",
|
|
241
|
+
"Duplication benchmark fixtures report deterministic recall, precision, false-positive rate, and latency",
|
|
239
242
|
"Duplication warnings now aggregate into the single completion receipt without changing verified status",
|
|
240
243
|
"Exact match ids support bounded, reason-coded suppression with audit and credential/code-block safeguards",
|
|
241
244
|
"Duplication audit tools are disclosed only when active warnings exist and repaired paths clear stale matches",
|
|
@@ -274,6 +277,7 @@ var package_default = {
|
|
|
274
277
|
"pretest:pty": "npm run build",
|
|
275
278
|
"test:pty": "sh test/pty/run-visual.sh",
|
|
276
279
|
"benchmark:context": "tsx scripts/benchmark-local-index.ts",
|
|
280
|
+
"benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
|
|
277
281
|
"verify:package": "node scripts/verify-package.mjs",
|
|
278
282
|
"release:verify": "npm run check && npm run verify:package --",
|
|
279
283
|
typecheck: "tsc --noEmit",
|
|
@@ -5674,7 +5678,7 @@ var reuseReceiptSchema = z5.object({
|
|
|
5674
5678
|
indexGeneration: z5.string().optional(),
|
|
5675
5679
|
changeSequence: z5.number().int().nonnegative(),
|
|
5676
5680
|
status: z5.enum(["warning", "skipped", "unresolved"]),
|
|
5677
|
-
warningOnly: z5.
|
|
5681
|
+
warningOnly: z5.boolean()
|
|
5678
5682
|
}).strict();
|
|
5679
5683
|
var duplicationMatchSchema = z5.object({
|
|
5680
5684
|
matchId: z5.string().regex(/^[a-f0-9]{24}$/u).optional(),
|
|
@@ -5689,7 +5693,8 @@ var duplicationAuditSchema = z5.object({
|
|
|
5689
5693
|
baselineGeneration: z5.string().min(1),
|
|
5690
5694
|
changeSequence: z5.number().int().nonnegative(),
|
|
5691
5695
|
status: z5.enum(["clear", "warning", "unresolved"]),
|
|
5692
|
-
warningOnly: z5.
|
|
5696
|
+
warningOnly: z5.boolean(),
|
|
5697
|
+
enforcement: z5.enum(["warning", "blocking"]).optional(),
|
|
5693
5698
|
checkedFunctions: z5.number().int().nonnegative(),
|
|
5694
5699
|
skippedSmallFunctions: z5.number().int().nonnegative(),
|
|
5695
5700
|
matches: z5.array(duplicationMatchSchema).max(8),
|
|
@@ -5759,7 +5764,7 @@ var lastRunSchema = z5.object({
|
|
|
5759
5764
|
detail: z5.string(),
|
|
5760
5765
|
mutationTracking: z5.enum(["complete", "unknown"]).optional(),
|
|
5761
5766
|
duplication: z5.object({
|
|
5762
|
-
enforcement: z5.
|
|
5767
|
+
enforcement: z5.enum(["warning", "blocking"]),
|
|
5763
5768
|
status: z5.enum(["clear", "warning", "unresolved", "suppressed"]),
|
|
5764
5769
|
warningCount: z5.number().int().nonnegative(),
|
|
5765
5770
|
unresolvedCount: z5.number().int().nonnegative(),
|
|
@@ -8397,6 +8402,16 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
|
|
|
8397
8402
|
...duplication ? { duplication } : {}
|
|
8398
8403
|
};
|
|
8399
8404
|
}
|
|
8405
|
+
if (duplication?.enforcement === "blocking" && duplication.warningCount > 0) {
|
|
8406
|
+
return {
|
|
8407
|
+
status: "unverified",
|
|
8408
|
+
changedFiles: files,
|
|
8409
|
+
checks: [],
|
|
8410
|
+
detail: "A high-confidence duplicate implementation requires reuse, extension, or an exact audited suppression before completion.",
|
|
8411
|
+
...acceptance ? { acceptance: acceptanceForCompletion(acceptance, "unverified") } : {},
|
|
8412
|
+
duplication
|
|
8413
|
+
};
|
|
8414
|
+
}
|
|
8400
8415
|
const latestByCommand = /* @__PURE__ */ new Map();
|
|
8401
8416
|
for (const item of evidence) {
|
|
8402
8417
|
if (item.changeSequence === currentChangeSequence) {
|
|
@@ -8445,6 +8460,13 @@ function buildRunCompletion(changedFiles, evidence, currentChangeSequence, mutat
|
|
|
8445
8460
|
};
|
|
8446
8461
|
}
|
|
8447
8462
|
function completionRecoveryDirective(completion) {
|
|
8463
|
+
if (completion.duplication?.enforcement === "blocking" && completion.duplication.warningCount > 0) {
|
|
8464
|
+
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");
|
|
8465
|
+
return `<runtime-completion-gate status="duplication_blocking" authorization="none">
|
|
8466
|
+
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.
|
|
8467
|
+
${matches || "- Active duplicate match details are unavailable."}
|
|
8468
|
+
</runtime-completion-gate>`;
|
|
8469
|
+
}
|
|
8448
8470
|
if (completion.acceptance && acceptanceUnresolved(completion.acceptance)) {
|
|
8449
8471
|
const unresolved2 = completion.acceptance.unresolved.map((item) => `- [${item.status}] ${item.id}: ${item.description}`).join("\n");
|
|
8450
8472
|
const failed = completion.checks.filter((check) => !check.ok).map((check) => `- ${check.command} (tool call ${check.toolCallId})`).join("\n");
|
|
@@ -8885,8 +8907,9 @@ function buildDuplicationCompletion(audit, suppressions = [], runStartedAt) {
|
|
|
8885
8907
|
const suppressedIds = new Set(suppressions.filter((item) => activeIds.has(item.matchId)).map((item) => item.matchId));
|
|
8886
8908
|
const warnings = [...active.values()].filter((match) => !suppressedIds.has(match.matchId));
|
|
8887
8909
|
const suppressedCount = active.size - warnings.length;
|
|
8910
|
+
const enforcement = warnings.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
|
|
8888
8911
|
return {
|
|
8889
|
-
enforcement
|
|
8912
|
+
enforcement,
|
|
8890
8913
|
status: unresolved2.size ? "unresolved" : warnings.length ? "warning" : suppressedCount ? "suppressed" : "clear",
|
|
8891
8914
|
warningCount: warnings.length,
|
|
8892
8915
|
unresolvedCount: unresolved2.size,
|
|
@@ -9807,9 +9830,13 @@ function roundScore(value) {
|
|
|
9807
9830
|
// src/agent/duplication-audit.ts
|
|
9808
9831
|
import { createHash as createHash14 } from "node:crypto";
|
|
9809
9832
|
import { readFile as readFile15 } from "node:fs/promises";
|
|
9810
|
-
var
|
|
9833
|
+
var DEFAULT_NEAR_CLONE_THRESHOLD = 0.55;
|
|
9811
9834
|
var MAX_MATCHES = 8;
|
|
9812
9835
|
async function auditChangedFunctions(input2) {
|
|
9836
|
+
const nearCloneThreshold = input2.nearCloneThreshold ?? DEFAULT_NEAR_CLONE_THRESHOLD;
|
|
9837
|
+
if (!Number.isFinite(nearCloneThreshold) || nearCloneThreshold < 0 || nearCloneThreshold > 1) {
|
|
9838
|
+
throw new Error("nearCloneThreshold must be a finite number between 0 and 1.");
|
|
9839
|
+
}
|
|
9813
9840
|
const auditableFiles = input2.changedFiles.filter(supportsFunctionFingerprintPath);
|
|
9814
9841
|
if (!auditableFiles.length) return void 0;
|
|
9815
9842
|
const reports = [];
|
|
@@ -9836,7 +9863,7 @@ async function auditChangedFunctions(input2) {
|
|
|
9836
9863
|
}
|
|
9837
9864
|
if (!input2.baseline) return unresolved(input2.changeSequence);
|
|
9838
9865
|
const lookup = createBaselineLookup(input2.baseline.functions);
|
|
9839
|
-
const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions);
|
|
9866
|
+
const previousByCurrent = matchCurrentFunctions(input2.baseline.functions, currentFunctions, nearCloneThreshold);
|
|
9840
9867
|
const added = currentFunctions.flatMap((current) => {
|
|
9841
9868
|
const previous = previousByCurrent.get(locationIdentity(current));
|
|
9842
9869
|
const recheck = input2.recheckFunctions?.has(identity(current)) || previous !== void 0 && input2.recheckFunctions?.has(identity(previous));
|
|
@@ -9849,7 +9876,7 @@ async function auditChangedFunctions(input2) {
|
|
|
9849
9876
|
for (const candidate of findCandidateFunctions(lookup, item)) {
|
|
9850
9877
|
if (previous && locationIdentity(candidate) === locationIdentity(previous)) continue;
|
|
9851
9878
|
const similarity = fingerprintSimilarity(item, candidate);
|
|
9852
|
-
if (similarity <
|
|
9879
|
+
if (similarity < nearCloneThreshold || best && best.similarity >= similarity) continue;
|
|
9853
9880
|
best = { candidate, similarity };
|
|
9854
9881
|
}
|
|
9855
9882
|
if (!best) continue;
|
|
@@ -9865,11 +9892,13 @@ async function auditChangedFunctions(input2) {
|
|
|
9865
9892
|
}
|
|
9866
9893
|
matches.sort((left, right) => right.similarity - left.similarity || left.changedPath.localeCompare(right.changedPath));
|
|
9867
9894
|
const bounded = matches.slice(0, MAX_MATCHES);
|
|
9895
|
+
const enforcement = bounded.some((match) => match.kind === "type-1-or-2") ? "blocking" : "warning";
|
|
9868
9896
|
return {
|
|
9869
9897
|
baselineGeneration: input2.baseline.generation,
|
|
9870
9898
|
changeSequence: input2.changeSequence,
|
|
9871
9899
|
status: bounded.length ? "warning" : "clear",
|
|
9872
|
-
warningOnly:
|
|
9900
|
+
warningOnly: enforcement === "warning",
|
|
9901
|
+
enforcement,
|
|
9873
9902
|
checkedFunctions: added.length,
|
|
9874
9903
|
skippedSmallFunctions,
|
|
9875
9904
|
matches: bounded,
|
|
@@ -9882,6 +9911,7 @@ function clear(baselineGeneration, changeSequence, skippedSmallFunctions) {
|
|
|
9882
9911
|
changeSequence,
|
|
9883
9912
|
status: "clear",
|
|
9884
9913
|
warningOnly: true,
|
|
9914
|
+
enforcement: "warning",
|
|
9885
9915
|
checkedFunctions: 0,
|
|
9886
9916
|
skippedSmallFunctions,
|
|
9887
9917
|
matches: [],
|
|
@@ -9931,15 +9961,15 @@ function findCandidateFunctions(lookup, current) {
|
|
|
9931
9961
|
}
|
|
9932
9962
|
return [...candidates.values()];
|
|
9933
9963
|
}
|
|
9934
|
-
function matchCurrentFunctions(baseline, current) {
|
|
9964
|
+
function matchCurrentFunctions(baseline, current, nearCloneThreshold) {
|
|
9935
9965
|
const matches = /* @__PURE__ */ new Map();
|
|
9936
9966
|
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);
|
|
9967
|
+
assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item) && linesOverlap(candidate, item)), false, nearCloneThreshold);
|
|
9968
|
+
assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => identity(candidate) === identity(item)), true, nearCloneThreshold);
|
|
9969
|
+
assignMatches(current, matches, available, (item) => [...available.values()].filter((candidate) => candidate.path === item.path && linesOverlap(candidate, item)), true, nearCloneThreshold);
|
|
9940
9970
|
return matches;
|
|
9941
9971
|
}
|
|
9942
|
-
function assignMatches(current, matches, available, candidatesFor, requireSimilarity) {
|
|
9972
|
+
function assignMatches(current, matches, available, candidatesFor, requireSimilarity, nearCloneThreshold) {
|
|
9943
9973
|
for (const item of current) {
|
|
9944
9974
|
const currentId = locationIdentity(item);
|
|
9945
9975
|
if (matches.has(currentId)) continue;
|
|
@@ -9948,7 +9978,7 @@ function assignMatches(current, matches, available, candidatesFor, requireSimila
|
|
|
9948
9978
|
const similarity = fingerprintSimilarity(candidate, item);
|
|
9949
9979
|
if (!best || similarity > best.similarity) best = { candidate, similarity };
|
|
9950
9980
|
}
|
|
9951
|
-
if (!best || requireSimilarity && best.similarity <
|
|
9981
|
+
if (!best || requireSimilarity && best.similarity < nearCloneThreshold) continue;
|
|
9952
9982
|
matches.set(currentId, best.candidate);
|
|
9953
9983
|
available.delete(locationIdentity(best.candidate));
|
|
9954
9984
|
}
|
|
@@ -9962,6 +9992,7 @@ function unresolved(changeSequence, generation = "unavailable") {
|
|
|
9962
9992
|
changeSequence,
|
|
9963
9993
|
status: "unresolved",
|
|
9964
9994
|
warningOnly: true,
|
|
9995
|
+
enforcement: "warning",
|
|
9965
9996
|
checkedFunctions: 0,
|
|
9966
9997
|
skippedSmallFunctions: 0,
|
|
9967
9998
|
matches: [],
|
|
@@ -10588,7 +10619,8 @@ Tool succeeded, but afterTool hook failed: ${afterHookError.message}` : executio
|
|
|
10588
10619
|
|
|
10589
10620
|
${completeContent}`;
|
|
10590
10621
|
if (duplicationAudit?.status === "warning" || duplicationAudit?.status === "unresolved") {
|
|
10591
|
-
|
|
10622
|
+
const enforcement = duplicationAudit.enforcement === "blocking" ? "completion-blocking Type-1/2" : "warning-only";
|
|
10623
|
+
completeContent = `Duplication audit (${enforcement}): ${duplicationAudit.rationale}
|
|
10592
10624
|
|
|
10593
10625
|
${completeContent}`;
|
|
10594
10626
|
}
|