@codacy/verity-cli 0.28.1-experimental.5ec7373 → 0.28.1-experimental.8cc9c66
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 +592 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,11 @@ No re-setup needed: your token, Standard, and run history all carry over.
|
|
|
63
63
|
| `verity token create --name <name>` | Mint a service token for CI (`--expires <days>` optional) |
|
|
64
64
|
| `verity token list` | List this repository's tokens (ids and metadata only) |
|
|
65
65
|
| `verity token revoke <id>` | Revoke a token by id |
|
|
66
|
+
| `verity sessions list` | List your logins — device, last use, expiry (logins expire after 90 days) |
|
|
67
|
+
| `verity sessions revoke <id>` | Revoke one login by session id |
|
|
68
|
+
| `verity logout` | Sign out on this machine |
|
|
69
|
+
| `verity logout --others` | Sign out every OTHER machine (e.g. a lost laptop) |
|
|
70
|
+
| `verity logout --all` | Sign out everywhere, including here |
|
|
66
71
|
| `verity hooks install` | Wire Claude Code hooks |
|
|
67
72
|
| `verity standard push` | Upload project Standard |
|
|
68
73
|
| **Knowledge** | |
|
package/bin/verity.js
CHANGED
|
@@ -10636,14 +10636,14 @@ async function readGlobalCredential(remote) {
|
|
|
10636
10636
|
const parsed = parseCredentialLine(line);
|
|
10637
10637
|
if (parsed && parsed.remote === key) last = parsed.rec;
|
|
10638
10638
|
}
|
|
10639
|
-
if (last) return last;
|
|
10639
|
+
if (last) return { ...last, keyed: true };
|
|
10640
10640
|
}
|
|
10641
10641
|
let plain = null;
|
|
10642
10642
|
for (const line of lines) {
|
|
10643
10643
|
const parsed = parseCredentialLine(line);
|
|
10644
10644
|
if (parsed && parsed.remote === "") plain = parsed.rec;
|
|
10645
10645
|
}
|
|
10646
|
-
return plain;
|
|
10646
|
+
return plain ? { ...plain, keyed: false } : null;
|
|
10647
10647
|
}
|
|
10648
10648
|
async function upsertGlobalCredential(remote, rec) {
|
|
10649
10649
|
const path = globalCredentialsPath();
|
|
@@ -10707,6 +10707,36 @@ async function removeSupersededUserCredentials(loginServiceUrl, loginUserId) {
|
|
|
10707
10707
|
}
|
|
10708
10708
|
return removed;
|
|
10709
10709
|
}
|
|
10710
|
+
async function removeGlobalCredential(remote) {
|
|
10711
|
+
const path = globalCredentialsPath();
|
|
10712
|
+
let content;
|
|
10713
|
+
try {
|
|
10714
|
+
content = await (0, import_promises.readFile)(path, "utf-8");
|
|
10715
|
+
} catch {
|
|
10716
|
+
return false;
|
|
10717
|
+
}
|
|
10718
|
+
const key = encodeRemoteKey(remote);
|
|
10719
|
+
const kept = [];
|
|
10720
|
+
let removed = false;
|
|
10721
|
+
for (const line of content.split("\n")) {
|
|
10722
|
+
const parsed = parseCredentialLine(line);
|
|
10723
|
+
if (parsed && parsed.remote === key) {
|
|
10724
|
+
removed = true;
|
|
10725
|
+
continue;
|
|
10726
|
+
}
|
|
10727
|
+
kept.push(line);
|
|
10728
|
+
}
|
|
10729
|
+
if (!removed) return false;
|
|
10730
|
+
while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
|
|
10731
|
+
try {
|
|
10732
|
+
await (0, import_promises.writeFile)(path, kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
|
|
10733
|
+
await (0, import_promises.chmod)(path, 384).catch(() => {
|
|
10734
|
+
});
|
|
10735
|
+
} catch {
|
|
10736
|
+
return false;
|
|
10737
|
+
}
|
|
10738
|
+
return true;
|
|
10739
|
+
}
|
|
10710
10740
|
function parseLocalCredentialFile(content) {
|
|
10711
10741
|
const tokenMatch = content.match(/token:\s*((?:gate_|verity_)[a-f0-9]+)/);
|
|
10712
10742
|
if (!tokenMatch) return null;
|
|
@@ -11188,7 +11218,13 @@ async function resolveToken(flagToken) {
|
|
|
11188
11218
|
if (rec) {
|
|
11189
11219
|
return {
|
|
11190
11220
|
ok: true,
|
|
11191
|
-
data: {
|
|
11221
|
+
data: {
|
|
11222
|
+
token: rec.token,
|
|
11223
|
+
source: "global",
|
|
11224
|
+
userId: rec.userId,
|
|
11225
|
+
email: rec.email,
|
|
11226
|
+
keyed: rec.keyed
|
|
11227
|
+
}
|
|
11192
11228
|
};
|
|
11193
11229
|
}
|
|
11194
11230
|
const local = await readLegacyLocalCredential();
|
|
@@ -11220,6 +11256,16 @@ function reverifyNudge(who) {
|
|
|
11220
11256
|
}
|
|
11221
11257
|
return null;
|
|
11222
11258
|
}
|
|
11259
|
+
function isLegacyPerRepoCredential(auth2) {
|
|
11260
|
+
return auth2.source === "local" || auth2.source === "global" && auth2.keyed === true;
|
|
11261
|
+
}
|
|
11262
|
+
async function shouldUpgradeOnLogin(auth2) {
|
|
11263
|
+
if (!isLegacyPerRepoCredential(auth2)) return false;
|
|
11264
|
+
if (auth2.userId == null) return true;
|
|
11265
|
+
const bare = await readGlobalCredential("");
|
|
11266
|
+
if (bare?.userId == null) return true;
|
|
11267
|
+
return bare.userId === auth2.userId;
|
|
11268
|
+
}
|
|
11223
11269
|
function authDenialRemedy(error) {
|
|
11224
11270
|
if (error.startsWith("STALE_VERIFICATION")) {
|
|
11225
11271
|
return {
|
|
@@ -11233,6 +11279,12 @@ function authDenialRemedy(error) {
|
|
|
11233
11279
|
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
11280
|
};
|
|
11235
11281
|
}
|
|
11282
|
+
if (error.startsWith("INVALID_TOKEN")) {
|
|
11283
|
+
return {
|
|
11284
|
+
code: "INVALID_TOKEN",
|
|
11285
|
+
remedy: 'Your Verity login has expired or was revoked \u2014 run "verity login" to sign in again.'
|
|
11286
|
+
};
|
|
11287
|
+
}
|
|
11236
11288
|
return null;
|
|
11237
11289
|
}
|
|
11238
11290
|
async function probeService(serviceUrl, verbose) {
|
|
@@ -11273,6 +11325,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
|
|
|
11273
11325
|
|
|
11274
11326
|
// src/lib/register.ts
|
|
11275
11327
|
var readline = __toESM(require("node:readline/promises"));
|
|
11328
|
+
var import_node_os = require("node:os");
|
|
11276
11329
|
|
|
11277
11330
|
// src/lib/provider-auth.ts
|
|
11278
11331
|
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -11460,6 +11513,15 @@ async function registerProject(opts) {
|
|
|
11460
11513
|
}
|
|
11461
11514
|
return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
|
|
11462
11515
|
}
|
|
11516
|
+
function deviceLabel() {
|
|
11517
|
+
const override = process.env.VERITY_DEVICE_LABEL?.trim();
|
|
11518
|
+
if (override) return override;
|
|
11519
|
+
try {
|
|
11520
|
+
return (0, import_node_os.hostname)() || void 0;
|
|
11521
|
+
} catch {
|
|
11522
|
+
return void 0;
|
|
11523
|
+
}
|
|
11524
|
+
}
|
|
11463
11525
|
async function loginOnce(opts) {
|
|
11464
11526
|
const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
|
|
11465
11527
|
const providerAuth = await githubDeviceFlow();
|
|
@@ -11480,13 +11542,19 @@ async function loginOnce(opts) {
|
|
|
11480
11542
|
path: "/auth/login",
|
|
11481
11543
|
serviceUrl: opts.serviceUrl,
|
|
11482
11544
|
extraHeaders: { "X-Provider-Token": providerToken },
|
|
11545
|
+
// Label the session so its owner can tell their machines apart in
|
|
11546
|
+
// `verity sessions list` — a list of identical "login" rows is unusable when
|
|
11547
|
+
// the question is "which of these is the laptop I lost?". The hostname is the
|
|
11548
|
+
// useful default; VERITY_DEVICE_LABEL overrides it for anyone who would
|
|
11549
|
+
// rather not send it. Server-side it is sanitized and capped.
|
|
11550
|
+
body: { device: deviceLabel() },
|
|
11483
11551
|
verbose: opts.verbose,
|
|
11484
11552
|
cmd: "login"
|
|
11485
11553
|
});
|
|
11486
11554
|
if (!result.ok) {
|
|
11487
11555
|
return { ok: false, error: result.error };
|
|
11488
11556
|
}
|
|
11489
|
-
const { token, service_url, user_id, user, repo_count } = result.data;
|
|
11557
|
+
const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
|
|
11490
11558
|
const loginUserId = user_id ?? user?.id ?? void 0;
|
|
11491
11559
|
try {
|
|
11492
11560
|
await upsertGlobalCredential("", {
|
|
@@ -11510,15 +11578,16 @@ async function loginOnce(opts) {
|
|
|
11510
11578
|
email: user?.email,
|
|
11511
11579
|
userId: loginUserId,
|
|
11512
11580
|
repoCount: repo_count ?? 0,
|
|
11513
|
-
prunedCredentials: pruned
|
|
11581
|
+
prunedCredentials: pruned,
|
|
11582
|
+
expiresAt: expires_at
|
|
11514
11583
|
}
|
|
11515
11584
|
};
|
|
11516
11585
|
}
|
|
11517
11586
|
|
|
11518
11587
|
// src/commands/auth.ts
|
|
11519
11588
|
function registerAuthCommands(program2) {
|
|
11520
|
-
const
|
|
11521
|
-
|
|
11589
|
+
const auth2 = program2.command("auth").description("Manage project authentication");
|
|
11590
|
+
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
11591
|
const globals = program2.opts();
|
|
11523
11592
|
const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
|
|
11524
11593
|
let remote = opts.remote;
|
|
@@ -11545,7 +11614,7 @@ function registerAuthCommands(program2) {
|
|
|
11545
11614
|
if (email) printInfo(`Authenticated as: ${email}`);
|
|
11546
11615
|
printJson({ project_id: projectId, service_url: resolvedUrl });
|
|
11547
11616
|
});
|
|
11548
|
-
|
|
11617
|
+
auth2.command("verify").description("Verify the current token is valid").action(async () => {
|
|
11549
11618
|
const globals = program2.opts();
|
|
11550
11619
|
const tokenResult = await resolveToken(globals.token);
|
|
11551
11620
|
if (!tokenResult.ok) {
|
|
@@ -11571,7 +11640,7 @@ function registerAuthCommands(program2) {
|
|
|
11571
11640
|
printInfo(`Token valid. Project: ${result.data.project_name}`);
|
|
11572
11641
|
printJson(result.data);
|
|
11573
11642
|
});
|
|
11574
|
-
|
|
11643
|
+
auth2.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
|
|
11575
11644
|
const globals = program2.opts();
|
|
11576
11645
|
let remote = opts.remote;
|
|
11577
11646
|
if (!remote) {
|
|
@@ -11627,13 +11696,19 @@ function registerLoginCommand(program2) {
|
|
|
11627
11696
|
const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
|
|
11628
11697
|
if (who.ok && who.data.logged_in) {
|
|
11629
11698
|
const nudge = reverifyNudge(who.data);
|
|
11630
|
-
|
|
11699
|
+
const upgrade = await shouldUpgradeOnLogin(existing.data);
|
|
11700
|
+
if (!nudge && !upgrade) {
|
|
11631
11701
|
printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
|
|
11632
11702
|
printInfo(" Re-authenticate with: verity login --force");
|
|
11633
11703
|
return;
|
|
11634
11704
|
}
|
|
11635
|
-
|
|
11636
|
-
|
|
11705
|
+
if (nudge) {
|
|
11706
|
+
printWarn(nudge);
|
|
11707
|
+
printInfo("Re-verifying your repository access\u2026");
|
|
11708
|
+
} else {
|
|
11709
|
+
printInfo("You are signed in with a per-repository token (the old format).");
|
|
11710
|
+
printInfo(" Upgrading to a single login that covers every repository you can write to\u2026");
|
|
11711
|
+
}
|
|
11637
11712
|
} else if (who.ok && who.data.anonymous) {
|
|
11638
11713
|
printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
|
|
11639
11714
|
} else if (!who.ok) {
|
|
@@ -11650,6 +11725,10 @@ function registerLoginCommand(program2) {
|
|
|
11650
11725
|
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11651
11726
|
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11652
11727
|
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11728
|
+
if (out.expiresAt) {
|
|
11729
|
+
printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
|
|
11730
|
+
printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
|
|
11731
|
+
}
|
|
11653
11732
|
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11654
11733
|
if (out.prunedCredentials > 0) {
|
|
11655
11734
|
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
|
|
@@ -11680,14 +11759,24 @@ function registerLoginCommand(program2) {
|
|
|
11680
11759
|
const rec = await readGlobalCredential(remote);
|
|
11681
11760
|
if (rec && rec.token !== out.token) {
|
|
11682
11761
|
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11762
|
+
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11683
11763
|
if (otherBackend) {
|
|
11684
11764
|
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11685
11765
|
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11686
11766
|
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11767
|
+
printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
|
|
11768
|
+
printWarn(" the full GitHub flow every time.");
|
|
11769
|
+
} else if (otherIdentity) {
|
|
11770
|
+
printWarn(" Note: this repository uses a different account's credential, which takes");
|
|
11771
|
+
printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
|
|
11772
|
+
printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
|
|
11773
|
+
printWarn(" only if you want this repository on the login you just completed.");
|
|
11687
11774
|
} else {
|
|
11688
11775
|
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11689
11776
|
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11690
11777
|
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11778
|
+
printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
|
|
11779
|
+
printWarn(" every time.");
|
|
11691
11780
|
}
|
|
11692
11781
|
}
|
|
11693
11782
|
}
|
|
@@ -11809,6 +11898,177 @@ function registerTokenCommand(program2) {
|
|
|
11809
11898
|
});
|
|
11810
11899
|
}
|
|
11811
11900
|
|
|
11901
|
+
// src/commands/sessions.ts
|
|
11902
|
+
function shortDate(iso) {
|
|
11903
|
+
return iso ? iso.slice(0, 10) : "\u2014";
|
|
11904
|
+
}
|
|
11905
|
+
function daysUntil(iso) {
|
|
11906
|
+
if (!iso) return null;
|
|
11907
|
+
const ms = Date.parse(iso);
|
|
11908
|
+
if (Number.isNaN(ms)) return null;
|
|
11909
|
+
return Math.round((ms - Date.now()) / 864e5);
|
|
11910
|
+
}
|
|
11911
|
+
async function auth(globals) {
|
|
11912
|
+
const tokenResult = await resolveToken(globals.token);
|
|
11913
|
+
if (!tokenResult.ok) {
|
|
11914
|
+
printError(tokenResult.error);
|
|
11915
|
+
process.exit(1);
|
|
11916
|
+
}
|
|
11917
|
+
const urlResult = await resolveServiceUrl(globals.serviceUrl);
|
|
11918
|
+
if (!urlResult.ok) {
|
|
11919
|
+
printError(urlResult.error);
|
|
11920
|
+
process.exit(1);
|
|
11921
|
+
}
|
|
11922
|
+
return { token: tokenResult.data.token, serviceUrl: urlResult.data };
|
|
11923
|
+
}
|
|
11924
|
+
function explain(error) {
|
|
11925
|
+
if (error.startsWith("FORBIDDEN")) {
|
|
11926
|
+
printInfo(' Sessions belong to a logged-in account \u2014 run "verity login" first.');
|
|
11927
|
+
} else if (error.startsWith("INVALID_TOKEN")) {
|
|
11928
|
+
printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
|
|
11929
|
+
}
|
|
11930
|
+
}
|
|
11931
|
+
function registerSessionsCommands(program2) {
|
|
11932
|
+
const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
|
|
11933
|
+
sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
|
|
11934
|
+
const globals = program2.opts();
|
|
11935
|
+
const { token, serviceUrl } = await auth(globals);
|
|
11936
|
+
const result = await apiRequest({
|
|
11937
|
+
method: "GET",
|
|
11938
|
+
path: "/auth/sessions",
|
|
11939
|
+
serviceUrl,
|
|
11940
|
+
token,
|
|
11941
|
+
verbose: globals.verbose,
|
|
11942
|
+
cmd: "sessions"
|
|
11943
|
+
});
|
|
11944
|
+
if (!result.ok) {
|
|
11945
|
+
printError(result.error);
|
|
11946
|
+
explain(result.error);
|
|
11947
|
+
process.exit(1);
|
|
11948
|
+
}
|
|
11949
|
+
if (opts.json) {
|
|
11950
|
+
printJson(result.data);
|
|
11951
|
+
return;
|
|
11952
|
+
}
|
|
11953
|
+
const list = result.data.sessions;
|
|
11954
|
+
if (list.length === 0) {
|
|
11955
|
+
printInfo('No active logins. (Run "verity login".)');
|
|
11956
|
+
return;
|
|
11957
|
+
}
|
|
11958
|
+
printInfo(`${list.length} active login${list.length === 1 ? "" : "s"}:`);
|
|
11959
|
+
printInfo("");
|
|
11960
|
+
printInfo(`${"SESSION ID".padEnd(38)}${"DEVICE".padEnd(24)}${"CREATED".padEnd(12)}${"LAST USED".padEnd(12)}EXPIRES`);
|
|
11961
|
+
for (const s of list) {
|
|
11962
|
+
const days = daysUntil(s.expires_at);
|
|
11963
|
+
const expiry = s.expires_at ? `${shortDate(s.expires_at)}${days != null ? ` (${days}d)` : ""}` : "never";
|
|
11964
|
+
const device = (s.device ?? "login").slice(0, 22);
|
|
11965
|
+
printInfo(
|
|
11966
|
+
`${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" : ""}`
|
|
11967
|
+
);
|
|
11968
|
+
}
|
|
11969
|
+
printInfo("");
|
|
11970
|
+
printInfo("Revoke one: verity sessions revoke <session-id>");
|
|
11971
|
+
printInfo("Sign out everywhere else: verity logout --others");
|
|
11972
|
+
});
|
|
11973
|
+
sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
|
|
11974
|
+
const globals = program2.opts();
|
|
11975
|
+
const { token, serviceUrl } = await auth(globals);
|
|
11976
|
+
const result = await apiRequest({
|
|
11977
|
+
method: "DELETE",
|
|
11978
|
+
path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
|
|
11979
|
+
serviceUrl,
|
|
11980
|
+
token,
|
|
11981
|
+
verbose: globals.verbose,
|
|
11982
|
+
cmd: "sessions-revoke"
|
|
11983
|
+
});
|
|
11984
|
+
if (!result.ok) {
|
|
11985
|
+
printError(result.error);
|
|
11986
|
+
if (result.http_status === 404) {
|
|
11987
|
+
printInfo(' No session with that id on your account \u2014 check "verity sessions list".');
|
|
11988
|
+
}
|
|
11989
|
+
explain(result.error);
|
|
11990
|
+
process.exit(1);
|
|
11991
|
+
}
|
|
11992
|
+
printInfo(`Session ${sessionId} revoked. \u2713`);
|
|
11993
|
+
if (result.data.was_current) {
|
|
11994
|
+
const cleared = await removeGlobalCredential("");
|
|
11995
|
+
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.');
|
|
11996
|
+
}
|
|
11997
|
+
});
|
|
11998
|
+
}
|
|
11999
|
+
function registerLogoutCommand(program2) {
|
|
12000
|
+
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) => {
|
|
12001
|
+
const globals = program2.opts();
|
|
12002
|
+
if (opts.all && opts.others) {
|
|
12003
|
+
printError("Use either --all or --others, not both.");
|
|
12004
|
+
process.exit(1);
|
|
12005
|
+
}
|
|
12006
|
+
const { token, serviceUrl } = await auth(globals);
|
|
12007
|
+
if (opts.all || opts.others) {
|
|
12008
|
+
const result = await apiRequest({
|
|
12009
|
+
method: "DELETE",
|
|
12010
|
+
path: opts.others ? "/auth/sessions?others=true" : "/auth/sessions",
|
|
12011
|
+
serviceUrl,
|
|
12012
|
+
token,
|
|
12013
|
+
verbose: globals.verbose,
|
|
12014
|
+
cmd: "logout"
|
|
12015
|
+
});
|
|
12016
|
+
if (!result.ok) {
|
|
12017
|
+
printError(result.error);
|
|
12018
|
+
explain(result.error);
|
|
12019
|
+
process.exit(1);
|
|
12020
|
+
}
|
|
12021
|
+
const n = result.data.revoked;
|
|
12022
|
+
printInfo(`Revoked ${n} login${n === 1 ? "" : "s"}. \u2713`);
|
|
12023
|
+
if (opts.others) {
|
|
12024
|
+
printInfo(" This machine is still signed in.");
|
|
12025
|
+
return;
|
|
12026
|
+
}
|
|
12027
|
+
const cleared2 = await removeGlobalCredential("");
|
|
12028
|
+
if (cleared2) printInfo(" Local credential cleared.");
|
|
12029
|
+
printInfo(' Run "verity login" to sign back in.');
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
12032
|
+
const list = await apiRequest({
|
|
12033
|
+
method: "GET",
|
|
12034
|
+
path: "/auth/sessions",
|
|
12035
|
+
serviceUrl,
|
|
12036
|
+
token,
|
|
12037
|
+
verbose: globals.verbose,
|
|
12038
|
+
cmd: "logout"
|
|
12039
|
+
});
|
|
12040
|
+
if (!list.ok) {
|
|
12041
|
+
printError(list.error);
|
|
12042
|
+
explain(list.error);
|
|
12043
|
+
process.exit(1);
|
|
12044
|
+
}
|
|
12045
|
+
const current = list.data.sessions.find((s) => s.current);
|
|
12046
|
+
if (!current) {
|
|
12047
|
+
printWarn("This machine is not signed in with a Verity login.");
|
|
12048
|
+
const cleared2 = await removeGlobalCredential("");
|
|
12049
|
+
if (cleared2) printInfo(" Cleared the local login credential anyway.");
|
|
12050
|
+
return;
|
|
12051
|
+
}
|
|
12052
|
+
const revoked = await apiRequest({
|
|
12053
|
+
method: "DELETE",
|
|
12054
|
+
path: `/auth/sessions/${current.id}`,
|
|
12055
|
+
serviceUrl,
|
|
12056
|
+
token,
|
|
12057
|
+
verbose: globals.verbose,
|
|
12058
|
+
cmd: "logout"
|
|
12059
|
+
});
|
|
12060
|
+
if (!revoked.ok) {
|
|
12061
|
+
printError(revoked.error);
|
|
12062
|
+
explain(revoked.error);
|
|
12063
|
+
process.exit(1);
|
|
12064
|
+
}
|
|
12065
|
+
const cleared = await removeGlobalCredential("");
|
|
12066
|
+
printInfo("Signed out on this machine. \u2713");
|
|
12067
|
+
if (cleared) printInfo(" Local credential cleared.");
|
|
12068
|
+
printInfo(' Your other machines are unaffected \u2014 use "verity logout --all" for all of them.');
|
|
12069
|
+
});
|
|
12070
|
+
}
|
|
12071
|
+
|
|
11812
12072
|
// src/lib/hooks.ts
|
|
11813
12073
|
var import_promises4 = require("node:fs/promises");
|
|
11814
12074
|
var import_node_path5 = require("node:path");
|
|
@@ -12430,7 +12690,7 @@ function getRecentCommitMessages() {
|
|
|
12430
12690
|
// src/lib/context-identity.ts
|
|
12431
12691
|
var import_node_crypto2 = require("node:crypto");
|
|
12432
12692
|
var import_node_fs5 = require("node:fs");
|
|
12433
|
-
var
|
|
12693
|
+
var import_node_os2 = require("node:os");
|
|
12434
12694
|
var import_node_path6 = require("node:path");
|
|
12435
12695
|
var SHARED_SENTINELS = /* @__PURE__ */ new Set([
|
|
12436
12696
|
"",
|
|
@@ -12485,7 +12745,7 @@ function contextIdentity(input) {
|
|
|
12485
12745
|
}
|
|
12486
12746
|
function verityHome() {
|
|
12487
12747
|
const override = process.env.VERITY_HOME;
|
|
12488
|
-
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0,
|
|
12748
|
+
return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".verity");
|
|
12489
12749
|
}
|
|
12490
12750
|
function dossierDir(identity) {
|
|
12491
12751
|
return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
|
|
@@ -13700,6 +13960,12 @@ function reduce(state, events, now) {
|
|
|
13700
13960
|
}
|
|
13701
13961
|
case "verdict": {
|
|
13702
13962
|
state.meta.last_verdict_seq = ev.seq;
|
|
13963
|
+
state.meta.channel = {
|
|
13964
|
+
emittedLast: ev.emitted === true,
|
|
13965
|
+
// Reset by ANY movement, so the counter measures a standstill rather
|
|
13966
|
+
// than session length.
|
|
13967
|
+
consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
|
|
13968
|
+
};
|
|
13703
13969
|
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
13970
|
if (ev.intent_sig) {
|
|
13705
13971
|
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 +14333,10 @@ function projectMemory(state, opts) {
|
|
|
14067
14333
|
...active.delivered && { delivered: active.delivered }
|
|
14068
14334
|
};
|
|
14069
14335
|
}
|
|
14336
|
+
if (opts.capture) {
|
|
14337
|
+
const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
|
|
14338
|
+
p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
|
|
14339
|
+
}
|
|
14070
14340
|
if (state.meta.last_adjudication) {
|
|
14071
14341
|
const a = state.meta.last_adjudication;
|
|
14072
14342
|
p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
|
|
@@ -14313,7 +14583,8 @@ function recall(d, input) {
|
|
|
14313
14583
|
continuity,
|
|
14314
14584
|
spoken: reanchored.spoken,
|
|
14315
14585
|
refused: reanchored.dropped.length,
|
|
14316
|
-
lastVerdictSeq
|
|
14586
|
+
lastVerdictSeq,
|
|
14587
|
+
...input.capture && { capture: input.capture }
|
|
14317
14588
|
});
|
|
14318
14589
|
return {
|
|
14319
14590
|
state: effective,
|
|
@@ -14527,6 +14798,11 @@ function recordVerdict(d, v) {
|
|
|
14527
14798
|
line_sha: at !== void 0 ? lineSha(at) : null
|
|
14528
14799
|
});
|
|
14529
14800
|
}
|
|
14801
|
+
const foldedNow = foldDossier(d);
|
|
14802
|
+
const sig = intentSignature(v.intent, {
|
|
14803
|
+
goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
|
|
14804
|
+
idle: v.idle !== false
|
|
14805
|
+
});
|
|
14530
14806
|
appendEvent(d, {
|
|
14531
14807
|
k: "verdict",
|
|
14532
14808
|
run_id: v.runId,
|
|
@@ -14534,15 +14810,19 @@ function recordVerdict(d, v) {
|
|
|
14534
14810
|
watermark_sha: v.watermarkSha,
|
|
14535
14811
|
branch: v.branch,
|
|
14536
14812
|
decision: v.decision,
|
|
14537
|
-
...
|
|
14813
|
+
...sig && { intent_sig: sig },
|
|
14814
|
+
emitted: v.emitted === true,
|
|
14815
|
+
idle: v.idle !== false,
|
|
14538
14816
|
...v.intent?.verdict && { intent_verdict: v.intent.verdict },
|
|
14539
14817
|
...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
|
|
14540
14818
|
});
|
|
14541
14819
|
}
|
|
14542
|
-
function intentSignature(intent) {
|
|
14820
|
+
function intentSignature(intent, ctx) {
|
|
14543
14821
|
if (!intent?.verdict) return null;
|
|
14544
14822
|
if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
|
|
14545
|
-
|
|
14823
|
+
const goal = ctx ? `g${ctx.goalSeq}` : "g?";
|
|
14824
|
+
const moved = ctx?.idle === false ? "active" : "idle";
|
|
14825
|
+
return `${intent.verdict}:${goal}:${moved}`;
|
|
14546
14826
|
}
|
|
14547
14827
|
function toRegister(severity) {
|
|
14548
14828
|
switch (severity) {
|
|
@@ -14579,7 +14859,9 @@ function recallMemory(d, identity, opts) {
|
|
|
14579
14859
|
const state = foldDossier(d);
|
|
14580
14860
|
const watermark = state.meta.watermark?.sha ?? null;
|
|
14581
14861
|
const watermarkPaths = (state.authored ?? []).map((a) => a.path);
|
|
14862
|
+
const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
|
|
14582
14863
|
const r = recall(d, {
|
|
14864
|
+
...captureCmp && { capture: captureCmp },
|
|
14583
14865
|
identity,
|
|
14584
14866
|
currentSessionKey: opts.currentSessionKey,
|
|
14585
14867
|
branchNow: getCurrentBranch(),
|
|
@@ -14836,6 +15118,8 @@ function collectCodeDelta(files, opts) {
|
|
|
14836
15118
|
let totalSize = 0;
|
|
14837
15119
|
let truncationReason = null;
|
|
14838
15120
|
const droppedPaths = [];
|
|
15121
|
+
const excluded = [];
|
|
15122
|
+
const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
|
|
14839
15123
|
for (const filepath of sorted) {
|
|
14840
15124
|
if (result.length >= maxFiles) {
|
|
14841
15125
|
truncationReason ??= "max_files";
|
|
@@ -14843,14 +15127,21 @@ function collectCodeDelta(files, opts) {
|
|
|
14843
15127
|
continue;
|
|
14844
15128
|
}
|
|
14845
15129
|
const resolved = resolveFile(filepath);
|
|
14846
|
-
if (!resolved)
|
|
15130
|
+
if (!resolved) {
|
|
15131
|
+
exclude(filepath, "path-not-resolvable");
|
|
15132
|
+
continue;
|
|
15133
|
+
}
|
|
14847
15134
|
let size;
|
|
14848
15135
|
try {
|
|
14849
15136
|
size = (0, import_node_fs11.statSync)(resolved).size;
|
|
14850
15137
|
} catch {
|
|
15138
|
+
exclude(filepath, "not-stattable");
|
|
15139
|
+
continue;
|
|
15140
|
+
}
|
|
15141
|
+
if (size > maxFileBytes) {
|
|
15142
|
+
exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
|
|
14851
15143
|
continue;
|
|
14852
15144
|
}
|
|
14853
|
-
if (size > maxFileBytes) continue;
|
|
14854
15145
|
if (totalSize + size > maxTotalBytes) {
|
|
14855
15146
|
truncationReason ??= "max_total_bytes";
|
|
14856
15147
|
const idx = sorted.indexOf(filepath);
|
|
@@ -14861,6 +15152,7 @@ function collectCodeDelta(files, opts) {
|
|
|
14861
15152
|
try {
|
|
14862
15153
|
content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
|
|
14863
15154
|
} catch {
|
|
15155
|
+
exclude(filepath, "not-readable");
|
|
14864
15156
|
continue;
|
|
14865
15157
|
}
|
|
14866
15158
|
totalSize += size;
|
|
@@ -14874,10 +15166,14 @@ function collectCodeDelta(files, opts) {
|
|
|
14874
15166
|
(sum, f) => sum + f.content.split("\n").length,
|
|
14875
15167
|
0
|
|
14876
15168
|
);
|
|
15169
|
+
for (const path of droppedPaths) {
|
|
15170
|
+
exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
|
|
15171
|
+
}
|
|
14877
15172
|
return {
|
|
14878
15173
|
files: result,
|
|
14879
15174
|
total_lines: totalLines,
|
|
14880
15175
|
total_files: result.length,
|
|
15176
|
+
excluded,
|
|
14881
15177
|
...truncationReason && {
|
|
14882
15178
|
truncated: {
|
|
14883
15179
|
reason: truncationReason,
|
|
@@ -16329,7 +16625,7 @@ function resolveTaskContext(opts) {
|
|
|
16329
16625
|
// src/lib/cli-version.ts
|
|
16330
16626
|
function cliVersion() {
|
|
16331
16627
|
try {
|
|
16332
|
-
return true ? "0.28.1-experimental.
|
|
16628
|
+
return true ? "0.28.1-experimental.8cc9c66" : "dev";
|
|
16333
16629
|
} catch {
|
|
16334
16630
|
return "dev";
|
|
16335
16631
|
}
|
|
@@ -16554,6 +16850,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
|
16554
16850
|
"subagent"
|
|
16555
16851
|
]);
|
|
16556
16852
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
|
|
16853
|
+
var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
|
|
16854
|
+
function hasUserText(record) {
|
|
16855
|
+
const message = record.message;
|
|
16856
|
+
const content = message?.content ?? record.content;
|
|
16857
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
16858
|
+
if (!Array.isArray(content)) return false;
|
|
16859
|
+
return content.some(
|
|
16860
|
+
(b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
|
|
16861
|
+
);
|
|
16862
|
+
}
|
|
16557
16863
|
var COMMAND_CLASSES = [
|
|
16558
16864
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
|
|
16559
16865
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
|
|
@@ -16680,6 +16986,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16680
16986
|
totalRecords: 0,
|
|
16681
16987
|
malformed: 0,
|
|
16682
16988
|
subagentFiles: 0,
|
|
16989
|
+
dispatched: 0,
|
|
16990
|
+
userMessages: 0,
|
|
16683
16991
|
subagentSkipped: 0,
|
|
16684
16992
|
compactions: 0,
|
|
16685
16993
|
complete: false
|
|
@@ -16706,7 +17014,8 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16706
17014
|
if (type === "system" && record.subtype === "compact_boundary") {
|
|
16707
17015
|
result.coverage.compactions++;
|
|
16708
17016
|
}
|
|
16709
|
-
|
|
17017
|
+
if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
|
|
17018
|
+
collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
|
|
16710
17019
|
}
|
|
16711
17020
|
};
|
|
16712
17021
|
try {
|
|
@@ -16778,7 +17087,7 @@ function classifyUnobserved(path) {
|
|
|
16778
17087
|
if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
|
|
16779
17088
|
return "no_edit_record";
|
|
16780
17089
|
}
|
|
16781
|
-
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
|
|
17090
|
+
function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
|
|
16782
17091
|
const message = record.message;
|
|
16783
17092
|
const content = message?.content ?? record.content;
|
|
16784
17093
|
const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
|
|
@@ -16800,6 +17109,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
|
|
|
16800
17109
|
byPath.set(path, entry);
|
|
16801
17110
|
}
|
|
16802
17111
|
}
|
|
17112
|
+
if (DISPATCH_TOOLS.has(name) && tally) {
|
|
17113
|
+
tally.dispatched += 1;
|
|
17114
|
+
}
|
|
16803
17115
|
if (name === "Bash") {
|
|
16804
17116
|
const cmd = typeof input.command === "string" ? input.command : "";
|
|
16805
17117
|
if (cmd) {
|
|
@@ -16858,6 +17170,59 @@ function checkConservation(changedFiles, result, repoRoot2) {
|
|
|
16858
17170
|
};
|
|
16859
17171
|
}
|
|
16860
17172
|
|
|
17173
|
+
// src/lib/verdict.ts
|
|
17174
|
+
function reconcileCoverage(changed, coverage) {
|
|
17175
|
+
const changedSet = new Set(changed);
|
|
17176
|
+
const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
|
|
17177
|
+
const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
|
|
17178
|
+
const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
|
|
17179
|
+
const notReviewed = [
|
|
17180
|
+
...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
|
|
17181
|
+
...unaccounted.map((path) => ({
|
|
17182
|
+
path,
|
|
17183
|
+
reason: "unaccounted",
|
|
17184
|
+
// Named so the eventual bug report writes itself: some stage removed this
|
|
17185
|
+
// path and did not say so.
|
|
17186
|
+
stage: "unknown-stage",
|
|
17187
|
+
// An undeclared drop is CAPACITY by default. A stage that cannot be
|
|
17188
|
+
// bothered to say why it dropped a file does not get the benefit of the
|
|
17189
|
+
// doubt — that default is what makes forgetting expensive.
|
|
17190
|
+
kind: "capacity"
|
|
17191
|
+
}))
|
|
17192
|
+
];
|
|
17193
|
+
return {
|
|
17194
|
+
coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
|
|
17195
|
+
unaccounted,
|
|
17196
|
+
balances: unaccounted.length === 0
|
|
17197
|
+
};
|
|
17198
|
+
}
|
|
17199
|
+
function resolveVerdict(proposed, coverage) {
|
|
17200
|
+
if (proposed === "FAIL") return "FAIL";
|
|
17201
|
+
const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17202
|
+
if (blocking.length === 0) return proposed;
|
|
17203
|
+
return "WARN";
|
|
17204
|
+
}
|
|
17205
|
+
function describeCoverage(coverage, maxPaths = 5) {
|
|
17206
|
+
const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17207
|
+
if (relevant.length === 0) return null;
|
|
17208
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
17209
|
+
for (const n of relevant) {
|
|
17210
|
+
const key = `${n.reason}`;
|
|
17211
|
+
const list = byReason.get(key) ?? [];
|
|
17212
|
+
list.push(n.path);
|
|
17213
|
+
byReason.set(key, list);
|
|
17214
|
+
}
|
|
17215
|
+
const lines = [];
|
|
17216
|
+
for (const [reason, paths] of [...byReason.entries()].sort()) {
|
|
17217
|
+
const shown = paths.slice(0, maxPaths).join(", ");
|
|
17218
|
+
const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
|
|
17219
|
+
lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
|
|
17220
|
+
}
|
|
17221
|
+
return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
|
|
17222
|
+
${lines.join("\n")}
|
|
17223
|
+
Treat those files as UNCHECKED, not as approved.`;
|
|
17224
|
+
}
|
|
17225
|
+
|
|
16861
17226
|
// src/lib/channel.ts
|
|
16862
17227
|
var MAX_AGENT_CONTEXT_CHARS = 1500;
|
|
16863
17228
|
var MAX_AGENT_ITEMS = 5;
|
|
@@ -16936,6 +17301,45 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
|
|
|
16936
17301
|
} : {}
|
|
16937
17302
|
};
|
|
16938
17303
|
}
|
|
17304
|
+
var IDLE_EPISODE_CAP = 3;
|
|
17305
|
+
function channelSilence(input) {
|
|
17306
|
+
const movedSomething = input.newUserPrompt || input.newAuthorship;
|
|
17307
|
+
if (movedSomething) return null;
|
|
17308
|
+
if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
|
|
17309
|
+
if (input.emittedLast) return "caused-by-our-own-emission";
|
|
17310
|
+
return null;
|
|
17311
|
+
}
|
|
17312
|
+
|
|
17313
|
+
// src/lib/emit.ts
|
|
17314
|
+
var YELLOW2 = "\x1B[33m";
|
|
17315
|
+
var NC2 = "\x1B[0m";
|
|
17316
|
+
function emitVerdict(input) {
|
|
17317
|
+
const exit = input.exit ?? ((code) => process.exit(code));
|
|
17318
|
+
const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
|
|
17319
|
+
const verdict = resolveVerdict(input.proposed, coverage);
|
|
17320
|
+
const note = describeCoverage(coverage);
|
|
17321
|
+
if (unaccounted.length > 0) {
|
|
17322
|
+
process.stderr.write(
|
|
17323
|
+
`${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
|
|
17324
|
+
`
|
|
17325
|
+
);
|
|
17326
|
+
}
|
|
17327
|
+
if (verdict === "FAIL") {
|
|
17328
|
+
input.renderBlocking?.();
|
|
17329
|
+
if (input.agentContext) {
|
|
17330
|
+
process.stderr.write(`
|
|
17331
|
+
${input.agentContext}
|
|
17332
|
+
`);
|
|
17333
|
+
}
|
|
17334
|
+
if (note) process.stderr.write(`
|
|
17335
|
+
${YELLOW2}${note}${NC2}
|
|
17336
|
+
`);
|
|
17337
|
+
return exit(2);
|
|
17338
|
+
}
|
|
17339
|
+
const agentBlock = [input.agentContext, note].filter(Boolean).join("\n\n") || null;
|
|
17340
|
+
printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
|
|
17341
|
+
return exit(0);
|
|
17342
|
+
}
|
|
16939
17343
|
|
|
16940
17344
|
// src/lib/cache-cleanup.ts
|
|
16941
17345
|
var import_node_fs21 = require("node:fs");
|
|
@@ -17319,6 +17723,13 @@ function buildSummary(lines) {
|
|
|
17319
17723
|
files_read: capArray(filesRead, MAX_FILES_LIST),
|
|
17320
17724
|
files_edited: capArray(filesEdited, MAX_FILES_LIST),
|
|
17321
17725
|
files_created: capArray(filesCreated, MAX_CREATED_LIST),
|
|
17726
|
+
// The complement of the two caps that affect SCOPE. `files_read` is excluded
|
|
17727
|
+
// deliberately: reading a file is not authoring it, so a capped read list
|
|
17728
|
+
// narrows nothing.
|
|
17729
|
+
capped_out: [
|
|
17730
|
+
...cappedOut(filesEdited, MAX_FILES_LIST),
|
|
17731
|
+
...cappedOut(filesCreated, MAX_CREATED_LIST)
|
|
17732
|
+
],
|
|
17322
17733
|
searches,
|
|
17323
17734
|
commands,
|
|
17324
17735
|
subagents,
|
|
@@ -17369,6 +17780,9 @@ function sanitizeCommand(rawCmd) {
|
|
|
17369
17780
|
function capArray(set, max) {
|
|
17370
17781
|
return Array.from(set).slice(0, max);
|
|
17371
17782
|
}
|
|
17783
|
+
function cappedOut(set, max) {
|
|
17784
|
+
return Array.from(set).slice(max);
|
|
17785
|
+
}
|
|
17372
17786
|
|
|
17373
17787
|
// src/lib/run-mode.ts
|
|
17374
17788
|
function parseAutonomousEnv(raw) {
|
|
@@ -17778,12 +18192,43 @@ function agentContextFor(response, intentRepeat = 0) {
|
|
|
17778
18192
|
});
|
|
17779
18193
|
}
|
|
17780
18194
|
var beaconCtx = null;
|
|
17781
|
-
async function passAndExit(reason, skip) {
|
|
18195
|
+
async function passAndExit(reason, skip, kindOverride) {
|
|
17782
18196
|
const sent = await sendSkipBeacon(beaconCtx, skip);
|
|
17783
18197
|
logEvent("skip", { reason: skip, beacon: sent });
|
|
17784
|
-
|
|
18198
|
+
const POLICY_SKIPS = /* @__PURE__ */ new Set([
|
|
18199
|
+
"no-analyzable-files",
|
|
18200
|
+
"verity-command",
|
|
18201
|
+
"bare-acknowledgment",
|
|
18202
|
+
"reflection-prompt",
|
|
18203
|
+
"skip-mode",
|
|
18204
|
+
"zero-increment",
|
|
18205
|
+
"debounce",
|
|
18206
|
+
"no-delta-since-last-review"
|
|
18207
|
+
]);
|
|
18208
|
+
const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
|
|
18209
|
+
const changed = skipCoverageChanged;
|
|
18210
|
+
const { coverage, unaccounted } = reconcileCoverage(changed, {
|
|
18211
|
+
reviewed: [],
|
|
18212
|
+
notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
|
|
18213
|
+
});
|
|
18214
|
+
const verdict = resolveVerdict("PASS", coverage);
|
|
18215
|
+
const note = describeCoverage(coverage);
|
|
18216
|
+
if (unaccounted.length > 0) {
|
|
18217
|
+
logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
|
|
18218
|
+
}
|
|
18219
|
+
printJsonCompact(
|
|
18220
|
+
buildHookOutput(
|
|
18221
|
+
verdict,
|
|
18222
|
+
`Verity: ${reason}`,
|
|
18223
|
+
// The agent's ONLY input is additionalContext. Sixteen of the nineteen
|
|
18224
|
+
// terminating paths wrote `systemMessage` — the human's field — and told
|
|
18225
|
+
// the agent nothing at all.
|
|
18226
|
+
note
|
|
18227
|
+
)
|
|
18228
|
+
);
|
|
17785
18229
|
process.exit(0);
|
|
17786
18230
|
}
|
|
18231
|
+
var skipCoverageChanged = [];
|
|
17787
18232
|
var EMPTY_STATIC = {
|
|
17788
18233
|
tool: "@codacy/analysis-cli",
|
|
17789
18234
|
findings: [],
|
|
@@ -17798,7 +18243,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
|
|
|
17798
18243
|
}
|
|
17799
18244
|
function localOnlyAndExit(staticResults) {
|
|
17800
18245
|
printJsonCompact({
|
|
17801
|
-
gate_decision: "
|
|
18246
|
+
gate_decision: "WARN",
|
|
17802
18247
|
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
18248
|
unauthenticated: true,
|
|
17804
18249
|
static_results: staticResults
|
|
@@ -17858,6 +18303,7 @@ async function runAnalyze(opts, globals) {
|
|
|
17858
18303
|
});
|
|
17859
18304
|
}
|
|
17860
18305
|
const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
|
|
18306
|
+
skipCoverageChanged = allChanged;
|
|
17861
18307
|
const analyzable = filterAnalyzable(allChanged);
|
|
17862
18308
|
const reviewable = filterReviewable(allChanged);
|
|
17863
18309
|
const securityFiles = filterSecurity(allChanged);
|
|
@@ -17933,7 +18379,8 @@ async function runAnalyze(opts, globals) {
|
|
|
17933
18379
|
let codeDelta = {
|
|
17934
18380
|
files: [],
|
|
17935
18381
|
total_lines: 0,
|
|
17936
|
-
total_files: 0
|
|
18382
|
+
total_files: 0,
|
|
18383
|
+
excluded: []
|
|
17937
18384
|
};
|
|
17938
18385
|
let snapshotResult = { has_snapshots: false, diffs: [] };
|
|
17939
18386
|
let contentHash = null;
|
|
@@ -17998,7 +18445,11 @@ async function runAnalyze(opts, globals) {
|
|
|
17998
18445
|
if (assistantResponse) {
|
|
17999
18446
|
analysisMode = "plan";
|
|
18000
18447
|
} else {
|
|
18001
|
-
await passAndExit(
|
|
18448
|
+
await passAndExit(
|
|
18449
|
+
"No files within size limits to analyze",
|
|
18450
|
+
"size-limit",
|
|
18451
|
+
codeDelta.excluded.length > 0 ? "capacity" : "policy"
|
|
18452
|
+
);
|
|
18002
18453
|
}
|
|
18003
18454
|
}
|
|
18004
18455
|
}
|
|
@@ -18172,7 +18623,10 @@ async function runAnalyze(opts, globals) {
|
|
|
18172
18623
|
priorState.capabilities
|
|
18173
18624
|
);
|
|
18174
18625
|
memory = recallMemory(memorySession.d, memorySession.identity, {
|
|
18175
|
-
currentSessionKey: memorySession.identity.sessionKey
|
|
18626
|
+
currentSessionKey: memorySession.identity.sessionKey,
|
|
18627
|
+
// The independent witness. Only meaningful when a transcript was folded —
|
|
18628
|
+
// otherwise it stays undefined and capture coverage reads as UNKNOWN.
|
|
18629
|
+
...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
|
|
18176
18630
|
});
|
|
18177
18631
|
if (memory && !memory.provenanceHolds) {
|
|
18178
18632
|
process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
|
|
@@ -18387,6 +18841,8 @@ async function runAnalyze(opts, globals) {
|
|
|
18387
18841
|
message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
|
|
18388
18842
|
} else if (result.error.startsWith("FORBIDDEN")) {
|
|
18389
18843
|
message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
|
|
18844
|
+
} else if (result.error.startsWith("INVALID_TOKEN")) {
|
|
18845
|
+
message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
|
|
18390
18846
|
} else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
|
|
18391
18847
|
message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
|
|
18392
18848
|
} else if (result.http_status && result.http_status >= 500) {
|
|
@@ -18401,8 +18857,78 @@ async function runAnalyze(opts, globals) {
|
|
|
18401
18857
|
const response = result.data;
|
|
18402
18858
|
const decision = response.gate_decision ?? "(unrecognised)";
|
|
18403
18859
|
const sentPaths = codeDelta.files.map((f) => f.path);
|
|
18860
|
+
const reviewCoverage = {
|
|
18861
|
+
reviewed: sentPaths,
|
|
18862
|
+
// Declared drops from the stages that DO report themselves today. The other
|
|
18863
|
+
// stages surface via `unaccounted`, which is the tripwire, not the design.
|
|
18864
|
+
notReviewed: [
|
|
18865
|
+
// Every exit from the collection loop, each named. Six reasons where there
|
|
18866
|
+
// used to be two recorded and four silent — the silent ones including the
|
|
18867
|
+
// per-file size cap, which could drop a whole source file without leaving a
|
|
18868
|
+
// trace anywhere in the payload or the run row.
|
|
18869
|
+
...codeDelta.excluded,
|
|
18870
|
+
// The server-side 300-line middle-out truncation. It only bites on the
|
|
18871
|
+
// full-file branch (a first analysis, before snapshots exist) because
|
|
18872
|
+
// analyze normally sends diffs — but on that branch the reviewer sees the
|
|
18873
|
+
// first and last 100 lines and nothing between, and until now said so to
|
|
18874
|
+
// nobody. CAPACITY: a partial look is not a look.
|
|
18875
|
+
...(response.metadata?.truncated_files ?? []).map((path) => ({
|
|
18876
|
+
path,
|
|
18877
|
+
reason: "file-middle-truncated-300-lines",
|
|
18878
|
+
stage: "prompt-builder",
|
|
18879
|
+
kind: "capacity"
|
|
18880
|
+
})),
|
|
18881
|
+
// The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
|
|
18882
|
+
// what is REVIEWED, not merely what is summarised — a session editing 25
|
|
18883
|
+
// files had five silently excluded from the reviewed set.
|
|
18884
|
+
...(actionSummary?.capped_out ?? []).map((path) => ({
|
|
18885
|
+
path,
|
|
18886
|
+
reason: "edit-list-cap-20",
|
|
18887
|
+
stage: "extractActionSummary",
|
|
18888
|
+
kind: "capacity"
|
|
18889
|
+
})),
|
|
18890
|
+
// The extension allowlist, and it is POLICY rather than capacity: a changed
|
|
18891
|
+
// README was never going to be reviewed, and treating that as a coverage
|
|
18892
|
+
// gap would downgrade nearly every PASS to WARN until WARN meant nothing.
|
|
18893
|
+
// Recorded so the ledger balances and so "what did Verity ignore entirely"
|
|
18894
|
+
// is answerable — but it never touches the verdict.
|
|
18895
|
+
...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
|
|
18896
|
+
path,
|
|
18897
|
+
reason: "not-a-reviewed-file-type",
|
|
18898
|
+
stage: "extension-allowlist",
|
|
18899
|
+
kind: "policy"
|
|
18900
|
+
}))
|
|
18901
|
+
]
|
|
18902
|
+
};
|
|
18404
18903
|
const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
|
|
18405
18904
|
const watermarkIsPartial = !!codeDelta.truncated;
|
|
18905
|
+
let silenced = null;
|
|
18906
|
+
let turnIsIdleForChannel = true;
|
|
18907
|
+
if (memorySession) {
|
|
18908
|
+
try {
|
|
18909
|
+
const st = foldDossier(memorySession.d);
|
|
18910
|
+
turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
|
|
18911
|
+
silenced = channelSilence({
|
|
18912
|
+
// The BUFFER, not intentContext.user_prompt: the latter falls back to a
|
|
18913
|
+
// linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
|
|
18914
|
+
// not a user utterance. Treating it as one would keep the loop alive on
|
|
18915
|
+
// exactly the autonomous cohort.
|
|
18916
|
+
newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
18917
|
+
newAuthorship: !turnIsIdleForChannel,
|
|
18918
|
+
emittedLast: st.meta.channel?.emittedLast === true,
|
|
18919
|
+
consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
|
|
18920
|
+
});
|
|
18921
|
+
} catch {
|
|
18922
|
+
silenced = null;
|
|
18923
|
+
}
|
|
18924
|
+
}
|
|
18925
|
+
if (silenced) {
|
|
18926
|
+
logEvent("channel_silenced", {
|
|
18927
|
+
reason: silenced,
|
|
18928
|
+
run_id: response.run_id ?? turnId,
|
|
18929
|
+
decision
|
|
18930
|
+
});
|
|
18931
|
+
}
|
|
18406
18932
|
let intentRepeatCount = 0;
|
|
18407
18933
|
if (memorySession) {
|
|
18408
18934
|
try {
|
|
@@ -18418,7 +18944,15 @@ async function runAnalyze(opts, globals) {
|
|
|
18418
18944
|
title: f.title,
|
|
18419
18945
|
severity: f.severity
|
|
18420
18946
|
})) ?? [],
|
|
18421
|
-
intent: response.intent_alignment ?? null
|
|
18947
|
+
intent: response.intent_alignment ?? null,
|
|
18948
|
+
// The same signal F1 introduced: bytes differing from the hash frozen at
|
|
18949
|
+
// the last verdict. A turn that moved nothing is the only kind that can
|
|
18950
|
+
// accumulate a repeat.
|
|
18951
|
+
idle: turnIsIdleForChannel,
|
|
18952
|
+
// What next turn reads as `emittedLast`. A suppressed turn did not
|
|
18953
|
+
// speak, so it cannot be the cause of the turn after it — which is what
|
|
18954
|
+
// keeps this from becoming a permanent gag.
|
|
18955
|
+
emitted: !silenced
|
|
18422
18956
|
});
|
|
18423
18957
|
intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
|
|
18424
18958
|
} catch {
|
|
@@ -18597,7 +19131,16 @@ ${YELLOW}${loginNudge.trim()}${NC}
|
|
|
18597
19131
|
if (grantNudge) process.stderr.write(`
|
|
18598
19132
|
${YELLOW}${grantNudge.trim()}${NC}
|
|
18599
19133
|
`);
|
|
18600
|
-
|
|
19134
|
+
emitVerdict({
|
|
19135
|
+
proposed: "FAIL",
|
|
19136
|
+
changed: skipCoverageChanged,
|
|
19137
|
+
coverage: reviewCoverage,
|
|
19138
|
+
userSummary: "",
|
|
19139
|
+
// Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
|
|
19140
|
+
// the findings themselves are rendered above by the blocking renderer,
|
|
19141
|
+
// so what the cut removes is the repeated commentary, never the defect.
|
|
19142
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
|
|
19143
|
+
});
|
|
18601
19144
|
break;
|
|
18602
19145
|
}
|
|
18603
19146
|
case "PASS": {
|
|
@@ -18609,10 +19152,13 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18609
19152
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18610
19153
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18611
19154
|
userSummary += loginNudge + grantNudge;
|
|
18612
|
-
|
|
18613
|
-
|
|
18614
|
-
|
|
18615
|
-
|
|
19155
|
+
emitVerdict({
|
|
19156
|
+
proposed: "PASS",
|
|
19157
|
+
changed: skipCoverageChanged,
|
|
19158
|
+
coverage: reviewCoverage,
|
|
19159
|
+
userSummary,
|
|
19160
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
|
|
19161
|
+
});
|
|
18616
19162
|
break;
|
|
18617
19163
|
}
|
|
18618
19164
|
case "WARN": {
|
|
@@ -18623,10 +19169,13 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18623
19169
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18624
19170
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18625
19171
|
userSummary += loginNudge + grantNudge;
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
19172
|
+
emitVerdict({
|
|
19173
|
+
proposed: "WARN",
|
|
19174
|
+
changed: skipCoverageChanged,
|
|
19175
|
+
coverage: reviewCoverage,
|
|
19176
|
+
userSummary,
|
|
19177
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
|
|
19178
|
+
});
|
|
18630
19179
|
break;
|
|
18631
19180
|
}
|
|
18632
19181
|
default: {
|
|
@@ -19042,7 +19591,7 @@ async function runGuard(opts, globals) {
|
|
|
19042
19591
|
cmd: "guard"
|
|
19043
19592
|
});
|
|
19044
19593
|
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." : "";
|
|
19594
|
+
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
19595
|
emitAllowNotice(
|
|
19047
19596
|
`\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
|
|
19048
19597
|
`Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
|
|
@@ -20426,7 +20975,7 @@ function registerTelemetryCommands(program2) {
|
|
|
20426
20975
|
}
|
|
20427
20976
|
|
|
20428
20977
|
// src/cli.ts
|
|
20429
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.
|
|
20978
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.8cc9c66").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
20979
|
try {
|
|
20431
20980
|
await foldLegacyLocalCredential();
|
|
20432
20981
|
} catch {
|
|
@@ -20435,6 +20984,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
|
|
|
20435
20984
|
registerAuthCommands(program);
|
|
20436
20985
|
registerLoginCommand(program);
|
|
20437
20986
|
registerTokenCommand(program);
|
|
20987
|
+
registerSessionsCommands(program);
|
|
20988
|
+
registerLogoutCommand(program);
|
|
20438
20989
|
registerHooksCommands(program);
|
|
20439
20990
|
registerIntentCommands(program);
|
|
20440
20991
|
registerLifecycleCommands(program);
|