@staff0rd/assist 0.239.0 → 0.240.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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +103 -115
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -91,13 +91,14 @@ After installation, the `assist` command will be available globally. You can als
91
91
  - `assist prs fixed <comment-id> <sha>` - Reply with commit link and resolve thread
92
92
  - `assist prs wontfix <comment-id> <reason>` - Reply with reason and resolve thread
93
93
  - `assist prs comment <path> <line> <body>` - Add a line comment to the pending review
94
- - `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.
95
- - `[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
94
+ - `assist review [number] [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.
95
+ - `[number]` - Run `gh pr checkout <number>` first, then review that PR's branch. If the checkout fails (dirty working tree, unknown PR number), the review aborts
96
96
  - `--no-prompt` - Skip all confirmations
97
97
  - `--submit` - Default the submit prompt to yes (or auto-submit when combined with `--no-prompt`)
98
98
  - `--force` - Clear all cached files and re-run every phase
99
99
  - `--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
100
100
  - `--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`
101
+ - `--backlog` - Skip posting; launch an interactive Claude session running `/bug` that files all findings (including `already-raised`) as a single bug backlog item with one phase per finding. `synthesis.md` is left untouched; `--submit` is ignored. Cannot be combined with `--refine` or `--apply`
101
102
  - `--verbose` - Disable the stacked-spinner UI and fall back to per-line log output. Non-TTY environments (CI) automatically use this mode
102
103
  - `assist news` - Start the news web UI showing latest RSS feed items (same as `news web`)
103
104
  - `assist news add [url]` - Add an RSS feed URL to the config
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.239.0",
9
+ version: "0.240.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -12863,6 +12863,9 @@ function registerRefactor(program2) {
12863
12863
  registerRestructure(refactorCommand);
12864
12864
  }
12865
12865
 
12866
+ // src/commands/review/review.ts
12867
+ import { execFileSync as execFileSync6 } from "child_process";
12868
+
12866
12869
  // src/commands/review/formatPriorComments.ts
