@codacy/verity-cli 0.28.1-experimental.dbd87b1 → 0.28.1-experimental.e79117b
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/LICENSE +156 -0
- package/README.md +5 -0
- package/bin/verity.js +1393 -226
- package/package.json +4 -2
package/bin/verity.js
CHANGED
|
@@ -10386,10 +10386,9 @@ function projectPath(relativePath) {
|
|
|
10386
10386
|
return (0, import_node_path.join)(repoRoot(), relativePath);
|
|
10387
10387
|
}
|
|
10388
10388
|
var MAX_DELTA_BYTES = 194560;
|
|
10389
|
-
var MAX_FILES =
|
|
10389
|
+
var MAX_FILES = 40;
|
|
10390
10390
|
var MAX_FILE_BYTES = 51200;
|
|
10391
10391
|
var DEBOUNCE_SECONDS = 30;
|
|
10392
|
-
var MAX_ITERATIONS = 2;
|
|
10393
10392
|
var MAX_SPEC_FILES = 10;
|
|
10394
10393
|
var MAX_SPEC_FILE_BYTES = 10240;
|
|
10395
10394
|
var MAX_TOTAL_SPEC_BYTES = 30720;
|
|
@@ -10636,14 +10635,14 @@ async function readGlobalCredential(remote) {
|
|
|
10636
10635
|
const parsed = parseCredentialLine(line);
|
|
10637
10636
|
if (parsed && parsed.remote === key) last = parsed.rec;
|
|
10638
10637
|
}
|
|
10639
|
-
if (last) return last;
|
|
10638
|
+
if (last) return { ...last, keyed: true };
|
|
10640
10639
|
}
|
|
10641
10640
|
let plain = null;
|
|
10642
10641
|
for (const line of lines) {
|
|
10643
10642
|
const parsed = parseCredentialLine(line);
|
|
10644
10643
|
if (parsed && parsed.remote === "") plain = parsed.rec;
|
|
10645
10644
|
}
|
|
10646
|
-
return plain;
|
|
10645
|
+
return plain ? { ...plain, keyed: false } : null;
|
|
10647
10646
|
}
|
|
10648
10647
|
async function upsertGlobalCredential(remote, rec) {
|
|
10649
10648
|
const path = globalCredentialsPath();
|
|
@@ -10707,6 +10706,36 @@ async function removeSupersededUserCredentials(loginServiceUrl, loginUserId) {
|
|
|
10707
10706
|
}
|
|
10708
10707
|
return removed;
|
|
10709
10708
|
}
|
|
10709
|
+
async function removeGlobalCredential(remote) {
|
|
10710
|
+
const path = globalCredentialsPath();
|
|
10711
|
+
let content;
|
|
10712
|
+
try {
|
|
10713
|
+
content = await (0, import_promises.readFile)(path, "utf-8");
|
|
10714
|
+
} catch {
|
|
10715
|
+
return false;
|
|
10716
|
+
}
|
|
10717
|
+
const key = encodeRemoteKey(remote);
|
|
10718
|
+
const kept = [];
|
|
10719
|
+
let removed = false;
|
|
10720
|
+
for (const line of content.split("\n")) {
|
|
10721
|
+
const parsed = parseCredentialLine(line);
|
|
10722
|
+
if (parsed && parsed.remote === key) {
|
|
10723
|
+
removed = true;
|
|
10724
|
+
continue;
|
|
10725
|
+
}
|
|
10726
|
+
kept.push(line);
|
|
10727
|
+
}
|
|
10728
|
+
if (!removed) return false;
|
|
10729
|
+
while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
|
|
10730
|
+
try {
|
|
10731
|
+
await (0, import_promises.writeFile)(path, kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
|
|
10732
|
+
await (0, import_promises.chmod)(path, 384).catch(() => {
|
|
10733
|
+
});
|
|
10734
|
+
} catch {
|
|
10735
|
+
return false;
|
|
10736
|
+
}
|
|
10737
|
+
return true;
|
|
10738
|
+
}
|
|
10710
10739
|
function parseLocalCredentialFile(content) {
|
|
10711
10740
|
const tokenMatch = content.match(/token:\s*((?:gate_|verity_)[a-f0-9]+)/);
|
|
10712
10741
|
if (!tokenMatch) return null;
|
|
@@ -10894,8 +10923,8 @@ function filterReviewable(files) {
|
|
|
10894
10923
|
const ext = (0, import_node_path3.extname)(f).slice(1);
|
|
10895
10924
|
if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
|
|
10896
10925
|
if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
|
|
10897
|
-
const
|
|
10898
|
-
if (REVIEWABLE_FILENAMES.has(
|
|
10926
|
+
const basename3 = f.split("/").pop() ?? "";
|
|
10927
|
+
if (REVIEWABLE_FILENAMES.has(basename3)) return true;
|
|
10899
10928
|
if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
|
|
10900
10929
|
return false;
|
|
10901
10930
|
});
|
|
@@ -10986,7 +11015,54 @@ function listTrackedFiles() {
|
|
|
10986
11015
|
return Array.from(set);
|
|
10987
11016
|
}
|
|
10988
11017
|
function sanitizeRemote(remote) {
|
|
10989
|
-
|
|
11018
|
+
const withoutUserinfo = remote.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
|
|
11019
|
+
return /[\u0000-\u001f\u007f]/.test(withoutUserinfo) ? "" : withoutUserinfo;
|
|
11020
|
+
}
|
|
11021
|
+
|
|
11022
|
+
// src/lib/token-pin.ts
|
|
11023
|
+
var cached;
|
|
11024
|
+
var warned = false;
|
|
11025
|
+
function warnOnce(message, warn) {
|
|
11026
|
+
if (!warned) {
|
|
11027
|
+
warned = true;
|
|
11028
|
+
warn(message);
|
|
11029
|
+
}
|
|
11030
|
+
return message;
|
|
11031
|
+
}
|
|
11032
|
+
async function storedCredential() {
|
|
11033
|
+
if (cached !== void 0) return cached;
|
|
11034
|
+
const rec = await readGlobalCredential(currentRemote());
|
|
11035
|
+
cached = rec ? { token: rec.token, serviceUrl: rec.serviceUrl } : null;
|
|
11036
|
+
return cached;
|
|
11037
|
+
}
|
|
11038
|
+
var userNamedUrl = null;
|
|
11039
|
+
function setUserNamedServiceUrl(url) {
|
|
11040
|
+
userNamedUrl = url?.trim() || null;
|
|
11041
|
+
}
|
|
11042
|
+
async function checkTokenPin(token, targetUrl) {
|
|
11043
|
+
if (process.env.VERITY_TOKEN && token === process.env.VERITY_TOKEN) return { attach: true };
|
|
11044
|
+
const envUrl = process.env.VERITY_SERVICE_URL?.trim();
|
|
11045
|
+
if (envUrl && normalizeUrl(envUrl) === normalizeUrl(targetUrl)) return { attach: true };
|
|
11046
|
+
if (userNamedUrl && normalizeUrl(userNamedUrl) === normalizeUrl(targetUrl)) return { attach: true };
|
|
11047
|
+
const stored = await storedCredential();
|
|
11048
|
+
if (!stored || stored.token !== token) return { attach: true };
|
|
11049
|
+
if (!stored.serviceUrl) return { attach: false, reason: "unpinnable" };
|
|
11050
|
+
return normalizeUrl(stored.serviceUrl) === normalizeUrl(targetUrl) ? { attach: true } : { attach: false, reason: "mismatch", mintedFor: stored.serviceUrl };
|
|
11051
|
+
}
|
|
11052
|
+
function normalizeUrl(raw) {
|
|
11053
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
11054
|
+
try {
|
|
11055
|
+
const u = new URL(trimmed);
|
|
11056
|
+
return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
11057
|
+
} catch {
|
|
11058
|
+
return trimmed.toLowerCase();
|
|
11059
|
+
}
|
|
11060
|
+
}
|
|
11061
|
+
function pinRefusalMessage(verdict, targetUrl) {
|
|
11062
|
+
if (verdict.reason === "unpinnable") {
|
|
11063
|
+
return `Refusing to send your Verity login to ${targetUrl}: this machine's credential does not record which service issued it, so it cannot be verified. Run "verity login" to re-issue it.`;
|
|
11064
|
+
}
|
|
11065
|
+
return `Refusing to send your Verity login to ${targetUrl}: it was issued by ${verdict.mintedFor}. A repository cannot redirect your credential to another service. If this service is genuinely yours, log in against it explicitly: VERITY_SERVICE_URL=${targetUrl} verity login`;
|
|
10990
11066
|
}
|
|
10991
11067
|
|
|
10992
11068
|
// src/lib/api-client.ts
|
|
@@ -11023,6 +11099,24 @@ async function apiRequest(options) {
|
|
|
11023
11099
|
"Content-Type": "application/json"
|
|
11024
11100
|
};
|
|
11025
11101
|
if (token) {
|
|
11102
|
+
const pin = await checkTokenPin(token, serviceUrl);
|
|
11103
|
+
if (!pin.attach) {
|
|
11104
|
+
const message = warnOnce(pinRefusalMessage(pin, serviceUrl), printWarn);
|
|
11105
|
+
logHttpCall({
|
|
11106
|
+
cmd,
|
|
11107
|
+
method,
|
|
11108
|
+
url,
|
|
11109
|
+
duration_ms: 0,
|
|
11110
|
+
http_status: null,
|
|
11111
|
+
// 'network' rather than a new category on purpose: no request left the
|
|
11112
|
+
// machine, and every caller's offline path is exactly the handling a
|
|
11113
|
+
// refusal wants (fall back to local, never fabricate a verdict).
|
|
11114
|
+
category: "network",
|
|
11115
|
+
error: "token_pin_refused",
|
|
11116
|
+
retry
|
|
11117
|
+
});
|
|
11118
|
+
return { ok: false, error: `TOKEN_PIN_REFUSED: ${message}`, category: "network" };
|
|
11119
|
+
}
|
|
11026
11120
|
headers["Authorization"] = `Bearer ${token}`;
|
|
11027
11121
|
}
|
|
11028
11122
|
const remote = requestRemote();
|
|
@@ -11135,14 +11229,9 @@ async function serviceUrlFromCredentials() {
|
|
|
11135
11229
|
async function serviceUrlFromVerityMd() {
|
|
11136
11230
|
try {
|
|
11137
11231
|
const content = await (0, import_promises2.readFile)(projectPath(VERITY_MD_FILE), "utf-8");
|
|
11138
|
-
const
|
|
11139
|
-
if (
|
|
11140
|
-
const urlMatch =
|
|
11141
|
-
if (urlMatch) return urlMatch[0];
|
|
11142
|
-
}
|
|
11143
|
-
const plainLine = content.split("\n").find((l) => /(?:url|service)\s*:/i.test(l));
|
|
11144
|
-
if (plainLine) {
|
|
11145
|
-
const urlMatch = plainLine.match(/https:\/\/[^\s]+/);
|
|
11232
|
+
const line = content.split("\n").find((l) => /\*{0,2}(?:url|service)\*{0,2}\s*:/i.test(l));
|
|
11233
|
+
if (line) {
|
|
11234
|
+
const urlMatch = line.match(/https:\/\/[^\s]+/);
|
|
11146
11235
|
if (urlMatch) return urlMatch[0];
|
|
11147
11236
|
}
|
|
11148
11237
|
} catch {
|
|
@@ -11165,7 +11254,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
|
|
|
11165
11254
|
if (mdUrl) {
|
|
11166
11255
|
return { ok: true, data: { url: mdUrl, source: "verity_md" } };
|
|
11167
11256
|
}
|
|
11168
|
-
return {
|
|
11257
|
+
return {
|
|
11258
|
+
ok: false,
|
|
11259
|
+
error: 'No Verity service URL found. Run "verity login" to get started, or /verity-setup to configure this project.'
|
|
11260
|
+
};
|
|
11261
|
+
}
|
|
11262
|
+
async function resolveServiceUrlForAuth(flagUrl) {
|
|
11263
|
+
const strict = await resolveServiceUrlDetailed(flagUrl);
|
|
11264
|
+
if (strict.ok) return strict.data;
|
|
11265
|
+
return { url: DEFAULT_SERVICE_URL, source: "default" };
|
|
11169
11266
|
}
|
|
11170
11267
|
async function resolveServiceUrl(flagUrl) {
|
|
11171
11268
|
const result = await resolveServiceUrlDetailed(flagUrl);
|
|
@@ -11188,7 +11285,13 @@ async function resolveToken(flagToken) {
|
|
|
11188
11285
|
if (rec) {
|
|
11189
11286
|
return {
|
|
11190
11287
|
ok: true,
|
|
11191
|
-
data: {
|
|
11288
|
+
data: {
|
|
11289
|
+
token: rec.token,
|
|
11290
|
+
source: "global",
|
|
11291
|
+
userId: rec.userId,
|
|
11292
|
+
email: rec.email,
|
|
11293
|
+
keyed: rec.keyed
|
|
11294
|
+
}
|
|
11192
11295
|
};
|
|
11193
11296
|
}
|
|
11194
11297
|
const local = await readLegacyLocalCredential();
|
|
@@ -11198,7 +11301,10 @@ async function resolveToken(flagToken) {
|
|
|
11198
11301
|
data: { token: local.token, source: "local", userId: local.userId, email: local.email }
|
|
11199
11302
|
};
|
|
11200
11303
|
}
|
|
11201
|
-
return {
|
|
11304
|
+
return {
|
|
11305
|
+
ok: false,
|
|
11306
|
+
error: 'No Verity token found. Run "verity login" to sign in, or /verity-setup to set up this project.'
|
|
11307
|
+
};
|
|
11202
11308
|
}
|
|
11203
11309
|
async function whoami(token, serviceUrl, verbose) {
|
|
11204
11310
|
return apiRequest({
|
|
@@ -11220,6 +11326,16 @@ function reverifyNudge(who) {
|
|
|
11220
11326
|
}
|
|
11221
11327
|
return null;
|
|
11222
11328
|
}
|
|
11329
|
+
function isLegacyPerRepoCredential(auth2) {
|
|
11330
|
+
return auth2.source === "local" || auth2.source === "global" && auth2.keyed === true;
|
|
11331
|
+
}
|
|
11332
|
+
async function shouldUpgradeOnLogin(auth2) {
|
|
11333
|
+
if (!isLegacyPerRepoCredential(auth2)) return false;
|
|
11334
|
+
if (auth2.userId == null) return true;
|
|
11335
|
+
const bare = await readGlobalCredential("");
|
|
11336
|
+
if (bare?.userId == null) return true;
|
|
11337
|
+
return bare.userId === auth2.userId;
|
|
11338
|
+
}
|
|
11223
11339
|
function authDenialRemedy(error) {
|
|
11224
11340
|
if (error.startsWith("STALE_VERIFICATION")) {
|
|
11225
11341
|
return {
|
|
@@ -11233,6 +11349,12 @@ function authDenialRemedy(error) {
|
|
|
11233
11349
|
remedy: 'No access grant for this repository \u2014 run "verity login" to refresh your grants (a repo granted after your last login needs one), or get write access to it.'
|
|
11234
11350
|
};
|
|
11235
11351
|
}
|
|
11352
|
+
if (error.startsWith("INVALID_TOKEN")) {
|
|
11353
|
+
return {
|
|
11354
|
+
code: "INVALID_TOKEN",
|
|
11355
|
+
remedy: 'Your Verity login has expired or was revoked \u2014 run "verity login" to sign in again.'
|
|
11356
|
+
};
|
|
11357
|
+
}
|
|
11236
11358
|
return null;
|
|
11237
11359
|
}
|
|
11238
11360
|
async function probeService(serviceUrl, verbose) {
|
|
@@ -11273,6 +11395,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
|
|
|
11273
11395
|
|
|
11274
11396
|
// src/lib/register.ts
|
|
11275
11397
|
var readline = __toESM(require("node:readline/promises"));
|
|
11398
|
+
var import_node_os = require("node:os");
|
|
11276
11399
|
|
|
11277
11400
|
// src/lib/provider-auth.ts
|
|
11278
11401
|
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -11460,6 +11583,15 @@ async function registerProject(opts) {
|
|
|
11460
11583
|
}
|
|
11461
11584
|
return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
|
|
11462
11585
|
}
|
|
11586
|
+
function deviceLabel() {
|
|
11587
|
+
const override = process.env.VERITY_DEVICE_LABEL?.trim();
|
|
11588
|
+
if (override) return override;
|
|
11589
|
+
try {
|
|
11590
|
+
return (0, import_node_os.hostname)() || void 0;
|
|
11591
|
+
} catch {
|
|
11592
|
+
return void 0;
|
|
11593
|
+
}
|
|
11594
|
+
}
|
|
11463
11595
|
async function loginOnce(opts) {
|
|
11464
11596
|
const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
|
|
11465
11597
|
const providerAuth = await githubDeviceFlow();
|
|
@@ -11480,13 +11612,19 @@ async function loginOnce(opts) {
|
|
|
11480
11612
|
path: "/auth/login",
|
|
11481
11613
|
serviceUrl: opts.serviceUrl,
|
|
11482
11614
|
extraHeaders: { "X-Provider-Token": providerToken },
|
|
11615
|
+
// Label the session so its owner can tell their machines apart in
|
|
11616
|
+
// `verity sessions list` — a list of identical "login" rows is unusable when
|
|
11617
|
+
// the question is "which of these is the laptop I lost?". The hostname is the
|
|
11618
|
+
// useful default; VERITY_DEVICE_LABEL overrides it for anyone who would
|
|
11619
|
+
// rather not send it. Server-side it is sanitized and capped.
|
|
11620
|
+
body: { device: deviceLabel() },
|
|
11483
11621
|
verbose: opts.verbose,
|
|
11484
11622
|
cmd: "login"
|
|
11485
11623
|
});
|
|
11486
11624
|
if (!result.ok) {
|
|
11487
11625
|
return { ok: false, error: result.error };
|
|
11488
11626
|
}
|
|
11489
|
-
const { token, service_url, user_id, user, repo_count } = result.data;
|
|
11627
|
+
const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
|
|
11490
11628
|
const loginUserId = user_id ?? user?.id ?? void 0;
|
|
11491
11629
|
try {
|
|
11492
11630
|
await upsertGlobalCredential("", {
|
|
@@ -11510,15 +11648,16 @@ async function loginOnce(opts) {
|
|
|
11510
11648
|
email: user?.email,
|
|
11511
11649
|
userId: loginUserId,
|
|
11512
11650
|
repoCount: repo_count ?? 0,
|
|
11513
|
-
prunedCredentials: pruned
|
|
11651
|
+
prunedCredentials: pruned,
|
|
11652
|
+
expiresAt: expires_at
|
|
11514
11653
|
}
|
|
11515
11654
|
};
|
|
11516
11655
|
}
|
|
11517
11656
|
|
|
11518
11657
|
// src/commands/auth.ts
|
|
11519
11658
|
function registerAuthCommands(program2) {
|
|
11520
|
-
const
|
|
11521
|
-
|
|
11659
|
+
const auth2 = program2.command("auth").description("Manage project authentication");
|
|
11660
|
+
auth2.command("register").description("Register a project with Verity").requiredOption("--project <name>", "Project name").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
|
|
11522
11661
|
const globals = program2.opts();
|
|
11523
11662
|
const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
|
|
11524
11663
|
let remote = opts.remote;
|
|
@@ -11545,7 +11684,7 @@ function registerAuthCommands(program2) {
|
|
|
11545
11684
|
if (email) printInfo(`Authenticated as: ${email}`);
|
|
11546
11685
|
printJson({ project_id: projectId, service_url: resolvedUrl });
|
|
11547
11686
|
});
|
|
11548
|
-
|
|
11687
|
+
auth2.command("verify").description("Verify the current token is valid").action(async () => {
|
|
11549
11688
|
const globals = program2.opts();
|
|
11550
11689
|
const tokenResult = await resolveToken(globals.token);
|
|
11551
11690
|
if (!tokenResult.ok) {
|
|
@@ -11571,7 +11710,7 @@ function registerAuthCommands(program2) {
|
|
|
11571
11710
|
printInfo(`Token valid. Project: ${result.data.project_name}`);
|
|
11572
11711
|
printJson(result.data);
|
|
11573
11712
|
});
|
|
11574
|
-
|
|
11713
|
+
auth2.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
|
|
11575
11714
|
const globals = program2.opts();
|
|
11576
11715
|
let remote = opts.remote;
|
|
11577
11716
|
if (!remote) {
|
|
@@ -11602,16 +11741,81 @@ function registerAuthCommands(program2) {
|
|
|
11602
11741
|
});
|
|
11603
11742
|
}
|
|
11604
11743
|
|
|
11744
|
+
// src/lib/login-report.ts
|
|
11745
|
+
async function reportLoginOutcome(out, opts = {}) {
|
|
11746
|
+
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11747
|
+
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11748
|
+
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11749
|
+
if (out.expiresAt) {
|
|
11750
|
+
printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
|
|
11751
|
+
printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
|
|
11752
|
+
}
|
|
11753
|
+
if (out.repoCount > 0) {
|
|
11754
|
+
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11755
|
+
}
|
|
11756
|
+
if (out.prunedCredentials > 0) {
|
|
11757
|
+
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, opts.verbose);
|
|
11758
|
+
} else if (out.prunedCredentials < 0) {
|
|
11759
|
+
printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
|
|
11760
|
+
printWarn(" will keep taking precedence over this login in their own repositories.");
|
|
11761
|
+
printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
|
|
11762
|
+
}
|
|
11763
|
+
if (out.repoCount === 0) {
|
|
11764
|
+
printWarn("The Verity GitHub App is not installed on any account you can access.");
|
|
11765
|
+
printInfo(" Install it (and grant your repositories), then re-run verity login:");
|
|
11766
|
+
printInfo(` ${githubAppInstallUrl(null)}`);
|
|
11767
|
+
return;
|
|
11768
|
+
}
|
|
11769
|
+
const remote = opts.remote;
|
|
11770
|
+
if (!remote) return;
|
|
11771
|
+
const who = await whoami(out.token, out.serviceUrl, opts.verbose);
|
|
11772
|
+
if (who.ok && who.data.grant_status != null) {
|
|
11773
|
+
printInfo(" \u2713 This repository is covered.");
|
|
11774
|
+
} else if (!who.ok) {
|
|
11775
|
+
printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
|
|
11776
|
+
} else {
|
|
11777
|
+
const parsed = parseRemote(remote);
|
|
11778
|
+
const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
|
|
11779
|
+
printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
|
|
11780
|
+
printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
|
|
11781
|
+
printInfo(` ${installUrl}`);
|
|
11782
|
+
}
|
|
11783
|
+
const rec = await readGlobalCredential(remote);
|
|
11784
|
+
if (rec && rec.token !== out.token) {
|
|
11785
|
+
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11786
|
+
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11787
|
+
if (otherBackend) {
|
|
11788
|
+
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11789
|
+
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11790
|
+
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11791
|
+
printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
|
|
11792
|
+
printWarn(" the full GitHub flow every time.");
|
|
11793
|
+
} else if (otherIdentity) {
|
|
11794
|
+
printWarn(" Note: this repository uses a different account's credential, which takes");
|
|
11795
|
+
printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
|
|
11796
|
+
printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
|
|
11797
|
+
printWarn(" only if you want this repository on the login you just completed.");
|
|
11798
|
+
} else {
|
|
11799
|
+
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11800
|
+
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11801
|
+
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11802
|
+
printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
|
|
11803
|
+
printWarn(" every time.");
|
|
11804
|
+
}
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11807
|
+
|
|
11605
11808
|
// src/commands/login.ts
|
|
11606
11809
|
function registerLoginCommand(program2) {
|
|
11607
11810
|
program2.command("login").description("Log in to Verity (one GitHub login grants access to all your repositories)").option("--force", "Re-authenticate even if already logged in").action(async (opts) => {
|
|
11608
11811
|
const globals = program2.opts();
|
|
11609
|
-
const
|
|
11610
|
-
if (
|
|
11611
|
-
|
|
11612
|
-
|
|
11812
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11813
|
+
if (resolution.source === "default") {
|
|
11814
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
11815
|
+
} else {
|
|
11816
|
+
printVerbose(`Service URL from ${resolution.source}: ${resolution.url}`, globals.verbose);
|
|
11613
11817
|
}
|
|
11614
|
-
const heal = await maybeHealServiceUrl(
|
|
11818
|
+
const heal = await maybeHealServiceUrl(resolution, globals.verbose);
|
|
11615
11819
|
const serviceUrl = heal.serviceUrl;
|
|
11616
11820
|
if (heal.healed) {
|
|
11617
11821
|
printInfo(" Completing login updates ~/.verity/credentials against the live service.");
|
|
@@ -11627,13 +11831,19 @@ function registerLoginCommand(program2) {
|
|
|
11627
11831
|
const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
|
|
11628
11832
|
if (who.ok && who.data.logged_in) {
|
|
11629
11833
|
const nudge = reverifyNudge(who.data);
|
|
11630
|
-
|
|
11834
|
+
const upgrade = await shouldUpgradeOnLogin(existing.data);
|
|
11835
|
+
if (!nudge && !upgrade) {
|
|
11631
11836
|
printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
|
|
11632
11837
|
printInfo(" Re-authenticate with: verity login --force");
|
|
11633
11838
|
return;
|
|
11634
11839
|
}
|
|
11635
|
-
|
|
11636
|
-
|
|
11840
|
+
if (nudge) {
|
|
11841
|
+
printWarn(nudge);
|
|
11842
|
+
printInfo("Re-verifying your repository access\u2026");
|
|
11843
|
+
} else {
|
|
11844
|
+
printInfo("You are signed in with a per-repository token (the old format).");
|
|
11845
|
+
printInfo(" Upgrading to a single login that covers every repository you can write to\u2026");
|
|
11846
|
+
}
|
|
11637
11847
|
} else if (who.ok && who.data.anonymous) {
|
|
11638
11848
|
printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
|
|
11639
11849
|
} else if (!who.ok) {
|
|
@@ -11646,51 +11856,7 @@ function registerLoginCommand(program2) {
|
|
|
11646
11856
|
printError(`Login failed: ${result.error}`);
|
|
11647
11857
|
process.exit(1);
|
|
11648
11858
|
}
|
|
11649
|
-
|
|
11650
|
-
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11651
|
-
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11652
|
-
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11653
|
-
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11654
|
-
if (out.prunedCredentials > 0) {
|
|
11655
|
-
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
|
|
11656
|
-
} else if (out.prunedCredentials < 0) {
|
|
11657
|
-
printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
|
|
11658
|
-
printWarn(" will keep taking precedence over this login in their own repositories.");
|
|
11659
|
-
printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
|
|
11660
|
-
}
|
|
11661
|
-
if (out.repoCount === 0) {
|
|
11662
|
-
printWarn("The Verity GitHub App is not installed on any account you can access.");
|
|
11663
|
-
printInfo(` Install it (and grant your repositories), then re-run verity login:`);
|
|
11664
|
-
printInfo(` ${githubAppInstallUrl(null)}`);
|
|
11665
|
-
return;
|
|
11666
|
-
}
|
|
11667
|
-
if (remote) {
|
|
11668
|
-
const who = await whoami(out.token, out.serviceUrl, globals.verbose);
|
|
11669
|
-
if (who.ok && who.data.grant_status != null) {
|
|
11670
|
-
printInfo(" \u2713 This repository is covered.");
|
|
11671
|
-
} else if (!who.ok) {
|
|
11672
|
-
printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
|
|
11673
|
-
} else {
|
|
11674
|
-
const parsed = parseRemote(remote);
|
|
11675
|
-
const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
|
|
11676
|
-
printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
|
|
11677
|
-
printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
|
|
11678
|
-
printInfo(` ${installUrl}`);
|
|
11679
|
-
}
|
|
11680
|
-
const rec = await readGlobalCredential(remote);
|
|
11681
|
-
if (rec && rec.token !== out.token) {
|
|
11682
|
-
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11683
|
-
if (otherBackend) {
|
|
11684
|
-
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11685
|
-
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11686
|
-
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11687
|
-
} else {
|
|
11688
|
-
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11689
|
-
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11690
|
-
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11691
|
-
}
|
|
11692
|
-
}
|
|
11693
|
-
}
|
|
11859
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
|
|
11694
11860
|
});
|
|
11695
11861
|
}
|
|
11696
11862
|
|
|
@@ -11809,6 +11975,205 @@ function registerTokenCommand(program2) {
|
|
|
11809
11975
|
});
|
|
11810
11976
|
}
|
|
11811
11977
|
|
|
11978
|
+
// src/commands/sessions.ts
|
|
11979
|
+
function shortDate(iso) {
|
|
11980
|
+
return iso ? iso.slice(0, 10) : "\u2014";
|
|
11981
|
+
}
|
|
11982
|
+
function daysUntil(iso) {
|
|
11983
|
+
if (!iso) return null;
|
|
11984
|
+
const ms = Date.parse(iso);
|
|
11985
|
+
if (Number.isNaN(ms)) return null;
|
|
11986
|
+
return Math.round((ms - Date.now()) / 864e5);
|
|
11987
|
+
}
|
|
11988
|
+
async function auth(globals) {
|
|
11989
|
+
const tokenResult = await resolveToken(globals.token);
|
|
11990
|
+
if (!tokenResult.ok) {
|
|
11991
|
+
printError(tokenResult.error);
|
|
11992
|
+
process.exit(1);
|
|
11993
|
+
}
|
|
11994
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11995
|
+
return {
|
|
11996
|
+
token: tokenResult.data.token,
|
|
11997
|
+
serviceUrl: resolution.url,
|
|
11998
|
+
keyed: tokenResult.data.keyed === true
|
|
11999
|
+
};
|
|
12000
|
+
}
|
|
12001
|
+
function explain(error) {
|
|
12002
|
+
if (error.startsWith("FORBIDDEN")) {
|
|
12003
|
+
printInfo(' Sessions belong to a logged-in account \u2014 run "verity login" first.');
|
|
12004
|
+
} else if (error.startsWith("INVALID_TOKEN")) {
|
|
12005
|
+
printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
|
|
12006
|
+
}
|
|
12007
|
+
}
|
|
12008
|
+
async function logoutLocallyOnly(keyed) {
|
|
12009
|
+
printWarn("Could not reach the Verity service to revoke this login (see above).");
|
|
12010
|
+
const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
12011
|
+
if (cleared) {
|
|
12012
|
+
printInfo(keyed ? "This repository\u2019s Verity credential cleared \u2014 it is signed out. \u2713" : "Local login credential cleared \u2014 this machine is signed out. \u2713");
|
|
12013
|
+
printInfo(" The session may still exist server-side; revoke it from another machine with");
|
|
12014
|
+
printInfo(' "verity sessions revoke <session-id>" if that matters.');
|
|
12015
|
+
} else {
|
|
12016
|
+
printInfo("No local credential to clear \u2014 already signed out here.");
|
|
12017
|
+
}
|
|
12018
|
+
}
|
|
12019
|
+
function isPinRefusal(error) {
|
|
12020
|
+
return error.startsWith("TOKEN_PIN_REFUSED");
|
|
12021
|
+
}
|
|
12022
|
+
function registerSessionsCommands(program2) {
|
|
12023
|
+
const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
|
|
12024
|
+
sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
|
|
12025
|
+
const globals = program2.opts();
|
|
12026
|
+
const { token, serviceUrl } = await auth(globals);
|
|
12027
|
+
const result = await apiRequest({
|
|
12028
|
+
method: "GET",
|
|
12029
|
+
path: "/auth/sessions",
|
|
12030
|
+
serviceUrl,
|
|
12031
|
+
token,
|
|
12032
|
+
verbose: globals.verbose,
|
|
12033
|
+
cmd: "sessions"
|
|
12034
|
+
});
|
|
12035
|
+
if (!result.ok) {
|
|
12036
|
+
printError(result.error);
|
|
12037
|
+
explain(result.error);
|
|
12038
|
+
process.exit(1);
|
|
12039
|
+
}
|
|
12040
|
+
if (opts.json) {
|
|
12041
|
+
printJson(result.data);
|
|
12042
|
+
return;
|
|
12043
|
+
}
|
|
12044
|
+
const list = result.data.sessions;
|
|
12045
|
+
if (list.length === 0) {
|
|
12046
|
+
printInfo('No active logins. (Run "verity login".)');
|
|
12047
|
+
return;
|
|
12048
|
+
}
|
|
12049
|
+
printInfo(`${list.length} active login${list.length === 1 ? "" : "s"}:`);
|
|
12050
|
+
printInfo("");
|
|
12051
|
+
printInfo(`${"SESSION ID".padEnd(38)}${"DEVICE".padEnd(24)}${"CREATED".padEnd(12)}${"LAST USED".padEnd(12)}EXPIRES`);
|
|
12052
|
+
for (const s of list) {
|
|
12053
|
+
const days = daysUntil(s.expires_at);
|
|
12054
|
+
const expiry = s.expires_at ? `${shortDate(s.expires_at)}${days != null ? ` (${days}d)` : ""}` : "never";
|
|
12055
|
+
const device = (s.device ?? "login").slice(0, 22);
|
|
12056
|
+
printInfo(
|
|
12057
|
+
`${s.id.padEnd(38)}${device.padEnd(24)}${shortDate(s.created_at).padEnd(12)}${shortDate(s.last_used_at).padEnd(12)}${expiry}${s.current ? " \u2190 this machine" : ""}`
|
|
12058
|
+
);
|
|
12059
|
+
}
|
|
12060
|
+
printInfo("");
|
|
12061
|
+
printInfo("Revoke one: verity sessions revoke <session-id>");
|
|
12062
|
+
printInfo("Sign out everywhere else: verity logout --others");
|
|
12063
|
+
});
|
|
12064
|
+
sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
|
|
12065
|
+
const globals = program2.opts();
|
|
12066
|
+
const { token, serviceUrl, keyed } = await auth(globals);
|
|
12067
|
+
const result = await apiRequest({
|
|
12068
|
+
method: "DELETE",
|
|
12069
|
+
path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
|
|
12070
|
+
serviceUrl,
|
|
12071
|
+
token,
|
|
12072
|
+
verbose: globals.verbose,
|
|
12073
|
+
cmd: "sessions-revoke"
|
|
12074
|
+
});
|
|
12075
|
+
if (!result.ok) {
|
|
12076
|
+
printError(result.error);
|
|
12077
|
+
if (result.http_status === 404) {
|
|
12078
|
+
printInfo(' No session with that id on your account \u2014 check "verity sessions list".');
|
|
12079
|
+
}
|
|
12080
|
+
explain(result.error);
|
|
12081
|
+
process.exit(1);
|
|
12082
|
+
}
|
|
12083
|
+
printInfo(`Session ${sessionId} revoked. \u2713`);
|
|
12084
|
+
if (result.data.was_current) {
|
|
12085
|
+
const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
12086
|
+
printInfo(cleared ? ' That was this machine \u2014 local credential cleared. Run "verity login" to sign back in.' : ' That was this machine. Run "verity login" to sign back in.');
|
|
12087
|
+
}
|
|
12088
|
+
});
|
|
12089
|
+
}
|
|
12090
|
+
function registerLogoutCommand(program2) {
|
|
12091
|
+
program2.command("logout").description("Sign out of Verity on this machine (--all / --others for every machine)").option("--all", "Revoke every login on every machine, including this one").option("--others", "Revoke every login EXCEPT this machine (e.g. a lost laptop)").action(async (opts) => {
|
|
12092
|
+
const globals = program2.opts();
|
|
12093
|
+
if (opts.all && opts.others) {
|
|
12094
|
+
printError("Use either --all or --others, not both.");
|
|
12095
|
+
process.exit(1);
|
|
12096
|
+
}
|
|
12097
|
+
const { token, serviceUrl, keyed } = await auth(globals);
|
|
12098
|
+
if (opts.all || opts.others) {
|
|
12099
|
+
const result = await apiRequest({
|
|
12100
|
+
method: "DELETE",
|
|
12101
|
+
path: opts.others ? "/auth/sessions?others=true" : "/auth/sessions",
|
|
12102
|
+
serviceUrl,
|
|
12103
|
+
token,
|
|
12104
|
+
verbose: globals.verbose,
|
|
12105
|
+
cmd: "logout"
|
|
12106
|
+
});
|
|
12107
|
+
if (!result.ok) {
|
|
12108
|
+
if (isPinRefusal(result.error) && !opts.others) {
|
|
12109
|
+
await logoutLocallyOnly(keyed);
|
|
12110
|
+
return;
|
|
12111
|
+
}
|
|
12112
|
+
printError(result.error);
|
|
12113
|
+
explain(result.error);
|
|
12114
|
+
process.exit(1);
|
|
12115
|
+
}
|
|
12116
|
+
const n = result.data.revoked;
|
|
12117
|
+
printInfo(`Revoked ${n} login${n === 1 ? "" : "s"}. \u2713`);
|
|
12118
|
+
if (opts.others) {
|
|
12119
|
+
printInfo(" This machine is still signed in.");
|
|
12120
|
+
return;
|
|
12121
|
+
}
|
|
12122
|
+
const cleared2 = await removeGlobalCredential("");
|
|
12123
|
+
if (cleared2) printInfo(" Local credential cleared.");
|
|
12124
|
+
printInfo(' Run "verity login" to sign back in.');
|
|
12125
|
+
return;
|
|
12126
|
+
}
|
|
12127
|
+
const list = await apiRequest({
|
|
12128
|
+
method: "GET",
|
|
12129
|
+
path: "/auth/sessions",
|
|
12130
|
+
serviceUrl,
|
|
12131
|
+
token,
|
|
12132
|
+
verbose: globals.verbose,
|
|
12133
|
+
cmd: "logout"
|
|
12134
|
+
});
|
|
12135
|
+
if (!list.ok) {
|
|
12136
|
+
if (isPinRefusal(list.error)) {
|
|
12137
|
+
await logoutLocallyOnly(keyed);
|
|
12138
|
+
return;
|
|
12139
|
+
}
|
|
12140
|
+
printError(list.error);
|
|
12141
|
+
explain(list.error);
|
|
12142
|
+
process.exit(1);
|
|
12143
|
+
}
|
|
12144
|
+
const current = list.data.sessions.find((s) => s.current);
|
|
12145
|
+
if (!current) {
|
|
12146
|
+
printWarn(keyed ? "This repository uses its own Verity credential, not a machine login." : "This machine is not signed in with a Verity login.");
|
|
12147
|
+
const cleared2 = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
12148
|
+
if (cleared2) {
|
|
12149
|
+
printInfo(keyed ? " Cleared this repository\u2019s credential; your machine login is untouched." : " Cleared the local login credential anyway.");
|
|
12150
|
+
}
|
|
12151
|
+
return;
|
|
12152
|
+
}
|
|
12153
|
+
const revoked = await apiRequest({
|
|
12154
|
+
method: "DELETE",
|
|
12155
|
+
path: `/auth/sessions/${current.id}`,
|
|
12156
|
+
serviceUrl,
|
|
12157
|
+
token,
|
|
12158
|
+
verbose: globals.verbose,
|
|
12159
|
+
cmd: "logout"
|
|
12160
|
+
});
|
|
12161
|
+
if (!revoked.ok) {
|
|
12162
|
+
if (isPinRefusal(revoked.error)) {
|
|
12163
|
+
await logoutLocallyOnly(keyed);
|
|
12164
|
+
return;
|
|
12165
|
+
}
|
|
12166
|
+
printError(revoked.error);
|
|
12167
|
+
explain(revoked.error);
|
|
12168
|
+
process.exit(1);
|
|
12169
|
+
}
|
|
12170
|
+
const cleared = await removeGlobalCredential("");
|
|
12171
|
+
printInfo("Signed out on this machine. \u2713");
|
|
12172
|
+
if (cleared) printInfo(" Local credential cleared.");
|
|
12173
|
+
printInfo(' Your other machines are unaffected \u2014 use "verity logout --all" for all of them.');
|
|
12174
|
+
});
|
|
12175
|
+
}
|
|
12176
|
+
|
|
11812
12177
|
// src/lib/hooks.ts
|
|
11813
12178
|
var import_promises4 = require("node:fs/promises");
|
|
11814
12179
|
var import_node_path5 = require("node:path");
|
|
@@ -12430,7 +12795,7 @@ function getRecentCommitMessages() {
|
|
|
12430
12795
|
// src/lib/context-identity.ts
|
|
12431
12796
|
var import_node_crypto2 = require("node:crypto");
|
|
12432
12797
|
var import_node_fs5 = require("node:fs");
|
|
12433
|
-
var
|
|
12798
|
+
var import_node_os2 = require("node:os");
|
|
12434
12799
|
var import_node_path6 = require("node:path");
|
|
12435
12800
|
var SHARED_SENTINELS = /* @__PURE__ */ new Set([
|
|
12436
12801
|
"",
|
|
@@ -12485,7 +12850,7 @@ function contextIdentity(input) {
|
|
|
12485
12850
|
}
|
|
12486
12851
|
function verityHome() {
|
|
12487
12852
|
const override = process.env.VERITY_HOME;
|
|
12488
|
-
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0,
|
|
12853
|
+
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".verity");
|
|
12489
12854
|
}
|
|
12490
12855
|
function dossierDir(identity) {
|
|
12491
12856
|
return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
|
|
@@ -13331,6 +13696,143 @@ var import_node_fs10 = require("node:fs");
|
|
|
13331
13696
|
var import_node_crypto5 = require("node:crypto");
|
|
13332
13697
|
var import_node_path11 = require("node:path");
|
|
13333
13698
|
|
|
13699
|
+
// src/lib/skip-detection.ts
|
|
13700
|
+
function isBareAckPrompt(prompt) {
|
|
13701
|
+
if (typeof prompt !== "string") return false;
|
|
13702
|
+
const trimmed = prompt.trim();
|
|
13703
|
+
if (trimmed.length === 0) return false;
|
|
13704
|
+
if (trimmed.length > 20) return false;
|
|
13705
|
+
const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
|
|
13706
|
+
return bareAckPattern.test(trimmed);
|
|
13707
|
+
}
|
|
13708
|
+
function isContinuationPrompt(prompt) {
|
|
13709
|
+
if (typeof prompt !== "string") return false;
|
|
13710
|
+
const trimmed = prompt.trim();
|
|
13711
|
+
if (trimmed.length === 0) return false;
|
|
13712
|
+
if (trimmed.length > 24) return false;
|
|
13713
|
+
const continuation = /^(let['’]?s\s+(go|do\s+it|start|continue)|go|go\s+ahead|go\s+on|proceed|continue|carry\s+on|keep\s+going|do\s+it|make\s+it\s+so|next|start|begin|ship\s+it|yes\s+please|please\s+continue|perfect|great|nice|excellent|agreed)[.!]*$/i;
|
|
13714
|
+
return continuation.test(trimmed) || isBareAckPrompt(trimmed);
|
|
13715
|
+
}
|
|
13716
|
+
function resolveGoalPrompt(prompts) {
|
|
13717
|
+
if (prompts.length === 0) return null;
|
|
13718
|
+
const latest = prompts[prompts.length - 1];
|
|
13719
|
+
if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
|
|
13720
|
+
for (let i = prompts.length - 2; i >= 0; i--) {
|
|
13721
|
+
if (!isContinuationPrompt(prompts[i].prompt)) {
|
|
13722
|
+
return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
|
|
13723
|
+
}
|
|
13724
|
+
}
|
|
13725
|
+
return { entry: latest, turnsBack: 0 };
|
|
13726
|
+
}
|
|
13727
|
+
function isReflectionQuestion(response) {
|
|
13728
|
+
if (!response || typeof response !== "string") return false;
|
|
13729
|
+
const markers = [
|
|
13730
|
+
/reflection\s+for\s+future\s+agents/i,
|
|
13731
|
+
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
13732
|
+
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
13733
|
+
/quick\s+reflection\s+question/i,
|
|
13734
|
+
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
13735
|
+
// interactive, asks the user to confirm/correct before recording. That
|
|
13736
|
+
// turn authors no code either, so it's still a reflection turn.
|
|
13737
|
+
/reflection\s+draft/i,
|
|
13738
|
+
/confirm,?\s+correct,?\s+or\s+add/i
|
|
13739
|
+
];
|
|
13740
|
+
return markers.some((m) => m.test(response));
|
|
13741
|
+
}
|
|
13742
|
+
function isMetaTaskLabel(label2) {
|
|
13743
|
+
if (label2 === null || label2 === void 0) return false;
|
|
13744
|
+
if (typeof label2 !== "string") return false;
|
|
13745
|
+
const trimmed = label2.trim();
|
|
13746
|
+
if (trimmed.length === 0) return true;
|
|
13747
|
+
const metaPatterns = [
|
|
13748
|
+
/^verity\s+[\w-]+\s+response$/i,
|
|
13749
|
+
// "Verity reflect response"
|
|
13750
|
+
/^simple user response$/i,
|
|
13751
|
+
/^verity\s+command$/i,
|
|
13752
|
+
// "Verity command"
|
|
13753
|
+
/^user\s+(question|reply|response|ack)$/i
|
|
13754
|
+
];
|
|
13755
|
+
return metaPatterns.some((p) => p.test(trimmed));
|
|
13756
|
+
}
|
|
13757
|
+
function shouldSkipForBareAck(input) {
|
|
13758
|
+
if (!isBareAckPrompt(input.prompt)) return false;
|
|
13759
|
+
if (input.turnAuthoredCode) return false;
|
|
13760
|
+
return input.canSeeTurnAuthorship;
|
|
13761
|
+
}
|
|
13762
|
+
|
|
13763
|
+
// src/lib/pending-repeat.ts
|
|
13764
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
13765
|
+
"the",
|
|
13766
|
+
"and",
|
|
13767
|
+
"that",
|
|
13768
|
+
"this",
|
|
13769
|
+
"with",
|
|
13770
|
+
"from",
|
|
13771
|
+
"have",
|
|
13772
|
+
"been",
|
|
13773
|
+
"were",
|
|
13774
|
+
"what",
|
|
13775
|
+
"when",
|
|
13776
|
+
"which",
|
|
13777
|
+
"their",
|
|
13778
|
+
"there",
|
|
13779
|
+
"these",
|
|
13780
|
+
"those",
|
|
13781
|
+
"would",
|
|
13782
|
+
"could",
|
|
13783
|
+
"should",
|
|
13784
|
+
"must",
|
|
13785
|
+
"will",
|
|
13786
|
+
"also",
|
|
13787
|
+
"just",
|
|
13788
|
+
"only",
|
|
13789
|
+
"into",
|
|
13790
|
+
"over",
|
|
13791
|
+
"than",
|
|
13792
|
+
"then",
|
|
13793
|
+
"them",
|
|
13794
|
+
"some",
|
|
13795
|
+
"such",
|
|
13796
|
+
"more",
|
|
13797
|
+
"most",
|
|
13798
|
+
"other",
|
|
13799
|
+
"about",
|
|
13800
|
+
"after",
|
|
13801
|
+
"before",
|
|
13802
|
+
"since",
|
|
13803
|
+
"because",
|
|
13804
|
+
"while",
|
|
13805
|
+
"where",
|
|
13806
|
+
"whether",
|
|
13807
|
+
"ensure",
|
|
13808
|
+
"confirm",
|
|
13809
|
+
"verify",
|
|
13810
|
+
"check"
|
|
13811
|
+
]);
|
|
13812
|
+
function pendingTokens(text) {
|
|
13813
|
+
if (!text || typeof text !== "string") return [];
|
|
13814
|
+
const out = /* @__PURE__ */ new Set();
|
|
13815
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
13816
|
+
if (raw.length <= 3) continue;
|
|
13817
|
+
if (STOP.has(raw)) continue;
|
|
13818
|
+
out.add(raw);
|
|
13819
|
+
}
|
|
13820
|
+
return [...out].sort();
|
|
13821
|
+
}
|
|
13822
|
+
var REPEAT_THRESHOLD = 0.3;
|
|
13823
|
+
function overlapCoefficient(a, b) {
|
|
13824
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
13825
|
+
const setB = new Set(b);
|
|
13826
|
+
let shared = 0;
|
|
13827
|
+
for (const t of a) if (setB.has(t)) shared++;
|
|
13828
|
+
return shared / Math.min(a.length, b.length);
|
|
13829
|
+
}
|
|
13830
|
+
function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
|
|
13831
|
+
const tokens = pendingTokens(text);
|
|
13832
|
+
if (tokens.length === 0) return false;
|
|
13833
|
+
return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
|
|
13834
|
+
}
|
|
13835
|
+
|
|
13334
13836
|
// src/lib/dossier.ts
|
|
13335
13837
|
var import_node_fs9 = require("node:fs");
|
|
13336
13838
|
var import_node_crypto4 = require("node:crypto");
|
|
@@ -13339,6 +13841,7 @@ var MAX_LINE_BYTES = 4096;
|
|
|
13339
13841
|
var MAX_GOAL_CHARS = 2e3;
|
|
13340
13842
|
var GOAL_KEEP = 8;
|
|
13341
13843
|
var GOAL_TOTAL_CAP = 32;
|
|
13844
|
+
var RECENT_PENDING_CAP = 20;
|
|
13342
13845
|
var HASH_WIDTH = 16;
|
|
13343
13846
|
var AUTHORED_CAP = 300;
|
|
13344
13847
|
var NOT_MINE_CAP = 300;
|
|
@@ -13579,6 +14082,13 @@ function reduce(state, events, now) {
|
|
|
13579
14082
|
});
|
|
13580
14083
|
break;
|
|
13581
14084
|
}
|
|
14085
|
+
case "goal_delivered": {
|
|
14086
|
+
const g = state.goal.find((x) => x.status === "active");
|
|
14087
|
+
if (!g) break;
|
|
14088
|
+
if (g.delivered) break;
|
|
14089
|
+
g.delivered = { at: ev.at, seq: ev.seq, summary: ev.summary };
|
|
14090
|
+
break;
|
|
14091
|
+
}
|
|
13582
14092
|
case "authored": {
|
|
13583
14093
|
authoredEvents++;
|
|
13584
14094
|
const e = byPath.get(ev.path) ?? {
|
|
@@ -13693,6 +14203,22 @@ function reduce(state, events, now) {
|
|
|
13693
14203
|
}
|
|
13694
14204
|
case "verdict": {
|
|
13695
14205
|
state.meta.last_verdict_seq = ev.seq;
|
|
14206
|
+
state.meta.channel = {
|
|
14207
|
+
emittedLast: ev.emitted === true,
|
|
14208
|
+
// Reset by ANY movement, so the counter measures a standstill rather
|
|
14209
|
+
// than session length.
|
|
14210
|
+
consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
|
|
14211
|
+
};
|
|
14212
|
+
state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
|
|
14213
|
+
if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
|
|
14214
|
+
const prior = state.meta.recent_pending_sigs ?? [];
|
|
14215
|
+
state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
|
|
14216
|
+
}
|
|
14217
|
+
if (ev.intent_sig) {
|
|
14218
|
+
state.meta.intent_repeat = state.meta.intent_repeat && state.meta.intent_repeat.sig === ev.intent_sig ? { sig: ev.intent_sig, consecutive: state.meta.intent_repeat.consecutive + 1 } : { sig: ev.intent_sig, consecutive: 1 };
|
|
14219
|
+
} else {
|
|
14220
|
+
state.meta.intent_repeat = void 0;
|
|
14221
|
+
}
|
|
13696
14222
|
state.meta.watermark = {
|
|
13697
14223
|
sha: ev.head_sha,
|
|
13698
14224
|
reviewed_hash: ev.watermark_sha,
|
|
@@ -13778,7 +14304,12 @@ function compactState(s) {
|
|
|
13778
14304
|
const ms = (iso) => Date.parse(iso) || 0;
|
|
13779
14305
|
return {
|
|
13780
14306
|
v: 1,
|
|
13781
|
-
|
|
14307
|
+
// ⚠ APPEND-ONLY POSITIONALLY. Indices 11-13 carry `delivered`; a cache row
|
|
14308
|
+
// written before it existed has length 11 and expands with `delivered`
|
|
14309
|
+
// absent, which is the correct reading of "nothing had been delivered yet".
|
|
14310
|
+
// Inserting rather than appending would silently re-interpret every existing
|
|
14311
|
+
// cached row.
|
|
14312
|
+
g: s.goal.map((g) => [g.seq, ms(g.at), g.hash, g.superseded_by, g.text ?? 0, g.text_len ?? 0, g.source ?? 0, g.status ?? 0, g.repeats ?? 0, g.truncated ? 1 : 0, g.collapsed ? 1 : 0, g.delivered ? ms(g.delivered.at) : 0, g.delivered?.seq ?? 0, g.delivered?.summary ?? 0]),
|
|
13782
14313
|
a: s.authored?.map((a) => [a.path, a.origin, a.edits, a.hunks, a.adds, a.dels, a.hash_now, a.hash_at_last_verdict, a.first_seq, a.last_seq]) ?? null,
|
|
13783
14314
|
n: s.not_mine?.map((n) => [n.path, n.reason, ms(n.at), n.head_sha]) ?? null,
|
|
13784
14315
|
t: s.statements.map((x) => [x.anchor_key, x.file, x.line, x.pattern_id, x.title_hash, x.register, x.line_sha, x.said_at_seq, ms(x.said_at), x.outcome, x.outcome_at ? ms(x.outcome_at) : 0, x.repeats, x.carried ? 1 : 0]),
|
|
@@ -13824,7 +14355,14 @@ function expandState(raw) {
|
|
|
13824
14355
|
...x[7] ? { status: x[7] } : {},
|
|
13825
14356
|
...x[8] ? { repeats: x[8] } : {},
|
|
13826
14357
|
...x[9] ? { truncated: true } : {},
|
|
13827
|
-
...x[10] ? { collapsed: true } : {}
|
|
14358
|
+
...x[10] ? { collapsed: true } : {},
|
|
14359
|
+
...x[11] ? {
|
|
14360
|
+
delivered: {
|
|
14361
|
+
at: iso(x[11]),
|
|
14362
|
+
seq: x[12] ?? 0,
|
|
14363
|
+
summary: x[13] || ""
|
|
14364
|
+
}
|
|
14365
|
+
} : {}
|
|
13828
14366
|
})),
|
|
13829
14367
|
authored: decodedAuthored,
|
|
13830
14368
|
// The cache stores the BOUNDED list, so this is the bounded list too. That
|
|
@@ -13900,12 +14438,12 @@ function readFoldCache(d) {
|
|
|
13900
14438
|
if (!(0, import_node_fs9.existsSync)(d.foldPath)) return null;
|
|
13901
14439
|
const raw = JSON.parse((0, import_node_fs9.readFileSync)(d.foldPath, "utf8"));
|
|
13902
14440
|
if (raw?.v !== 1) return null;
|
|
13903
|
-
const
|
|
13904
|
-
if (!
|
|
14441
|
+
const cached2 = expandState(raw);
|
|
14442
|
+
if (!cached2?.meta) return null;
|
|
13905
14443
|
const size = (0, import_node_fs9.existsSync)(d.eventsPath) ? (0, import_node_fs9.statSync)(d.eventsPath).size : 0;
|
|
13906
14444
|
const rotations = (0, import_node_fs9.existsSync)(d.rotatedDir) ? (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).length : 0;
|
|
13907
|
-
if (
|
|
13908
|
-
return
|
|
14445
|
+
if (cached2.meta.upto_offset !== size || cached2.meta.rotations !== rotations) return null;
|
|
14446
|
+
return cached2;
|
|
13909
14447
|
} catch {
|
|
13910
14448
|
return null;
|
|
13911
14449
|
}
|
|
@@ -14038,9 +14576,18 @@ function projectMemory(state, opts) {
|
|
|
14038
14576
|
seq: active.seq,
|
|
14039
14577
|
superseded,
|
|
14040
14578
|
truncated: active.truncated === true,
|
|
14041
|
-
collapsed: state.meta.collapsed.goal ?? 0
|
|
14579
|
+
collapsed: state.meta.collapsed.goal ?? 0,
|
|
14580
|
+
...active.delivered && { delivered: active.delivered }
|
|
14042
14581
|
};
|
|
14043
14582
|
}
|
|
14583
|
+
if (opts.capture) {
|
|
14584
|
+
const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
|
|
14585
|
+
p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
|
|
14586
|
+
}
|
|
14587
|
+
if (state.meta.last_adjudication) {
|
|
14588
|
+
const a = state.meta.last_adjudication;
|
|
14589
|
+
p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
|
|
14590
|
+
}
|
|
14044
14591
|
const ageOf = (seq, carried) => carried ? "carried" : seq > lastVerdictSeq ? "this_turn" : "this_session";
|
|
14045
14592
|
if (opts.spoken.length > 0) {
|
|
14046
14593
|
p.statements = opts.spoken.map((s) => ({
|
|
@@ -14283,7 +14830,8 @@ function recall(d, input) {
|
|
|
14283
14830
|
continuity,
|
|
14284
14831
|
spoken: reanchored.spoken,
|
|
14285
14832
|
refused: reanchored.dropped.length,
|
|
14286
|
-
lastVerdictSeq
|
|
14833
|
+
lastVerdictSeq,
|
|
14834
|
+
...input.capture && { capture: input.capture }
|
|
14287
14835
|
});
|
|
14288
14836
|
return {
|
|
14289
14837
|
state: effective,
|
|
@@ -14394,7 +14942,19 @@ function sessionDossier(token, sessionId) {
|
|
|
14394
14942
|
const d = openDossier(identity);
|
|
14395
14943
|
return d ? { d, identity } : null;
|
|
14396
14944
|
}
|
|
14945
|
+
function hasActiveGoal(d) {
|
|
14946
|
+
try {
|
|
14947
|
+
if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
|
|
14948
|
+
return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
|
|
14949
|
+
} catch {
|
|
14950
|
+
return false;
|
|
14951
|
+
}
|
|
14952
|
+
}
|
|
14397
14953
|
function recordGoal(d, prompt, source = "prompt") {
|
|
14954
|
+
if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
|
|
14955
|
+
appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
|
|
14956
|
+
return;
|
|
14957
|
+
}
|
|
14398
14958
|
const text = prompt.slice(0, MAX_GOAL_CHARS);
|
|
14399
14959
|
appendEvent(d, {
|
|
14400
14960
|
k: "goal",
|
|
@@ -14466,6 +15026,10 @@ function toStatus(n) {
|
|
|
14466
15026
|
}
|
|
14467
15027
|
function recordVerdict(d, v) {
|
|
14468
15028
|
const root = repoRoot();
|
|
15029
|
+
if (v.intent?.verdict === "aligned") {
|
|
15030
|
+
const summary = (v.intent.implemented ?? "").trim().slice(0, 400);
|
|
15031
|
+
if (summary) appendEvent(d, { k: "goal_delivered", summary });
|
|
15032
|
+
}
|
|
14469
15033
|
const lines = /* @__PURE__ */ new Map();
|
|
14470
15034
|
for (const f of v.findings) {
|
|
14471
15035
|
if (!f.file || typeof f.line !== "number" || !f.pattern_id) continue;
|
|
@@ -14493,15 +15057,39 @@ function recordVerdict(d, v) {
|
|
|
14493
15057
|
line_sha: at !== void 0 ? lineSha(at) : null
|
|
14494
15058
|
});
|
|
14495
15059
|
}
|
|
15060
|
+
const foldedNow = foldDossier(d);
|
|
15061
|
+
const sig = intentSignature(v.intent, {
|
|
15062
|
+
goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
|
|
15063
|
+
idle: v.idle !== false
|
|
15064
|
+
});
|
|
14496
15065
|
appendEvent(d, {
|
|
14497
15066
|
k: "verdict",
|
|
14498
15067
|
run_id: v.runId,
|
|
14499
15068
|
head_sha: getCurrentCommit(),
|
|
14500
15069
|
watermark_sha: v.watermarkSha,
|
|
14501
15070
|
branch: v.branch,
|
|
14502
|
-
decision: v.decision
|
|
15071
|
+
decision: v.decision,
|
|
15072
|
+
...sig && { intent_sig: sig },
|
|
15073
|
+
// Fingerprints of the pending items this verdict delivered, so the NEXT turn
|
|
15074
|
+
// can tell a repeat from a new requirement. Only recorded when the channel
|
|
15075
|
+
// actually spoke — a silenced turn delivered nothing, so nothing was "said
|
|
15076
|
+
// before" and labelling the next turn's items as repeats would be a lie.
|
|
15077
|
+
...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
|
|
15078
|
+
pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
|
|
15079
|
+
},
|
|
15080
|
+
emitted: v.emitted === true,
|
|
15081
|
+
idle: v.idle !== false,
|
|
15082
|
+
...v.intent?.verdict && { intent_verdict: v.intent.verdict },
|
|
15083
|
+
...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
|
|
14503
15084
|
});
|
|
14504
15085
|
}
|
|
15086
|
+
function intentSignature(intent, ctx) {
|
|
15087
|
+
if (!intent?.verdict) return null;
|
|
15088
|
+
if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
|
|
15089
|
+
const goal = ctx ? `g${ctx.goalSeq}` : "g?";
|
|
15090
|
+
const moved = ctx?.idle === false ? "active" : "idle";
|
|
15091
|
+
return `${intent.verdict}:${goal}:${moved}`;
|
|
15092
|
+
}
|
|
14505
15093
|
function toRegister(severity) {
|
|
14506
15094
|
switch (severity) {
|
|
14507
15095
|
case "critical":
|
|
@@ -14537,7 +15125,9 @@ function recallMemory(d, identity, opts) {
|
|
|
14537
15125
|
const state = foldDossier(d);
|
|
14538
15126
|
const watermark = state.meta.watermark?.sha ?? null;
|
|
14539
15127
|
const watermarkPaths = (state.authored ?? []).map((a) => a.path);
|
|
15128
|
+
const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
|
|
14540
15129
|
const r = recall(d, {
|
|
15130
|
+
...captureCmp && { capture: captureCmp },
|
|
14541
15131
|
identity,
|
|
14542
15132
|
currentSessionKey: opts.currentSessionKey,
|
|
14543
15133
|
branchNow: getCurrentBranch(),
|
|
@@ -14794,6 +15384,8 @@ function collectCodeDelta(files, opts) {
|
|
|
14794
15384
|
let totalSize = 0;
|
|
14795
15385
|
let truncationReason = null;
|
|
14796
15386
|
const droppedPaths = [];
|
|
15387
|
+
const excluded = [];
|
|
15388
|
+
const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
|
|
14797
15389
|
for (const filepath of sorted) {
|
|
14798
15390
|
if (result.length >= maxFiles) {
|
|
14799
15391
|
truncationReason ??= "max_files";
|
|
@@ -14801,14 +15393,21 @@ function collectCodeDelta(files, opts) {
|
|
|
14801
15393
|
continue;
|
|
14802
15394
|
}
|
|
14803
15395
|
const resolved = resolveFile(filepath);
|
|
14804
|
-
if (!resolved)
|
|
15396
|
+
if (!resolved) {
|
|
15397
|
+
exclude(filepath, "path-not-resolvable");
|
|
15398
|
+
continue;
|
|
15399
|
+
}
|
|
14805
15400
|
let size;
|
|
14806
15401
|
try {
|
|
14807
15402
|
size = (0, import_node_fs11.statSync)(resolved).size;
|
|
14808
15403
|
} catch {
|
|
15404
|
+
exclude(filepath, "not-stattable");
|
|
15405
|
+
continue;
|
|
15406
|
+
}
|
|
15407
|
+
if (size > maxFileBytes) {
|
|
15408
|
+
exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
|
|
14809
15409
|
continue;
|
|
14810
15410
|
}
|
|
14811
|
-
if (size > maxFileBytes) continue;
|
|
14812
15411
|
if (totalSize + size > maxTotalBytes) {
|
|
14813
15412
|
truncationReason ??= "max_total_bytes";
|
|
14814
15413
|
const idx = sorted.indexOf(filepath);
|
|
@@ -14819,6 +15418,7 @@ function collectCodeDelta(files, opts) {
|
|
|
14819
15418
|
try {
|
|
14820
15419
|
content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
|
|
14821
15420
|
} catch {
|
|
15421
|
+
exclude(filepath, "not-readable");
|
|
14822
15422
|
continue;
|
|
14823
15423
|
}
|
|
14824
15424
|
totalSize += size;
|
|
@@ -14832,10 +15432,14 @@ function collectCodeDelta(files, opts) {
|
|
|
14832
15432
|
(sum, f) => sum + f.content.split("\n").length,
|
|
14833
15433
|
0
|
|
14834
15434
|
);
|
|
15435
|
+
for (const path of droppedPaths) {
|
|
15436
|
+
exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
|
|
15437
|
+
}
|
|
14835
15438
|
return {
|
|
14836
15439
|
files: result,
|
|
14837
15440
|
total_lines: totalLines,
|
|
14838
15441
|
total_files: result.length,
|
|
15442
|
+
excluded,
|
|
14839
15443
|
...truncationReason && {
|
|
14840
15444
|
truncated: {
|
|
14841
15445
|
reason: truncationReason,
|
|
@@ -15084,8 +15688,8 @@ function preImage(repoRelPath, baseline) {
|
|
|
15084
15688
|
perBaseline = /* @__PURE__ */ new Map();
|
|
15085
15689
|
preImageCache.set(baseline, perBaseline);
|
|
15086
15690
|
}
|
|
15087
|
-
const
|
|
15088
|
-
if (
|
|
15691
|
+
const cached2 = perBaseline.get(repoRelPath);
|
|
15692
|
+
if (cached2) return cached2;
|
|
15089
15693
|
const resolved = resolvePreImage(repoRelPath, baseline);
|
|
15090
15694
|
perBaseline.set(repoRelPath, resolved);
|
|
15091
15695
|
return resolved;
|
|
@@ -15129,6 +15733,34 @@ ${addedLines}`,
|
|
|
15129
15733
|
}
|
|
15130
15734
|
return { diffs, has_baseline: true };
|
|
15131
15735
|
}
|
|
15736
|
+
function absorbIntoBaseline(paths, sessionId) {
|
|
15737
|
+
const baseline = readBaseline(sessionId);
|
|
15738
|
+
if (!baseline || paths.length === 0) return 0;
|
|
15739
|
+
const dir = sessionDir(sessionKey(baseline.session_id));
|
|
15740
|
+
let adopted = 0;
|
|
15741
|
+
const dirty = new Set(baseline.dirty_paths);
|
|
15742
|
+
for (const p of paths) {
|
|
15743
|
+
try {
|
|
15744
|
+
const content = safeReadForMirror(projectPath(p));
|
|
15745
|
+
if (content === null) continue;
|
|
15746
|
+
const dest = mirrorPath(dir, p);
|
|
15747
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
15748
|
+
(0, import_node_fs13.writeFileSync)(dest, content);
|
|
15749
|
+
dirty.add(p);
|
|
15750
|
+
adopted++;
|
|
15751
|
+
} catch {
|
|
15752
|
+
}
|
|
15753
|
+
}
|
|
15754
|
+
if (adopted === 0) return 0;
|
|
15755
|
+
try {
|
|
15756
|
+
const updated = { ...baseline, dirty_paths: [...dirty] };
|
|
15757
|
+
(0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
|
|
15758
|
+
preImageCache.delete(baseline);
|
|
15759
|
+
} catch {
|
|
15760
|
+
return 0;
|
|
15761
|
+
}
|
|
15762
|
+
return adopted;
|
|
15763
|
+
}
|
|
15132
15764
|
function changedSinceBaseline(repoRelPath, baseline) {
|
|
15133
15765
|
const pre = preImage(repoRelPath, baseline);
|
|
15134
15766
|
let current;
|
|
@@ -16005,40 +16637,40 @@ function narrowToRecent(files, sessionId) {
|
|
|
16005
16637
|
});
|
|
16006
16638
|
return recent.length > 0 ? recent : files;
|
|
16007
16639
|
}
|
|
16008
|
-
function
|
|
16009
|
-
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
|
|
16640
|
+
function readIterationState(currentCommit) {
|
|
16641
|
+
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
|
|
16010
16642
|
try {
|
|
16011
16643
|
const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
|
|
16012
16644
|
const parts = stored.split(":");
|
|
16013
16645
|
const iter = parseInt(parts[0], 10);
|
|
16014
16646
|
const storedCommit = parts[1] ?? "";
|
|
16015
16647
|
const storedTimestamp = parseInt(parts[2] ?? "0", 10);
|
|
16016
|
-
|
|
16017
|
-
if (
|
|
16648
|
+
const fingerprint = parts.slice(3).join(":") || null;
|
|
16649
|
+
if (isNaN(iter)) return { iteration: 1, fingerprint: null };
|
|
16650
|
+
if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
|
|
16018
16651
|
if (storedTimestamp > 0) {
|
|
16019
16652
|
const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
|
|
16020
|
-
if (elapsed > 600) return 1;
|
|
16653
|
+
if (elapsed > 600) return { iteration: 1, fingerprint: null };
|
|
16021
16654
|
}
|
|
16022
|
-
return iter;
|
|
16655
|
+
return { iteration: iter, fingerprint };
|
|
16023
16656
|
} catch {
|
|
16024
|
-
return 1;
|
|
16657
|
+
return { iteration: 1, fingerprint: null };
|
|
16025
16658
|
}
|
|
16026
16659
|
}
|
|
16027
|
-
function
|
|
16028
|
-
const
|
|
16029
|
-
|
|
16030
|
-
writeIteration(1, currentCommit, contentHash);
|
|
16031
|
-
return {
|
|
16032
|
-
skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
|
|
16033
|
-
iteration
|
|
16034
|
-
};
|
|
16035
|
-
}
|
|
16036
|
-
return { skip: null, iteration };
|
|
16660
|
+
function findingsFingerprint(findings) {
|
|
16661
|
+
const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
|
|
16662
|
+
return [...new Set(keys)].sort().join(",");
|
|
16037
16663
|
}
|
|
16038
|
-
function
|
|
16664
|
+
function isSameProblem(previous, current) {
|
|
16665
|
+
if (!previous || !current) return false;
|
|
16666
|
+
const prev = new Set(previous.split(","));
|
|
16667
|
+
return current.split(",").some((k) => prev.has(k));
|
|
16668
|
+
}
|
|
16669
|
+
function writeIteration(iteration, commit, _contentHash, fingerprint) {
|
|
16039
16670
|
(0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
|
|
16040
16671
|
const ts = Math.floor(Date.now() / 1e3);
|
|
16041
|
-
|
|
16672
|
+
const fp = fingerprint ? `:${fingerprint}` : "";
|
|
16673
|
+
(0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
|
|
16042
16674
|
}
|
|
16043
16675
|
|
|
16044
16676
|
// src/lib/static-analysis.ts
|
|
@@ -16287,7 +16919,7 @@ function resolveTaskContext(opts) {
|
|
|
16287
16919
|
// src/lib/cli-version.ts
|
|
16288
16920
|
function cliVersion() {
|
|
16289
16921
|
try {
|
|
16290
|
-
return true ? "0.28.1-experimental.
|
|
16922
|
+
return true ? "0.28.1-experimental.e79117b" : "dev";
|
|
16291
16923
|
} catch {
|
|
16292
16924
|
return "dev";
|
|
16293
16925
|
}
|
|
@@ -16381,10 +17013,26 @@ function cacheRequest(body) {
|
|
|
16381
17013
|
(0, import_node_fs18.mkdirSync)(CACHE_DIR, { recursive: true });
|
|
16382
17014
|
const suffix = (0, import_node_crypto9.randomBytes)(4).toString("hex");
|
|
16383
17015
|
const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
|
|
16384
|
-
(0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
|
|
17016
|
+
(0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
|
|
16385
17017
|
} catch {
|
|
16386
17018
|
}
|
|
16387
17019
|
}
|
|
17020
|
+
function redactRequest(body) {
|
|
17021
|
+
const b = body ?? {};
|
|
17022
|
+
const delta = b.code_delta ?? {};
|
|
17023
|
+
const files = Array.isArray(delta.files) ? delta.files : [];
|
|
17024
|
+
return {
|
|
17025
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17026
|
+
undelivered: true,
|
|
17027
|
+
total_files: files.length,
|
|
17028
|
+
total_bytes: files.reduce(
|
|
17029
|
+
(n, f) => n + (typeof f.content === "string" ? f.content.length : 0),
|
|
17030
|
+
0
|
|
17031
|
+
),
|
|
17032
|
+
// Paths only. A path is already visible in the repository; the content is not.
|
|
17033
|
+
files: files.map((f) => typeof f.path === "string" ? f.path : "<unknown>").slice(0, 100)
|
|
17034
|
+
};
|
|
17035
|
+
}
|
|
16388
17036
|
function buildOfflineFallback(reason, staticResults) {
|
|
16389
17037
|
return {
|
|
16390
17038
|
// G12 / INV-18 — WARN, not PASS.
|
|
@@ -16512,6 +17160,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
|
16512
17160
|
"subagent"
|
|
16513
17161
|
]);
|
|
16514
17162
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
|
|
17163
|
+
var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
|
|
17164
|
+
function hasUserText(record) {
|
|
17165
|
+
const message = record.message;
|
|
17166
|
+
const content = message?.content ?? record.content;
|
|
17167
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
17168
|
+
if (!Array.isArray(content)) return false;
|
|
17169
|
+
return content.some(
|
|
17170
|
+
(b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
|
|
17171
|
+
);
|
|
17172
|
+
}
|
|
16515
17173
|
var COMMAND_CLASSES = [
|
|
16516
17174
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
|
|
16517
17175
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
|
|
@@ -16601,8 +17259,8 @@ function commandShape(cmd) {
|
|
|
16601
17259
|
var COMMAND_HEAD_CHARS = 80;
|
|
16602
17260
|
var rootCandidateCache = /* @__PURE__ */ new Map();
|
|
16603
17261
|
function candidateRoots(repoRoot2) {
|
|
16604
|
-
const
|
|
16605
|
-
if (
|
|
17262
|
+
const cached2 = rootCandidateCache.get(repoRoot2);
|
|
17263
|
+
if (cached2) return cached2;
|
|
16606
17264
|
const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
16607
17265
|
const out = [norm];
|
|
16608
17266
|
try {
|
|
@@ -16638,6 +17296,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16638
17296
|
totalRecords: 0,
|
|
16639
17297
|
malformed: 0,
|
|
16640
17298
|
subagentFiles: 0,
|
|
17299
|
+
dispatched: 0,
|
|
17300
|
+
userMessages: 0,
|
|
16641
17301
|
subagentSkipped: 0,
|
|
16642
17302
|
compactions: 0,
|
|
16643
17303
|
complete: false
|
|
@@ -16664,7 +17324,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16664
17324
|
if (type === "system" && record.subtype === "compact_boundary") {
|
|
16665
17325
|
result.coverage.compactions++;
|
|
16666
17326
|
}
|
|
16667
|
-
|
|
17327
|
+
if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
|
|
17328
|
+
collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
|
|
16668
17329
|
}
|
|
16669
17330
|
};
|
|
16670
17331
|
try {
|
|
@@ -16736,7 +17397,7 @@ function classifyUnobserved(path) {
|
|
|
16736
17397
|
if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
|
|
16737
17398
|
return "no_edit_record";
|
|
16738
17399
|
}
|
|
16739
|
-
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
|
|
17400
|
+
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
|
|
16740
17401
|
const message = record.message;
|
|
16741
17402
|
const content = message?.content ?? record.content;
|
|
16742
17403
|
const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
|
|
@@ -16758,6 +17419,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
|
|
|
16758
17419
|
byPath.set(path, entry);
|
|
16759
17420
|
}
|
|
16760
17421
|
}
|
|
17422
|
+
if (DISPATCH_TOOLS.has(name) && tally) {
|
|
17423
|
+
tally.dispatched += 1;
|
|
17424
|
+
}
|
|
16761
17425
|
if (name === "Bash") {
|
|
16762
17426
|
const cmd = typeof input.command === "string" ? input.command : "";
|
|
16763
17427
|
if (cmd) {
|
|
@@ -16816,6 +17480,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
|
|
|
16816
17480
|
};
|
|
16817
17481
|
}
|
|
16818
17482
|
|
|
17483
|
+
// src/lib/verdict.ts
|
|
17484
|
+
function reconcileCoverage(changed, coverage) {
|
|
17485
|
+
const changedSet = new Set(changed);
|
|
17486
|
+
const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
|
|
17487
|
+
const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
|
|
17488
|
+
const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
|
|
17489
|
+
const notReviewed = [
|
|
17490
|
+
...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
|
|
17491
|
+
...unaccounted.map((path) => ({
|
|
17492
|
+
path,
|
|
17493
|
+
reason: "unaccounted",
|
|
17494
|
+
// Named so the eventual bug report writes itself: some stage removed this
|
|
17495
|
+
// path and did not say so.
|
|
17496
|
+
stage: "unknown-stage",
|
|
17497
|
+
// An undeclared drop is CAPACITY by default. A stage that cannot be
|
|
17498
|
+
// bothered to say why it dropped a file does not get the benefit of the
|
|
17499
|
+
// doubt — that default is what makes forgetting expensive.
|
|
17500
|
+
kind: "capacity"
|
|
17501
|
+
}))
|
|
17502
|
+
];
|
|
17503
|
+
return {
|
|
17504
|
+
coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
|
|
17505
|
+
unaccounted,
|
|
17506
|
+
balances: unaccounted.length === 0
|
|
17507
|
+
};
|
|
17508
|
+
}
|
|
17509
|
+
function resolveVerdict(proposed, coverage) {
|
|
17510
|
+
if (proposed === "FAIL") return "FAIL";
|
|
17511
|
+
const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17512
|
+
if (blocking.length === 0) return proposed;
|
|
17513
|
+
return "WARN";
|
|
17514
|
+
}
|
|
17515
|
+
function describeCoverage(coverage, maxPaths = 5) {
|
|
17516
|
+
const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17517
|
+
if (relevant.length === 0) return null;
|
|
17518
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
17519
|
+
for (const n of relevant) {
|
|
17520
|
+
const key = `${n.reason}`;
|
|
17521
|
+
const list = byReason.get(key) ?? [];
|
|
17522
|
+
list.push(n.path);
|
|
17523
|
+
byReason.set(key, list);
|
|
17524
|
+
}
|
|
17525
|
+
const lines = [];
|
|
17526
|
+
for (const [reason, paths] of [...byReason.entries()].sort()) {
|
|
17527
|
+
const shown = paths.slice(0, maxPaths).join(", ");
|
|
17528
|
+
const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
|
|
17529
|
+
lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
|
|
17530
|
+
}
|
|
17531
|
+
return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
|
|
17532
|
+
${lines.join("\n")}
|
|
17533
|
+
Treat those files as UNCHECKED, not as approved.`;
|
|
17534
|
+
}
|
|
17535
|
+
function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
|
|
17536
|
+
const reviewed = new Set(reviewedNow);
|
|
17537
|
+
const out = [];
|
|
17538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17539
|
+
for (const s of statements) {
|
|
17540
|
+
if (s.outcome !== "open") continue;
|
|
17541
|
+
if (s.register !== "BLOCK") continue;
|
|
17542
|
+
if (s.carried) continue;
|
|
17543
|
+
if (reviewed.has(s.file)) continue;
|
|
17544
|
+
if (!s.line_sha) continue;
|
|
17545
|
+
if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
|
|
17546
|
+
const key = `${s.file}::${s.pattern_id}`;
|
|
17547
|
+
if (seen.has(key)) continue;
|
|
17548
|
+
seen.add(key);
|
|
17549
|
+
out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
|
|
17550
|
+
}
|
|
17551
|
+
return out;
|
|
17552
|
+
}
|
|
17553
|
+
function describeOpenElsewhere(open) {
|
|
17554
|
+
if (open.length === 0) return null;
|
|
17555
|
+
const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
|
|
17556
|
+
const more = open.length > 5 ? `
|
|
17557
|
+
(+${open.length - 5} more)` : "";
|
|
17558
|
+
return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
|
|
17559
|
+
${lines.join("\n")}${more}
|
|
17560
|
+
This verdict covers the current change only. The tree is not clean.`;
|
|
17561
|
+
}
|
|
17562
|
+
|
|
16819
17563
|
// src/lib/channel.ts
|
|
16820
17564
|
var MAX_AGENT_CONTEXT_CHARS = 1500;
|
|
16821
17565
|
var MAX_AGENT_ITEMS = 5;
|
|
@@ -16838,9 +17582,16 @@ function buildAgentContext(input) {
|
|
|
16838
17582
|
}
|
|
16839
17583
|
if (input.intentVerdict === "misaligned" || input.intentVerdict === "partial") {
|
|
16840
17584
|
const gap = input.intentGaps?.[0];
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
17585
|
+
const repeat = input.intentRepeat ?? 0;
|
|
17586
|
+
if (repeat > 0) {
|
|
17587
|
+
lines.push(
|
|
17588
|
+
`- Same intent flag as the last ${repeat === 1 ? "turn" : `${repeat} turns`}, on a goal that has not changed. Nothing new here \u2014 do not relay or re-explain it again; either act on it or carry on.`
|
|
17589
|
+
);
|
|
17590
|
+
} else {
|
|
17591
|
+
lines.push(
|
|
17592
|
+
`- This does not look like the change that was asked for` + (gap ? `: ${gap}` : ".") + " Not blocking \u2014 but check it against what you were asked to do."
|
|
17593
|
+
);
|
|
17594
|
+
}
|
|
16844
17595
|
}
|
|
16845
17596
|
const agentFindings = (input.findings ?? []).filter((f) => f.scope !== "pre-existing");
|
|
16846
17597
|
for (const f of agentFindings) {
|
|
@@ -16858,9 +17609,13 @@ function buildAgentContext(input) {
|
|
|
16858
17609
|
}
|
|
16859
17610
|
for (const p of input.pendingItems ?? []) {
|
|
16860
17611
|
if (lines.length >= MAX_AGENT_ITEMS) break;
|
|
17612
|
+
if (p.pattern_id === "intent-misalignment") continue;
|
|
16861
17613
|
const text = p.description ?? p.title ?? p.reason;
|
|
16862
17614
|
if (!text) continue;
|
|
16863
|
-
|
|
17615
|
+
const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
|
|
17616
|
+
lines.push(
|
|
17617
|
+
renderItem("", text, p.pattern_id, p.file, p.line) + (seenBefore ? "\n (raised earlier this session and still open \u2014 do not re-explain it; act on it or carry on)" : "")
|
|
17618
|
+
);
|
|
16864
17619
|
}
|
|
16865
17620
|
if (lines.length === 0) return null;
|
|
16866
17621
|
const body = `${REPORT_PREFIX}
|
|
@@ -16887,6 +17642,47 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
|
|
|
16887
17642
|
} : {}
|
|
16888
17643
|
};
|
|
16889
17644
|
}
|
|
17645
|
+
var IDLE_EPISODE_CAP = 3;
|
|
17646
|
+
function channelSilence(input) {
|
|
17647
|
+
const movedSomething = input.newUserPrompt || input.newAuthorship;
|
|
17648
|
+
if (movedSomething) return null;
|
|
17649
|
+
if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
|
|
17650
|
+
if (input.emittedLast) return "caused-by-our-own-emission";
|
|
17651
|
+
return null;
|
|
17652
|
+
}
|
|
17653
|
+
|
|
17654
|
+
// src/lib/emit.ts
|
|
17655
|
+
var YELLOW2 = "\x1B[33m";
|
|
17656
|
+
var NC2 = "\x1B[0m";
|
|
17657
|
+
function emitVerdict(input) {
|
|
17658
|
+
const exit = input.exit ?? ((code) => process.exit(code));
|
|
17659
|
+
const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
|
|
17660
|
+
let verdict = resolveVerdict(input.proposed, coverage);
|
|
17661
|
+
const openElsewhere = input.openElsewhere ?? [];
|
|
17662
|
+
if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
|
|
17663
|
+
const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
|
|
17664
|
+
if (unaccounted.length > 0) {
|
|
17665
|
+
process.stderr.write(
|
|
17666
|
+
`${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
|
|
17667
|
+
`
|
|
17668
|
+
);
|
|
17669
|
+
}
|
|
17670
|
+
if (verdict === "FAIL") {
|
|
17671
|
+
input.renderBlocking?.();
|
|
17672
|
+
if (input.agentContext) {
|
|
17673
|
+
process.stderr.write(`
|
|
17674
|
+
${input.agentContext}
|
|
17675
|
+
`);
|
|
17676
|
+
}
|
|
17677
|
+
if (note && !input.silenced) process.stderr.write(`
|
|
17678
|
+
${YELLOW2}${note}${NC2}
|
|
17679
|
+
`);
|
|
17680
|
+
return exit(2);
|
|
17681
|
+
}
|
|
17682
|
+
const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
|
|
17683
|
+
printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
|
|
17684
|
+
return exit(0);
|
|
17685
|
+
}
|
|
16890
17686
|
|
|
16891
17687
|
// src/lib/cache-cleanup.ts
|
|
16892
17688
|
var import_node_fs21 = require("node:fs");
|
|
@@ -16969,6 +17765,13 @@ function isGitOnlyPrompt(prompt) {
|
|
|
16969
17765
|
return true;
|
|
16970
17766
|
}
|
|
16971
17767
|
function reconcileAnalysisMode(predictedMode, signals) {
|
|
17768
|
+
const mode = resolveAnalysisMode(predictedMode, signals);
|
|
17769
|
+
if (mode !== "skip") return mode;
|
|
17770
|
+
const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
|
|
17771
|
+
if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
|
|
17772
|
+
return mode;
|
|
17773
|
+
}
|
|
17774
|
+
function resolveAnalysisMode(predictedMode, signals) {
|
|
16972
17775
|
if (!predictedMode || !isValidMode(predictedMode)) {
|
|
16973
17776
|
return detectAnalysisMode(
|
|
16974
17777
|
signals.noFilesChanged,
|
|
@@ -17067,46 +17870,6 @@ function shouldWarmRetryAnalyze(result) {
|
|
|
17067
17870
|
return false;
|
|
17068
17871
|
}
|
|
17069
17872
|
|
|
17070
|
-
// src/lib/skip-detection.ts
|
|
17071
|
-
function isBareAckPrompt(prompt) {
|
|
17072
|
-
if (typeof prompt !== "string") return false;
|
|
17073
|
-
const trimmed = prompt.trim();
|
|
17074
|
-
if (trimmed.length === 0) return false;
|
|
17075
|
-
if (trimmed.length > 20) return false;
|
|
17076
|
-
const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
|
|
17077
|
-
return bareAckPattern.test(trimmed);
|
|
17078
|
-
}
|
|
17079
|
-
function isReflectionQuestion(response) {
|
|
17080
|
-
if (!response || typeof response !== "string") return false;
|
|
17081
|
-
const markers = [
|
|
17082
|
-
/reflection\s+for\s+future\s+agents/i,
|
|
17083
|
-
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
17084
|
-
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
17085
|
-
/quick\s+reflection\s+question/i,
|
|
17086
|
-
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
17087
|
-
// interactive, asks the user to confirm/correct before recording. That
|
|
17088
|
-
// turn authors no code either, so it's still a reflection turn.
|
|
17089
|
-
/reflection\s+draft/i,
|
|
17090
|
-
/confirm,?\s+correct,?\s+or\s+add/i
|
|
17091
|
-
];
|
|
17092
|
-
return markers.some((m) => m.test(response));
|
|
17093
|
-
}
|
|
17094
|
-
function isMetaTaskLabel(label2) {
|
|
17095
|
-
if (label2 === null || label2 === void 0) return false;
|
|
17096
|
-
if (typeof label2 !== "string") return false;
|
|
17097
|
-
const trimmed = label2.trim();
|
|
17098
|
-
if (trimmed.length === 0) return true;
|
|
17099
|
-
const metaPatterns = [
|
|
17100
|
-
/^verity\s+[\w-]+\s+response$/i,
|
|
17101
|
-
// "Verity reflect response"
|
|
17102
|
-
/^simple user response$/i,
|
|
17103
|
-
/^verity\s+command$/i,
|
|
17104
|
-
// "Verity command"
|
|
17105
|
-
/^user\s+(question|reply|response|ack)$/i
|
|
17106
|
-
];
|
|
17107
|
-
return metaPatterns.some((p) => p.test(trimmed));
|
|
17108
|
-
}
|
|
17109
|
-
|
|
17110
17873
|
// src/lib/transcript.ts
|
|
17111
17874
|
var import_node_fs22 = require("node:fs");
|
|
17112
17875
|
var MAX_READ_BYTES = 256 * 1024;
|
|
@@ -17120,9 +17883,11 @@ var MAX_SUMMARY_BYTES = 4096;
|
|
|
17120
17883
|
var HOME = process.env.HOME ?? "";
|
|
17121
17884
|
async function extractActionSummary(transcriptPath) {
|
|
17122
17885
|
try {
|
|
17123
|
-
const
|
|
17124
|
-
if (!
|
|
17125
|
-
|
|
17886
|
+
const read = readTurnLines(transcriptPath);
|
|
17887
|
+
if (!read || read.lines.length === 0) return null;
|
|
17888
|
+
const summary = buildSummary(read.lines);
|
|
17889
|
+
if (summary) summary.transcript_windowed = read.window;
|
|
17890
|
+
return summary;
|
|
17126
17891
|
} catch {
|
|
17127
17892
|
return null;
|
|
17128
17893
|
}
|
|
@@ -17136,9 +17901,11 @@ function readTurnLines(transcriptPath) {
|
|
|
17136
17901
|
}
|
|
17137
17902
|
if (size === 0) return null;
|
|
17138
17903
|
let raw;
|
|
17904
|
+
let windowed = false;
|
|
17139
17905
|
if (size <= SMALL_FILE_BYTES) {
|
|
17140
17906
|
raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
|
|
17141
17907
|
} else {
|
|
17908
|
+
windowed = true;
|
|
17142
17909
|
const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
|
|
17143
17910
|
const fd = require("node:fs").openSync(transcriptPath, "r");
|
|
17144
17911
|
try {
|
|
@@ -17156,17 +17923,22 @@ function readTurnLines(transcriptPath) {
|
|
|
17156
17923
|
const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
17157
17924
|
if (allLines.length === 0) return null;
|
|
17158
17925
|
let turnStart = 0;
|
|
17926
|
+
let boundaryFound = false;
|
|
17159
17927
|
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
17160
17928
|
try {
|
|
17161
17929
|
const parsed = JSON.parse(allLines[i]);
|
|
17162
17930
|
if (parsed.type === "user" && isRealUserMessage(parsed)) {
|
|
17163
17931
|
turnStart = i;
|
|
17932
|
+
boundaryFound = true;
|
|
17164
17933
|
break;
|
|
17165
17934
|
}
|
|
17166
17935
|
} catch {
|
|
17167
17936
|
}
|
|
17168
17937
|
}
|
|
17169
|
-
return
|
|
17938
|
+
return {
|
|
17939
|
+
lines: allLines.slice(turnStart),
|
|
17940
|
+
window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
|
|
17941
|
+
};
|
|
17170
17942
|
}
|
|
17171
17943
|
function isRealUserMessage(parsed) {
|
|
17172
17944
|
const message = parsed.message;
|
|
@@ -17270,6 +18042,13 @@ function buildSummary(lines) {
|
|
|
17270
18042
|
files_read: capArray(filesRead, MAX_FILES_LIST),
|
|
17271
18043
|
files_edited: capArray(filesEdited, MAX_FILES_LIST),
|
|
17272
18044
|
files_created: capArray(filesCreated, MAX_CREATED_LIST),
|
|
18045
|
+
// The complement of the two caps that affect SCOPE. `files_read` is excluded
|
|
18046
|
+
// deliberately: reading a file is not authoring it, so a capped read list
|
|
18047
|
+
// narrows nothing.
|
|
18048
|
+
capped_out: [
|
|
18049
|
+
...cappedOut(filesEdited, MAX_FILES_LIST),
|
|
18050
|
+
...cappedOut(filesCreated, MAX_CREATED_LIST)
|
|
18051
|
+
],
|
|
17273
18052
|
searches,
|
|
17274
18053
|
commands,
|
|
17275
18054
|
subagents,
|
|
@@ -17320,6 +18099,9 @@ function sanitizeCommand(rawCmd) {
|
|
|
17320
18099
|
function capArray(set, max) {
|
|
17321
18100
|
return Array.from(set).slice(0, max);
|
|
17322
18101
|
}
|
|
18102
|
+
function cappedOut(set, max) {
|
|
18103
|
+
return Array.from(set).slice(max);
|
|
18104
|
+
}
|
|
17323
18105
|
|
|
17324
18106
|
// src/lib/run-mode.ts
|
|
17325
18107
|
function parseAutonomousEnv(raw) {
|
|
@@ -17714,10 +18496,12 @@ async function readStopHookStdin() {
|
|
|
17714
18496
|
return empty;
|
|
17715
18497
|
}
|
|
17716
18498
|
}
|
|
17717
|
-
function agentContextFor(response) {
|
|
18499
|
+
function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
|
|
17718
18500
|
const metadata = response.metadata ?? {};
|
|
17719
18501
|
const intent = response.intent_alignment ?? {};
|
|
17720
18502
|
return buildAgentContext({
|
|
18503
|
+
intentRepeat,
|
|
18504
|
+
priorPendingFingerprints,
|
|
17721
18505
|
gateDecision: String(response.gate_decision ?? ""),
|
|
17722
18506
|
findings: response.findings ?? [],
|
|
17723
18507
|
pendingItems: response.pending_items ?? [],
|
|
@@ -17728,12 +18512,45 @@ function agentContextFor(response) {
|
|
|
17728
18512
|
});
|
|
17729
18513
|
}
|
|
17730
18514
|
var beaconCtx = null;
|
|
17731
|
-
async function passAndExit(reason, skip) {
|
|
18515
|
+
async function passAndExit(reason, skip, kindOverride) {
|
|
17732
18516
|
const sent = await sendSkipBeacon(beaconCtx, skip);
|
|
17733
18517
|
logEvent("skip", { reason: skip, beacon: sent });
|
|
17734
|
-
|
|
18518
|
+
const POLICY_SKIPS = /* @__PURE__ */ new Set([
|
|
18519
|
+
"no-analyzable-files",
|
|
18520
|
+
"verity-command",
|
|
18521
|
+
"bare-acknowledgment",
|
|
18522
|
+
"reflection-prompt",
|
|
18523
|
+
"skip-mode",
|
|
18524
|
+
"zero-increment",
|
|
18525
|
+
"debounce",
|
|
18526
|
+
"no-delta-since-last-review"
|
|
18527
|
+
]);
|
|
18528
|
+
const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
|
|
18529
|
+
const changed = skipCoverageChanged;
|
|
18530
|
+
const { coverage, unaccounted } = reconcileCoverage(changed, {
|
|
18531
|
+
reviewed: [],
|
|
18532
|
+
notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
|
|
18533
|
+
});
|
|
18534
|
+
const verdict = resolveVerdict("PASS", coverage);
|
|
18535
|
+
const note = describeCoverage(coverage);
|
|
18536
|
+
if (unaccounted.length > 0) {
|
|
18537
|
+
logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
|
|
18538
|
+
}
|
|
18539
|
+
const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
|
|
18540
|
+
const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
|
|
18541
|
+
printJsonCompact(
|
|
18542
|
+
buildHookOutput(
|
|
18543
|
+
verdict,
|
|
18544
|
+
`Verity: ${reason}`,
|
|
18545
|
+
// The agent's ONLY input is additionalContext. Sixteen of the nineteen
|
|
18546
|
+
// terminating paths wrote `systemMessage` — the human's field — and told
|
|
18547
|
+
// the agent nothing at all.
|
|
18548
|
+
agentNote
|
|
18549
|
+
)
|
|
18550
|
+
);
|
|
17735
18551
|
process.exit(0);
|
|
17736
18552
|
}
|
|
18553
|
+
var skipCoverageChanged = [];
|
|
17737
18554
|
var EMPTY_STATIC = {
|
|
17738
18555
|
tool: "@codacy/analysis-cli",
|
|
17739
18556
|
findings: [],
|
|
@@ -17748,7 +18565,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
|
|
|
17748
18565
|
}
|
|
17749
18566
|
function localOnlyAndExit(staticResults) {
|
|
17750
18567
|
printJsonCompact({
|
|
17751
|
-
gate_decision: "
|
|
18568
|
+
gate_decision: "WARN",
|
|
17752
18569
|
systemMessage: "Verity: not authenticated \u2014 ran a local static-only check (no deep review, no upload). Run `verity init` to authenticate and enable the full quality gate.",
|
|
17753
18570
|
unauthenticated: true,
|
|
17754
18571
|
static_results: staticResults
|
|
@@ -17808,6 +18625,7 @@ async function runAnalyze(opts, globals) {
|
|
|
17808
18625
|
});
|
|
17809
18626
|
}
|
|
17810
18627
|
const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
|
|
18628
|
+
skipCoverageChanged = allChanged;
|
|
17811
18629
|
const analyzable = filterAnalyzable(allChanged);
|
|
17812
18630
|
const reviewable = filterReviewable(allChanged);
|
|
17813
18631
|
const securityFiles = filterSecurity(allChanged);
|
|
@@ -17819,15 +18637,27 @@ async function runAnalyze(opts, globals) {
|
|
|
17819
18637
|
const conversation = await readAndClearConversationBuffer(baselineSessionId);
|
|
17820
18638
|
const specs = discoverSpecs();
|
|
17821
18639
|
const plans = discoverPlans();
|
|
18640
|
+
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
18641
|
+
const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
|
|
18642
|
+
const authorshipIsObservable = !!actionSummary && actionSummary.transcript_windowed !== "orphaned" || !!baseline;
|
|
18643
|
+
const canSeeTurnAuthorship = authorshipIsObservable;
|
|
18644
|
+
let earlyFold = null;
|
|
17822
18645
|
const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
|
|
17823
18646
|
if (/^\s*\/verity-/i.test(latestPrompt)) {
|
|
18647
|
+
const setupAuthored = [
|
|
18648
|
+
...actionSummary?.files_edited ?? [],
|
|
18649
|
+
...actionSummary?.files_created ?? []
|
|
18650
|
+
];
|
|
18651
|
+
if (setupAuthored.length > 0) {
|
|
18652
|
+
const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
|
|
18653
|
+
logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
|
|
18654
|
+
}
|
|
17824
18655
|
await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
|
|
17825
18656
|
}
|
|
17826
|
-
if (
|
|
18657
|
+
if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
|
|
17827
18658
|
await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
|
|
17828
18659
|
}
|
|
17829
|
-
|
|
17830
|
-
if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
|
|
18660
|
+
if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
|
|
17831
18661
|
await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
|
|
17832
18662
|
}
|
|
17833
18663
|
const urlResult = await resolveServiceUrl(globals.serviceUrl);
|
|
@@ -17873,7 +18703,11 @@ async function runAnalyze(opts, globals) {
|
|
|
17873
18703
|
);
|
|
17874
18704
|
}
|
|
17875
18705
|
if (analysisMode === "skip") {
|
|
17876
|
-
await passAndExit(
|
|
18706
|
+
await passAndExit(
|
|
18707
|
+
"Skip mode \u2014 no code work to analyze",
|
|
18708
|
+
"skip-mode",
|
|
18709
|
+
turnAuthoredCode ? "capacity" : void 0
|
|
18710
|
+
);
|
|
17877
18711
|
}
|
|
17878
18712
|
let staticResults = {
|
|
17879
18713
|
tool: "@codacy/analysis-cli",
|
|
@@ -17883,7 +18717,8 @@ async function runAnalyze(opts, globals) {
|
|
|
17883
18717
|
let codeDelta = {
|
|
17884
18718
|
files: [],
|
|
17885
18719
|
total_lines: 0,
|
|
17886
|
-
total_files: 0
|
|
18720
|
+
total_files: 0,
|
|
18721
|
+
excluded: []
|
|
17887
18722
|
};
|
|
17888
18723
|
let snapshotResult = { has_snapshots: false, diffs: [] };
|
|
17889
18724
|
let contentHash = null;
|
|
@@ -17924,10 +18759,43 @@ async function runAnalyze(opts, globals) {
|
|
|
17924
18759
|
contentHash = hashResult.hash;
|
|
17925
18760
|
if (analysisMode !== "plan") {
|
|
17926
18761
|
const scoped = scopeToAuthored(allForReview, actionSummary);
|
|
17927
|
-
|
|
18762
|
+
const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
|
|
18763
|
+
if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
|
|
17928
18764
|
await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
|
|
17929
18765
|
}
|
|
17930
|
-
|
|
18766
|
+
if (scoped.signal === "none-authored" && !authorshipIsObservable) {
|
|
18767
|
+
logEvent("none_authored_unverifiable", {
|
|
18768
|
+
reason: "orphaned_window_no_baseline",
|
|
18769
|
+
would_have_skipped: allForReview.length
|
|
18770
|
+
});
|
|
18771
|
+
}
|
|
18772
|
+
const narrowingIsTrustworthy = scoped.signal === "authored" && scoped.files.length > 0 && actionSummary?.transcript_windowed !== "orphaned";
|
|
18773
|
+
let recoveredScope = [];
|
|
18774
|
+
if (!narrowingIsTrustworthy && transcriptPath) {
|
|
18775
|
+
try {
|
|
18776
|
+
earlyFold = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18777
|
+
const authoredWhole = new Set(earlyFold.authored.map((a) => a.p));
|
|
18778
|
+
if (authoredWhole.size > 0) {
|
|
18779
|
+
const root = repoRoot();
|
|
18780
|
+
recoveredScope = allForReview.filter((f) => authoredWhole.has(toRepoRelative(f, root)));
|
|
18781
|
+
}
|
|
18782
|
+
logEvent("scope_recovered_from_fold", {
|
|
18783
|
+
window_saw: scoped.files.length,
|
|
18784
|
+
fold_saw: authoredWhole.size,
|
|
18785
|
+
recovered: recoveredScope.length,
|
|
18786
|
+
would_have_widened_to: allForReview.length
|
|
18787
|
+
});
|
|
18788
|
+
} catch {
|
|
18789
|
+
earlyFold = null;
|
|
18790
|
+
}
|
|
18791
|
+
}
|
|
18792
|
+
if (!narrowingIsTrustworthy && scoped.signal === "authored" && recoveredScope.length === 0) {
|
|
18793
|
+
logEvent("scope_widened_orphaned_window", {
|
|
18794
|
+
would_have_sent: scoped.files.length,
|
|
18795
|
+
widened_to: allForReview.length
|
|
18796
|
+
});
|
|
18797
|
+
}
|
|
18798
|
+
const baseForReview = narrowingIsTrustworthy ? scoped.files : recoveredScope.length > 0 ? recoveredScope : allForReview;
|
|
17931
18799
|
const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
|
|
17932
18800
|
if (!opts.skipStatic && isCodacyAvailable()) {
|
|
17933
18801
|
let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
|
|
@@ -17948,7 +18816,11 @@ async function runAnalyze(opts, globals) {
|
|
|
17948
18816
|
if (assistantResponse) {
|
|
17949
18817
|
analysisMode = "plan";
|
|
17950
18818
|
} else {
|
|
17951
|
-
await passAndExit(
|
|
18819
|
+
await passAndExit(
|
|
18820
|
+
"No files within size limits to analyze",
|
|
18821
|
+
"size-limit",
|
|
18822
|
+
codeDelta.excluded.length > 0 ? "capacity" : "policy"
|
|
18823
|
+
);
|
|
17952
18824
|
}
|
|
17953
18825
|
}
|
|
17954
18826
|
}
|
|
@@ -17961,19 +18833,13 @@ async function runAnalyze(opts, globals) {
|
|
|
17961
18833
|
snapshotResult = generateSnapshotDiffs(codeDelta.files);
|
|
17962
18834
|
}
|
|
17963
18835
|
currentCommit = getCurrentCommit();
|
|
17964
|
-
|
|
17965
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
|
|
17966
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
17967
|
-
iteration = iterResult.iteration;
|
|
18836
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
17968
18837
|
}
|
|
17969
18838
|
}
|
|
17970
18839
|
if (analysisMode === "plan") {
|
|
17971
18840
|
recordAnalysisStart();
|
|
17972
18841
|
currentCommit = getCurrentCommit();
|
|
17973
|
-
|
|
17974
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations);
|
|
17975
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
17976
|
-
iteration = iterResult.iteration;
|
|
18842
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
17977
18843
|
}
|
|
17978
18844
|
const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
|
|
17979
18845
|
for (const f of codeDelta.files) {
|
|
@@ -18040,7 +18906,7 @@ async function runAnalyze(opts, globals) {
|
|
|
18040
18906
|
let foldConservation = null;
|
|
18041
18907
|
if (transcriptPath) {
|
|
18042
18908
|
try {
|
|
18043
|
-
foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18909
|
+
foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18044
18910
|
foldConservation = checkConservation(allForReview, foldResult, repoRoot());
|
|
18045
18911
|
if (!foldConservation.holds) {
|
|
18046
18912
|
process.stderr.write(
|
|
@@ -18122,7 +18988,10 @@ async function runAnalyze(opts, globals) {
|
|
|
18122
18988
|
priorState.capabilities
|
|
18123
18989
|
);
|
|
18124
18990
|
memory = recallMemory(memorySession.d, memorySession.identity, {
|
|
18125
|
-
currentSessionKey: memorySession.identity.sessionKey
|
|
18991
|
+
currentSessionKey: memorySession.identity.sessionKey,
|
|
18992
|
+
// The independent witness. Only meaningful when a transcript was folded —
|
|
18993
|
+
// otherwise it stays undefined and capture coverage reads as UNKNOWN.
|
|
18994
|
+
...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
|
|
18126
18995
|
});
|
|
18127
18996
|
if (memory && !memory.provenanceHolds) {
|
|
18128
18997
|
process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
|
|
@@ -18143,7 +19012,30 @@ async function runAnalyze(opts, globals) {
|
|
|
18143
19012
|
hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
18144
19013
|
isTTY: process.stdout.isTTY === true
|
|
18145
19014
|
});
|
|
19015
|
+
const excludedByReason = {};
|
|
19016
|
+
for (const e of codeDelta.excluded ?? []) {
|
|
19017
|
+
excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
19018
|
+
}
|
|
19019
|
+
const coverageTelemetry = {
|
|
19020
|
+
// git's whole answer, before ANY narrowing. The number that has never been sent.
|
|
19021
|
+
changed_all: allChanged.length,
|
|
19022
|
+
analyzable: analyzable.length,
|
|
19023
|
+
reviewable: reviewable.length,
|
|
19024
|
+
security: securityFiles.length,
|
|
19025
|
+
// after the allowlist, before authorship scoping and the caps
|
|
19026
|
+
for_review: allForReview.length,
|
|
19027
|
+
// what actually reaches the reviewer
|
|
19028
|
+
sent: codeDelta.files.length,
|
|
19029
|
+
// the two silent narrowings, counted separately so they can be told apart
|
|
19030
|
+
capped_out: actionSummary?.capped_out?.length ?? 0,
|
|
19031
|
+
excluded: (codeDelta.excluded ?? []).length,
|
|
19032
|
+
excluded_by_reason: excludedByReason,
|
|
19033
|
+
// was the transcript itself truncated? The 256 KB window means "this turn"
|
|
19034
|
+
// can quietly mean "the last 256 KB of it".
|
|
19035
|
+
transcript_windowed: actionSummary?.transcript_windowed ?? null
|
|
19036
|
+
};
|
|
18146
19037
|
const requestBody = {
|
|
19038
|
+
coverage_telemetry: coverageTelemetry,
|
|
18147
19039
|
static_results: staticResults,
|
|
18148
19040
|
code_delta: codeDelta,
|
|
18149
19041
|
changed_files: allForReview,
|
|
@@ -18201,6 +19093,25 @@ async function runAnalyze(opts, globals) {
|
|
|
18201
19093
|
projection: {
|
|
18202
19094
|
v: 1,
|
|
18203
19095
|
authored: foldResult?.authored ?? [],
|
|
19096
|
+
// ⚠ AUTHORED *SINCE THE LAST VERDICT* — a different question from `authored`.
|
|
19097
|
+
//
|
|
19098
|
+
// `authored` above is the fold of the session TRANSCRIPT, which is
|
|
19099
|
+
// cumulative: on turn 3 it still lists the files turn 2 edited. The
|
|
19100
|
+
// account's close-out reads it as "was this file edited since the
|
|
19101
|
+
// statement was raised", and those are not the same set.
|
|
19102
|
+
//
|
|
19103
|
+
// Measured 2026-08-03 (shirt-seller): four real vulnerabilities were
|
|
19104
|
+
// raised on the turn that wrote them, then marked `fixed` 36 seconds
|
|
19105
|
+
// later by a SUMMARY turn that edited nothing — `authored` still said 2,
|
|
19106
|
+
// the turn carried 0 findings, so "gone + file authored" resolved to
|
|
19107
|
+
// fixed. The vulnerabilities were still on disk. A silent false `fixed`
|
|
19108
|
+
// is worse than a false `open`: it retires the statement the Account
|
|
19109
|
+
// exists to keep.
|
|
19110
|
+
//
|
|
19111
|
+
// `hash_at_last_verdict` is frozen at each verdict and `hash_now` tracks
|
|
19112
|
+
// disk, so their inequality IS "changed since we last spoke" — already
|
|
19113
|
+
// computed, already maintained by the divergence machinery.
|
|
19114
|
+
authored_since_verdict: memorySession ? foldDossier(memorySession.d).authored_all.filter((a) => a.hash_now !== a.hash_at_last_verdict).map((a) => a.path) : [],
|
|
18204
19115
|
unobserved: foldResult?.unobserved ?? [],
|
|
18205
19116
|
commands: foldResult?.commands ?? [],
|
|
18206
19117
|
unknown_types: foldResult?.unknownTypes ?? [],
|
|
@@ -18210,6 +19121,54 @@ async function runAnalyze(opts, globals) {
|
|
|
18210
19121
|
// replaced a population floor with ≈35% power that was sub-integer for
|
|
18211
19122
|
// three-quarters of the fleet.
|
|
18212
19123
|
conservation: foldConservation,
|
|
19124
|
+
// ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
|
|
19125
|
+
//
|
|
19126
|
+
// The whole "task-scoped delta" design space rests on an assumption that
|
|
19127
|
+
// has been observed exactly ONCE: that delta files routinely belong to
|
|
19128
|
+
// earlier work. Three designs were built on it and all three were killed
|
|
19129
|
+
// adversarially — two by measurement — so before another is attempted,
|
|
19130
|
+
// measure the base rate.
|
|
19131
|
+
//
|
|
19132
|
+
// `authored_under_earlier_goal` counts delta paths whose LAST authorship
|
|
19133
|
+
// event precedes the seq of the goal now in force. Both numbers come from
|
|
19134
|
+
// the same append-only counter (`nextSeq`), so the comparison is exact.
|
|
19135
|
+
//
|
|
19136
|
+
// Keyed on the GOAL, deliberately, not on the task id. The task classifier
|
|
19137
|
+
// reported `is_new_task` on two consecutive turns of one task 25 seconds
|
|
19138
|
+
// apart, so a task-keyed number would measure its unreliability rather
|
|
19139
|
+
// than the phenomenon. And this only became meaningful once `recordGoal`
|
|
19140
|
+
// stopped letting a bare "ok" supersede the goal — before that the seq
|
|
19141
|
+
// advanced every turn and this would have degenerated to "not edited this
|
|
19142
|
+
// turn", which is the exact mistake that sank one of the three designs.
|
|
19143
|
+
//
|
|
19144
|
+
// Changes no payload the reviewer sees, no narrowing, no verdict.
|
|
19145
|
+
vrt52: (() => {
|
|
19146
|
+
const goalSeq = memory?.projection.goal?.seq;
|
|
19147
|
+
if (goalSeq === void 0 || !memorySession) return { known: false };
|
|
19148
|
+
const lastSeq2 = new Map(
|
|
19149
|
+
foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
|
|
19150
|
+
);
|
|
19151
|
+
let earlier = 0;
|
|
19152
|
+
let unknown = 0;
|
|
19153
|
+
for (const f of codeDelta.files) {
|
|
19154
|
+
const seen = lastSeq2.get(f.path);
|
|
19155
|
+
if (seen === void 0) unknown++;
|
|
19156
|
+
else if (seen < goalSeq) earlier++;
|
|
19157
|
+
}
|
|
19158
|
+
return {
|
|
19159
|
+
known: true,
|
|
19160
|
+
goal_seq: goalSeq,
|
|
19161
|
+
delta: codeDelta.files.length,
|
|
19162
|
+
// Files this delta carries that were last written under an EARLIER
|
|
19163
|
+
// instruction. If this stays near zero, VRT-52's code half is
|
|
19164
|
+
// unnecessary and should be closed saying so.
|
|
19165
|
+
authored_under_earlier_goal: earlier,
|
|
19166
|
+
// Delta files the dossier has no authorship record for at all —
|
|
19167
|
+
// pre-existing tree state, or an authorship channel the fold cannot
|
|
19168
|
+
// see. Reported separately so a blind spot is never counted as a zero.
|
|
19169
|
+
no_authorship_record: unknown
|
|
19170
|
+
};
|
|
19171
|
+
})(),
|
|
18213
19172
|
// P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
|
|
18214
19173
|
// narrowed to the within-session increment: what changed since the last
|
|
18215
19174
|
// VERDICT rather than since task start.
|
|
@@ -18242,7 +19201,20 @@ async function runAnalyze(opts, globals) {
|
|
|
18242
19201
|
const intentContext = {};
|
|
18243
19202
|
if (conversation && conversation.prompts.length > 0) {
|
|
18244
19203
|
const latest = conversation.prompts[conversation.prompts.length - 1];
|
|
18245
|
-
|
|
19204
|
+
const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
|
|
19205
|
+
intentContext.user_prompt = goalPrompt.entry.prompt;
|
|
19206
|
+
if (isContinuationPrompt(intentContext.user_prompt)) {
|
|
19207
|
+
const carried = memory?.projection.goal?.text;
|
|
19208
|
+
if (carried && !isContinuationPrompt(carried)) {
|
|
19209
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19210
|
+
intentContext.user_prompt = carried;
|
|
19211
|
+
logEvent("goal_from_dossier", { chars: carried.length });
|
|
19212
|
+
}
|
|
19213
|
+
}
|
|
19214
|
+
if (goalPrompt.turnsBack > 0) {
|
|
19215
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19216
|
+
logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
|
|
19217
|
+
}
|
|
18246
19218
|
intentContext.session_id = latest.session_id || void 0;
|
|
18247
19219
|
intentContext.prompt_captured_at = latest.captured_at || void 0;
|
|
18248
19220
|
if (conversation.prompts.length > 1) {
|
|
@@ -18318,6 +19290,8 @@ async function runAnalyze(opts, globals) {
|
|
|
18318
19290
|
message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
|
|
18319
19291
|
} else if (result.error.startsWith("FORBIDDEN")) {
|
|
18320
19292
|
message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
|
|
19293
|
+
} else if (result.error.startsWith("INVALID_TOKEN")) {
|
|
19294
|
+
message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
|
|
18321
19295
|
} else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
|
|
18322
19296
|
message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
|
|
18323
19297
|
} else if (result.http_status && result.http_status >= 500) {
|
|
@@ -18332,8 +19306,122 @@ async function runAnalyze(opts, globals) {
|
|
|
18332
19306
|
const response = result.data;
|
|
18333
19307
|
const decision = response.gate_decision ?? "(unrecognised)";
|
|
18334
19308
|
const sentPaths = codeDelta.files.map((f) => f.path);
|
|
19309
|
+
let openElsewhere = [];
|
|
19310
|
+
if (memorySession) {
|
|
19311
|
+
try {
|
|
19312
|
+
const st = foldDossier(memorySession.d);
|
|
19313
|
+
openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
|
|
19314
|
+
try {
|
|
19315
|
+
const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
|
|
19316
|
+
const at = src[line - 1];
|
|
19317
|
+
return at === void 0 ? null : lineSha(at);
|
|
19318
|
+
} catch {
|
|
19319
|
+
return null;
|
|
19320
|
+
}
|
|
19321
|
+
});
|
|
19322
|
+
} catch {
|
|
19323
|
+
}
|
|
19324
|
+
}
|
|
19325
|
+
const reviewCoverage = {
|
|
19326
|
+
reviewed: sentPaths,
|
|
19327
|
+
// Declared drops from the stages that DO report themselves today. The other
|
|
19328
|
+
// stages surface via `unaccounted`, which is the tripwire, not the design.
|
|
19329
|
+
notReviewed: [
|
|
19330
|
+
// Every exit from the collection loop, each named. Six reasons where there
|
|
19331
|
+
// used to be two recorded and four silent — the silent ones including the
|
|
19332
|
+
// per-file size cap, which could drop a whole source file without leaving a
|
|
19333
|
+
// trace anywhere in the payload or the run row.
|
|
19334
|
+
...codeDelta.excluded,
|
|
19335
|
+
// The server-side 300-line middle-out truncation. It only bites on the
|
|
19336
|
+
// full-file branch (a first analysis, before snapshots exist) because
|
|
19337
|
+
// analyze normally sends diffs — but on that branch the reviewer sees the
|
|
19338
|
+
// first and last 100 lines and nothing between, and until now said so to
|
|
19339
|
+
// nobody. CAPACITY: a partial look is not a look.
|
|
19340
|
+
...(response.metadata?.truncated_files ?? []).map((path) => ({
|
|
19341
|
+
path,
|
|
19342
|
+
reason: "file-middle-truncated-300-lines",
|
|
19343
|
+
stage: "prompt-builder",
|
|
19344
|
+
kind: "capacity"
|
|
19345
|
+
})),
|
|
19346
|
+
// The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
|
|
19347
|
+
// what is REVIEWED, not merely what is summarised — a session editing 25
|
|
19348
|
+
// files had five silently excluded from the reviewed set.
|
|
19349
|
+
...(actionSummary?.capped_out ?? []).map((path) => ({
|
|
19350
|
+
path,
|
|
19351
|
+
reason: "edit-list-cap-20",
|
|
19352
|
+
stage: "extractActionSummary",
|
|
19353
|
+
kind: "capacity"
|
|
19354
|
+
})),
|
|
19355
|
+
// ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
|
|
19356
|
+
//
|
|
19357
|
+
// The universe is `allChanged`, git's whole dirty tree. The reviewed set is
|
|
19358
|
+
// scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
|
|
19359
|
+
// every pre-existing dirty file is in the universe, absent from `reviewed`,
|
|
19360
|
+
// and — until now — declared by nobody. It fell through to `unaccounted`,
|
|
19361
|
+
// became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
|
|
19362
|
+
// that was never this session's to review.
|
|
19363
|
+
//
|
|
19364
|
+
// Measured 2026-08-04: three consecutive runs over an untouched tree gave
|
|
19365
|
+
// three different answers — .claude/settings.json, then admin.js, then six
|
|
19366
|
+
// files — because each run took a different path and each path had a
|
|
19367
|
+
// different idea of the universe. POLICY: not this session's work is not a
|
|
19368
|
+
// coverage gap, it is the cure working.
|
|
19369
|
+
...allChanged.filter((p) => !sentPaths.includes(p) && !codeDelta.excluded.some((e) => e.path === p)).filter((p) => analyzable.includes(p) || reviewable.includes(p) || securityFiles.includes(p)).map((path) => ({
|
|
19370
|
+
path,
|
|
19371
|
+
reason: "not-authored-this-session",
|
|
19372
|
+
stage: "baseline-scoping",
|
|
19373
|
+
kind: "policy"
|
|
19374
|
+
})),
|
|
19375
|
+
// The extension allowlist, and it is POLICY rather than capacity: a changed
|
|
19376
|
+
// README was never going to be reviewed, and treating that as a coverage
|
|
19377
|
+
// gap would downgrade nearly every PASS to WARN until WARN meant nothing.
|
|
19378
|
+
// Recorded so the ledger balances and so "what did Verity ignore entirely"
|
|
19379
|
+
// is answerable — but it never touches the verdict.
|
|
19380
|
+
...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
|
|
19381
|
+
path,
|
|
19382
|
+
reason: "not-a-reviewed-file-type",
|
|
19383
|
+
stage: "extension-allowlist",
|
|
19384
|
+
kind: "policy"
|
|
19385
|
+
}))
|
|
19386
|
+
]
|
|
19387
|
+
};
|
|
18335
19388
|
const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
|
|
18336
19389
|
const watermarkIsPartial = !!codeDelta.truncated;
|
|
19390
|
+
let silenced = null;
|
|
19391
|
+
let turnIsIdleForChannel = true;
|
|
19392
|
+
if (memorySession) {
|
|
19393
|
+
try {
|
|
19394
|
+
const st = foldDossier(memorySession.d);
|
|
19395
|
+
turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
|
|
19396
|
+
silenced = channelSilence({
|
|
19397
|
+
// The BUFFER, not intentContext.user_prompt: the latter falls back to a
|
|
19398
|
+
// linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
|
|
19399
|
+
// not a user utterance. Treating it as one would keep the loop alive on
|
|
19400
|
+
// exactly the autonomous cohort.
|
|
19401
|
+
newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
19402
|
+
newAuthorship: !turnIsIdleForChannel,
|
|
19403
|
+
emittedLast: st.meta.channel?.emittedLast === true,
|
|
19404
|
+
consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
|
|
19405
|
+
});
|
|
19406
|
+
} catch {
|
|
19407
|
+
silenced = null;
|
|
19408
|
+
}
|
|
19409
|
+
}
|
|
19410
|
+
if (silenced) {
|
|
19411
|
+
logEvent("channel_silenced", {
|
|
19412
|
+
reason: silenced,
|
|
19413
|
+
run_id: response.run_id ?? turnId,
|
|
19414
|
+
decision
|
|
19415
|
+
});
|
|
19416
|
+
}
|
|
19417
|
+
let intentRepeatCount = 0;
|
|
19418
|
+
const priorPendingFingerprints = memorySession ? (() => {
|
|
19419
|
+
try {
|
|
19420
|
+
return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
|
|
19421
|
+
} catch {
|
|
19422
|
+
return [];
|
|
19423
|
+
}
|
|
19424
|
+
})() : [];
|
|
18337
19425
|
if (memorySession) {
|
|
18338
19426
|
try {
|
|
18339
19427
|
recordVerdict(memorySession.d, {
|
|
@@ -18347,8 +19435,21 @@ async function runAnalyze(opts, globals) {
|
|
|
18347
19435
|
pattern_id: f.pattern_id ?? f.rule_id,
|
|
18348
19436
|
title: f.title,
|
|
18349
19437
|
severity: f.severity
|
|
18350
|
-
})) ?? []
|
|
19438
|
+
})) ?? [],
|
|
19439
|
+
intent: response.intent_alignment ?? null,
|
|
19440
|
+
// The same signal F1 introduced: bytes differing from the hash frozen at
|
|
19441
|
+
// the last verdict. A turn that moved nothing is the only kind that can
|
|
19442
|
+
// accumulate a repeat.
|
|
19443
|
+
idle: turnIsIdleForChannel,
|
|
19444
|
+
// What next turn reads as `emittedLast`. A suppressed turn did not
|
|
19445
|
+
// speak, so it cannot be the cause of the turn after it — which is what
|
|
19446
|
+
// keeps this from becoming a permanent gag.
|
|
19447
|
+
emitted: !silenced,
|
|
19448
|
+
// Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
|
|
19449
|
+
// carry no `pattern_id`, so their content is the only available key.
|
|
19450
|
+
pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
|
|
18351
19451
|
});
|
|
19452
|
+
intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
|
|
18352
19453
|
} catch {
|
|
18353
19454
|
}
|
|
18354
19455
|
}
|
|
@@ -18446,9 +19547,41 @@ async function runAnalyze(opts, globals) {
|
|
|
18446
19547
|
reverify_by: response.reverify_by
|
|
18447
19548
|
});
|
|
18448
19549
|
const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
|
|
18449
|
-
|
|
19550
|
+
let capReleased = false;
|
|
19551
|
+
let effectiveDecision = decision;
|
|
19552
|
+
if (decision === "FAIL") {
|
|
19553
|
+
const blocking = (response.findings ?? []).filter((f) => {
|
|
19554
|
+
const sev = String(f.severity ?? "").toLowerCase();
|
|
19555
|
+
return sev === "critical" || sev === "high";
|
|
19556
|
+
});
|
|
19557
|
+
const fingerprint = findingsFingerprint(blocking);
|
|
19558
|
+
const prior = readIterationState(currentCommit);
|
|
19559
|
+
const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
|
|
19560
|
+
const nextIteration = sameProblem ? prior.iteration + 1 : 1;
|
|
19561
|
+
const maxIterations = parseInt(opts.maxIterations, 10);
|
|
19562
|
+
writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
|
|
19563
|
+
iteration = nextIteration;
|
|
19564
|
+
if (nextIteration > maxIterations) {
|
|
19565
|
+
capReleased = true;
|
|
19566
|
+
effectiveDecision = "WARN";
|
|
19567
|
+
logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
|
|
19568
|
+
}
|
|
19569
|
+
}
|
|
19570
|
+
if (capReleased) {
|
|
19571
|
+
const findings = response.findings ?? [];
|
|
19572
|
+
const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
|
|
19573
|
+
emitVerdict({
|
|
19574
|
+
proposed: "WARN",
|
|
19575
|
+
changed: skipCoverageChanged,
|
|
19576
|
+
coverage: reviewCoverage,
|
|
19577
|
+
userSummary: `Verity: WARN \u2014 self-healing limit (${opts.maxIterations}) reached on the same finding. NO LONGER BLOCKING, but ${findings.length} finding(s) remain OPEN and were NOT fixed. Human review required before deploying.
|
|
19578
|
+
${lines.join("\n")}`,
|
|
19579
|
+
agentContext: null,
|
|
19580
|
+
silenced: true
|
|
19581
|
+
});
|
|
19582
|
+
}
|
|
19583
|
+
switch (effectiveDecision) {
|
|
18450
19584
|
case "FAIL": {
|
|
18451
|
-
writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
|
|
18452
19585
|
const assessment = response.assessment;
|
|
18453
19586
|
const narrative = assessment?.narrative ?? "";
|
|
18454
19587
|
const findings = response.findings ?? [];
|
|
@@ -18525,7 +19658,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
|
|
|
18525
19658
|
if (grantNudge) process.stderr.write(`
|
|
18526
19659
|
${YELLOW}${grantNudge.trim()}${NC}
|
|
18527
19660
|
`);
|
|
18528
|
-
|
|
19661
|
+
emitVerdict({
|
|
19662
|
+
proposed: "FAIL",
|
|
19663
|
+
changed: skipCoverageChanged,
|
|
19664
|
+
coverage: reviewCoverage,
|
|
19665
|
+
userSummary: "",
|
|
19666
|
+
// Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
|
|
19667
|
+
// the findings themselves are rendered above by the blocking renderer,
|
|
19668
|
+
// so what the cut removes is the repeated commentary, never the defect.
|
|
19669
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19670
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19671
|
+
silenced: !!silenced,
|
|
19672
|
+
openElsewhere
|
|
19673
|
+
});
|
|
18529
19674
|
break;
|
|
18530
19675
|
}
|
|
18531
19676
|
case "PASS": {
|
|
@@ -18537,10 +19682,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18537
19682
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18538
19683
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18539
19684
|
userSummary += loginNudge + grantNudge;
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
19685
|
+
emitVerdict({
|
|
19686
|
+
proposed: "PASS",
|
|
19687
|
+
changed: skipCoverageChanged,
|
|
19688
|
+
coverage: reviewCoverage,
|
|
19689
|
+
userSummary,
|
|
19690
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19691
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19692
|
+
silenced: !!silenced,
|
|
19693
|
+
openElsewhere
|
|
19694
|
+
});
|
|
18544
19695
|
break;
|
|
18545
19696
|
}
|
|
18546
19697
|
case "WARN": {
|
|
@@ -18551,10 +19702,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18551
19702
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18552
19703
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18553
19704
|
userSummary += loginNudge + grantNudge;
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
19705
|
+
emitVerdict({
|
|
19706
|
+
proposed: "WARN",
|
|
19707
|
+
changed: skipCoverageChanged,
|
|
19708
|
+
coverage: reviewCoverage,
|
|
19709
|
+
userSummary,
|
|
19710
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19711
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19712
|
+
silenced: !!silenced,
|
|
19713
|
+
openElsewhere
|
|
19714
|
+
});
|
|
18558
19715
|
break;
|
|
18559
19716
|
}
|
|
18560
19717
|
default: {
|
|
@@ -18667,8 +19824,8 @@ async function runReview(opts, globals) {
|
|
|
18667
19824
|
for (const p of specPaths) {
|
|
18668
19825
|
if (!(0, import_node_fs26.existsSync)(p)) continue;
|
|
18669
19826
|
try {
|
|
18670
|
-
const { readFileSync:
|
|
18671
|
-
const content =
|
|
19827
|
+
const { readFileSync: readFileSync16 } = await import("node:fs");
|
|
19828
|
+
const content = readFileSync16(p, "utf-8");
|
|
18672
19829
|
specs.push({ path: p, content: content.slice(0, 10240) });
|
|
18673
19830
|
} catch {
|
|
18674
19831
|
}
|
|
@@ -18970,7 +20127,7 @@ async function runGuard(opts, globals) {
|
|
|
18970
20127
|
cmd: "guard"
|
|
18971
20128
|
});
|
|
18972
20129
|
if (!result.ok) {
|
|
18973
|
-
const authRemedy = result.error.startsWith("STALE_VERIFICATION") ? " Your GitHub verification expired \u2014 run `verity login` to re-verify." : result.error.startsWith("FORBIDDEN") ? " No access grant for this repository \u2014 run `verity login` to refresh your grants." : "";
|
|
20130
|
+
const authRemedy = result.error.startsWith("STALE_VERIFICATION") ? " Your GitHub verification expired \u2014 run `verity login` to re-verify." : result.error.startsWith("FORBIDDEN") ? " No access grant for this repository \u2014 run `verity login` to refresh your grants." : result.error.startsWith("INVALID_TOKEN") ? " Your Verity login expired or was revoked \u2014 run `verity login` to sign in again." : "";
|
|
18974
20131
|
emitAllowNotice(
|
|
18975
20132
|
`\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
|
|
18976
20133
|
`Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
|
|
@@ -19471,6 +20628,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19471
20628
|
return "handled";
|
|
19472
20629
|
}
|
|
19473
20630
|
if (!who.ok) {
|
|
20631
|
+
const denial = authDenialRemedy(who.error);
|
|
20632
|
+
if (denial) {
|
|
20633
|
+
console.log("");
|
|
20634
|
+
printWarn(`Your existing Verity credential was rejected (${denial.code}).`);
|
|
20635
|
+
printInfo(` ${denial.remedy}`);
|
|
20636
|
+
return "drive-login";
|
|
20637
|
+
}
|
|
19474
20638
|
if (existing.data.userId != null) {
|
|
19475
20639
|
printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
|
|
19476
20640
|
} else {
|
|
@@ -19486,15 +20650,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19486
20650
|
return "drive-login";
|
|
19487
20651
|
}
|
|
19488
20652
|
async function runOptionalAuth(resolution, opts = {}) {
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
}
|
|
20653
|
+
if (resolution.source === "default") {
|
|
20654
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
20655
|
+
}
|
|
20656
|
+
const heal = await maybeHealServiceUrl(resolution, opts.verbose);
|
|
20657
|
+
const serviceUrl = heal.serviceUrl;
|
|
20658
|
+
const healed = heal.healed;
|
|
20659
|
+
if (healed) {
|
|
20660
|
+
printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
|
|
19498
20661
|
}
|
|
19499
20662
|
let remote = "";
|
|
19500
20663
|
try {
|
|
@@ -19512,8 +20675,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19512
20675
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
19513
20676
|
console.log("");
|
|
19514
20677
|
console.log(" Signing in is optional. What it does:");
|
|
19515
|
-
console.log(" - Confirms you
|
|
19516
|
-
console.log("
|
|
20678
|
+
console.log(" - Confirms which repositories you can write to. The GitHub token is");
|
|
20679
|
+
console.log(" used once for that check, then discarded \u2014 Verity never stores it.");
|
|
20680
|
+
console.log(" - One login covers every repository you can write to \u2014 other repos");
|
|
20681
|
+
console.log(" need no further sign-in on this machine.");
|
|
19517
20682
|
console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
|
|
19518
20683
|
console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
|
|
19519
20684
|
console.log(" - It is required to store and access run history for this repo");
|
|
@@ -19528,17 +20693,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19528
20693
|
localOnlyNote();
|
|
19529
20694
|
return;
|
|
19530
20695
|
}
|
|
19531
|
-
if (!remote) {
|
|
19532
|
-
printWarn("No git remote found \u2014 cannot authenticate yet.");
|
|
19533
|
-
localOnlyNote();
|
|
19534
|
-
return;
|
|
19535
|
-
}
|
|
19536
|
-
const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
|
|
19537
20696
|
printInfo("Authenticating with GitHub\u2026");
|
|
19538
|
-
const result = await
|
|
20697
|
+
const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
|
|
19539
20698
|
if (result.ok) {
|
|
19540
|
-
|
|
19541
|
-
printInfo(`Logged in${who ? ` as ${who}` : ""} \u2713 \u2014 runs, history, and cloud memory now sync to Verity.`);
|
|
20699
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: opts.verbose });
|
|
19542
20700
|
} else {
|
|
19543
20701
|
printWarn(`Authentication did not complete: ${result.error}`);
|
|
19544
20702
|
localOnlyNote();
|
|
@@ -19689,8 +20847,8 @@ function registerInitCommand(program2) {
|
|
|
19689
20847
|
console.log("");
|
|
19690
20848
|
try {
|
|
19691
20849
|
const globals = program2.opts();
|
|
19692
|
-
const
|
|
19693
|
-
await runOptionalAuth(
|
|
20850
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
20851
|
+
await runOptionalAuth(resolution, {
|
|
19694
20852
|
token: globals.token,
|
|
19695
20853
|
verbose: globals.verbose
|
|
19696
20854
|
});
|
|
@@ -20306,6 +21464,12 @@ function registerTelemetryCommands(program2) {
|
|
|
20306
21464
|
printError(urlResult.error);
|
|
20307
21465
|
process.exit(1);
|
|
20308
21466
|
}
|
|
21467
|
+
const pin = await checkTokenPin(tokenResult.data.token, urlResult.data);
|
|
21468
|
+
if (!pin.attach && pin.reason === "unpinnable") {
|
|
21469
|
+
printWarn("Your Verity credential does not record which service issued it, so this endpoint");
|
|
21470
|
+
printWarn(" cannot be verified \u2014 a repository could point your telemetry elsewhere.");
|
|
21471
|
+
printWarn(' Run "verity login" to re-issue the credential; Verity can check it after that.');
|
|
21472
|
+
}
|
|
20309
21473
|
const result = await installTelemetry(urlResult.data);
|
|
20310
21474
|
if (!result.ok) {
|
|
20311
21475
|
printError(result.error);
|
|
@@ -20354,7 +21518,8 @@ function registerTelemetryCommands(program2) {
|
|
|
20354
21518
|
}
|
|
20355
21519
|
|
|
20356
21520
|
// src/cli.ts
|
|
20357
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.
|
|
21521
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.e79117b").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
|
|
21522
|
+
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
20358
21523
|
try {
|
|
20359
21524
|
await foldLegacyLocalCredential();
|
|
20360
21525
|
} catch {
|
|
@@ -20363,6 +21528,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
|
|
|
20363
21528
|
registerAuthCommands(program);
|
|
20364
21529
|
registerLoginCommand(program);
|
|
20365
21530
|
registerTokenCommand(program);
|
|
21531
|
+
registerSessionsCommands(program);
|
|
21532
|
+
registerLogoutCommand(program);
|
|
20366
21533
|
registerHooksCommands(program);
|
|
20367
21534
|
registerIntentCommands(program);
|
|
20368
21535
|
registerLifecycleCommands(program);
|