@staff0rd/assist 0.220.0 → 0.220.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,7 +88,14 @@ After installation, the `assist` command will be available globally. You can als
88
88
  - `assist prs fixed <comment-id> <sha>` - Reply with commit link and resolve thread
89
89
  - `assist prs wontfix <comment-id> <reason>` - Reply with reason and resolve thread
90
90
  - `assist prs comment <path> <line> <body>` - Add a line comment to the pending review
91
- - `assist review [sha] [--no-prompt] [--submit] [--force] [--refine] [--apply] [--verbose]` - Run Claude and Codex in parallel to review the open PR for the current branch. The diff is fetched from GitHub (base SHA → head SHA via `gh pr diff`), so stale local base branches don't pollute the review; fails fast if no PR is open. By default, prompts before posting line-bound comments and then prompts again to submit the pending review (defaulting to no). Cached `claude.md` / `codex.md` / `synthesis.md` are reused when present; if any reviewer is re-run, the synthesis is invalidated. `--no-prompt` skips all confirmations. `--submit` defaults the submit prompt to yes (or auto-submits when combined with `--no-prompt`). `--force` clears all cached files and re-runs every phase. `--refine` skips posting and instead launches an interactive Claude session that walks through `synthesis.md` and edits it in place; a subsequent `assist review` (no flag) reuses the refined file and posts only the surviving findings. `--apply` skips posting and instead launches an interactive Claude session that walks through each remaining finding asking apply/skip; applied findings have their code fix made in the working tree (unstaged) and are removed from `synthesis.md`, while skipped findings stay so a subsequent `assist review` posts them. `--apply` cannot be combined with `--refine`. `--verbose` disables the stacked-spinner UI and falls back to per-line log output (per-tool lines, starting/done lines); non-TTY environments (CI) automatically fall back to verbose-style output. Pass a commit `<sha>` to review that commit's diff (`sha^..sha`) instead; the review files land under `.assist/reviews/<shortSha>/`, no GitHub lookup or posting happens, and `--refine` / `--apply` / `--submit` are rejected.
91
+ - `assist review [sha] [options]` - Run Claude and Codex in parallel to review the open PR for the current branch. The diff is fetched from GitHub (base SHA → head SHA via `gh pr diff`), so stale local base branches don't pollute the review; fails fast if no PR is open. By default, prompts before posting line-bound comments and then prompts again to submit the pending review (defaulting to no). Cached `claude.md` / `codex.md` / `synthesis.md` are reused when present; if any reviewer is re-run, the synthesis is invalidated.
92
+ - `[sha]` - Review that commit's diff (`sha^..sha`) instead of the open PR. Files land under `.assist/reviews/<shortSha>/`, no GitHub lookup or posting happens, and `--refine` / `--apply` / `--submit` are rejected
93
+ - `--no-prompt` - Skip all confirmations
94
+ - `--submit` - Default the submit prompt to yes (or auto-submit when combined with `--no-prompt`)
95
+ - `--force` - Clear all cached files and re-run every phase
96
+ - `--refine` - Skip posting; launch an interactive Claude session that walks through `synthesis.md` and edits it in place. A subsequent `assist review` reuses the refined file and posts only the surviving findings
97
+ - `--apply` - Skip posting; launch an interactive Claude session that walks through each finding asking apply/skip. Applied findings are fixed in the working tree (unstaged) and removed from `synthesis.md`; skipped findings stay so a subsequent `assist review` posts them. Cannot be combined with `--refine`
98
+ - `--verbose` - Disable the stacked-spinner UI and fall back to per-line log output. Non-TTY environments (CI) automatically use this mode
92
99
  - `assist news` - Start the news web UI showing latest RSS feed items (same as `news web`)
93
100
  - `assist news add [url]` - Add an RSS feed URL to the config
94
101
  - `assist news web [-p, --port <number>]` - Start a web view of the news feeds (default port 3001)
