@staff0rd/assist 0.239.1 → 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 +75 -112
  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.1",
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
  \`\`\`
@@ -13465,6 +13451,29 @@ async function runApplySession(synthesisPath) {
13465
13451
  await done2;
13466
13452
  }
13467
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
+
13468
13477
  // src/commands/review/cachedReviewerResult.ts
13469
13478
  import { statSync as statSync2 } from "fs";
13470
13479
  function cachedReviewerResult(name, outputPath) {
@@ -14381,6 +14390,10 @@ function setupReviewDir(repoRoot, context, force) {
14381
14390
  return paths;
14382
14391
  }
14383
14392
  async function runPostSynthesis(synthesisPath, options2) {
14393
+ if (options2.backlog) {
14394
+ await runBacklogSession(synthesisPath);
14395
+ return;
14396
+ }
14384
14397
  if (options2.apply) {
14385
14398
  await runApplySession(synthesisPath);
14386
14399
  return;
@@ -14401,58 +14414,6 @@ async function reviewPr(repoRoot, options2) {
14401
14414
  console.log(`Done. Review folder: ${paths.reviewDir}`);
14402
14415
  }
14403
14416
 
14404
- // src/commands/review/gatherShaContext.ts
14405
- import { execSync as execSync44 } from "child_process";
14406
- function resolveSha(ref, format2) {
14407
- const flag = format2 === "short" ? "--short=7 " : "";
14408
- try {
14409
- return execSync44(`git rev-parse --verify ${flag}${ref}^{commit}`, {
14410
- encoding: "utf-8",
14411
- stdio: ["ignore", "pipe", "pipe"]
14412
- }).trim();
14413
- } catch {
14414
- console.error(`Error: could not resolve commit \`${ref}\`.`);
14415
- process.exit(1);
14416
- }
14417
- }
14418
- function gatherShaContext(ref) {
14419
- const sha = resolveSha(ref, "long");
14420
- const shortSha = resolveSha(sha, "short");
14421
- const parentSha = resolveSha(`${sha}^`, "long");
14422
- const range = `${parentSha}..${sha}`;
14423
- const changedFiles = execSync44(`git diff --name-only ${range}`, {
14424
- encoding: "utf-8",
14425
- maxBuffer: 64 * 1024 * 1024
14426
- }).trim().split("\n").filter(Boolean);
14427
- const diff2 = execSync44(`git diff ${range}`, {
14428
- encoding: "utf-8",
14429
- maxBuffer: 256 * 1024 * 1024
14430
- });
14431
- return { sha, shortSha, parentSha, changedFiles, diff: diff2 };
14432
- }
14433
-
14434
- // src/commands/review/reviewSha.ts
14435
- function gatherShaChangedContext(ref) {
14436
- const context = gatherShaContext(ref);
14437
- if (context.changedFiles.length > 0) return context;
14438
- console.error(
14439
- `Error: commit ${context.sha} has no changed files \u2014 nothing to review.`
14440
- );
14441
- process.exit(1);
14442
- }
14443
- function setupShaReviewDir(repoRoot, context, force) {
14444
- const paths = buildReviewPaths(repoRoot, context.shortSha);
14445
- prepareReviewDir(paths, buildShaRequest(context), force);
14446
- console.log(`Review folder: ${paths.reviewDir}`);
14447
- return paths;
14448
- }
14449
- async function reviewSha(repoRoot, options2) {
14450
- const context = gatherShaChangedContext(options2.sha);
14451
- const paths = setupShaReviewDir(repoRoot, context, options2.force ?? false);
14452
- await runReviewPipeline(paths, { verbose: options2.verbose ?? false });
14453
- console.log(`Done. Review folder: ${paths.reviewDir}`);
14454
- }
14455
-
14456
14417
  // src/commands/review/review.ts
