skalpel 4.0.57 → 4.0.58
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/anchor-shadow.mjs +14 -3
- package/eval-harness.mjs +10 -4
- package/package.json +1 -1
- package/skalpel-statusline.mjs +55 -1
- package/verify-shadow.mjs +39 -15
package/anchor-shadow.mjs
CHANGED
|
@@ -505,11 +505,22 @@ function findPushBranch(text) {
|
|
|
505
505
|
return { remote, branch };
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
// findTag(text) → a release tag (
|
|
508
|
+
// findTag(text) → a release tag, COMMAND-ANCHORED (2026-07-13 false-accusation incident): a git-tag
|
|
509
|
+
// target may ONLY be minted when the tag token appears on a line that is itself a `git tag` / `git push`
|
|
510
|
+
// COMMAND — NEVER scavenged from arbitrary prose/output. A bare `v26.3.1` in quoted `node --version`
|
|
511
|
+
// output once became a "release tag", probed `git ls-remote --tags origin v26.3.1`, and minted a false
|
|
512
|
+
// SHIP_FAIL on a true claim. Implementation: extract ONLY the candidate lines matching
|
|
513
|
+
// /\bgit\s+(?:tag|push)\b/ and run the (unchanged) tag patterns over those lines alone; no such line →
|
|
514
|
+
// no target, ever (→ NO_TARGET, no probe, no accusation). Validation (SHIP_SAFE_REF) unchanged.
|
|
509
515
|
function findTag(text) {
|
|
510
|
-
const
|
|
516
|
+
const cmdLines = String(text || "")
|
|
517
|
+
.split(/\r?\n/)
|
|
518
|
+
.filter((l) => /\bgit\s+(?:tag|push)\b/i.test(l));
|
|
519
|
+
if (!cmdLines.length) return null; // no git tag/push command anywhere in context → no tag target
|
|
520
|
+
const cmds = cmdLines.join("\n");
|
|
521
|
+
const near = /\bgit\s+(?:tag|push[^\n;&|]*?)\s+(v?\d+\.\d+\.\d+[0-9A-Za-z.\-+]*)\b/i.exec(cmds);
|
|
511
522
|
if (near && SHIP_SAFE_REF.test(near[1])) return near[1];
|
|
512
|
-
const m = /\b(v\d+\.\d+\.\d+[0-9A-Za-z.\-+]*)\b/.exec(
|
|
523
|
+
const m = /\b(v\d+\.\d+\.\d+[0-9A-Za-z.\-+]*)\b/.exec(cmds);
|
|
513
524
|
return m && SHIP_SAFE_REF.test(m[1]) ? m[1] : null;
|
|
514
525
|
}
|
|
515
526
|
|
package/eval-harness.mjs
CHANGED
|
@@ -1127,6 +1127,7 @@ const SHIP_FIXTURES = [
|
|
|
1127
1127
|
expectReprobe: true,
|
|
1128
1128
|
},
|
|
1129
1129
|
// (d) TRUE CATCH: pushed-claim + sha NOT on the real remote tip → red (it never left the machine).
|
|
1130
|
+
// Since the all-reds confirm-gate (2026-07-13 incident), even a hard FAIL must confirm 2/2.
|
|
1130
1131
|
{
|
|
1131
1132
|
id: "d",
|
|
1132
1133
|
label: "pushed + sha-not-on-remote → RED",
|
|
@@ -1136,8 +1137,9 @@ const SHIP_FIXTURES = [
|
|
|
1136
1137
|
exec: gitRemoteExec({ tip: SHA_OLD, ancestor: false }),
|
|
1137
1138
|
expectOutcome: "SHIP_FAIL",
|
|
1138
1139
|
expectRed: true,
|
|
1140
|
+
expectReprobe: true,
|
|
1139
1141
|
},
|
|
1140
|
-
// (e) TRUE CATCH: merged-claim + PR is OPEN → red.
|
|
1142
|
+
// (e) TRUE CATCH: merged-claim + PR is OPEN → red (confirmed 2/2).
|
|
1141
1143
|
{
|
|
1142
1144
|
id: "e",
|
|
1143
1145
|
label: "merged + PR OPEN → RED",
|
|
@@ -1147,6 +1149,7 @@ const SHIP_FIXTURES = [
|
|
|
1147
1149
|
exec: ghPrExec("OPEN", null),
|
|
1148
1150
|
expectOutcome: "SHIP_FAIL",
|
|
1149
1151
|
expectRed: true,
|
|
1152
|
+
expectReprobe: true,
|
|
1150
1153
|
},
|
|
1151
1154
|
// (f) TRUE CATCH: live-claim + confirmed 404 × 2 → red (only the double-negative fires).
|
|
1152
1155
|
{
|
|
@@ -1217,7 +1220,7 @@ const SHIP_FIXTURES = [
|
|
|
1217
1220
|
expectRed: true,
|
|
1218
1221
|
expectReprobe: true,
|
|
1219
1222
|
},
|
|
1220
|
-
// (l) published + NOT on the registry → red (npm view E404).
|
|
1223
|
+
// (l) published + NOT on the registry → red (npm view E404, confirmed 2/2).
|
|
1221
1224
|
{
|
|
1222
1225
|
id: "l",
|
|
1223
1226
|
label: "published + npm E404 → RED",
|
|
@@ -1227,6 +1230,7 @@ const SHIP_FIXTURES = [
|
|
|
1227
1230
|
exec: npmExec(false, "9.9.9"),
|
|
1228
1231
|
expectOutcome: "SHIP_FAIL",
|
|
1229
1232
|
expectRed: true,
|
|
1233
|
+
expectReprobe: true,
|
|
1230
1234
|
},
|
|
1231
1235
|
// (m) published + IS on the registry → NO red.
|
|
1232
1236
|
{
|
|
@@ -1239,7 +1243,7 @@ const SHIP_FIXTURES = [
|
|
|
1239
1243
|
expectOutcome: "SHIP_OK",
|
|
1240
1244
|
expectRed: false,
|
|
1241
1245
|
},
|
|
1242
|
-
// (n) CI green claim + checks FAILING → red.
|
|
1246
|
+
// (n) CI green claim + checks FAILING → red (confirmed 2/2).
|
|
1243
1247
|
{
|
|
1244
1248
|
id: "n",
|
|
1245
1249
|
label: "ci-green + checks failing → RED",
|
|
@@ -1249,6 +1253,7 @@ const SHIP_FIXTURES = [
|
|
|
1249
1253
|
exec: ghChecksExec([{ bucket: "pass" }, { bucket: "fail" }]),
|
|
1250
1254
|
expectOutcome: "SHIP_FAIL",
|
|
1251
1255
|
expectRed: true,
|
|
1256
|
+
expectReprobe: true,
|
|
1252
1257
|
},
|
|
1253
1258
|
// (o) CI green claim + checks passing → NO red.
|
|
1254
1259
|
{
|
|
@@ -1261,7 +1266,7 @@ const SHIP_FIXTURES = [
|
|
|
1261
1266
|
expectOutcome: "SHIP_OK",
|
|
1262
1267
|
expectRed: false,
|
|
1263
1268
|
},
|
|
1264
|
-
// (p) released (git tag) + tag NOT on origin → red.
|
|
1269
|
+
// (p) released (git tag) + tag NOT on origin → red (confirmed 2/2).
|
|
1265
1270
|
{
|
|
1266
1271
|
id: "p",
|
|
1267
1272
|
label: "released tag + missing on origin → RED",
|
|
@@ -1271,6 +1276,7 @@ const SHIP_FIXTURES = [
|
|
|
1271
1276
|
exec: gitTagExec(false, "v2.0.0"),
|
|
1272
1277
|
expectOutcome: "SHIP_FAIL",
|
|
1273
1278
|
expectRed: true,
|
|
1279
|
+
expectReprobe: true,
|
|
1274
1280
|
},
|
|
1275
1281
|
// (q) released (git tag) + tag present on origin → NO red.
|
|
1276
1282
|
{
|
package/package.json
CHANGED
package/skalpel-statusline.mjs
CHANGED
|
@@ -39,7 +39,16 @@ import { readFileSync, rmSync } from "node:fs";
|
|
|
39
39
|
import { homedir } from "node:os";
|
|
40
40
|
import { join } from "node:path";
|
|
41
41
|
import { tailLines } from "./transcript.mjs";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
cleanText,
|
|
44
|
+
REVEAL_PATH,
|
|
45
|
+
VERDICT_PATH,
|
|
46
|
+
revealEnabled,
|
|
47
|
+
autoDarkened,
|
|
48
|
+
ADJUDICATIONS_PATH,
|
|
49
|
+
SUPPRESS_PATH,
|
|
50
|
+
proofSig,
|
|
51
|
+
} from "./insights.mjs";
|
|
43
52
|
|
|
44
53
|
const CYAN = "\x1b[36m";
|
|
45
54
|
const GREEN = "\x1b[32m";
|
|
@@ -352,6 +361,38 @@ function compactMeasured(ms) {
|
|
|
352
361
|
: `${(Math.floor(h * 10) / 10).toFixed(1).replace(/\.0$/, "")}h`;
|
|
353
362
|
}
|
|
354
363
|
|
|
364
|
+
// adjudicatedNoKeys() → the set of `${session}|${proofSig}` keys the USER adjudicated "not a real catch"
|
|
365
|
+
// (`skalpel no` appends a verdict:"no" row to verify-adjudications.ndjson AND a session-scoped row to
|
|
366
|
+
// verify-suppressed.ndjson — both are read, either is sufficient evidence). Bounded tail reads (same
|
|
367
|
+
// tailLines guard as the shadow log), fail-open (any error → empty set → tally unchanged). Called LAZILY
|
|
368
|
+
// from shadowTally only when a mismatch row actually exists, so a log with no catches — and any install
|
|
369
|
+
// where the adjudication files are absent — pays zero extra cost.
|
|
370
|
+
const ADJ_READ_BYTES = 256 * 1024;
|
|
371
|
+
function adjudicatedNoKeys() {
|
|
372
|
+
const keys = new Set();
|
|
373
|
+
try {
|
|
374
|
+
for (const l of tailLines(ADJUDICATIONS_PATH, ADJ_READ_BYTES)) {
|
|
375
|
+
try {
|
|
376
|
+
const r = JSON.parse(l);
|
|
377
|
+
if (r && r.verdict === "no" && r.proof) keys.add(`${r.session ?? ""}|${proofSig(r.proof)}`);
|
|
378
|
+
} catch {
|
|
379
|
+
/* partial / garbage line — skip */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const l of tailLines(SUPPRESS_PATH, ADJ_READ_BYTES)) {
|
|
383
|
+
try {
|
|
384
|
+
const r = JSON.parse(l);
|
|
385
|
+
if (r && r.session && r.proof) keys.add(`${r.session}|${proofSig(r.proof)}`);
|
|
386
|
+
} catch {
|
|
387
|
+
/* partial / garbage line — skip */
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
/* fail-open: no adjudication data → nothing excluded */
|
|
392
|
+
}
|
|
393
|
+
return keys;
|
|
394
|
+
}
|
|
395
|
+
|
|
355
396
|
// measuredCaughtTotalMs() → the CUMULATIVE LIFETIME sum, in ms, of EVERY real measured red→green delta:
|
|
356
397
|
// each caught false "done" (mismatch row) that has a later same-session same-proof PASS contributes that
|
|
357
398
|
// one measured (t_pass − t_false) delta; a catch with no later PASS contributes 0 (never estimated). This
|
|
@@ -388,7 +429,20 @@ function shadowTally() {
|
|
|
388
429
|
const CHECKED = new Set(["PASS", "GENUINE_FAIL", "SHIP_OK", "SHIP_FAIL"]);
|
|
389
430
|
let checked = 0;
|
|
390
431
|
let lies = 0;
|
|
432
|
+
// ADJUDICATION-AWARE (2026-07-13 false-accusation incident): a mismatch row the USER marked "not a
|
|
433
|
+
// real catch" (`skalpel no`) must stop counting as `false` on the bar — and an adjudicated-wrong
|
|
434
|
+
// check isn't a check we stand behind, so it leaves `checked` (and its fixDelta leaves the measured
|
|
435
|
+
// total) too. Lazy: the adjudication files are only read once a mismatch row actually exists.
|
|
436
|
+
let noKeys = null;
|
|
437
|
+
const adjudicatedWrong = (r) => {
|
|
438
|
+
if (r.mismatch !== true) return false;
|
|
439
|
+
if (noKeys === null) noKeys = adjudicatedNoKeys();
|
|
440
|
+
if (noKeys.size === 0) return false;
|
|
441
|
+
// The join key mirrors the reveal/adjudication sig: a ship row's "proof" is its probe command.
|
|
442
|
+
return noKeys.has(`${r.session ?? ""}|${proofSig(r.proof_command || r.probe_command)}`);
|
|
443
|
+
};
|
|
391
444
|
for (const r of rows) {
|
|
445
|
+
if (adjudicatedWrong(r)) continue; // user said "no" — not a lie, not a check, no measured delta
|
|
392
446
|
if (CHECKED.has(r.outcome)) checked += 1;
|
|
393
447
|
if (r.mismatch !== true) continue; // only a caught false "done" (verify-shadow's money metric)
|
|
394
448
|
lies += 1;
|
package/verify-shadow.mjs
CHANGED
|
@@ -1013,6 +1013,21 @@ const SHIP_CLAIM_PATTERNS = [
|
|
|
1013
1013
|
const SHIP_FUTURE_OR_QUESTION =
|
|
1014
1014
|
/\b(?:will|i'?ll|we'?ll|going\s+to|gonna|about\s+to|let\s+me|planning\s+to|need\s+to|shall\s+i|should\s+i|can\s+i|once\s+|after\s+i|before\s+i|ready\s+to)\b/i;
|
|
1015
1015
|
|
|
1016
|
+
// PRECISION (2026-07-13 false-accusation incident): markdown STRUCTURE is not a declarative sentence.
|
|
1017
|
+
// A real ship claim ("Deployed to prod.", "it's live at https://…") is a plain declarative line; a
|
|
1018
|
+
// bullet / table row / blockquote / heading / bold-lead line is the assistant NARRATING or summarizing —
|
|
1019
|
+
// a status-table bullet ("- **`npx skalpel@latest` (what customers install):** 4.0.52 — …") once minted
|
|
1020
|
+
// a false SHIP_FAIL on the founder's own machine. NARROWING only: skip such lines outright.
|
|
1021
|
+
const SHIP_MARKDOWN_STRUCTURE = /^(?:[-*+]\s|\||>|#|\*\*)/;
|
|
1022
|
+
// shipMatchInBackticks(line, offset) → does the match offset fall inside an ODD backtick segment (i.e.
|
|
1023
|
+
// inside inline `code`)? A ship keyword inside backticks is being QUOTED (a command, a flag, output),
|
|
1024
|
+
// never asserted. Split-on-backtick parity: chars after an odd number of ` are inside code.
|
|
1025
|
+
function shipMatchInBackticks(line, offset) {
|
|
1026
|
+
let ticks = 0;
|
|
1027
|
+
for (let i = 0; i < offset && i < line.length; i++) if (line[i] === "`") ticks++;
|
|
1028
|
+
return ticks % 2 === 1;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1016
1031
|
// detectShipClaim(text) → { claim:bool, kind, text:<the claim line, <=120c> }. Scans lines; returns the
|
|
1017
1032
|
// first ASSERTIVE ship-status line. `kind` is the canonical claim keyword reconstructShipTargetV1 consumes.
|
|
1018
1033
|
export function detectShipClaim(text) {
|
|
@@ -1023,8 +1038,10 @@ export function detectShipClaim(text) {
|
|
|
1023
1038
|
if (!l) continue;
|
|
1024
1039
|
if (/\?\s*$/.test(l)) continue; // a question is not a claim
|
|
1025
1040
|
if (SHIP_FUTURE_OR_QUESTION.test(l)) continue; // intention, not "it's done"
|
|
1041
|
+
if (SHIP_MARKDOWN_STRUCTURE.test(l)) continue; // markdown structure, not a declarative sentence
|
|
1026
1042
|
for (const [kind, re] of SHIP_CLAIM_PATTERNS) {
|
|
1027
|
-
|
|
1043
|
+
const m = re.exec(l);
|
|
1044
|
+
if (m && !shipMatchInBackticks(l, m.index)) {
|
|
1028
1045
|
return { claim: true, kind, text: l.replace(/\s+/g, " ").trim().slice(0, 120) };
|
|
1029
1046
|
}
|
|
1030
1047
|
}
|
|
@@ -1289,9 +1306,10 @@ function maybeSurfaceReveal(row) {
|
|
|
1289
1306
|
}
|
|
1290
1307
|
}
|
|
1291
1308
|
|
|
1292
|
-
// SHIP-STATUS v1 confirm-gate: the wait between a URL FAIL_CANDIDATE
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1309
|
+
// SHIP-STATUS v1 confirm-gate: the wait between ANY first-probe negative (a URL FAIL_CANDIDATE — 404/
|
|
1310
|
+
// 410/NXDOMAIN/refused — or a hard registry/tag/pr/git/checks FAIL) and its ONE confirming re-probe.
|
|
1311
|
+
// Default ~60s; overridable via env (SKALPEL_SHIP_CONFIRM_MS, 0 for tests). The wait happens ONLY inside
|
|
1312
|
+
// the DETACHED, unref'd `--run` worker — never on the per-turn hook deadline.
|
|
1295
1313
|
const SHIP_CONFIRM_DELAY_MS = (() => {
|
|
1296
1314
|
const v = parseInt(process.env.SKALPEL_SHIP_CONFIRM_MS ?? "", 10);
|
|
1297
1315
|
return Number.isFinite(v) && v >= 0 ? v : 60_000;
|
|
@@ -1305,7 +1323,7 @@ function shipSleep(ms) {
|
|
|
1305
1323
|
|
|
1306
1324
|
// resolveShipStatus({kind, contextText, payload, target?, exec?, ...}) — the TESTABLE core of the ship
|
|
1307
1325
|
// catch: reconstruct the strict single-family target (or accept a prebuilt one), run its ONE read-only
|
|
1308
|
-
// probe, and (
|
|
1326
|
+
// probe, and (for EVERY first-probe negative, any kind) confirm-gate before ever calling it a red. Returns a plain
|
|
1309
1327
|
// result object — no I/O, no lock, no reveal. `exec` (the read-only command runner) and `sleep` are
|
|
1310
1328
|
// injectable purely so the eval harness can drive every branch hermetically. Never throws to the caller.
|
|
1311
1329
|
// outcome ∈ { NO_TARGET, SHIP_OK, SHIP_FAIL, SHIP_UNKNOWN }; mismatch === (outcome === "SHIP_FAIL").
|
|
@@ -1338,10 +1356,16 @@ export async function resolveShipStatus({
|
|
|
1338
1356
|
let evidence = first.evidence;
|
|
1339
1357
|
let confirm_reprobe = false;
|
|
1340
1358
|
let confirm_outcome = null;
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
|
|
1359
|
+
// CONFIRM-GATE for ALL ship reds (Defect 3, widened after the 2026-07-13 false accusation): NO
|
|
1360
|
+
// first-probe negative EVER reds immediately — not a URL FAIL_CANDIDATE (404/410/NXDOMAIN/refused:
|
|
1361
|
+
// pure CDN/propagation lag) and not a hard registry/tag/pr/git/checks "FAIL" (replication lag, an
|
|
1362
|
+
// eventually-consistent registry read, a stale gh view). Every negative waits (SHIP_CONFIRM_DELAY_MS,
|
|
1363
|
+
// or the injected confirmDelayMs) and re-probes ONCE; only a CONFIRMED 2/2 negative becomes a red.
|
|
1364
|
+
// A PASS on the confirm means it was transient → SHIP_OK; an UNKNOWN on the confirm → SHIP_UNKNOWN
|
|
1365
|
+
// (never a red we can't stand behind). runShipProbeV1's verdict vocabulary is exactly
|
|
1366
|
+
// PASS | FAIL | FAIL_CANDIDATE | UNKNOWN — both negative forms gate identically here.
|
|
1367
|
+
const shipNegative = (v) => v === "FAIL" || v === "FAIL_CANDIDATE";
|
|
1368
|
+
if (shipNegative(verdict)) {
|
|
1345
1369
|
if (typeof beforeReprobe === "function") {
|
|
1346
1370
|
try {
|
|
1347
1371
|
beforeReprobe();
|
|
@@ -1355,11 +1379,11 @@ export async function resolveShipStatus({
|
|
|
1355
1379
|
confirm_reprobe = true;
|
|
1356
1380
|
confirm_outcome = second.verdict;
|
|
1357
1381
|
evidence = second.evidence || evidence;
|
|
1358
|
-
verdict =
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1382
|
+
verdict = shipNegative(second.verdict)
|
|
1383
|
+
? "FAIL"
|
|
1384
|
+
: second.verdict === "PASS"
|
|
1385
|
+
? "PASS"
|
|
1386
|
+
: "UNKNOWN";
|
|
1363
1387
|
}
|
|
1364
1388
|
const outcome =
|
|
1365
1389
|
verdict === "PASS" ? "SHIP_OK" : verdict === "FAIL" ? "SHIP_FAIL" : "SHIP_UNKNOWN";
|
|
@@ -1447,7 +1471,7 @@ async function maybeShipStatusCatch({ text, session, cwd, transcriptPath }) {
|
|
|
1447
1471
|
pass_fail,
|
|
1448
1472
|
outcome,
|
|
1449
1473
|
mismatch: outcome === "SHIP_FAIL", // claimed shipped, the read-only probe DECISIVELY says it is NOT
|
|
1450
|
-
// The instrument records the confirm re-probe (
|
|
1474
|
+
// The instrument records the confirm re-probe (any first-probe negative) and its second verdict.
|
|
1451
1475
|
confirm_reprobe: resolved.confirm_reprobe,
|
|
1452
1476
|
confirm_outcome: resolved.confirm_outcome,
|
|
1453
1477
|
evidence: String(resolved.evidence || "").slice(0, 200),
|