skalpel 4.0.11 → 4.0.13
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/autopsy.mjs +239 -23
- package/bootstrap.mjs +3 -1
- package/package.json +1 -1
- package/reveal.mjs +22 -6
- package/skalpel-setup.mjs +12 -2
package/autopsy.mjs
CHANGED
|
@@ -192,8 +192,14 @@ function extractAnchors(text) {
|
|
|
192
192
|
const fileRe =
|
|
193
193
|
/\b[\w-]+(?:\/[\w.-]+)*\.(?:rs|ts|tsx|jsx|mjs|cjs|go|py|sql|toml|yaml|yml|sh|astro|vue|svelte)\b/g;
|
|
194
194
|
while ((m = fileRe.exec(text))) {
|
|
195
|
-
const
|
|
196
|
-
|
|
195
|
+
const raw = m[0]; // the mentioned path token, e.g. "src/components/Hero.astro" or "Users/ryan/…/Hero.astro"
|
|
196
|
+
const base = raw.split("/").pop();
|
|
197
|
+
if (!GENERIC.has(base.split(".")[0])) {
|
|
198
|
+
// fileRe cannot start on the leading "/", so an absolute path shows up as raw="Users/…" with a
|
|
199
|
+
// "/" immediately before it. Detect that so the path can be resolved to a real file IDENTITY.
|
|
200
|
+
const abs = m.index > 0 && text[m.index - 1] === "/";
|
|
201
|
+
out.push({ kind: "sym:file", anchor: base, at: m.index, rawPath: raw, abs });
|
|
202
|
+
}
|
|
197
203
|
}
|
|
198
204
|
const rustRe = /\b[a-z_][a-z0-9_]{2,}::[a-z_][a-z0-9_:]{2,}/g;
|
|
199
205
|
while ((m = rustRe.exec(text))) push("sym:path", m[0], m.index);
|
|
@@ -631,7 +637,10 @@ function reproduceCount(anchor, witnesses) {
|
|
|
631
637
|
const total = witnesses.length;
|
|
632
638
|
for (const w of witnesses) {
|
|
633
639
|
const txt = reopenTurnText(w);
|
|
634
|
-
|
|
640
|
+
// For files, w.reAnchor is the FULL mentioned path (e.g. "src/components/Hero.astro" or the
|
|
641
|
+
// absolute) — re-confirming it, not the bare basename, is what proves the same actual file.
|
|
642
|
+
const need = w.reAnchor || anchor;
|
|
643
|
+
if (txt && norm(txt).includes(norm(need))) confirmed++;
|
|
635
644
|
}
|
|
636
645
|
return { confirmed, total };
|
|
637
646
|
}
|
|
@@ -683,6 +692,23 @@ function projTail(root) {
|
|
|
683
692
|
const tail = parts.slice(-2).join("/");
|
|
684
693
|
return tail ? `~/${tail}` : null;
|
|
685
694
|
}
|
|
695
|
+
// Resolve a mentioned file token to a stable, absolute IDENTITY so cross-vendor "same file" means the
|
|
696
|
+
// SAME ACTUAL FILE — not a shared basename. Returns a normalized absolute path, or null when the
|
|
697
|
+
// mention cannot be pinned to a real file:
|
|
698
|
+
// • absolute path in the text → that path IS the identity (works across every vendor).
|
|
699
|
+
// • relative path WITH a directory → resolved against the session's project root (cwd), but ONLY
|
|
700
|
+
// when that root is a real project (never the home catch-all,
|
|
701
|
+
// and never Cursor's rootless bubbles) — otherwise unknowable.
|
|
702
|
+
// • bare basename (no directory) → null. A basename alone can never prove same-file.
|
|
703
|
+
function resolveFileIdentity(rawPath, abs, root) {
|
|
704
|
+
if (!rawPath) return null;
|
|
705
|
+
if (abs) return path.posix.normalize("/" + rawPath.replace(/^\/+/, ""));
|
|
706
|
+
if (!rawPath.includes("/")) return null; // bare basename — not a same-file proof
|
|
707
|
+
if (/^\.\.?\//.test(rawPath)) return null; // "./x" or "../x": ambiguous relative to an unknown subdir
|
|
708
|
+
const base = decodeRoot(root); // "" when root is null (Cursor) or unknown
|
|
709
|
+
if (!base || base === HOME) return null; // no real project root to anchor a relative path against
|
|
710
|
+
return path.posix.normalize(base.replace(/\/+$/, "") + "/" + rawPath);
|
|
711
|
+
}
|
|
686
712
|
// Detect any email (incl. TLDs truncated by the 200-char span slice, e.g. "name@host.c" or "name@host").
|
|
687
713
|
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9][A-Za-z0-9.-]*/g;
|
|
688
714
|
|
|
@@ -726,13 +752,30 @@ const AUTOPSY_ECHO = [
|
|
|
726
752
|
"cross-vendor",
|
|
727
753
|
"appears in both",
|
|
728
754
|
"connect a 2nd agent",
|
|
755
|
+
"basename collision",
|
|
756
|
+
"basename-only",
|
|
757
|
+
"same actual file",
|
|
758
|
+
"same actual path",
|
|
759
|
+
"same absolute path",
|
|
760
|
+
"same project-root",
|
|
761
|
+
"followed you across",
|
|
762
|
+
"honest-absence",
|
|
763
|
+
"honest absence",
|
|
764
|
+
"single-vendor",
|
|
765
|
+
"cross-vendor autopsy",
|
|
766
|
+
"the same file followed you",
|
|
767
|
+
"distinctive symbol",
|
|
768
|
+
"distinctive-error",
|
|
729
769
|
];
|
|
770
|
+
// Case-insensitive: a receipt/meta-discussion turn can quote the vocabulary in any case (BASENAME,
|
|
771
|
+
// Cross-Vendor, …). Two distinct markers in the first 800 chars ⇒ it is talking ABOUT the autopsy,
|
|
772
|
+
// so it is excluded from anchor mining — the tool must never report on its own machinery.
|
|
730
773
|
const isAutopsyEcho = (s) => {
|
|
731
774
|
if (!s) return false;
|
|
732
|
-
const l = s.slice(0, 800);
|
|
775
|
+
const l = s.slice(0, 800).toLowerCase();
|
|
733
776
|
let hits = 0;
|
|
734
777
|
for (const m of AUTOPSY_ECHO) {
|
|
735
|
-
if (l.includes(m)) {
|
|
778
|
+
if (l.includes(m.toLowerCase())) {
|
|
736
779
|
hits++;
|
|
737
780
|
if (hits >= 2) return true;
|
|
738
781
|
}
|
|
@@ -845,15 +888,112 @@ const CROSS_COMMON_CORES = new Set([
|
|
|
845
888
|
"components",
|
|
846
889
|
"handler",
|
|
847
890
|
]);
|
|
848
|
-
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
|
|
891
|
+
// Common library / runtime / build tokens: identical across everyone's projects, so they are NOT a
|
|
892
|
+
// distinctive "your work followed you" anchor. A symbol must clear this list (and a frequency ceiling)
|
|
893
|
+
// to be a genuine cross-vendor signal. Not exhaustive — the frequency ceiling is the real backstop.
|
|
894
|
+
const SCAFFOLD_SYMBOLS = new Set([
|
|
895
|
+
"addeventlistener",
|
|
896
|
+
"removeeventlistener",
|
|
897
|
+
"getelementbyid",
|
|
898
|
+
"queryselector",
|
|
899
|
+
"queryselectorall",
|
|
900
|
+
"createelement",
|
|
901
|
+
"appendchild",
|
|
902
|
+
"setattribute",
|
|
903
|
+
"getattribute",
|
|
904
|
+
"classlist",
|
|
905
|
+
"innerhtml",
|
|
906
|
+
"textcontent",
|
|
907
|
+
"usestate",
|
|
908
|
+
"useeffect",
|
|
909
|
+
"usecallback",
|
|
910
|
+
"usememo",
|
|
911
|
+
"usecontext",
|
|
912
|
+
"useref",
|
|
913
|
+
"usereducer",
|
|
914
|
+
"foreach",
|
|
915
|
+
"tostring",
|
|
916
|
+
"valueof",
|
|
917
|
+
"constructor",
|
|
918
|
+
"prototype",
|
|
919
|
+
"stringify",
|
|
920
|
+
"parseint",
|
|
921
|
+
"parsefloat",
|
|
922
|
+
"tolowercase",
|
|
923
|
+
"touppercase",
|
|
924
|
+
"trimstart",
|
|
925
|
+
"trimend",
|
|
926
|
+
"startswith",
|
|
927
|
+
"endswith",
|
|
928
|
+
"indexof",
|
|
929
|
+
"lastindexof",
|
|
930
|
+
"includes",
|
|
931
|
+
"createinterface",
|
|
932
|
+
"readfilesync",
|
|
933
|
+
"writefilesync",
|
|
934
|
+
"readdirsync",
|
|
935
|
+
"existssync",
|
|
936
|
+
"createreadstream",
|
|
937
|
+
"spawnsync",
|
|
938
|
+
"execsync",
|
|
939
|
+
"setinterval",
|
|
940
|
+
"setttimeout",
|
|
941
|
+
"settimeout",
|
|
942
|
+
"clearinterval",
|
|
943
|
+
"cleartimeout",
|
|
944
|
+
"unwrap",
|
|
945
|
+
"to_string",
|
|
946
|
+
"into_iter",
|
|
947
|
+
"as_str",
|
|
948
|
+
"as_ref",
|
|
949
|
+
"clone",
|
|
950
|
+
"collect",
|
|
951
|
+
"serialize",
|
|
952
|
+
"deserialize",
|
|
953
|
+
"async_trait",
|
|
954
|
+
"tokio",
|
|
955
|
+
"serde",
|
|
956
|
+
"sqlx",
|
|
957
|
+
"anyhow",
|
|
958
|
+
"thiserror",
|
|
959
|
+
"node_modules",
|
|
960
|
+
"package_json",
|
|
961
|
+
"tsconfig_json",
|
|
962
|
+
"sourcemappingurl",
|
|
963
|
+
"getserversideprops",
|
|
964
|
+
"getstaticprops",
|
|
965
|
+
"getstaticpaths",
|
|
966
|
+
]);
|
|
967
|
+
|
|
968
|
+
// A distinctive symbol/function name is genuinely PORTABLE across vendors: the identical token string
|
|
969
|
+
// in two agents is the same code entity (unlike a basename). Eligibility: not generic/template/
|
|
970
|
+
// scaffolding, long enough to be specific, and NOT so ubiquitous it is obviously a library token
|
|
971
|
+
// (appearing in more than a handful of distinct sessions ⇒ shared vocabulary, not a personal anchor).
|
|
972
|
+
function symbolDistinctive(kind, anchor, sessionCount) {
|
|
973
|
+
if (kind !== "sym:path" && kind !== "sym:ident") return false;
|
|
974
|
+
const a = anchor.toLowerCase();
|
|
975
|
+
if (isTemplateTok(anchor)) return false;
|
|
976
|
+
if (GENERIC.has(a) || CROSS_COMMON_CORES.has(a) || SCAFFOLD_SYMBOLS.has(a)) return false;
|
|
977
|
+
// any path/ident segment being a scaffold token drags the whole anchor toward library noise
|
|
978
|
+
for (const seg of a.split(/[^a-z0-9]+/).filter(Boolean))
|
|
979
|
+
if (SCAFFOLD_SYMBOLS.has(seg)) return false;
|
|
980
|
+
if (kind === "sym:path") {
|
|
981
|
+
if (anchor.length < 10) return false; // namespaced `a::b` — needs real length to be specific
|
|
982
|
+
} else {
|
|
983
|
+
if (anchor.length < 12) return false; // multi-segment ident — long enough to be non-accidental
|
|
984
|
+
}
|
|
985
|
+
if (sessionCount > 6) return false; // ubiquitous ⇒ library / harness vocabulary, not a personal anchor
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// A cross-vendor NON-FILE anchor (files are matched by absolute-path identity elsewhere): an
|
|
990
|
+
// identifier-carrying error, or a distinctive symbol. Bare / generic tokens never qualify.
|
|
991
|
+
function crossAnchorEligible(kind, anchor, sessionCount) {
|
|
852
992
|
if (isTemplateTok(anchor)) return false;
|
|
853
993
|
if (kind.startsWith("err")) return ERR_SPECIFIC.has(kind);
|
|
854
|
-
if (kind === "sym:
|
|
855
|
-
return
|
|
856
|
-
return false; // sym:
|
|
994
|
+
if (kind === "sym:path" || kind === "sym:ident")
|
|
995
|
+
return symbolDistinctive(kind, anchor, sessionCount);
|
|
996
|
+
return false; // sym:file is handled by path identity, never by basename here
|
|
857
997
|
}
|
|
858
998
|
|
|
859
999
|
// ---------- main ----------
|
|
@@ -918,15 +1058,43 @@ async function main() {
|
|
|
918
1058
|
const presentVendors = Object.keys(inv).filter((v) => inv[v].sessions > 0);
|
|
919
1059
|
|
|
920
1060
|
// ============ CELL A: cross-session recurrence on deterministic anchors (across the UNION) ============
|
|
1061
|
+
// filePathMap indexes file mentions by RESOLVED ABSOLUTE IDENTITY (not basename) so a cross-vendor
|
|
1062
|
+
// "same file" claim can require the SAME ACTUAL FILE. identity → Map(source_tool → Map(sessionId,witness)).
|
|
921
1063
|
const anchorMap = new Map();
|
|
1064
|
+
const filePathMap = new Map();
|
|
922
1065
|
for (const s of sessions) {
|
|
923
1066
|
for (const turn of s.turns) {
|
|
924
1067
|
const mineText = turn.boiler ? "" : turn.text || turn.toolErrText || "";
|
|
925
1068
|
if (!mineText || isAutopsyEcho(mineText)) continue;
|
|
926
1069
|
const errOnly = turn.mineErrOnly;
|
|
927
1070
|
const seenThisTurn = new Set();
|
|
928
|
-
for (const
|
|
1071
|
+
for (const ex of extractAnchors(mineText)) {
|
|
1072
|
+
const { kind, anchor, at } = ex;
|
|
929
1073
|
if (errOnly && !kind.startsWith("err")) continue;
|
|
1074
|
+
// ---- same-file identity index (message text only; tool output is skipped above) ----
|
|
1075
|
+
if (kind === "sym:file") {
|
|
1076
|
+
const identity = resolveFileIdentity(ex.rawPath, ex.abs, turn.root);
|
|
1077
|
+
if (identity) {
|
|
1078
|
+
if (!filePathMap.has(identity))
|
|
1079
|
+
filePathMap.set(identity, { basename: anchor, byTool: new Map() });
|
|
1080
|
+
const fe = filePathMap.get(identity);
|
|
1081
|
+
if (!fe.byTool.has(turn.source_tool)) fe.byTool.set(turn.source_tool, new Map());
|
|
1082
|
+
const perSess = fe.byTool.get(turn.source_tool);
|
|
1083
|
+
if (!perSess.has(s.sessionId))
|
|
1084
|
+
perSess.set(s.sessionId, {
|
|
1085
|
+
session_id: s.sessionId,
|
|
1086
|
+
turn_idx: turn.turn_idx,
|
|
1087
|
+
ts: turn.ts,
|
|
1088
|
+
role: turn.role,
|
|
1089
|
+
root: turn.root,
|
|
1090
|
+
from: "message",
|
|
1091
|
+
source_tool: turn.source_tool,
|
|
1092
|
+
bubbleKey: turn.bubbleKey,
|
|
1093
|
+
verbatim: span(mineText, at, ex.rawPath.length),
|
|
1094
|
+
reAnchor: ex.rawPath, // re-confirm the FULL mentioned path on a fresh re-read, not the basename
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
930
1098
|
const key = kind.startsWith("sym") ? anchor : kind + "|" + anchor.toLowerCase();
|
|
931
1099
|
if (seenThisTurn.has(key)) continue;
|
|
932
1100
|
seenThisTurn.add(key);
|
|
@@ -987,13 +1155,17 @@ async function main() {
|
|
|
987
1155
|
}
|
|
988
1156
|
cellA.sort((a, b) => b.relatedN - a.relatedN || b.spec - a.spec);
|
|
989
1157
|
|
|
990
|
-
// ============ CELL X: CROSS-VENDOR recurrence —
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
1158
|
+
// ============ CELL X: CROSS-VENDOR recurrence — the SAME specific anchor across >=2 agents ============
|
|
1159
|
+
// Two disjoint, both-genuine sources:
|
|
1160
|
+
// (1) errors-with-identifiers + distinctive symbols — from anchorMap. The identical token in two
|
|
1161
|
+
// agents IS the same entity (portable by construction), so >=2 distinct source tools qualifies.
|
|
1162
|
+
// (2) files — from filePathMap, keyed by RESOLVED ABSOLUTE IDENTITY. A shared basename is NOT a
|
|
1163
|
+
// match; only the same actual path across >=2 tools qualifies. proofPath carries that identity
|
|
1164
|
+
// so the receipt (and the pre-push self-verify) can print the exact shared file.
|
|
1165
|
+
// Each vendor's witnesses stay separate so the receipt reproduces EACH side from its own raw store.
|
|
994
1166
|
const cellX = [];
|
|
995
1167
|
for (const [, rec] of anchorMap) {
|
|
996
|
-
if (!
|
|
1168
|
+
if (!crossAnchorEligible(rec.kind, rec.anchor, rec.sessions.size)) continue;
|
|
997
1169
|
const byVendor = new Map();
|
|
998
1170
|
for (const w of rec.sessions.values()) {
|
|
999
1171
|
const st = w.source_tool || SRC.claude;
|
|
@@ -1009,12 +1181,31 @@ async function main() {
|
|
|
1009
1181
|
cellX.push({
|
|
1010
1182
|
kind: rec.kind,
|
|
1011
1183
|
anchor: rec.anchor,
|
|
1184
|
+
proofPath: null,
|
|
1012
1185
|
vendorCount: byVendor.size,
|
|
1013
1186
|
totalSessions: rec.sessions.size,
|
|
1014
1187
|
perVendor,
|
|
1015
1188
|
spec,
|
|
1016
1189
|
});
|
|
1017
1190
|
}
|
|
1191
|
+
for (const [identity, fe] of filePathMap) {
|
|
1192
|
+
if (fe.byTool.size < 2) continue; // same actual file must appear in >=2 different agents
|
|
1193
|
+
let totalSessions = 0;
|
|
1194
|
+
const perVendor = [...fe.byTool.entries()].map(([src, perSess]) => {
|
|
1195
|
+
const ws = [...perSess.values()].sort((a, b) => (a.turn_idx || 0) - (b.turn_idx || 0));
|
|
1196
|
+
totalSessions += ws.length;
|
|
1197
|
+
return { src, witnesses: ws };
|
|
1198
|
+
});
|
|
1199
|
+
cellX.push({
|
|
1200
|
+
kind: "sym:file",
|
|
1201
|
+
anchor: fe.basename,
|
|
1202
|
+
proofPath: identity, // the exact shared absolute path — the same-file proof
|
|
1203
|
+
vendorCount: fe.byTool.size,
|
|
1204
|
+
totalSessions,
|
|
1205
|
+
perVendor,
|
|
1206
|
+
spec: identity.length + 6,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1018
1209
|
cellX.sort(
|
|
1019
1210
|
(a, b) => b.vendorCount - a.vendorCount || b.totalSessions - a.totalSessions || b.spec - a.spec,
|
|
1020
1211
|
);
|
|
@@ -1131,7 +1322,7 @@ async function main() {
|
|
|
1131
1322
|
}
|
|
1132
1323
|
if (legs.length < 2) continue; // must still be genuinely cross-vendor after a fresh re-read
|
|
1133
1324
|
legs.sort((a, b) => b.rep.confirmed - a.rep.confirmed);
|
|
1134
|
-
renderedX.push({ kind: r.kind, anchor: r.anchor, legs });
|
|
1325
|
+
renderedX.push({ kind: r.kind, anchor: r.anchor, proofPath: r.proofPath, legs });
|
|
1135
1326
|
usedAnchors.add(anchorKey(r.kind, r.anchor));
|
|
1136
1327
|
}
|
|
1137
1328
|
|
|
@@ -1210,6 +1401,7 @@ async function main() {
|
|
|
1210
1401
|
}
|
|
1211
1402
|
P();
|
|
1212
1403
|
P(" " + RULE);
|
|
1404
|
+
renderNoCrossVendor(P, presentVendors, renderedX.length > 0);
|
|
1213
1405
|
renderConnectInvite(P, presentVendors);
|
|
1214
1406
|
renderFooter(P, presentVendors);
|
|
1215
1407
|
process.stdout.write(out.join("\n") + "\n");
|
|
@@ -1229,11 +1421,15 @@ async function main() {
|
|
|
1229
1421
|
: names.slice(0, -1).join(", ") + ", and " + names.slice(-1);
|
|
1230
1422
|
const counts = rx.legs.map((l) => `${l.rep.total} in ${srcLabel(l.src)}`).join(", ");
|
|
1231
1423
|
if (rx.kind === "sym:file") {
|
|
1232
|
-
P(`
|
|
1233
|
-
P(`
|
|
1234
|
-
|
|
1424
|
+
P(` The SAME FILE followed you across ${nameList} (${counts}):`);
|
|
1425
|
+
P(` ${redact(rx.proofPath)}`);
|
|
1426
|
+
P(` — same absolute path in every agent, not just a shared filename.`);
|
|
1427
|
+
} else if (rx.kind.startsWith("err")) {
|
|
1235
1428
|
P(` ${plainAnchor(rx.kind, rx.anchor)} appears in BOTH ${nameList} — the same specific`);
|
|
1236
1429
|
P(` error surfaced in more than one agent (${counts}).`);
|
|
1430
|
+
} else {
|
|
1431
|
+
P(` ${plainAnchor(rx.kind, rx.anchor)} appears in BOTH ${nameList} — the same distinctive`);
|
|
1432
|
+
P(` symbol carried across more than one agent (${counts}).`);
|
|
1237
1433
|
}
|
|
1238
1434
|
P(` No single model vendor can see this; it needs all your logs at once.`);
|
|
1239
1435
|
P();
|
|
@@ -1335,12 +1531,32 @@ async function main() {
|
|
|
1335
1531
|
P(" " + RULE);
|
|
1336
1532
|
}
|
|
1337
1533
|
|
|
1338
|
-
// --- honest
|
|
1534
|
+
// --- honest cross-vendor status: strict miss when >=2 agents present, or a connect invite for one.
|
|
1535
|
+
renderNoCrossVendor(P, presentVendors, renderedX.length > 0);
|
|
1339
1536
|
renderConnectInvite(P, presentVendors);
|
|
1340
1537
|
renderFooter(P, presentVendors);
|
|
1341
1538
|
process.stdout.write(out.join("\n") + "\n");
|
|
1342
1539
|
}
|
|
1343
1540
|
|
|
1541
|
+
// When >=2 agents ARE connected but nothing genuinely crosses between them, say so plainly instead of
|
|
1542
|
+
// manufacturing a basename-collision "match". This is the honest counterpart to a real CELL X finding.
|
|
1543
|
+
function renderNoCrossVendor(P, presentVendors, hasCrossVendor) {
|
|
1544
|
+
if (presentVendors.length < 2 || hasCrossVendor) return;
|
|
1545
|
+
const names = presentVendors.map(srcLabel);
|
|
1546
|
+
const list =
|
|
1547
|
+
names.length === 2
|
|
1548
|
+
? names.join(" and ")
|
|
1549
|
+
: names.slice(0, -1).join(", ") + ", and " + names.slice(-1);
|
|
1550
|
+
P();
|
|
1551
|
+
P(` No same-file or same-error crossed your tools yet (read: ${list}).`);
|
|
1552
|
+
P(` Cross-vendor is deliberately strict: it fires only when the SAME actual file`);
|
|
1553
|
+
P(` path — or the SAME specific error/symbol — shows up in two different agents.`);
|
|
1554
|
+
P(` A shared filename across different projects is NOT a match and is suppressed.`);
|
|
1555
|
+
P(` Keep working across your tools; the first genuine crossing renders itself here.`);
|
|
1556
|
+
P();
|
|
1557
|
+
P(" " + RULE);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1344
1560
|
// When only ONE agent has usable history, the cross-vendor verdict cannot exist yet. Say so honestly
|
|
1345
1561
|
// and invite a second connection — the multi-vendor connect is the whole point of this cell.
|
|
1346
1562
|
function renderConnectInvite(P, presentVendors) {
|
package/bootstrap.mjs
CHANGED
|
@@ -368,7 +368,9 @@ function recordProfile(profile, sessions) {
|
|
|
368
368
|
if (headline) {
|
|
369
369
|
recordInsight({
|
|
370
370
|
kind: "profile",
|
|
371
|
-
|
|
371
|
+
// RED LINE 1: hours/rework_pct are the server's LLM-derived ESTIMATES, not a stopwatch and not
|
|
372
|
+
// locally reproducible — label them as estimates and point to the `skalpel autopsy` receipt.
|
|
373
|
+
display: `estimated ~${headline.hours ?? 0}h active with AI across ${headline.sessions ?? sessions} sessions; est. ~${headline.rework_pct ?? 0}% rework (run 'skalpel autopsy' for the exact receipt)`,
|
|
372
374
|
});
|
|
373
375
|
} else {
|
|
374
376
|
recordInsight({ kind: "profile", display: `standing profile built from ${sessions} sessions` });
|
package/package.json
CHANGED
package/reveal.mjs
CHANGED
|
@@ -31,13 +31,25 @@ export function graphReadyNote() {
|
|
|
31
31
|
const trap = m.report && (m.report.discriminative || [])[0] && m.report.discriminative[0].seq;
|
|
32
32
|
const n = m.n_sessions || h.sessions || 0;
|
|
33
33
|
|
|
34
|
+
// PROVENANCE / HONESTY (RED LINE 1): both `hours` and `rework_pct` come from the hosted graph
|
|
35
|
+
// server's profile, which is built from an LLM judge's per-intent classification (intent_class +
|
|
36
|
+
// resolution) over the ingested transcripts. `hours` = summed, banded human read/dwell time on
|
|
37
|
+
// non-`other` intents (NOT raw session wall-clock); `rework_pct` = share of that time whose
|
|
38
|
+
// LLM-judged resolution is a "waste" class. Neither is a stopwatch measurement and neither
|
|
39
|
+
// re-derives locally the way `skalpel autopsy` does — so they must be surfaced as ESTIMATES, never
|
|
40
|
+
// as measured fact. The reproducible receipt is `skalpel autopsy`.
|
|
34
41
|
const facts = [];
|
|
35
42
|
if (h.hours)
|
|
36
|
-
facts.push(
|
|
43
|
+
facts.push(
|
|
44
|
+
`skalpel ESTIMATES they've spent about ${h.hours} hours of active time working with AI across ` +
|
|
45
|
+
`these sessions (inferred from their history — an estimate, not an exact clock)`,
|
|
46
|
+
);
|
|
37
47
|
if (h.rework_pct)
|
|
38
48
|
facts.push(
|
|
39
|
-
`roughly ${h.rework_pct}% of that
|
|
40
|
-
`re-fix things they'd already worked on (
|
|
49
|
+
`and it estimates roughly ${h.rework_pct}% of that went into REWORK-type patterns — going back ` +
|
|
50
|
+
`to redo, re-explain, or re-fix things they'd already worked on (this % is skalpel's ESTIMATE ` +
|
|
51
|
+
`from reading their sessions, NOT a stopwatch figure — it's the kind of loop skalpel is built ` +
|
|
52
|
+
`to help with)`,
|
|
41
53
|
);
|
|
42
54
|
if (trap)
|
|
43
55
|
facts.push(
|
|
@@ -54,8 +66,12 @@ export function graphReadyNote() {
|
|
|
54
66
|
`"🔬 skalpel · your graph's ready — here's what I noticed about how you work:" — then, in 2-3 ` +
|
|
55
67
|
`friendly plain-English sentences, tell them what it found` +
|
|
56
68
|
(facts.length ? `: ${facts.join("; ")}` : "") +
|
|
57
|
-
`.
|
|
58
|
-
`
|
|
59
|
-
`
|
|
69
|
+
`. HONESTY RULE: present every number as skalpel's ESTIMATE inferred from their history — hedge ` +
|
|
70
|
+
`naturally ("about", "roughly", "skalpel estimates") and NEVER state a figure as an exact ` +
|
|
71
|
+
`measurement. If they want the exact, locally-verified receipt that re-derives from the coding ` +
|
|
72
|
+
`logs already on their own machine, tell them they can run \`skalpel autopsy\` anytime. Close by ` +
|
|
73
|
+
`reassuring them this is exactly the loop skalpel now quietly watches for and steers them out of, ` +
|
|
74
|
+
`live, while they code — they don't have to track it themselves. Then continue with whatever they ` +
|
|
75
|
+
`actually asked. Keep the whole thing short and human.]`
|
|
60
76
|
);
|
|
61
77
|
}
|
package/skalpel-setup.mjs
CHANGED
|
@@ -698,12 +698,15 @@ async function revealInsights(report) {
|
|
|
698
698
|
console.log("");
|
|
699
699
|
await line(` ${B}Here's what I learned about how you work:${X}\n`, 480);
|
|
700
700
|
|
|
701
|
+
// RED LINE 1: h.hours / h.rework_pct are the hosted graph server's ESTIMATES, built from an LLM
|
|
702
|
+
// judge's per-intent classification — not a stopwatch, and not locally reproducible. Surface them
|
|
703
|
+
// as estimates and point to `skalpel autopsy` (the zero-network receipt that re-derives from disk).
|
|
701
704
|
const rw = h.rework_pct || 0;
|
|
702
705
|
await line(
|
|
703
|
-
` ${R}▸${X}
|
|
706
|
+
` ${R}▸${X} skalpel estimates about ${B}${h.hours}h${X} of active time with AI across ${h.sessions} sessions — and that about ${B}${R}${rw}%${X} of it went to ${B}rework${X}:`,
|
|
704
707
|
);
|
|
705
708
|
await line(
|
|
706
|
-
` ${D}circling back, re-explaining, re-fixing the same thing
|
|
709
|
+
` ${D}circling back, re-explaining, re-fixing the same thing — the loop skalpel goes after.${X}\n`,
|
|
707
710
|
520,
|
|
708
711
|
);
|
|
709
712
|
|
|
@@ -738,6 +741,13 @@ async function revealInsights(report) {
|
|
|
738
741
|
}
|
|
739
742
|
|
|
740
743
|
console.log("");
|
|
744
|
+
// RED LINE 1: everything above (hours, rework %, archetype waste-hours, time-sink %, escape %) is
|
|
745
|
+
// the hosted graph's ESTIMATE from an LLM read of your sessions — not a stopwatch. State that once,
|
|
746
|
+
// plainly, and point to the zero-network receipt that re-derives from the logs on THIS machine.
|
|
747
|
+
await line(
|
|
748
|
+
` ${D}These are skalpel's estimates from your recent sessions — run ${X}${B}skalpel autopsy${X}${D} for the exact receipt that re-derives from the logs on this machine.${X}`,
|
|
749
|
+
260,
|
|
750
|
+
);
|
|
741
751
|
await line(
|
|
742
752
|
` ${G}skalpel now watches for these every session and steers you off them in real time.${X}`,
|
|
743
753
|
260,
|