14457
14418
  function resolveRepoRoot() {
14458
14419
  const repoRoot = findRepoRoot(process.cwd());
@@ -14460,41 +14421,40 @@ function resolveRepoRoot() {
14460
14421
  console.error("Error: not inside a git repository.");
14461
14422
  process.exit(1);
14462
14423
  }
14463
- function rejectShaFlag(flag) {
14464
- console.error(`Error: ${flag} cannot be combined with a SHA argument.`);
14465
- process.exit(1);
14466
- }
14467
14424
  function validateOptions(options2) {
14468
14425
  if (options2.apply && options2.refine) {
14469
14426
  console.error("Error: --apply cannot be combined with --refine.");
14470
14427
  process.exit(1);
14471
14428
  }
14472
- if (!options2.sha) return;
14473
- if (options2.refine) rejectShaFlag("--refine");
14474
- if (options2.apply) rejectShaFlag("--apply");
14475
- 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
+ }
14476
14443
  }
14477
14444
  async function review(options2 = {}) {
14478
14445
  validateOptions(options2);
14479
14446
  const repoRoot = resolveRepoRoot();
14480
- if (options2.sha) {
14481
- await reviewSha(repoRoot, {
14482
- sha: options2.sha,
14483
- force: options2.force,
14484
- verbose: options2.verbose
14485
- });
14486
- return;
14487
- }
14447
+ if (options2.number) checkoutPr(options2.number);
14488
14448
  await reviewPr(repoRoot, options2);
14489
14449
  }
14490
14450
 
14491
14451
  // src/commands/registerReview.ts