package/claude/CLAUDE.md CHANGED
@@ -11,6 +11,4 @@ When using extract, name the destination file after the exported function (e.g.
11
11
 
12
12
  Do not modify `claude/settings.json` without asking the user first. Only read-only commands should be added to the allow list — write operations (add, remove, set, delete) must require confirmation.
13
13
 
14
- When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use these commands to fetch context:
15
- - `assist jira view <issue-key>` — print the title and description
16
- - `assist jira ac <issue-key>` — print acceptance criteria
14
+ When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use the Atlassian MCP to fetch context.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.220.0",
9
+ version: "0.220.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -6135,9 +6135,9 @@ import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as re
6135
6135
  import { homedir as homedir6 } from "os";
6136
6136
  import { join as join22 } from "path";
6137
6137
 
6138
- // src/commands/permitCliReads/assertCliExists.ts
6138
+ // src/shared/checkCliAvailable.ts
6139
6139
  import { execSync as execSync17 } from "child_process";
6140
- function assertCliExists(cli) {
6140
+ function checkCliAvailable(cli) {
6141
6141
  const binary = cli.split(/\s+/)[0];
6142
6142
  const opts = {
6143
6143
  encoding: "utf-8",
@@ -6145,16 +6145,24 @@ function assertCliExists(cli) {
6145
6145
  };
6146
6146
  try {
6147
6147
  execSync17(`command -v ${binary}`, opts);
6148
+ return true;
6148
6149
  } catch {
6149
6150
  try {
6150
6151
  execSync17(`where ${binary}`, opts);
6152
+ return true;
6151
6153
  } catch {
6152
- console.error(`CLI "${cli}" not found in PATH`);
6153
- process.exit(1);
6154
+ return false;
6154
6155
  }
6155
6156
  }
6156
6157
  }
6157
6158
 
6159
+ // src/commands/permitCliReads/assertCliExists.ts
6160
+ function assertCliExists(cli) {
6161
+ if (checkCliAvailable(cli)) return;
6162
+ console.error(`CLI "${cli}" not found in PATH`);
6163
+ process.exit(1);
6164
+ }
6165
+
6158
6166
  // src/commands/permitCliReads/colorize.ts
6159
6167
  import chalk66 from "chalk";
6160
6168
  function colorize(plainOutput) {
@@ -12216,68 +12224,18 @@ async function runApplySession(synthesisPath) {
12216
12224
  await done2;
12217
12225
  }
12218
12226
 
12219
- // src/commands/review/runReviewPipeline.ts
12220
- import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
12221
-
12222
- // src/commands/review/buildReviewerStdin.ts
12223
- var REVIEW_PROMPT = `You are acting as a reviewer for a proposed code change made by another engineer. The full review request \u2014 branch, base, changed files, and unified diff \u2014 is in request.md in the current working directory.
12224
-
12225
- Read request.md, then produce a thorough code review in Markdown.
12226
-
12227
- ## When to flag a finding
12228
-
12229
- A finding is worth raising only if all of the following hold:
12230
-
12231
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
12232
- 2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
12233
- 3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
12234
- 4. The issue was introduced by this change. Do not flag pre-existing bugs.
12235
- 5. The original author would likely fix it if made aware.
12236
- 6. It does not rely on unstated assumptions about the codebase or author's intent.
12237
- 7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
12238
- 8. It is clearly not an intentional change by the author.
12239
-
12240
- ## How to write the comment (Impact + Recommendation)
12241
-
12242
- 1. Make clear *why* the issue is a bug.
12243
- 2. Communicate severity accurately \u2014 do not inflate.
12244
- 3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
12245
- 4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
12246
- 5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
12247
- 6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
12248
- 7. Write so the author grasps the point on first read.
12249
- 8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
12250
-
12251
- Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
12252
-
12253
- ## How many findings to return
12254
-
12255
- List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
12256
-
12257
- ## Output format
12258
-
12259
- For each finding include:
12260
- - Severity (blocker, major, minor, nit) \u2014 see rubric below
12261
- - File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
12262
- - Impact: what could go wrong, including the conditions under which it manifests
12263
- - Recommendation: a concrete change
12264
-
12265
- Severity rubric:
12266
- - **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
12267
- - **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
12268
- - **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
12269
- - **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
12270
-
12271
- Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
12272
-
12273
- Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
12274
-
12275
- Output only the review Markdown. Do not include any preamble or commentary about the process.`;
12276
- function buildReviewerStdin(requestPath) {
12277
- return `${REVIEW_PROMPT}
12278
-
12279
- The review request is at: ${requestPath}
12280
- `;
12227
+ // src/commands/review/cachedReviewerResult.ts
12228
+ import { statSync as statSync4 } from "fs";
12229
+ function cachedReviewerResult(name, outputPath) {
12230
+ let size;
12231
+ try {
12232
+ size = statSync4(outputPath).size;
12233
+ } catch {
12234
+ return null;
12235
+ }
12236
+ if (size === 0) return null;
12237
+ console.log(`[${name}] cached \u2192 ${outputPath} (${size} bytes)`);
12238
+ return { name, outputPath, exitCode: 0, stderr: "" };
12281
12239
  }
12282
12240
 
12283
12241
  // src/commands/review/MultiSpinner.ts
@@ -12382,18 +12340,114 @@ var MultiSpinner = class {
12382
12340
  }
12383
12341
  };
12384
12342
 
12385
- // src/commands/review/cachedReviewerResult.ts
12386
- import { statSync as statSync4 } from "fs";
12387
- function cachedReviewerResult(name, outputPath) {
12388
- let size;
12389
- try {
12390
- size = statSync4(outputPath).size;
12391
- } catch {
12392
- return null;
12343
+ // src/commands/review/ensureCodexAvailable.ts
12344
+ import { spawnSync as spawnSync3 } from "child_process";
12345
+ function runNpmInstall() {
12346
+ const result = spawnSync3("npm", ["install", "-g", "@openai/codex"], {
12347
+ stdio: "inherit",
12348
+ shell: true
12349
+ });
12350
+ return result.status === 0 && result.error === void 0;
12351
+ }
12352
+ async function ensureCodexAvailable() {
12353
+ if (checkCliAvailable("codex")) return "available";
12354
+ console.log("codex CLI was not found on PATH.");
12355
+ const install = await promptConfirm(
12356
+ "Install @openai/codex globally via npm install -g @openai/codex?",
12357
+ true
12358
+ );
12359
+ if (!install) {
12360
+ console.log("Skipping codex reviewer.");
12361
+ return "skipped";
12393
12362
  }
12394
- if (size === 0) return null;
12395
- console.log(`[${name}] cached \u2192 ${outputPath} (${size} bytes)`);
12396
- return { name, outputPath, exitCode: 0, stderr: "" };
12363
+ console.log("Installing @openai/codex...");
12364
+ if (!runNpmInstall()) {
12365
+ console.error(
12366
+ "npm install -g @openai/codex failed. Skipping codex reviewer."
12367
+ );
12368
+ return "skipped";
12369
+ }
12370
+ if (checkCliAvailable("codex")) return "available";
12371
+ console.error(
12372
+ "codex still not found on PATH after install. Skipping codex reviewer."
12373
+ );
12374
+ return "skipped";
12375
+ }
12376
+
12377
+ // src/commands/review/planCodexReviewer.ts
12378
+ async function planCodexReviewer(codexPath) {
12379
+ const cached = cachedReviewerResult("codex", codexPath);
12380
+ if (cached) return { kind: "cached", cached };
12381
+ const status2 = await ensureCodexAvailable();
12382
+ if (status2 === "available") return { kind: "run" };
12383
+ return { kind: "skipped" };
12384
+ }
12385
+ function skippedCodexResult(outputPath) {
12386
+ return { name: "codex", outputPath, exitCode: 0, stderr: "" };
12387
+ }
12388
+
12389
+ // src/commands/review/runAndSynthesise.ts
12390
+ import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
12391
+
12392
+ // src/commands/review/buildReviewerStdin.ts
12393
+ var REVIEW_PROMPT = `You are acting as a reviewer for a proposed code change made by another engineer. The full review request \u2014 branch, base, changed files, and unified diff \u2014 is in request.md in the current working directory.
12394
+
12395
+ Read request.md, then produce a thorough code review in Markdown.
12396
+
12397
+ ## When to flag a finding
12398
+
12399
+ A finding is worth raising only if all of the following hold:
12400
+
12401
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
12402
+ 2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
12403
+ 3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
12404
+ 4. The issue was introduced by this change. Do not flag pre-existing bugs.
12405
+ 5. The original author would likely fix it if made aware.
12406
+ 6. It does not rely on unstated assumptions about the codebase or author's intent.
12407
+ 7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
12408
+ 8. It is clearly not an intentional change by the author.
12409
+
12410
+ ## How to write the comment (Impact + Recommendation)
12411
+
12412
+ 1. Make clear *why* the issue is a bug.
12413
+ 2. Communicate severity accurately \u2014 do not inflate.
12414
+ 3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
12415
+ 4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
12416
+ 5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
12417
+ 6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
12418
+ 7. Write so the author grasps the point on first read.
12419
+ 8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
12420
+
12421
+ Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
12422
+
12423
+ ## How many findings to return
12424
+
12425
+ List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
12426
+
12427
+ ## Output format
12428
+
12429
+ For each finding include:
12430
+ - Severity (blocker, major, minor, nit) \u2014 see rubric below
12431
+ - File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
12432
+ - Impact: what could go wrong, including the conditions under which it manifests
12433
+ - Recommendation: a concrete change
12434
+
12435
+ Severity rubric:
12436
+ - **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
12437
+ - **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
12438
+ - **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
12439
+ - **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
12440
+
12441
+ Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
12442
+
12443
+ Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
12444
+
12445
+ Output only the review Markdown. Do not include any preamble or commentary about the process.`;
12446
+ function buildReviewerStdin(requestPath) {
12447
+ return `${REVIEW_PROMPT}
12448
+
12449
+ The review request is at: ${requestPath}
12450
+ `;
12397
12451
  }
12398
12452
 
12399
12453
  // src/commands/review/printReviewerFailures.ts
@@ -12577,42 +12631,90 @@ function handleChildClose(args) {
12577
12631
  return { exitCode, elapsedMs };
12578
12632
  }
12579
12633
 
12634
+ // src/commands/review/handleSpawnError.ts
12635
+ function messageFor(err, command) {
12636
+ if (err.code === "ENOENT") return `command not found: ${command}`;
12637
+ return err.message || String(err);
12638
+ }
12639
+ function handleSpawnError(ctx, err) {
12640
+ const message = messageFor(err, ctx.command);
12641
+ const stderr = ctx.stderr ? `${ctx.stderr}
12642
+ ${message}` : message;
12643
+ if (!ctx.quiet) console.error(`[${ctx.name}] failed: ${message}`);
12644
+ return {
12645
+ exitCode: 127,
12646
+ stderr,
12647
+ elapsedMs: Date.now() - ctx.startedAt
12648
+ };
12649
+ }
12650
+
12651
+ // src/commands/review/waitForChildExit.ts
12652
+ function onErrorResult(ctx, err) {
12653
+ ctx.flushPending();
12654
+ return handleSpawnError(
12655
+ {
12656
+ command: ctx.command,
12657
+ name: ctx.name,
12658
+ stderr: ctx.stderr.value,
12659
+ startedAt: ctx.startedAt,
12660
+ quiet: ctx.quiet
12661
+ },
12662
+ err
12663
+ );
12664
+ }
12665
+ function onCloseResult(ctx, code) {
12666
+ ctx.flushPending();
12667
+ const closed = handleChildClose({
12668
+ code,
12669
+ startedAt: ctx.startedAt,
12670
+ name: ctx.name,
12671
+ stderr: ctx.stderr.value,
12672
+ quiet: ctx.quiet
12673
+ });
12674
+ return { ...closed, stderr: ctx.stderr.value };
12675
+ }
12676
+ function waitForChildExit(ctx) {
12677
+ return new Promise((resolve15) => {
12678
+ let settled = false;
12679
+ const settle = (result) => {
12680
+ if (settled) return;
12681
+ settled = true;
12682
+ resolve15(result);
12683
+ };
12684
+ ctx.child.on("error", (err) => settle(onErrorResult(ctx, err)));
12685
+ ctx.child.on("close", (code) => settle(onCloseResult(ctx, code)));
12686
+ });
12687
+ }
12688
+
12580
12689
  // src/commands/review/runStreamingChild.ts
12690
+ function writeStdinSafely(child, payload) {
12691
+ child.stdin?.on("error", () => {
12692
+ });
12693
+ try {
12694
+ child.stdin?.write(payload);
12695
+ child.stdin?.end();
12696
+ } catch {
12697
+ }
12698
+ }
12581
12699
  function startChild(spec) {
12582
12700
  const child = spawn6(spec.command, spec.args, {
12583
12701
  stdio: ["pipe", "pipe", "pipe"]
12584
12702
  });
12585
12703
  const flushPending = attachLineParser(child, spec.onLine);
12586
12704
  const stderr = attachStderrCollector(child);
12587
- child.stdin?.write(spec.stdin);
12588
- child.stdin?.end();
12705
+ writeStdinSafely(child, spec.stdin);
12589
12706
  return { child, flushPending, stderr };
12590
12707
  }
12591
- function waitForExit(ctx) {
12592
- return new Promise((resolve15, reject) => {
12593
- ctx.child.on("error", reject);
12594
- ctx.child.on("close", (code) => {
12595
- ctx.flushPending();
12596
- const closed = handleChildClose({
12597
- code,
12598
- startedAt: ctx.startedAt,
12599
- name: ctx.name,
12600
- stderr: ctx.stderr.value,
12601
- quiet: ctx.quiet
12602
- });
12603
- resolve15({ ...closed, stderr: ctx.stderr.value });
12604
- });
12605
- });
12606
- }
12607
12708
  function runStreamingChild(spec) {
12608
12709
  const startedAt = Date.now();
12609
12710
  if (!spec.quiet) console.log(`[${spec.name}] starting`);
12610
12711
  const { child, flushPending, stderr } = startChild(spec);
12611
- return waitForExit({
12712
+ return waitForChildExit({
12612
12713
  child,
12613
12714
  flushPending,
12614
12715
  stderr,
12615
12716
  name: spec.name,
12717
+ command: spec.command,
12616
12718
  startedAt,
12617
12719
  quiet: spec.quiet ?? false
12618
12720
  });
@@ -12650,6 +12752,19 @@ async function runClaudeReviewer(spec) {
12650
12752
  return finaliseReviewerRun(spec, spinner, result);
12651
12753
  }
12652
12754
 
12755
+ // src/commands/review/resolveClaude.ts
12756
+ function resolveClaude(args) {
12757
+ if (args.cached) return Promise.resolve(args.cached);
12758
+ const spinner = args.multi?.create("claude \u2014 starting");
12759
+ return runClaudeReviewer({
12760
+ name: "claude",
12761
+ reviewDir: args.reviewDir,
12762
+ stdin: args.stdin,
12763
+ outputPath: args.claudePath,
12764
+ spinner
12765
+ });
12766
+ }
12767
+
12653
12768
  // src/commands/review/runCodexReviewer.ts
12654
12769
  import { existsSync as existsSync34, unlinkSync as unlinkSync11 } from "fs";
12655
12770
 
@@ -12710,38 +12825,42 @@ async function runCodexReviewer(spec) {
12710
12825
  return finaliseReviewerRun(spec, spinner, result);
12711
12826
  }
12712
12827
 
12713
- // src/commands/review/runReviewers.ts
12714
- function spinnerFor(multi, name, cached) {
12715
- if (cached || !multi) return void 0;
12716
- return multi.create(`${name} \u2014 starting`);
12828
+ // src/commands/review/resolveCodex.ts
12829
+ function resolveCodex(args) {
12830
+ if (args.plan.kind === "cached") return Promise.resolve(args.plan.cached);
12831
+ if (args.plan.kind === "skipped") {
12832
+ return Promise.resolve(skippedCodexResult(args.codexPath));
12833
+ }
12834
+ const spinner = args.multi?.create("codex \u2014 starting");
12835
+ return runCodexReviewer({
12836
+ name: "codex",
12837
+ reviewDir: args.reviewDir,
12838
+ stdin: args.stdin,
12839
+ outputPath: args.codexPath,
12840
+ spinner
12841
+ });
12717
12842
  }
12843
+
12844
+ // src/commands/review/runReviewers.ts
12718
12845
  async function runReviewers(reviewDir, claudePath, codexPath, stdinPrompt, options2) {
12719
- const cachedClaude = cachedReviewerResult("claude", claudePath);
12720
- const cachedCodex = cachedReviewerResult("codex", codexPath);
12721
- if (cachedClaude && cachedCodex) {
12722
- return { results: [cachedClaude, cachedCodex], anyFresh: false };
12723
- }
12724
- const { multi } = options2;
12725
- const claudeSpinner = spinnerFor(multi, "claude", cachedClaude);
12726
- const codexSpinner = spinnerFor(multi, "codex", cachedCodex);
12727
- const results = await Promise.all([
12728
- cachedClaude ? Promise.resolve(cachedClaude) : runClaudeReviewer({
12729
- name: "claude",
12730
- reviewDir,
12731
- stdin: stdinPrompt,
12732
- outputPath: claudePath,
12733
- spinner: claudeSpinner
12734
- }),
12735
- cachedCodex ? Promise.resolve(cachedCodex) : runCodexReviewer({
12736
- name: "codex",
12737
- reviewDir,
12738
- stdin: stdinPrompt,
12739
- outputPath: codexPath,
12740
- spinner: codexSpinner
12741
- })
12742
- ]);
12743
- if (multi) printReviewerFailures(results);
12744
- return { results, anyFresh: true };
12846
+ const claudePromise = resolveClaude({
12847
+ reviewDir,
12848
+ claudePath,
12849
+ stdin: stdinPrompt,
12850
+ cached: options2.cachedClaude,
12851
+ multi: options2.multi
12852
+ });
12853
+ const codexPromise = resolveCodex({
12854
+ reviewDir,
12855
+ codexPath,
12856
+ stdin: stdinPrompt,
12857
+ plan: options2.codexPlan,
12858
+ multi: options2.multi
12859
+ });
12860
+ const results = await Promise.all([claudePromise, codexPromise]);
12861
+ if (options2.multi) printReviewerFailures(results);
12862
+ const anyFresh = options2.cachedClaude === null || options2.codexPlan.kind !== "cached";
12863
+ return { results, anyFresh };
12745
12864
  }
12746
12865
 
12747
12866
  // src/commands/review/synthesise.ts
@@ -12750,7 +12869,9 @@ import { readFileSync as readFileSync30 } from "fs";
12750
12869
  // src/commands/review/buildSynthesisStdin.ts
12751
12870
  var SYNTHESIS_PROMPT = `You are consolidating two independent code reviews of the same change. The original review request is in request.md. The two reviews are in claude.md and codex.md in the current working directory.
12752
12871
 
12753
- Read all three files, deduplicate findings, and produce a single consolidated review in Markdown with this exact structure:
12872
+ If codex.md does not exist on disk, the codex reviewer was skipped (CLI unavailable). In that case, work from claude.md alone; treat every finding as 'claude-only', do not mark anything 'confirmed' or 'codex-only', and add a note in the Summary that the codex reviewer was skipped.
12873
+
12874
+ Read all available review files, deduplicate findings, and produce a single consolidated review in Markdown with this exact structure:
12754
12875
 
12755
12876
  # Code review synthesis
12756
12877
 
@@ -12830,6 +12951,29 @@ async function synthesise(paths, options2) {
12830
12951
  return result;
12831
12952
  }
12832
12953
 
12954
+ // src/commands/review/runAndSynthesise.ts
12955
+ async function runAndSynthesise(args) {
12956
+ const { paths, multi } = args;
12957
+ const { results, anyFresh } = await runReviewers(
12958
+ paths.reviewDir,
12959
+ paths.claudePath,
12960
+ paths.codexPath,
12961
+ buildReviewerStdin(paths.requestPath),
12962
+ { multi, codexPlan: args.codexPlan, cachedClaude: args.cachedClaude }
12963
+ );
12964
+ if (results.every((r) => r.exitCode !== 0)) {
12965
+ console.error(
12966
+ "Both reviewers failed; skipping synthesis. See review folder for stderr details."
12967
+ );
12968
+ return false;
12969
+ }
12970
+ if (anyFresh && existsSync35(paths.synthesisPath)) {
12971
+ unlinkSync12(paths.synthesisPath);
12972
+ }
12973
+ const synthesisResult = await synthesise(paths, { multi });
12974
+ return synthesisResult.exitCode === 0;
12975
+ }
12976
+
12833
12977
  // src/commands/review/useSpinnerUi.ts
12834
12978
  function useSpinnerUi(verbose) {
12835
12979
  if (verbose) return false;
@@ -12851,27 +12995,16 @@ function finishUi(ui, ok) {
12851
12995
  else ui.elapsed.fail(label2);
12852
12996
  }
12853
12997
  async function runReviewPipeline(paths, options2) {
12998
+ const cachedClaude = cachedReviewerResult("claude", paths.claudePath);
12999
+ const codexPlan = await planCodexReviewer(paths.codexPath);
12854
13000
  const ui = createUi(useSpinnerUi(options2.verbose));
12855
13001
  try {
12856
- const { results, anyFresh } = await runReviewers(
12857
- paths.reviewDir,
12858
- paths.claudePath,
12859
- paths.codexPath,
12860
- buildReviewerStdin(paths.requestPath),
12861
- { multi: ui.multi }
12862
- );
12863
- if (results.every((r) => r.exitCode !== 0)) {
12864
- console.error(
12865
- "Both reviewers failed; skipping synthesis. See review folder for stderr details."
12866
- );
12867
- finishUi(ui, false);
12868
- return false;
12869
- }
12870
- if (anyFresh && existsSync35(paths.synthesisPath)) {
12871
- unlinkSync12(paths.synthesisPath);
12872
- }
12873
- const synthesisResult = await synthesise(paths, { multi: ui.multi });
12874
- const ok = synthesisResult.exitCode === 0;
13002
+ const ok = await runAndSynthesise({
13003
+ paths,
13004
+ cachedClaude,
13005
+ codexPlan,
13006
+ multi: ui.multi
13007
+ });
12875
13008
  finishUi(ui, ok);
12876
13009
  return ok;
12877
13010
  } catch (err) {
@@ -14313,7 +14446,7 @@ function registerVerify(program2) {
14313
14446
  }
14314
14447
 
14315
14448
  // src/commands/voice/devices.ts
14316
- import { spawnSync as spawnSync3 } from "child_process";
14449
+ import { spawnSync as spawnSync4 } from "child_process";
14317
14450
  import { join as join43 } from "path";
14318
14451
 
14319
14452
  // src/commands/voice/shared.ts
@@ -14346,7 +14479,7 @@ function getLockFile() {
14346
14479
  // src/commands/voice/devices.ts
14347
14480
  function devices() {
14348
14481
  const script = join43(getPythonDir(), "list_devices.py");
14349
- spawnSync3(getVenvPython(), [script], { stdio: "inherit" });
14482
+ spawnSync4(getVenvPython(), [script], { stdio: "inherit" });
14350
14483
  }
14351
14484
 
14352
14485
  // src/commands/voice/logs.ts
@@ -14378,7 +14511,7 @@ function logs(options2) {
14378
14511
  }
14379
14512
 
14380
14513
  // src/commands/voice/setup.ts
14381
- import { spawnSync as spawnSync4 } from "child_process";
14514
+ import { spawnSync as spawnSync5 } from "child_process";
14382
14515
  import { mkdirSync as mkdirSync14 } from "fs";
14383
14516
  import { join as join45 } from "path";
14384
14517
 
@@ -14439,7 +14572,7 @@ function setup() {
14439
14572
  bootstrapVenv();
14440
14573
  console.log("\nDownloading models...\n");
14441
14574
  const script = join45(getPythonDir(), "setup_models.py");
14442
- const result = spawnSync4(getVenvPython(), [script], {
14575
+ const result = spawnSync5(getVenvPython(), [script], {
14443
14576
  stdio: "inherit",
14444
14577
  env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
14445
14578
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.220.0",
3
+ "version": "0.220.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {