@codacy/verity-cli 0.28.1-experimental.055cd97 → 0.28.1-experimental.2cbb593
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 +788 -198
- 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
|
});
|
|
@@ -11195,7 +11194,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
|
|
|
11195
11194
|
if (mdUrl) {
|
|
11196
11195
|
return { ok: true, data: { url: mdUrl, source: "verity_md" } };
|
|
11197
11196
|
}
|
|
11198
|
-
return {
|
|
11197
|
+
return {
|
|
11198
|
+
ok: false,
|
|
11199
|
+
error: 'No Verity service URL found. Run "verity login" to get started, or /verity-setup to configure this project.'
|
|
11200
|
+
};
|
|
11201
|
+
}
|
|
11202
|
+
async function resolveServiceUrlForAuth(flagUrl) {
|
|
11203
|
+
const strict = await resolveServiceUrlDetailed(flagUrl);
|
|
11204
|
+
if (strict.ok) return strict.data;
|
|
11205
|
+
return { url: DEFAULT_SERVICE_URL, source: "default" };
|
|
11199
11206
|
}
|
|
11200
11207
|
async function resolveServiceUrl(flagUrl) {
|
|
11201
11208
|
const result = await resolveServiceUrlDetailed(flagUrl);
|
|
@@ -11234,7 +11241,10 @@ async function resolveToken(flagToken) {
|
|
|
11234
11241
|
data: { token: local.token, source: "local", userId: local.userId, email: local.email }
|
|
11235
11242
|
};
|
|
11236
11243
|
}
|
|
11237
|
-
return {
|
|
11244
|
+
return {
|
|
11245
|
+
ok: false,
|
|
11246
|
+
error: 'No Verity token found. Run "verity login" to sign in, or /verity-setup to set up this project.'
|
|
11247
|
+
};
|
|
11238
11248
|
}
|
|
11239
11249
|
async function whoami(token, serviceUrl, verbose) {
|
|
11240
11250
|
return apiRequest({
|
|
@@ -11671,16 +11681,81 @@ function registerAuthCommands(program2) {
|
|
|
11671
11681
|
});
|
|
11672
11682
|
}
|
|
11673
11683
|
|
|
11684
|
+
// src/lib/login-report.ts
|
|
11685
|
+
async function reportLoginOutcome(out, opts = {}) {
|
|
11686
|
+
const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
|
|
11687
|
+
printInfo(`Logged in as ${identity}. \u2713`);
|
|
11688
|
+
printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
|
|
11689
|
+
if (out.expiresAt) {
|
|
11690
|
+
printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
|
|
11691
|
+
printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
|
|
11692
|
+
}
|
|
11693
|
+
if (out.repoCount > 0) {
|
|
11694
|
+
printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
|
|
11695
|
+
}
|
|
11696
|
+
if (out.prunedCredentials > 0) {
|
|
11697
|
+
printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, opts.verbose);
|
|
11698
|
+
} else if (out.prunedCredentials < 0) {
|
|
11699
|
+
printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
|
|
11700
|
+
printWarn(" will keep taking precedence over this login in their own repositories.");
|
|
11701
|
+
printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
|
|
11702
|
+
}
|
|
11703
|
+
if (out.repoCount === 0) {
|
|
11704
|
+
printWarn("The Verity GitHub App is not installed on any account you can access.");
|
|
11705
|
+
printInfo(" Install it (and grant your repositories), then re-run verity login:");
|
|
11706
|
+
printInfo(` ${githubAppInstallUrl(null)}`);
|
|
11707
|
+
return;
|
|
11708
|
+
}
|
|
11709
|
+
const remote = opts.remote;
|
|
11710
|
+
if (!remote) return;
|
|
11711
|
+
const who = await whoami(out.token, out.serviceUrl, opts.verbose);
|
|
11712
|
+
if (who.ok && who.data.grant_status != null) {
|
|
11713
|
+
printInfo(" \u2713 This repository is covered.");
|
|
11714
|
+
} else if (!who.ok) {
|
|
11715
|
+
printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
|
|
11716
|
+
} else {
|
|
11717
|
+
const parsed = parseRemote(remote);
|
|
11718
|
+
const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
|
|
11719
|
+
printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
|
|
11720
|
+
printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
|
|
11721
|
+
printInfo(` ${installUrl}`);
|
|
11722
|
+
}
|
|
11723
|
+
const rec = await readGlobalCredential(remote);
|
|
11724
|
+
if (rec && rec.token !== out.token) {
|
|
11725
|
+
const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
|
|
11726
|
+
const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
|
|
11727
|
+
if (otherBackend) {
|
|
11728
|
+
printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
|
|
11729
|
+
printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
|
|
11730
|
+
printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
|
|
11731
|
+
printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
|
|
11732
|
+
printWarn(" the full GitHub flow every time.");
|
|
11733
|
+
} else if (otherIdentity) {
|
|
11734
|
+
printWarn(" Note: this repository uses a different account's credential, which takes");
|
|
11735
|
+
printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
|
|
11736
|
+
printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
|
|
11737
|
+
printWarn(" only if you want this repository on the login you just completed.");
|
|
11738
|
+
} else {
|
|
11739
|
+
const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
|
|
11740
|
+
printWarn(` Note: this repository has a ${kind} credential that takes`);
|
|
11741
|
+
printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
|
|
11742
|
+
printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
|
|
11743
|
+
printWarn(" every time.");
|
|
11744
|
+
}
|
|
11745
|
+
}
|
|
11746
|
+
}
|
|
11747
|
+
|
|
11674
11748
|
// src/commands/login.ts
|
|
11675
11749
|
function registerLoginCommand(program2) {
|
|
11676
11750
|
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
11751
|
const globals = program2.opts();
|
|
11678
|
-
const
|
|
11679
|
-
if (
|
|
11680
|
-
|
|
11681
|
-
|
|
11752
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11753
|
+
if (resolution.source === "default") {
|
|
11754
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
11755
|
+
} else {
|
|
11756
|
+
printVerbose(`Service URL from ${resolution.source}: ${resolution.url}`, globals.verbose);
|
|
11682
11757
|
}
|
|
11683
|
-
const heal = await maybeHealServiceUrl(
|
|
11758
|
+
const heal = await maybeHealServiceUrl(resolution, globals.verbose);
|
|
11684
11759
|
const serviceUrl = heal.serviceUrl;
|
|
11685
11760
|
if (heal.healed) {
|
|
11686
11761
|
printInfo(" Completing login updates ~/.verity/credentials against the live service.");
|
|
@@ -11721,65 +11796,7 @@ function registerLoginCommand(program2) {
|
|
|
11721
11796
|
printError(`Login failed: ${result.error}`);
|
|
11722
11797
|
process.exit(1);
|
|
11723
11798
|
}
|
|
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
|
-
}
|
|
11799
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
|
|
11783
11800
|
});
|
|
11784
11801
|
}
|
|
11785
11802
|
|
|
@@ -11914,12 +11931,8 @@ async function auth(globals) {
|
|
|
11914
11931
|
printError(tokenResult.error);
|
|
11915
11932
|
process.exit(1);
|
|
11916
11933
|
}
|
|
11917
|
-
const
|
|
11918
|
-
|
|
11919
|
-
printError(urlResult.error);
|
|
11920
|
-
process.exit(1);
|
|
11921
|
-
}
|
|
11922
|
-
return { token: tokenResult.data.token, serviceUrl: urlResult.data };
|
|
11934
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
11935
|
+
return { token: tokenResult.data.token, serviceUrl: resolution.url };
|
|
11923
11936
|
}
|
|
11924
11937
|
function explain(error) {
|
|
11925
11938
|
if (error.startsWith("FORBIDDEN")) {
|
|
@@ -13591,6 +13604,143 @@ var import_node_fs10 = require("node:fs");
|
|
|
13591
13604
|
var import_node_crypto5 = require("node:crypto");
|
|
13592
13605
|
var import_node_path11 = require("node:path");
|
|
13593
13606
|
|
|
13607
|
+
// src/lib/skip-detection.ts
|
|
13608
|
+
function isBareAckPrompt(prompt) {
|
|
13609
|
+
if (typeof prompt !== "string") return false;
|
|
13610
|
+
const trimmed = prompt.trim();
|
|
13611
|
+
if (trimmed.length === 0) return false;
|
|
13612
|
+
if (trimmed.length > 20) return false;
|
|
13613
|
+
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;
|
|
13614
|
+
return bareAckPattern.test(trimmed);
|
|
13615
|
+
}
|
|
13616
|
+
function isContinuationPrompt(prompt) {
|
|
13617
|
+
if (typeof prompt !== "string") return false;
|
|
13618
|
+
const trimmed = prompt.trim();
|
|
13619
|
+
if (trimmed.length === 0) return false;
|
|
13620
|
+
if (trimmed.length > 24) return false;
|
|
13621
|
+
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;
|
|
13622
|
+
return continuation.test(trimmed) || isBareAckPrompt(trimmed);
|
|
13623
|
+
}
|
|
13624
|
+
function resolveGoalPrompt(prompts) {
|
|
13625
|
+
if (prompts.length === 0) return null;
|
|
13626
|
+
const latest = prompts[prompts.length - 1];
|
|
13627
|
+
if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
|
|
13628
|
+
for (let i = prompts.length - 2; i >= 0; i--) {
|
|
13629
|
+
if (!isContinuationPrompt(prompts[i].prompt)) {
|
|
13630
|
+
return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
|
|
13631
|
+
}
|
|
13632
|
+
}
|
|
13633
|
+
return { entry: latest, turnsBack: 0 };
|
|
13634
|
+
}
|
|
13635
|
+
function isReflectionQuestion(response) {
|
|
13636
|
+
if (!response || typeof response !== "string") return false;
|
|
13637
|
+
const markers = [
|
|
13638
|
+
/reflection\s+for\s+future\s+agents/i,
|
|
13639
|
+
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
13640
|
+
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
13641
|
+
/quick\s+reflection\s+question/i,
|
|
13642
|
+
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
13643
|
+
// interactive, asks the user to confirm/correct before recording. That
|
|
13644
|
+
// turn authors no code either, so it's still a reflection turn.
|
|
13645
|
+
/reflection\s+draft/i,
|
|
13646
|
+
/confirm,?\s+correct,?\s+or\s+add/i
|
|
13647
|
+
];
|
|
13648
|
+
return markers.some((m) => m.test(response));
|
|
13649
|
+
}
|
|
13650
|
+
function isMetaTaskLabel(label2) {
|
|
13651
|
+
if (label2 === null || label2 === void 0) return false;
|
|
13652
|
+
if (typeof label2 !== "string") return false;
|
|
13653
|
+
const trimmed = label2.trim();
|
|
13654
|
+
if (trimmed.length === 0) return true;
|
|
13655
|
+
const metaPatterns = [
|
|
13656
|
+
/^verity\s+[\w-]+\s+response$/i,
|
|
13657
|
+
// "Verity reflect response"
|
|
13658
|
+
/^simple user response$/i,
|
|
13659
|
+
/^verity\s+command$/i,
|
|
13660
|
+
// "Verity command"
|
|
13661
|
+
/^user\s+(question|reply|response|ack)$/i
|
|
13662
|
+
];
|
|
13663
|
+
return metaPatterns.some((p) => p.test(trimmed));
|
|
13664
|
+
}
|
|
13665
|
+
function shouldSkipForBareAck(input) {
|
|
13666
|
+
if (!isBareAckPrompt(input.prompt)) return false;
|
|
13667
|
+
if (input.turnAuthoredCode) return false;
|
|
13668
|
+
return input.canSeeTurnAuthorship;
|
|
13669
|
+
}
|
|
13670
|
+
|
|
13671
|
+
// src/lib/pending-repeat.ts
|
|
13672
|
+
var STOP = /* @__PURE__ */ new Set([
|
|
13673
|
+
"the",
|
|
13674
|
+
"and",
|
|
13675
|
+
"that",
|
|
13676
|
+
"this",
|
|
13677
|
+
"with",
|
|
13678
|
+
"from",
|
|
13679
|
+
"have",
|
|
13680
|
+
"been",
|
|
13681
|
+
"were",
|
|
13682
|
+
"what",
|
|
13683
|
+
"when",
|
|
13684
|
+
"which",
|
|
13685
|
+
"their",
|
|
13686
|
+
"there",
|
|
13687
|
+
"these",
|
|
13688
|
+
"those",
|
|
13689
|
+
"would",
|
|
13690
|
+
"could",
|
|
13691
|
+
"should",
|
|
13692
|
+
"must",
|
|
13693
|
+
"will",
|
|
13694
|
+
"also",
|
|
13695
|
+
"just",
|
|
13696
|
+
"only",
|
|
13697
|
+
"into",
|
|
13698
|
+
"over",
|
|
13699
|
+
"than",
|
|
13700
|
+
"then",
|
|
13701
|
+
"them",
|
|
13702
|
+
"some",
|
|
13703
|
+
"such",
|
|
13704
|
+
"more",
|
|
13705
|
+
"most",
|
|
13706
|
+
"other",
|
|
13707
|
+
"about",
|
|
13708
|
+
"after",
|
|
13709
|
+
"before",
|
|
13710
|
+
"since",
|
|
13711
|
+
"because",
|
|
13712
|
+
"while",
|
|
13713
|
+
"where",
|
|
13714
|
+
"whether",
|
|
13715
|
+
"ensure",
|
|
13716
|
+
"confirm",
|
|
13717
|
+
"verify",
|
|
13718
|
+
"check"
|
|
13719
|
+
]);
|
|
13720
|
+
function pendingTokens(text) {
|
|
13721
|
+
if (!text || typeof text !== "string") return [];
|
|
13722
|
+
const out = /* @__PURE__ */ new Set();
|
|
13723
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
13724
|
+
if (raw.length <= 3) continue;
|
|
13725
|
+
if (STOP.has(raw)) continue;
|
|
13726
|
+
out.add(raw);
|
|
13727
|
+
}
|
|
13728
|
+
return [...out].sort();
|
|
13729
|
+
}
|
|
13730
|
+
var REPEAT_THRESHOLD = 0.3;
|
|
13731
|
+
function overlapCoefficient(a, b) {
|
|
13732
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
13733
|
+
const setB = new Set(b);
|
|
13734
|
+
let shared = 0;
|
|
13735
|
+
for (const t of a) if (setB.has(t)) shared++;
|
|
13736
|
+
return shared / Math.min(a.length, b.length);
|
|
13737
|
+
}
|
|
13738
|
+
function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
|
|
13739
|
+
const tokens = pendingTokens(text);
|
|
13740
|
+
if (tokens.length === 0) return false;
|
|
13741
|
+
return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
|
|
13742
|
+
}
|
|
13743
|
+
|
|
13594
13744
|
// src/lib/dossier.ts
|
|
13595
13745
|
var import_node_fs9 = require("node:fs");
|
|
13596
13746
|
var import_node_crypto4 = require("node:crypto");
|
|
@@ -13599,6 +13749,7 @@ var MAX_LINE_BYTES = 4096;
|
|
|
13599
13749
|
var MAX_GOAL_CHARS = 2e3;
|
|
13600
13750
|
var GOAL_KEEP = 8;
|
|
13601
13751
|
var GOAL_TOTAL_CAP = 32;
|
|
13752
|
+
var RECENT_PENDING_CAP = 20;
|
|
13602
13753
|
var HASH_WIDTH = 16;
|
|
13603
13754
|
var AUTHORED_CAP = 300;
|
|
13604
13755
|
var NOT_MINE_CAP = 300;
|
|
@@ -13967,6 +14118,10 @@ function reduce(state, events, now) {
|
|
|
13967
14118
|
consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
|
|
13968
14119
|
};
|
|
13969
14120
|
state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
|
|
14121
|
+
if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
|
|
14122
|
+
const prior = state.meta.recent_pending_sigs ?? [];
|
|
14123
|
+
state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
|
|
14124
|
+
}
|
|
13970
14125
|
if (ev.intent_sig) {
|
|
13971
14126
|
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 };
|
|
13972
14127
|
} else {
|
|
@@ -14695,7 +14850,19 @@ function sessionDossier(token, sessionId) {
|
|
|
14695
14850
|
const d = openDossier(identity);
|
|
14696
14851
|
return d ? { d, identity } : null;
|
|
14697
14852
|
}
|
|
14853
|
+
function hasActiveGoal(d) {
|
|
14854
|
+
try {
|
|
14855
|
+
if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
|
|
14856
|
+
return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
|
|
14857
|
+
} catch {
|
|
14858
|
+
return false;
|
|
14859
|
+
}
|
|
14860
|
+
}
|
|
14698
14861
|
function recordGoal(d, prompt, source = "prompt") {
|
|
14862
|
+
if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
|
|
14863
|
+
appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
|
|
14864
|
+
return;
|
|
14865
|
+
}
|
|
14699
14866
|
const text = prompt.slice(0, MAX_GOAL_CHARS);
|
|
14700
14867
|
appendEvent(d, {
|
|
14701
14868
|
k: "goal",
|
|
@@ -14811,6 +14978,13 @@ function recordVerdict(d, v) {
|
|
|
14811
14978
|
branch: v.branch,
|
|
14812
14979
|
decision: v.decision,
|
|
14813
14980
|
...sig && { intent_sig: sig },
|
|
14981
|
+
// Fingerprints of the pending items this verdict delivered, so the NEXT turn
|
|
14982
|
+
// can tell a repeat from a new requirement. Only recorded when the channel
|
|
14983
|
+
// actually spoke — a silenced turn delivered nothing, so nothing was "said
|
|
14984
|
+
// before" and labelling the next turn's items as repeats would be a lie.
|
|
14985
|
+
...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
|
|
14986
|
+
pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
|
|
14987
|
+
},
|
|
14814
14988
|
emitted: v.emitted === true,
|
|
14815
14989
|
idle: v.idle !== false,
|
|
14816
14990
|
...v.intent?.verdict && { intent_verdict: v.intent.verdict },
|
|
@@ -15118,6 +15292,8 @@ function collectCodeDelta(files, opts) {
|
|
|
15118
15292
|
let totalSize = 0;
|
|
15119
15293
|
let truncationReason = null;
|
|
15120
15294
|
const droppedPaths = [];
|
|
15295
|
+
const excluded = [];
|
|
15296
|
+
const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
|
|
15121
15297
|
for (const filepath of sorted) {
|
|
15122
15298
|
if (result.length >= maxFiles) {
|
|
15123
15299
|
truncationReason ??= "max_files";
|
|
@@ -15125,14 +15301,21 @@ function collectCodeDelta(files, opts) {
|
|
|
15125
15301
|
continue;
|
|
15126
15302
|
}
|
|
15127
15303
|
const resolved = resolveFile(filepath);
|
|
15128
|
-
if (!resolved)
|
|
15304
|
+
if (!resolved) {
|
|
15305
|
+
exclude(filepath, "path-not-resolvable");
|
|
15306
|
+
continue;
|
|
15307
|
+
}
|
|
15129
15308
|
let size;
|
|
15130
15309
|
try {
|
|
15131
15310
|
size = (0, import_node_fs11.statSync)(resolved).size;
|
|
15132
15311
|
} catch {
|
|
15312
|
+
exclude(filepath, "not-stattable");
|
|
15313
|
+
continue;
|
|
15314
|
+
}
|
|
15315
|
+
if (size > maxFileBytes) {
|
|
15316
|
+
exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
|
|
15133
15317
|
continue;
|
|
15134
15318
|
}
|
|
15135
|
-
if (size > maxFileBytes) continue;
|
|
15136
15319
|
if (totalSize + size > maxTotalBytes) {
|
|
15137
15320
|
truncationReason ??= "max_total_bytes";
|
|
15138
15321
|
const idx = sorted.indexOf(filepath);
|
|
@@ -15143,6 +15326,7 @@ function collectCodeDelta(files, opts) {
|
|
|
15143
15326
|
try {
|
|
15144
15327
|
content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
|
|
15145
15328
|
} catch {
|
|
15329
|
+
exclude(filepath, "not-readable");
|
|
15146
15330
|
continue;
|
|
15147
15331
|
}
|
|
15148
15332
|
totalSize += size;
|
|
@@ -15156,10 +15340,14 @@ function collectCodeDelta(files, opts) {
|
|
|
15156
15340
|
(sum, f) => sum + f.content.split("\n").length,
|
|
15157
15341
|
0
|
|
15158
15342
|
);
|
|
15343
|
+
for (const path of droppedPaths) {
|
|
15344
|
+
exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
|
|
15345
|
+
}
|
|
15159
15346
|
return {
|
|
15160
15347
|
files: result,
|
|
15161
15348
|
total_lines: totalLines,
|
|
15162
15349
|
total_files: result.length,
|
|
15350
|
+
excluded,
|
|
15163
15351
|
...truncationReason && {
|
|
15164
15352
|
truncated: {
|
|
15165
15353
|
reason: truncationReason,
|
|
@@ -15453,6 +15641,34 @@ ${addedLines}`,
|
|
|
15453
15641
|
}
|
|
15454
15642
|
return { diffs, has_baseline: true };
|
|
15455
15643
|
}
|
|
15644
|
+
function absorbIntoBaseline(paths, sessionId) {
|
|
15645
|
+
const baseline = readBaseline(sessionId);
|
|
15646
|
+
if (!baseline || paths.length === 0) return 0;
|
|
15647
|
+
const dir = sessionDir(sessionKey(baseline.session_id));
|
|
15648
|
+
let adopted = 0;
|
|
15649
|
+
const dirty = new Set(baseline.dirty_paths);
|
|
15650
|
+
for (const p of paths) {
|
|
15651
|
+
try {
|
|
15652
|
+
const content = safeReadForMirror(projectPath(p));
|
|
15653
|
+
if (content === null) continue;
|
|
15654
|
+
const dest = mirrorPath(dir, p);
|
|
15655
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
|
|
15656
|
+
(0, import_node_fs13.writeFileSync)(dest, content);
|
|
15657
|
+
dirty.add(p);
|
|
15658
|
+
adopted++;
|
|
15659
|
+
} catch {
|
|
15660
|
+
}
|
|
15661
|
+
}
|
|
15662
|
+
if (adopted === 0) return 0;
|
|
15663
|
+
try {
|
|
15664
|
+
const updated = { ...baseline, dirty_paths: [...dirty] };
|
|
15665
|
+
(0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
|
|
15666
|
+
preImageCache.delete(baseline);
|
|
15667
|
+
} catch {
|
|
15668
|
+
return 0;
|
|
15669
|
+
}
|
|
15670
|
+
return adopted;
|
|
15671
|
+
}
|
|
15456
15672
|
function changedSinceBaseline(repoRelPath, baseline) {
|
|
15457
15673
|
const pre = preImage(repoRelPath, baseline);
|
|
15458
15674
|
let current;
|
|
@@ -16329,40 +16545,40 @@ function narrowToRecent(files, sessionId) {
|
|
|
16329
16545
|
});
|
|
16330
16546
|
return recent.length > 0 ? recent : files;
|
|
16331
16547
|
}
|
|
16332
|
-
function
|
|
16333
|
-
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
|
|
16548
|
+
function readIterationState(currentCommit) {
|
|
16549
|
+
if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
|
|
16334
16550
|
try {
|
|
16335
16551
|
const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
|
|
16336
16552
|
const parts = stored.split(":");
|
|
16337
16553
|
const iter = parseInt(parts[0], 10);
|
|
16338
16554
|
const storedCommit = parts[1] ?? "";
|
|
16339
16555
|
const storedTimestamp = parseInt(parts[2] ?? "0", 10);
|
|
16340
|
-
|
|
16341
|
-
if (
|
|
16556
|
+
const fingerprint = parts.slice(3).join(":") || null;
|
|
16557
|
+
if (isNaN(iter)) return { iteration: 1, fingerprint: null };
|
|
16558
|
+
if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
|
|
16342
16559
|
if (storedTimestamp > 0) {
|
|
16343
16560
|
const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
|
|
16344
|
-
if (elapsed > 600) return 1;
|
|
16561
|
+
if (elapsed > 600) return { iteration: 1, fingerprint: null };
|
|
16345
16562
|
}
|
|
16346
|
-
return iter;
|
|
16563
|
+
return { iteration: iter, fingerprint };
|
|
16347
16564
|
} catch {
|
|
16348
|
-
return 1;
|
|
16565
|
+
return { iteration: 1, fingerprint: null };
|
|
16349
16566
|
}
|
|
16350
16567
|
}
|
|
16351
|
-
function
|
|
16352
|
-
const
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
}
|
|
16360
|
-
return { skip: null, iteration };
|
|
16568
|
+
function findingsFingerprint(findings) {
|
|
16569
|
+
const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
|
|
16570
|
+
return [...new Set(keys)].sort().join(",");
|
|
16571
|
+
}
|
|
16572
|
+
function isSameProblem(previous, current) {
|
|
16573
|
+
if (!previous || !current) return false;
|
|
16574
|
+
const prev = new Set(previous.split(","));
|
|
16575
|
+
return current.split(",").some((k) => prev.has(k));
|
|
16361
16576
|
}
|
|
16362
|
-
function writeIteration(iteration, commit, _contentHash) {
|
|
16577
|
+
function writeIteration(iteration, commit, _contentHash, fingerprint) {
|
|
16363
16578
|
(0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
|
|
16364
16579
|
const ts = Math.floor(Date.now() / 1e3);
|
|
16365
|
-
|
|
16580
|
+
const fp = fingerprint ? `:${fingerprint}` : "";
|
|
16581
|
+
(0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
|
|
16366
16582
|
}
|
|
16367
16583
|
|
|
16368
16584
|
// src/lib/static-analysis.ts
|
|
@@ -16611,7 +16827,7 @@ function resolveTaskContext(opts) {
|
|
|
16611
16827
|
// src/lib/cli-version.ts
|
|
16612
16828
|
function cliVersion() {
|
|
16613
16829
|
try {
|
|
16614
|
-
return true ? "0.28.1-experimental.
|
|
16830
|
+
return true ? "0.28.1-experimental.2cbb593" : "dev";
|
|
16615
16831
|
} catch {
|
|
16616
16832
|
return "dev";
|
|
16617
16833
|
}
|
|
@@ -17156,6 +17372,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
|
|
|
17156
17372
|
};
|
|
17157
17373
|
}
|
|
17158
17374
|
|
|
17375
|
+
// src/lib/verdict.ts
|
|
17376
|
+
function reconcileCoverage(changed, coverage) {
|
|
17377
|
+
const changedSet = new Set(changed);
|
|
17378
|
+
const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
|
|
17379
|
+
const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
|
|
17380
|
+
const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
|
|
17381
|
+
const notReviewed = [
|
|
17382
|
+
...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
|
|
17383
|
+
...unaccounted.map((path) => ({
|
|
17384
|
+
path,
|
|
17385
|
+
reason: "unaccounted",
|
|
17386
|
+
// Named so the eventual bug report writes itself: some stage removed this
|
|
17387
|
+
// path and did not say so.
|
|
17388
|
+
stage: "unknown-stage",
|
|
17389
|
+
// An undeclared drop is CAPACITY by default. A stage that cannot be
|
|
17390
|
+
// bothered to say why it dropped a file does not get the benefit of the
|
|
17391
|
+
// doubt — that default is what makes forgetting expensive.
|
|
17392
|
+
kind: "capacity"
|
|
17393
|
+
}))
|
|
17394
|
+
];
|
|
17395
|
+
return {
|
|
17396
|
+
coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
|
|
17397
|
+
unaccounted,
|
|
17398
|
+
balances: unaccounted.length === 0
|
|
17399
|
+
};
|
|
17400
|
+
}
|
|
17401
|
+
function resolveVerdict(proposed, coverage) {
|
|
17402
|
+
if (proposed === "FAIL") return "FAIL";
|
|
17403
|
+
const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17404
|
+
if (blocking.length === 0) return proposed;
|
|
17405
|
+
return "WARN";
|
|
17406
|
+
}
|
|
17407
|
+
function describeCoverage(coverage, maxPaths = 5) {
|
|
17408
|
+
const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
|
|
17409
|
+
if (relevant.length === 0) return null;
|
|
17410
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
17411
|
+
for (const n of relevant) {
|
|
17412
|
+
const key = `${n.reason}`;
|
|
17413
|
+
const list = byReason.get(key) ?? [];
|
|
17414
|
+
list.push(n.path);
|
|
17415
|
+
byReason.set(key, list);
|
|
17416
|
+
}
|
|
17417
|
+
const lines = [];
|
|
17418
|
+
for (const [reason, paths] of [...byReason.entries()].sort()) {
|
|
17419
|
+
const shown = paths.slice(0, maxPaths).join(", ");
|
|
17420
|
+
const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
|
|
17421
|
+
lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
|
|
17422
|
+
}
|
|
17423
|
+
return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
|
|
17424
|
+
${lines.join("\n")}
|
|
17425
|
+
Treat those files as UNCHECKED, not as approved.`;
|
|
17426
|
+
}
|
|
17427
|
+
function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
|
|
17428
|
+
const reviewed = new Set(reviewedNow);
|
|
17429
|
+
const out = [];
|
|
17430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17431
|
+
for (const s of statements) {
|
|
17432
|
+
if (s.outcome !== "open") continue;
|
|
17433
|
+
if (s.register !== "BLOCK") continue;
|
|
17434
|
+
if (s.carried) continue;
|
|
17435
|
+
if (reviewed.has(s.file)) continue;
|
|
17436
|
+
if (!s.line_sha) continue;
|
|
17437
|
+
if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
|
|
17438
|
+
const key = `${s.file}::${s.pattern_id}`;
|
|
17439
|
+
if (seen.has(key)) continue;
|
|
17440
|
+
seen.add(key);
|
|
17441
|
+
out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
|
|
17442
|
+
}
|
|
17443
|
+
return out;
|
|
17444
|
+
}
|
|
17445
|
+
function describeOpenElsewhere(open) {
|
|
17446
|
+
if (open.length === 0) return null;
|
|
17447
|
+
const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
|
|
17448
|
+
const more = open.length > 5 ? `
|
|
17449
|
+
(+${open.length - 5} more)` : "";
|
|
17450
|
+
return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
|
|
17451
|
+
${lines.join("\n")}${more}
|
|
17452
|
+
This verdict covers the current change only. The tree is not clean.`;
|
|
17453
|
+
}
|
|
17454
|
+
|
|
17159
17455
|
// src/lib/channel.ts
|
|
17160
17456
|
var MAX_AGENT_CONTEXT_CHARS = 1500;
|
|
17161
17457
|
var MAX_AGENT_ITEMS = 5;
|
|
@@ -17205,9 +17501,13 @@ function buildAgentContext(input) {
|
|
|
17205
17501
|
}
|
|
17206
17502
|
for (const p of input.pendingItems ?? []) {
|
|
17207
17503
|
if (lines.length >= MAX_AGENT_ITEMS) break;
|
|
17504
|
+
if (p.pattern_id === "intent-misalignment") continue;
|
|
17208
17505
|
const text = p.description ?? p.title ?? p.reason;
|
|
17209
17506
|
if (!text) continue;
|
|
17210
|
-
|
|
17507
|
+
const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
|
|
17508
|
+
lines.push(
|
|
17509
|
+
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)" : "")
|
|
17510
|
+
);
|
|
17211
17511
|
}
|
|
17212
17512
|
if (lines.length === 0) return null;
|
|
17213
17513
|
const body = `${REPORT_PREFIX}
|
|
@@ -17243,6 +17543,39 @@ function channelSilence(input) {
|
|
|
17243
17543
|
return null;
|
|
17244
17544
|
}
|
|
17245
17545
|
|
|
17546
|
+
// src/lib/emit.ts
|
|
17547
|
+
var YELLOW2 = "\x1B[33m";
|
|
17548
|
+
var NC2 = "\x1B[0m";
|
|
17549
|
+
function emitVerdict(input) {
|
|
17550
|
+
const exit = input.exit ?? ((code) => process.exit(code));
|
|
17551
|
+
const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
|
|
17552
|
+
let verdict = resolveVerdict(input.proposed, coverage);
|
|
17553
|
+
const openElsewhere = input.openElsewhere ?? [];
|
|
17554
|
+
if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
|
|
17555
|
+
const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
|
|
17556
|
+
if (unaccounted.length > 0) {
|
|
17557
|
+
process.stderr.write(
|
|
17558
|
+
`${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
|
|
17559
|
+
`
|
|
17560
|
+
);
|
|
17561
|
+
}
|
|
17562
|
+
if (verdict === "FAIL") {
|
|
17563
|
+
input.renderBlocking?.();
|
|
17564
|
+
if (input.agentContext) {
|
|
17565
|
+
process.stderr.write(`
|
|
17566
|
+
${input.agentContext}
|
|
17567
|
+
`);
|
|
17568
|
+
}
|
|
17569
|
+
if (note && !input.silenced) process.stderr.write(`
|
|
17570
|
+
${YELLOW2}${note}${NC2}
|
|
17571
|
+
`);
|
|
17572
|
+
return exit(2);
|
|
17573
|
+
}
|
|
17574
|
+
const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
|
|
17575
|
+
printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
|
|
17576
|
+
return exit(0);
|
|
17577
|
+
}
|
|
17578
|
+
|
|
17246
17579
|
// src/lib/cache-cleanup.ts
|
|
17247
17580
|
var import_node_fs21 = require("node:fs");
|
|
17248
17581
|
var import_node_path18 = require("node:path");
|
|
@@ -17422,46 +17755,6 @@ function shouldWarmRetryAnalyze(result) {
|
|
|
17422
17755
|
return false;
|
|
17423
17756
|
}
|
|
17424
17757
|
|
|
17425
|
-
// src/lib/skip-detection.ts
|
|
17426
|
-
function isBareAckPrompt(prompt) {
|
|
17427
|
-
if (typeof prompt !== "string") return false;
|
|
17428
|
-
const trimmed = prompt.trim();
|
|
17429
|
-
if (trimmed.length === 0) return false;
|
|
17430
|
-
if (trimmed.length > 20) return false;
|
|
17431
|
-
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;
|
|
17432
|
-
return bareAckPattern.test(trimmed);
|
|
17433
|
-
}
|
|
17434
|
-
function isReflectionQuestion(response) {
|
|
17435
|
-
if (!response || typeof response !== "string") return false;
|
|
17436
|
-
const markers = [
|
|
17437
|
-
/reflection\s+for\s+future\s+agents/i,
|
|
17438
|
-
/what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
|
|
17439
|
-
/say\s+['"]?skip['"]?\s+to\s+skip/i,
|
|
17440
|
-
/quick\s+reflection\s+question/i,
|
|
17441
|
-
// Post-flip (VRT-21): the agent drafts the reflection itself and, when
|
|
17442
|
-
// interactive, asks the user to confirm/correct before recording. That
|
|
17443
|
-
// turn authors no code either, so it's still a reflection turn.
|
|
17444
|
-
/reflection\s+draft/i,
|
|
17445
|
-
/confirm,?\s+correct,?\s+or\s+add/i
|
|
17446
|
-
];
|
|
17447
|
-
return markers.some((m) => m.test(response));
|
|
17448
|
-
}
|
|
17449
|
-
function isMetaTaskLabel(label2) {
|
|
17450
|
-
if (label2 === null || label2 === void 0) return false;
|
|
17451
|
-
if (typeof label2 !== "string") return false;
|
|
17452
|
-
const trimmed = label2.trim();
|
|
17453
|
-
if (trimmed.length === 0) return true;
|
|
17454
|
-
const metaPatterns = [
|
|
17455
|
-
/^verity\s+[\w-]+\s+response$/i,
|
|
17456
|
-
// "Verity reflect response"
|
|
17457
|
-
/^simple user response$/i,
|
|
17458
|
-
/^verity\s+command$/i,
|
|
17459
|
-
// "Verity command"
|
|
17460
|
-
/^user\s+(question|reply|response|ack)$/i
|
|
17461
|
-
];
|
|
17462
|
-
return metaPatterns.some((p) => p.test(trimmed));
|
|
17463
|
-
}
|
|
17464
|
-
|
|
17465
17758
|
// src/lib/transcript.ts
|
|
17466
17759
|
var import_node_fs22 = require("node:fs");
|
|
17467
17760
|
var MAX_READ_BYTES = 256 * 1024;
|
|
@@ -17475,9 +17768,11 @@ var MAX_SUMMARY_BYTES = 4096;
|
|
|
17475
17768
|
var HOME = process.env.HOME ?? "";
|
|
17476
17769
|
async function extractActionSummary(transcriptPath) {
|
|
17477
17770
|
try {
|
|
17478
|
-
const
|
|
17479
|
-
if (!
|
|
17480
|
-
|
|
17771
|
+
const read = readTurnLines(transcriptPath);
|
|
17772
|
+
if (!read || read.lines.length === 0) return null;
|
|
17773
|
+
const summary = buildSummary(read.lines);
|
|
17774
|
+
if (summary) summary.transcript_windowed = read.window;
|
|
17775
|
+
return summary;
|
|
17481
17776
|
} catch {
|
|
17482
17777
|
return null;
|
|
17483
17778
|
}
|
|
@@ -17491,9 +17786,11 @@ function readTurnLines(transcriptPath) {
|
|
|
17491
17786
|
}
|
|
17492
17787
|
if (size === 0) return null;
|
|
17493
17788
|
let raw;
|
|
17789
|
+
let windowed = false;
|
|
17494
17790
|
if (size <= SMALL_FILE_BYTES) {
|
|
17495
17791
|
raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
|
|
17496
17792
|
} else {
|
|
17793
|
+
windowed = true;
|
|
17497
17794
|
const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
|
|
17498
17795
|
const fd = require("node:fs").openSync(transcriptPath, "r");
|
|
17499
17796
|
try {
|
|
@@ -17511,17 +17808,22 @@ function readTurnLines(transcriptPath) {
|
|
|
17511
17808
|
const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
17512
17809
|
if (allLines.length === 0) return null;
|
|
17513
17810
|
let turnStart = 0;
|
|
17811
|
+
let boundaryFound = false;
|
|
17514
17812
|
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
17515
17813
|
try {
|
|
17516
17814
|
const parsed = JSON.parse(allLines[i]);
|
|
17517
17815
|
if (parsed.type === "user" && isRealUserMessage(parsed)) {
|
|
17518
17816
|
turnStart = i;
|
|
17817
|
+
boundaryFound = true;
|
|
17519
17818
|
break;
|
|
17520
17819
|
}
|
|
17521
17820
|
} catch {
|
|
17522
17821
|
}
|
|
17523
17822
|
}
|
|
17524
|
-
return
|
|
17823
|
+
return {
|
|
17824
|
+
lines: allLines.slice(turnStart),
|
|
17825
|
+
window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
|
|
17826
|
+
};
|
|
17525
17827
|
}
|
|
17526
17828
|
function isRealUserMessage(parsed) {
|
|
17527
17829
|
const message = parsed.message;
|
|
@@ -17625,6 +17927,13 @@ function buildSummary(lines) {
|
|
|
17625
17927
|
files_read: capArray(filesRead, MAX_FILES_LIST),
|
|
17626
17928
|
files_edited: capArray(filesEdited, MAX_FILES_LIST),
|
|
17627
17929
|
files_created: capArray(filesCreated, MAX_CREATED_LIST),
|
|
17930
|
+
// The complement of the two caps that affect SCOPE. `files_read` is excluded
|
|
17931
|
+
// deliberately: reading a file is not authoring it, so a capped read list
|
|
17932
|
+
// narrows nothing.
|
|
17933
|
+
capped_out: [
|
|
17934
|
+
...cappedOut(filesEdited, MAX_FILES_LIST),
|
|
17935
|
+
...cappedOut(filesCreated, MAX_CREATED_LIST)
|
|
17936
|
+
],
|
|
17628
17937
|
searches,
|
|
17629
17938
|
commands,
|
|
17630
17939
|
subagents,
|
|
@@ -17675,6 +17984,9 @@ function sanitizeCommand(rawCmd) {
|
|
|
17675
17984
|
function capArray(set, max) {
|
|
17676
17985
|
return Array.from(set).slice(0, max);
|
|
17677
17986
|
}
|
|
17987
|
+
function cappedOut(set, max) {
|
|
17988
|
+
return Array.from(set).slice(max);
|
|
17989
|
+
}
|
|
17678
17990
|
|
|
17679
17991
|
// src/lib/run-mode.ts
|
|
17680
17992
|
function parseAutonomousEnv(raw) {
|
|
@@ -18069,11 +18381,12 @@ async function readStopHookStdin() {
|
|
|
18069
18381
|
return empty;
|
|
18070
18382
|
}
|
|
18071
18383
|
}
|
|
18072
|
-
function agentContextFor(response, intentRepeat = 0) {
|
|
18384
|
+
function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
|
|
18073
18385
|
const metadata = response.metadata ?? {};
|
|
18074
18386
|
const intent = response.intent_alignment ?? {};
|
|
18075
18387
|
return buildAgentContext({
|
|
18076
18388
|
intentRepeat,
|
|
18389
|
+
priorPendingFingerprints,
|
|
18077
18390
|
gateDecision: String(response.gate_decision ?? ""),
|
|
18078
18391
|
findings: response.findings ?? [],
|
|
18079
18392
|
pendingItems: response.pending_items ?? [],
|
|
@@ -18084,12 +18397,45 @@ function agentContextFor(response, intentRepeat = 0) {
|
|
|
18084
18397
|
});
|
|
18085
18398
|
}
|
|
18086
18399
|
var beaconCtx = null;
|
|
18087
|
-
async function passAndExit(reason, skip) {
|
|
18400
|
+
async function passAndExit(reason, skip, kindOverride) {
|
|
18088
18401
|
const sent = await sendSkipBeacon(beaconCtx, skip);
|
|
18089
18402
|
logEvent("skip", { reason: skip, beacon: sent });
|
|
18090
|
-
|
|
18403
|
+
const POLICY_SKIPS = /* @__PURE__ */ new Set([
|
|
18404
|
+
"no-analyzable-files",
|
|
18405
|
+
"verity-command",
|
|
18406
|
+
"bare-acknowledgment",
|
|
18407
|
+
"reflection-prompt",
|
|
18408
|
+
"skip-mode",
|
|
18409
|
+
"zero-increment",
|
|
18410
|
+
"debounce",
|
|
18411
|
+
"no-delta-since-last-review"
|
|
18412
|
+
]);
|
|
18413
|
+
const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
|
|
18414
|
+
const changed = skipCoverageChanged;
|
|
18415
|
+
const { coverage, unaccounted } = reconcileCoverage(changed, {
|
|
18416
|
+
reviewed: [],
|
|
18417
|
+
notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
|
|
18418
|
+
});
|
|
18419
|
+
const verdict = resolveVerdict("PASS", coverage);
|
|
18420
|
+
const note = describeCoverage(coverage);
|
|
18421
|
+
if (unaccounted.length > 0) {
|
|
18422
|
+
logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
|
|
18423
|
+
}
|
|
18424
|
+
const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
|
|
18425
|
+
const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
|
|
18426
|
+
printJsonCompact(
|
|
18427
|
+
buildHookOutput(
|
|
18428
|
+
verdict,
|
|
18429
|
+
`Verity: ${reason}`,
|
|
18430
|
+
// The agent's ONLY input is additionalContext. Sixteen of the nineteen
|
|
18431
|
+
// terminating paths wrote `systemMessage` — the human's field — and told
|
|
18432
|
+
// the agent nothing at all.
|
|
18433
|
+
agentNote
|
|
18434
|
+
)
|
|
18435
|
+
);
|
|
18091
18436
|
process.exit(0);
|
|
18092
18437
|
}
|
|
18438
|
+
var skipCoverageChanged = [];
|
|
18093
18439
|
var EMPTY_STATIC = {
|
|
18094
18440
|
tool: "@codacy/analysis-cli",
|
|
18095
18441
|
findings: [],
|
|
@@ -18104,7 +18450,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
|
|
|
18104
18450
|
}
|
|
18105
18451
|
function localOnlyAndExit(staticResults) {
|
|
18106
18452
|
printJsonCompact({
|
|
18107
|
-
gate_decision: "
|
|
18453
|
+
gate_decision: "WARN",
|
|
18108
18454
|
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.",
|
|
18109
18455
|
unauthenticated: true,
|
|
18110
18456
|
static_results: staticResults
|
|
@@ -18164,6 +18510,7 @@ async function runAnalyze(opts, globals) {
|
|
|
18164
18510
|
});
|
|
18165
18511
|
}
|
|
18166
18512
|
const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
|
|
18513
|
+
skipCoverageChanged = allChanged;
|
|
18167
18514
|
const analyzable = filterAnalyzable(allChanged);
|
|
18168
18515
|
const reviewable = filterReviewable(allChanged);
|
|
18169
18516
|
const securityFiles = filterSecurity(allChanged);
|
|
@@ -18175,14 +18522,24 @@ async function runAnalyze(opts, globals) {
|
|
|
18175
18522
|
const conversation = await readAndClearConversationBuffer(baselineSessionId);
|
|
18176
18523
|
const specs = discoverSpecs();
|
|
18177
18524
|
const plans = discoverPlans();
|
|
18525
|
+
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
18526
|
+
const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
|
|
18527
|
+
const canSeeTurnAuthorship = !!actionSummary || !!baseline;
|
|
18178
18528
|
const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
|
|
18179
18529
|
if (/^\s*\/verity-/i.test(latestPrompt)) {
|
|
18530
|
+
const setupAuthored = [
|
|
18531
|
+
...actionSummary?.files_edited ?? [],
|
|
18532
|
+
...actionSummary?.files_created ?? []
|
|
18533
|
+
];
|
|
18534
|
+
if (setupAuthored.length > 0) {
|
|
18535
|
+
const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
|
|
18536
|
+
logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
|
|
18537
|
+
}
|
|
18180
18538
|
await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
|
|
18181
18539
|
}
|
|
18182
|
-
if (
|
|
18540
|
+
if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
|
|
18183
18541
|
await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
|
|
18184
18542
|
}
|
|
18185
|
-
const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
|
|
18186
18543
|
if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
|
|
18187
18544
|
await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
|
|
18188
18545
|
}
|
|
@@ -18229,7 +18586,11 @@ async function runAnalyze(opts, globals) {
|
|
|
18229
18586
|
);
|
|
18230
18587
|
}
|
|
18231
18588
|
if (analysisMode === "skip") {
|
|
18232
|
-
await passAndExit(
|
|
18589
|
+
await passAndExit(
|
|
18590
|
+
"Skip mode \u2014 no code work to analyze",
|
|
18591
|
+
"skip-mode",
|
|
18592
|
+
turnAuthoredCode ? "capacity" : void 0
|
|
18593
|
+
);
|
|
18233
18594
|
}
|
|
18234
18595
|
let staticResults = {
|
|
18235
18596
|
tool: "@codacy/analysis-cli",
|
|
@@ -18239,7 +18600,8 @@ async function runAnalyze(opts, globals) {
|
|
|
18239
18600
|
let codeDelta = {
|
|
18240
18601
|
files: [],
|
|
18241
18602
|
total_lines: 0,
|
|
18242
|
-
total_files: 0
|
|
18603
|
+
total_files: 0,
|
|
18604
|
+
excluded: []
|
|
18243
18605
|
};
|
|
18244
18606
|
let snapshotResult = { has_snapshots: false, diffs: [] };
|
|
18245
18607
|
let contentHash = null;
|
|
@@ -18304,7 +18666,11 @@ async function runAnalyze(opts, globals) {
|
|
|
18304
18666
|
if (assistantResponse) {
|
|
18305
18667
|
analysisMode = "plan";
|
|
18306
18668
|
} else {
|
|
18307
|
-
await passAndExit(
|
|
18669
|
+
await passAndExit(
|
|
18670
|
+
"No files within size limits to analyze",
|
|
18671
|
+
"size-limit",
|
|
18672
|
+
codeDelta.excluded.length > 0 ? "capacity" : "policy"
|
|
18673
|
+
);
|
|
18308
18674
|
}
|
|
18309
18675
|
}
|
|
18310
18676
|
}
|
|
@@ -18317,19 +18683,13 @@ async function runAnalyze(opts, globals) {
|
|
|
18317
18683
|
snapshotResult = generateSnapshotDiffs(codeDelta.files);
|
|
18318
18684
|
}
|
|
18319
18685
|
currentCommit = getCurrentCommit();
|
|
18320
|
-
|
|
18321
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
|
|
18322
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18323
|
-
iteration = iterResult.iteration;
|
|
18686
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18324
18687
|
}
|
|
18325
18688
|
}
|
|
18326
18689
|
if (analysisMode === "plan") {
|
|
18327
18690
|
recordAnalysisStart();
|
|
18328
18691
|
currentCommit = getCurrentCommit();
|
|
18329
|
-
|
|
18330
|
-
const iterResult = checkMaxIterations(currentCommit, maxIterations);
|
|
18331
|
-
if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
|
|
18332
|
-
iteration = iterResult.iteration;
|
|
18692
|
+
iteration = readIterationState(currentCommit).iteration;
|
|
18333
18693
|
}
|
|
18334
18694
|
const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
|
|
18335
18695
|
for (const f of codeDelta.files) {
|
|
@@ -18502,7 +18862,30 @@ async function runAnalyze(opts, globals) {
|
|
|
18502
18862
|
hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
|
|
18503
18863
|
isTTY: process.stdout.isTTY === true
|
|
18504
18864
|
});
|
|
18865
|
+
const excludedByReason = {};
|
|
18866
|
+
for (const e of codeDelta.excluded ?? []) {
|
|
18867
|
+
excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
|
|
18868
|
+
}
|
|
18869
|
+
const coverageTelemetry = {
|
|
18870
|
+
// git's whole answer, before ANY narrowing. The number that has never been sent.
|
|
18871
|
+
changed_all: allChanged.length,
|
|
18872
|
+
analyzable: analyzable.length,
|
|
18873
|
+
reviewable: reviewable.length,
|
|
18874
|
+
security: securityFiles.length,
|
|
18875
|
+
// after the allowlist, before authorship scoping and the caps
|
|
18876
|
+
for_review: allForReview.length,
|
|
18877
|
+
// what actually reaches the reviewer
|
|
18878
|
+
sent: codeDelta.files.length,
|
|
18879
|
+
// the two silent narrowings, counted separately so they can be told apart
|
|
18880
|
+
capped_out: actionSummary?.capped_out?.length ?? 0,
|
|
18881
|
+
excluded: (codeDelta.excluded ?? []).length,
|
|
18882
|
+
excluded_by_reason: excludedByReason,
|
|
18883
|
+
// was the transcript itself truncated? The 256 KB window means "this turn"
|
|
18884
|
+
// can quietly mean "the last 256 KB of it".
|
|
18885
|
+
transcript_windowed: actionSummary?.transcript_windowed ?? null
|
|
18886
|
+
};
|
|
18505
18887
|
const requestBody = {
|
|
18888
|
+
coverage_telemetry: coverageTelemetry,
|
|
18506
18889
|
static_results: staticResults,
|
|
18507
18890
|
code_delta: codeDelta,
|
|
18508
18891
|
changed_files: allForReview,
|
|
@@ -18588,6 +18971,54 @@ async function runAnalyze(opts, globals) {
|
|
|
18588
18971
|
// replaced a population floor with ≈35% power that was sub-integer for
|
|
18589
18972
|
// three-quarters of the fleet.
|
|
18590
18973
|
conservation: foldConservation,
|
|
18974
|
+
// ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
|
|
18975
|
+
//
|
|
18976
|
+
// The whole "task-scoped delta" design space rests on an assumption that
|
|
18977
|
+
// has been observed exactly ONCE: that delta files routinely belong to
|
|
18978
|
+
// earlier work. Three designs were built on it and all three were killed
|
|
18979
|
+
// adversarially — two by measurement — so before another is attempted,
|
|
18980
|
+
// measure the base rate.
|
|
18981
|
+
//
|
|
18982
|
+
// `authored_under_earlier_goal` counts delta paths whose LAST authorship
|
|
18983
|
+
// event precedes the seq of the goal now in force. Both numbers come from
|
|
18984
|
+
// the same append-only counter (`nextSeq`), so the comparison is exact.
|
|
18985
|
+
//
|
|
18986
|
+
// Keyed on the GOAL, deliberately, not on the task id. The task classifier
|
|
18987
|
+
// reported `is_new_task` on two consecutive turns of one task 25 seconds
|
|
18988
|
+
// apart, so a task-keyed number would measure its unreliability rather
|
|
18989
|
+
// than the phenomenon. And this only became meaningful once `recordGoal`
|
|
18990
|
+
// stopped letting a bare "ok" supersede the goal — before that the seq
|
|
18991
|
+
// advanced every turn and this would have degenerated to "not edited this
|
|
18992
|
+
// turn", which is the exact mistake that sank one of the three designs.
|
|
18993
|
+
//
|
|
18994
|
+
// Changes no payload the reviewer sees, no narrowing, no verdict.
|
|
18995
|
+
vrt52: (() => {
|
|
18996
|
+
const goalSeq = memory?.projection.goal?.seq;
|
|
18997
|
+
if (goalSeq === void 0 || !memorySession) return { known: false };
|
|
18998
|
+
const lastSeq2 = new Map(
|
|
18999
|
+
foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
|
|
19000
|
+
);
|
|
19001
|
+
let earlier = 0;
|
|
19002
|
+
let unknown = 0;
|
|
19003
|
+
for (const f of codeDelta.files) {
|
|
19004
|
+
const seen = lastSeq2.get(f.path);
|
|
19005
|
+
if (seen === void 0) unknown++;
|
|
19006
|
+
else if (seen < goalSeq) earlier++;
|
|
19007
|
+
}
|
|
19008
|
+
return {
|
|
19009
|
+
known: true,
|
|
19010
|
+
goal_seq: goalSeq,
|
|
19011
|
+
delta: codeDelta.files.length,
|
|
19012
|
+
// Files this delta carries that were last written under an EARLIER
|
|
19013
|
+
// instruction. If this stays near zero, VRT-52's code half is
|
|
19014
|
+
// unnecessary and should be closed saying so.
|
|
19015
|
+
authored_under_earlier_goal: earlier,
|
|
19016
|
+
// Delta files the dossier has no authorship record for at all —
|
|
19017
|
+
// pre-existing tree state, or an authorship channel the fold cannot
|
|
19018
|
+
// see. Reported separately so a blind spot is never counted as a zero.
|
|
19019
|
+
no_authorship_record: unknown
|
|
19020
|
+
};
|
|
19021
|
+
})(),
|
|
18591
19022
|
// P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
|
|
18592
19023
|
// narrowed to the within-session increment: what changed since the last
|
|
18593
19024
|
// VERDICT rather than since task start.
|
|
@@ -18620,7 +19051,20 @@ async function runAnalyze(opts, globals) {
|
|
|
18620
19051
|
const intentContext = {};
|
|
18621
19052
|
if (conversation && conversation.prompts.length > 0) {
|
|
18622
19053
|
const latest = conversation.prompts[conversation.prompts.length - 1];
|
|
18623
|
-
|
|
19054
|
+
const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
|
|
19055
|
+
intentContext.user_prompt = goalPrompt.entry.prompt;
|
|
19056
|
+
if (isContinuationPrompt(intentContext.user_prompt)) {
|
|
19057
|
+
const carried = memory?.projection.goal?.text;
|
|
19058
|
+
if (carried && !isContinuationPrompt(carried)) {
|
|
19059
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19060
|
+
intentContext.user_prompt = carried;
|
|
19061
|
+
logEvent("goal_from_dossier", { chars: carried.length });
|
|
19062
|
+
}
|
|
19063
|
+
}
|
|
19064
|
+
if (goalPrompt.turnsBack > 0) {
|
|
19065
|
+
intentContext.continuation_prompt = latest.prompt;
|
|
19066
|
+
logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
|
|
19067
|
+
}
|
|
18624
19068
|
intentContext.session_id = latest.session_id || void 0;
|
|
18625
19069
|
intentContext.prompt_captured_at = latest.captured_at || void 0;
|
|
18626
19070
|
if (conversation.prompts.length > 1) {
|
|
@@ -18712,6 +19156,85 @@ async function runAnalyze(opts, globals) {
|
|
|
18712
19156
|
const response = result.data;
|
|
18713
19157
|
const decision = response.gate_decision ?? "(unrecognised)";
|
|
18714
19158
|
const sentPaths = codeDelta.files.map((f) => f.path);
|
|
19159
|
+
let openElsewhere = [];
|
|
19160
|
+
if (memorySession) {
|
|
19161
|
+
try {
|
|
19162
|
+
const st = foldDossier(memorySession.d);
|
|
19163
|
+
openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
|
|
19164
|
+
try {
|
|
19165
|
+
const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
|
|
19166
|
+
const at = src[line - 1];
|
|
19167
|
+
return at === void 0 ? null : lineSha(at);
|
|
19168
|
+
} catch {
|
|
19169
|
+
return null;
|
|
19170
|
+
}
|
|
19171
|
+
});
|
|
19172
|
+
} catch {
|
|
19173
|
+
}
|
|
19174
|
+
}
|
|
19175
|
+
const reviewCoverage = {
|
|
19176
|
+
reviewed: sentPaths,
|
|
19177
|
+
// Declared drops from the stages that DO report themselves today. The other
|
|
19178
|
+
// stages surface via `unaccounted`, which is the tripwire, not the design.
|
|
19179
|
+
notReviewed: [
|
|
19180
|
+
// Every exit from the collection loop, each named. Six reasons where there
|
|
19181
|
+
// used to be two recorded and four silent — the silent ones including the
|
|
19182
|
+
// per-file size cap, which could drop a whole source file without leaving a
|
|
19183
|
+
// trace anywhere in the payload or the run row.
|
|
19184
|
+
...codeDelta.excluded,
|
|
19185
|
+
// The server-side 300-line middle-out truncation. It only bites on the
|
|
19186
|
+
// full-file branch (a first analysis, before snapshots exist) because
|
|
19187
|
+
// analyze normally sends diffs — but on that branch the reviewer sees the
|
|
19188
|
+
// first and last 100 lines and nothing between, and until now said so to
|
|
19189
|
+
// nobody. CAPACITY: a partial look is not a look.
|
|
19190
|
+
...(response.metadata?.truncated_files ?? []).map((path) => ({
|
|
19191
|
+
path,
|
|
19192
|
+
reason: "file-middle-truncated-300-lines",
|
|
19193
|
+
stage: "prompt-builder",
|
|
19194
|
+
kind: "capacity"
|
|
19195
|
+
})),
|
|
19196
|
+
// The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
|
|
19197
|
+
// what is REVIEWED, not merely what is summarised — a session editing 25
|
|
19198
|
+
// files had five silently excluded from the reviewed set.
|
|
19199
|
+
...(actionSummary?.capped_out ?? []).map((path) => ({
|
|
19200
|
+
path,
|
|
19201
|
+
reason: "edit-list-cap-20",
|
|
19202
|
+
stage: "extractActionSummary",
|
|
19203
|
+
kind: "capacity"
|
|
19204
|
+
})),
|
|
19205
|
+
// ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
|
|
19206
|
+
//
|
|
19207
|
+
// The universe is `allChanged`, git's whole dirty tree. The reviewed set is
|
|
19208
|
+
// scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
|
|
19209
|
+
// every pre-existing dirty file is in the universe, absent from `reviewed`,
|
|
19210
|
+
// and — until now — declared by nobody. It fell through to `unaccounted`,
|
|
19211
|
+
// became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
|
|
19212
|
+
// that was never this session's to review.
|
|
19213
|
+
//
|
|
19214
|
+
// Measured 2026-08-04: three consecutive runs over an untouched tree gave
|
|
19215
|
+
// three different answers — .claude/settings.json, then admin.js, then six
|
|
19216
|
+
// files — because each run took a different path and each path had a
|
|
19217
|
+
// different idea of the universe. POLICY: not this session's work is not a
|
|
19218
|
+
// coverage gap, it is the cure working.
|
|
19219
|
+
...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) => ({
|
|
19220
|
+
path,
|
|
19221
|
+
reason: "not-authored-this-session",
|
|
19222
|
+
stage: "baseline-scoping",
|
|
19223
|
+
kind: "policy"
|
|
19224
|
+
})),
|
|
19225
|
+
// The extension allowlist, and it is POLICY rather than capacity: a changed
|
|
19226
|
+
// README was never going to be reviewed, and treating that as a coverage
|
|
19227
|
+
// gap would downgrade nearly every PASS to WARN until WARN meant nothing.
|
|
19228
|
+
// Recorded so the ledger balances and so "what did Verity ignore entirely"
|
|
19229
|
+
// is answerable — but it never touches the verdict.
|
|
19230
|
+
...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
|
|
19231
|
+
path,
|
|
19232
|
+
reason: "not-a-reviewed-file-type",
|
|
19233
|
+
stage: "extension-allowlist",
|
|
19234
|
+
kind: "policy"
|
|
19235
|
+
}))
|
|
19236
|
+
]
|
|
19237
|
+
};
|
|
18715
19238
|
const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
|
|
18716
19239
|
const watermarkIsPartial = !!codeDelta.truncated;
|
|
18717
19240
|
let silenced = null;
|
|
@@ -18742,6 +19265,13 @@ async function runAnalyze(opts, globals) {
|
|
|
18742
19265
|
});
|
|
18743
19266
|
}
|
|
18744
19267
|
let intentRepeatCount = 0;
|
|
19268
|
+
const priorPendingFingerprints = memorySession ? (() => {
|
|
19269
|
+
try {
|
|
19270
|
+
return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
|
|
19271
|
+
} catch {
|
|
19272
|
+
return [];
|
|
19273
|
+
}
|
|
19274
|
+
})() : [];
|
|
18745
19275
|
if (memorySession) {
|
|
18746
19276
|
try {
|
|
18747
19277
|
recordVerdict(memorySession.d, {
|
|
@@ -18764,7 +19294,10 @@ async function runAnalyze(opts, globals) {
|
|
|
18764
19294
|
// What next turn reads as `emittedLast`. A suppressed turn did not
|
|
18765
19295
|
// speak, so it cannot be the cause of the turn after it — which is what
|
|
18766
19296
|
// keeps this from becoming a permanent gag.
|
|
18767
|
-
emitted: !silenced
|
|
19297
|
+
emitted: !silenced,
|
|
19298
|
+
// Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
|
|
19299
|
+
// carry no `pattern_id`, so their content is the only available key.
|
|
19300
|
+
pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
|
|
18768
19301
|
});
|
|
18769
19302
|
intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
|
|
18770
19303
|
} catch {
|
|
@@ -18864,9 +19397,41 @@ async function runAnalyze(opts, globals) {
|
|
|
18864
19397
|
reverify_by: response.reverify_by
|
|
18865
19398
|
});
|
|
18866
19399
|
const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
|
|
18867
|
-
|
|
19400
|
+
let capReleased = false;
|
|
19401
|
+
let effectiveDecision = decision;
|
|
19402
|
+
if (decision === "FAIL") {
|
|
19403
|
+
const blocking = (response.findings ?? []).filter((f) => {
|
|
19404
|
+
const sev = String(f.severity ?? "").toLowerCase();
|
|
19405
|
+
return sev === "critical" || sev === "high";
|
|
19406
|
+
});
|
|
19407
|
+
const fingerprint = findingsFingerprint(blocking);
|
|
19408
|
+
const prior = readIterationState(currentCommit);
|
|
19409
|
+
const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
|
|
19410
|
+
const nextIteration = sameProblem ? prior.iteration + 1 : 1;
|
|
19411
|
+
const maxIterations = parseInt(opts.maxIterations, 10);
|
|
19412
|
+
writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
|
|
19413
|
+
iteration = nextIteration;
|
|
19414
|
+
if (nextIteration > maxIterations) {
|
|
19415
|
+
capReleased = true;
|
|
19416
|
+
effectiveDecision = "WARN";
|
|
19417
|
+
logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
|
|
19418
|
+
}
|
|
19419
|
+
}
|
|
19420
|
+
if (capReleased) {
|
|
19421
|
+
const findings = response.findings ?? [];
|
|
19422
|
+
const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
|
|
19423
|
+
emitVerdict({
|
|
19424
|
+
proposed: "WARN",
|
|
19425
|
+
changed: skipCoverageChanged,
|
|
19426
|
+
coverage: reviewCoverage,
|
|
19427
|
+
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.
|
|
19428
|
+
${lines.join("\n")}`,
|
|
19429
|
+
agentContext: null,
|
|
19430
|
+
silenced: true
|
|
19431
|
+
});
|
|
19432
|
+
}
|
|
19433
|
+
switch (effectiveDecision) {
|
|
18868
19434
|
case "FAIL": {
|
|
18869
|
-
writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
|
|
18870
19435
|
const assessment = response.assessment;
|
|
18871
19436
|
const narrative = assessment?.narrative ?? "";
|
|
18872
19437
|
const findings = response.findings ?? [];
|
|
@@ -18943,7 +19508,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
|
|
|
18943
19508
|
if (grantNudge) process.stderr.write(`
|
|
18944
19509
|
${YELLOW}${grantNudge.trim()}${NC}
|
|
18945
19510
|
`);
|
|
18946
|
-
|
|
19511
|
+
emitVerdict({
|
|
19512
|
+
proposed: "FAIL",
|
|
19513
|
+
changed: skipCoverageChanged,
|
|
19514
|
+
coverage: reviewCoverage,
|
|
19515
|
+
userSummary: "",
|
|
19516
|
+
// Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
|
|
19517
|
+
// the findings themselves are rendered above by the blocking renderer,
|
|
19518
|
+
// so what the cut removes is the repeated commentary, never the defect.
|
|
19519
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19520
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19521
|
+
silenced: !!silenced,
|
|
19522
|
+
openElsewhere
|
|
19523
|
+
});
|
|
18947
19524
|
break;
|
|
18948
19525
|
}
|
|
18949
19526
|
case "PASS": {
|
|
@@ -18955,10 +19532,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18955
19532
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18956
19533
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18957
19534
|
userSummary += loginNudge + grantNudge;
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
19535
|
+
emitVerdict({
|
|
19536
|
+
proposed: "PASS",
|
|
19537
|
+
changed: skipCoverageChanged,
|
|
19538
|
+
coverage: reviewCoverage,
|
|
19539
|
+
userSummary,
|
|
19540
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19541
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19542
|
+
silenced: !!silenced,
|
|
19543
|
+
openElsewhere
|
|
19544
|
+
});
|
|
18962
19545
|
break;
|
|
18963
19546
|
}
|
|
18964
19547
|
case "WARN": {
|
|
@@ -18969,10 +19552,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
|
|
|
18969
19552
|
if (viewUrl) userSummary += ` Report: ${viewUrl}`;
|
|
18970
19553
|
if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
|
|
18971
19554
|
userSummary += loginNudge + grantNudge;
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
19555
|
+
emitVerdict({
|
|
19556
|
+
proposed: "WARN",
|
|
19557
|
+
changed: skipCoverageChanged,
|
|
19558
|
+
coverage: reviewCoverage,
|
|
19559
|
+
userSummary,
|
|
19560
|
+
agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
|
|
19561
|
+
// The coverage note is silenced with it — half a channel is still a channel.
|
|
19562
|
+
silenced: !!silenced,
|
|
19563
|
+
openElsewhere
|
|
19564
|
+
});
|
|
18976
19565
|
break;
|
|
18977
19566
|
}
|
|
18978
19567
|
default: {
|
|
@@ -19085,8 +19674,8 @@ async function runReview(opts, globals) {
|
|
|
19085
19674
|
for (const p of specPaths) {
|
|
19086
19675
|
if (!(0, import_node_fs26.existsSync)(p)) continue;
|
|
19087
19676
|
try {
|
|
19088
|
-
const { readFileSync:
|
|
19089
|
-
const content =
|
|
19677
|
+
const { readFileSync: readFileSync16 } = await import("node:fs");
|
|
19678
|
+
const content = readFileSync16(p, "utf-8");
|
|
19090
19679
|
specs.push({ path: p, content: content.slice(0, 10240) });
|
|
19091
19680
|
} catch {
|
|
19092
19681
|
}
|
|
@@ -19889,6 +20478,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19889
20478
|
return "handled";
|
|
19890
20479
|
}
|
|
19891
20480
|
if (!who.ok) {
|
|
20481
|
+
const denial = authDenialRemedy(who.error);
|
|
20482
|
+
if (denial) {
|
|
20483
|
+
console.log("");
|
|
20484
|
+
printWarn(`Your existing Verity credential was rejected (${denial.code}).`);
|
|
20485
|
+
printInfo(` ${denial.remedy}`);
|
|
20486
|
+
return "drive-login";
|
|
20487
|
+
}
|
|
19892
20488
|
if (existing.data.userId != null) {
|
|
19893
20489
|
printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
|
|
19894
20490
|
} else {
|
|
@@ -19904,15 +20500,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
|
|
|
19904
20500
|
return "drive-login";
|
|
19905
20501
|
}
|
|
19906
20502
|
async function runOptionalAuth(resolution, opts = {}) {
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
}
|
|
20503
|
+
if (resolution.source === "default") {
|
|
20504
|
+
printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
|
|
20505
|
+
}
|
|
20506
|
+
const heal = await maybeHealServiceUrl(resolution, opts.verbose);
|
|
20507
|
+
const serviceUrl = heal.serviceUrl;
|
|
20508
|
+
const healed = heal.healed;
|
|
20509
|
+
if (healed) {
|
|
20510
|
+
printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
|
|
19916
20511
|
}
|
|
19917
20512
|
let remote = "";
|
|
19918
20513
|
try {
|
|
@@ -19930,8 +20525,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19930
20525
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
19931
20526
|
console.log("");
|
|
19932
20527
|
console.log(" Signing in is optional. What it does:");
|
|
19933
|
-
console.log(" - Confirms you
|
|
19934
|
-
console.log("
|
|
20528
|
+
console.log(" - Confirms which repositories you can write to. The GitHub token is");
|
|
20529
|
+
console.log(" used once for that check, then discarded \u2014 Verity never stores it.");
|
|
20530
|
+
console.log(" - One login covers every repository you can write to \u2014 other repos");
|
|
20531
|
+
console.log(" need no further sign-in on this machine.");
|
|
19935
20532
|
console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
|
|
19936
20533
|
console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
|
|
19937
20534
|
console.log(" - It is required to store and access run history for this repo");
|
|
@@ -19946,17 +20543,10 @@ async function runOptionalAuth(resolution, opts = {}) {
|
|
|
19946
20543
|
localOnlyNote();
|
|
19947
20544
|
return;
|
|
19948
20545
|
}
|
|
19949
|
-
if (!remote) {
|
|
19950
|
-
printWarn("No git remote found \u2014 cannot authenticate yet.");
|
|
19951
|
-
localOnlyNote();
|
|
19952
|
-
return;
|
|
19953
|
-
}
|
|
19954
|
-
const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
|
|
19955
20546
|
printInfo("Authenticating with GitHub\u2026");
|
|
19956
|
-
const result = await
|
|
20547
|
+
const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
|
|
19957
20548
|
if (result.ok) {
|
|
19958
|
-
|
|
19959
|
-
printInfo(`Logged in${who ? ` as ${who}` : ""} \u2713 \u2014 runs, history, and cloud memory now sync to Verity.`);
|
|
20549
|
+
await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: opts.verbose });
|
|
19960
20550
|
} else {
|
|
19961
20551
|
printWarn(`Authentication did not complete: ${result.error}`);
|
|
19962
20552
|
localOnlyNote();
|
|
@@ -20107,8 +20697,8 @@ function registerInitCommand(program2) {
|
|
|
20107
20697
|
console.log("");
|
|
20108
20698
|
try {
|
|
20109
20699
|
const globals = program2.opts();
|
|
20110
|
-
const
|
|
20111
|
-
await runOptionalAuth(
|
|
20700
|
+
const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
|
|
20701
|
+
await runOptionalAuth(resolution, {
|
|
20112
20702
|
token: globals.token,
|
|
20113
20703
|
verbose: globals.verbose
|
|
20114
20704
|
});
|
|
@@ -20772,7 +21362,7 @@ function registerTelemetryCommands(program2) {
|
|
|
20772
21362
|
}
|
|
20773
21363
|
|
|
20774
21364
|
// src/cli.ts
|
|
20775
|
-
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.
|
|
21365
|
+
program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.2cbb593").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
|
|
20776
21366
|
try {
|
|
20777
21367
|
await foldLegacyLocalCredential();
|
|
20778
21368
|
} catch {
|