@wrongstack/cli 0.305.1 → 0.306.2

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.
@@ -5,9 +5,10 @@ import {
5
5
  normalizeTuiThinkingWord,
6
6
  resolveActualTarget,
7
7
  resolvePersistPath,
8
+ runGit,
8
9
  setAutoSuggestions,
9
10
  setSuggestions
10
- } from "./chunk-V3XH6XBJ.js";
11
+ } from "./chunk-T6YAVFXA.js";
11
12
  import {
12
13
  startCliHqConnection
13
14
  } from "./chunk-FKWHSFX4.js";
@@ -37,23 +38,24 @@ import {
37
38
  writeWebuiSessionChildReady
38
39
  } from "./chunk-3JPTNADJ.js";
39
40
  import {
40
- resolveActiveApiKey
41
- } from "./chunk-SZ42FYPT.js";
42
- import {
43
- activeProfileConfigPath
44
- } from "./chunk-YMXXOOFN.js";
41
+ CLI_VERSION
42
+ } from "./chunk-XJXDOF63.js";
45
43
  import {
46
44
  terminalLink,
47
45
  terminalText
48
46
  } from "./chunk-KE7E7DPX.js";
49
47
  import {
50
- CLI_VERSION
51
- } from "./chunk-XJXDOF63.js";
48
+ resolveActiveApiKey
49
+ } from "./chunk-SZ42FYPT.js";
50
+ import {
51
+ activeProfileConfigPath
52
+ } from "./chunk-YMXXOOFN.js";
52
53
  import "./chunk-7OCVIDC7.js";
53
54
 
54
55
  // src/execution.ts
55
56
  import * as path13 from "node:path";
56
57
  import { effectiveFallbackChain as effectiveFallbackChain2, setQueuedMessagesSnapshot as setQueuedMessagesSnapshot2 } from "@wrongstack/core/agent";
58
+ import { updateReviewReportEvidence } from "@wrongstack/core/plugin";
57
59
  import { attachTodosCheckpoint as attachTodosCheckpoint2 } from "@wrongstack/core/storage";
58
60
  import { normalizeTokenSavingTier as normalizeTokenSavingTier2 } from "@wrongstack/core/types";
59
61
  import { mergeCustomModelDefs } from "@wrongstack/core/utils";
@@ -2746,6 +2748,128 @@ import * as fsp from "node:fs/promises";
2746
2748
  import * as path10 from "node:path";
2747
2749
  import { emitReviewIfChanged } from "@wrongstack/core/plugin";
2748
2750
 