12867
12870
  function threadKey(c, byId) {
12868
12871
  if (c.threadId) return c.threadId;
@@ -12936,23 +12939,6 @@ ${formatFiles(context.changedFiles)}
12936
12939
  ${priorBlock}
12937
12940
  ## Diff (PR #${context.prNumber}: ${context.baseSha}..${context.headSha})
12938
12941
 
12939
- \`\`\`diff
12940
- ${context.diff.trimEnd()}
12941
- \`\`\`
12942
- `;
12943
- }
12944
- function buildShaRequest(context) {
12945
- return `# Code review request
12946
-
12947
- - Commit: \`${context.sha}\`
12948
- - Parent: \`${context.parentSha}\`
12949
-
12950
- ## Changed files
12951
-
12952
- ${formatFiles(context.changedFiles)}
12953
-
12954
- ## Diff (commit ${context.sha}: ${context.parentSha}..${context.sha})
12955
-
12956
12942
  \`\`\`diff
12957
12943
  ${context.diff.trimEnd()}
12958
12944
  \`\`\`
@@ -13006,12 +12992,41 @@ function fetchExistingComments() {
13006
12992
  }
13007
12993
 
13008
12994
  // src/commands/review/gatherContext.ts
13009
- import { execSync as execSync42 } from "child_process";
12995
+ import { execSync as execSync43 } from "child_process";
13010
12996
 
13011
- // src/commands/review/fetchPrDiffInfo.ts
12997
+ // src/commands/review/fetchPrDiff.ts
13012
12998
  import { execSync as execSync41 } from "child_process";
12999
+ function fetchPrDiff(prNumber, baseSha, headSha) {
13000
+ const { org, repo } = getRepoInfo();
13001
+ try {
13002
+ return execSync41(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
13003
+ encoding: "utf-8",
13004
+ maxBuffer: 256 * 1024 * 1024,
13005
+ stdio: ["ignore", "pipe", "pipe"]
13006
+ });
13007
+ } catch (error) {
13008
+ if (!isDiffTooLarge(error)) throw error;
13009
+ return fetchDiffViaGit(baseSha, headSha);
13010
+ }
13011
+ }
13012
+ function isDiffTooLarge(error) {
13013
+ return error instanceof Error && (error.message.includes("too_large") || error.message.includes("maximum number of files"));
13014
+ }
13015
+ function fetchDiffViaGit(baseSha, headSha) {
13016
+ try {
13017
+ execSync41(`git fetch origin ${baseSha} ${headSha}`, { stdio: "ignore" });
13018
+ } catch {
13019
+ }
13020
+ return execSync41(`git diff ${baseSha}...${headSha}`, {
13021
+ encoding: "utf-8",
13022
+ maxBuffer: 256 * 1024 * 1024
13023
+ });
13024
+ }
13025
+
13026
+ // src/commands/review/fetchPrDiffInfo.ts
13027
+ import { execSync as execSync42 } from "child_process";
13013
13028
  function getCurrentBranch2() {
13014
- return execSync41("git rev-parse --abbrev-ref HEAD", {
13029
+ return execSync42("git rev-parse --abbrev-ref HEAD", {
13015
13030
  encoding: "utf-8"
13016
13031
  }).trim();
13017
13032
  }
@@ -13021,7 +13036,7 @@ function fetchPrDiffInfo() {
13021
13036
  const fields = "number,baseRefName,baseRefOid,headRefName,headRefOid";
13022
13037
  let raw;
13023
13038
  try {
13024
- raw = execSync41(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
13039
+ raw = execSync42(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
13025
13040
  encoding: "utf-8",
13026
13041
  stdio: ["ignore", "pipe", "pipe"]
13027
13042
  });
@@ -13045,32 +13060,28 @@ function fetchPrDiffInfo() {
13045
13060
  }
13046
13061
  function fetchPrChangedFiles(prNumber) {
13047
13062
  const { org, repo } = getRepoInfo();
13048
- const out = execSync41(`gh pr diff ${prNumber} --name-only -R ${org}/${repo}`, {
13049
- encoding: "utf-8",
13050
- maxBuffer: 64 * 1024 * 1024
13051
- });
13063
+ const out = execSync42(
13064
+ `gh api repos/${org}/${repo}/pulls/${prNumber}/files --paginate --jq ".[].filename"`,
13065
+ {
13066
+ encoding: "utf-8",
13067
+ maxBuffer: 64 * 1024 * 1024
13068
+ }
13069
+ );
13052
13070
  return out.trim().split("\n").filter(Boolean);
13053
13071
  }
13054
- function fetchPrDiff(prNumber) {
13055
- const { org, repo } = getRepoInfo();
13056
- return execSync41(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
13057
- encoding: "utf-8",
13058
- maxBuffer: 256 * 1024 * 1024
13059
- });
13060
- }
13061
13072
 
13062
13073
  // src/commands/review/gatherContext.ts
13063
13074
  function gatherContext() {
13064
- const branch = execSync42("git rev-parse --abbrev-ref HEAD", {
13075
+ const branch = execSync43("git rev-parse --abbrev-ref HEAD", {
13065
13076
  encoding: "utf-8"
13066
13077
  }).trim();
13067
- const sha = execSync42("git rev-parse HEAD", { encoding: "utf-8" }).trim();
13068
- const shortSha = execSync42("git rev-parse --short=7 HEAD", {
13078
+ const sha = execSync43("git rev-parse HEAD", { encoding: "utf-8" }).trim();
13079
+ const shortSha = execSync43("git rev-parse --short=7 HEAD", {
13069
13080
  encoding: "utf-8"
13070
13081
  }).trim();
13071
13082
  const prInfo = fetchPrDiffInfo();
13072
13083
  const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
13073
- const diff2 = fetchPrDiff(prInfo.prNumber);
13084
+ const diff2 = fetchPrDiff(prInfo.prNumber, prInfo.baseSha, prInfo.headSha);
13074
13085
  return {
13075
13086
  branch,
13076
13087
  sha,
@@ -13440,6 +13451,29 @@ async function runApplySession(synthesisPath) {
13440
13451
  await done2;
13441
13452
  }
13442
13453
 
13454
+ // src/commands/review/runBacklogSession.ts
13455
+ function buildBacklogPrompt(synthesisPath) {
13456
+ return `/bug add each finding in ${synthesisPath} as a phase
13457
+
13458
+ Read ${synthesisPath}. It contains a list of findings under the \`## Findings\` heading, each in a \`### Finding:\` block with Severity, Source, Location, Impact, and Recommendation fields.
13459
+
13460
+ File ONE bug backlog item covering the review findings:
13461
+ - Include EVERY finding in the file as a phase on that single item \u2014 including findings whose Source is \`already-raised\`.
13462
+ - Each phase should be named after its finding's title and its tasks should capture the finding's Location, Impact, and Recommendation.
13463
+ - Use \`assist backlog add\` to create the item, then \`assist backlog add-phase\` for each finding.
13464
+
13465
+ Important constraints:
13466
+ - Do not edit ${synthesisPath} \u2014 leave it untouched.
13467
+ - Do not post anything to a PR.
13468
+ - Do not stage, commit, or push any changes.`;
13469
+ }
13470
+ async function runBacklogSession(synthesisPath) {
13471
+ const { done: done2 } = spawnClaude(buildBacklogPrompt(synthesisPath), {
13472
+ allowEdits: true
13473
+ });
13474
+ await done2;
13475
+ }
13476
+
13443
13477
  // src/commands/review/cachedReviewerResult.ts
