@delegance/claude-autopilot 1.8.0 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0] — 2026-04-22
4
+
5
+ ### Added
6
+ - **`autopilot ci`** — opinionated single-command CI entrypoint; defaults to `--post-comments`, `--format sarif`, and base ref from `GITHUB_BASE_REF`/`CI_MERGE_REQUEST_TARGET_BRANCH_NAME`/`HEAD~1`; supports `--base`, `--output`, `--no-post-comments`
7
+ - **`.github/actions/ci/action.yml`** — composite GitHub Actions action; accepts `anthropic-api-key`, `openai-api-key`, `gemini-api-key`, `groq-api-key`, `base-ref`, `config`, `sarif-output`, `post-comments` inputs; runs `npx autopilot ci`, uploads SARIF via `codeql-action/upload-sarif@v3`
8
+ - **Updated `skills/autopilot.md`** — complete rewrite covering all adapters, auto-detection, `--post-comments`, `ci` command, action.yml usage
9
+
10
+ ## [1.9.0] — 2026-04-22
11
+
12
+ ### Added
13
+ - **`--post-comments` flag on `run`** — posts a formatted markdown summary to the open PR after the pipeline; edits existing autopilot comment on re-runs instead of creating a new one (tracked via `<!-- autopilot-review -->` marker)
14
+ - **`detectPrNumber()`** — reads `PR_NUMBER`/`GH_PR_NUMBER`/`GITHUB_PR_NUMBER` env vars (CI) or falls back to `gh pr view` (local)
15
+ - **`formatComment()`** — status badge, context line, phase table, critical/warning findings with `file:line`, notes in `<details>`, cost footer
16
+ - 10 new formatter tests — **215 total**
17
+
18
+ ## [1.8.0] — 2026-04-22
19
+
20
+ ### Added
21
+ - **Shared `parseReviewOutput()`** (`src/adapters/review-engine/parse-output.ts`) — extracts `file:line` attribution from review finding bodies; used by all five adapters; eliminates ~100 lines of duplicated parser code
22
+
23
+ ### Fixed
24
+ - `hardcoded-secrets` false positive on route object keys containing `password` (e.g. `forgot_password: '/forgot-password'`)
25
+
26
+ ## [1.7.2] — 2026-04-22
27
+
28
+ ### Fixed
29
+ - `hardcoded-secrets` rule no longer fires on route path values (values starting with `/`)
30
+
31
+ ## [1.7.1] — 2026-04-22
32
+
33
+ ### Added
34
+ - Detection logging: `auto-detected:` line in run output shows stack, protected paths, and test command when inferred; git context (branch + last commit) shown on every run
35
+
3
36
  ## [1.7.0] — 2026-04-22
4
37
 
5
38
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Claude Code automation pipeline: spec → plan → implement → validate → PR",
6
6
  "keywords": [
@@ -5,138 +5,153 @@ description: Run the @delegance/claude-autopilot code review pipeline — static
5
5
 
6
6
  # autopilot — Code Review Pipeline
7
7
 
8
- Runs static rules, optional LLM review (Codex), and impact-aware snapshot regression tests on git-changed files. Outputs findings inline and optionally as SARIF for GitHub Code Scanning.
8
+ Runs static rules, optional LLM review, and impact-aware snapshot regression on git-changed files. Auto-detects stack, protected paths, and test command from the project. Outputs findings inline and optionally as SARIF for GitHub Code Scanning.
9
9
 
10
10
  ## When to Use
11
11
 
12
- - Before creating a PR (catch issues before review)
13
- - After completing a feature branch (validate the full changeset)
14
- - Inside a CI pipeline step (use `--format sarif --output results.sarif`)
15
- - Anytime `validate` is called in a dev pipeline
12
+ - Before creating a PR: `npx autopilot run --base main`
13
+ - Inside CI: `npx autopilot ci` (one-command, posts PR comment + SARIF)
14
+ - Dev loop: `npx autopilot watch`
15
+ - First setup: `npx autopilot setup && npx autopilot doctor`
16
16
 
17
17
  ## Prerequisites
18
18
 
19
- Run `npx autopilot doctor` once per project setup to verify:
20
- - Node 22+, tsx, gh CLI authenticated, claude CLI, OPENAI_API_KEY, git user config
19
+ ```bash
20
+ npx autopilot doctor # checks Node 22+, tsx, gh CLI, API key, git config
21
+ ```
21
22
 
22
23
  ## Commands
23
24
 
24
- ### Run pipeline on changed files
25
+ ### `run` pipeline on git-changed files
25
26
 
26
27
  ```bash
27
- # Diff against HEAD~1 (default — last commit)
28
- npx autopilot run
29
-
30
- # Diff against a branch (typical pre-PR use)
31
- npx autopilot run --base main
32
-
33
- # Explicit file list (skip git detection)
34
- npx autopilot run --files src/foo.ts,src/bar.ts
35
-
36
- # Dry run — show what would run, no execution
37
- npx autopilot run --dry-run
38
-
39
- # SARIF output for GitHub Code Scanning
28
+ npx autopilot run # diff HEAD~1 (default)
29
+ npx autopilot run --base main # diff against branch
30
+ npx autopilot run --files src/a.ts,src/b.ts # explicit files
31
+ npx autopilot run --dry-run # show what would run
32
+ npx autopilot run --post-comments # post/update summary on open PR
40
33
  npx autopilot run --format sarif --output autopilot.sarif
41
34
  ```
42
35
 
43
- ### Zero-prompt setup (new project)
36
+ ### `ci` opinionated CI entrypoint
44
37
 
45
38
  ```bash
46
- npx autopilot setup
39
+ npx autopilot ci # base=GITHUB_BASE_REF, post-comments=true, sarif written
40
+ npx autopilot ci --base develop
41
+ npx autopilot ci --no-post-comments
42
+ npx autopilot ci --output results.sarif
47
43
  ```
48
44
 
49
- Auto-detects project type (Go, Rails, FastAPI, T3, Next.js+Supabase), writes `autopilot.config.yaml`, installs pre-push hook, runs doctor.
45
+ Equivalent to `run --base <ref> --post-comments --format sarif --output <path>`. Base ref resolves from `GITHUB_BASE_REF` `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` `HEAD~1`.
50
46
 
51
- ### Check prerequisites
47
+ ### `setup` — zero-prompt first run
52
48
 
53
49
  ```bash
54
- npx autopilot doctor
50
+ npx autopilot setup # auto-detect stack, write config, install hook
51
+ npx autopilot setup --force # overwrite existing config
55
52
  ```
56
53
 
57
- Exits 1 if blockers found. Safe to re-run anytime.
54
+ Auto-detects: Go, Rails, FastAPI, T3, Next.js+Supabase. Runs doctor at end.
58
55
 
59
- ### Watch mode (dev loop)
56
+ ### `watch` dev loop
60
57
 
61
58
  ```bash
62
- npx autopilot watch # re-run on every file save
59
+ npx autopilot watch
63
60
  npx autopilot watch --debounce 500
64
61
  ```
65
62
 
66
- ### Snapshot regression testing
63
+ ### `autoregress` — snapshot regression
67
64
 
68
65
  ```bash
69
- # Generate baselines for changed files (requires OPENAI_API_KEY)
70
- npx autopilot autoregress generate
71
-
72
- # Run only impact-selected snapshots (default fast)
73
- npx autopilot autoregress run
74
-
75
- # Run all snapshots
76
- npx autopilot autoregress run --all
77
-
78
- # Show diffs vs baselines
79
- npx autopilot autoregress diff
80
-
81
- # Overwrite baselines after intentional behavior change
82
- npx autopilot autoregress update
66
+ npx autopilot autoregress generate # create baselines for changed files
67
+ npx autopilot autoregress run # run impact-selected snapshots
68
+ npx autopilot autoregress run --all # run all snapshots
69
+ npx autopilot autoregress diff # show diffs vs baselines
70
+ npx autopilot autoregress update # overwrite baselines after intentional change
83
71
  ```
84
72
 
85
- ### Pre-push git hook
73
+ ### `hook` — pre-push git hook
86
74
 
87
75
  ```bash
88
- npx autopilot hook install # write .git/hooks/pre-push
76
+ npx autopilot hook install
89
77
  npx autopilot hook uninstall
90
78
  npx autopilot hook status
91
79
  ```
92
80
 
81
+ ## LLM Review Adapters
82
+
83
+ Configure via `reviewEngine.adapter` in `autopilot.config.yaml`:
84
+
85
+ | Adapter | Key env var | Notes |
86
+ |---------|-------------|-------|
87
+ | `auto` | any | Picks available provider; prefers the one already used in code |
88
+ | `claude` | `ANTHROPIC_API_KEY` | Claude Opus 4.7 |
89
+ | `gemini` | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini 2.5 Pro, 1M context |
90
+ | `codex` | `OPENAI_API_KEY` | gpt-5.3-codex via responses API |
91
+ | `openai-compatible` | configurable | Any OpenAI-API endpoint (Groq, Ollama, Together) |
92
+
93
+ `auto` priority order: Anthropic → Gemini → OpenAI → Groq. When multiple keys are present, `auto` scans the project source files and prefers the provider already referenced most.
94
+
95
+ ## Auto-Detection
96
+
97
+ When config fields are absent, `autopilot run` fills them in automatically:
98
+
99
+ - **stack** — parsed from `package.json`, `go.mod`, `Cargo.toml`, `requirements.txt`, `Gemfile`; injected into review prompt
100
+ - **protectedPaths** — migration dirs (`data/deltas/`, `migrations/`, `prisma/migrations/`, etc.), schema files, infra dirs (`terraform/`, `.github/workflows/`)
101
+ - **testCommand** — re-detected at run time from project files; set `testCommand: null` to disable explicitly
102
+ - **git context** — branch + last commit injected as `Change context: branch: X | last commit: Y`
103
+
104
+ Detection lines are printed dim after the file count: `auto-detected: stack: Next.js + Supabase | protected: data/deltas/** ...`
105
+
93
106
  ## Interpreting Results
94
107
 
95
- **Exit code 0** — no findings, or only warnings. Safe to proceed.
108
+ **Exit code 0** — pass or warnings only. Safe to proceed.
109
+ **Exit code 1** — blocking findings. Fix before merging.
96
110
 
97
- **Exit code 1** one or more blocking findings. Fix before merging.
111
+ Finding severities: `critical` blocks merge, `warning` should fix, `note` informational.
98
112
 
99
- **Finding severities:**
100
- - `error` — blocks merge (hardcoded secrets, npm audit Critical/High, failed tests)
101
- - `warning` — should fix, won't block
102
- - `info` — informational
113
+ PR comment (via `--post-comments` or `ci`): status badge, phase table, critical/warning findings, cost footer. Edits existing comment on re-runs (tracked via `<!-- autopilot-review -->` marker).
103
114
 
104
- **SARIF output** upload to GitHub Code Scanning with `github/codeql-action/upload-sarif@v3` for inline PR annotations.
115
+ SARIF output: upload with `github/codeql-action/upload-sarif@v3` for inline PR diff annotations. Also auto-emits `::error`/`::warning` annotations when `GITHUB_ACTIONS=true`.
105
116
 
106
117
  ## Config (`autopilot.config.yaml`)
107
118
 
108
119
  ```yaml
109
120
  configVersion: 1
110
121
  reviewEngine:
111
- adapter: codex # LLM review via OpenAI (requires OPENAI_API_KEY)
112
- testCommand: npm test
113
- protectedPaths:
114
- - src/core/**
122
+ adapter: auto # auto, claude, gemini, codex, openai-compatible
123
+ testCommand: npm test # null to disable
124
+ protectedPaths: # auto-detected if omitted
125
+ - data/deltas/**
126
+ - .github/workflows/**
115
127
  staticRules:
116
128
  - hardcoded-secrets
117
129
  - npm-audit
130
+ - package-lock-sync
131
+ - console-log
132
+ - todo-fixme
133
+ - large-file
134
+ - missing-tests
118
135
  ```
119
136
 
120
- Full schema and preset defaults: `node_modules/@delegance/claude-autopilot/presets/<name>/autopilot.config.yaml`
137
+ Preset defaults at: `node_modules/@delegance/claude-autopilot/presets/<name>/autopilot.config.yaml`
121
138
 
122
- ## Integration with Development Pipeline
139
+ ## GitHub Actions
140
+
141
+ ```yaml
142
+ - uses: axledbetter/claude-autopilot/.github/actions/ci@main
143
+ with:
144
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # or openai/gemini/groq
145
+ ```
146
+
147
+ Runs `npx autopilot ci`, uploads SARIF, annotates PR diff. All API key inputs optional — whichever is set gets used by `auto`.
123
148
 
124
- In a full spec→PR pipeline, `autopilot run` replaces the validate step:
149
+ ## Integration with Development Pipeline
125
150
 
126
151
  ```bash
127
- # After implementing feature on branch
152
+ # After implementing feature
128
153
  npx autopilot run --base main
129
154
 
130
155
  # If findings → fix → re-run (max 3 iterations)
131
156
  # If clean → push → create PR
132
157
  ```
133
-
134
- ## GitHub Actions
135
-
136
- ```yaml
137
- - uses: axledbetter/claude-autopilot/.github/actions/ci@main
138
- with:
139
- openai-api-key: ${{ secrets.OPENAI_API_KEY }}
140
- ```
141
-
142
- Runs the pipeline, uploads SARIF, annotates the PR diff inline.
package/src/cli/ci.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { runCommand } from './run.ts';
2
+
3
+ export interface CiCommandOptions {
4
+ cwd?: string;
5
+ configPath?: string;
6
+ base?: string;
7
+ postComments?: boolean;
8
+ sarifOutput?: string;
9
+ }
10
+
11
+ /**
12
+ * `autopilot ci` — opinionated single-command CI entrypoint.
13
+ *
14
+ * Equivalent to:
15
+ * autopilot run --base <ref> --post-comments --format sarif --output <path>
16
+ *
17
+ * Defaults:
18
+ * base GITHUB_BASE_REF → HEAD~1
19
+ * output autopilot.sarif
20
+ * post-comments true (skip if no PR detected — run.ts handles gracefully)
21
+ */
22
+ export async function runCi(options: CiCommandOptions = {}): Promise<number> {
23
+ const base = options.base
24
+ ?? process.env.GITHUB_BASE_REF
25
+ ?? process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME // GitLab
26
+ ?? 'HEAD~1';
27
+
28
+ const sarifOutput = options.sarifOutput ?? 'autopilot.sarif';
29
+
30
+ return runCommand({
31
+ cwd: options.cwd,
32
+ configPath: options.configPath,
33
+ base,
34
+ postComments: options.postComments ?? true,
35
+ format: 'sarif',
36
+ outputPath: sarifOutput,
37
+ });
38
+ }
package/src/cli/index.ts CHANGED
@@ -14,6 +14,7 @@ import { runCommand } from './run.ts';
14
14
  import { runWatch } from './watch.ts';
15
15
  import { runSetup } from './setup.ts';
16
16
  import { runDoctor } from './preflight.ts';
17
+ import { runCi } from './ci.ts';
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
 
@@ -27,7 +28,7 @@ if (args[0] === '--version' || args[0] === '-v') {
27
28
  process.exit(0);
28
29
  }
29
30
 
30
- const SUBCOMMANDS = ['init', 'run', 'watch', 'hook', 'autoregress', 'doctor', 'preflight', 'setup', 'help', '--help', '-h'] as const;
31
+ const SUBCOMMANDS = ['init', 'run', 'ci', 'watch', 'hook', 'autoregress', 'doctor', 'preflight', 'setup', 'help', '--help', '-h'] as const;
31
32
  const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce'];
32
33
 
33
34
  // Detect first non-flag arg as subcommand, default to 'run'
@@ -65,6 +66,7 @@ Options (run):
65
66
  --config <path> Path to config file (default: ./autopilot.config.yaml)
66
67
  --files <a,b,c> Explicit comma-separated file list (skips git detection)
67
68
  --dry-run Show what would run without executing
69
+ --post-comments Post/update a summary comment on the open PR
68
70
  --format <text|sarif> Output format (default: text)
69
71
  --output <path> Output file path (required with --format sarif)
70
72
 
@@ -118,6 +120,7 @@ switch (subcommand) {
118
120
  const config = flag('config');
119
121
  const filesArg = flag('files');
120
122
  const dryRun = boolFlag('dry-run');
123
+ const postComments = boolFlag('post-comments');
121
124
  const formatArg = flag('format');
122
125
  const outputPath = flag('output');
123
126
 
@@ -135,6 +138,7 @@ switch (subcommand) {
135
138
  configPath: config,
136
139
  files: filesArg ? filesArg.split(',').map(f => f.trim()) : undefined,
137
140
  dryRun,
141
+ postComments,
138
142
  format: formatArg as 'text' | 'sarif' | undefined,
139
143
  outputPath,
140
144
  });
@@ -142,6 +146,21 @@ switch (subcommand) {
142
146
  break;
143
147
  }
144
148
 
149
+ case 'ci': {
150
+ const base = flag('base');
151
+ const config = flag('config');
152
+ const outputPath = flag('output');
153
+ const noPostComments = boolFlag('no-post-comments');
154
+ const code = await runCi({
155
+ configPath: config,
156
+ base,
157
+ sarifOutput: outputPath,
158
+ postComments: noPostComments ? false : undefined,
159
+ });
160
+ process.exit(code);
161
+ break;
162
+ }
163
+
145
164
  case 'hook': {
146
165
  const { runHook } = await import('./hook.ts');
147
166
  const hookSub = args[1] ?? 'status';
@@ -0,0 +1,137 @@
1
+ import { runSafe } from '../core/shell.ts';
2
+ import type { RunResult } from '../core/pipeline/run.ts';
3
+ import type { AutopilotConfig } from '../core/config/types.ts';
4
+ import type { GitContext } from '../core/detect/git-context.ts';
5
+ import { readFileSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const COMMENT_MARKER = '<!-- autopilot-review -->';
10
+
11
+ function readVersion(): string {
12
+ try {
13
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
14
+ return (JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string }).version;
15
+ } catch { return 'unknown'; }
16
+ }
17
+
18
+ /** Detect the current open PR number via gh CLI or CI env vars. */
19
+ export function detectPrNumber(cwd: string): number | null {
20
+ // CI env vars set by GitHub Actions
21
+ const fromEnv = process.env.PR_NUMBER ?? process.env.GH_PR_NUMBER ?? process.env.GITHUB_PR_NUMBER;
22
+ if (fromEnv && /^\d+$/.test(fromEnv)) return parseInt(fromEnv, 10);
23
+
24
+ // gh CLI — works locally and in CI when gh is authenticated
25
+ const raw = runSafe('gh', ['pr', 'view', '--json', 'number', '--jq', '.number'], { cwd });
26
+ if (raw) {
27
+ const n = parseInt(raw.trim(), 10);
28
+ if (!isNaN(n)) return n;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /** Find the ID of a previously-posted autopilot comment, if any. */
34
+ function findExistingCommentId(pr: number, cwd: string): number | null {
35
+ const raw = runSafe('gh', ['api', `repos/{owner}/{repo}/issues/${pr}/comments`,
36
+ '--jq', `[.[] | select(.body | startswith("${COMMENT_MARKER}")) | .id] | first`], { cwd });
37
+ if (!raw) return null;
38
+ const n = parseInt(raw.trim(), 10);
39
+ return isNaN(n) ? null : n;
40
+ }
41
+
42
+ /** Format a RunResult into a markdown PR comment. */
43
+ export function formatComment(
44
+ result: RunResult,
45
+ config: AutopilotConfig,
46
+ gitCtx: GitContext,
47
+ touchedFileCount: number,
48
+ ): string {
49
+ const statusIcon = result.status === 'pass' ? '✅' : result.status === 'warn' ? '⚠️' : '❌';
50
+ const statusLabel = result.status === 'pass' ? 'Passed' : result.status === 'warn' ? 'Passed with warnings' : 'Failed';
51
+
52
+ const lines: string[] = [
53
+ COMMENT_MARKER,
54
+ `## ${statusIcon} Autopilot Review — ${statusLabel}`,
55
+ '',
56
+ ];
57
+
58
+ // Context line
59
+ const ctx: string[] = [];
60
+ if (config.stack) ctx.push(`**Stack:** ${config.stack}`);
61
+ if (gitCtx.branch) ctx.push(`**Branch:** \`${gitCtx.branch}\``);
62
+ if (gitCtx.commitMessage) ctx.push(`**Commit:** ${gitCtx.commitMessage}`);
63
+ ctx.push(`**Files reviewed:** ${touchedFileCount}`);
64
+ lines.push(ctx.join(' · '), '');
65
+
66
+ // Phase table
67
+ lines.push('| Phase | Status | Findings |');
68
+ lines.push('|---|:---:|:---:|');
69
+ for (const phase of result.phases) {
70
+ const icon = phase.status === 'pass' ? '✅' : phase.status === 'skip' ? '—' :
71
+ phase.status === 'warn' ? '⚠️' : '❌';
72
+ lines.push(`| ${phase.phase} | ${icon} | ${phase.findings.length} |`);
73
+ }
74
+ lines.push('');
75
+
76
+ // Findings by severity
77
+ const critical = result.allFindings.filter(f => f.severity === 'critical');
78
+ const warnings = result.allFindings.filter(f => f.severity === 'warning');
79
+ const notes = result.allFindings.filter(f => f.severity === 'note');
80
+
81
+ if (critical.length > 0) {
82
+ lines.push('### 🚨 Critical');
83
+ for (const f of critical) {
84
+ const loc = f.file !== '<unspecified>' ? `\`${f.file}${f.line ? `:${f.line}` : ''}\` — ` : '';
85
+ lines.push(`- ${loc}${f.message}`);
86
+ if (f.suggestion) lines.push(` > ${f.suggestion}`);
87
+ }
88
+ lines.push('');
89
+ }
90
+
91
+ if (warnings.length > 0) {
92
+ lines.push('### ⚠️ Warnings');
93
+ for (const f of warnings) {
94
+ const loc = f.file !== '<unspecified>' ? `\`${f.file}${f.line ? `:${f.line}` : ''}\` — ` : '';
95
+ lines.push(`- ${loc}${f.message}`);
96
+ if (f.suggestion) lines.push(` > ${f.suggestion}`);
97
+ }
98
+ lines.push('');
99
+ }
100
+
101
+ if (notes.length > 0) {
102
+ lines.push('<details><summary>Notes</summary>\n');
103
+ for (const f of notes) {
104
+ const loc = f.file !== '<unspecified>' ? `\`${f.file}${f.line ? `:${f.line}` : ''}\` — ` : '';
105
+ lines.push(`- ${loc}${f.message}`);
106
+ }
107
+ lines.push('\n</details>\n');
108
+ }
109
+
110
+ if (result.totalCostUSD !== undefined) {
111
+ lines.push(`*Cost: $${result.totalCostUSD.toFixed(4)} · ${result.durationMs}ms · [@delegance/claude-autopilot](https://github.com/axledbetter/claude-autopilot) v${readVersion()}*`);
112
+ } else {
113
+ lines.push(`*${result.durationMs}ms · [@delegance/claude-autopilot](https://github.com/axledbetter/claude-autopilot) v${readVersion()}*`);
114
+ }
115
+
116
+ return lines.join('\n');
117
+ }
118
+
119
+ /** Post or update the autopilot comment on the given PR. */
120
+ export async function postPrComment(
121
+ pr: number,
122
+ body: string,
123
+ cwd: string,
124
+ ): Promise<{ action: 'created' | 'updated'; url: string | null }> {
125
+ const existingId = findExistingCommentId(pr, cwd);
126
+
127
+ if (existingId) {
128
+ runSafe('gh', ['api', `repos/{owner}/{repo}/issues/comments/${existingId}`,
129
+ '--method', 'PATCH', '--field', `body=${body}`], { cwd });
130
+ return { action: 'updated', url: null };
131
+ }
132
+
133
+ const raw = runSafe('gh', ['pr', 'comment', String(pr), '--body', body], { cwd });
134
+ // gh outputs the comment URL on success
135
+ const url = raw?.trim() ?? null;
136
+ return { action: 'created', url };
137
+ }
package/src/cli/run.ts CHANGED
@@ -37,6 +37,7 @@ import { detectStack } from '../core/detect/stack.ts';
37
37
  import { detectProtectedPaths } from '../core/detect/protected-paths.ts';
38
38
  import { detectGitContext } from '../core/detect/git-context.ts';
39
39
  import { detectProject } from './detector.ts';
40
+ import { detectPrNumber, formatComment, postPrComment } from './pr-comment.ts';
40
41
 
41
42
  function readToolVersion(): string {
42
43
  const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../package.json');
@@ -60,11 +61,12 @@ function fmt(color: keyof typeof C, text: string): string {
60
61
  export interface RunCommandOptions {
61
62
  cwd?: string;
62
63
  configPath?: string;
63
- base?: string; // git base ref (default HEAD~1)
64
- files?: string[]; // explicit file list (skips git detection)
65
- dryRun?: boolean; // skip review, print what would run
64
+ base?: string; // git base ref (default HEAD~1)
65
+ files?: string[]; // explicit file list (skips git detection)
66
+ dryRun?: boolean; // skip review, print what would run
66
67
  format?: 'text' | 'sarif';
67
68
  outputPath?: string;
69
+ postComments?: boolean; // post/update summary comment on the open PR
68
70
  }
69
71
 
70
72
  /**
@@ -189,6 +191,22 @@ export async function runCommand(options: RunCommandOptions = {}): Promise<numbe
189
191
  console.log(fmt('dim', `[run] SARIF written to ${options.outputPath}`));
190
192
  }
191
193
 
194
+ // Post PR comment if requested
195
+ if (options.postComments) {
196
+ const pr = detectPrNumber(cwd);
197
+ if (!pr) {
198
+ console.log(fmt('yellow', ' [run] --post-comments: no open PR found — skipping comment'));
199
+ } else {
200
+ try {
201
+ const body = formatComment(result, config, gitCtx, touchedFiles.length);
202
+ const { action } = await postPrComment(pr, body, cwd);
203
+ console.log(fmt('dim', ` [run] PR #${pr} comment ${action}`));
204
+ } catch (err) {
205
+ console.error(fmt('yellow', ` [run] Failed to post PR comment: ${err instanceof Error ? err.message : String(err)}`));
206
+ }
207
+ }
208
+ }
209
+
192
210
  // Print phase summaries
193
211
  for (const phase of result.phases) {
194
212
  const icon = phase.status === 'pass' ? fmt('green', '✓') :