14492
14452
  function registerReview(program2) {
14493
14453
  program2.command("review").description(
14494
- "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"
14495
14455
  ).argument(
14496
- "[sha]",
14497
- "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"
14498
14458
  ).option(
14499
14459
  "--no-prompt",
14500
14460
  "Skip confirmation prompts; use flag defaults non-interactively"
@@ -14510,11 +14470,14 @@ function registerReview(program2) {
14510
14470
  ).option(
14511
14471
  "--apply",
14512
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"
14513
14476
  ).option(
14514
14477
  "--verbose",
14515
14478
  "Disable spinner UI and use per-line log output (per-tool lines, starting/done lines)"
14516
14479
  ).action(
14517
- (sha, options2) => review({ ...options2, sha })
14480
+ (number, options2) => review({ ...options2, number })
14518
14481
  );
14519
14482
  }
14520
14483
 
@@ -15859,7 +15822,7 @@ import { mkdirSync as mkdirSync14 } from "fs";
15859
15822
  import { join as join45 } from "path";
15860
15823
 
15861
15824
  // src/commands/voice/checkLockFile.ts
15862
- import { execSync as execSync45 } from "child_process";
15825
+ import { execSync as execSync44 } from "child_process";
15863
15826
  import { existsSync as existsSync44, mkdirSync as mkdirSync13, readFileSync as readFileSync34, writeFileSync as writeFileSync27 } from "fs";
15864
15827
  import { join as join44 } from "path";
15865
15828
  function isProcessAlive2(pid) {
@@ -15888,7 +15851,7 @@ function bootstrapVenv() {
15888
15851
  if (existsSync44(getVenvPython())) return;
15889
15852
  console.log("Setting up Python environment...");
15890
15853
  const pythonDir = getPythonDir();
15891
- execSync45(
15854
+ execSync44(
15892
15855
  `uv sync --project "${pythonDir}" --extra runtime --no-install-project`,
15893
15856
  {
15894
15857
  stdio: "inherit",
@@ -16055,11 +16018,11 @@ import { randomBytes } from "crypto";
16055
16018
  import chalk153 from "chalk";
16056
16019
 
16057
16020
  // src/lib/openBrowser.ts
16058
- import { execSync as execSync46 } from "child_process";
16021
+ import { execSync as execSync45 } from "child_process";
16059
16022
  function tryExec(commands) {
16060
16023
  for (const cmd of commands) {
16061
16024
  try {
16062
- execSync46(cmd);
16025
+ execSync45(cmd);
16063
16026
  return true;
16064
16027
  } catch {
16065
16028
  }
@@ -16254,7 +16217,7 @@ async function auth() {
16254
16217
  }
16255
16218
 
16256
16219
  // src/commands/roam/postRoamActivity.ts
16257
- import { execFileSync as execFileSync6 } from "child_process";
16220
+ import { execFileSync as execFileSync7 } from "child_process";
16258
16221
  import { readdirSync as readdirSync7, readFileSync as readFileSync37, statSync as statSync4 } from "fs";
16259
16222
  import { join as join47 } from "path";
16260
16223
  function findPortFile(roamDir) {
@@ -16287,7 +16250,7 @@ function postRoamActivity(app, event) {
16287
16250
  }
16288
16251
  const url = `http://127.0.0.1:${port}/api/v1/activity/${app}/${event}?pid=${app === "codex" ? 99998 : 99999}`;
16289
16252
  try {
16290
- execFileSync6("curl", ["-sf", "--max-time", "0.2", "-X", "POST", url], {
16253
+ execFileSync7("curl", ["-sf", "--max-time", "0.2", "-X", "POST", url], {
16291
16254
  stdio: "ignore"
16292
16255
  });
16293
16256
  } catch {
@@ -16401,11 +16364,11 @@ function resolveParams(params, cliArgs) {
16401
16364
  }
16402
16365
 
16403
16366
  // src/commands/run/runPreCommands.ts
16404
- import { execSync as execSync47 } from "child_process";
16367
+ import { execSync as execSync46 } from "child_process";
16405
16368
  function runPreCommands(pre, cwd) {
16406
16369
  for (const cmd of pre) {
16407
16370
  try {
16408
- execSync47(cmd, { stdio: "inherit", cwd });
16371
+ execSync46(cmd, { stdio: "inherit", cwd });
16409
16372
  } catch (err) {
16410
16373
  const code = err && typeof err === "object" && "status" in err ? err.status : 1;
16411
16374
  process.exit(code);
@@ -16414,13 +16377,13 @@ function runPreCommands(pre, cwd) {
16414
16377
  }
16415
16378
 
16416
16379
  // src/commands/run/spawnRunCommand.ts
16417
- import { execFileSync as execFileSync7, spawn as spawn8 } from "child_process";
16380
+ import { execFileSync as execFileSync8, spawn as spawn8 } from "child_process";
16418
16381
  import { existsSync as existsSync47 } from "fs";
16419
16382
  import { dirname as dirname25, join as join48, resolve as resolve11 } from "path";
16420
16383
  function resolveCommand2(command) {
16421
16384
  if (process.platform !== "win32" || command !== "bash") return command;
16422
16385
  try {
16423
- 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];
16424
16387
  const gitRoot = resolve11(dirname25(gitPath), "..");
16425
16388
  const gitBash = join48(gitRoot, "bin", "bash.exe");
16426
16389
  if (existsSync47(gitBash)) return gitBash;
@@ -16668,7 +16631,7 @@ function registerRun(program2) {
16668
16631
  }
16669
16632
 
16670
16633
  // src/commands/screenshot/index.ts
16671
- import { execSync as execSync48 } from "child_process";
16634
+ import { execSync as execSync47 } from "child_process";
16672
16635
  import { existsSync as existsSync49, mkdirSync as mkdirSync17, unlinkSync as unlinkSync15, writeFileSync as writeFileSync30 } from "fs";
16673
16636
  import { tmpdir as tmpdir7 } from "os";
16674
16637
  import { join as join51, resolve as resolve13 } from "path";
@@ -16811,7 +16774,7 @@ function runPowerShellScript(processName, outputPath) {
16811
16774
  const scriptPath = join51(tmpdir7(), `assist-screenshot-${Date.now()}.ps1`);
16812
16775
  writeFileSync30(scriptPath, captureWindowPs1, "utf-8");
16813
16776
  try {
16814
- execSync48(
16777
+ execSync47(
16815
16778
  `powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -ProcessName "${processName}" -OutputPath "${outputPath}"`,
16816
16779
  { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
16817
16780
  );
@@ -16852,7 +16815,7 @@ function summaryPathFor(jsonlPath2) {
16852
16815
  }
16853
16816
 
16854
16817
  // src/commands/sessions/summarise/summariseSession.ts
16855
- import { execFileSync as execFileSync8 } from "child_process";
16818
+ import { execFileSync as execFileSync9 } from "child_process";
16856
16819
 
16857
16820
  // src/commands/sessions/summarise/iterateUserMessages.ts
16858
16821
  import * as fs27 from "fs";
@@ -16929,7 +16892,7 @@ function summariseSession(jsonlPath2) {
16929
16892
  }
16930
16893
  const prompt = buildPrompt2(firstMessage, backlogIds);
16931
16894
  try {
16932
- const output = execFileSync8("claude", ["-p", "--model", "haiku", prompt], {
16895
+ const output = execFileSync9("claude", ["-p", "--model", "haiku", prompt], {
16933
16896
  encoding: "utf8",
16934
16897
  timeout: 3e4,
16935
16898
  stdio: ["ignore", "pipe", "ignore"]
@@ -17199,7 +17162,7 @@ function syncCommands(claudeDir, targetBase) {
17199
17162
  }
17200
17163
 
17201
17164
  // src/commands/update.ts
17202
- import { execSync as execSync49 } from "child_process";
17165
+ import { execSync as execSync48 } from "child_process";
17203
17166
  import * as path51 from "path";
17204
17167
  function isGlobalNpmInstall(dir) {
17205
17168
  try {
@@ -17207,7 +17170,7 @@ function isGlobalNpmInstall(dir) {
17207
17170
  if (resolved.split(path51.sep).includes("node_modules")) {
17208
17171
  return true;
17209
17172
  }
17210
- const globalPrefix = execSync49("npm prefix -g", { stdio: "pipe" }).toString().trim();
17173
+ const globalPrefix = execSync48("npm prefix -g", { stdio: "pipe" }).toString().trim();
17211
17174
  return resolved.toLowerCase().startsWith(path51.resolve(globalPrefix).toLowerCase());
17212
17175
  } catch {
17213
17176
  return false;
@@ -17218,18 +17181,18 @@ async function update2() {
17218
17181
  console.log(`Assist is installed at: ${installDir}`);
17219
17182
  if (isGitRepo(installDir)) {
17220
17183
  console.log("Detected git repo installation, pulling latest...");
17221
- execSync49("git pull", { cwd: installDir, stdio: "inherit" });
17184
+ execSync48("git pull", { cwd: installDir, stdio: "inherit" });
17222
17185
  console.log("Installing dependencies...");
17223
- execSync49("npm i", { cwd: installDir, stdio: "inherit" });
17186
+ execSync48("npm i", { cwd: installDir, stdio: "inherit" });
17224
17187
  console.log("Building...");
17225
- execSync49("npm run build", { cwd: installDir, stdio: "inherit" });
17188
+ execSync48("npm run build", { cwd: installDir, stdio: "inherit" });
17226
17189
  console.log("Syncing commands...");
17227
- execSync49("assist sync", { stdio: "inherit" });
17190
+ execSync48("assist sync", { stdio: "inherit" });
17228
17191
  } else if (isGlobalNpmInstall(installDir)) {
17229
17192
  console.log("Detected global npm installation, updating...");
17230
- execSync49("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
17193
+ execSync48("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
17231
17194
  console.log("Syncing commands...");
17232
- execSync49("assist sync", { stdio: "inherit" });
17195
+ execSync48("assist sync", { stdio: "inherit" });
17233
17196
  } else {
17234
17197
  console.error(
17235
17198
  "Could not determine installation method. Expected a git repo or global npm install."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.239.1",
3
+ "version": "0.240.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {