@staff0rd/assist 0.293.0 → 0.294.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/claude/commands/pr.md +50 -2
- package/dist/index.js +55 -10
- package/package.json +1 -1
package/claude/commands/pr.md
CHANGED
|
@@ -12,9 +12,57 @@ Use `assist prs raise` to create the PR — do not use `gh pr create` directly.
|
|
|
12
12
|
- `--how <how>` — optional; how the change works (rendered as `## How`). Omit it unless the approach genuinely needs explaining.
|
|
13
13
|
- `--resolves <key>` — Jira issue key resolved by this PR; repeatable. Each key's browse URL is appended inline to the `## Why` section. Unless a Jira key is already known from the session (e.g. mentioned by the user or present on the branch), ask the user whether this PR resolves a Jira issue and for the key before raising; omit `--resolves` only if they say there isn't one.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Wrap symbols, file paths, function names, class names, variable names, config keys, CLI commands, and flag names in backticks.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
One section, one question. Each section answers exactly one thing, and implementation detail lives only in `## How`:
|
|
18
|
+
|
|
19
|
+
- `## What` — what is observably different for someone using or calling this, and nothing else. Not how it's built, not which functions/files changed.
|
|
20
|
+
- `## Why` — the problem or motivation that made the change worth doing. Not how the solution works.
|
|
21
|
+
- `## How` — only the non-obvious decisions the diff alone won't explain: a deliberate trade-off, a workaround, a reason the obvious approach was rejected. Omit it entirely by default. Never restate `## What` as mechanism, and never walk through the diff.
|
|
22
|
+
|
|
23
|
+
The most common failure is altitude bleed: the author has an implementation detail they want to include, so they spray it into whichever section they're writing — `## What` narrates the diff, `## How` restates `## What`, `## Why` smuggles in mechanism. Don't. If a sentence describes a mechanism, it belongs in `## How` — and probably shouldn't exist at all unless it's a genuine non-obvious decision.
|
|
24
|
+
|
|
25
|
+
Litmus — a sentence is almost certainly mechanism (so `## How`, or cut it) if it contains "by …ing", "so that it can", "because the X filters/needs/uses", or names an internal component, property, function, or file.
|
|
26
|
+
|
|
27
|
+
Brevity budget — keep each section within these limits:
|
|
28
|
+
|
|
29
|
+
- `## What` — a few sentences (2–3).
|
|
30
|
+
- `## Why` — 1–2 sentences.
|
|
31
|
+
- `## How` — omit by default; a sentence or two, only for non-obvious decisions, not a diff walkthrough.
|
|
32
|
+
|
|
33
|
+
Write prose. A short paragraph of a few sentences is the natural form for `## What` and `## Why` — bullets in `## What` are a smell, usually a sign the change is being narrated point-by-point like a diff. Use bullets only when there are genuinely several independent, parallel items (3+) that don't read as a paragraph; a single bullet is never right — make it a sentence. Don't pack multiple points into one long running paragraph either: a single non-list paragraph over ~600 characters or ~4 sentences is a wall of text and `assist prs raise`/`assist prs edit` will reject it.
|
|
34
|
+
|
|
35
|
+
Worked example — altitude bleed across every section.
|
|
36
|
+
|
|
37
|
+
Bad — mechanism leaks everywhere:
|
|
38
|
+
|
|
39
|
+
> ## What
|
|
40
|
+
>
|
|
41
|
+
> Adds a `RetryPolicy` wrapper that catches transient errors and re-invokes the handler with exponential backoff so callers don't see intermittent failures.
|
|
42
|
+
>
|
|
43
|
+
> ## Why
|
|
44
|
+
>
|
|
45
|
+
> Intermittent failures were surfacing to users. The handler also needs to distinguish transient from permanent errors, which the new `isTransient` check does by inspecting the status code.
|
|
46
|
+
>
|
|
47
|
+
> ## How
|
|
48
|
+
>
|
|
49
|
+
> The wrapper retries with exponential backoff on transient errors.
|
|
50
|
+
|
|
51
|
+
`## What` describes the implementation (a wrapper, backoff) rather than the observable change. `## Why`'s second sentence is mechanism dressed as motivation. `## How` just restates `## What` at a lower altitude. Good — each section stays at its own altitude:
|
|
52
|
+
|
|
53
|
+
> ## What
|
|
54
|
+
>
|
|
55
|
+
> Intermittent failures are now retried automatically instead of surfacing to the user.
|
|
56
|
+
>
|
|
57
|
+
> ## Why
|
|
58
|
+
>
|
|
59
|
+
> Transient errors were failing requests that would have succeeded on a second attempt.
|
|
60
|
+
>
|
|
61
|
+
> ## How
|
|
62
|
+
>
|
|
63
|
+
> Only errors flagged transient are retried; permanent failures still fail fast, to avoid masking real bugs behind retries.
|
|
64
|
+
|
|
65
|
+
Only the genuinely non-obvious decision survives in `## How`; everything that merely paraphrased `## What` is gone.
|
|
18
66
|
|
|
19
67
|
`assist prs raise` errors if a pull request already exists for the branch. Pass `--force` to fully overwrite the existing PR's title and body.
|
|
20
68
|
|
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.
|
|
9
|
+
version: "0.294.1",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -11327,6 +11327,45 @@ ${resolves}` : baseWhy);
|
|
|
11327
11327
|
}
|
|
11328
11328
|
|
|
11329
11329
|
// src/commands/prs/validatePrContent.ts
|
|
11330
|
+
var MAX_PARAGRAPH_CHARS = 600;
|
|
11331
|
+
var MAX_PARAGRAPH_SENTENCES = 4;
|
|
11332
|
+
function isListLine(line) {
|
|
11333
|
+
return /^\s*([-*+]|\d+[.)])\s/.test(line);
|
|
11334
|
+
}
|
|
11335
|
+
function countSentences(paragraph) {
|
|
11336
|
+
return paragraph.match(/[.!?]+(\s|$)/g)?.length ?? 0;
|
|
11337
|
+
}
|
|
11338
|
+
function splitParagraphs(body) {
|
|
11339
|
+
const paragraphs = [];
|
|
11340
|
+
let section2 = "(intro)";
|
|
11341
|
+
let lines = [];
|
|
11342
|
+
const flush = () => {
|
|
11343
|
+
if (lines.length > 0) {
|
|
11344
|
+
paragraphs.push({ section: section2, lines });
|
|
11345
|
+
lines = [];
|
|
11346
|
+
}
|
|
11347
|
+
};
|
|
11348
|
+
for (const line of body.split("\n")) {
|
|
11349
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
11350
|
+
if (heading) {
|
|
11351
|
+
flush();
|
|
11352
|
+
section2 = heading[1].trim();
|
|
11353
|
+
} else if (line.trim() === "") {
|
|
11354
|
+
flush();
|
|
11355
|
+
} else {
|
|
11356
|
+
lines.push(line);
|
|
11357
|
+
}
|
|
11358
|
+
}
|
|
11359
|
+
flush();
|
|
11360
|
+
return paragraphs;
|
|
11361
|
+
}
|
|
11362
|
+
function isWallOfText(lines) {
|
|
11363
|
+
if (lines.some(isListLine)) {
|
|
11364
|
+
return false;
|
|
11365
|
+
}
|
|
11366
|
+
const text3 = lines.join(" ").trim();
|
|
11367
|
+
return text3.length > MAX_PARAGRAPH_CHARS || countSentences(text3) > MAX_PARAGRAPH_SENTENCES;
|
|
11368
|
+
}
|
|
11330
11369
|
function validatePrContent(title, body) {
|
|
11331
11370
|
if (title.toLowerCase().includes("claude")) {
|
|
11332
11371
|
console.error("Error: PR title must not reference Claude");
|
|
@@ -11336,6 +11375,14 @@ function validatePrContent(title, body) {
|
|
|
11336
11375
|
console.error("Error: PR body must not reference Claude");
|
|
11337
11376
|
process.exit(1);
|
|
11338
11377
|
}
|
|
11378
|
+
const wall = splitParagraphs(body).find((p) => isWallOfText(p.lines));
|
|
11379
|
+
if (wall) {
|
|
11380
|
+
const chars = wall.lines.join(" ").trim().length;
|
|
11381
|
+
console.error(
|
|
11382
|
+
`Error: the "${wall.section}" section contains a wall-of-text paragraph (${chars} chars). Be concise \u2014 split it into bullet points or trim it.`
|
|
11383
|
+
);
|
|
11384
|
+
process.exit(1);
|
|
11385
|
+
}
|
|
11339
11386
|
}
|
|
11340
11387
|
|
|
11341
11388
|
// src/commands/prs/edit.ts
|
|
@@ -14969,9 +15016,11 @@ function printReviewerFailures(results) {
|
|
|
14969
15016
|
import { existsSync as existsSync38, unlinkSync as unlinkSync14 } from "fs";
|
|
14970
15017
|
|
|
14971
15018
|
// src/commands/review/buildReviewerStdin.ts
|
|
14972
|
-
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
|
|
15019
|
+
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 the request file whose absolute path is given below.
|
|
15020
|
+
|
|
15021
|
+
Your working directory is the repository under review, so you can read any file in it for context beyond the diff.
|
|
14973
15022
|
|
|
14974
|
-
Read request
|
|
15023
|
+
Read the request file, then produce a thorough code review in Markdown.
|
|
14975
15024
|
|
|
14976
15025
|
## When to flag a finding
|
|
14977
15026
|
|
|
@@ -15007,7 +15056,7 @@ List every finding that the original author would want to know about and fix. Do
|
|
|
15007
15056
|
|
|
15008
15057
|
For each finding include:
|
|
15009
15058
|
- Severity (blocker, major, minor, nit) \u2014 see rubric below
|
|
15010
|
-
- File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location. Take the line number from the diff's left gutter (its line in the new file); never count lines in request.
|
|
15059
|
+
- File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location. Take the line number from the diff's left gutter (its line in the new file); never count lines in the request file.
|
|
15011
15060
|
- Impact: what could go wrong, including the conditions under which it manifests
|
|
15012
15061
|
- Recommendation: a concrete change
|
|
15013
15062
|
|
|
@@ -15411,11 +15460,9 @@ function parseCodexEvent(line) {
|
|
|
15411
15460
|
}
|
|
15412
15461
|
|
|
15413
15462
|
// src/commands/review/runCodexReviewer.ts
|
|
15414
|
-
function codexArgs(
|
|
15463
|
+
function codexArgs(outputPath) {
|
|
15415
15464
|
return [
|
|
15416
15465
|
"exec",
|
|
15417
|
-
"--cd",
|
|
15418
|
-
reviewDir,
|
|
15419
15466
|
"--sandbox",
|
|
15420
15467
|
"read-only",
|
|
15421
15468
|
"--json",
|
|
@@ -15429,7 +15476,7 @@ async function runCodexReviewer(spec) {
|
|
|
15429
15476
|
const result = await runStreamingChild({
|
|
15430
15477
|
name: spec.name,
|
|
15431
15478
|
command,
|
|
15432
|
-
args: codexArgs(spec.
|
|
15479
|
+
args: codexArgs(spec.outputPath),
|
|
15433
15480
|
stdin: spec.stdin,
|
|
15434
15481
|
quiet: Boolean(spinner),
|
|
15435
15482
|
onLine: (line) => {
|
|
@@ -15453,7 +15500,6 @@ function resolveCodex(args) {
|
|
|
15453
15500
|
const spinner = args.multi?.create("codex \u2014 starting");
|
|
15454
15501
|
return runCodexReviewer({
|
|
15455
15502
|
name: "codex",
|
|
15456
|
-
reviewDir: args.reviewDir,
|
|
15457
15503
|
stdin: args.stdin,
|
|
15458
15504
|
outputPath: args.codexPath,
|
|
15459
15505
|
spinner
|
|
@@ -15470,7 +15516,6 @@ async function runReviewers(reviewDir, claudePath, codexPath, stdinPrompt, optio
|
|
|
15470
15516
|
multi: options2.multi
|
|
15471
15517
|
});
|
|
15472
15518
|
const codexPromise = resolveCodex({
|
|
15473
|
-
reviewDir,
|
|
15474
15519
|
codexPath,
|
|
15475
15520
|
stdin: stdinPrompt,
|
|
15476
15521
|
plan: options2.codexPlan,
|