@codacy/verity-cli 0.28.1-experimental.5ec7373 → 0.28.1-experimental.6eac6aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/bin/verity.js +946 -122
- package/package.json +1 -1
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;
|
|
@@ -11188,7 +11217,13 @@ async function resolveToken(flagToken) {
|
|
|
11188
11217
|
if (rec) {
|
|
11189
11218
|
return {
|
|
11190
11219
|
ok: true,
|
|
11191
|
-
data: {
|
|
11220
|
+
data: {
|
|
11221
|
+
token: rec.token,
|
|
11222
|
+
source: "global",
|
|
11223
|
+
userId: rec.userId,
|
|
11224
|
+
email: rec.email,
|
|
11225
|
+
keyed: rec.keyed
|
|
11226
|
+
}
|
|
11192
11227
|
};
|
|
11193
11228
|
}
|
|
11194
11229
|
const local = await readLegacyLocalCredential();
|
|
@@ -11220,6 +11255,16 @@ function reverifyNudge(who) {
|
|
|
11220
11255
|
}
|
|
11221
11256
|
return null;
|
|
11222
11257
|
}
|
|
11258
|
+
function isLegacyPerRepoCredential(auth2) {
|
|
11259
|
+
return auth2.source === "local" || auth2.source === "global" && auth2.keyed === true;
|
|
11260
|
+
}
|
|
11261
|
+
async function shouldUpgradeOnLogin(auth2) {
|
|
11262
|
+
if (!isLegacyPerRepoCredential(auth2)) return false;
|
|
11263
|
+
if (auth2.userId == null) return true;
|
|
11264
|
+
const bare = await readGlobalCredential("");
|
|
11265
|
+
if (bare?.userId == null) return true;
|
|
11266
|
+
return bare.userId === auth2.userId;
|
|
11267
|
+
}
|
|
11223
11268
|
function authDenialRemedy(error) {
|
|
11224
11269
|
if (error.startsWith("STALE_VERIFICATION")) {
|
|
11225
11270
|
return {
|
|
@@ -11233,6 +11278,12 @@ function authDenialRemedy(error) {
|
|
|
11233
11278
|
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
11279
|
};
|
|
11235
11280
|
}
|
|
11281
|
+
if (error.startsWith("INVALID_TOKEN")) {
|
|
11282
|
+
return {
|
|
11283
|
+
code: "INVALID_TOKEN",
|
|
11284
|
+
remedy: 'Your Verity login has expired or was revoked \u2014 run "verity login" to sign in again.'
|
|
11285
|
+
};
|
|
11286
|
+
}
|
|
11236
11287
|
return null;
|
|
11237
11288
|
}
|
|
11238
11289
|
async function probeService(serviceUrl, verbose) {
|
|
@@ -11273,6 +11324,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
|
|
|
11273
11324
|
|
|
11274
11325
|
// src/lib/register.ts
|
|
11275
11326
|
var readline = __toESM(require("node:readline/promises"));
|
|
11327
|
+
var import_node_os = require("node:os");
|
|
11276
11328
|
|
|
11277
11329
|
// src/lib/provider-auth.ts
|
|
11278
11330
|
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -11460,6 +11512,15 @@ async function registerProject(opts) {
|
|
|
11460
11512
|
}
|
|
11461
11513
|
return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
|
|
11462
11514
|
}
|
|
11515
|
+
function deviceLabel() {
|
|
11516
|
+
const override = process.env.VERITY_DEVICE_LABEL?.trim();
|
|
11517
|
+
if (override) return override;
|
|
11518
|
+
try {
|
|
11519
|
+
return (0, import_node_os.hostname)() || void 0;
|
|
11520
|
+
} catch {
|
|
11521
|
+
return void 0;
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
11463
11524
|
async function loginOnce(opts) {
|
|
11464
11525
|
const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
|
|
11465
11526
|
const providerAuth = await githubDeviceFlow();
|
|
@@ -11480,13 +11541,19 @@ async function loginOnce(opts) {
|
|
|
11480
11541
|
path: "/auth/login",
|
|
11481
11542
|
serviceUrl: opts.serviceUrl,
|
|
11482
11543
|
extraHeaders: { "X-Provider-Token": providerToken },
|
|
11544
|
+
// Label the session so its owner can tell their machines apart in
|
|
11545
|
+
// `verity sessions list` — a list of identical "login" rows is unusable when
|
|
11546
|
+
// the question is "which of these is the laptop I lost?". The hostname is the
|
|
11547
|
+
// useful default; VERITY_DEVICE_LABEL overrides it for anyone who would
|
|
11548
|
+
// rather not send it. Server-side it is sanitized and capped.
|
|
11549
|
+
body: { device: deviceLabel() },
|
|
11483
11550
|
verbose: opts.verbose,
|
|
11484
11551
|
cmd: "login"
|
|
11485
11552
|
});
|
|
11486
11553
|
if (!result.ok) {
|
|
11487
11554
|
return { ok: false, error: result.error };
|
|
11488
11555
|
}
|
|
11489
|
-
const { token, service_url, user_id, user, repo_count } = result.data;
|
|
11556
|
+
const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
|
|
11490
11557
|
const loginUserId = user_id ?? user?.id ?? void 0;
|
|
11491
11558
|
try {
|
|
11492
11559
|
await upsertGlobalCredential("", {
|
|
@@ -11510,15 +11577,16 @@ async function loginOnce(opts) {
|
|
|
11510
11577
|
email: user?.email,
|
|
11511
11578
|
userId: loginUserId,
|
|
11512
11579
|
repoCount: repo_count ?? 0,
|
|
11513
|
-
prunedCredentials: pruned
|
|
11580
|
+
prunedCredentials: pruned,
|
|
11581
|
+
expiresAt: expires_at
|
|
11514
11582
|
}
|
|
11515
11583
|
};
|
|
11516
11584
|
}
|
|
11517
11585
|
|
|
11518
11586
|
// src/commands/auth.ts
|
|
11519
11587
|
function registerAuthCommands(program2) {
|
|
11520
|
-
const
|
|
11521
|
-
|
|
11588
|
+
const auth2 = program2.command("auth").description("Manage project authentication");
|
|
11589
|
+
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
11590
|
const globals = program2.opts();
|
|
11523
11591
|
const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
|
|
11524
11592
|
let remote = opts.remote;
|
|
@@ -11545,7 +11613,7 @@ function registerAuthCommands(program2) {
|
|
|
11545
11613
|
if (email) printInfo(`Authenticated as: ${email}`);
|
|
11546
11614
|
printJson({ project_id: projectId, service_url: resolvedUrl });
|
|
11547
11615
|
});
|
|
11548
|
-
|
|
11616
|
+
auth2.command("verify").description("Verify the current token is valid").action(async () => {
|
|
11549
11617
|
const globals = program2.opts();
|
|
11550
11618
|
const tokenResult = await resolveToken(globals.token);
|
|
11551
11619
|
if (!tokenResult.ok) {
|
|
@@ -11571,7 +11639,7 @@ function registerAuthCommands(program2) {
|
|
|
11571
11639
|
printInfo(`Token valid. Project: ${result.data.project_name}`);
|
|
11572
11640
|
printJson(result.data);
|
|
11573
11641
|
});
|
|
11574
|
-
|
|
11642
|
+
auth2.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
|
|
11575
11643
|
const globals = program2.opts();
|
|
11576
11644
|
let remote = opts.remote;
|
|
11577
11645
|
if (!remote) {
|
|
@@ -11627,13 +11695,19 @@ function registerLoginCommand(program2) {
|
|
|
11627
11695
|
const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
|
|
11628
11696
|
if (who.ok && who.data.logged_in) {
|
|
11629
11697
|
const nudge = reverifyNudge(who.data);
|
|
11630
|
-
|
|
11698
|
+
const upgrade = await shouldUpgradeOnLogin(existing.data);
|
|
11699
|
+
if (!nudge && !upgrade) {
|
|
11631
11700
|
printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
|
|
11632
11701
|
printInfo(" Re-authenticate with: verity login --force");
|
|
11633
11702
|
return;
|
|
11634
11703
|
}
|
|
11635
|
-
|
|
11636
|
-
|
|
11704
|
+
if (nudge) {
|
|
11705
|
+
printWarn(nudge);
|
|
11706
|
+
printInfo("Re-verifying your repository access\u2026");
|
|
11707
|
+
} else {
|
|
11708
|
+
printInfo("You are signed in with a per-repository token (the old format).");
|
|
11709
|
+
printInfo(" Upgrading to a single login that covers every repository you can write to\u2026");
|
|
11710
|
+
}
|
|
11637
11711
|
} else if (who.ok && who.data.anonymous) {
|
|
11638
11712
|
printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
|
|
11639
11713
|
} else if (!who.ok) {
|
|
@@ -11650,6 +11724,10 @@ function registerLoginCommand(program2) {
|
|
|
11650
11724
|
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11651
11725
|
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11652
11726
|
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11727
|
+
if (out.expiresAt) {
|
|
11728
|
+
printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
|
|
11729
|
+
printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
|
|
11730
|
+
}
|
|
11653
11731
|
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11654
11732
|
if (out.prunedCredentials > 0) {
|
|
11655
11733
|
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
|
|
@@ -11680,14 +11758,24 @@ function registerLoginCommand(program2) {
|
|
|
11680
11758
|
const rec = await readGlobalCredential(remote);
|
|
11681
11759
|
if (rec && rec.token !== out.token) {
|
|
11682
11760
|
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11761
|
+
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11683
11762
|
if (otherBackend) {
|
|
11684
11763
|
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11685
11764
|
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11686
11765
|
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11766
|
+
printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
|
|
11767
|
+
printWarn(" the full GitHub flow every time.");
|
|
11768
|
+
} else if (otherIdentity) {
|
|
11769
|
+
printWarn(" Note: this repository uses a different account's credential, which takes");
|
|
11770
|
+
printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
|
|
11771
|
+
printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
|
|
11772
|
+
printWarn(" only if you want this repository on the login you just completed.");
|
|
11687
11773
|
} else {
|
|
11688
11774
|
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11689
11775
|
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11690
11776
|
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11777
|
+
printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
|
|
11778
|
+
printWarn(" every time.");
|
|
11691
11779
|
}
|
|
11692
11780
|
}
|
|
11693
11781
|
}
|
|
@@ -11809,6 +11897,177 @@ function registerTokenCommand(program2) {
|
|
|
11809
11897
|
});
|
|
11810
11898
|
}
|
|
11811
11899
|
|
|
11900
|
+
// src/commands/sessions.ts
|
|
11901
|
+
function shortDate(iso) {
|
|
11902
|
+
return iso ? iso.slice(0, 10) : "\u2014";
|
|
11903
|
+
}
|
|
11904
|
+
function daysUntil(iso) {
|
|
11905
|
+
if (!iso) return null;
|
|
11906
|
+
const ms = Date.parse(iso);
|
|
11907
|
+
if (Number.isNaN(ms)) return null;
|
|
11908
|
+
return Math.round((ms - Date.now()) / 864e5);
|
|
11909
|
+
}
|
|
11910
|
+
async function auth(globals) {
|
|
11911
|
+
const tokenResult = await resolveToken(globals.token);
|
|
11912
|
+
if (!tokenResult.ok) {
|
|
11913
|
+
printError(tokenResult.error);
|
|
11914
|
+
process.exit(1);
|
|
11915
|
+
}
|
|
11916
|
+
const urlResult = await resolveServiceUrl(globals.serviceUrl);
|
|
11917
|
+
if (!urlResult.ok) {
|
|
11918
|
+
printError(urlResult.error);
|
|
11919
|
+
process.exit(1);
|
|
11920
|
+
}
|
|
11921
|
+
return { token: tokenResult.data.token, serviceUrl: urlResult.data };
|
|
11922
|
+
}
|
|
11923
|
+
function explain(error) {
|
|
11924
|
+
if (error.startsWith("FORBIDDEN")) {
|
|
11925
|
+
printInfo(' Sessions belong to a logged-in account \u2014 run "verity login" first.');
|
|
11926
|
+
} else if (error.startsWith("INVALID_TOKEN")) {
|
|
11927
|
+
printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
|
|
11928
|
+
}
|
|
11929
|
+
}
|
|
11930
|
+
function registerSessionsCommands(program2) {
|
|
11931
|
+
const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
|
|
11932
|
+
sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
|
|
11933
|
+
const globals = program2.opts();
|
|
11934
|
+
const { token, serviceUrl } = await auth(globals);
|
|
11935
|
+
const result = await apiRequest({
|
|
11936
|
+
method: "GET",
|
|
11937
|
+
path: "/auth/sessions",
|
|
11938
|
+
serviceUrl,
|
|
11939
|
+
token,
|
|
11940
|
+
verbose: globals.verbose,
|
|
11941
|
+
cmd: "sessions"
|
|
11942
|
+
});
|
|
11943
|
+
if (!result.ok) {
|
|
11944
|
+
printError(result.error);
|
|
11945
|
+
explain(result.error);
|
|
11946
|
+
process.exit(1);
|
|
11947
|
+
}
|
|
11948
|
+
if (opts.json) {
|
|
11949
|
+
printJson(result.data);
|
|
11950
|
+
return;
|
|
11951
|
+
}
|
|
11952
|
+
const list = result.data.sessions;
|
|
11953
|
+
if (list.length === 0) {
|
|
11954
|
+
printInfo('No active logins. (Run "verity login".)');
|
|
11955
|
+
return;
|
|
11956
|
+
}
|
|
11957
|
+
printInfo(`${list.length} active login${list.length === 1 ? "" : "s"}:`);
|
|
11958
|
+
printInfo("");
|
|
11959
|
+
printInfo(`${"SESSION ID".padEnd(38)}${"DEVICE".padEnd(24)}${"CREATED".padEnd(12)}${"LAST USED".padEnd(12)}EXPIRES`);
|
|
11960
|
+
for (const s of list) {
|
|
11961
|
+
const days = daysUntil(s.expires_at);
|
|
11962
|
+
const expiry = s.expires_at ? `${shortDate(s.expires_at)}${days != null ? ` (${days}d)` : ""}` : "never";
|
|
11963
|
+
const device = (s.device ?? "login").slice(0, 22);
|
|
11964
|
+
printInfo(
|
|
11965
|
+
`${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" : ""}`
|
|
11966
|
+
);
|
|
11967
|
+
}
|
|
11968
|
+
printInfo("");
|
|
11969
|
+
printInfo("Revoke one: verity sessions revoke <session-id>");
|
|
11970
|
+
printInfo("Sign out everywhere else: verity logout --others");
|
|
11971
|
+
});
|
|
11972
|
+
sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
|
|
11973
|
+
const globals = program2.opts();
|
|
11974
|
+
const { token, serviceUrl } = await auth(globals);
|
|
11975
|
+
const result = await apiRequest({
|
|
11976
|
+
method: "DELETE",
|
|
11977
|
+
path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
|
|
11978
|
+
serviceUrl,
|
|
11979
|
+
token,
|
|
11980
|
+
verbose: globals.verbose,
|
|
11981
|
+
cmd: "sessions-revoke"
|
|
11982
|
+
});
|
|
11983
|
+
if (!result.ok) {
|
|
11984
|
+
printError(result.error);
|
|
11985
|
+
if (result.http_status === 404) {
|
|
11986
|
+
printInfo(' No session with that id on your account \u2014 check "verity sessions list".');
|
|
11987
|
+
}
|
|
11988
|
+
explain(result.error);
|
|
11989
|
+
process.exit(1);
|
|
11990
|
+
}
|
|
11991
|
+
printInfo(`Session ${sessionId} revoked. \u2713`);
|
|
11992
|
+
if (result.data.was_current) {
|
|
11993
|
+
const cleared = await removeGlobalCredential("");
|
|
11994
|
+
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.');
|
|
11995
|
+
}
|
|
11996
|
+
});
|
|
11997
|
+
}
|
|
11998
|
+
function registerLogoutCommand(program2) {
|
|
11999
|
+
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) => {
|
|
12000
|
+
const globals = program2.opts();
|
|
12001
|
+
if (opts.all && opts.others) {
|
|
12002
|
+
printError("Use either --all or --others, not both.");
|
|
12003
|
+
process.exit(1);
|
|
12004
|
+
}
|
|
12005
|
+
const { token, serviceUrl } = await auth(globals);
|
|
12006
|
+
if (opts.all || opts.others) {
|
|
12007
|
+
const result = await apiRequest({
|
|
12008
|
+
method: "DELETE",
|
|
12009
|
+
path: opts.others ? "/auth/sessions?others=true" : "/auth/sessions",
|
|
12010
|
+
serviceUrl,
|
|
12011
|
+
token,
|
|
12012
|
+
verbose: globals.verbose,
|
|
12013
|
+
cmd: "logout"
|
|
12014
|
+
});
|
|
12015
|
+
if (!result.ok) {
|
|
12016
|
+
printError(result.error);
|
|
12017
|
+
explain(result.error);
|
|
12018
|
+
process.exit(1);
|
|
12019
|
+
}
|
|
12020
|
+
const n = result.data.revoked;
|
|
12021
|
+
printInfo(`Revoked ${n} login${n === 1 ? "" : "s"}. \u2713`);
|
|
12022
|
+
if (opts.others) {
|
|
12023
|
+
printInfo(" This machine is still signed in.");
|
|
12024
|
+
return;
|
|
12025
|
+
}
|
|
12026
|
+
const cleared2 = await removeGlobalCredential("");
|
|
12027
|
+
if (cleared2) printInfo(" Local credential cleared.");
|
|
12028
|
+
printInfo(' Run "verity login" to sign back in.');
|
|
12029
|
+
return;
|
|
12030
|
+
}
|
|
12031
|
+
const list = await apiRequest({
|
|
12032
|
+
method: "GET",
|
|
12033
|
+
path: "/auth/sessions",
|
|
12034
|
+
serviceUrl,
|
|
12035
|
+
token,
|
|
12036
|
+
verbose: globals.verbose,
|
|
12037
|
+
cmd: "logout"
|
|
12038
|
+
});
|
|
12039
|
+
if (!list.ok) {
|
|
12040
|
+
printError(list.error);
|
|
12041
|
+
explain(list.error);
|
|
12042
|
+
process.exit(1);
|
|
12043
|
+
}
|
|
12044
|
+
const current = list.data.sessions.find((s) => s.current);
|
|
12045
|
+
if (!current) {
|
|
12046
|
+
printWarn("This machine is not signed in with a Verity login.");
|
|
12047
|
+
const cleared2 = await removeGlobalCredential("");
|
|
12048
|
+
if (cleared2) printInfo(" Cleared the local login credential anyway.");
|
|
12049
|
+
return;
|
|
12050
|
+
}
|
|
12051
|
+
const revoked = await apiRequest({
|
|
12052
|
+
method: "DELETE",
|
|
12053
|
+
path: `/auth/sessions/${current.id}`,
|
|
12054
|
+
serviceUrl,
|
|
12055
|
+
token,
|
|
12056
|
+
verbose: globals.verbose,
|
|
12057
|
+
cmd: "logout"
|
|
12058
|
+
});
|
|
12059
|
+
if (!revoked.ok) {
|
|
12060
|
+
printError(revoked.error);
|
|
12061
|
+
explain(revoked.error);
|
|
12062
|
+
process.exit(1);
|
|
12063
|
+
}
|
|
12064
|
+
const cleared = await removeGlobalCredential("");
|
|
12065
|
+
printInfo("Signed out on this machine. \u2713");
|
|
12066
|
+
if (cleared) printInfo(" Local credential cleared.");
|
|
12067
|
+
printInfo(' Your other machines are unaffected \u2014 use "verity logout --all" for all of them.');
|
|
12068
|
+
});
|
|
12069
|
+
}
|
|
12070
|
+
|
|
11812
12071
|
// src/lib/hooks.ts
|
|
11813
12072
|
var import_promises4 = require("node:fs/promises");
|
|
11814
12073
|
var import_node_path5 = require("node:path");
|
|
@@ -12430,7 +12689,7 @@ function getRecentCommitMessages() {
|
|
|
12430
12689
|
// src/lib/context-identity.ts
|
|
12431
12690
|
var import_node_crypto2 = require("node:crypto");
|
|
12432
12691
|
var import_node_fs5 = require("node:fs");
|
|
12433
|
-
var
|
|
12692
|
+
var import_node_os2 = require("node:os");
|
|
12434
12693
|
var import_node_path6 = require("node:path");
|
|
12435
12694
|
var SHARED_SENTINELS = /* @__PURE__ */ new Set([
|
|
12436
12695
|
"",
|
|
@@ -12485,7 +12744,7 @@ function contextIdentity(input) {
|
|
|
12485
12744
|
}
|
|
12486
12745
|
function verityHome() {
|
|
12487
12746
|
const override = process.env.VERITY_HOME;
|
|
12488
|
-
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0,
|
|
12747
|
+
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".verity");
|
|
12489
12748
|
}
|
|
12490
12749
|
function dossierDir(identity) {
|
|
12491
12750
|
return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
|
|
@@ -13331,6 +13590,70 @@ var import_node_fs10 = require("node:fs");
|
|
|
13331
13590
|
var import_node_crypto5 = require("node:crypto");
|
|
13332
13591
|
var import_node_path11 = require("node:path");
|
|
13333
13592
|
|
|
13593
|
+
// src/lib/skip-detection.ts
|
|
13594
|
+
function isBareAckPrompt(prompt) {
|
|
13595
|
+
if (typeof prompt !== "string") return false;
|
|
13596
|
+
const trimmed = prompt.trim();
|
|
13597
|
+
if (trimmed.length === 0) return false;
|
|
13598
|
+
if (trimmed.length > 20) return false;
|
|
13599
|
+
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;
|
|
13600
|
+
return bareAckPattern.test(trimmed);
|
|
13601
|
+
}
|
|
13602
|
+
function isContinuationPrompt(prompt) {
|
|
13603
|
+
if (typeof prompt !== "string") return false;
|
|
13604
|
+
const trimmed = prompt.trim();
|
|
13605
|
+
if (trimmed.length === 0) return false;
|
|
13606
|
+
if (trimmed.length > 24) return false;
|
|
13607
|
+
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;
|
|
13608
|
+
return continuation.test(trimmed) || isBareAckPrompt(trimmed);
|
|
13609
|
+
}
|
|
13610
|
+
function resolveGoalPrompt(prompts) {
|
|
13611
|
+
if (prompts.length === 0) return null;
|
|
13612
|
+
const latest = prompts[prompts.length - 1];
|
|
13613
|
+
if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
|
|
13614
|
+
for (let i = prompts.length - 2; i >= 0; i--) {
|
|
13615
|
+
if (!isContinuationPrompt(prompts[i].prompt)) {
|
|
13616
|
+
return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
return { entry: latest, turnsBack: 0 };
|
|
13620
|
+
}
|
|
13621
|
+
function isReflectionQuestion(response) {
|
|
13622
|
+
if (!response || typeof response !== "string") return false;
|
|
13623
|
+
const markers = [
|
|
13624
|
+
/reflection\s+for\s+future\s+agents/i,
|
|
13625
|
+
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
13626
|
+
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
13627
|
+
/quick\s+reflection\s+question/i,
|
|
13628
|
+
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
13629
|
+
// interactive, asks the user to confirm/correct before recording. That
|
|
13630
|
+
// turn authors no code either, so it's still a reflection turn.
|
|
13631
|
+
/reflection\s+draft/i,
|
|
13632
|
+
/confirm,?\s+correct,?\s+or\s+add/i
|
|
13633
|
+
];
|
|
13634
|
+
return markers.some((m) => m.test(response));
|
|
13635
|
+
}
|
|
13636
|
+
function isMetaTaskLabel(label2) {
|
|
13637
|
+
if (label2 === null || label2 === void 0) return false;
|
|
13638
|
+
if (typeof label2 !== "string") return false;
|
|
13639
|
+
const trimmed = label2.trim();
|
|
13640
|
+
if (trimmed.length === 0) return true;
|
|
13641
|
+
const metaPatterns = [
|
|
13642
|
+
/^verity\s+[\w-]+\s+response$/i,
|
|
13643
|
+
// "Verity reflect response"
|
|
13644
|
+
/^simple user response$/i,
|
|
13645
|
+
/^verity\s+command$/i,
|
|
13646
|
+
// "Verity command"
|
|
13647
|
+
/^user\s+(question|reply|response|ack)$/i
|
|
13648
|
+
];
|
|
13649
|
+
return metaPatterns.some((p) => p.test(trimmed));
|
|
13650
|
+
}
|
|
13651
|
+
function shouldSkipForBareAck(input) {
|
|
13652
|
+
if (!isBareAckPrompt(input.prompt)) return false;
|
|
13653
|
+
if (input.turnAuthoredCode) return false;
|
|
13654
|
+
return input.canSeeTurnAuthorship;
|
|
13655
|
+
}
|
|
13656
|
+
|
|
13334
13657
|
// src/lib/dossier.ts
|
|
13335
13658
|
var import_node_fs9 = require("node:fs");
|
|
13336
13659
|
var import_node_crypto4 = require("node:crypto");
|
|
@@ -13700,6 +14023,12 @@ function reduce(state, events, now) {
|
|
|
13700
14023
|
}
|
|
13701
14024
|
case "verdict": {
|
|
13702
14025
|
state.meta.last_verdict_seq = ev.seq;
|
|
14026
|
+
state.meta.channel = {
|
|
14027
|
+
emittedLast: ev.emitted === true,
|
|
14028
|
+
// Reset by ANY movement, so the counter measures a standstill rather
|
|
14029
|
+
// than session length.
|
|
14030
|
+
consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
|
|
14031
|
+
};
|
|
13703
14032
|
state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
|
|
13704
14033
|
if (ev.intent_sig) {
|
|
13705
14034
|
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 };
|
|
@@ -14067,6 +14396,10 @@ function projectMemory(state, opts) {
|
|
|
14067
14396
|
...active.delivered && { delivered: active.delivered }
|
|
14068
14397
|
};
|
|
14069
14398
|
}
|
|
14399
|
+
if (opts.capture) {
|
|
14400
|
+
const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
|
|
14401
|
+
p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
|
|
14402
|
+
}
|
|
14070
14403
|
if (state.meta.last_adjudication) {
|
|
14071
14404
|
const a = state.meta.last_adjudication;
|
|
14072
14405
|
p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
|
|
@@ -14313,7 +14646,8 @@ function recall(d, input) {
|
|
|
14313
14646
|
continuity,
|
|
14314
14647
|
spoken: reanchored.spoken,
|
|
14315
14648
|
refused: reanchored.dropped.length,
|
|
14316
|
-
lastVerdictSeq
|
|
14649
|
+
lastVerdictSeq,
|
|
14650
|
+
...input.capture && { capture: input.capture }
|
|
14317
14651
|
});
|
|
14318
14652
|
return {
|
|
14319
14653
|
state: effective,
|
|
@@ -14424,7 +14758,19 @@ function sessionDossier(token, sessionId) {
|
|
|
14424
14758
|
const d = openDossier(identity);
|
|
14425
14759
|
return d ? { d, identity } : null;
|
|
14426
14760
|
}
|
|
14761
|
+
function hasActiveGoal(d) {
|
|
14762
|
+
try {
|
|
14763
|
+
if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
|
|
14764
|
+
return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
|
|
14765
|
+
} catch {
|
|
14766
|
+
return false;
|
|
14767
|
+
}
|
|
14768
|
+
}
|
|
14427
14769
|
function recordGoal(d, prompt, source = "prompt") {
|
|
14770
|
+
if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
|
|
14771
|
+
appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
|
|
14772
|
+
return;
|
|
14773
|
+
}
|
|
14428
14774
|
const text = prompt.slice(0, MAX_GOAL_CHARS);
|
|
14429
14775
|
appendEvent(d, {
|
|
14430
14776
|
k: "goal",
|
|
@@ -14527,6 +14873,11 @@ function recordVerdict(d, v) {
|
|
|
14527
14873
|
line_sha: at !== void 0 ? lineSha(at) : null
|
|
14528
14874
|
});
|
|
14529
14875
|
}
|
|
14876
|
+
const foldedNow = foldDossier(d);
|
|
14877
|
+
const sig = intentSignature(v.intent, {
|
|
14878
|
+
goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
|
|
14879
|
+
idle: v.idle !== false
|
|
14880
|
+
});
|
|
14530
14881
|
appendEvent(d, {
|
|
14531
14882
|
k: "verdict",
|
|
14532
14883
|
run_id: v.runId,
|
|
@@ -14534,15 +14885,19 @@ function recordVerdict(d, v) {
|
|
|
14534
14885
|
watermark_sha: v.watermarkSha,
|
|
14535
14886
|
branch: v.branch,
|
|
14536
14887
|
decision: v.decision,
|
|
14537
|
-
...
|
|
14888
|
+
...sig && { intent_sig: sig },
|
|
14889
|
+
emitted: v.emitted === true,
|
|
14890
|
+
idle: v.idle !== false,
|
|
14538
14891
|
...v.intent?.verdict && { intent_verdict: v.intent.verdict },
|
|
14539
14892
|
...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
|
|
14540
14893
|
});
|
|
14541
14894
|
}
|
|
14542
|
-
function intentSignature(intent) {
|
|
14895
|
+
function intentSignature(intent, ctx) {
|
|
14543
14896
|
if (!intent?.verdict) return null;
|
|
14544
14897
|
if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
|
|
14545
|
-
|
|
14898
|
+
const goal = ctx ? `g${ctx.goalSeq}` : "g?";
|
|
14899
|
+
const moved = ctx?.idle === false ? "active" : "idle";
|
|
14900
|
+
return `${intent.verdict}:${goal}:${moved}`;
|
|
14546
14901
|
}
|
|
14547
14902
|
function toRegister(severity) {
|
|
14548
14903
|
switch (severity) {
|
|
@@ -14579,7 +14934,9 @@ function recallMemory(d, identity, opts) {
|
|
|
14579
14934
|
const state = foldDossier(d);
|
|
14580
14935
|
const watermark = state.meta.watermark?.sha ?? null;
|
|
14581
14936
|
const watermarkPaths = (state.authored ?? []).map((a) => a.path);
|
|
14937
|
+
const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
|
|
14582
14938
|
const r = recall(d, {
|
|
14939
|
+
...captureCmp && { capture: captureCmp },
|
|
14583
14940
|
identity,
|
|
14584
14941
|
currentSessionKey: opts.currentSessionKey,
|
|
14585
14942
|
branchNow: getCurrentBranch(),
|
|
@@ -14836,6 +15193,8 @@ function collectCodeDelta(files, opts) {
|
|
|
14836
15193
|
let totalSize = 0;
|
|
14837
15194
|
let truncationReason = null;
|
|
14838
15195
|
const droppedPaths = [];
|
|
15196
|
+
const excluded = [];
|
|
15197
|
+
const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
|
|
14839
15198
|
for (const filepath of sorted) {
|
|
14840
15199
|
if (result.length >= maxFiles) {
|
|
14841
15200
|
truncationReason ??= "max_files";
|
|
@@ -14843,14 +15202,21 @@ function collectCodeDelta(files, opts) {
|
|
|
14843
15202
|
continue;
|
|
14844
15203
|
}
|
|
14845
15204
|
const resolved = resolveFile(filepath);
|
|
14846
|
-
if (!resolved)
|
|
15205
|
+
if (!resolved) {
|
|
15206
|
+
exclude(filepath, "path-not-resolvable");
|
|
15207
|
+
continue;
|
|
15208
|
+
}
|
|
14847
15209
|
let size;
|
|
14848
15210
|
try {
|
|
14849
15211
|
size = (0, import_node_fs11.statSync)(resolved).size;
|
|
14850
15212
|
} catch {
|
|
15213
|
+
exclude(filepath, "not-stattable");
|
|
15214
|
+
continue;
|
|
15215
|
+
}
|
|
15216
|
+
if (size > maxFileBytes) {
|
|
15217
|
+
exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
|
|
14851
15218
|
continue;
|
|
14852
15219
|
}
|
|
14853
|
-
if (size > maxFileBytes) continue;
|
|
14854
15220
|
if (totalSize + size > maxTotalBytes) {
|
|
14855
15221
|
truncationReason ??= "max_total_bytes";
|
|
14856
15222
|
const idx = sorted.indexOf(filepath);
|
|
@@ -14861,6 +15227,7 @@ function collectCodeDelta(files, opts) {
|
|
|
14861
15227
|
try {
|
|
14862
15228
|
content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
|
|
14863
15229
|
} catch {
|
|
15230
|
+
exclude(filepath, "not-readable");
|
|
14864
15231
|
continue;
|
|
14865
15232
|
}
|
|
14866
15233
|
totalSize += size;
|
|
@@ -14874,10 +15241,14 @@ function collectCodeDelta(files, opts) {
|
|
|
14874
15241
|
(sum, f) => sum + f.content.split("\n").length,
|
|
14875
15242
|
0
|
|
14876
15243
|
);
|
|
15244
|
+
for (const path of droppedPaths) {
|
|
15245
|
+
exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
|
|
15246
|
+
}
|
|
14877
15247
|
return {
|
|
14878
15248
|
files: result,
|
|
14879
15249
|
total_lines: totalLines,
|
|
14880
15250
|
total_files: result.length,
|
|
15251
|
+
excluded,
|
|
14881
15252
|
...truncationReason && {
|
|
14882
15253
|
truncated: {
|
|
14883
15254
|
reason: truncationReason,
|
|
@@ -15171,6 +15542,34 @@ ${addedLines}`,
|
|
|
15171
15542
|
}
|
|
15172
15543
|
return { diffs, has_baseline: true };
|
|
15173
15544
|
}
|
|
15545
|
+
function absorbIntoBaseline(paths, sessionId) {
|
|
15546
|
+
const baseline = readBaseline(sessionId);
|
|
15547
|
+
if (!baseline || paths.length === 0) return 0;
|
|
15548
|
+
const dir = sessionDir(sessionKey(baseline.session_id));
|
|
15549
|
+
let adopted = 0;
|
|
15550
|
+
const dirty = new Set(baseline.dirty_paths);
|
|
15551
|
+
for (const p of paths) {
|
|
15552
|
+
try {
|
|
15553
|
+
const content = safeReadForMirror(projectPath(p));
|
|
15554
|
+
if (content === null) continue;
|
|
15555
|
+
const dest = mirrorPath(dir, p);
|
|
15556
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
15557
|
+
(0, import_node_fs13.writeFileSync)(dest, content);
|
|
15558
|
+
dirty.add(p);
|
|
15559
|
+
adopted++;
|
|
15560
|
+
} catch {
|
|
15561
|
+
}
|
|
15562
|
+
}
|
|
15563
|
+
if (adopted === 0) return 0;
|
|
15564
|
+
try {
|
|
15565
|
+
const updated = { ...baseline, dirty_paths: [...dirty] };
|
|
15566
|
+
(0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
|
|
15567
|
+
preImageCache.delete(baseline);
|
|
15568
|
+
} catch {
|
|
15569
|
+
return 0;
|
|
15570
|
+
}
|
|
15571
|
+
return adopted;
|
|
15572
|
+
}
|
|
15174
15573
|
function changedSinceBaseline(repoRelPath, baseline) {
|
|
15175
15574
|
const pre = preImage(repoRelPath, baseline);
|
|
15176
15575
|
let current;
|
|
@@ -16047,40 +16446,40 @@ function narrowToRecent(files, sessionId) {
|
|
|
16047
16446
|
});
|
|
16048
16447
|
return recent.length > 0 ? recent : files;
|
|
16049
16448
|
}
|
|
16050
|
-
function
|
|
16051
|
-
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
|
|
16449
|
+
function readIterationState(currentCommit) {
|
|
16450
|
+
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
|
|
16052
16451
|
try {
|
|
16053
16452
|
const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
|
|
16054
16453
|
const parts = stored.split(":");
|
|
16055
16454
|
const iter = parseInt(parts[0], 10);
|
|
16056
16455
|
const storedCommit = parts[1] ?? "";
|
|
16057
16456
|
const storedTimestamp = parseInt(parts[2] ?? "0", 10);
|
|
16058
|
-
|
|
16059
|
-
if (
|
|
16457
|
+
const fingerprint = parts.slice(3).join(":") || null;
|
|
16458
|
+
if (isNaN(iter)) return { iteration: 1, fingerprint: null };
|
|
16459
|
+
if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
|
|
16060
16460
|
if (storedTimestamp > 0) {
|
|
16061
16461
|
const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
|
|
16062
|
-
if (elapsed > 600) return 1;
|
|
16462
|
+
if (elapsed > 600) return { iteration: 1, fingerprint: null };
|
|
16063
16463
|
}
|
|
16064
|
-
return iter;
|
|
16464
|
+
return { iteration: iter, fingerprint };
|
|
16065
16465
|
} catch {
|
|
16066
|
-
return 1;
|
|
16466
|
+
return { iteration: 1, fingerprint: null };
|
|
16067
16467
|
}
|
|
16068
16468
|
}
|
|
16069
|
-
function
|
|
16070
|
-
const
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
|
|
16077
|
-
}
|
|
16078
|
-
return { skip: null, iteration };
|
|
16469
|
+
function findingsFingerprint(findings) {
|
|
16470
|
+
const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
|
|
16471
|
+
return [...new Set(keys)].sort().join(",");
|
|
16472
|
+
}
|
|
16473
|
+
function isSameProblem(previous, current) {
|
|
16474
|
+
if (!previous || !current) return false;
|
|
16475
|
+
const prev = new Set(previous.split(","));
|
|
16476
|
+
return current.split(",").some((k) => prev.has(k));
|
|
16079
16477
|
}
|
|
16080
|
-
function writeIteration(iteration, commit, _contentHash) {
|
|
16478
|
+
function writeIteration(iteration, commit, _contentHash, fingerprint) {
|
|
16081
16479
|
(0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
|
|
16082
16480
|
const ts = Math.floor(Date.now() / 1e3);
|
|
16083
|
-
|
|
16481
|
+
const fp = fingerprint ? `:${fingerprint}` : "";
|
|
16482
|
+
(0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
|
|
16084
16483
|
}
|
|
16085
16484
|
|
|
16086
16485
|
// src/lib/static-analysis.ts
|
|
@@ -16329,7 +16728,7 @@ function resolveTaskContext(opts) {
|
|
|
16329
16728
|
// src/lib/cli-version.ts
|
|
16330
16729
|
function cliVersion() {
|
|
16331
16730
|
try {
|
|
16332
|
-
return true ? "0.28.1-experimental.
|
|
16731
|
+
return true ? "0.28.1-experimental.6eac6aa" : "dev";
|
|
16333
16732
|
} catch {
|
|
16334
16733
|
return "dev";
|
|
16335
16734
|
}
|
|
@@ -16554,6 +16953,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
|
16554
16953
|
"subagent"
|
|
16555
16954
|
]);
|
|
16556
16955
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
|
|
16956
|
+
var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
|
|
16957
|
+
function hasUserText(record) {
|
|
16958
|
+
const message = record.message;
|
|
16959
|
+
const content = message?.content ?? record.content;
|
|
16960
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
16961
|
+
if (!Array.isArray(content)) return false;
|
|
16962
|
+
return content.some(
|
|
16963
|
+
(b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
|
|
16964
|
+
);
|
|
16965
|
+
}
|
|
16557
16966
|
var COMMAND_CLASSES = [
|
|
16558
16967
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
|
|
16559
16968
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
|
|
@@ -16680,6 +17089,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16680
17089
|
totalRecords: 0,
|
|
16681
17090
|
malformed: 0,
|
|
16682
17091
|
subagentFiles: 0,
|
|
17092
|
+
dispatched: 0,
|
|
17093
|
+
userMessages: 0,
|
|
16683
17094
|
subagentSkipped: 0,
|
|
16684
17095
|
compactions: 0,
|
|
16685
17096
|
complete: false
|
|
@@ -16706,7 +17117,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16706
17117
|
if (type === "system" && record.subtype === "compact_boundary") {
|
|
16707
17118
|
result.coverage.compactions++;
|
|
16708
17119
|
}
|
|
16709
|
-
|
|
17120
|
+
if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
|
|
17121
|
+
collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
|
|
16710
17122
|
}
|
|
16711
17123
|
};
|
|
16712
17124
|
try {
|
|
@@ -16778,7 +17190,7 @@ function classifyUnobserved(path) {
|
|
|
16778
17190
|
if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
|
|
16779
17191
|
return "no_edit_record";
|
|
16780
17192
|
}
|
|
16781
|
-
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
|
|
17193
|
+
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
|
|
16782
17194
|
const message = record.message;
|
|
16783
17195
|
const content = message?.content ?? record.content;
|
|
16784
17196
|
const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
|
|
@@ -16800,6 +17212,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
|
|
|
16800
17212
|
byPath.set(path, entry);
|
|
16801
17213
|
}
|
|
16802
17214
|
}
|
|
17215
|
+
if (DISPATCH_TOOLS.has(name) && tally) {
|
|
17216
|
+
tally.dispatched += 1;
|
|
17217
|
+
}
|
|
16803
17218
|
if (name === "Bash") {
|
|
16804
17219
|
const cmd = typeof input.command === "string" ? input.command : "";
|
|
16805
17220
|
if (cmd) {
|
|
@@ -16858,6 +17273,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
|
|
|
16858
17273
|
};
|
|
16859
17274
|
}
|
|
16860
17275
|
|
|
17276
|
+
// src/lib/verdict.ts
|
|
17277
|
+
function reconcileCoverage(changed, coverage) {
|
|
17278
|
+
const changedSet = new Set(changed);
|
|
17279
|
+
const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
|
|
17280
|
+
const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
|
|
17281
|
+
const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
|
|
17282
|
+
const notReviewed = [
|
|
17283
|
+
...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
|
|
17284
|
+
...unaccounted.map((path) => ({
|
|
17285
|
+
path,
|
|
17286
|
+
reason: "unaccounted",
|
|
17287
|
+
// Named so the eventual bug report writes itself: some stage removed this
|
|
17288
|
+
// path and did not say so.
|
|
17289
|
+
stage: "unknown-stage",
|
|
17290
|
+
// An undeclared drop is CAPACITY by default. A stage that cannot be
|
|
17291
|
+
// bothered to say why it dropped a file does not get the benefit of the
|
|
17292
|
+
// doubt — that default is what makes forgetting expensive.
|
|
17293
|
+
kind: "capacity"
|
|
17294
|
+
}))
|
|
17295
|
+
];
|
|
17296
|
+
return {
|
|
17297
|
+
coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
|
|
17298
|
+
unaccounted,
|
|
17299
|
+
balances: unaccounted.length === 0
|
|
17300
|
+
};
|
|
17301
|
+
}
|
|
17302
|
+
function resolveVerdict(proposed, coverage) {
|
|
17303
|
+
if (proposed === "FAIL") return "FAIL";
|
|
17304
|
+
const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17305
|
+
if (blocking.length === 0) return proposed;
|
|
17306
|
+
return "WARN";
|
|
17307
|
+
}
|
|
17308
|
+
function describeCoverage(coverage, maxPaths = 5) {
|
|
17309
|
+
const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17310
|
+
if (relevant.length === 0) return null;
|
|
17311
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
17312
|
+
for (const n of relevant) {
|
|
17313
|
+
const key = `${n.reason}`;
|
|
17314
|
+
const list = byReason.get(key) ?? [];
|
|
17315
|
+
list.push(n.path);
|
|
17316
|
+
byReason.set(key, list);
|
|
17317
|
+
}
|
|
17318
|
+
const lines = [];
|
|
17319
|
+
for (const [reason, paths] of [...byReason.entries()].sort()) {
|
|
17320
|
+
const shown = paths.slice(0, maxPaths).join(", ");
|
|
17321
|
+
const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
|
|
17322
|
+
lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
|
|
17323
|
+
}
|
|
17324
|
+
return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
|
|
17325
|
+
${lines.join("\n")}
|
|
17326
|
+
Treat those files as UNCHECKED, not as approved.`;
|
|
17327
|
+
}
|
|
17328
|
+
function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
|
|
17329
|
+
const reviewed = new Set(reviewedNow);
|
|
17330
|
+
const out = [];
|
|
17331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17332
|
+
for (const s of statements) {
|
|
17333
|
+
if (s.outcome !== "open") continue;
|
|
17334
|
+
if (s.register !== "BLOCK") continue;
|
|
17335
|
+
if (s.carried) continue;
|
|
17336
|
+
if (reviewed.has(s.file)) continue;
|
|
17337
|
+
if (!s.line_sha) continue;
|
|
17338
|
+
if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
|
|
17339
|
+
const key = `${s.file}::${s.pattern_id}`;
|
|
17340
|
+
if (seen.has(key)) continue;
|
|
17341
|
+
seen.add(key);
|
|
17342
|
+
out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
|
|
17343
|
+
}
|
|
17344
|
+
return out;
|
|
17345
|
+
}
|
|
17346
|
+
function describeOpenElsewhere(open) {
|
|
17347
|
+
if (open.length === 0) return null;
|
|
17348
|
+
const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
|
|
17349
|
+
const more = open.length > 5 ? `
|
|
17350
|
+
(+${open.length - 5} more)` : "";
|
|
17351
|
+
return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
|
|
17352
|
+
${lines.join("\n")}${more}
|
|
17353
|
+
This verdict covers the current change only. The tree is not clean.`;
|
|
17354
|
+
}
|
|
17355
|
+
|
|
16861
17356
|
// src/lib/channel.ts
|
|
16862
17357
|
var MAX_AGENT_CONTEXT_CHARS = 1500;
|
|
16863
17358
|
var MAX_AGENT_ITEMS = 5;
|
|
@@ -16907,6 +17402,7 @@ function buildAgentContext(input) {
|
|
|
16907
17402
|
}
|
|
16908
17403
|
for (const p of input.pendingItems ?? []) {
|
|
16909
17404
|
if (lines.length >= MAX_AGENT_ITEMS) break;
|
|
17405
|
+
if (p.pattern_id === "intent-misalignment") continue;
|
|
16910
17406
|
const text = p.description ?? p.title ?? p.reason;
|
|
16911
17407
|
if (!text) continue;
|
|
16912
17408
|
lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
|
|
@@ -16936,6 +17432,47 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
|
|
|
16936
17432
|
} : {}
|
|
16937
17433
|
};
|
|
16938
17434
|
}
|
|
17435
|
+
var IDLE_EPISODE_CAP = 3;
|
|
17436
|
+
function channelSilence(input) {
|
|
17437
|
+
const movedSomething = input.newUserPrompt || input.newAuthorship;
|
|
17438
|
+
if (movedSomething) return null;
|
|
17439
|
+
if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
|
|
17440
|
+
if (input.emittedLast) return "caused-by-our-own-emission";
|
|
17441
|
+
return null;
|
|
17442
|
+
}
|
|
17443
|
+
|
|
17444
|
+
// src/lib/emit.ts
|
|
17445
|
+
var YELLOW2 = "\x1B[33m";
|
|
17446
|
+
var NC2 = "\x1B[0m";
|
|
17447
|
+
function emitVerdict(input) {
|
|
17448
|
+
const exit = input.exit ?? ((code) => process.exit(code));
|
|
17449
|
+
const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
|
|
17450
|
+
let verdict = resolveVerdict(input.proposed, coverage);
|
|
17451
|
+
const openElsewhere = input.openElsewhere ?? [];
|
|
17452
|
+
if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
|
|
17453
|
+
const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
|
|
17454
|
+
if (unaccounted.length > 0) {
|
|
17455
|
+
process.stderr.write(
|
|
17456
|
+
`${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
|
|
17457
|
+
`
|
|
17458
|
+
);
|
|
17459
|
+
}
|
|
17460
|
+
if (verdict === "FAIL") {
|
|
17461
|
+
input.renderBlocking?.();
|
|
17462
|
+
if (input.agentContext) {
|
|
17463
|
+
process.stderr.write(`
|
|
17464
|
+
${input.agentContext}
|
|
17465
|
+
`);
|
|
17466
|
+
}
|
|
17467
|
+
if (note && !input.silenced) process.stderr.write(`
|
|
17468
|
+
${YELLOW2}${note}${NC2}
|
|
17469
|
+
`);
|
|
17470
|
+
return exit(2);
|
|
17471
|
+
}
|
|
17472
|
+
const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
|
|
17473
|
+
printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
|
|
17474
|
+
return exit(0);
|
|
17475
|
+
}
|
|
16939
17476
|
|
|
16940
17477
|
// src/lib/cache-cleanup.ts
|
|
16941
17478
|
var import_node_fs21 = require("node:fs");
|
|
@@ -17116,46 +17653,6 @@ function shouldWarmRetryAnalyze(result) {
|
|
|
17116
17653
|
return false;
|
|
17117
17654
|
}
|
|
17118
17655
|
|
|
17119
|
-
// src/lib/skip-detection.ts
|
|
17120
|
-
function isBareAckPrompt(prompt) {
|
|
17121
|
-
if (typeof prompt !== "string") return false;
|
|
17122
|
-
const trimmed = prompt.trim();
|
|
17123
|
-
if (trimmed.length === 0) return false;
|
|
17124
|
-
if (trimmed.length > 20) return false;
|
|
17125
|
-
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;
|
|
17126
|
-
return bareAckPattern.test(trimmed);
|
|
17127
|
-
}
|
|
17128
|
-
function isReflectionQuestion(response) {
|
|
17129
|
-
if (!response || typeof response !== "string") return false;
|
|
17130
|
-
const markers = [
|
|
17131
|
-
/reflection\s+for\s+future\s+agents/i,
|
|
17132
|
-
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
17133
|
-
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
17134
|
-
/quick\s+reflection\s+question/i,
|
|
17135
|
-
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
17136
|
-
// interactive, asks the user to confirm/correct before recording. That
|
|
17137
|
-
// turn authors no code either, so it's still a reflection turn.
|
|
17138
|
-
/reflection\s+draft/i,
|
|
17139
|
-
/confirm,?\s+correct,?\s+or\s+add/i
|
|
17140
|
-
];
|
|
17141
|
-
return markers.some((m) => m.test(response));
|
|
17142
|
-
}
|
|
17143
|
-
function isMetaTaskLabel(label2) {
|
|
17144
|
-
if (label2 === null || label2 === void 0) return false;
|
|
17145
|
-
if (typeof label2 !== "string") return false;
|
|
17146
|
-
const trimmed = label2.trim();
|
|
17147
|
-
if (trimmed.length === 0) return true;
|
|
17148
|
-
const metaPatterns = [
|
|
17149
|
-
/^verity\s+[\w-]+\s+response$/i,
|
|
17150
|
-
// "Verity reflect response"
|
|
17151
|
-
/^simple user response$/i,
|
|
17152
|
-
/^verity\s+command$/i,
|
|
17153
|
-
// "Verity command"
|
|
17154
|
-
/^user\s+(question|reply|response|ack)$/i
|
|
17155
|
-
];
|
|
17156
|
-
return metaPatterns.some((p) => p.test(trimmed));
|
|
17157
|
-
}
|
|
17158
|
-
|
|
17159
17656
|
// src/lib/transcript.ts
|
|
17160
17657
|
var import_node_fs22 = require("node:fs");
|
|
17161
17658
|
var MAX_READ_BYTES = 256 * 1024;
|
|
@@ -17169,9 +17666,11 @@ var MAX_SUMMARY_BYTES = 4096;
|
|
|
17169
17666
|
var HOME = process.env.HOME ?? "";
|
|
17170
17667
|
async function extractActionSummary(transcriptPath) {
|
|
17171
17668
|
try {
|
|
17172
|
-
const
|
|
17173
|
-
if (!
|
|
17174
|
-
|
|
17669
|
+
const read = readTurnLines(transcriptPath);
|
|
17670
|
+
if (!read || read.lines.length === 0) return null;
|
|
17671
|
+
const summary = buildSummary(read.lines);
|
|
17672
|
+
if (summary) summary.transcript_windowed = read.window;
|
|
17673
|
+
return summary;
|
|
17175
17674
|
} catch {
|
|
17176
17675
|
return null;
|
|
17177
17676
|
}
|
|
@@ -17185,9 +17684,11 @@ function readTurnLines(transcriptPath) {
|
|
|
17185
17684
|
}
|
|
17186
17685
|
if (size === 0) return null;
|
|
17187
17686
|
let raw;
|
|
17687
|
+
let windowed = false;
|
|
17188
17688
|
if (size <= SMALL_FILE_BYTES) {
|
|
17189
17689
|
raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
|
|
17190
17690
|
} else {
|
|
17691
|
+
windowed = true;
|
|
17191
17692
|
const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
|
|
17192
17693
|
const fd = require("node:fs").openSync(transcriptPath, "r");
|
|
17193
17694
|
try {
|
|
@@ -17205,17 +17706,22 @@ function readTurnLines(transcriptPath) {
|
|
|
17205
17706
|
const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
17206
17707
|
if (allLines.length === 0) return null;
|
|
17207
17708
|
let turnStart = 0;
|
|
17709
|
+
let boundaryFound = false;
|
|
17208
17710
|
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
17209
17711
|
try {
|
|
17210
17712
|
const parsed = JSON.parse(allLines[i]);
|
|
17211
17713
|
if (parsed.type === "user" && isRealUserMessage(parsed)) {
|
|
17212
17714
|
turnStart = i;
|
|
17715
|
+
boundaryFound = true;
|
|
17213
17716
|
break;
|
|
17214
17717
|
}
|
|
17215
17718
|
} catch {
|
|
17216
17719
|
}
|
|
17217
17720
|
}
|
|
17218
|
-
return
|
|
17721
|
+
return {
|
|
17722
|
+
lines: allLines.slice(turnStart),
|
|
17723
|
+
window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
|
|
17724
|
+
};
|
|
17219
17725
|
}
|
|
17220
17726
|
function isRealUserMessage(parsed) {
|
|
17221
17727
|
const message = parsed.message;
|
|
@@ -17319,6 +17825,13 @@ function buildSummary(lines) {
|
|
|
17319
17825
|
files_read: capArray(filesRead, MAX_FILES_LIST),
|
|
17320
17826
|
files_edited: capArray(filesEdited, MAX_FILES_LIST),
|
|
17321
17827
|
files_created: capArray(filesCreated, MAX_CREATED_LIST),
|
|
17828
|
+
// The complement of the two caps that affect SCOPE. `files_read` is excluded
|
|
17829
|
+
// deliberately: reading a file is not authoring it, so a capped read list
|
|
17830
|
+
// narrows nothing.
|
|
17831
|
+
capped_out: [
|
|
17832
|
+
...cappedOut(filesEdited, MAX_FILES_LIST),
|
|
17833
|
+
...cappedOut(filesCreated, MAX_CREATED_LIST)
|
|
17834
|
+
],
|
|
17322
17835
|
searches,
|
|
17323
17836
|
commands,
|
|
17324
17837
|
subagents,
|
|
@@ -17369,6 +17882,9 @@ function sanitizeCommand(rawCmd) {
|
|
|
17369
17882
|
function capArray(set, max) {
|
|
17370
17883
|
return Array.from(set).slice(0, max);
|
|
17371
17884
|
}
|
|
17885
|
+
function cappedOut(set, max) {
|
|
17886
|
+
return Array.from(set).slice(max);
|
|
17887
|
+
}
|
|
17372
17888
|
|
|
17373
17889
|
// src/lib/run-mode.ts
|
|
17374
17890
|
function parseAutonomousEnv(raw) {
|
|
@@ -17778,12 +18294,45 @@ function agentContextFor(response, intentRepeat = 0) {
|
|
|
17778
18294
|
});
|
|
17779
18295
|
}
|
|
17780
18296
|
var beaconCtx = null;
|
|
17781
|
-
async function passAndExit(reason, skip) {
|
|
18297
|
+
async function passAndExit(reason, skip, kindOverride) {
|
|
17782
18298
|
const sent = await sendSkipBeacon(beaconCtx, skip);
|
|
17783
18299
|
logEvent("skip", { reason: skip, beacon: sent });
|
|
17784
|
-
|
|
18300
|
+
const POLICY_SKIPS = /* @__PURE__ */ new Set([
|
|
18301
|
+
"no-analyzable-files",
|
|
18302
|
+
"verity-command",
|
|
18303
|
+
"bare-acknowledgment",
|
|
18304
|
+
"reflection-prompt",
|
|
18305
|
+
"skip-mode",
|
|
18306
|
+
"zero-increment",
|
|
18307
|
+
"debounce",
|
|
18308
|
+
"no-delta-since-last-review"
|
|
18309
|
+
]);
|
|
18310
|
+
const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
|
|
18311
|
+
const changed = skipCoverageChanged;
|
|
18312
|
+
const { coverage, unaccounted } = reconcileCoverage(changed, {
|
|
18313
|
+
reviewed: [],
|
|
18314
|
+
notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
|
|
18315
|
+
});
|
|
18316
|
+
const verdict = resolveVerdict("PASS", coverage);
|
|
18317
|
+
const note = describeCoverage(coverage);
|
|
18318
|
+
if (unaccounted.length > 0) {
|
|
18319
|
+
logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
|
|
18320
|
+
}
|
|
18321
|
+
const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
|
|
18322
|
+
const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
|
|
18323
|
+
printJsonCompact(
|
|
18324
|
+
buildHookOutput(
|
|
18325
|
+
verdict,
|
|
18326
|
+
`Verity: ${reason}`,
|
|
18327
|
+
// The agent's ONLY input is additionalContext. Sixteen of the nineteen
|
|
18328
|
+
// terminating paths wrote `systemMessage` — the human's field — and told
|
|
18329
|
+
// the agent nothing at all.
|
|
18330
|
+
agentNote
|
|
18331
|
+
)
|
|
18332
|
+
);
|
|
17785
18333
|
process.exit(0);
|
|
17786
18334
|
}
|
|
18335
|
+
var skipCoverageChanged = [];
|
|
17787
18336
|
var EMPTY_STATIC = {
|
|
17788
18337
|
tool: "@codacy/analysis-cli",
|
|
17789
18338
|
findings: [],
|
|
@@ -17798,7 +18347,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
|
|
|
17798
18347
|
}
|
|
17799
18348
|
function localOnlyAndExit(staticResults) {
|
|
17800
18349
|
printJsonCompact({
|
|
17801
|
-
gate_decision: "
|
|
18350
|
+
gate_decision: "WARN",
|
|
17802
18351
|
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.",
|
|
17803
18352
|
unauthenticated: true,
|
|
17804
18353
|
static_results: staticResults
|
|
@@ -17858,6 +18407,7 @@ async function runAnalyze(opts, globals) {
|
|
|
17858
18407
|
});
|
|
17859
18408
|
}
|
|
17860
18409
|
const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
|
|
18410
|
+
skipCoverageChanged = allChanged;
|
|
17861
18411
|
const analyzable = filterAnalyzable(allChanged);
|
|
17862
18412
|
const reviewable = filterReviewable(allChanged);
|
|
17863
18413
|
const securityFiles = filterSecurity(allChanged);
|
|
@@ -17869,14 +18419,24 @@ async function runAnalyze(opts, globals) {
|
|
|
17869
18419
|
const conversation = await readAndClearConversationBuffer(baselineSessionId);
|
|
17870
18420
|
const specs = discoverSpecs();
|
|
17871
18421
|
const plans = discoverPlans();
|
|
18422
|
+
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
18423
|
+
const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
|
|
18424
|
+
const canSeeTurnAuthorship = !!actionSummary || !!baseline;
|
|
17872
18425
|
const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
|
|
17873
18426
|
if (/^\s*\/verity-/i.test(latestPrompt)) {
|
|
18427
|
+
const setupAuthored = [
|
|
18428
|
+
...actionSummary?.files_edited ?? [],
|
|
18429
|
+
...actionSummary?.files_created ?? []
|
|
18430
|
+
];
|
|
18431
|
+
if (setupAuthored.length > 0) {
|
|
18432
|
+
const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
|
|
18433
|
+
logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
|
|
18434
|
+
}
|
|
17874
18435
|
await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
|
|
17875
18436
|
}
|
|
17876
|
-
if (
|
|
18437
|
+
if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
|
|
17877
18438
|
await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
|
|
17878
18439
|
}
|
|
17879
|
-
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
17880
18440
|
if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
|
|
17881
18441
|
await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
|
|
17882
18442
|
}
|
|
@@ -17923,7 +18483,11 @@ async function runAnalyze(opts, globals) {
|
|
|
17923
18483
|
);
|
|
17924
18484
|
}
|
|
17925
18485
|
if (analysisMode === "skip") {
|
|
17926
|
-
await passAndExit(
|
|
18486
|
+
await passAndExit(
|
|
18487
|
+
"Skip mode \u2014 no code work to analyze",
|
|
18488
|
+
"skip-mode",
|
|
18489
|
+
turnAuthoredCode ? "capacity" : void 0
|
|
18490
|
+
);
|
|
17927
18491
|
}
|
|
17928
18492
|
let staticResults = {
|
|
17929
18493
|
tool: "@codacy/analysis-cli",
|
|
@@ -17933,7 +18497,8 @@ async function runAnalyze(opts, globals) {
|
|
|
17933
18497
|
let codeDelta = {
|
|
17934
18498
|
files: [],
|
|
17935
18499
|
total_lines: 0,
|
|
17936
|
-
total_files: 0
|
|
18500
|
+
total_files: 0,
|
|
18501
|
+
excluded: []
|
|
17937
18502
|
};
|
|
17938
18503
|
let snapshotResult = { has_snapshots: false, diffs: [] };
|
|
17939
18504
|
let contentHash = null;
|
|
@@ -17998,7 +18563,11 @@ async function runAnalyze(opts, globals) {
|
|
|
17998
18563
|
if (assistantResponse) {
|
|
17999
18564
|
analysisMode = "plan";
|
|
18000
18565
|
} else {
|
|
18001
|
-
await passAndExit(
|
|
18566
|
+
await passAndExit(
|
|
18567
|
+
"No files within size limits to analyze",
|
|
18568
|
+
"size-limit",
|
|
18569
|
+
codeDelta.excluded.length > 0 ? "capacity" : "policy"
|
|
18570
|
+
);
|
|
18002
18571
|
}
|
|
18003
18572
|
}
|
|
18004
18573
|
}
|
|
@@ -18011,19 +18580,13 @@ async function runAnalyze(opts, globals) {
|
|
|
18011
18580
|
snapshotResult = generateSnapshotDiffs(codeDelta.files);
|
|
18012
18581
|
}
|
|
18013
18582
|
currentCommit = getCurrentCommit();
|
|
18014
|
-
|
|
18015
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
|
|
18016
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18017
|
-
iteration = iterResult.iteration;
|
|
18583
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18018
18584
|
}
|
|
18019
18585
|
}
|
|
18020
18586
|
if (analysisMode === "plan") {
|
|
18021
18587
|
recordAnalysisStart();
|
|
18022
18588
|
currentCommit = getCurrentCommit();
|
|
18023
|
-
|
|
18024
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations);
|
|
18025
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18026
|
-
iteration = iterResult.iteration;
|
|
18589
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18027
18590
|
}
|
|
18028
18591
|
const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
|
|
18029
18592
|
for (const f of codeDelta.files) {
|
|
@@ -18172,7 +18735,10 @@ async function runAnalyze(opts, globals) {
|
|
|
18172
18735
|
priorState.capabilities
|
|
18173
18736
|
);
|
|
18174
18737
|
memory = recallMemory(memorySession.d, memorySession.identity, {
|
|
18175
|
-
currentSessionKey: memorySession.identity.sessionKey
|
|
18738
|
+
currentSessionKey: memorySession.identity.sessionKey,
|
|
18739
|
+
// The independent witness. Only meaningful when a transcript was folded —
|
|
18740
|
+
// otherwise it stays undefined and capture coverage reads as UNKNOWN.
|
|
18741
|
+
...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
|
|
18176
18742
|
});
|
|
18177
18743
|
if (memory && !memory.provenanceHolds) {
|
|
18178
18744
|
process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
|
|
@@ -18193,7 +18759,30 @@ async function runAnalyze(opts, globals) {
|
|
|
18193
18759
|
hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
18194
18760
|
isTTY: process.stdout.isTTY === true
|
|
18195
18761
|
});
|
|
18762
|
+
const excludedByReason = {};
|
|
18763
|
+
for (const e of codeDelta.excluded ?? []) {
|
|
18764
|
+
excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
18765
|
+
}
|
|
18766
|
+
const coverageTelemetry = {
|
|
18767
|
+
// git's whole answer, before ANY narrowing. The number that has never been sent.
|
|
18768
|
+
changed_all: allChanged.length,
|
|
18769
|
+
analyzable: analyzable.length,
|
|
18770
|
+
reviewable: reviewable.length,
|
|
18771
|
+
security: securityFiles.length,
|
|
18772
|
+
// after the allowlist, before authorship scoping and the caps
|
|
18773
|
+
for_review: allForReview.length,
|
|
18774
|
+
// what actually reaches the reviewer
|
|
18775
|
+
sent: codeDelta.files.length,
|
|
18776
|
+
// the two silent narrowings, counted separately so they can be told apart
|
|
18777
|
+
capped_out: actionSummary?.capped_out?.length ?? 0,
|
|
18778
|
+
excluded: (codeDelta.excluded ?? []).length,
|
|
18779
|
+
excluded_by_reason: excludedByReason,
|
|
18780
|
+
// was the transcript itself truncated? The 256 KB window means "this turn"
|
|
18781
|
+
// can quietly mean "the last 256 KB of it".
|
|
18782
|
+
transcript_windowed: actionSummary?.transcript_windowed ?? null
|
|
18783
|
+
};
|
|
18196
18784
|
const requestBody = {
|
|
18785
|
+
coverage_telemetry: coverageTelemetry,
|
|
18197
18786
|
static_results: staticResults,
|
|
18198
18787
|
code_delta: codeDelta,
|
|
18199
18788
|
changed_files: allForReview,
|
|
@@ -18279,6 +18868,54 @@ async function runAnalyze(opts, globals) {
|
|
|
18279
18868
|
// replaced a population floor with ≈35% power that was sub-integer for
|
|
18280
18869
|
// three-quarters of the fleet.
|
|
18281
18870
|
conservation: foldConservation,
|
|
18871
|
+
// ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
|
|
18872
|
+
//
|
|
18873
|
+
// The whole "task-scoped delta" design space rests on an assumption that
|
|
18874
|
+
// has been observed exactly ONCE: that delta files routinely belong to
|
|
18875
|
+
// earlier work. Three designs were built on it and all three were killed
|
|
18876
|
+
// adversarially — two by measurement — so before another is attempted,
|
|
18877
|
+
// measure the base rate.
|
|
18878
|
+
//
|
|
18879
|
+
// `authored_under_earlier_goal` counts delta paths whose LAST authorship
|
|
18880
|
+
// event precedes the seq of the goal now in force. Both numbers come from
|
|
18881
|
+
// the same append-only counter (`nextSeq`), so the comparison is exact.
|
|
18882
|
+
//
|
|
18883
|
+
// Keyed on the GOAL, deliberately, not on the task id. The task classifier
|
|
18884
|
+
// reported `is_new_task` on two consecutive turns of one task 25 seconds
|
|
18885
|
+
// apart, so a task-keyed number would measure its unreliability rather
|
|
18886
|
+
// than the phenomenon. And this only became meaningful once `recordGoal`
|
|
18887
|
+
// stopped letting a bare "ok" supersede the goal — before that the seq
|
|
18888
|
+
// advanced every turn and this would have degenerated to "not edited this
|
|
18889
|
+
// turn", which is the exact mistake that sank one of the three designs.
|
|
18890
|
+
//
|
|
18891
|
+
// Changes no payload the reviewer sees, no narrowing, no verdict.
|
|
18892
|
+
vrt52: (() => {
|
|
18893
|
+
const goalSeq = memory?.projection.goal?.seq;
|
|
18894
|
+
if (goalSeq === void 0 || !memorySession) return { known: false };
|
|
18895
|
+
const lastSeq2 = new Map(
|
|
18896
|
+
foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
|
|
18897
|
+
);
|
|
18898
|
+
let earlier = 0;
|
|
18899
|
+
let unknown = 0;
|
|
18900
|
+
for (const f of codeDelta.files) {
|
|
18901
|
+
const seen = lastSeq2.get(f.path);
|
|
18902
|
+
if (seen === void 0) unknown++;
|
|
18903
|
+
else if (seen < goalSeq) earlier++;
|
|
18904
|
+
}
|
|
18905
|
+
return {
|
|
18906
|
+
known: true,
|
|
18907
|
+
goal_seq: goalSeq,
|
|
18908
|
+
delta: codeDelta.files.length,
|
|
18909
|
+
// Files this delta carries that were last written under an EARLIER
|
|
18910
|
+
// instruction. If this stays near zero, VRT-52's code half is
|
|
18911
|
+
// unnecessary and should be closed saying so.
|
|
18912
|
+
authored_under_earlier_goal: earlier,
|
|
18913
|
+
// Delta files the dossier has no authorship record for at all —
|
|
18914
|
+
// pre-existing tree state, or an authorship channel the fold cannot
|
|
18915
|
+
// see. Reported separately so a blind spot is never counted as a zero.
|
|
18916
|
+
no_authorship_record: unknown
|
|
18917
|
+
};
|
|
18918
|
+
})(),
|
|
18282
18919
|
// P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
|
|
18283
18920
|
// narrowed to the within-session increment: what changed since the last
|
|
18284
18921
|
// VERDICT rather than since task start.
|
|
@@ -18311,7 +18948,20 @@ async function runAnalyze(opts, globals) {
|
|
|
18311
18948
|
const intentContext = {};
|
|
18312
18949
|
if (conversation && conversation.prompts.length > 0) {
|
|
18313
18950
|
const latest = conversation.prompts[conversation.prompts.length - 1];
|
|
18314
|
-
|
|
18951
|
+
const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
|
|
18952
|
+
intentContext.user_prompt = goalPrompt.entry.prompt;
|
|
18953
|
+
if (isContinuationPrompt(intentContext.user_prompt)) {
|
|
18954
|
+
const carried = memory?.projection.goal?.text;
|
|
18955
|
+
if (carried && !isContinuationPrompt(carried)) {
|
|
18956
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
18957
|
+
intentContext.user_prompt = carried;
|
|
18958
|
+
logEvent("goal_from_dossier", { chars: carried.length });
|
|
18959
|
+
}
|
|
18960
|
+
}
|
|
18961
|
+
if (goalPrompt.turnsBack > 0) {
|
|
18962
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
18963
|
+
logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
|
|
18964
|
+
}
|
|
18315
18965
|
intentContext.session_id = latest.session_id || void 0;
|
|
18316
18966
|
intentContext.prompt_captured_at = latest.captured_at || void 0;
|
|
18317
18967
|
if (conversation.prompts.length > 1) {
|
|
@@ -18387,6 +19037,8 @@ async function runAnalyze(opts, globals) {
|
|
|
18387
19037
|
message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
|
|
18388
19038
|
} else if (result.error.startsWith("FORBIDDEN")) {
|
|
18389
19039
|
message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
|
|
19040
|
+
} else if (result.error.startsWith("INVALID_TOKEN")) {
|
|
19041
|
+
message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
|
|
18390
19042
|
} else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
|
|
18391
19043
|
message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
|
|
18392
19044
|
} else if (result.http_status && result.http_status >= 500) {
|
|
@@ -18401,8 +19053,114 @@ async function runAnalyze(opts, globals) {
|
|
|
18401
19053
|
const response = result.data;
|
|
18402
19054
|
const decision = response.gate_decision ?? "(unrecognised)";
|
|
18403
19055
|
const sentPaths = codeDelta.files.map((f) => f.path);
|
|
19056
|
+
let openElsewhere = [];
|
|
19057
|
+
if (memorySession) {
|
|
19058
|
+
try {
|
|
19059
|
+
const st = foldDossier(memorySession.d);
|
|
19060
|
+
openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
|
|
19061
|
+
try {
|
|
19062
|
+
const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
|
|
19063
|
+
const at = src[line - 1];
|
|
19064
|
+
return at === void 0 ? null : lineSha(at);
|
|
19065
|
+
} catch {
|
|
19066
|
+
return null;
|
|
19067
|
+
}
|
|
19068
|
+
});
|
|
19069
|
+
} catch {
|
|
19070
|
+
}
|
|
19071
|
+
}
|
|
19072
|
+
const reviewCoverage = {
|
|
19073
|
+
reviewed: sentPaths,
|
|
19074
|
+
// Declared drops from the stages that DO report themselves today. The other
|
|
19075
|
+
// stages surface via `unaccounted`, which is the tripwire, not the design.
|
|
19076
|
+
notReviewed: [
|
|
19077
|
+
// Every exit from the collection loop, each named. Six reasons where there
|
|
19078
|
+
// used to be two recorded and four silent — the silent ones including the
|
|
19079
|
+
// per-file size cap, which could drop a whole source file without leaving a
|
|
19080
|
+
// trace anywhere in the payload or the run row.
|
|
19081
|
+
...codeDelta.excluded,
|
|
19082
|
+
// The server-side 300-line middle-out truncation. It only bites on the
|
|
19083
|
+
// full-file branch (a first analysis, before snapshots exist) because
|
|
19084
|
+
// analyze normally sends diffs — but on that branch the reviewer sees the
|
|
19085
|
+
// first and last 100 lines and nothing between, and until now said so to
|
|
19086
|
+
// nobody. CAPACITY: a partial look is not a look.
|
|
19087
|
+
...(response.metadata?.truncated_files ?? []).map((path) => ({
|
|
19088
|
+
path,
|
|
19089
|
+
reason: "file-middle-truncated-300-lines",
|
|
19090
|
+
stage: "prompt-builder",
|
|
19091
|
+
kind: "capacity"
|
|
19092
|
+
})),
|
|
19093
|
+
// The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
|
|
19094
|
+
// what is REVIEWED, not merely what is summarised — a session editing 25
|
|
19095
|
+
// files had five silently excluded from the reviewed set.
|
|
19096
|
+
...(actionSummary?.capped_out ?? []).map((path) => ({
|
|
19097
|
+
path,
|
|
19098
|
+
reason: "edit-list-cap-20",
|
|
19099
|
+
stage: "extractActionSummary",
|
|
19100
|
+
kind: "capacity"
|
|
19101
|
+
})),
|
|
19102
|
+
// ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
|
|
19103
|
+
//
|
|
19104
|
+
// The universe is `allChanged`, git's whole dirty tree. The reviewed set is
|
|
19105
|
+
// scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
|
|
19106
|
+
// every pre-existing dirty file is in the universe, absent from `reviewed`,
|
|
19107
|
+
// and — until now — declared by nobody. It fell through to `unaccounted`,
|
|
19108
|
+
// became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
|
|
19109
|
+
// that was never this session's to review.
|
|
19110
|
+
//
|
|
19111
|
+
// Measured 2026-08-04: three consecutive runs over an untouched tree gave
|
|
19112
|
+
// three different answers — .claude/settings.json, then admin.js, then six
|
|
19113
|
+
// files — because each run took a different path and each path had a
|
|
19114
|
+
// different idea of the universe. POLICY: not this session's work is not a
|
|
19115
|
+
// coverage gap, it is the cure working.
|
|
19116
|
+
...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) => ({
|
|
19117
|
+
path,
|
|
19118
|
+
reason: "not-authored-this-session",
|
|
19119
|
+
stage: "baseline-scoping",
|
|
19120
|
+
kind: "policy"
|
|
19121
|
+
})),
|
|
19122
|
+
// The extension allowlist, and it is POLICY rather than capacity: a changed
|
|
19123
|
+
// README was never going to be reviewed, and treating that as a coverage
|
|
19124
|
+
// gap would downgrade nearly every PASS to WARN until WARN meant nothing.
|
|
19125
|
+
// Recorded so the ledger balances and so "what did Verity ignore entirely"
|
|
19126
|
+
// is answerable — but it never touches the verdict.
|
|
19127
|
+
...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
|
|
19128
|
+
path,
|
|
19129
|
+
reason: "not-a-reviewed-file-type",
|
|
19130
|
+
stage: "extension-allowlist",
|
|
19131
|
+
kind: "policy"
|
|
19132
|
+
}))
|
|
19133
|
+
]
|
|
19134
|
+
};
|
|
18404
19135
|
const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
|
|
18405
19136
|
const watermarkIsPartial = !!codeDelta.truncated;
|
|
19137
|
+
let silenced = null;
|
|
19138
|
+
let turnIsIdleForChannel = true;
|
|
19139
|
+
if (memorySession) {
|
|
19140
|
+
try {
|
|
19141
|
+
const st = foldDossier(memorySession.d);
|
|
19142
|
+
turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
|
|
19143
|
+
silenced = channelSilence({
|
|
19144
|
+
// The BUFFER, not intentContext.user_prompt: the latter falls back to a
|
|
19145
|
+
// linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
|
|
19146
|
+
// not a user utterance. Treating it as one would keep the loop alive on
|
|
19147
|
+
// exactly the autonomous cohort.
|
|
19148
|
+
newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
19149
|
+
newAuthorship: !turnIsIdleForChannel,
|
|
19150
|
+
emittedLast: st.meta.channel?.emittedLast === true,
|
|
19151
|
+
consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
|
|
19152
|
+
});
|
|
19153
|
+
} catch {
|
|
19154
|
+
silenced = null;
|
|
19155
|
+
}
|
|
19156
|
+
}
|
|
19157
|
+
if (silenced) {
|
|
19158
|
+
logEvent("channel_silenced", {
|
|
19159
|
+
reason: silenced,
|
|
19160
|
+
run_id: response.run_id ?? turnId,
|
|
19161
|
+
decision
|
|
19162
|
+
});
|
|
19163
|
+
}
|
|
18406
19164
|
let intentRepeatCount = 0;
|
|
18407
19165
|
if (memorySession) {
|
|
18408
19166
|
try {
|
|
@@ -18418,7 +19176,15 @@ async function runAnalyze(opts, globals) {
|
|
|
18418
19176
|
title: f.title,
|
|
18419
19177
|
severity: f.severity
|
|
18420
19178
|
})) ?? [],
|
|
18421
|
-
intent: response.intent_alignment ?? null
|
|
19179
|
+
intent: response.intent_alignment ?? null,
|
|
19180
|
+
// The same signal F1 introduced: bytes differing from the hash frozen at
|
|
19181
|
+
// the last verdict. A turn that moved nothing is the only kind that can
|
|
19182
|
+
// accumulate a repeat.
|
|
19183
|
+
idle: turnIsIdleForChannel,
|
|
19184
|
+
// What next turn reads as `emittedLast`. A suppressed turn did not
|
|
19185
|
+
// speak, so it cannot be the cause of the turn after it — which is what
|
|
19186
|
+
// keeps this from becoming a permanent gag.
|
|
19187
|
+
emitted: !silenced
|
|
18422
19188
|
});
|
|
18423
19189
|
intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
|
|
18424
19190
|
} catch {
|
|
@@ -18518,9 +19284,41 @@ async function runAnalyze(opts, globals) {
|
|
|
18518
19284
|
reverify_by: response.reverify_by
|
|
18519
19285
|
});
|
|
18520
19286
|
const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
|
|
18521
|
-
|
|
19287
|
+
let capReleased = false;
|
|
19288
|
+
let effectiveDecision = decision;
|
|
19289
|
+
if (decision === "FAIL") {
|
|
19290
|
+
const blocking = (response.findings ?? []).filter((f) => {
|
|
19291
|
+
const sev = String(f.severity ?? "").toLowerCase();
|
|
19292
|
+
return sev === "critical" || sev === "high";
|
|
19293
|
+
});
|
|
19294
|
+
const fingerprint = findingsFingerprint(blocking);
|
|
19295
|
+
const prior = readIterationState(currentCommit);
|
|
19296
|
+
const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
|
|
19297
|
+
const nextIteration = sameProblem ? prior.iteration + 1 : 1;
|
|
19298
|
+
const maxIterations = parseInt(opts.maxIterations, 10);
|
|
19299
|
+
writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
|
|
19300
|
+
iteration = nextIteration;
|
|
19301
|
+
if (nextIteration > maxIterations) {
|
|
19302
|
+
capReleased = true;
|
|
19303
|
+
effectiveDecision = "WARN";
|
|
19304
|
+
logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
|
|
19305
|
+
}
|
|
19306
|
+
}
|
|
19307
|
+
if (capReleased) {
|
|
19308
|
+
const findings = response.findings ?? [];
|
|
19309
|
+
const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
|
|
19310
|
+
emitVerdict({
|
|
19311
|
+
proposed: "WARN",
|
|
19312
|
+
changed: skipCoverageChanged,
|
|
19313
|
+
coverage: reviewCoverage,
|
|
19314
|
+
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.
|
|
19315
|
+
${lines.join("\n")}`,
|
|
19316
|
+
agentContext: null,
|
|
19317
|
+
silenced: true
|
|
19318
|
+
});
|
|
19319
|
+
}
|
|
19320
|
+
switch (effectiveDecision) {
|
|
18522
19321
|
case "FAIL": {
|
|
18523
|
-
writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
|
|
18524
19322
|
const assessment = response.assessment;
|
|
18525
19323
|
const narrative = assessment?.narrative ?? "";
|
|
18526
19324
|
const findings = response.findings ?? [];
|
|
@@ -18597,7 +19395,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
|
|
|
18597
19395
|
if (grantNudge) process.stderr.write(`
|
|
18598
19396
|
${YELLOW}${grantNudge.trim()}${NC}
|
|
18599
19397
|
`);
|
|
18600
|
-
|
|
19398
|
+
emitVerdict({
|
|
19399
|
+
proposed: "FAIL",
|
|
19400
|
+
changed: skipCoverageChanged,
|
|
19401
|
+
coverage: reviewCoverage,
|
|
19402
|
+
userSummary: "",
|
|
19403
|
+
// Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
|
|
19404
|
+
// the findings themselves are rendered above by the blocking renderer,
|
|
19405
|
+
// so what the cut removes is the repeated commentary, never the defect.
|
|
19406
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
|
|
19407
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19408
|
+
silenced: !!silenced,
|
|
19409
|
+
openElsewhere
|
|
19410
|
+
});
|
|
18601
19411
|
break;
|
|
18602
19412
|
}
|
|
18603
19413
|
case "PASS": {
|
|
@@ -18609,10 +19419,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18609
19419
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18610
19420
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18611
19421
|
userSummary += loginNudge + grantNudge;
|
|
18612
|
-
|
|
18613
|
-
|
|
18614
|
-
|
|
18615
|
-
|
|
19422
|
+
emitVerdict({
|
|
19423
|
+
proposed: "PASS",
|
|
19424
|
+
changed: skipCoverageChanged,
|
|
19425
|
+
coverage: reviewCoverage,
|
|
19426
|
+
userSummary,
|
|
19427
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
|
|
19428
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19429
|
+
silenced: !!silenced,
|
|
19430
|
+
openElsewhere
|
|
19431
|
+
});
|
|
18616
19432
|
break;
|
|
18617
19433
|
}
|
|
18618
19434
|
case "WARN": {
|
|
@@ -18623,10 +19439,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18623
19439
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18624
19440
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18625
19441
|
userSummary += loginNudge + grantNudge;
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
19442
|
+
emitVerdict({
|
|
19443
|
+
proposed: "WARN",
|
|
19444
|
+
changed: skipCoverageChanged,
|
|
19445
|
+
coverage: reviewCoverage,
|
|
19446
|
+
userSummary,
|
|
19447
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
|
|
19448
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19449
|
+
silenced: !!silenced,
|
|
19450
|
+
openElsewhere
|
|
19451
|
+
});
|
|
18630
19452
|
break;
|
|
18631
19453
|
}
|
|
18632
19454
|
default: {
|
|
@@ -18739,8 +19561,8 @@ async function runReview(opts, globals) {
|
|
|
18739
19561
|
for (const p of specPaths) {
|
|
18740
19562
|
if (!(0, import_node_fs26.existsSync)(p)) continue;
|
|
18741
19563
|
try {
|
|
18742
|
-
const { readFileSync:
|
|
18743
|
-
const content =
|
|
19564
|
+
const { readFileSync: readFileSync16 } = await import("node:fs");
|
|
19565
|
+
const content = readFileSync16(p, "utf-8");
|
|
18744
19566
|
specs.push({ path: p, content: content.slice(0, 10240) });
|
|
18745
19567
|
} catch {
|
|
18746
19568
|
}
|
|
@@ -19042,7 +19864,7 @@ async function runGuard(opts, globals) {
|
|
|
19042
19864
|
cmd: "guard"
|
|
19043
19865
|
});
|
|
19044
19866
|
if (!result.ok) {
|
|
19045
|
-
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." : "";
|
|
19867
|
+
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." : "";
|
|
19046
19868
|
emitAllowNotice(
|
|
19047
19869
|
`\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
|
|
19048
19870
|
`Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
|
|
@@ -20426,7 +21248,7 @@ function registerTelemetryCommands(program2) {
|
|
|
20426
21248
|
}
|
|
20427
21249
|
|
|
20428
21250
|
// src/cli.ts
|
|
20429
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.
|
|
21251
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.6eac6aa").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
|
|
20430
21252
|
try {
|
|
20431
21253
|
await foldLegacyLocalCredential();
|
|
20432
21254
|
} catch {
|
|
@@ -20435,6 +21257,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
|
|
|
20435
21257
|
registerAuthCommands(program);
|
|
20436
21258
|
registerLoginCommand(program);
|
|
20437
21259
|
registerTokenCommand(program);
|
|
21260
|
+
registerSessionsCommands(program);
|
|
21261
|
+
registerLogoutCommand(program);
|
|
20438
21262
|
registerHooksCommands(program);
|
|
20439
21263
|
registerIntentCommands(program);
|
|
20440
21264
|
registerLifecycleCommands(program);
|