@codacy/verity-cli 0.28.1-experimental.4da2503 → 0.28.1-experimental.644501f
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/bin/verity.js +1043 -228
- 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;
|
|
@@ -10924,8 +10923,8 @@ function filterReviewable(files) {
|
|
|
10924
10923
|
const ext = (0, import_node_path3.extname)(f).slice(1);
|
|
10925
10924
|
if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
|
|
10926
10925
|
if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
|
|
10927
|
-
const
|
|
10928
|
-
if (REVIEWABLE_FILENAMES.has(
|
|
10926
|
+
const basename3 = f.split("/").pop() ?? "";
|
|
10927
|
+
if (REVIEWABLE_FILENAMES.has(basename3)) return true;
|
|
10929
10928
|
if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
|
|
10930
10929
|
return false;
|
|
10931
10930
|
});
|
|
@@ -11016,7 +11015,54 @@ function listTrackedFiles() {
|
|
|
11016
11015
|
return Array.from(set);
|
|
11017
11016
|
}
|
|
11018
11017
|
function sanitizeRemote(remote) {
|
|
11019
|
-
|
|
11018
|
+
const withoutUserinfo = remote.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
|
|
11019
|
+
return /[\u0000-\u001f\u007f]/.test(withoutUserinfo) ? "" : withoutUserinfo;
|
|
11020
|
+
}
|
|
11021
|
+
|
|
11022
|
+
// src/lib/token-pin.ts
|
|
11023
|
+
var cached;
|
|
11024
|
+
var warned = false;
|
|
11025
|
+
function warnOnce(message, warn) {
|
|
11026
|
+
if (!warned) {
|
|
11027
|
+
warned = true;
|
|
11028
|
+
warn(message);
|
|
11029
|
+
}
|
|
11030
|
+
return message;
|
|
11031
|
+
}
|
|
11032
|
+
async function storedCredential() {
|
|
11033
|
+
if (cached !== void 0) return cached;
|
|
11034
|
+
const rec = await readGlobalCredential(currentRemote());
|
|
11035
|
+
cached = rec ? { token: rec.token, serviceUrl: rec.serviceUrl } : null;
|
|
11036
|
+
return cached;
|
|
11037
|
+
}
|
|
11038
|
+
var userNamedUrl = null;
|
|
11039
|
+
function setUserNamedServiceUrl(url) {
|
|
11040
|
+
userNamedUrl = url?.trim() || null;
|
|
11041
|
+
}
|
|
11042
|
+
async function checkTokenPin(token, targetUrl) {
|
|
11043
|
+
if (process.env.VERITY_TOKEN && token === process.env.VERITY_TOKEN) return { attach: true };
|
|
11044
|
+
const envUrl = process.env.VERITY_SERVICE_URL?.trim();
|
|
11045
|
+
if (envUrl && normalizeUrl(envUrl) === normalizeUrl(targetUrl)) return { attach: true };
|
|
11046
|
+
if (userNamedUrl && normalizeUrl(userNamedUrl) === normalizeUrl(targetUrl)) return { attach: true };
|
|
11047
|
+
const stored = await storedCredential();
|
|
11048
|
+
if (!stored || stored.token !== token) return { attach: true };
|
|
11049
|
+
if (!stored.serviceUrl) return { attach: false, reason: "unpinnable" };
|
|
11050
|
+
return normalizeUrl(stored.serviceUrl) === normalizeUrl(targetUrl) ? { attach: true } : { attach: false, reason: "mismatch", mintedFor: stored.serviceUrl };
|
|
11051
|
+
}
|
|
11052
|
+
function normalizeUrl(raw) {
|
|
11053
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
11054
|
+
try {
|
|
11055
|
+
const u = new URL(trimmed);
|
|
11056
|
+
return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
|
|
11057
|
+
} catch {
|
|
11058
|
+
return trimmed.toLowerCase();
|
|
11059
|
+
}
|
|
11060
|
+
}
|
|
11061
|
+
function pinRefusalMessage(verdict, targetUrl) {
|
|
11062
|
+
if (verdict.reason === "unpinnable") {
|
|
11063
|
+
return `Refusing to send your Verity login to ${targetUrl}: this machine's credential does not record which service issued it, so it cannot be verified. Run "verity login" to re-issue it.`;
|
|
11064
|
+
}
|
|
11065
|
+
return `Refusing to send your Verity login to ${targetUrl}: it was issued by ${verdict.mintedFor}. A repository cannot redirect your credential to another service. If this service is genuinely yours, log in against it explicitly: VERITY_SERVICE_URL=${targetUrl} verity login`;
|
|
11020
11066
|
}
|
|
11021
11067
|
|
|
11022
11068
|
// src/lib/api-client.ts
|
|
@@ -11053,6 +11099,24 @@ async function apiRequest(options) {
|
|
|
11053
11099
|
"Content-Type": "application/json"
|
|
11054
11100
|
};
|
|
11055
11101
|
if (token) {
|
|
11102
|
+
const pin = await checkTokenPin(token, serviceUrl);
|
|
11103
|
+
if (!pin.attach) {
|
|
11104
|
+
const message = warnOnce(pinRefusalMessage(pin, serviceUrl), printWarn);
|
|
11105
|
+
logHttpCall({
|
|
11106
|
+
cmd,
|
|
11107
|
+
method,
|
|
11108
|
+
url,
|
|
11109
|
+
duration_ms: 0,
|
|
11110
|
+
http_status: null,
|
|
11111
|
+
// 'network' rather than a new category on purpose: no request left the
|
|
11112
|
+
// machine, and every caller's offline path is exactly the handling a
|
|
11113
|
+
// refusal wants (fall back to local, never fabricate a verdict).
|
|
11114
|
+
category: "network",
|
|
11115
|
+
error: "token_pin_refused",
|
|
11116
|
+
retry
|
|
11117
|
+
});
|
|
11118
|
+
return { ok: false, error: `TOKEN_PIN_REFUSED: ${message}`, category: "network" };
|
|
11119
|
+
}
|
|
11056
11120
|
headers["Authorization"] = `Bearer ${token}`;
|
|
11057
11121
|
}
|
|
11058
11122
|
const remote = requestRemote();
|
|
@@ -11165,14 +11229,9 @@ async function serviceUrlFromCredentials() {
|
|
|
11165
11229
|
async function serviceUrlFromVerityMd() {
|
|
11166
11230
|
try {
|
|
11167
11231
|
const content = await (0, import_promises2.readFile)(projectPath(VERITY_MD_FILE), "utf-8");
|
|
11168
|
-
const
|
|
11169
|
-
if (
|
|
11170
|
-
const urlMatch =
|
|
11171
|
-
if (urlMatch) return urlMatch[0];
|
|
11172
|
-
}
|
|
11173
|
-
const plainLine = content.split("\n").find((l) => /(?:url|service)\s*:/i.test(l));
|
|
11174
|
-
if (plainLine) {
|
|
11175
|
-
const urlMatch = plainLine.match(/https:\/\/[^\s]+/);
|
|
11232
|
+
const line = content.split("\n").find((l) => /\*{0,2}(?:url|service)\*{0,2}\s*:/i.test(l));
|
|
11233
|
+
if (line) {
|
|
11234
|
+
const urlMatch = line.match(/https:\/\/[^\s]+/);
|
|
11176
11235
|
if (urlMatch) return urlMatch[0];
|
|
11177
11236
|
}
|
|
11178
11237
|
} catch {
|
|
@@ -11195,7 +11254,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
|
|
|
11195
11254
|
if (mdUrl) {
|
|
11196
11255
|
return { ok: true, data: { url: mdUrl, source: "verity_md" } };
|
|
11197
11256
|
}
|
|
11198
|
-
return {
|
|
11257
|
+
return {
|
|
11258
|
+
ok: false,
|
|
11259
|
+
error: 'No Verity service URL found. Run "verity login" to get started, or /verity-setup to configure this project.'
|
|
11260
|
+
};
|
|
11261
|
+
}
|
|
11262
|
+
async function resolveServiceUrlForAuth(flagUrl) {
|
|
11263
|
+
const strict = await resolveServiceUrlDetailed(flagUrl);
|
|
11264
|
+
if (strict.ok) return strict.data;
|
|
11265
|
+
return { url: DEFAULT_SERVICE_URL, source: "default" };
|
|
11199
11266
|
}
|
|
11200
11267
|
async function resolveServiceUrl(flagUrl) {
|
|
11201
11268
|
const result = await resolveServiceUrlDetailed(flagUrl);
|
|
@@ -11234,7 +11301,10 @@ async function resolveToken(flagToken) {
|
|
|
11234
11301
|
data: { token: local.token, source: "local", userId: local.userId, email: local.email }
|
|
11235
11302
|
};
|
|
11236
11303
|
}
|
|
11237
|
-
return {
|
|
11304
|
+
return {
|
|
11305
|
+
ok: false,
|
|
11306
|
+
error: 'No Verity token found. Run "verity login" to sign in, or /verity-setup to set up this project.'
|
|
11307
|
+
};
|
|
11238
11308
|
}
|
|
11239
11309
|
async function whoami(token, serviceUrl, verbose) {
|
|
11240
11310
|
return apiRequest({
|
|
@@ -11671,16 +11741,81 @@ function registerAuthCommands(program2) {
|
|
|
11671
11741
|
});
|
|
11672
11742
|
}
|
|
11673
11743
|
|
|
11744
|
+
// src/lib/login-report.ts
|
|
11745
|
+
async function reportLoginOutcome(out, opts = {}) {
|
|
11746
|
+
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11747
|
+
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11748
|
+
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11749
|
+
if (out.expiresAt) {
|
|
11750
|
+
printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
|
|
11751
|
+
printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
|
|
11752
|
+
}
|
|
11753
|
+
if (out.repoCount > 0) {
|
|
11754
|
+
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11755
|
+
}
|
|
11756
|
+
if (out.prunedCredentials > 0) {
|
|
11757
|
+
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, opts.verbose);
|
|
11758
|
+
} else if (out.prunedCredentials < 0) {
|
|
11759
|
+
printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
|
|
11760
|
+
printWarn(" will keep taking precedence over this login in their own repositories.");
|
|
11761
|
+
printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
|
|
11762
|
+
}
|
|
11763
|
+
if (out.repoCount === 0) {
|
|
11764
|
+
printWarn("The Verity GitHub App is not installed on any account you can access.");
|
|
11765
|
+
printInfo(" Install it (and grant your repositories), then re-run verity login:");
|
|
11766
|
+
printInfo(` ${githubAppInstallUrl(null)}`);
|
|
11767
|
+
return;
|
|
11768
|
+
}
|
|
11769
|
+
const remote = opts.remote;
|
|
11770
|
+
if (!remote) return;
|
|
11771
|
+
const who = await whoami(out.token, out.serviceUrl, opts.verbose);
|
|
11772
|
+
if (who.ok && who.data.grant_status != null) {
|
|
11773
|
+
printInfo(" \u2713 This repository is covered.");
|
|
11774
|
+
} else if (!who.ok) {
|
|
11775
|
+
printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
|
|
11776
|
+
} else {
|
|
11777
|
+
const parsed = parseRemote(remote);
|
|
11778
|
+
const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
|
|
11779
|
+
printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
|
|
11780
|
+
printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
|
|
11781
|
+
printInfo(` ${installUrl}`);
|
|
11782
|
+
}
|
|
11783
|
+
const rec = await readGlobalCredential(remote);
|
|
11784
|
+
if (rec && rec.token !== out.token) {
|
|
11785
|
+
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11786
|
+
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11787
|
+
if (otherBackend) {
|
|
11788
|
+
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11789
|
+
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11790
|
+
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11791
|
+
printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
|
|
11792
|
+
printWarn(" the full GitHub flow every time.");
|
|
11793
|
+
} else if (otherIdentity) {
|
|
11794
|
+
printWarn(" Note: this repository uses a different account's credential, which takes");
|
|
11795
|
+
printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
|
|
11796
|
+
printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
|
|
11797
|
+
printWarn(" only if you want this repository on the login you just completed.");
|
|
11798
|
+
} else {
|
|
11799
|
+
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11800
|
+
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11801
|
+
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11802
|
+
printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
|
|
11803
|
+
printWarn(" every time.");
|
|
11804
|
+
}
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11807
|
+
|
|
11674
11808
|
// src/commands/login.ts
|
|
11675
11809
|
function registerLoginCommand(program2) {
|
|
11676
11810
|
program2.command("login").description("Log in to Verity (one GitHub login grants access to all your repositories)").option("--force", "Re-authenticate even if already logged in").action(async (opts) => {
|
|
11677
11811
|
const globals = program2.opts();
|
|
11678
|
-
const
|
|
11679
|
-
if (
|
|
11680
|
-
|
|
11681
|
-
|
|
11812
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11813
|
+
if (resolution.source === "default") {
|
|
11814
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
11815
|
+
} else {
|
|
11816
|
+
printVerbose(`Service URL from ${resolution.source}: ${resolution.url}`, globals.verbose);
|
|
11682
11817
|
}
|
|
11683
|
-
const heal = await maybeHealServiceUrl(
|
|
11818
|
+
const heal = await maybeHealServiceUrl(resolution, globals.verbose);
|
|
11684
11819
|
const serviceUrl = heal.serviceUrl;
|
|
11685
11820
|
if (heal.healed) {
|
|
11686
11821
|
printInfo(" Completing login updates ~/.verity/credentials against the live service.");
|
|
@@ -11721,65 +11856,7 @@ function registerLoginCommand(program2) {
|
|
|
11721
11856
|
printError(`Login failed: ${result.error}`);
|
|
11722
11857
|
process.exit(1);
|
|
11723
11858
|
}
|
|
11724
|
-
|
|
11725
|
-
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11726
|
-
printInfo(`Logged in as ${identity}. \u2713`);
|
|
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
|
-
}
|
|
11732
|
-
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11733
|
-
if (out.prunedCredentials > 0) {
|
|
11734
|
-
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
|
|
11735
|
-
} else if (out.prunedCredentials < 0) {
|
|
11736
|
-
printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
|
|
11737
|
-
printWarn(" will keep taking precedence over this login in their own repositories.");
|
|
11738
|
-
printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
|
|
11739
|
-
}
|
|
11740
|
-
if (out.repoCount === 0) {
|
|
11741
|
-
printWarn("The Verity GitHub App is not installed on any account you can access.");
|
|
11742
|
-
printInfo(` Install it (and grant your repositories), then re-run verity login:`);
|
|
11743
|
-
printInfo(` ${githubAppInstallUrl(null)}`);
|
|
11744
|
-
return;
|
|
11745
|
-
}
|
|
11746
|
-
if (remote) {
|
|
11747
|
-
const who = await whoami(out.token, out.serviceUrl, globals.verbose);
|
|
11748
|
-
if (who.ok && who.data.grant_status != null) {
|
|
11749
|
-
printInfo(" \u2713 This repository is covered.");
|
|
11750
|
-
} else if (!who.ok) {
|
|
11751
|
-
printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
|
|
11752
|
-
} else {
|
|
11753
|
-
const parsed = parseRemote(remote);
|
|
11754
|
-
const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
|
|
11755
|
-
printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
|
|
11756
|
-
printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
|
|
11757
|
-
printInfo(` ${installUrl}`);
|
|
11758
|
-
}
|
|
11759
|
-
const rec = await readGlobalCredential(remote);
|
|
11760
|
-
if (rec && rec.token !== out.token) {
|
|
11761
|
-
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11762
|
-
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11763
|
-
if (otherBackend) {
|
|
11764
|
-
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11765
|
-
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
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.");
|
|
11774
|
-
} else {
|
|
11775
|
-
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11776
|
-
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
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.");
|
|
11780
|
-
}
|
|
11781
|
-
}
|
|
11782
|
-
}
|
|
11859
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
|
|
11783
11860
|
});
|
|
11784
11861
|
}
|
|
11785
11862
|
|
|
@@ -11914,12 +11991,12 @@ async function auth(globals) {
|
|
|
11914
11991
|
printError(tokenResult.error);
|
|
11915
11992
|
process.exit(1);
|
|
11916
11993
|
}
|
|
11917
|
-
const
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11994
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11995
|
+
return {
|
|
11996
|
+
token: tokenResult.data.token,
|
|
11997
|
+
serviceUrl: resolution.url,
|
|
11998
|
+
keyed: tokenResult.data.keyed === true
|
|
11999
|
+
};
|
|
11923
12000
|
}
|
|
11924
12001
|
function explain(error) {
|
|
11925
12002
|
if (error.startsWith("FORBIDDEN")) {
|
|
@@ -11928,6 +12005,20 @@ function explain(error) {
|
|
|
11928
12005
|
printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
|
|
11929
12006
|
}
|
|
11930
12007
|
}
|
|
12008
|
+
async function logoutLocallyOnly(keyed) {
|
|
12009
|
+
printWarn("Could not reach the Verity service to revoke this login (see above).");
|
|
12010
|
+
const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
12011
|
+
if (cleared) {
|
|
12012
|
+
printInfo(keyed ? "This repository\u2019s Verity credential cleared \u2014 it is signed out. \u2713" : "Local login credential cleared \u2014 this machine is signed out. \u2713");
|
|
12013
|
+
printInfo(" The session may still exist server-side; revoke it from another machine with");
|
|
12014
|
+
printInfo(' "verity sessions revoke <session-id>" if that matters.');
|
|
12015
|
+
} else {
|
|
12016
|
+
printInfo("No local credential to clear \u2014 already signed out here.");
|
|
12017
|
+
}
|
|
12018
|
+
}
|
|
12019
|
+
function isPinRefusal(error) {
|
|
12020
|
+
return error.startsWith("TOKEN_PIN_REFUSED");
|
|
12021
|
+
}
|
|
11931
12022
|
function registerSessionsCommands(program2) {
|
|
11932
12023
|
const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
|
|
11933
12024
|
sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
|
|
@@ -11972,7 +12063,7 @@ function registerSessionsCommands(program2) {
|
|
|
11972
12063
|
});
|
|
11973
12064
|
sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
|
|
11974
12065
|
const globals = program2.opts();
|
|
11975
|
-
const { token, serviceUrl } = await auth(globals);
|
|
12066
|
+
const { token, serviceUrl, keyed } = await auth(globals);
|
|
11976
12067
|
const result = await apiRequest({
|
|
11977
12068
|
method: "DELETE",
|
|
11978
12069
|
path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
|
|
@@ -11991,7 +12082,7 @@ function registerSessionsCommands(program2) {
|
|
|
11991
12082
|
}
|
|
11992
12083
|
printInfo(`Session ${sessionId} revoked. \u2713`);
|
|
11993
12084
|
if (result.data.was_current) {
|
|
11994
|
-
const cleared = await removeGlobalCredential("");
|
|
12085
|
+
const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
11995
12086
|
printInfo(cleared ? ' That was this machine \u2014 local credential cleared. Run "verity login" to sign back in.' : ' That was this machine. Run "verity login" to sign back in.');
|
|
11996
12087
|
}
|
|
11997
12088
|
});
|
|
@@ -12003,7 +12094,7 @@ function registerLogoutCommand(program2) {
|
|
|
12003
12094
|
printError("Use either --all or --others, not both.");
|
|
12004
12095
|
process.exit(1);
|
|
12005
12096
|
}
|
|
12006
|
-
const { token, serviceUrl } = await auth(globals);
|
|
12097
|
+
const { token, serviceUrl, keyed } = await auth(globals);
|
|
12007
12098
|
if (opts.all || opts.others) {
|
|
12008
12099
|
const result = await apiRequest({
|
|
12009
12100
|
method: "DELETE",
|
|
@@ -12014,6 +12105,10 @@ function registerLogoutCommand(program2) {
|
|
|
12014
12105
|
cmd: "logout"
|
|
12015
12106
|
});
|
|
12016
12107
|
if (!result.ok) {
|
|
12108
|
+
if (isPinRefusal(result.error) && !opts.others) {
|
|
12109
|
+
await logoutLocallyOnly(keyed);
|
|
12110
|
+
return;
|
|
12111
|
+
}
|
|
12017
12112
|
printError(result.error);
|
|
12018
12113
|
explain(result.error);
|
|
12019
12114
|
process.exit(1);
|
|
@@ -12038,15 +12133,21 @@ function registerLogoutCommand(program2) {
|
|
|
12038
12133
|
cmd: "logout"
|
|
12039
12134
|
});
|
|
12040
12135
|
if (!list.ok) {
|
|
12136
|
+
if (isPinRefusal(list.error)) {
|
|
12137
|
+
await logoutLocallyOnly(keyed);
|
|
12138
|
+
return;
|
|
12139
|
+
}
|
|
12041
12140
|
printError(list.error);
|
|
12042
12141
|
explain(list.error);
|
|
12043
12142
|
process.exit(1);
|
|
12044
12143
|
}
|
|
12045
12144
|
const current = list.data.sessions.find((s) => s.current);
|
|
12046
12145
|
if (!current) {
|
|
12047
|
-
printWarn("This machine is not signed in with a Verity login.");
|
|
12048
|
-
const cleared2 = await removeGlobalCredential("");
|
|
12049
|
-
if (cleared2)
|
|
12146
|
+
printWarn(keyed ? "This repository uses its own Verity credential, not a machine login." : "This machine is not signed in with a Verity login.");
|
|
12147
|
+
const cleared2 = await removeGlobalCredential(keyed ? currentRemote() : "");
|
|
12148
|
+
if (cleared2) {
|
|
12149
|
+
printInfo(keyed ? " Cleared this repository\u2019s credential; your machine login is untouched." : " Cleared the local login credential anyway.");
|
|
12150
|
+
}
|
|
12050
12151
|
return;
|
|
12051
12152
|
}
|
|
12052
12153
|
const revoked = await apiRequest({
|
|
@@ -12058,6 +12159,10 @@ function registerLogoutCommand(program2) {
|
|
|
12058
12159
|
cmd: "logout"
|
|
12059
12160
|
});
|
|
12060
12161
|
if (!revoked.ok) {
|
|
12162
|
+
if (isPinRefusal(revoked.error)) {
|
|
12163
|
+
await logoutLocallyOnly(keyed);
|
|
12164
|
+
return;
|
|
12165
|
+
}
|
|
12061
12166
|
printError(revoked.error);
|
|
12062
12167
|
explain(revoked.error);
|
|
12063
12168
|
process.exit(1);
|
|
@@ -13591,6 +13696,143 @@ var import_node_fs10 = require("node:fs");
|
|
|
13591
13696
|
var import_node_crypto5 = require("node:crypto");
|
|
13592
13697
|
var import_node_path11 = require("node:path");
|
|
13593
13698
|
|
|
13699
|
+
// src/lib/skip-detection.ts
|
|
13700
|
+
function isBareAckPrompt(prompt) {
|
|
13701
|
+
if (typeof prompt !== "string") return false;
|
|
13702
|
+
const trimmed = prompt.trim();
|
|
13703
|
+
if (trimmed.length === 0) return false;
|
|
13704
|
+
if (trimmed.length > 20) return false;
|
|
13705
|
+
const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
|
|
13706
|
+
return bareAckPattern.test(trimmed);
|
|
13707
|
+
}
|
|
13708
|
+
function isContinuationPrompt(prompt) {
|
|
13709
|
+
if (typeof prompt !== "string") return false;
|
|
13710
|
+
const trimmed = prompt.trim();
|
|
13711
|
+
if (trimmed.length === 0) return false;
|
|
13712
|
+
if (trimmed.length > 24) return false;
|
|
13713
|
+
const continuation = /^(let['’]?s\s+(go|do\s+it|start|continue)|go|go\s+ahead|go\s+on|proceed|continue|carry\s+on|keep\s+going|do\s+it|make\s+it\s+so|next|start|begin|ship\s+it|yes\s+please|please\s+continue|perfect|great|nice|excellent|agreed)[.!]*$/i;
|
|
13714
|
+
return continuation.test(trimmed) || isBareAckPrompt(trimmed);
|
|
13715
|
+
}
|
|
13716
|
+
function resolveGoalPrompt(prompts) {
|
|
13717
|
+
if (prompts.length === 0) return null;
|
|
13718
|
+
const latest = prompts[prompts.length - 1];
|
|
13719
|
+
if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
|
|
13720
|
+
for (let i = prompts.length - 2; i >= 0; i--) {
|
|
13721
|
+
if (!isContinuationPrompt(prompts[i].prompt)) {
|
|
13722
|
+
return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
|
|
13723
|
+
}
|
|
13724
|
+
}
|
|
13725
|
+
return { entry: latest, turnsBack: 0 };
|
|
13726
|
+
}
|
|
13727
|
+
function isReflectionQuestion(response) {
|
|
13728
|
+
if (!response || typeof response !== "string") return false;
|
|
13729
|
+
const markers = [
|
|
13730
|
+
/reflection\s+for\s+future\s+agents/i,
|
|
13731
|
+
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
13732
|
+
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
13733
|
+
/quick\s+reflection\s+question/i,
|
|
13734
|
+
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
13735
|
+
// interactive, asks the user to confirm/correct before recording. That
|
|
13736
|
+
// turn authors no code either, so it's still a reflection turn.
|
|
13737
|
+
/reflection\s+draft/i,
|
|
13738
|
+
/confirm,?\s+correct,?\s+or\s+add/i
|
|
13739
|
+
];
|
|
13740
|
+
return markers.some((m) => m.test(response));
|
|
13741
|
+
}
|
|
13742
|
+
function isMetaTaskLabel(label2) {
|
|
13743
|
+
if (label2 === null || label2 === void 0) return false;
|
|
13744
|
+
if (typeof label2 !== "string") return false;
|
|
13745
|
+
const trimmed = label2.trim();
|
|
13746
|
+
if (trimmed.length === 0) return true;
|
|
13747
|
+
const metaPatterns = [
|
|
13748
|
+
/^verity\s+[\w-]+\s+response$/i,
|
|
13749
|
+
// "Verity reflect response"
|
|
13750
|
+
/^simple user response$/i,
|
|
13751
|
+
/^verity\s+command$/i,
|
|
13752
|
+
// "Verity command"
|
|
13753
|
+
/^user\s+(question|reply|response|ack)$/i
|
|
13754
|
+
];
|
|
13755
|
+
return metaPatterns.some((p) => p.test(trimmed));
|
|
13756
|
+
}
|
|
13757
|
+
function shouldSkipForBareAck(input) {
|
|
13758
|
+
if (!isBareAckPrompt(input.prompt)) return false;
|
|
13759
|
+
if (input.turnAuthoredCode) return false;
|
|
13760
|
+
return input.canSeeTurnAuthorship;
|
|
13761
|
+
}
|
|
13762
|
+
|
|
13763
|
+
// src/lib/pending-repeat.ts
|
|
13764
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
13765
|
+
"the",
|
|
13766
|
+
"and",
|
|
13767
|
+
"that",
|
|
13768
|
+
"this",
|
|
13769
|
+
"with",
|
|
13770
|
+
"from",
|
|
13771
|
+
"have",
|
|
13772
|
+
"been",
|
|
13773
|
+
"were",
|
|
13774
|
+
"what",
|
|
13775
|
+
"when",
|
|
13776
|
+
"which",
|
|
13777
|
+
"their",
|
|
13778
|
+
"there",
|
|
13779
|
+
"these",
|
|
13780
|
+
"those",
|
|
13781
|
+
"would",
|
|
13782
|
+
"could",
|
|
13783
|
+
"should",
|
|
13784
|
+
"must",
|
|
13785
|
+
"will",
|
|
13786
|
+
"also",
|
|
13787
|
+
"just",
|
|
13788
|
+
"only",
|
|
13789
|
+
"into",
|
|
13790
|
+
"over",
|
|
13791
|
+
"than",
|
|
13792
|
+
"then",
|
|
13793
|
+
"them",
|
|
13794
|
+
"some",
|
|
13795
|
+
"such",
|
|
13796
|
+
"more",
|
|
13797
|
+
"most",
|
|
13798
|
+
"other",
|
|
13799
|
+
"about",
|
|
13800
|
+
"after",
|
|
13801
|
+
"before",
|
|
13802
|
+
"since",
|
|
13803
|
+
"because",
|
|
13804
|
+
"while",
|
|
13805
|
+
"where",
|
|
13806
|
+
"whether",
|
|
13807
|
+
"ensure",
|
|
13808
|
+
"confirm",
|
|
13809
|
+
"verify",
|
|
13810
|
+
"check"
|
|
13811
|
+
]);
|
|
13812
|
+
function pendingTokens(text) {
|
|
13813
|
+
if (!text || typeof text !== "string") return [];
|
|
13814
|
+
const out = /* @__PURE__ */ new Set();
|
|
13815
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
13816
|
+
if (raw.length <= 3) continue;
|
|
13817
|
+
if (STOP.has(raw)) continue;
|
|
13818
|
+
out.add(raw);
|
|
13819
|
+
}
|
|
13820
|
+
return [...out].sort();
|
|
13821
|
+
}
|
|
13822
|
+
var REPEAT_THRESHOLD = 0.3;
|
|
13823
|
+
function overlapCoefficient(a, b) {
|
|
13824
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
13825
|
+
const setB = new Set(b);
|
|
13826
|
+
let shared = 0;
|
|
13827
|
+
for (const t of a) if (setB.has(t)) shared++;
|
|
13828
|
+
return shared / Math.min(a.length, b.length);
|
|
13829
|
+
}
|
|
13830
|
+
function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
|
|
13831
|
+
const tokens = pendingTokens(text);
|
|
13832
|
+
if (tokens.length === 0) return false;
|
|
13833
|
+
return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
|
|
13834
|
+
}
|
|
13835
|
+
|
|
13594
13836
|
// src/lib/dossier.ts
|
|
13595
13837
|
var import_node_fs9 = require("node:fs");
|
|
13596
13838
|
var import_node_crypto4 = require("node:crypto");
|
|
@@ -13599,6 +13841,7 @@ var MAX_LINE_BYTES = 4096;
|
|
|
13599
13841
|
var MAX_GOAL_CHARS = 2e3;
|
|
13600
13842
|
var GOAL_KEEP = 8;
|
|
13601
13843
|
var GOAL_TOTAL_CAP = 32;
|
|
13844
|
+
var RECENT_PENDING_CAP = 20;
|
|
13602
13845
|
var HASH_WIDTH = 16;
|
|
13603
13846
|
var AUTHORED_CAP = 300;
|
|
13604
13847
|
var NOT_MINE_CAP = 300;
|
|
@@ -13960,7 +14203,17 @@ function reduce(state, events, now) {
|
|
|
13960
14203
|
}
|
|
13961
14204
|
case "verdict": {
|
|
13962
14205
|
state.meta.last_verdict_seq = ev.seq;
|
|
14206
|
+
state.meta.channel = {
|
|
14207
|
+
emittedLast: ev.emitted === true,
|
|
14208
|
+
// Reset by ANY movement, so the counter measures a standstill rather
|
|
14209
|
+
// than session length.
|
|
14210
|
+
consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
|
|
14211
|
+
};
|
|
13963
14212
|
state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
|
|
14213
|
+
if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
|
|
14214
|
+
const prior = state.meta.recent_pending_sigs ?? [];
|
|
14215
|
+
state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
|
|
14216
|
+
}
|
|
13964
14217
|
if (ev.intent_sig) {
|
|
13965
14218
|
state.meta.intent_repeat = state.meta.intent_repeat && state.meta.intent_repeat.sig === ev.intent_sig ? { sig: ev.intent_sig, consecutive: state.meta.intent_repeat.consecutive + 1 } : { sig: ev.intent_sig, consecutive: 1 };
|
|
13966
14219
|
} else {
|
|
@@ -14185,12 +14438,12 @@ function readFoldCache(d) {
|
|
|
14185
14438
|
if (!(0, import_node_fs9.existsSync)(d.foldPath)) return null;
|
|
14186
14439
|
const raw = JSON.parse((0, import_node_fs9.readFileSync)(d.foldPath, "utf8"));
|
|
14187
14440
|
if (raw?.v !== 1) return null;
|
|
14188
|
-
const
|
|
14189
|
-
if (!
|
|
14441
|
+
const cached2 = expandState(raw);
|
|
14442
|
+
if (!cached2?.meta) return null;
|
|
14190
14443
|
const size = (0, import_node_fs9.existsSync)(d.eventsPath) ? (0, import_node_fs9.statSync)(d.eventsPath).size : 0;
|
|
14191
14444
|
const rotations = (0, import_node_fs9.existsSync)(d.rotatedDir) ? (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).length : 0;
|
|
14192
|
-
if (
|
|
14193
|
-
return
|
|
14445
|
+
if (cached2.meta.upto_offset !== size || cached2.meta.rotations !== rotations) return null;
|
|
14446
|
+
return cached2;
|
|
14194
14447
|
} catch {
|
|
14195
14448
|
return null;
|
|
14196
14449
|
}
|
|
@@ -14327,6 +14580,10 @@ function projectMemory(state, opts) {
|
|
|
14327
14580
|
...active.delivered && { delivered: active.delivered }
|
|
14328
14581
|
};
|
|
14329
14582
|
}
|
|
14583
|
+
if (opts.capture) {
|
|
14584
|
+
const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
|
|
14585
|
+
p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
|
|
14586
|
+
}
|
|
14330
14587
|
if (state.meta.last_adjudication) {
|
|
14331
14588
|
const a = state.meta.last_adjudication;
|
|
14332
14589
|
p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
|
|
@@ -14573,7 +14830,8 @@ function recall(d, input) {
|
|
|
14573
14830
|
continuity,
|
|
14574
14831
|
spoken: reanchored.spoken,
|
|
14575
14832
|
refused: reanchored.dropped.length,
|
|
14576
|
-
lastVerdictSeq
|
|
14833
|
+
lastVerdictSeq,
|
|
14834
|
+
...input.capture && { capture: input.capture }
|
|
14577
14835
|
});
|
|
14578
14836
|
return {
|
|
14579
14837
|
state: effective,
|
|
@@ -14684,7 +14942,19 @@ function sessionDossier(token, sessionId) {
|
|
|
14684
14942
|
const d = openDossier(identity);
|
|
14685
14943
|
return d ? { d, identity } : null;
|
|
14686
14944
|
}
|
|
14945
|
+
function hasActiveGoal(d) {
|
|
14946
|
+
try {
|
|
14947
|
+
if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
|
|
14948
|
+
return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
|
|
14949
|
+
} catch {
|
|
14950
|
+
return false;
|
|
14951
|
+
}
|
|
14952
|
+
}
|
|
14687
14953
|
function recordGoal(d, prompt, source = "prompt") {
|
|
14954
|
+
if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
|
|
14955
|
+
appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
|
|
14956
|
+
return;
|
|
14957
|
+
}
|
|
14688
14958
|
const text = prompt.slice(0, MAX_GOAL_CHARS);
|
|
14689
14959
|
appendEvent(d, {
|
|
14690
14960
|
k: "goal",
|
|
@@ -14800,6 +15070,15 @@ function recordVerdict(d, v) {
|
|
|
14800
15070
|
branch: v.branch,
|
|
14801
15071
|
decision: v.decision,
|
|
14802
15072
|
...sig && { intent_sig: sig },
|
|
15073
|
+
// Fingerprints of the pending items this verdict delivered, so the NEXT turn
|
|
15074
|
+
// can tell a repeat from a new requirement. Only recorded when the channel
|
|
15075
|
+
// actually spoke — a silenced turn delivered nothing, so nothing was "said
|
|
15076
|
+
// before" and labelling the next turn's items as repeats would be a lie.
|
|
15077
|
+
...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
|
|
15078
|
+
pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
|
|
15079
|
+
},
|
|
15080
|
+
emitted: v.emitted === true,
|
|
15081
|
+
idle: v.idle !== false,
|
|
14803
15082
|
...v.intent?.verdict && { intent_verdict: v.intent.verdict },
|
|
14804
15083
|
...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
|
|
14805
15084
|
});
|
|
@@ -14846,7 +15125,9 @@ function recallMemory(d, identity, opts) {
|
|
|
14846
15125
|
const state = foldDossier(d);
|
|
14847
15126
|
const watermark = state.meta.watermark?.sha ?? null;
|
|
14848
15127
|
const watermarkPaths = (state.authored ?? []).map((a) => a.path);
|
|
15128
|
+
const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
|
|
14849
15129
|
const r = recall(d, {
|
|
15130
|
+
...captureCmp && { capture: captureCmp },
|
|
14850
15131
|
identity,
|
|
14851
15132
|
currentSessionKey: opts.currentSessionKey,
|
|
14852
15133
|
branchNow: getCurrentBranch(),
|
|
@@ -15103,6 +15384,8 @@ function collectCodeDelta(files, opts) {
|
|
|
15103
15384
|
let totalSize = 0;
|
|
15104
15385
|
let truncationReason = null;
|
|
15105
15386
|
const droppedPaths = [];
|
|
15387
|
+
const excluded = [];
|
|
15388
|
+
const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
|
|
15106
15389
|
for (const filepath of sorted) {
|
|
15107
15390
|
if (result.length >= maxFiles) {
|
|
15108
15391
|
truncationReason ??= "max_files";
|
|
@@ -15110,14 +15393,21 @@ function collectCodeDelta(files, opts) {
|
|
|
15110
15393
|
continue;
|
|
15111
15394
|
}
|
|
15112
15395
|
const resolved = resolveFile(filepath);
|
|
15113
|
-
if (!resolved)
|
|
15396
|
+
if (!resolved) {
|
|
15397
|
+
exclude(filepath, "path-not-resolvable");
|
|
15398
|
+
continue;
|
|
15399
|
+
}
|
|
15114
15400
|
let size;
|
|
15115
15401
|
try {
|
|
15116
15402
|
size = (0, import_node_fs11.statSync)(resolved).size;
|
|
15117
15403
|
} catch {
|
|
15404
|
+
exclude(filepath, "not-stattable");
|
|
15405
|
+
continue;
|
|
15406
|
+
}
|
|
15407
|
+
if (size > maxFileBytes) {
|
|
15408
|
+
exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
|
|
15118
15409
|
continue;
|
|
15119
15410
|
}
|
|
15120
|
-
if (size > maxFileBytes) continue;
|
|
15121
15411
|
if (totalSize + size > maxTotalBytes) {
|
|
15122
15412
|
truncationReason ??= "max_total_bytes";
|
|
15123
15413
|
const idx = sorted.indexOf(filepath);
|
|
@@ -15128,6 +15418,7 @@ function collectCodeDelta(files, opts) {
|
|
|
15128
15418
|
try {
|
|
15129
15419
|
content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
|
|
15130
15420
|
} catch {
|
|
15421
|
+
exclude(filepath, "not-readable");
|
|
15131
15422
|
continue;
|
|
15132
15423
|
}
|
|
15133
15424
|
totalSize += size;
|
|
@@ -15141,10 +15432,14 @@ function collectCodeDelta(files, opts) {
|
|
|
15141
15432
|
(sum, f) => sum + f.content.split("\n").length,
|
|
15142
15433
|
0
|
|
15143
15434
|
);
|
|
15435
|
+
for (const path of droppedPaths) {
|
|
15436
|
+
exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
|
|
15437
|
+
}
|
|
15144
15438
|
return {
|
|
15145
15439
|
files: result,
|
|
15146
15440
|
total_lines: totalLines,
|
|
15147
15441
|
total_files: result.length,
|
|
15442
|
+
excluded,
|
|
15148
15443
|
...truncationReason && {
|
|
15149
15444
|
truncated: {
|
|
15150
15445
|
reason: truncationReason,
|
|
@@ -15393,8 +15688,8 @@ function preImage(repoRelPath, baseline) {
|
|
|
15393
15688
|
perBaseline = /* @__PURE__ */ new Map();
|
|
15394
15689
|
preImageCache.set(baseline, perBaseline);
|
|
15395
15690
|
}
|
|
15396
|
-
const
|
|
15397
|
-
if (
|
|
15691
|
+
const cached2 = perBaseline.get(repoRelPath);
|
|
15692
|
+
if (cached2) return cached2;
|
|
15398
15693
|
const resolved = resolvePreImage(repoRelPath, baseline);
|
|
15399
15694
|
perBaseline.set(repoRelPath, resolved);
|
|
15400
15695
|
return resolved;
|
|
@@ -15438,6 +15733,34 @@ ${addedLines}`,
|
|
|
15438
15733
|
}
|
|
15439
15734
|
return { diffs, has_baseline: true };
|
|
15440
15735
|
}
|
|
15736
|
+
function absorbIntoBaseline(paths, sessionId) {
|
|
15737
|
+
const baseline = readBaseline(sessionId);
|
|
15738
|
+
if (!baseline || paths.length === 0) return 0;
|
|
15739
|
+
const dir = sessionDir(sessionKey(baseline.session_id));
|
|
15740
|
+
let adopted = 0;
|
|
15741
|
+
const dirty = new Set(baseline.dirty_paths);
|
|
15742
|
+
for (const p of paths) {
|
|
15743
|
+
try {
|
|
15744
|
+
const content = safeReadForMirror(projectPath(p));
|
|
15745
|
+
if (content === null) continue;
|
|
15746
|
+
const dest = mirrorPath(dir, p);
|
|
15747
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
15748
|
+
(0, import_node_fs13.writeFileSync)(dest, content);
|
|
15749
|
+
dirty.add(p);
|
|
15750
|
+
adopted++;
|
|
15751
|
+
} catch {
|
|
15752
|
+
}
|
|
15753
|
+
}
|
|
15754
|
+
if (adopted === 0) return 0;
|
|
15755
|
+
try {
|
|
15756
|
+
const updated = { ...baseline, dirty_paths: [...dirty] };
|
|
15757
|
+
(0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
|
|
15758
|
+
preImageCache.delete(baseline);
|
|
15759
|
+
} catch {
|
|
15760
|
+
return 0;
|
|
15761
|
+
}
|
|
15762
|
+
return adopted;
|
|
15763
|
+
}
|
|
15441
15764
|
function changedSinceBaseline(repoRelPath, baseline) {
|
|
15442
15765
|
const pre = preImage(repoRelPath, baseline);
|
|
15443
15766
|
let current;
|
|
@@ -16314,40 +16637,40 @@ function narrowToRecent(files, sessionId) {
|
|
|
16314
16637
|
});
|
|
16315
16638
|
return recent.length > 0 ? recent : files;
|
|
16316
16639
|
}
|
|
16317
|
-
function
|
|
16318
|
-
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
|
|
16640
|
+
function readIterationState(currentCommit) {
|
|
16641
|
+
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
|
|
16319
16642
|
try {
|
|
16320
16643
|
const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
|
|
16321
16644
|
const parts = stored.split(":");
|
|
16322
16645
|
const iter = parseInt(parts[0], 10);
|
|
16323
16646
|
const storedCommit = parts[1] ?? "";
|
|
16324
16647
|
const storedTimestamp = parseInt(parts[2] ?? "0", 10);
|
|
16325
|
-
|
|
16326
|
-
if (
|
|
16648
|
+
const fingerprint = parts.slice(3).join(":") || null;
|
|
16649
|
+
if (isNaN(iter)) return { iteration: 1, fingerprint: null };
|
|
16650
|
+
if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
|
|
16327
16651
|
if (storedTimestamp > 0) {
|
|
16328
16652
|
const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
|
|
16329
|
-
if (elapsed > 600) return 1;
|
|
16653
|
+
if (elapsed > 600) return { iteration: 1, fingerprint: null };
|
|
16330
16654
|
}
|
|
16331
|
-
return iter;
|
|
16655
|
+
return { iteration: iter, fingerprint };
|
|
16332
16656
|
} catch {
|
|
16333
|
-
return 1;
|
|
16657
|
+
return { iteration: 1, fingerprint: null };
|
|
16334
16658
|
}
|
|
16335
16659
|
}
|
|
16336
|
-
function
|
|
16337
|
-
const
|
|
16338
|
-
|
|
16339
|
-
writeIteration(1, currentCommit, contentHash);
|
|
16340
|
-
return {
|
|
16341
|
-
skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
|
|
16342
|
-
iteration
|
|
16343
|
-
};
|
|
16344
|
-
}
|
|
16345
|
-
return { skip: null, iteration };
|
|
16660
|
+
function findingsFingerprint(findings) {
|
|
16661
|
+
const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
|
|
16662
|
+
return [...new Set(keys)].sort().join(",");
|
|
16346
16663
|
}
|
|
16347
|
-
function
|
|
16664
|
+
function isSameProblem(previous, current) {
|
|
16665
|
+
if (!previous || !current) return false;
|
|
16666
|
+
const prev = new Set(previous.split(","));
|
|
16667
|
+
return current.split(",").some((k) => prev.has(k));
|
|
16668
|
+
}
|
|
16669
|
+
function writeIteration(iteration, commit, _contentHash, fingerprint) {
|
|
16348
16670
|
(0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
|
|
16349
16671
|
const ts = Math.floor(Date.now() / 1e3);
|
|
16350
|
-
|
|
16672
|
+
const fp = fingerprint ? `:${fingerprint}` : "";
|
|
16673
|
+
(0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
|
|
16351
16674
|
}
|
|
16352
16675
|
|
|
16353
16676
|
// src/lib/static-analysis.ts
|
|
@@ -16596,7 +16919,7 @@ function resolveTaskContext(opts) {
|
|
|
16596
16919
|
// src/lib/cli-version.ts
|
|
16597
16920
|
function cliVersion() {
|
|
16598
16921
|
try {
|
|
16599
|
-
return true ? "0.28.1-experimental.
|
|
16922
|
+
return true ? "0.28.1-experimental.644501f" : "dev";
|
|
16600
16923
|
} catch {
|
|
16601
16924
|
return "dev";
|
|
16602
16925
|
}
|
|
@@ -16690,10 +17013,26 @@ function cacheRequest(body) {
|
|
|
16690
17013
|
(0, import_node_fs18.mkdirSync)(CACHE_DIR, { recursive: true });
|
|
16691
17014
|
const suffix = (0, import_node_crypto9.randomBytes)(4).toString("hex");
|
|
16692
17015
|
const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
|
|
16693
|
-
(0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
|
|
17016
|
+
(0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
|
|
16694
17017
|
} catch {
|
|
16695
17018
|
}
|
|
16696
17019
|
}
|
|
17020
|
+
function redactRequest(body) {
|
|
17021
|
+
const b = body ?? {};
|
|
17022
|
+
const delta = b.code_delta ?? {};
|
|
17023
|
+
const files = Array.isArray(delta.files) ? delta.files : [];
|
|
17024
|
+
return {
|
|
17025
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17026
|
+
undelivered: true,
|
|
17027
|
+
total_files: files.length,
|
|
17028
|
+
total_bytes: files.reduce(
|
|
17029
|
+
(n, f) => n + (typeof f.content === "string" ? f.content.length : 0),
|
|
17030
|
+
0
|
|
17031
|
+
),
|
|
17032
|
+
// Paths only. A path is already visible in the repository; the content is not.
|
|
17033
|
+
files: files.map((f) => typeof f.path === "string" ? f.path : "<unknown>").slice(0, 100)
|
|
17034
|
+
};
|
|
17035
|
+
}
|
|
16697
17036
|
function buildOfflineFallback(reason, staticResults) {
|
|
16698
17037
|
return {
|
|
16699
17038
|
// G12 / INV-18 — WARN, not PASS.
|
|
@@ -16822,6 +17161,15 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
|
|
|
16822
17161
|
]);
|
|
16823
17162
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
|
|
16824
17163
|
var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
|
|
17164
|
+
function hasUserText(record) {
|
|
17165
|
+
const message = record.message;
|
|
17166
|
+
const content = message?.content ?? record.content;
|
|
17167
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
17168
|
+
if (!Array.isArray(content)) return false;
|
|
17169
|
+
return content.some(
|
|
17170
|
+
(b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
|
|
17171
|
+
);
|
|
17172
|
+
}
|
|
16825
17173
|
var COMMAND_CLASSES = [
|
|
16826
17174
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
|
|
16827
17175
|
[/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
|
|
@@ -16911,8 +17259,8 @@ function commandShape(cmd) {
|
|
|
16911
17259
|
var COMMAND_HEAD_CHARS = 80;
|
|
16912
17260
|
var rootCandidateCache = /* @__PURE__ */ new Map();
|
|
16913
17261
|
function candidateRoots(repoRoot2) {
|
|
16914
|
-
const
|
|
16915
|
-
if (
|
|
17262
|
+
const cached2 = rootCandidateCache.get(repoRoot2);
|
|
17263
|
+
if (cached2) return cached2;
|
|
16916
17264
|
const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
16917
17265
|
const out = [norm];
|
|
16918
17266
|
try {
|
|
@@ -16949,6 +17297,7 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16949
17297
|
malformed: 0,
|
|
16950
17298
|
subagentFiles: 0,
|
|
16951
17299
|
dispatched: 0,
|
|
17300
|
+
userMessages: 0,
|
|
16952
17301
|
subagentSkipped: 0,
|
|
16953
17302
|
compactions: 0,
|
|
16954
17303
|
complete: false
|
|
@@ -16975,6 +17324,7 @@ function fold(transcriptPath, opts = {}) {
|
|
|
16975
17324
|
if (type === "system" && record.subtype === "compact_boundary") {
|
|
16976
17325
|
result.coverage.compactions++;
|
|
16977
17326
|
}
|
|
17327
|
+
if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
|
|
16978
17328
|
collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
|
|
16979
17329
|
}
|
|
16980
17330
|
};
|
|
@@ -17130,6 +17480,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
|
|
|
17130
17480
|
};
|
|
17131
17481
|
}
|
|
17132
17482
|
|
|
17483
|
+
// src/lib/verdict.ts
|
|
17484
|
+
function reconcileCoverage(changed, coverage) {
|
|
17485
|
+
const changedSet = new Set(changed);
|
|
17486
|
+
const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
|
|
17487
|
+
const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
|
|
17488
|
+
const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
|
|
17489
|
+
const notReviewed = [
|
|
17490
|
+
...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
|
|
17491
|
+
...unaccounted.map((path) => ({
|
|
17492
|
+
path,
|
|
17493
|
+
reason: "unaccounted",
|
|
17494
|
+
// Named so the eventual bug report writes itself: some stage removed this
|
|
17495
|
+
// path and did not say so.
|
|
17496
|
+
stage: "unknown-stage",
|
|
17497
|
+
// An undeclared drop is CAPACITY by default. A stage that cannot be
|
|
17498
|
+
// bothered to say why it dropped a file does not get the benefit of the
|
|
17499
|
+
// doubt — that default is what makes forgetting expensive.
|
|
17500
|
+
kind: "capacity"
|
|
17501
|
+
}))
|
|
17502
|
+
];
|
|
17503
|
+
return {
|
|
17504
|
+
coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
|
|
17505
|
+
unaccounted,
|
|
17506
|
+
balances: unaccounted.length === 0
|
|
17507
|
+
};
|
|
17508
|
+
}
|
|
17509
|
+
function resolveVerdict(proposed, coverage) {
|
|
17510
|
+
if (proposed === "FAIL") return "FAIL";
|
|
17511
|
+
const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17512
|
+
if (blocking.length === 0) return proposed;
|
|
17513
|
+
return "WARN";
|
|
17514
|
+
}
|
|
17515
|
+
function describeCoverage(coverage, maxPaths = 5) {
|
|
17516
|
+
const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17517
|
+
if (relevant.length === 0) return null;
|
|
17518
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
17519
|
+
for (const n of relevant) {
|
|
17520
|
+
const key = `${n.reason}`;
|
|
17521
|
+
const list = byReason.get(key) ?? [];
|
|
17522
|
+
list.push(n.path);
|
|
17523
|
+
byReason.set(key, list);
|
|
17524
|
+
}
|
|
17525
|
+
const lines = [];
|
|
17526
|
+
for (const [reason, paths] of [...byReason.entries()].sort()) {
|
|
17527
|
+
const shown = paths.slice(0, maxPaths).join(", ");
|
|
17528
|
+
const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
|
|
17529
|
+
lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
|
|
17530
|
+
}
|
|
17531
|
+
return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
|
|
17532
|
+
${lines.join("\n")}
|
|
17533
|
+
Treat those files as UNCHECKED, not as approved.`;
|
|
17534
|
+
}
|
|
17535
|
+
function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
|
|
17536
|
+
const reviewed = new Set(reviewedNow);
|
|
17537
|
+
const out = [];
|
|
17538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17539
|
+
for (const s of statements) {
|
|
17540
|
+
if (s.outcome !== "open") continue;
|
|
17541
|
+
if (s.register !== "BLOCK") continue;
|
|
17542
|
+
if (s.carried) continue;
|
|
17543
|
+
if (reviewed.has(s.file)) continue;
|
|
17544
|
+
if (!s.line_sha) continue;
|
|
17545
|
+
if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
|
|
17546
|
+
const key = `${s.file}::${s.pattern_id}`;
|
|
17547
|
+
if (seen.has(key)) continue;
|
|
17548
|
+
seen.add(key);
|
|
17549
|
+
out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
|
|
17550
|
+
}
|
|
17551
|
+
return out;
|
|
17552
|
+
}
|
|
17553
|
+
function describeOpenElsewhere(open) {
|
|
17554
|
+
if (open.length === 0) return null;
|
|
17555
|
+
const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
|
|
17556
|
+
const more = open.length > 5 ? `
|
|
17557
|
+
(+${open.length - 5} more)` : "";
|
|
17558
|
+
return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
|
|
17559
|
+
${lines.join("\n")}${more}
|
|
17560
|
+
This verdict covers the current change only. The tree is not clean.`;
|
|
17561
|
+
}
|
|
17562
|
+
|
|
17133
17563
|
// src/lib/channel.ts
|
|
17134
17564
|
var MAX_AGENT_CONTEXT_CHARS = 1500;
|
|
17135
17565
|
var MAX_AGENT_ITEMS = 5;
|
|
@@ -17179,9 +17609,13 @@ function buildAgentContext(input) {
|
|
|
17179
17609
|
}
|
|
17180
17610
|
for (const p of input.pendingItems ?? []) {
|
|
17181
17611
|
if (lines.length >= MAX_AGENT_ITEMS) break;
|
|
17612
|
+
if (p.pattern_id === "intent-misalignment") continue;
|
|
17182
17613
|
const text = p.description ?? p.title ?? p.reason;
|
|
17183
17614
|
if (!text) continue;
|
|
17184
|
-
|
|
17615
|
+
const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
|
|
17616
|
+
lines.push(
|
|
17617
|
+
renderItem("", text, p.pattern_id, p.file, p.line) + (seenBefore ? "\n (raised earlier this session and still open \u2014 do not re-explain it; act on it or carry on)" : "")
|
|
17618
|
+
);
|
|
17185
17619
|
}
|
|
17186
17620
|
if (lines.length === 0) return null;
|
|
17187
17621
|
const body = `${REPORT_PREFIX}
|
|
@@ -17208,6 +17642,47 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
|
|
|
17208
17642
|
} : {}
|
|
17209
17643
|
};
|
|
17210
17644
|
}
|
|
17645
|
+
var IDLE_EPISODE_CAP = 3;
|
|
17646
|
+
function channelSilence(input) {
|
|
17647
|
+
const movedSomething = input.newUserPrompt || input.newAuthorship;
|
|
17648
|
+
if (movedSomething) return null;
|
|
17649
|
+
if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
|
|
17650
|
+
if (input.emittedLast) return "caused-by-our-own-emission";
|
|
17651
|
+
return null;
|
|
17652
|
+
}
|
|
17653
|
+
|
|
17654
|
+
// src/lib/emit.ts
|
|
17655
|
+
var YELLOW2 = "\x1B[33m";
|
|
17656
|
+
var NC2 = "\x1B[0m";
|
|
17657
|
+
function emitVerdict(input) {
|
|
17658
|
+
const exit = input.exit ?? ((code) => process.exit(code));
|
|
17659
|
+
const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
|
|
17660
|
+
let verdict = resolveVerdict(input.proposed, coverage);
|
|
17661
|
+
const openElsewhere = input.openElsewhere ?? [];
|
|
17662
|
+
if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
|
|
17663
|
+
const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
|
|
17664
|
+
if (unaccounted.length > 0) {
|
|
17665
|
+
process.stderr.write(
|
|
17666
|
+
`${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
|
|
17667
|
+
`
|
|
17668
|
+
);
|
|
17669
|
+
}
|
|
17670
|
+
if (verdict === "FAIL") {
|
|
17671
|
+
input.renderBlocking?.();
|
|
17672
|
+
if (input.agentContext) {
|
|
17673
|
+
process.stderr.write(`
|
|
17674
|
+
${input.agentContext}
|
|
17675
|
+
`);
|
|
17676
|
+
}
|
|
17677
|
+
if (note && !input.silenced) process.stderr.write(`
|
|
17678
|
+
${YELLOW2}${note}${NC2}
|
|
17679
|
+
`);
|
|
17680
|
+
return exit(2);
|
|
17681
|
+
}
|
|
17682
|
+
const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
|
|
17683
|
+
printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
|
|
17684
|
+
return exit(0);
|
|
17685
|
+
}
|
|
17211
17686
|
|
|
17212
17687
|
// src/lib/cache-cleanup.ts
|
|
17213
17688
|
var import_node_fs21 = require("node:fs");
|
|
@@ -17290,6 +17765,13 @@ function isGitOnlyPrompt(prompt) {
|
|
|
17290
17765
|
return true;
|
|
17291
17766
|
}
|
|
17292
17767
|
function reconcileAnalysisMode(predictedMode, signals) {
|
|
17768
|
+
const mode = resolveAnalysisMode(predictedMode, signals);
|
|
17769
|
+
if (mode !== "skip") return mode;
|
|
17770
|
+
const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
|
|
17771
|
+
if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
|
|
17772
|
+
return mode;
|
|
17773
|
+
}
|
|
17774
|
+
function resolveAnalysisMode(predictedMode, signals) {
|
|
17293
17775
|
if (!predictedMode || !isValidMode(predictedMode)) {
|
|
17294
17776
|
return detectAnalysisMode(
|
|
17295
17777
|
signals.noFilesChanged,
|
|
@@ -17388,46 +17870,6 @@ function shouldWarmRetryAnalyze(result) {
|
|
|
17388
17870
|
return false;
|
|
17389
17871
|
}
|
|
17390
17872
|
|
|
17391
|
-
// src/lib/skip-detection.ts
|
|
17392
|
-
function isBareAckPrompt(prompt) {
|
|
17393
|
-
if (typeof prompt !== "string") return false;
|
|
17394
|
-
const trimmed = prompt.trim();
|
|
17395
|
-
if (trimmed.length === 0) return false;
|
|
17396
|
-
if (trimmed.length > 20) return false;
|
|
17397
|
-
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;
|
|
17398
|
-
return bareAckPattern.test(trimmed);
|
|
17399
|
-
}
|
|
17400
|
-
function isReflectionQuestion(response) {
|
|
17401
|
-
if (!response || typeof response !== "string") return false;
|
|
17402
|
-
const markers = [
|
|
17403
|
-
/reflection\s+for\s+future\s+agents/i,
|
|
17404
|
-
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
17405
|
-
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
17406
|
-
/quick\s+reflection\s+question/i,
|
|
17407
|
-
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
17408
|
-
// interactive, asks the user to confirm/correct before recording. That
|
|
17409
|
-
// turn authors no code either, so it's still a reflection turn.
|
|
17410
|
-
/reflection\s+draft/i,
|
|
17411
|
-
/confirm,?\s+correct,?\s+or\s+add/i
|
|
17412
|
-
];
|
|
17413
|
-
return markers.some((m) => m.test(response));
|
|
17414
|
-
}
|
|
17415
|
-
function isMetaTaskLabel(label2) {
|
|
17416
|
-
if (label2 === null || label2 === void 0) return false;
|
|
17417
|
-
if (typeof label2 !== "string") return false;
|
|
17418
|
-
const trimmed = label2.trim();
|
|
17419
|
-
if (trimmed.length === 0) return true;
|
|
17420
|
-
const metaPatterns = [
|
|
17421
|
-
/^verity\s+[\w-]+\s+response$/i,
|
|
17422
|
-
// "Verity reflect response"
|
|
17423
|
-
/^simple user response$/i,
|
|
17424
|
-
/^verity\s+command$/i,
|
|
17425
|
-
// "Verity command"
|
|
17426
|
-
/^user\s+(question|reply|response|ack)$/i
|
|
17427
|
-
];
|
|
17428
|
-
return metaPatterns.some((p) => p.test(trimmed));
|
|
17429
|
-
}
|
|
17430
|
-
|
|
17431
17873
|
// src/lib/transcript.ts
|
|
17432
17874
|
var import_node_fs22 = require("node:fs");
|
|
17433
17875
|
var MAX_READ_BYTES = 256 * 1024;
|
|
@@ -17441,9 +17883,11 @@ var MAX_SUMMARY_BYTES = 4096;
|
|
|
17441
17883
|
var HOME = process.env.HOME ?? "";
|
|
17442
17884
|
async function extractActionSummary(transcriptPath) {
|
|
17443
17885
|
try {
|
|
17444
|
-
const
|
|
17445
|
-
if (!
|
|
17446
|
-
|
|
17886
|
+
const read = readTurnLines(transcriptPath);
|
|
17887
|
+
if (!read || read.lines.length === 0) return null;
|
|
17888
|
+
const summary = buildSummary(read.lines);
|
|
17889
|
+
if (summary) summary.transcript_windowed = read.window;
|
|
17890
|
+
return summary;
|
|
17447
17891
|
} catch {
|
|
17448
17892
|
return null;
|
|
17449
17893
|
}
|
|
@@ -17457,9 +17901,11 @@ function readTurnLines(transcriptPath) {
|
|
|
17457
17901
|
}
|
|
17458
17902
|
if (size === 0) return null;
|
|
17459
17903
|
let raw;
|
|
17904
|
+
let windowed = false;
|
|
17460
17905
|
if (size <= SMALL_FILE_BYTES) {
|
|
17461
17906
|
raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
|
|
17462
17907
|
} else {
|
|
17908
|
+
windowed = true;
|
|
17463
17909
|
const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
|
|
17464
17910
|
const fd = require("node:fs").openSync(transcriptPath, "r");
|
|
17465
17911
|
try {
|
|
@@ -17477,17 +17923,22 @@ function readTurnLines(transcriptPath) {
|
|
|
17477
17923
|
const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
17478
17924
|
if (allLines.length === 0) return null;
|
|
17479
17925
|
let turnStart = 0;
|
|
17926
|
+
let boundaryFound = false;
|
|
17480
17927
|
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
17481
17928
|
try {
|
|
17482
17929
|
const parsed = JSON.parse(allLines[i]);
|
|
17483
17930
|
if (parsed.type === "user" && isRealUserMessage(parsed)) {
|
|
17484
17931
|
turnStart = i;
|
|
17932
|
+
boundaryFound = true;
|
|
17485
17933
|
break;
|
|
17486
17934
|
}
|
|
17487
17935
|
} catch {
|
|
17488
17936
|
}
|
|
17489
17937
|
}
|
|
17490
|
-
return
|
|
17938
|
+
return {
|
|
17939
|
+
lines: allLines.slice(turnStart),
|
|
17940
|
+
window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
|
|
17941
|
+
};
|
|
17491
17942
|
}
|
|
17492
17943
|
function isRealUserMessage(parsed) {
|
|
17493
17944
|
const message = parsed.message;
|
|
@@ -17591,6 +18042,13 @@ function buildSummary(lines) {
|
|
|
17591
18042
|
files_read: capArray(filesRead, MAX_FILES_LIST),
|
|
17592
18043
|
files_edited: capArray(filesEdited, MAX_FILES_LIST),
|
|
17593
18044
|
files_created: capArray(filesCreated, MAX_CREATED_LIST),
|
|
18045
|
+
// The complement of the two caps that affect SCOPE. `files_read` is excluded
|
|
18046
|
+
// deliberately: reading a file is not authoring it, so a capped read list
|
|
18047
|
+
// narrows nothing.
|
|
18048
|
+
capped_out: [
|
|
18049
|
+
...cappedOut(filesEdited, MAX_FILES_LIST),
|
|
18050
|
+
...cappedOut(filesCreated, MAX_CREATED_LIST)
|
|
18051
|
+
],
|
|
17594
18052
|
searches,
|
|
17595
18053
|
commands,
|
|
17596
18054
|
subagents,
|
|
@@ -17641,6 +18099,9 @@ function sanitizeCommand(rawCmd) {
|
|
|
17641
18099
|
function capArray(set, max) {
|
|
17642
18100
|
return Array.from(set).slice(0, max);
|
|
17643
18101
|
}
|
|
18102
|
+
function cappedOut(set, max) {
|
|
18103
|
+
return Array.from(set).slice(max);
|
|
18104
|
+
}
|
|
17644
18105
|
|
|
17645
18106
|
// src/lib/run-mode.ts
|
|
17646
18107
|
function parseAutonomousEnv(raw) {
|
|
@@ -18035,11 +18496,12 @@ async function readStopHookStdin() {
|
|
|
18035
18496
|
return empty;
|
|
18036
18497
|
}
|
|
18037
18498
|
}
|
|
18038
|
-
function agentContextFor(response, intentRepeat = 0) {
|
|
18499
|
+
function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
|
|
18039
18500
|
const metadata = response.metadata ?? {};
|
|
18040
18501
|
const intent = response.intent_alignment ?? {};
|
|
18041
18502
|
return buildAgentContext({
|
|
18042
18503
|
intentRepeat,
|
|
18504
|
+
priorPendingFingerprints,
|
|
18043
18505
|
gateDecision: String(response.gate_decision ?? ""),
|
|
18044
18506
|
findings: response.findings ?? [],
|
|
18045
18507
|
pendingItems: response.pending_items ?? [],
|
|
@@ -18050,12 +18512,45 @@ function agentContextFor(response, intentRepeat = 0) {
|
|
|
18050
18512
|
});
|
|
18051
18513
|
}
|
|
18052
18514
|
var beaconCtx = null;
|
|
18053
|
-
async function passAndExit(reason, skip) {
|
|
18515
|
+
async function passAndExit(reason, skip, kindOverride) {
|
|
18054
18516
|
const sent = await sendSkipBeacon(beaconCtx, skip);
|
|
18055
18517
|
logEvent("skip", { reason: skip, beacon: sent });
|
|
18056
|
-
|
|
18518
|
+
const POLICY_SKIPS = /* @__PURE__ */ new Set([
|
|
18519
|
+
"no-analyzable-files",
|
|
18520
|
+
"verity-command",
|
|
18521
|
+
"bare-acknowledgment",
|
|
18522
|
+
"reflection-prompt",
|
|
18523
|
+
"skip-mode",
|
|
18524
|
+
"zero-increment",
|
|
18525
|
+
"debounce",
|
|
18526
|
+
"no-delta-since-last-review"
|
|
18527
|
+
]);
|
|
18528
|
+
const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
|
|
18529
|
+
const changed = skipCoverageChanged;
|
|
18530
|
+
const { coverage, unaccounted } = reconcileCoverage(changed, {
|
|
18531
|
+
reviewed: [],
|
|
18532
|
+
notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
|
|
18533
|
+
});
|
|
18534
|
+
const verdict = resolveVerdict("PASS", coverage);
|
|
18535
|
+
const note = describeCoverage(coverage);
|
|
18536
|
+
if (unaccounted.length > 0) {
|
|
18537
|
+
logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
|
|
18538
|
+
}
|
|
18539
|
+
const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
|
|
18540
|
+
const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
|
|
18541
|
+
printJsonCompact(
|
|
18542
|
+
buildHookOutput(
|
|
18543
|
+
verdict,
|
|
18544
|
+
`Verity: ${reason}`,
|
|
18545
|
+
// The agent's ONLY input is additionalContext. Sixteen of the nineteen
|
|
18546
|
+
// terminating paths wrote `systemMessage` — the human's field — and told
|
|
18547
|
+
// the agent nothing at all.
|
|
18548
|
+
agentNote
|
|
18549
|
+
)
|
|
18550
|
+
);
|
|
18057
18551
|
process.exit(0);
|
|
18058
18552
|
}
|
|
18553
|
+
var skipCoverageChanged = [];
|
|
18059
18554
|
var EMPTY_STATIC = {
|
|
18060
18555
|
tool: "@codacy/analysis-cli",
|
|
18061
18556
|
findings: [],
|
|
@@ -18070,7 +18565,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
|
|
|
18070
18565
|
}
|
|
18071
18566
|
function localOnlyAndExit(staticResults) {
|
|
18072
18567
|
printJsonCompact({
|
|
18073
|
-
gate_decision: "
|
|
18568
|
+
gate_decision: "WARN",
|
|
18074
18569
|
systemMessage: "Verity: not authenticated \u2014 ran a local static-only check (no deep review, no upload). Run `verity init` to authenticate and enable the full quality gate.",
|
|
18075
18570
|
unauthenticated: true,
|
|
18076
18571
|
static_results: staticResults
|
|
@@ -18130,6 +18625,7 @@ async function runAnalyze(opts, globals) {
|
|
|
18130
18625
|
});
|
|
18131
18626
|
}
|
|
18132
18627
|
const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
|
|
18628
|
+
skipCoverageChanged = allChanged;
|
|
18133
18629
|
const analyzable = filterAnalyzable(allChanged);
|
|
18134
18630
|
const reviewable = filterReviewable(allChanged);
|
|
18135
18631
|
const securityFiles = filterSecurity(allChanged);
|
|
@@ -18141,15 +18637,27 @@ async function runAnalyze(opts, globals) {
|
|
|
18141
18637
|
const conversation = await readAndClearConversationBuffer(baselineSessionId);
|
|
18142
18638
|
const specs = discoverSpecs();
|
|
18143
18639
|
const plans = discoverPlans();
|
|
18640
|
+
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
18641
|
+
const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
|
|
18642
|
+
const authorshipIsObservable = !!actionSummary && actionSummary.transcript_windowed !== "orphaned" || !!baseline;
|
|
18643
|
+
const canSeeTurnAuthorship = authorshipIsObservable;
|
|
18644
|
+
let earlyFold = null;
|
|
18144
18645
|
const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
|
|
18145
18646
|
if (/^\s*\/verity-/i.test(latestPrompt)) {
|
|
18647
|
+
const setupAuthored = [
|
|
18648
|
+
...actionSummary?.files_edited ?? [],
|
|
18649
|
+
...actionSummary?.files_created ?? []
|
|
18650
|
+
];
|
|
18651
|
+
if (setupAuthored.length > 0) {
|
|
18652
|
+
const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
|
|
18653
|
+
logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
|
|
18654
|
+
}
|
|
18146
18655
|
await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
|
|
18147
18656
|
}
|
|
18148
|
-
if (
|
|
18657
|
+
if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
|
|
18149
18658
|
await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
|
|
18150
18659
|
}
|
|
18151
|
-
|
|
18152
|
-
if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
|
|
18660
|
+
if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
|
|
18153
18661
|
await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
|
|
18154
18662
|
}
|
|
18155
18663
|
const urlResult = await resolveServiceUrl(globals.serviceUrl);
|
|
@@ -18195,7 +18703,11 @@ async function runAnalyze(opts, globals) {
|
|
|
18195
18703
|
);
|
|
18196
18704
|
}
|
|
18197
18705
|
if (analysisMode === "skip") {
|
|
18198
|
-
await passAndExit(
|
|
18706
|
+
await passAndExit(
|
|
18707
|
+
"Skip mode \u2014 no code work to analyze",
|
|
18708
|
+
"skip-mode",
|
|
18709
|
+
turnAuthoredCode ? "capacity" : void 0
|
|
18710
|
+
);
|
|
18199
18711
|
}
|
|
18200
18712
|
let staticResults = {
|
|
18201
18713
|
tool: "@codacy/analysis-cli",
|
|
@@ -18205,7 +18717,8 @@ async function runAnalyze(opts, globals) {
|
|
|
18205
18717
|
let codeDelta = {
|
|
18206
18718
|
files: [],
|
|
18207
18719
|
total_lines: 0,
|
|
18208
|
-
total_files: 0
|
|
18720
|
+
total_files: 0,
|
|
18721
|
+
excluded: []
|
|
18209
18722
|
};
|
|
18210
18723
|
let snapshotResult = { has_snapshots: false, diffs: [] };
|
|
18211
18724
|
let contentHash = null;
|
|
@@ -18246,10 +18759,43 @@ async function runAnalyze(opts, globals) {
|
|
|
18246
18759
|
contentHash = hashResult.hash;
|
|
18247
18760
|
if (analysisMode !== "plan") {
|
|
18248
18761
|
const scoped = scopeToAuthored(allForReview, actionSummary);
|
|
18249
|
-
|
|
18762
|
+
const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
|
|
18763
|
+
if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
|
|
18250
18764
|
await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
|
|
18251
18765
|
}
|
|
18252
|
-
|
|
18766
|
+
if (scoped.signal === "none-authored" && !authorshipIsObservable) {
|
|
18767
|
+
logEvent("none_authored_unverifiable", {
|
|
18768
|
+
reason: "orphaned_window_no_baseline",
|
|
18769
|
+
would_have_skipped: allForReview.length
|
|
18770
|
+
});
|
|
18771
|
+
}
|
|
18772
|
+
const narrowingIsTrustworthy = scoped.signal === "authored" && scoped.files.length > 0 && actionSummary?.transcript_windowed !== "orphaned";
|
|
18773
|
+
let recoveredScope = [];
|
|
18774
|
+
if (!narrowingIsTrustworthy && transcriptPath) {
|
|
18775
|
+
try {
|
|
18776
|
+
earlyFold = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18777
|
+
const authoredWhole = new Set(earlyFold.authored.map((a) => a.p));
|
|
18778
|
+
if (authoredWhole.size > 0) {
|
|
18779
|
+
const root = repoRoot();
|
|
18780
|
+
recoveredScope = allForReview.filter((f) => authoredWhole.has(toRepoRelative(f, root)));
|
|
18781
|
+
}
|
|
18782
|
+
logEvent("scope_recovered_from_fold", {
|
|
18783
|
+
window_saw: scoped.files.length,
|
|
18784
|
+
fold_saw: authoredWhole.size,
|
|
18785
|
+
recovered: recoveredScope.length,
|
|
18786
|
+
would_have_widened_to: allForReview.length
|
|
18787
|
+
});
|
|
18788
|
+
} catch {
|
|
18789
|
+
earlyFold = null;
|
|
18790
|
+
}
|
|
18791
|
+
}
|
|
18792
|
+
if (!narrowingIsTrustworthy && scoped.signal === "authored" && recoveredScope.length === 0) {
|
|
18793
|
+
logEvent("scope_widened_orphaned_window", {
|
|
18794
|
+
would_have_sent: scoped.files.length,
|
|
18795
|
+
widened_to: allForReview.length
|
|
18796
|
+
});
|
|
18797
|
+
}
|
|
18798
|
+
const baseForReview = narrowingIsTrustworthy ? scoped.files : recoveredScope.length > 0 ? recoveredScope : allForReview;
|
|
18253
18799
|
const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
|
|
18254
18800
|
if (!opts.skipStatic && isCodacyAvailable()) {
|
|
18255
18801
|
let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
|
|
@@ -18270,7 +18816,11 @@ async function runAnalyze(opts, globals) {
|
|
|
18270
18816
|
if (assistantResponse) {
|
|
18271
18817
|
analysisMode = "plan";
|
|
18272
18818
|
} else {
|
|
18273
|
-
await passAndExit(
|
|
18819
|
+
await passAndExit(
|
|
18820
|
+
"No files within size limits to analyze",
|
|
18821
|
+
"size-limit",
|
|
18822
|
+
codeDelta.excluded.length > 0 ? "capacity" : "policy"
|
|
18823
|
+
);
|
|
18274
18824
|
}
|
|
18275
18825
|
}
|
|
18276
18826
|
}
|
|
@@ -18283,19 +18833,13 @@ async function runAnalyze(opts, globals) {
|
|
|
18283
18833
|
snapshotResult = generateSnapshotDiffs(codeDelta.files);
|
|
18284
18834
|
}
|
|
18285
18835
|
currentCommit = getCurrentCommit();
|
|
18286
|
-
|
|
18287
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
|
|
18288
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18289
|
-
iteration = iterResult.iteration;
|
|
18836
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18290
18837
|
}
|
|
18291
18838
|
}
|
|
18292
18839
|
if (analysisMode === "plan") {
|
|
18293
18840
|
recordAnalysisStart();
|
|
18294
18841
|
currentCommit = getCurrentCommit();
|
|
18295
|
-
|
|
18296
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations);
|
|
18297
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18298
|
-
iteration = iterResult.iteration;
|
|
18842
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18299
18843
|
}
|
|
18300
18844
|
const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
|
|
18301
18845
|
for (const f of codeDelta.files) {
|
|
@@ -18362,7 +18906,7 @@ async function runAnalyze(opts, globals) {
|
|
|
18362
18906
|
let foldConservation = null;
|
|
18363
18907
|
if (transcriptPath) {
|
|
18364
18908
|
try {
|
|
18365
|
-
foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18909
|
+
foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
|
|
18366
18910
|
foldConservation = checkConservation(allForReview, foldResult, repoRoot());
|
|
18367
18911
|
if (!foldConservation.holds) {
|
|
18368
18912
|
process.stderr.write(
|
|
@@ -18444,7 +18988,10 @@ async function runAnalyze(opts, globals) {
|
|
|
18444
18988
|
priorState.capabilities
|
|
18445
18989
|
);
|
|
18446
18990
|
memory = recallMemory(memorySession.d, memorySession.identity, {
|
|
18447
|
-
currentSessionKey: memorySession.identity.sessionKey
|
|
18991
|
+
currentSessionKey: memorySession.identity.sessionKey,
|
|
18992
|
+
// The independent witness. Only meaningful when a transcript was folded —
|
|
18993
|
+
// otherwise it stays undefined and capture coverage reads as UNKNOWN.
|
|
18994
|
+
...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
|
|
18448
18995
|
});
|
|
18449
18996
|
if (memory && !memory.provenanceHolds) {
|
|
18450
18997
|
process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
|
|
@@ -18465,7 +19012,30 @@ async function runAnalyze(opts, globals) {
|
|
|
18465
19012
|
hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
18466
19013
|
isTTY: process.stdout.isTTY === true
|
|
18467
19014
|
});
|
|
19015
|
+
const excludedByReason = {};
|
|
19016
|
+
for (const e of codeDelta.excluded ?? []) {
|
|
19017
|
+
excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
19018
|
+
}
|
|
19019
|
+
const coverageTelemetry = {
|
|
19020
|
+
// git's whole answer, before ANY narrowing. The number that has never been sent.
|
|
19021
|
+
changed_all: allChanged.length,
|
|
19022
|
+
analyzable: analyzable.length,
|
|
19023
|
+
reviewable: reviewable.length,
|
|
19024
|
+
security: securityFiles.length,
|
|
19025
|
+
// after the allowlist, before authorship scoping and the caps
|
|
19026
|
+
for_review: allForReview.length,
|
|
19027
|
+
// what actually reaches the reviewer
|
|
19028
|
+
sent: codeDelta.files.length,
|
|
19029
|
+
// the two silent narrowings, counted separately so they can be told apart
|
|
19030
|
+
capped_out: actionSummary?.capped_out?.length ?? 0,
|
|
19031
|
+
excluded: (codeDelta.excluded ?? []).length,
|
|
19032
|
+
excluded_by_reason: excludedByReason,
|
|
19033
|
+
// was the transcript itself truncated? The 256 KB window means "this turn"
|
|
19034
|
+
// can quietly mean "the last 256 KB of it".
|
|
19035
|
+
transcript_windowed: actionSummary?.transcript_windowed ?? null
|
|
19036
|
+
};
|
|
18468
19037
|
const requestBody = {
|
|
19038
|
+
coverage_telemetry: coverageTelemetry,
|
|
18469
19039
|
static_results: staticResults,
|
|
18470
19040
|
code_delta: codeDelta,
|
|
18471
19041
|
changed_files: allForReview,
|
|
@@ -18551,6 +19121,54 @@ async function runAnalyze(opts, globals) {
|
|
|
18551
19121
|
// replaced a population floor with ≈35% power that was sub-integer for
|
|
18552
19122
|
// three-quarters of the fleet.
|
|
18553
19123
|
conservation: foldConservation,
|
|
19124
|
+
// ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
|
|
19125
|
+
//
|
|
19126
|
+
// The whole "task-scoped delta" design space rests on an assumption that
|
|
19127
|
+
// has been observed exactly ONCE: that delta files routinely belong to
|
|
19128
|
+
// earlier work. Three designs were built on it and all three were killed
|
|
19129
|
+
// adversarially — two by measurement — so before another is attempted,
|
|
19130
|
+
// measure the base rate.
|
|
19131
|
+
//
|
|
19132
|
+
// `authored_under_earlier_goal` counts delta paths whose LAST authorship
|
|
19133
|
+
// event precedes the seq of the goal now in force. Both numbers come from
|
|
19134
|
+
// the same append-only counter (`nextSeq`), so the comparison is exact.
|
|
19135
|
+
//
|
|
19136
|
+
// Keyed on the GOAL, deliberately, not on the task id. The task classifier
|
|
19137
|
+
// reported `is_new_task` on two consecutive turns of one task 25 seconds
|
|
19138
|
+
// apart, so a task-keyed number would measure its unreliability rather
|
|
19139
|
+
// than the phenomenon. And this only became meaningful once `recordGoal`
|
|
19140
|
+
// stopped letting a bare "ok" supersede the goal — before that the seq
|
|
19141
|
+
// advanced every turn and this would have degenerated to "not edited this
|
|
19142
|
+
// turn", which is the exact mistake that sank one of the three designs.
|
|
19143
|
+
//
|
|
19144
|
+
// Changes no payload the reviewer sees, no narrowing, no verdict.
|
|
19145
|
+
vrt52: (() => {
|
|
19146
|
+
const goalSeq = memory?.projection.goal?.seq;
|
|
19147
|
+
if (goalSeq === void 0 || !memorySession) return { known: false };
|
|
19148
|
+
const lastSeq2 = new Map(
|
|
19149
|
+
foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
|
|
19150
|
+
);
|
|
19151
|
+
let earlier = 0;
|
|
19152
|
+
let unknown = 0;
|
|
19153
|
+
for (const f of codeDelta.files) {
|
|
19154
|
+
const seen = lastSeq2.get(f.path);
|
|
19155
|
+
if (seen === void 0) unknown++;
|
|
19156
|
+
else if (seen < goalSeq) earlier++;
|
|
19157
|
+
}
|
|
19158
|
+
return {
|
|
19159
|
+
known: true,
|
|
19160
|
+
goal_seq: goalSeq,
|
|
19161
|
+
delta: codeDelta.files.length,
|
|
19162
|
+
// Files this delta carries that were last written under an EARLIER
|
|
19163
|
+
// instruction. If this stays near zero, VRT-52's code half is
|
|
19164
|
+
// unnecessary and should be closed saying so.
|
|
19165
|
+
authored_under_earlier_goal: earlier,
|
|
19166
|
+
// Delta files the dossier has no authorship record for at all —
|
|
19167
|
+
// pre-existing tree state, or an authorship channel the fold cannot
|
|
19168
|
+
// see. Reported separately so a blind spot is never counted as a zero.
|
|
19169
|
+
no_authorship_record: unknown
|
|
19170
|
+
};
|
|
19171
|
+
})(),
|
|
18554
19172
|
// P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
|
|
18555
19173
|
// narrowed to the within-session increment: what changed since the last
|
|
18556
19174
|
// VERDICT rather than since task start.
|
|
@@ -18583,7 +19201,20 @@ async function runAnalyze(opts, globals) {
|
|
|
18583
19201
|
const intentContext = {};
|
|
18584
19202
|
if (conversation && conversation.prompts.length > 0) {
|
|
18585
19203
|
const latest = conversation.prompts[conversation.prompts.length - 1];
|
|
18586
|
-
|
|
19204
|
+
const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
|
|
19205
|
+
intentContext.user_prompt = goalPrompt.entry.prompt;
|
|
19206
|
+
if (isContinuationPrompt(intentContext.user_prompt)) {
|
|
19207
|
+
const carried = memory?.projection.goal?.text;
|
|
19208
|
+
if (carried && !isContinuationPrompt(carried)) {
|
|
19209
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19210
|
+
intentContext.user_prompt = carried;
|
|
19211
|
+
logEvent("goal_from_dossier", { chars: carried.length });
|
|
19212
|
+
}
|
|
19213
|
+
}
|
|
19214
|
+
if (goalPrompt.turnsBack > 0) {
|
|
19215
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19216
|
+
logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
|
|
19217
|
+
}
|
|
18587
19218
|
intentContext.session_id = latest.session_id || void 0;
|
|
18588
19219
|
intentContext.prompt_captured_at = latest.captured_at || void 0;
|
|
18589
19220
|
if (conversation.prompts.length > 1) {
|
|
@@ -18675,9 +19306,122 @@ async function runAnalyze(opts, globals) {
|
|
|
18675
19306
|
const response = result.data;
|
|
18676
19307
|
const decision = response.gate_decision ?? "(unrecognised)";
|
|
18677
19308
|
const sentPaths = codeDelta.files.map((f) => f.path);
|
|
19309
|
+
let openElsewhere = [];
|
|
19310
|
+
if (memorySession) {
|
|
19311
|
+
try {
|
|
19312
|
+
const st = foldDossier(memorySession.d);
|
|
19313
|
+
openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
|
|
19314
|
+
try {
|
|
19315
|
+
const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
|
|
19316
|
+
const at = src[line - 1];
|
|
19317
|
+
return at === void 0 ? null : lineSha(at);
|
|
19318
|
+
} catch {
|
|
19319
|
+
return null;
|
|
19320
|
+
}
|
|
19321
|
+
});
|
|
19322
|
+
} catch {
|
|
19323
|
+
}
|
|
19324
|
+
}
|
|
19325
|
+
const reviewCoverage = {
|
|
19326
|
+
reviewed: sentPaths,
|
|
19327
|
+
// Declared drops from the stages that DO report themselves today. The other
|
|
19328
|
+
// stages surface via `unaccounted`, which is the tripwire, not the design.
|
|
19329
|
+
notReviewed: [
|
|
19330
|
+
// Every exit from the collection loop, each named. Six reasons where there
|
|
19331
|
+
// used to be two recorded and four silent — the silent ones including the
|
|
19332
|
+
// per-file size cap, which could drop a whole source file without leaving a
|
|
19333
|
+
// trace anywhere in the payload or the run row.
|
|
19334
|
+
...codeDelta.excluded,
|
|
19335
|
+
// The server-side 300-line middle-out truncation. It only bites on the
|
|
19336
|
+
// full-file branch (a first analysis, before snapshots exist) because
|
|
19337
|
+
// analyze normally sends diffs — but on that branch the reviewer sees the
|
|
19338
|
+
// first and last 100 lines and nothing between, and until now said so to
|
|
19339
|
+
// nobody. CAPACITY: a partial look is not a look.
|
|
19340
|
+
...(response.metadata?.truncated_files ?? []).map((path) => ({
|
|
19341
|
+
path,
|
|
19342
|
+
reason: "file-middle-truncated-300-lines",
|
|
19343
|
+
stage: "prompt-builder",
|
|
19344
|
+
kind: "capacity"
|
|
19345
|
+
})),
|
|
19346
|
+
// The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
|
|
19347
|
+
// what is REVIEWED, not merely what is summarised — a session editing 25
|
|
19348
|
+
// files had five silently excluded from the reviewed set.
|
|
19349
|
+
...(actionSummary?.capped_out ?? []).map((path) => ({
|
|
19350
|
+
path,
|
|
19351
|
+
reason: "edit-list-cap-20",
|
|
19352
|
+
stage: "extractActionSummary",
|
|
19353
|
+
kind: "capacity"
|
|
19354
|
+
})),
|
|
19355
|
+
// ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
|
|
19356
|
+
//
|
|
19357
|
+
// The universe is `allChanged`, git's whole dirty tree. The reviewed set is
|
|
19358
|
+
// scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
|
|
19359
|
+
// every pre-existing dirty file is in the universe, absent from `reviewed`,
|
|
19360
|
+
// and — until now — declared by nobody. It fell through to `unaccounted`,
|
|
19361
|
+
// became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
|
|
19362
|
+
// that was never this session's to review.
|
|
19363
|
+
//
|
|
19364
|
+
// Measured 2026-08-04: three consecutive runs over an untouched tree gave
|
|
19365
|
+
// three different answers — .claude/settings.json, then admin.js, then six
|
|
19366
|
+
// files — because each run took a different path and each path had a
|
|
19367
|
+
// different idea of the universe. POLICY: not this session's work is not a
|
|
19368
|
+
// coverage gap, it is the cure working.
|
|
19369
|
+
...allChanged.filter((p) => !sentPaths.includes(p) && !codeDelta.excluded.some((e) => e.path === p)).filter((p) => analyzable.includes(p) || reviewable.includes(p) || securityFiles.includes(p)).map((path) => ({
|
|
19370
|
+
path,
|
|
19371
|
+
reason: "not-authored-this-session",
|
|
19372
|
+
stage: "baseline-scoping",
|
|
19373
|
+
kind: "policy"
|
|
19374
|
+
})),
|
|
19375
|
+
// The extension allowlist, and it is POLICY rather than capacity: a changed
|
|
19376
|
+
// README was never going to be reviewed, and treating that as a coverage
|
|
19377
|
+
// gap would downgrade nearly every PASS to WARN until WARN meant nothing.
|
|
19378
|
+
// Recorded so the ledger balances and so "what did Verity ignore entirely"
|
|
19379
|
+
// is answerable — but it never touches the verdict.
|
|
19380
|
+
...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
|
|
19381
|
+
path,
|
|
19382
|
+
reason: "not-a-reviewed-file-type",
|
|
19383
|
+
stage: "extension-allowlist",
|
|
19384
|
+
kind: "policy"
|
|
19385
|
+
}))
|
|
19386
|
+
]
|
|
19387
|
+
};
|
|
18678
19388
|
const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
|
|
18679
19389
|
const watermarkIsPartial = !!codeDelta.truncated;
|
|
19390
|
+
let silenced = null;
|
|
19391
|
+
let turnIsIdleForChannel = true;
|
|
19392
|
+
if (memorySession) {
|
|
19393
|
+
try {
|
|
19394
|
+
const st = foldDossier(memorySession.d);
|
|
19395
|
+
turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
|
|
19396
|
+
silenced = channelSilence({
|
|
19397
|
+
// The BUFFER, not intentContext.user_prompt: the latter falls back to a
|
|
19398
|
+
// linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
|
|
19399
|
+
// not a user utterance. Treating it as one would keep the loop alive on
|
|
19400
|
+
// exactly the autonomous cohort.
|
|
19401
|
+
newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
19402
|
+
newAuthorship: !turnIsIdleForChannel,
|
|
19403
|
+
emittedLast: st.meta.channel?.emittedLast === true,
|
|
19404
|
+
consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
|
|
19405
|
+
});
|
|
19406
|
+
} catch {
|
|
19407
|
+
silenced = null;
|
|
19408
|
+
}
|
|
19409
|
+
}
|
|
19410
|
+
if (silenced) {
|
|
19411
|
+
logEvent("channel_silenced", {
|
|
19412
|
+
reason: silenced,
|
|
19413
|
+
run_id: response.run_id ?? turnId,
|
|
19414
|
+
decision
|
|
19415
|
+
});
|
|
19416
|
+
}
|
|
18680
19417
|
let intentRepeatCount = 0;
|
|
19418
|
+
const priorPendingFingerprints = memorySession ? (() => {
|
|
19419
|
+
try {
|
|
19420
|
+
return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
|
|
19421
|
+
} catch {
|
|
19422
|
+
return [];
|
|
19423
|
+
}
|
|
19424
|
+
})() : [];
|
|
18681
19425
|
if (memorySession) {
|
|
18682
19426
|
try {
|
|
18683
19427
|
recordVerdict(memorySession.d, {
|
|
@@ -18696,7 +19440,14 @@ async function runAnalyze(opts, globals) {
|
|
|
18696
19440
|
// The same signal F1 introduced: bytes differing from the hash frozen at
|
|
18697
19441
|
// the last verdict. A turn that moved nothing is the only kind that can
|
|
18698
19442
|
// accumulate a repeat.
|
|
18699
|
-
idle:
|
|
19443
|
+
idle: turnIsIdleForChannel,
|
|
19444
|
+
// What next turn reads as `emittedLast`. A suppressed turn did not
|
|
19445
|
+
// speak, so it cannot be the cause of the turn after it — which is what
|
|
19446
|
+
// keeps this from becoming a permanent gag.
|
|
19447
|
+
emitted: !silenced,
|
|
19448
|
+
// Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
|
|
19449
|
+
// carry no `pattern_id`, so their content is the only available key.
|
|
19450
|
+
pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
|
|
18700
19451
|
});
|
|
18701
19452
|
intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
|
|
18702
19453
|
} catch {
|
|
@@ -18796,9 +19547,41 @@ async function runAnalyze(opts, globals) {
|
|
|
18796
19547
|
reverify_by: response.reverify_by
|
|
18797
19548
|
});
|
|
18798
19549
|
const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
|
|
18799
|
-
|
|
19550
|
+
let capReleased = false;
|
|
19551
|
+
let effectiveDecision = decision;
|
|
19552
|
+
if (decision === "FAIL") {
|
|
19553
|
+
const blocking = (response.findings ?? []).filter((f) => {
|
|
19554
|
+
const sev = String(f.severity ?? "").toLowerCase();
|
|
19555
|
+
return sev === "critical" || sev === "high";
|
|
19556
|
+
});
|
|
19557
|
+
const fingerprint = findingsFingerprint(blocking);
|
|
19558
|
+
const prior = readIterationState(currentCommit);
|
|
19559
|
+
const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
|
|
19560
|
+
const nextIteration = sameProblem ? prior.iteration + 1 : 1;
|
|
19561
|
+
const maxIterations = parseInt(opts.maxIterations, 10);
|
|
19562
|
+
writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
|
|
19563
|
+
iteration = nextIteration;
|
|
19564
|
+
if (nextIteration > maxIterations) {
|
|
19565
|
+
capReleased = true;
|
|
19566
|
+
effectiveDecision = "WARN";
|
|
19567
|
+
logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
|
|
19568
|
+
}
|
|
19569
|
+
}
|
|
19570
|
+
if (capReleased) {
|
|
19571
|
+
const findings = response.findings ?? [];
|
|
19572
|
+
const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
|
|
19573
|
+
emitVerdict({
|
|
19574
|
+
proposed: "WARN",
|
|
19575
|
+
changed: skipCoverageChanged,
|
|
19576
|
+
coverage: reviewCoverage,
|
|
19577
|
+
userSummary: `Verity: WARN \u2014 self-healing limit (${opts.maxIterations}) reached on the same finding. NO LONGER BLOCKING, but ${findings.length} finding(s) remain OPEN and were NOT fixed. Human review required before deploying.
|
|
19578
|
+
${lines.join("\n")}`,
|
|
19579
|
+
agentContext: null,
|
|
19580
|
+
silenced: true
|
|
19581
|
+
});
|
|
19582
|
+
}
|
|
19583
|
+
switch (effectiveDecision) {
|
|
18800
19584
|
case "FAIL": {
|
|
18801
|
-
writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
|
|
18802
19585
|
const assessment = response.assessment;
|
|
18803
19586
|
const narrative = assessment?.narrative ?? "";
|
|
18804
19587
|
const findings = response.findings ?? [];
|
|
@@ -18875,7 +19658,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
|
|
|
18875
19658
|
if (grantNudge) process.stderr.write(`
|
|
18876
19659
|
${YELLOW}${grantNudge.trim()}${NC}
|
|
18877
19660
|
`);
|
|
18878
|
-
|
|
19661
|
+
emitVerdict({
|
|
19662
|
+
proposed: "FAIL",
|
|
19663
|
+
changed: skipCoverageChanged,
|
|
19664
|
+
coverage: reviewCoverage,
|
|
19665
|
+
userSummary: "",
|
|
19666
|
+
// Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
|
|
19667
|
+
// the findings themselves are rendered above by the blocking renderer,
|
|
19668
|
+
// so what the cut removes is the repeated commentary, never the defect.
|
|
19669
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19670
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19671
|
+
silenced: !!silenced,
|
|
19672
|
+
openElsewhere
|
|
19673
|
+
});
|
|
18879
19674
|
break;
|
|
18880
19675
|
}
|
|
18881
19676
|
case "PASS": {
|
|
@@ -18887,10 +19682,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18887
19682
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18888
19683
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18889
19684
|
userSummary += loginNudge + grantNudge;
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
19685
|
+
emitVerdict({
|
|
19686
|
+
proposed: "PASS",
|
|
19687
|
+
changed: skipCoverageChanged,
|
|
19688
|
+
coverage: reviewCoverage,
|
|
19689
|
+
userSummary,
|
|
19690
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19691
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19692
|
+
silenced: !!silenced,
|
|
19693
|
+
openElsewhere
|
|
19694
|
+
});
|
|
18894
19695
|
break;
|
|
18895
19696
|
}
|
|
18896
19697
|
case "WARN": {
|
|
@@ -18901,10 +19702,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18901
19702
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18902
19703
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18903
19704
|
userSummary += loginNudge + grantNudge;
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
19705
|
+
emitVerdict({
|
|
19706
|
+
proposed: "WARN",
|
|
19707
|
+
changed: skipCoverageChanged,
|
|
19708
|
+
coverage: reviewCoverage,
|
|
19709
|
+
userSummary,
|
|
19710
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19711
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19712
|
+
silenced: !!silenced,
|
|
19713
|
+
openElsewhere
|
|
19714
|
+
});
|
|
18908
19715
|
break;
|
|
18909
19716
|
}
|
|
18910
19717
|
default: {
|
|
@@ -19017,8 +19824,8 @@ async function runReview(opts, globals) {
|
|
|
19017
19824
|
for (const p of specPaths) {
|
|
19018
19825
|
if (!(0, import_node_fs26.existsSync)(p)) continue;
|
|
19019
19826
|
try {
|
|
19020
|
-
const { readFileSync:
|
|
19021
|
-
const content =
|
|
19827
|
+
const { readFileSync: readFileSync16 } = await import("node:fs");
|
|
19828
|
+
const content = readFileSync16(p, "utf-8");
|
|
19022
19829
|
specs.push({ path: p, content: content.slice(0, 10240) });
|
|
19023
19830
|
} catch {
|
|
19024
19831
|
}
|
|
@@ -19821,6 +20628,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19821
20628
|
return "handled";
|
|
19822
20629
|
}
|
|
19823
20630
|
if (!who.ok) {
|
|
20631
|
+
const denial = authDenialRemedy(who.error);
|
|
20632
|
+
if (denial) {
|
|
20633
|
+
console.log("");
|
|
20634
|
+
printWarn(`Your existing Verity credential was rejected (${denial.code}).`);
|
|
20635
|
+
printInfo(` ${denial.remedy}`);
|
|
20636
|
+
return "drive-login";
|
|
20637
|
+
}
|
|
19824
20638
|
if (existing.data.userId != null) {
|
|
19825
20639
|
printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
|
|
19826
20640
|
} else {
|
|
@@ -19836,15 +20650,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19836
20650
|
return "drive-login";
|
|
19837
20651
|
}
|
|
19838
20652
|
async function runOptionalAuth(resolution, opts = {}) {
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
}
|
|
20653
|
+
if (resolution.source === "default") {
|
|
20654
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
20655
|
+
}
|
|
20656
|
+
const heal = await maybeHealServiceUrl(resolution, opts.verbose);
|
|
20657
|
+
const serviceUrl = heal.serviceUrl;
|
|
20658
|
+
const healed = heal.healed;
|
|
20659
|
+
if (healed) {
|
|
20660
|
+
printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
|
|
19848
20661
|
}
|
|
19849
20662
|
let remote = "";
|
|
19850
20663
|
try {
|
|
@@ -19862,8 +20675,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19862
20675
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
19863
20676
|
console.log("");
|
|
19864
20677
|
console.log(" Signing in is optional. What it does:");
|
|
19865
|
-
console.log(" - Confirms you
|
|
19866
|
-
console.log("
|
|
20678
|
+
console.log(" - Confirms which repositories you can write to. The GitHub token is");
|
|
20679
|
+
console.log(" used once for that check, then discarded \u2014 Verity never stores it.");
|
|
20680
|
+
console.log(" - One login covers every repository you can write to \u2014 other repos");
|
|
20681
|
+
console.log(" need no further sign-in on this machine.");
|
|
19867
20682
|
console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
|
|
19868
20683
|
console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
|
|
19869
20684
|
console.log(" - It is required to store and access run history for this repo");
|
|
@@ -19878,17 +20693,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19878
20693
|
localOnlyNote();
|
|
19879
20694
|
return;
|
|
19880
20695
|
}
|
|
19881
|
-
if (!remote) {
|
|
19882
|
-
printWarn("No git remote found \u2014 cannot authenticate yet.");
|
|
19883
|
-
localOnlyNote();
|
|
19884
|
-
return;
|
|
19885
|
-
}
|
|
19886
|
-
const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
|
|
19887
20696
|
printInfo("Authenticating with GitHub\u2026");
|
|
19888
|
-
const result = await
|
|
20697
|
+
const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
|
|
19889
20698
|
if (result.ok) {
|
|
19890
|
-
|
|
19891
|
-
printInfo(`Logged in${who ? ` as ${who}` : ""} \u2713 \u2014 runs, history, and cloud memory now sync to Verity.`);
|
|
20699
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: opts.verbose });
|
|
19892
20700
|
} else {
|
|
19893
20701
|
printWarn(`Authentication did not complete: ${result.error}`);
|
|
19894
20702
|
localOnlyNote();
|
|
@@ -20039,8 +20847,8 @@ function registerInitCommand(program2) {
|
|
|
20039
20847
|
console.log("");
|
|
20040
20848
|
try {
|
|
20041
20849
|
const globals = program2.opts();
|
|
20042
|
-
const
|
|
20043
|
-
await runOptionalAuth(
|
|
20850
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
20851
|
+
await runOptionalAuth(resolution, {
|
|
20044
20852
|
token: globals.token,
|
|
20045
20853
|
verbose: globals.verbose
|
|
20046
20854
|
});
|
|
@@ -20656,6 +21464,12 @@ function registerTelemetryCommands(program2) {
|
|
|
20656
21464
|
printError(urlResult.error);
|
|
20657
21465
|
process.exit(1);
|
|
20658
21466
|
}
|
|
21467
|
+
const pin = await checkTokenPin(tokenResult.data.token, urlResult.data);
|
|
21468
|
+
if (!pin.attach && pin.reason === "unpinnable") {
|
|
21469
|
+
printWarn("Your Verity credential does not record which service issued it, so this endpoint");
|
|
21470
|
+
printWarn(" cannot be verified \u2014 a repository could point your telemetry elsewhere.");
|
|
21471
|
+
printWarn(' Run "verity login" to re-issue the credential; Verity can check it after that.');
|
|
21472
|
+
}
|
|
20659
21473
|
const result = await installTelemetry(urlResult.data);
|
|
20660
21474
|
if (!result.ok) {
|
|
20661
21475
|
printError(result.error);
|
|
@@ -20704,7 +21518,8 @@ function registerTelemetryCommands(program2) {
|
|
|
20704
21518
|
}
|
|
20705
21519
|
|
|
20706
21520
|
// src/cli.ts
|
|
20707
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.
|
|
21521
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.644501f").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
|
|
21522
|
+
setUserNamedServiceUrl(program.opts().serviceUrl);
|
|
20708
21523
|
try {
|
|
20709
21524
|
await foldLegacyLocalCredential();
|
|
20710
21525
|
} catch {
|