13444
13478
  import { statSync as statSync2 } from "fs";
13445
13479
  function cachedReviewerResult(name, outputPath) {
@@ -14356,6 +14390,10 @@ function setupReviewDir(repoRoot, context, force) {
14356
14390
  return paths;
14357
14391
  }
14358
14392
  async function runPostSynthesis(synthesisPath, options2) {
14393
+ if (options2.backlog) {
14394
+ await runBacklogSession(synthesisPath);
14395
+ return;
14396
+ }
14359
14397
  if (options2.apply) {
14360
14398
  await runApplySession(synthesisPath);
14361
14399
  return;
@@ -14376,58 +14414,6 @@ async function reviewPr(repoRoot, options2) {
14376
14414
  console.log(`Done. Review folder: ${paths.reviewDir}`);
14377
14415
  }
14378
14416
 
14379
- // src/commands/review/gatherShaContext.ts
14380
- import { execSync as execSync43 } from "child_process";
14381
- function resolveSha(ref, format2) {
14382
- const flag = format2 === "short" ? "--short=7 " : "";
14383
- try {
14384
- return execSync43(`git rev-parse --verify ${flag}${ref}^{commit}`, {
14385
- encoding: "utf-8",
14386
- stdio: ["ignore", "pipe", "pipe"]
14387
- }).trim();
14388
- } catch {
14389
- console.error(`Error: could not resolve commit \`${ref}\`.`);
14390
- process.exit(1);
14391
- }
14392
- }
14393
- function gatherShaContext(ref) {
14394
- const sha = resolveSha(ref, "long");
14395
- const shortSha = resolveSha(sha, "short");
14396
- const parentSha = resolveSha(`${sha}^`, "long");
14397
- const range = `${parentSha}..${sha}`;
14398
- const changedFiles = execSync43(`git diff --name-only ${range}`, {
14399
- encoding: "utf-8",
14400
- maxBuffer: 64 * 1024 * 1024
14401
- }).trim().split("\n").filter(Boolean);
14402
- const diff2 = execSync43(`git diff ${range}`, {
14403
- encoding: "utf-8",
14404
- maxBuffer: 256 * 1024 * 1024
14405
- });
14406
- return { sha, shortSha, parentSha, changedFiles, diff: diff2 };
14407
- }
14408
-
14409
- // src/commands/review/reviewSha.ts
14410
- function gatherShaChangedContext(ref) {
14411
- const context = gatherShaContext(ref);
14412
- if (context.changedFiles.length > 0) return context;
14413
- console.error(
14414
- `Error: commit ${context.sha} has no changed files \u2014 nothing to review.`
14415
- );
14416
- process.exit(1);
14417
- }
14418
- function setupShaReviewDir(repoRoot, context, force) {
14419
- const paths = buildReviewPaths(repoRoot, context.shortSha);
14420
- prepareReviewDir(paths, buildShaRequest(context), force);
14421
- console.log(`Review folder: ${paths.reviewDir}`);
14422
- return paths;
14423
- }
14424
- async function reviewSha(repoRoot, options2) {
14425
- const context = gatherShaChangedContext(options2.sha);
14426
- const paths = setupShaReviewDir(repoRoot, context, options2.force ?? false);
14427
- await runReviewPipeline(paths, { verbose: options2.verbose ?? false });
14428
- console.log(`Done. Review folder: ${paths.reviewDir}`);
14429
- }
14430
-
14431
14417
  // src/commands/review/review.ts
14432
14418
  function resolveRepoRoot() {
14433
14419
  const repoRoot = findRepoRoot(process.cwd());
@@ -14435,41 +14421,40 @@ function resolveRepoRoot() {
14435
14421
  console.error("Error: not inside a git repository.");
14436
14422
  process.exit(1);
14437
14423
  }
14438
- function rejectShaFlag(flag) {
14439
- console.error(`Error: ${flag} cannot be combined with a SHA argument.`);
14440
- process.exit(1);
14441
- }
14442
14424
  function validateOptions(options2) {
14443
14425
  if (options2.apply && options2.refine) {
14444
14426
  console.error("Error: --apply cannot be combined with --refine.");
14445
14427
  process.exit(1);
14446
14428
  }
14447
- if (!options2.sha) return;
14448
- if (options2.refine) rejectShaFlag("--refine");
14449
- if (options2.apply) rejectShaFlag("--apply");
14450
- if (options2.submit) rejectShaFlag("--submit");
14429
+ if (options2.backlog && (options2.refine || options2.apply)) {
14430
+ console.error(
14431
+ "Error: --backlog cannot be combined with --refine or --apply."
14432
+ );
14433
+ process.exit(1);
14434
+ }
14435
+ }
14436
+ function checkoutPr(number) {
14437
+ try {
14438
+ execFileSync6("gh", ["pr", "checkout", number], { stdio: "inherit" });
14439
+ } catch {
14440
+ console.error(`gh pr checkout ${number} failed; aborting.`);
14441
+ process.exit(1);
14442
+ }
14451
14443
  }
14452
14444
  async function review(options2 = {}) {
14453
14445
  validateOptions(options2);
14454
14446
  const repoRoot = resolveRepoRoot();
14455
- if (options2.sha) {
14456
- await reviewSha(repoRoot, {
14457
- sha: options2.sha,
14458
- force: options2.force,
14459
- verbose: options2.verbose
14460
- });
14461
- return;
14462
- }
14447
+ if (options2.number) checkoutPr(options2.number);
14463
14448
  await reviewPr(repoRoot, options2);
14464
14449
  }
14465
14450
 
14466
14451
  // src/commands/registerReview.ts
14467
14452
  function registerReview(program2) {
14468
14453
  program2.command("review").description(
14469
- "Run Claude and Codex in parallel to review the current branch, or a single commit when a SHA is given"
14454
+ "Run Claude and Codex in parallel to review the current branch's PR, or check out a PR by number first when given"
14470
14455
  ).argument(
14471
- "[sha]",
14472
- "Optional commit SHA to review (sha^..sha); when provided, no PR lookup or GitHub posting happens"
14456
+ "[number]",
14457
+ "Optional PR number; when provided, runs `gh pr checkout <number>` before reviewing"
14473
14458
  ).option(
14474
14459
  "--no-prompt",
14475
14460
  "Skip confirmation prompts; use flag defaults non-interactively"
@@ -14485,11 +14470,14 @@ function registerReview(program2) {
14485
14470
  ).option(
14486
14471
  "--apply",
14487
14472
  "After synthesis, launch an interactive Claude session to apply fixes for each finding; applied findings are removed from synthesis, skipped ones remain for a later post"
14473
+ ).option(
14474
+ "--backlog",
14475
+ "After synthesis, launch an interactive Claude session running /bug to file all findings as a single backlog item with one phase per finding, instead of posting to the PR"
14488
14476
  ).option(
14489
14477
  "--verbose",
14490
14478
  "Disable spinner UI and use per-line log output (per-tool lines, starting/done lines)"
14491
14479
  ).action(
14492
- (sha, options2) => review({ ...options2, sha })
14480
+ (number, options2) => review({ ...options2, number })
14493
14481
  );
14494
14482
  }
14495
14483
 
@@ -16229,7 +16217,7 @@ async function auth() {
16229
16217
  }
16230
16218
 
16231
16219
  // src/commands/roam/postRoamActivity.ts
16232
- import { execFileSync as execFileSync6 } from "child_process";
16220
+ import { execFileSync as execFileSync7 } from "child_process";
16233
16221
  import { readdirSync as readdirSync7, readFileSync as readFileSync37, statSync as statSync4 } from "fs";
16234
16222
  import { join as join47 } from "path";
16235
16223
  function findPortFile(roamDir) {
@@ -16262,7 +16250,7 @@ function postRoamActivity(app, event) {
16262
16250
  }
16263
16251
  const url = `http://127.0.0.1:${port}/api/v1/activity/${app}/${event}?pid=${app === "codex" ? 99998 : 99999}`;
16264
16252
  try {
16265
- execFileSync6("curl", ["-sf", "--max-time", "0.2", "-X", "POST", url], {
16253
+ execFileSync7("curl", ["-sf", "--max-time", "0.2", "-X", "POST", url], {
16266
16254
  stdio: "ignore"
16267
16255
  });
16268
16256
  } catch {
@@ -16389,13 +16377,13 @@ function runPreCommands(pre, cwd) {
16389
16377
  }
16390
16378
 
16391
16379
  // src/commands/run/spawnRunCommand.ts
16392
- import { execFileSync as execFileSync7, spawn as spawn8 } from "child_process";
16380
+ import { execFileSync as execFileSync8, spawn as spawn8 } from "child_process";
16393
16381
  import { existsSync as existsSync47 } from "fs";
16394
16382
  import { dirname as dirname25, join as join48, resolve as resolve11 } from "path";
16395
16383
  function resolveCommand2(command) {
16396
16384
  if (process.platform !== "win32" || command !== "bash") return command;
16397
16385
  try {
16398
- const gitPath = execFileSync7("where", ["git"], { encoding: "utf8" }).trim().split("\r\n")[0];
16386
+ const gitPath = execFileSync8("where", ["git"], { encoding: "utf8" }).trim().split("\r\n")[0];
16399
16387
  const gitRoot = resolve11(dirname25(gitPath), "..");
16400
16388
  const gitBash = join48(gitRoot, "bin", "bash.exe");
16401
16389
  if (existsSync47(gitBash)) return gitBash;
@@ -16827,7 +16815,7 @@ function summaryPathFor(jsonlPath2) {
16827
16815
  }
16828
16816
 
16829
16817
  // src/commands/sessions/summarise/summariseSession.ts
16830
- import { execFileSync as execFileSync8 } from "child_process";
16818
+ import { execFileSync as execFileSync9 } from "child_process";
16831
16819
 
16832
16820
  // src/commands/sessions/summarise/iterateUserMessages.ts
16833
16821
  import * as fs27 from "fs";
@@ -16904,7 +16892,7 @@ function summariseSession(jsonlPath2) {
16904
16892
  }
16905
16893
  const prompt = buildPrompt2(firstMessage, backlogIds);
16906
16894
  try {
16907
- const output = execFileSync8("claude", ["-p", "--model", "haiku", prompt], {
16895
+ const output = execFileSync9("claude", ["-p", "--model", "haiku", prompt], {
16908
16896
  encoding: "utf8",
16909
16897
  timeout: 3e4,
16910
16898
  stdio: ["ignore", "pipe", "ignore"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.239.0",
3
+ "version": "0.240.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {