@wrongstack/cli 0.305.0 → 0.306.0

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.
@@ -37,23 +37,24 @@ import {
37
37
  writeWebuiSessionChildReady
38
38
  } from "./chunk-3JPTNADJ.js";
39
39
  import {
40
- resolveActiveApiKey
41
- } from "./chunk-SZ42FYPT.js";
42
- import {
43
- activeProfileConfigPath
44
- } from "./chunk-YMXXOOFN.js";
40
+ CLI_VERSION
41
+ } from "./chunk-XJXDOF63.js";
45
42
  import {
46
43
  terminalLink,
47
44
  terminalText
48
45
  } from "./chunk-KE7E7DPX.js";
49
46
  import {
50
- CLI_VERSION
51
- } from "./chunk-XJXDOF63.js";
47
+ resolveActiveApiKey
48
+ } from "./chunk-SZ42FYPT.js";
49
+ import {
50
+ activeProfileConfigPath
51
+ } from "./chunk-YMXXOOFN.js";
52
52
  import "./chunk-7OCVIDC7.js";
53
53
 
54
54
  // src/execution.ts
55
55
  import * as path13 from "node:path";
56
56
  import { effectiveFallbackChain as effectiveFallbackChain2, setQueuedMessagesSnapshot as setQueuedMessagesSnapshot2 } from "@wrongstack/core/agent";
57
+ import { updateReviewReportEvidence } from "@wrongstack/core/plugin";
57
58
  import { attachTodosCheckpoint as attachTodosCheckpoint2 } from "@wrongstack/core/storage";
58
59
  import { normalizeTokenSavingTier as normalizeTokenSavingTier2 } from "@wrongstack/core/types";
59
60
  import { mergeCustomModelDefs } from "@wrongstack/core/utils";
@@ -1873,24 +1874,9 @@ function createSettingsAdapter(ctx) {
1873
1874
  // src/boot/tui-theme-adapter.ts
1874
1875
  import * as fs4 from "node:fs/promises";
1875
1876
  import * as path9 from "node:path";
1877
+ import { THEME_PRESET_IDS } from "@wrongstack/core/types";
1876
1878
  import { atomicWrite as atomicWrite2 } from "@wrongstack/core/utils";
1877
- var VALID_PRESETS = /* @__PURE__ */ new Set([
1878
- "catppuccin",
1879
- "tokyo-night",
1880
- "nord",
1881
- "cyberpunk",
1882
- "dracula",
1883
- "gruvbox-dark",
1884
- "solarized-dark",
1885
- "one-dark",
1886
- "monokai",
1887
- "rose-pine",
1888
- "kanagawa",
1889
- "ayu-dark",
1890
- "everforest",
1891
- "night-owl",
1892
- "synthwave"
1893
- ]);
1879
+ var VALID_PRESETS = new Set(THEME_PRESET_IDS);
1894
1880
  function createThemeAdapter({ configStore, wpaths }) {
1895
1881
  return {
1896
1882
  getThemePreset: () => {
@@ -2761,6 +2747,128 @@ import * as fsp from "node:fs/promises";
2761
2747
  import * as path10 from "node:path";
2762
2748
  import { emitReviewIfChanged } from "@wrongstack/core/plugin";
2763
2749
 
2750
+ // src/chimera-cascade-evidence.ts
2751
+ import { spawn as spawn2 } from "node:child_process";
2752
+ var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
2753
+ function extractCascadeEvidence(text) {
2754
+ if (!text) return null;
2755
+ for (const match of text.matchAll(JSON_BLOCK_RE)) {
2756
+ const body = match[1]?.trim();
2757
+ if (!body) continue;
2758
+ let parsed;
2759
+ try {
2760
+ parsed = JSON.parse(body);
2761
+ } catch {
2762
+ continue;
2763
+ }
2764
+ const evidence = parseEvidenceObject(parsed);
2765
+ if (evidence) return evidence;
2766
+ }
2767
+ return null;
2768
+ }
2769
+ function parseEvidenceObject(parsed) {
2770
+ if (typeof parsed !== "object" || parsed === null) return null;
2771
+ const root = parsed;
2772
+ const raw = root.verification_evidence;
2773
+ if (typeof raw !== "object" || raw === null) return null;
2774
+ const checks = raw;
2775
+ const out = {};
2776
+ let found = false;
2777
+ for (const key of ["typecheck", "lint", "tests"]) {
2778
+ const check = parseSingleCheck(checks[key]);
2779
+ if (check) {
2780
+ out[key] = check;
2781
+ found = true;
2782
+ }
2783
+ }
2784
+ return found ? out : null;
2785
+ }
2786
+ function parseSingleCheck(raw) {
2787
+ if (typeof raw !== "object" || raw === null) return void 0;
2788
+ const check = raw;
2789
+ const command = check.command;
2790
+ const exitCode = check.exitCode;
2791
+ if (typeof command !== "string" || command.trim().length === 0) return void 0;
2792
+ if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
2793
+ return void 0;
2794
+ }
2795
+ const output = typeof check.output === "string" ? truncateOutput(check.output) : void 0;
2796
+ return { command: command.trim(), exitCode, output };
2797
+ }
2798
+ var MAX_EVIDENCE_OUTPUT_CHARS = 2e3;
2799
+ function truncateOutput(output) {
2800
+ return output.length > MAX_EVIDENCE_OUTPUT_CHARS ? `${output.slice(0, MAX_EVIDENCE_OUTPUT_CHARS)}
2801
+ \u2026 (truncated)` : output;
2802
+ }
2803
+ var SAFE_EXECUTABLES = /* @__PURE__ */ new Set([
2804
+ "pnpm",
2805
+ "npm",
2806
+ "yarn",
2807
+ "npx",
2808
+ "bun",
2809
+ "node",
2810
+ "tsc",
2811
+ "biome",
2812
+ "eslint",
2813
+ "vitest",
2814
+ "jest"
2815
+ ]);
2816
+ var CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS = 12e4;
2817
+ var CASCADE_EVIDENCE_UNSAFE_EXIT = 126;
2818
+ var CASCADE_EVIDENCE_RUN_ERROR_EXIT = 127;
2819
+ function isSafeCascadeCommand(command) {
2820
+ if (!command || command.length > 500) return false;
2821
+ const tokens = command.trim().split(/\s+/);
2822
+ if (tokens.length === 0) return false;
2823
+ if (!SAFE_EXECUTABLES.has(tokens[0])) return false;
2824
+ return tokens.every((token) => /^[A-Za-z0-9@/._:=~+*?\[\]-]+$/.test(token));
2825
+ }
2826
+ var runCascadeVerificationCommand = (command, cwd, timeoutMs) => {
2827
+ if (!isSafeCascadeCommand(command)) {
2828
+ return Promise.resolve({ exitCode: CASCADE_EVIDENCE_UNSAFE_EXIT });
2829
+ }
2830
+ const tokens = command.trim().split(/\s+/);
2831
+ const executable = tokens[0];
2832
+ const args = tokens.slice(1);
2833
+ return new Promise((resolve3) => {
2834
+ let child;
2835
+ try {
2836
+ child = spawn2(executable, args, {
2837
+ cwd,
2838
+ stdio: "ignore",
2839
+ windowsHide: true,
2840
+ signal: AbortSignal.timeout(timeoutMs)
2841
+ });
2842
+ } catch {
2843
+ resolve3({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
2844
+ return;
2845
+ }
2846
+ child.on("error", () => resolve3({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
2847
+ child.on("close", (code) => resolve3({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
2848
+ });
2849
+ };
2850
+ async function verifyCascadeEvidence(evidence, cwd, runCommand = runCascadeVerificationCommand, timeoutMs = CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS) {
2851
+ if (!evidence) return { status: "missing", checks: [] };
2852
+ if (!evidence.typecheck) return { status: "failed", checks: [] };
2853
+ const checks = [];
2854
+ for (const name of ["typecheck", "lint", "tests"]) {
2855
+ const claimed = evidence[name];
2856
+ if (!claimed) continue;
2857
+ const actual = await runCommand(claimed.command, cwd, timeoutMs);
2858
+ const ok = claimed.exitCode === 0 && actual.exitCode === claimed.exitCode;
2859
+ checks.push({
2860
+ name,
2861
+ command: claimed.command,
2862
+ claimedExitCode: claimed.exitCode,
2863
+ actualExitCode: actual.exitCode,
2864
+ ok
2865
+ });
2866
+ }
2867
+ if (checks.length === 0) return { status: "missing", checks };
2868
+ const status = checks.every((c) => c.ok) ? "verified" : "failed";
2869
+ return { status, checks };
2870
+ }
2871
+
2764
2872
  // src/chimera-review-task.ts
2765
2873
  import { parseReviewSeverity } from "@wrongstack/core/plugin";
2766
2874
  function truncateAtCodePointBoundary(text, maxCodeUnits) {
@@ -2861,6 +2969,34 @@ function buildChimeraReviewTaskDescription(p) {
2861
2969
  lines.push("structured review report.");
2862
2970
  return lines.join("\n");
2863
2971
  }
2972
+ var CASCADE_EVIDENCE_INSTRUCTIONS = [
2973
+ `After fixing, run the project's REAL verification commands on the code you changed`,
2974
+ `(typecheck, lint, and the tests covering the fixed code) and return a machine-evidence`,
2975
+ `block as the LAST section of your response. The block MUST be a fenced JSON block`,
2976
+ `(exactly \`\`\`json ... \`\`\`) with this shape:`,
2977
+ ``,
2978
+ "```json",
2979
+ "{",
2980
+ ' "verification_evidence": {',
2981
+ ' "typecheck": { "command": "pnpm typecheck", "exitCode": 0 },',
2982
+ ' "lint": { "command": "pnpm lint", "exitCode": 0 },',
2983
+ ' "tests": { "command": "pnpm test --filter affected", "exitCode": 0 }',
2984
+ " }",
2985
+ "}",
2986
+ "```",
2987
+ ``,
2988
+ `Rules:`,
2989
+ `- \`command\` must be the EXACT command you ran (plain executable + args, no shell`,
2990
+ ` chaining like \`&&\`/\`;\`/\`|\`, no redirection, no shell expansion). These commands are`,
2991
+ ` re-run verbatim by the orchestrator to verify your work.`,
2992
+ `- \`exitCode\` is the real process exit code you observed: 0 = passed, non-zero = failed.`,
2993
+ ` Never invent or omit an exit code. If a command cannot be run, omit that key entirely`,
2994
+ ` and explain why in your report text \u2014 an omitted key is honest, a fabricated 0 is not.`,
2995
+ `- If your fix does not pass a command, report the true non-zero exit code; the`,
2996
+ ` orchestrator will treat the fix as unverified and keep the finding open.`,
2997
+ `- At least \`typecheck\` must be present; include \`lint\` and \`tests\` when the project`,
2998
+ ` has them.`
2999
+ ].join("\n");
2864
3000
  function buildChimeraCascadeTaskDescription(agentKind, p) {
2865
3001
  const fileList = p.bundle.files.map((f) => `- ${f.path}`).join("\n");
2866
3002
  const reportSlice = truncateAtCodePointBoundary(p.reviewText, 12e3);
@@ -2881,8 +3017,9 @@ function buildChimeraCascadeTaskDescription(agentKind, p) {
2881
3017
  `Investigate the security findings above. Read the flagged files, confirm or refute`,
2882
3018
  `each finding, and **apply fixes** for confirmed vulnerabilities using the edit tool.`,
2883
3019
  `Use severity (Critical/High/Medium), file:line citations, and remediation steps.`,
2884
- `After fixing, run the project's typecheck and linter to verify. If a finding is a`,
2885
- `false positive, say so and do not modify the file.`
3020
+ `If a finding is a false positive, say so and do not modify the file.`,
3021
+ ``,
3022
+ CASCADE_EVIDENCE_INSTRUCTIONS
2886
3023
  ].join("\n");
2887
3024
  }
2888
3025
  return [
@@ -2899,9 +3036,10 @@ function buildChimeraCascadeTaskDescription(agentKind, p) {
2899
3036
  ``,
2900
3037
  `Hunt for the bugs flagged above. Read the affected files, trace each finding to its`,
2901
3038
  `root cause, and **apply minimal fixes** for confirmed bugs using the edit tool.`,
2902
- `Use severity (Critical/High/Medium), file:line citations. After fixing, run the`,
2903
- `project's typecheck and linter to verify. If a finding is a false positive, say so`,
2904
- `and do not modify the file.`
3039
+ `Use severity (Critical/High/Medium), file:line citations. If a finding is a false`,
3040
+ `positive, say so and do not modify the file.`,
3041
+ ``,
3042
+ CASCADE_EVIDENCE_INSTRUCTIONS
2905
3043
  ].join("\n");
2906
3044
  }
2907
3045
 
@@ -3018,7 +3156,9 @@ function installChimeraCascadeHandler({
3018
3156
  session,
3019
3157
  getPendingWork,
3020
3158
  trackWork,
3021
- buildLadder
3159
+ buildLadder,
3160
+ verifyEvidence = verifyCascadeEvidence,
3161
+ persistEvidence
3022
3162
  }) {
3023
3163
  events.onPattern("chimera.cascade_needed", (_event, payload) => {
3024
3164
  const p = payload;
@@ -3031,6 +3171,7 @@ function installChimeraCascadeHandler({
3031
3171
  await previousWork;
3032
3172
  } catch {
3033
3173
  }
3174
+ const agentEvidence = [];
3034
3175
  for (const agentKind of p.agents) {
3035
3176
  try {
3036
3177
  const taskDesc = buildChimeraCascadeTaskDescription(agentKind, p);
@@ -3074,6 +3215,10 @@ function installChimeraCascadeHandler({
3074
3215
  const result = outcome.result;
3075
3216
  if (result?.status === "success") {
3076
3217
  const resultText = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
3218
+ const claimed = extractCascadeEvidence(resultText);
3219
+ if (claimed) {
3220
+ agentEvidence.push({ agentKind, evidence: claimed });
3221
+ }
3077
3222
  await session.append({
3078
3223
  type: "llm_response",
3079
3224
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3104,7 +3249,21 @@ function installChimeraCascadeHandler({
3104
3249
  });
3105
3250
  }
3106
3251
  }
3107
- await maybeReReviewCascade({ events, session, bundle: p.bundle });
3252
+ const accumulatedEvidence = {};
3253
+ for (const { evidence } of agentEvidence) {
3254
+ for (const name of ["typecheck", "lint", "tests"]) {
3255
+ if (evidence[name]) accumulatedEvidence[name] = evidence[name];
3256
+ }
3257
+ }
3258
+ await maybeReReviewCascade({
3259
+ events,
3260
+ session,
3261
+ bundle: p.bundle,
3262
+ reportId: p.reportId,
3263
+ claimedEvidence: accumulatedEvidence,
3264
+ verifyEvidence,
3265
+ persistEvidence
3266
+ });
3108
3267
  })();
3109
3268
  trackWork(pendingWork);
3110
3269
  });
@@ -3112,16 +3271,71 @@ function installChimeraCascadeHandler({
3112
3271
  async function maybeReReviewCascade({
3113
3272
  events,
3114
3273
  session,
3115
- bundle
3274
+ bundle,
3275
+ reportId,
3276
+ claimedEvidence,
3277
+ verifyEvidence,
3278
+ persistEvidence
3116
3279
  }) {
3117
3280
  const maxDepth = bundle.maxCascadeDepth ?? 0;
3118
3281
  const currentDepth = bundle.cascadeDepth ?? 0;
3282
+ const hasClaimedEvidence = Object.keys(claimedEvidence).length > 0;
3283
+ const verification = hasClaimedEvidence ? await verifyEvidence(
3284
+ claimedEvidence,
3285
+ bundle.cwd,
3286
+ void 0,
3287
+ CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS
3288
+ ) : { status: "missing", checks: [] };
3289
+ const evidenceBundle = {
3290
+ ...bundle,
3291
+ evidenceStatus: verification.status,
3292
+ evidenceChecks: verification.checks
3293
+ };
3294
+ if (reportId && persistEvidence) {
3295
+ try {
3296
+ await persistEvidence(reportId, verification.status, verification.checks);
3297
+ } catch (error) {
3298
+ await session.append({
3299
+ type: "error",
3300
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3301
+ message: `Chimera cascade evidence persistence failed: ${errorText2(error)}`,
3302
+ phase: "agent"
3303
+ });
3304
+ }
3305
+ }
3306
+ await session.append({
3307
+ type: "llm_response",
3308
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3309
+ content: [
3310
+ {
3311
+ type: "text",
3312
+ text: renderEvidenceSummary(verification.status, verification.checks)
3313
+ }
3314
+ ],
3315
+ stopReason: "end_turn",
3316
+ usage: { input: 0, output: 0 }
3317
+ });
3318
+ if (verification.status !== "verified") {
3319
+ await session.append({
3320
+ type: "llm_response",
3321
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3322
+ content: [
3323
+ {
3324
+ type: "text",
3325
+ text: `\u{1F982} Chimera cascade evidence ${verification.status} \u2014 fix not confirmed, skipping re-review. Findings remain open.`
3326
+ }
3327
+ ],
3328
+ stopReason: "end_turn",
3329
+ usage: { input: 0, output: 0 }
3330
+ });
3331
+ return;
3332
+ }
3119
3333
  if (maxDepth > 0 && currentDepth < maxDepth) {
3120
3334
  try {
3121
3335
  const reReadFiles = [];
3122
- for (const f of bundle.files) {
3336
+ for (const f of evidenceBundle.files) {
3123
3337
  try {
3124
- const absPath = path10.join(bundle.cwd, f.path);
3338
+ const absPath = path10.join(evidenceBundle.cwd, f.path);
3125
3339
  const content = await fsp.readFile(absPath, "utf8");
3126
3340
  reReadFiles.push({ path: f.path, status: "modified", content });
3127
3341
  } catch {
@@ -3129,7 +3343,7 @@ async function maybeReReviewCascade({
3129
3343
  }
3130
3344
  if (reReadFiles.length > 0) {
3131
3345
  const reReviewBundle = {
3132
- ...bundle,
3346
+ ...evidenceBundle,
3133
3347
  files: reReadFiles,
3134
3348
  cascadeDepth: currentDepth + 1
3135
3349
  };
@@ -3187,18 +3401,28 @@ async function maybeReReviewCascade({
3187
3401
  });
3188
3402
  }
3189
3403
  }
3404
+ function renderEvidenceSummary(status, checks) {
3405
+ if (status === "missing") return "no machine evidence supplied";
3406
+ const summary = checks.map((check) => `${check.name}:${check.ok ? "pass" : "fail"}(${check.actualExitCode})`).join(", ");
3407
+ return `${status}${summary ? ` \u2014 ${summary}` : ""}`;
3408
+ }
3409
+ function errorText2(error) {
3410
+ return error instanceof Error ? error.message : String(error);
3411
+ }
3190
3412
 
3191
3413
  // src/execution-chimera-review.ts
3192
3414
  import { randomUUID as randomUUID3 } from "node:crypto";
3193
3415
  import path11 from "node:path";
3194
- import { effectiveFallbackChain } from "@wrongstack/core/agent";
3416
+ import { effectiveFallbackChain, fallbackProfileChain as fallbackProfileChain2 } from "@wrongstack/core/agent";
3195
3417
  import {
3196
3418
  CHIMERA_REVIEW_PROMPT,
3419
+ classifyChimeraReviewSource,
3197
3420
  integrateFindings,
3198
3421
  maybeCompactReviewStores,
3199
3422
  parseChimeraReviewReport,
3200
3423
  persistReviewReport,
3201
- recordCompletedReview
3424
+ recordCompletedReview,
3425
+ verifyFindingsAgainstDisk
3202
3426
  } from "@wrongstack/core/plugin";
3203
3427
  function normalizeFileKeyForCitation(raw, cwd) {
3204
3428
  const forward = raw.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -3282,7 +3506,13 @@ function installChimeraReviewHandler({
3282
3506
  const rawModel = p.reviewFallbackModels ? p.config.model?.trim() || void 0 : tModel;
3283
3507
  const baseProvider = rawProvider || tProvider || config.provider;
3284
3508
  const baseModel = rawModel || tModel || config.model;
3285
- const baseFallbacks = p.reviewFallbackModels ? [...p.reviewFallbackModels] : resolveReviewerFallbackModels(void 0);
3509
+ const configFallbacks = [...p.config.fallbackModels ?? []];
3510
+ if (p.config.fallbackProfile) {
3511
+ configFallbacks.push(...fallbackProfileChain2(config, p.config.fallbackProfile));
3512
+ }
3513
+ const baseFallbacks = p.reviewFallbackModels ? [...p.reviewFallbackModels] : resolveReviewerFallbackModels(
3514
+ configFallbacks.length > 0 ? configFallbacks : void 0
3515
+ );
3286
3516
  const assigned = assignReviewerModels(
3287
3517
  baseProvider,
3288
3518
  baseModel,
@@ -3383,18 +3613,34 @@ function installChimeraReviewHandler({
3383
3613
  }
3384
3614
  const reviewText = typeof result.result === "string" ? result.result.trim() : JSON.stringify(result.result);
3385
3615
  const reportId = randomUUID3();
3616
+ const reviewSource = classifyChimeraReviewSource(p);
3617
+ const winningAttempt = outcome.wonAt ? ladder[outcome.wonAt - 1] : void 0;
3618
+ const parsedReport = parseChimeraReviewReport(reviewText, {
3619
+ sessionId: reviewSessionId,
3620
+ agentId: p.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review",
3621
+ reviewerModel: winningAttempt ? attemptLabel(winningAttempt) : void 0,
3622
+ reviewType: reviewSource,
3623
+ reportId
3624
+ });
3625
+ const verifiedFindings = await verifyFindingsAgainstDisk(parsedReport.findings, {
3626
+ cwd: p.cwd
3627
+ });
3628
+ const verifiedParsedReport = {
3629
+ ...parsedReport,
3630
+ findings: verifiedFindings
3631
+ };
3386
3632
  await persistAndEmitCompletion({
3387
3633
  bundle: p,
3388
3634
  reviewText,
3389
3635
  status: "success",
3390
3636
  cwd: p.cwd,
3391
3637
  sessionId: reviewSessionId,
3392
- reportId
3638
+ reportId,
3639
+ parsedReport: verifiedParsedReport
3393
3640
  });
3394
3641
  if (reviewText) {
3395
3642
  const reviewHasFindings = !isChimeraAllClearReview(reviewText);
3396
- const parsedReview = parseChimeraReviewReport(reviewText);
3397
- const citedFindings = parsedReview.findings.filter((finding) => finding.location);
3643
+ const citedFindings = verifiedFindings.filter((finding) => finding.location);
3398
3644
  const citedPaths = new Set(
3399
3645
  p.files.map((file) => normalizeFileKeyForCitation(file.path, p.cwd))
3400
3646
  );
@@ -3406,14 +3652,28 @@ function installChimeraReviewHandler({
3406
3652
  if (bucket) bucket.push(normalized);
3407
3653
  else changedBasenameIndex.set(basename7, [normalized]);
3408
3654
  }
3409
- const droppedCitations = citedFindings.filter((finding) => {
3655
+ const inScopeCited = citedFindings.filter((finding) => {
3410
3656
  const citedKey = normalizeFileKeyForCitation(finding.location.file, p.cwd);
3411
- if (citedPaths.has(citedKey)) return false;
3657
+ if (citedPaths.has(citedKey)) return true;
3412
3658
  const basename7 = path11.basename(finding.location.file).toLowerCase();
3413
3659
  const bucket = changedBasenameIndex.get(basename7);
3414
- return !(bucket && bucket.length === 1);
3660
+ return !!(bucket && bucket.length === 1);
3415
3661
  });
3416
- const effectiveHasFindings = reviewHasFindings && (parsedReview.findings.length === 0 || parsedReview.findings.length > droppedCitations.length);
3662
+ const droppedCitations = citedFindings.filter(
3663
+ (finding) => !inScopeCited.includes(finding)
3664
+ );
3665
+ const actionableFindings = inScopeCited.filter(
3666
+ (finding) => finding.verification?.status === "verified"
3667
+ );
3668
+ const unverifiedInScope = inScopeCited.filter(
3669
+ (finding) => finding.verification?.status === "unverified"
3670
+ );
3671
+ const failedVerificationCount = inScopeCited.filter(
3672
+ (finding) => finding.verification?.status === "failed"
3673
+ ).length;
3674
+ const effectiveHasFindings = reviewHasFindings && actionableFindings.length > 0;
3675
+ const verifiedCount = actionableFindings.length;
3676
+ const unverifiedCount = unverifiedInScope.length;
3417
3677
  const hallucinationNote = droppedCitations.length > 0 ? `
3418
3678
 
3419
3679
  \u26A0\uFE0F Citation validation: ${droppedCitations.length} finding(s) cite files outside the changed-file set and should be verified before acting.` : "";
@@ -3456,7 +3716,7 @@ ${reviewBody}`;
3456
3716
  reviewLength: reviewText.length,
3457
3717
  messageId: reviewMailMessageId
3458
3718
  });
3459
- if (!effectiveHasFindings) {
3719
+ if (!reviewHasFindings) {
3460
3720
  try {
3461
3721
  await mailbox.ack({
3462
3722
  messageId: reviewMailMessageId,
@@ -3500,11 +3760,8 @@ ${reviewBody}`;
3500
3760
  phase: "agent"
3501
3761
  });
3502
3762
  }
3503
- const findingCount = Math.max(
3504
- 0,
3505
- parsedReview.findings.length - droppedCitations.length
3506
- );
3507
- const message = effectiveHasFindings ? `\u{1F982} Chimera report ready \u2014 ${findingCount || "unparsed"} potential finding(s) across ${p.files.length} file(s). No follow-up started; open the mailbox and explicitly ask the leader to act if wanted.` : `\u{1F982} Chimera report ready \u2014 ${p.files.length} file(s) checked, no actionable findings. No follow-up started.`;
3763
+ const findingCount = actionableFindings.length;
3764
+ const message = effectiveHasFindings ? `\u{1F982} Chimera report ready \u2014 ${findingCount || "unparsed"} potential finding(s) (${verifiedCount} verified against disk, ${unverifiedCount} unverified, ${failedVerificationCount} failed) across ${p.files.length} file(s). No follow-up started; open the mailbox and explicitly ask the leader to act if wanted.` : `\u{1F982} Chimera report ready \u2014 ${p.files.length} file(s) checked, no actionable findings. No follow-up started.`;
3508
3765
  events.emitCustom("chimera.report_available", {
3509
3766
  reportId,
3510
3767
  sessionId: reviewSessionId,
@@ -3726,7 +3983,7 @@ function parseSuggestionsFromOutput(finalText, todos) {
3726
3983
  setAutoSuggestions([]);
3727
3984
  return null;
3728
3985
  }
3729
- const { texts, autoTexts } = parseNextSteps(finalText, false);
3986
+ const { texts, autoTexts } = parseNextSteps(finalText);
3730
3987
  if (autoTexts.length > 0) {
3731
3988
  setAutoSuggestions(autoTexts);
3732
3989
  }
@@ -4924,6 +5181,9 @@ async function execute(deps) {
4924
5181
  session: { provider: config.provider, model: config.model }
4925
5182
  }),
4926
5183
  getPendingWork: () => chimeraWork.pending(),
5184
+ persistEvidence: async (reportId, status, checks) => {
5185
+ await updateReviewReportEvidence(reportId, wpaths.projectDir, status, checks);
5186
+ },
4927
5187
  trackWork: (work) => {
4928
5188
  chimeraWork.track(work);
4929
5189
  }
@@ -5362,4 +5622,4 @@ export {
5362
5622
  execute,
5363
5623
  resolveReviewerFallbackModels
5364
5624
  };
5365
- //# sourceMappingURL=execution-YMBD7YWC.js.map
5625
+ //# sourceMappingURL=execution-TTE7R5UY.js.map
@@ -1,3 +1,5 @@
1
+ import type { CascadeEvidenceCheckResult, CascadeEvidenceStatus } from '@wrongstack/core/plugin';
2
+ import { verifyCascadeEvidence } from './chimera-cascade-evidence.js';
1
3
  import type { ReviewerAttempt } from './chimera-reviewer-policy.js';
2
4
  import type { ExecuteDeps } from './execute-deps.js';
3
5
  type Director = NonNullable<ExecuteDeps['fleet']['director']>;
@@ -14,7 +16,11 @@ export type InstallChimeraCascadeHandlerOptions = {
14
16
  * holds the live config. Omit to keep the single unpinned spawn.
15
17
  */
16
18
  buildLadder?: (() => ReviewerAttempt[]) | undefined;
19
+ /** Test seam; production defaults to the real safe command verifier. */
20
+ verifyEvidence?: typeof verifyCascadeEvidence | undefined;
21
+ /** Persist every evidence verdict on the source report, even when re-review stops. */
22
+ persistEvidence?: ((reportId: string, status: CascadeEvidenceStatus, checks: CascadeEvidenceCheckResult[]) => Promise<void>) | undefined;
17
23
  };
18
- export declare function installChimeraCascadeHandler({ events, director, session, getPendingWork, trackWork, buildLadder, }: InstallChimeraCascadeHandlerOptions): void;
24
+ export declare function installChimeraCascadeHandler({ events, director, session, getPendingWork, trackWork, buildLadder, verifyEvidence, persistEvidence, }: InstallChimeraCascadeHandlerOptions): void;
19
25
  export {};
20
26
  //# sourceMappingURL=execution-chimera-cascade.d.ts.map