2751
+ // src/chimera-cascade-evidence.ts
2752
+ import { spawn as spawn2 } from "node:child_process";
2753
+ var JSON_BLOCK_RE = /```json\s*([\s\S]*?)```/gi;
2754
+ function extractCascadeEvidence(text) {
2755
+ if (!text) return null;
2756
+ for (const match of text.matchAll(JSON_BLOCK_RE)) {
2757
+ const body = match[1]?.trim();
2758
+ if (!body) continue;
2759
+ let parsed;
2760
+ try {
2761
+ parsed = JSON.parse(body);
2762
+ } catch {
2763
+ continue;
2764
+ }
2765
+ const evidence = parseEvidenceObject(parsed);
2766
+ if (evidence) return evidence;
2767
+ }
2768
+ return null;
2769
+ }
2770
+ function parseEvidenceObject(parsed) {
2771
+ if (typeof parsed !== "object" || parsed === null) return null;
2772
+ const root = parsed;
2773
+ const raw = root.verification_evidence;
2774
+ if (typeof raw !== "object" || raw === null) return null;
2775
+ const checks = raw;
2776
+ const out = {};
2777
+ let found = false;
2778
+ for (const key of ["typecheck", "lint", "tests"]) {
2779
+ const check = parseSingleCheck(checks[key]);
2780
+ if (check) {
2781
+ out[key] = check;
2782
+ found = true;
2783
+ }
2784
+ }
2785
+ return found ? out : null;
2786
+ }
2787
+ function parseSingleCheck(raw) {
2788
+ if (typeof raw !== "object" || raw === null) return void 0;
2789
+ const check = raw;
2790
+ const command = check.command;
2791
+ const exitCode = check.exitCode;
2792
+ if (typeof command !== "string" || command.trim().length === 0) return void 0;
2793
+ if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
2794
+ return void 0;
2795
+ }
2796
+ const output = typeof check.output === "string" ? truncateOutput(check.output) : void 0;
2797
+ return { command: command.trim(), exitCode, output };
2798
+ }
2799
+ var MAX_EVIDENCE_OUTPUT_CHARS = 2e3;
2800
+ function truncateOutput(output) {
2801
+ return output.length > MAX_EVIDENCE_OUTPUT_CHARS ? `${output.slice(0, MAX_EVIDENCE_OUTPUT_CHARS)}
2802
+ \u2026 (truncated)` : output;
2803
+ }
2804
+ var SAFE_EXECUTABLES = /* @__PURE__ */ new Set([
2805
+ "pnpm",
2806
+ "npm",
2807
+ "yarn",
2808
+ "npx",
2809
+ "bun",
2810
+ "node",
2811
+ "tsc",
2812
+ "biome",
2813
+ "eslint",
2814
+ "vitest",
2815
+ "jest"
2816
+ ]);
2817
+ var CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS = 12e4;
2818
+ var CASCADE_EVIDENCE_UNSAFE_EXIT = 126;
2819
+ var CASCADE_EVIDENCE_RUN_ERROR_EXIT = 127;
2820
+ function isSafeCascadeCommand(command) {
2821
+ if (!command || command.length > 500) return false;
2822
+ const tokens = command.trim().split(/\s+/);
2823
+ if (tokens.length === 0) return false;
2824
+ if (!SAFE_EXECUTABLES.has(tokens[0])) return false;
2825
+ return tokens.every((token) => /^[A-Za-z0-9@/._:=~+*?\[\]-]+$/.test(token));
2826
+ }
2827
+ var runCascadeVerificationCommand = (command, cwd, timeoutMs) => {
2828
+ if (!isSafeCascadeCommand(command)) {
2829
+ return Promise.resolve({ exitCode: CASCADE_EVIDENCE_UNSAFE_EXIT });
2830
+ }
2831
+ const tokens = command.trim().split(/\s+/);
2832
+ const executable = tokens[0];
2833
+ const args = tokens.slice(1);
2834
+ return new Promise((resolve3) => {
2835
+ let child;
2836
+ try {
2837
+ child = spawn2(executable, args, {
2838
+ cwd,
2839
+ stdio: "ignore",
2840
+ windowsHide: true,
2841
+ signal: AbortSignal.timeout(timeoutMs)
2842
+ });
2843
+ } catch {
2844
+ resolve3({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT });
2845
+ return;
2846
+ }
2847
+ child.on("error", () => resolve3({ exitCode: CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
2848
+ child.on("close", (code) => resolve3({ exitCode: code ?? CASCADE_EVIDENCE_RUN_ERROR_EXIT }));
2849
+ });
2850
+ };
2851
+ async function verifyCascadeEvidence(evidence, cwd, runCommand = runCascadeVerificationCommand, timeoutMs = CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS) {
2852
+ if (!evidence) return { status: "missing", checks: [] };
2853
+ if (!evidence.typecheck) return { status: "failed", checks: [] };
2854
+ const checks = [];
2855
+ for (const name of ["typecheck", "lint", "tests"]) {
2856
+ const claimed = evidence[name];
2857
+ if (!claimed) continue;
2858
+ const actual = await runCommand(claimed.command, cwd, timeoutMs);
2859
+ const ok = claimed.exitCode === 0 && actual.exitCode === claimed.exitCode;
2860
+ checks.push({
2861
+ name,
2862
+ command: claimed.command,
2863
+ claimedExitCode: claimed.exitCode,
2864
+ actualExitCode: actual.exitCode,
2865
+ ok
2866
+ });
2867
+ }
2868
+ if (checks.length === 0) return { status: "missing", checks };
2869
+ const status = checks.every((c) => c.ok) ? "verified" : "failed";
2870
+ return { status, checks };
2871
+ }
2872
+
2749
2873
  // src/chimera-review-task.ts
2750
2874
  import { parseReviewSeverity } from "@wrongstack/core/plugin";
2751
2875
  function truncateAtCodePointBoundary(text, maxCodeUnits) {
@@ -2846,6 +2970,34 @@ function buildChimeraReviewTaskDescription(p) {
2846
2970
  lines.push("structured review report.");
2847
2971
  return lines.join("\n");
2848
2972
  }
2973
+ var CASCADE_EVIDENCE_INSTRUCTIONS = [
2974
+ `After fixing, run the project's REAL verification commands on the code you changed`,
2975
+ `(typecheck, lint, and the tests covering the fixed code) and return a machine-evidence`,
2976
+ `block as the LAST section of your response. The block MUST be a fenced JSON block`,
2977
+ `(exactly \`\`\`json ... \`\`\`) with this shape:`,
2978
+ ``,
2979
+ "```json",
2980
+ "{",
2981
+ ' "verification_evidence": {',
2982
+ ' "typecheck": { "command": "pnpm typecheck", "exitCode": 0 },',
2983
+ ' "lint": { "command": "pnpm lint", "exitCode": 0 },',
2984
+ ' "tests": { "command": "pnpm test --filter affected", "exitCode": 0 }',
2985
+ " }",
2986
+ "}",
2987
+ "```",
2988
+ ``,
2989
+ `Rules:`,
2990
+ `- \`command\` must be the EXACT command you ran (plain executable + args, no shell`,
2991
+ ` chaining like \`&&\`/\`;\`/\`|\`, no redirection, no shell expansion). These commands are`,
2992
+ ` re-run verbatim by the orchestrator to verify your work.`,
2993
+ `- \`exitCode\` is the real process exit code you observed: 0 = passed, non-zero = failed.`,
2994
+ ` Never invent or omit an exit code. If a command cannot be run, omit that key entirely`,
2995
+ ` and explain why in your report text \u2014 an omitted key is honest, a fabricated 0 is not.`,
2996
+ `- If your fix does not pass a command, report the true non-zero exit code; the`,
2997
+ ` orchestrator will treat the fix as unverified and keep the finding open.`,
2998
+ `- At least \`typecheck\` must be present; include \`lint\` and \`tests\` when the project`,
2999
+ ` has them.`
3000
+ ].join("\n");
2849
3001
  function buildChimeraCascadeTaskDescription(agentKind, p) {
2850
3002
  const fileList = p.bundle.files.map((f) => `- ${f.path}`).join("\n");
2851
3003
  const reportSlice = truncateAtCodePointBoundary(p.reviewText, 12e3);
@@ -2866,8 +3018,9 @@ function buildChimeraCascadeTaskDescription(agentKind, p) {
2866
3018
  `Investigate the security findings above. Read the flagged files, confirm or refute`,
2867
3019
  `each finding, and **apply fixes** for confirmed vulnerabilities using the edit tool.`,
2868
3020
  `Use severity (Critical/High/Medium), file:line citations, and remediation steps.`,
2869
- `After fixing, run the project's typecheck and linter to verify. If a finding is a`,
2870
- `false positive, say so and do not modify the file.`
3021
+ `If a finding is a false positive, say so and do not modify the file.`,
3022
+ ``,
3023
+ CASCADE_EVIDENCE_INSTRUCTIONS
2871
3024
  ].join("\n");
2872
3025
  }
2873
3026
  return [
@@ -2884,9 +3037,10 @@ function buildChimeraCascadeTaskDescription(agentKind, p) {
2884
3037
  ``,
2885
3038
  `Hunt for the bugs flagged above. Read the affected files, trace each finding to its`,
2886
3039
  `root cause, and **apply minimal fixes** for confirmed bugs using the edit tool.`,
2887
- `Use severity (Critical/High/Medium), file:line citations. After fixing, run the`,
2888
- `project's typecheck and linter to verify. If a finding is a false positive, say so`,
2889
- `and do not modify the file.`
3040
+ `Use severity (Critical/High/Medium), file:line citations. If a finding is a false`,
3041
+ `positive, say so and do not modify the file.`,
3042
+ ``,
3043
+ CASCADE_EVIDENCE_INSTRUCTIONS
2890
3044
  ].join("\n");
2891
3045
  }
2892
3046
 
@@ -3003,7 +3157,9 @@ function installChimeraCascadeHandler({
3003
3157
  session,
3004
3158
  getPendingWork,
3005
3159
  trackWork,
3006
- buildLadder
3160
+ buildLadder,
3161
+ verifyEvidence = verifyCascadeEvidence,
3162
+ persistEvidence
3007
3163
  }) {
3008
3164
  events.onPattern("chimera.cascade_needed", (_event, payload) => {
3009
3165
  const p = payload;
@@ -3016,6 +3172,7 @@ function installChimeraCascadeHandler({
3016
3172
  await previousWork;
3017
3173
  } catch {
3018
3174
  }
3175
+ const agentEvidence = [];
3019
3176
  for (const agentKind of p.agents) {
3020
3177
  try {
3021
3178
  const taskDesc = buildChimeraCascadeTaskDescription(agentKind, p);
@@ -3059,6 +3216,10 @@ function installChimeraCascadeHandler({
3059
3216
  const result = outcome.result;
3060
3217
  if (result?.status === "success") {
3061
3218
  const resultText = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
3219
+ const claimed = extractCascadeEvidence(resultText);
3220
+ if (claimed) {
3221
+ agentEvidence.push({ agentKind, evidence: claimed });
3222
+ }
3062
3223
  await session.append({
3063
3224
  type: "llm_response",
3064
3225
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3089,7 +3250,21 @@ function installChimeraCascadeHandler({
3089
3250
  });
3090
3251
  }
3091
3252
  }
3092
- await maybeReReviewCascade({ events, session, bundle: p.bundle });
3253
+ const accumulatedEvidence = {};
3254
+ for (const { evidence } of agentEvidence) {
3255
+ for (const name of ["typecheck", "lint", "tests"]) {
3256
+ if (evidence[name]) accumulatedEvidence[name] = evidence[name];
3257
+ }
3258
+ }
3259
+ await maybeReReviewCascade({
3260
+ events,
3261
+ session,
3262
+ bundle: p.bundle,
3263
+ reportId: p.reportId,
3264
+ claimedEvidence: accumulatedEvidence,
3265
+ verifyEvidence,
3266
+ persistEvidence
3267
+ });
3093
3268
  })();
3094
3269
  trackWork(pendingWork);
3095
3270
  });
@@ -3097,16 +3272,71 @@ function installChimeraCascadeHandler({
3097
3272
  async function maybeReReviewCascade({
3098
3273
  events,
3099
3274
  session,
3100
- bundle
3275
+ bundle,
3276
+ reportId,
3277
+ claimedEvidence,
3278
+ verifyEvidence,
3279
+ persistEvidence
3101
3280
  }) {
3102
3281
  const maxDepth = bundle.maxCascadeDepth ?? 0;
3103
3282
  const currentDepth = bundle.cascadeDepth ?? 0;
3283
+ const hasClaimedEvidence = Object.keys(claimedEvidence).length > 0;
3284
+ const verification = hasClaimedEvidence ? await verifyEvidence(
3285
+ claimedEvidence,
3286
+ bundle.cwd,
3287
+ void 0,
3288
+ CASCADE_EVIDENCE_COMMAND_TIMEOUT_MS
3289
+ ) : { status: "missing", checks: [] };
3290
+ const evidenceBundle = {
3291
+ ...bundle,
3292
+ evidenceStatus: verification.status,
3293
+ evidenceChecks: verification.checks
3294
+ };
3295
+ if (reportId && persistEvidence) {
3296
+ try {
3297
+ await persistEvidence(reportId, verification.status, verification.checks);
3298
+ } catch (error) {
3299
+ await session.append({
3300
+ type: "error",
3301
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3302
+ message: `Chimera cascade evidence persistence failed: ${errorText2(error)}`,
3303
+ phase: "agent"
3304
+ });
3305
+ }
3306
+ }
3307
+ await session.append({
3308
+ type: "llm_response",
3309
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3310
+ content: [
3311
+ {
3312
+ type: "text",
3313
+ text: renderEvidenceSummary(verification.status, verification.checks)
3314
+ }
3315
+ ],
3316
+ stopReason: "end_turn",
3317
+ usage: { input: 0, output: 0 }
3318
+ });
3319
+ if (verification.status !== "verified") {
3320
+ await session.append({
3321
+ type: "llm_response",
3322
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3323
+ content: [
3324
+ {
3325
+ type: "text",
3326
+ text: `\u{1F982} Chimera cascade evidence ${verification.status} \u2014 fix not confirmed, skipping re-review. Findings remain open.`
3327
+ }
3328
+ ],
3329
+ stopReason: "end_turn",
3330
+ usage: { input: 0, output: 0 }
3331
+ });
3332
+ return;
3333
+ }
3104
3334
  if (maxDepth > 0 && currentDepth < maxDepth) {
3105
3335
  try {
3106
3336
  const reReadFiles = [];
3107
- for (const f of bundle.files) {
3337
+ for (const f of evidenceBundle.files) {
3108
3338
  try {
3109
- const absPath = path10.join(bundle.cwd, f.path);
3339
+ const absPath = path10.join(evidenceBundle.cwd, f.path);
3110
3340
  const content = await fsp.readFile(absPath, "utf8");
3111
3341
  reReadFiles.push({ path: f.path, status: "modified", content });
3112
3342
  } catch {
@@ -3114,7 +3344,7 @@ async function maybeReReviewCascade({
3114
3344
  }
3115
3345
  if (reReadFiles.length > 0) {
3116
3346
  const reReviewBundle = {
3117
- ...bundle,
3347
+ ...evidenceBundle,
3118
3348
  files: reReadFiles,
3119
3349
  cascadeDepth: currentDepth + 1
3120
3350
  };
@@ -3172,18 +3402,28 @@ async function maybeReReviewCascade({
3172
3402
  });
3173
3403
  }
3174
3404
  }
3405
+ function renderEvidenceSummary(status, checks) {
3406
+ if (status === "missing") return "no machine evidence supplied";
3407
+ const summary = checks.map((check) => `${check.name}:${check.ok ? "pass" : "fail"}(${check.actualExitCode})`).join(", ");
3408
+ return `${status}${summary ? ` \u2014 ${summary}` : ""}`;
3409
+ }
3410
+ function errorText2(error) {
3411
+ return error instanceof Error ? error.message : String(error);
3412
+ }
3175
3413
 
3176
3414
  // src/execution-chimera-review.ts
3177
3415
  import { randomUUID as randomUUID3 } from "node:crypto";
3178
3416
  import path11 from "node:path";
3179
- import { effectiveFallbackChain } from "@wrongstack/core/agent";
3417
+ import { effectiveFallbackChain, fallbackProfileChain as fallbackProfileChain2 } from "@wrongstack/core/agent";
3180
3418
  import {
3181
3419
  CHIMERA_REVIEW_PROMPT,
3420
+ classifyChimeraReviewSource,
3182
3421
  integrateFindings,
3183
3422
  maybeCompactReviewStores,
3184
3423
  parseChimeraReviewReport,
3185
3424
  persistReviewReport,
3186
- recordCompletedReview
3425
+ recordCompletedReview,
3426
+ verifyFindingsAgainstDisk
3187
3427
  } from "@wrongstack/core/plugin";
3188
3428
  function normalizeFileKeyForCitation(raw, cwd) {
3189
3429
  const forward = raw.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -3267,7 +3507,13 @@ function installChimeraReviewHandler({
3267
3507
  const rawModel = p.reviewFallbackModels ? p.config.model?.trim() || void 0 : tModel;
3268
3508
  const baseProvider = rawProvider || tProvider || config.provider;
3269
3509
  const baseModel = rawModel || tModel || config.model;
3270
- const baseFallbacks = p.reviewFallbackModels ? [...p.reviewFallbackModels] : resolveReviewerFallbackModels(void 0);
3510
+ const configFallbacks = [...p.config.fallbackModels ?? []];
3511
+ if (p.config.fallbackProfile) {
3512
+ configFallbacks.push(...fallbackProfileChain2(config, p.config.fallbackProfile));
3513
+ }
3514
+ const baseFallbacks = p.reviewFallbackModels ? [...p.reviewFallbackModels] : resolveReviewerFallbackModels(
3515
+ configFallbacks.length > 0 ? configFallbacks : void 0
3516
+ );
3271
3517
  const assigned = assignReviewerModels(
3272
3518
  baseProvider,
3273
3519
  baseModel,
@@ -3368,18 +3614,34 @@ function installChimeraReviewHandler({
3368
3614
  }
3369
3615
  const reviewText = typeof result.result === "string" ? result.result.trim() : JSON.stringify(result.result);
3370
3616
  const reportId = randomUUID3();
3617
+ const reviewSource = classifyChimeraReviewSource(p);
3618
+ const winningAttempt = outcome.wonAt ? ladder[outcome.wonAt - 1] : void 0;
3619
+ const parsedReport = parseChimeraReviewReport(reviewText, {
3620
+ sessionId: reviewSessionId,
3621
+ agentId: p.fileProvenance?.find((entry) => entry.agentId)?.agentId ?? "chimera-review",
3622
+ reviewerModel: winningAttempt ? attemptLabel(winningAttempt) : void 0,
3623
+ reviewType: reviewSource,
3624
+ reportId
3625
+ });
3626
+ const verifiedFindings = await verifyFindingsAgainstDisk(parsedReport.findings, {
3627
+ cwd: p.cwd
3628
+ });
3629
+ const verifiedParsedReport = {
3630
+ ...parsedReport,
3631
+ findings: verifiedFindings
3632
+ };
3371
3633
  await persistAndEmitCompletion({
3372
3634
  bundle: p,
3373
3635
  reviewText,
3374
3636
  status: "success",
3375
3637
  cwd: p.cwd,
3376
3638
  sessionId: reviewSessionId,
3377
- reportId
3639
+ reportId,
3640
+ parsedReport: verifiedParsedReport
3378
3641
  });
3379
3642
  if (reviewText) {
3380
3643
  const reviewHasFindings = !isChimeraAllClearReview(reviewText);
3381
- const parsedReview = parseChimeraReviewReport(reviewText);
3382
- const citedFindings = parsedReview.findings.filter((finding) => finding.location);
3644
+ const citedFindings = verifiedFindings.filter((finding) => finding.location);
3383
3645
  const citedPaths = new Set(
3384
3646
  p.files.map((file) => normalizeFileKeyForCitation(file.path, p.cwd))
3385
3647
  );
@@ -3391,14 +3653,28 @@ function installChimeraReviewHandler({
3391
3653
  if (bucket) bucket.push(normalized);
3392
3654
  else changedBasenameIndex.set(basename7, [normalized]);
3393
3655
  }
3394
- const droppedCitations = citedFindings.filter((finding) => {
3656
+ const inScopeCited = citedFindings.filter((finding) => {
3395
3657
  const citedKey = normalizeFileKeyForCitation(finding.location.file, p.cwd);
3396
- if (citedPaths.has(citedKey)) return false;
3658
+ if (citedPaths.has(citedKey)) return true;
3397
3659
  const basename7 = path11.basename(finding.location.file).toLowerCase();
3398
3660
  const bucket = changedBasenameIndex.get(basename7);
3399
- return !(bucket && bucket.length === 1);
3661
+ return !!(bucket && bucket.length === 1);
3400
3662
  });
3401
- const effectiveHasFindings = reviewHasFindings && (parsedReview.findings.length === 0 || parsedReview.findings.length > droppedCitations.length);
3663
+ const droppedCitations = citedFindings.filter(
3664
+ (finding) => !inScopeCited.includes(finding)
3665
+ );
3666
+ const actionableFindings = inScopeCited.filter(
3667
+ (finding) => finding.verification?.status === "verified"
3668
+ );
3669
+ const unverifiedInScope = inScopeCited.filter(
3670
+ (finding) => finding.verification?.status === "unverified"
3671
+ );
3672
+ const failedVerificationCount = inScopeCited.filter(
3673
+ (finding) => finding.verification?.status === "failed"
3674
+ ).length;
3675
+ const effectiveHasFindings = reviewHasFindings && actionableFindings.length > 0;
3676
+ const verifiedCount = actionableFindings.length;
3677
+ const unverifiedCount = unverifiedInScope.length;
3402
3678
  const hallucinationNote = droppedCitations.length > 0 ? `
3403
3679
 
3404
3680
  \u26A0\uFE0F Citation validation: ${droppedCitations.length} finding(s) cite files outside the changed-file set and should be verified before acting.` : "";
@@ -3441,7 +3717,7 @@ ${reviewBody}`;
3441
3717
  reviewLength: reviewText.length,
3442
3718
  messageId: reviewMailMessageId
3443
3719
  });
3444
- if (!effectiveHasFindings) {
3720
+ if (!reviewHasFindings) {
3445
3721
  try {
3446
3722
  await mailbox.ack({
3447
3723
  messageId: reviewMailMessageId,
@@ -3485,11 +3761,8 @@ ${reviewBody}`;
3485
3761
  phase: "agent"
3486
3762
  });
3487
3763
  }
3488
- const findingCount = Math.max(
3489
- 0,
3490
- parsedReview.findings.length - droppedCitations.length
3491
- );
3492
- 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.`;
3764
+ const findingCount = actionableFindings.length;
3765
+ 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.`;
3493
3766
  events.emitCustom("chimera.report_available", {
3494
3767
  reportId,
3495
3768
  sessionId: reviewSessionId,
@@ -3711,7 +3984,7 @@ function parseSuggestionsFromOutput(finalText, todos) {
3711
3984
  setAutoSuggestions([]);
3712
3985
  return null;
3713
3986
  }
3714
- const { texts, autoTexts } = parseNextSteps(finalText, false);
3987
+ const { texts, autoTexts } = parseNextSteps(finalText);
3715
3988
  if (autoTexts.length > 0) {
3716
3989
  setAutoSuggestions(autoTexts);
3717
3990
  }
@@ -4752,6 +5025,373 @@ function createTuiNextStepCallbacks(input) {
4752
5025
  };
4753
5026
  }
4754
5027
 
5028
+ // src/tui-resource-menus.ts
5029
+ import { readdir } from "node:fs/promises";
5030
+ import { readJsonObjectFile } from "@wrongstack/core/utils";
5031
+ function createTuiResourceMenuGetter(ctx) {
5032
+ return async (id) => {
5033
+ switch (id) {
5034
+ case "fallback":
5035
+ return fallbackMenu(ctx.configStore);
5036
+ case "profile":
5037
+ return profileMenu(ctx.configStore, ctx.paths);
5038
+ case "provider-status":
5039
+ return providerStatusMenu(ctx.statusTracker);
5040
+ case "memory":
5041
+ return memoryMenu(ctx.memoryStore);
5042
+ case "worktree":
5043
+ return worktreeMenu(ctx.projectRoot);
5044
+ case "git":
5045
+ return gitMenu(ctx.projectRoot);
5046
+ }
5047
+ };
5048
+ }
5049
+ function fallbackMenu(store) {
5050
+ const config = store.get();
5051
+ const chain = config.fallbackModels ?? [];
5052
+ const profiles = config.fallbackProfiles ?? {};
5053
+ const favorites = config.favoriteModels ?? [];
5054
+ const auto = config.fallbackAuto !== false;
5055
+ const items = [
5056
+ {
5057
+ id: "leader",
5058
+ label: "Leader",
5059
+ status: "good",
5060
+ summary: `${config.provider}/${config.model}`,
5061
+ details: [
5062
+ { label: "provider", value: config.provider },
5063
+ { label: "model", value: config.model },
5064
+ { label: "bridge", value: config.fallbackBridge || "disabled" }
5065
+ ],
5066
+ body: "The active session model. The bridge, when configured, is tried before the ordered fallback chain."
5067
+ },
5068
+ {
5069
+ id: "auto",
5070
+ label: "Smart fallback",
5071
+ status: auto ? "good" : "muted",
5072
+ summary: auto ? "enabled" : "disabled",
5073
+ details: [
5074
+ { label: "mode", value: auto ? "auto-derived when chain is empty" : "explicit chain only" },
5075
+ { label: "favorites only", value: config.favoriteModelsOnly ? "yes" : "no" },
5076
+ { label: "favorites", value: String(favorites.length) }
5077
+ ],
5078
+ actions: [
5079
+ {
5080
+ key: "t",
5081
+ label: auto ? "turn off" : "turn on",
5082
+ command: `/fallback auto ${auto ? "off" : "on"}`
5083
+ }
5084
+ ]
5085
+ },
5086
+ ...chain.map((ref, index) => ({
5087
+ id: `chain:${index}`,
5088
+ label: `${index + 1}. ${ref}`,
5089
+ status: "warn",
5090
+ summary: "explicit fallback",
5091
+ details: [
5092
+ { label: "position", value: String(index + 1) },
5093
+ { label: "model ref", value: ref },
5094
+ { label: "after", value: index === 0 ? "leader/bridge" : chain[index - 1] ?? "leader" }
5095
+ ],
5096
+ actions: [
5097
+ { key: "x", label: "remove", command: `/fallback remove ${index + 1}`, confirm: true }
5098
+ ]
5099
+ })),
5100
+ ...Object.entries(profiles).sort(([a], [b]) => a.localeCompare(b)).map(([name, refs]) => ({
5101
+ id: `profile:${name}`,
5102
+ label: `Profile: ${name}`,
5103
+ status: refs.length > 0 ? "good" : "muted",
5104
+ summary: `${refs.length} model${refs.length === 1 ? "" : "s"}`,
5105
+ details: [
5106
+ { label: "name", value: name },
5107
+ { label: "length", value: String(refs.length) }
5108
+ ],
5109
+ body: refs.join(" \u2192 ") || "(empty profile)",
5110
+ actions: [
5111
+ { key: "u", label: "use", command: `/fallback profile use ${name}`, confirm: true },
5112
+ { key: "x", label: "delete", command: `/fallback profile remove ${name}`, confirm: true }
5113
+ ]
5114
+ })),
5115
+ ...favorites.map((ref, index) => ({
5116
+ id: `favorite:${index}`,
5117
+ label: `\u2605 ${ref}`,
5118
+ status: "good",
5119
+ summary: "favorite model",
5120
+ details: [{ label: "model ref", value: ref }],
5121
+ actions: [
5122
+ {
5123
+ key: "x",
5124
+ label: "unfavorite",
5125
+ command: `/fallback fav remove ${index + 1}`,
5126
+ confirm: true
5127
+ }
5128
+ ]
5129
+ }))
5130
+ ];
5131
+ return {
5132
+ id: "fallback",
5133
+ title: "Fallback routing",
5134
+ subtitle: `${chain.length} explicit \xB7 ${Object.keys(profiles).length} profiles \xB7 ${favorites.length} favorites`,
5135
+ items
5136
+ };
5137
+ }
5138
+ async function profileMenu(store, paths) {
5139
+ const active = store.get().activeProfile ?? paths.profileName ?? "default";
5140
+ let names = [];
5141
+ try {
5142
+ names = (await readdir(paths.profilesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
5143
+ } catch {
5144
+ }
5145
+ const items = await Promise.all(
5146
+ names.map(async (name) => {
5147
+ const path14 = paths.profileConfig(name);
5148
+ const data = await readJsonObjectFile(path14);
5149
+ const provider = typeof data["provider"] === "string" ? data["provider"] : "unset";
5150
+ const model = typeof data["model"] === "string" ? data["model"] : "unset";
5151
+ const fallbackCount = Array.isArray(data["fallbackModels"]) ? data["fallbackModels"].length : 0;
5152
+ const profileCount = isRecord(data["fallbackProfiles"]) ? Object.keys(data["fallbackProfiles"]).length : 0;
5153
+ const isActive = name === active;
5154
+ return {
5155
+ id: name,
5156
+ label: name,
5157
+ status: isActive ? "good" : "muted",
5158
+ summary: isActive ? "active" : `${provider}/${model}`,
5159
+ details: [
5160
+ { label: "active", value: isActive ? "yes" : "no" },
5161
+ { label: "provider", value: provider },
5162
+ { label: "model", value: model },
5163
+ {
5164
+ label: "theme",
5165
+ value: typeof data["themePreset"] === "string" ? data["themePreset"] : "default"
5166
+ },
5167
+ { label: "fallbacks", value: String(fallbackCount) },
5168
+ { label: "fallback profiles", value: String(profileCount) },
5169
+ { label: "config", value: path14 }
5170
+ ],
5171
+ body: isActive ? "This profile owns the current session configuration." : "Switching profiles closes this session so every service restarts with the selected configuration.",
5172
+ actions: isActive ? [] : [{ key: "s", label: "switch", command: `/profile switch ${name}`, confirm: true }]
5173
+ };
5174
+ })
5175
+ );
5176
+ return {
5177
+ id: "profile",
5178
+ title: "Profiles",
5179
+ subtitle: `active: ${active}`,
5180
+ emptyText: "No profiles found.",
5181
+ items
5182
+ };
5183
+ }
5184
+ function providerStatusMenu(tracker) {
5185
+ const snapshot = tracker?.getSnapshot();
5186
+ const items = snapshot?.statuses.map((status) => ({
5187
+ id: `${status.providerId}/${status.model}`,
5188
+ label: `${status.providerId}/${status.model}`,
5189
+ status: status.state === "healthy" ? "good" : status.state === "degraded" ? "warn" : "bad",
5190
+ summary: status.state,
5191
+ details: [
5192
+ { label: "state", value: status.state },
5193
+ { label: "successes", value: String(status.totalSuccesses) },
5194
+ { label: "failures", value: String(status.totalFailures) },
5195
+ { label: "rate limits", value: String(status.rateLimitHits) },
5196
+ { label: "consecutive failures", value: String(status.consecutiveFailures) },
5197
+ {
5198
+ label: "cooldown",
5199
+ value: status.stateExpiresAt ? new Date(status.stateExpiresAt).toLocaleString() : "none"
5200
+ },
5201
+ { label: "last error kind", value: status.lastErrorKind ?? "none" }
5202
+ ],
5203
+ body: status.lastErrorMessage ?? "No recorded error.",
5204
+ actions: [
5205
+ ...status.state !== "healthy" ? [
5206
+ {
5207
+ key: "r",
5208
+ label: "retry now",
5209
+ command: `/provider-status retry ${status.providerId} ${status.model}`
5210
+ }
5211
+ ] : [],
5212
+ {
5213
+ key: "x",
5214
+ label: "clear history",
5215
+ command: `/provider-status clear ${status.providerId} ${status.model}`,
5216
+ confirm: true
5217
+ }
5218
+ ]
5219
+ })) ?? [];
5220
+ return {
5221
+ id: "provider-status",
5222
+ title: "Provider health",
5223
+ subtitle: snapshot ? `${snapshot.healthy} healthy \xB7 ${snapshot.degraded} degraded \xB7 ${snapshot.blocked} blocked` : "tracker unavailable",
5224
+ emptyText: "No provider/model activity has been recorded.",
5225
+ items
5226
+ };
5227
+ }
5228
+ async function memoryMenu(store) {
5229
+ if (!store)
5230
+ return {
5231
+ id: "memory",
5232
+ title: "Memory",
5233
+ emptyText: "Memory is disabled in this host.",
5234
+ items: []
5235
+ };
5236
+ const [health, entries] = await Promise.all([store.health(), store.list(void 0, 100)]);
5237
+ return {
5238
+ id: "memory",
5239
+ title: "Memory",
5240
+ subtitle: `${health.status} \xB7 ${health.backend} \xB7 newest 100`,
5241
+ emptyText: "No memories found.",
5242
+ items: entries.map((entry, index) => ({
5243
+ id: `${entry.ts}:${index}`,
5244
+ label: truncate(entry.text.replace(/\s+/g, " "), 52),
5245
+ status: entry.priority === "critical" || entry.priority === "high" ? "warn" : "muted",
5246
+ summary: `${entry.scope} \xB7 ${entry.type ?? "fact"}`,
5247
+ details: [
5248
+ { label: "scope", value: entry.scope },
5249
+ { label: "type", value: entry.type ?? "fact" },
5250
+ { label: "priority", value: entry.priority ?? "medium" },
5251
+ {
5252
+ label: "confidence",
5253
+ value: entry.confidence === void 0 ? "unspecified" : entry.confidence.toFixed(2)
5254
+ },
5255
+ { label: "created", value: entry.ts },
5256
+ { label: "last accessed", value: entry.lastAccessed ?? "never" },
5257
+ { label: "tags", value: entry.tags?.join(", ") || "none" },
5258
+ { label: "source", value: entry.source ?? "unknown" }
5259
+ ],
5260
+ body: entry.text,
5261
+ actions: entry.scope === "project-memory" ? [
5262
+ {
5263
+ key: "x",
5264
+ label: "forget matching text",
5265
+ command: `/memory forget ${entry.text.replace(/\s+/g, " ").trim()} --exact`,
5266
+ confirm: true
5267
+ }
5268
+ ] : []
5269
+ }))
5270
+ };
5271
+ }
5272
+ async function gitMenu(projectRoot) {
5273
+ const [branch, head, status] = await Promise.all([
5274
+ runGit(["branch", "--show-current"], projectRoot),
5275
+ runGit(["rev-parse", "--short", "HEAD"], projectRoot),
5276
+ runGit(["status", "--porcelain=v1"], projectRoot)
5277
+ ]);
5278
+ if (status.code !== 0)
5279
+ return { id: "git", title: "Git", emptyText: "Not a git repository.", items: [] };
5280
+ const lines = status.stdout.split(/\r?\n/).filter(Boolean);
5281
+ const items = [
5282
+ {
5283
+ id: "repo",
5284
+ label: branch.stdout.trim() || "(detached)",
5285
+ status: lines.length === 0 ? "good" : "warn",
5286
+ summary: `${head.stdout.trim() || "no HEAD"} \xB7 ${lines.length === 0 ? "clean" : `${lines.length} changes`}`,
5287
+ details: [
5288
+ { label: "branch", value: branch.stdout.trim() || "(detached)" },
5289
+ { label: "HEAD", value: head.stdout.trim() || "(unborn)" },
5290
+ {
5291
+ label: "working tree",
5292
+ value: lines.length === 0 ? "clean" : `${lines.length} changed paths`
5293
+ }
5294
+ ],
5295
+ actions: [{ key: "c", label: "commit workflow", command: "/commit", confirm: true }]
5296
+ },
5297
+ ...lines.map((line, index) => {
5298
+ const code = line.slice(0, 2);
5299
+ const path14 = line.slice(3);
5300
+ const staged = code[0] !== " " && code[0] !== "?";
5301
+ const unstaged = code[1] !== " " || code === "??";
5302
+ return {
5303
+ id: `change:${index}:${path14}`,
5304
+ label: path14,
5305
+ status: staged ? "good" : "warn",
5306
+ summary: `${code} \xB7 ${staged ? "staged" : "not staged"}${unstaged ? " \xB7 working tree" : ""}`,
5307
+ details: [
5308
+ { label: "status", value: code },
5309
+ { label: "staged", value: staged ? "yes" : "no" },
5310
+ { label: "unstaged", value: unstaged ? "yes" : "no" },
5311
+ { label: "path", value: path14 }
5312
+ ],
5313
+ actions: [
5314
+ { key: "d", label: "diff summary", command: staged ? "/git diff --staged" : "/git diff" }
5315
+ ]
5316
+ };
5317
+ })
5318
+ ];
5319
+ return { id: "git", title: "Git", subtitle: projectRoot, items };
5320
+ }
5321
+ async function worktreeMenu(projectRoot) {
5322
+ const result = await runGit(["worktree", "list", "--porcelain"], projectRoot);
5323
+ if (result.code !== 0) {
5324
+ return {
5325
+ id: "worktree",
5326
+ title: "Worktrees",
5327
+ emptyText: result.stderr || "Not a git repository.",
5328
+ items: []
5329
+ };
5330
+ }
5331
+ const records = result.stdout.trim().split(/\r?\n\r?\n/).map((block) => block.split(/\r?\n/).filter(Boolean)).filter((lines) => lines.length > 0);
5332
+ const rows = await Promise.all(
5333
+ records.map(async (lines, index) => {
5334
+ const fields = Object.fromEntries(
5335
+ lines.map((line) => {
5336
+ const split = line.indexOf(" ");
5337
+ return split < 0 ? [line, "yes"] : [line.slice(0, split), line.slice(split + 1)];
5338
+ })
5339
+ );
5340
+ const path14 = fields["worktree"] ?? "(unknown path)";
5341
+ const branchRef = fields["branch"];
5342
+ const branch = branchRef?.replace(/^refs\/heads\//, "") ?? (fields["detached"] ? "(detached)" : "(bare)");
5343
+ const status = path14 === "(unknown path)" || fields["prunable"] ? null : await runGit(["status", "--porcelain=v1"], path14);
5344
+ const changes = status?.code === 0 ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
5345
+ const dirty = changes.length > 0;
5346
+ const isMain = index === 0;
5347
+ return {
5348
+ id: path14,
5349
+ label: branch,
5350
+ status: fields["prunable"] ? "bad" : dirty ? "warn" : "good",
5351
+ summary: `${isMain ? "main" : "linked"} \xB7 ${dirty ? `${changes.length} changes` : "clean"}`,
5352
+ details: [
5353
+ { label: "path", value: path14 },
5354
+ { label: "branch", value: branch },
5355
+ { label: "HEAD", value: fields["HEAD"]?.slice(0, 12) ?? "unknown" },
5356
+ { label: "working tree", value: dirty ? `${changes.length} changed paths` : "clean" },
5357
+ { label: "locked", value: fields["locked"] ?? "no" },
5358
+ { label: "prunable", value: fields["prunable"] ?? "no" }
5359
+ ],
5360
+ body: changes.slice(0, 30).join("\n") || (isMain ? "Primary checkout." : "Linked checkout."),
5361
+ actions: isMain ? [
5362
+ { key: "p", label: "prune stale metadata", command: "/worktree prune" },
5363
+ {
5364
+ key: "x",
5365
+ label: "clean managed worktrees",
5366
+ command: "/worktree clean --yes",
5367
+ confirm: true
5368
+ }
5369
+ ] : branchRef ? [
5370
+ {
5371
+ key: "m",
5372
+ label: "squash merge",
5373
+ command: `/worktree merge ${branch} --yes`,
5374
+ confirm: true
5375
+ }
5376
+ ] : []
5377
+ };
5378
+ })
5379
+ );
5380
+ return {
5381
+ id: "worktree",
5382
+ title: "Worktrees",
5383
+ subtitle: `${rows.length} checkout${rows.length === 1 ? "" : "s"}`,
5384
+ emptyText: "No worktrees found.",
5385
+ items: rows
5386
+ };
5387
+ }
5388
+ function isRecord(value) {
5389
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5390
+ }
5391
+ function truncate(value, max) {
5392
+ return value.length <= max ? value : `${value.slice(0, max - 1)}\u2026`;
5393
+ }
5394
+
4755
5395
  // src/execution.ts
4756
5396
  async function execute(deps) {
4757
5397
  const {
@@ -4909,6 +5549,9 @@ async function execute(deps) {
4909
5549
  session: { provider: config.provider, model: config.model }
4910
5550
  }),
4911
5551
  getPendingWork: () => chimeraWork.pending(),
5552
+ persistEvidence: async (reportId, status, checks) => {
5553
+ await updateReviewReportEvidence(reportId, wpaths.projectDir, status, checks);
5554
+ },
4912
5555
  trackWork: (work) => {
4913
5556
  chimeraWork.track(work);
4914
5557
  }
@@ -5031,6 +5674,14 @@ async function execute(deps) {
5031
5674
  agent,
5032
5675
  events,
5033
5676
  slashRegistry,
5677
+ skillLoader,
5678
+ getResourceMenu: createTuiResourceMenuGetter({
5679
+ configStore,
5680
+ paths: wpaths,
5681
+ memoryStore,
5682
+ statusTracker,
5683
+ projectRoot
5684
+ }),
5034
5685
  secretInputController,
5035
5686
  attachments,
5036
5687
  tokenCounter,
@@ -5347,4 +5998,4 @@ export {
5347
5998
  execute,
5348
5999
  resolveReviewerFallbackModels
5349
6000
  };
5350
- //# sourceMappingURL=execution-QYFWDD3Y.js.map
6001
+ //# sourceMappingURL=execution-4IG5XKOL.js.map