@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 +33 -0
- package/package.json +1 -1
- package/skills/autopilot.md +86 -71
- package/src/cli/ci.ts +38 -0
- package/src/cli/index.ts +20 -1
- package/src/cli/pr-comment.ts +137 -0
- package/src/cli/run.ts +21 -3
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
package/skills/autopilot.md
CHANGED
|
@@ -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
|
|
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
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
###
|
|
25
|
+
### `run` — pipeline on git-changed files
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
#
|
|
28
|
-
npx autopilot run
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
npx autopilot run --
|
|
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
|
-
###
|
|
36
|
+
### `ci` — opinionated CI entrypoint
|
|
44
37
|
|
|
45
38
|
```bash
|
|
46
|
-
npx autopilot
|
|
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
|
-
|
|
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
|
-
###
|
|
47
|
+
### `setup` — zero-prompt first run
|
|
52
48
|
|
|
53
49
|
```bash
|
|
54
|
-
npx autopilot
|
|
50
|
+
npx autopilot setup # auto-detect stack, write config, install hook
|
|
51
|
+
npx autopilot setup --force # overwrite existing config
|
|
55
52
|
```
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
Auto-detects: Go, Rails, FastAPI, T3, Next.js+Supabase. Runs doctor at end.
|
|
58
55
|
|
|
59
|
-
###
|
|
56
|
+
### `watch` — dev loop
|
|
60
57
|
|
|
61
58
|
```bash
|
|
62
|
-
npx autopilot watch
|
|
59
|
+
npx autopilot watch
|
|
63
60
|
npx autopilot watch --debounce 500
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
###
|
|
63
|
+
### `autoregress` — snapshot regression
|
|
67
64
|
|
|
68
65
|
```bash
|
|
69
|
-
#
|
|
70
|
-
npx autopilot autoregress
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
npx autopilot autoregress
|
|
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
|
-
###
|
|
73
|
+
### `hook` — pre-push git hook
|
|
86
74
|
|
|
87
75
|
```bash
|
|
88
|
-
npx autopilot hook install
|
|
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** —
|
|
108
|
+
**Exit code 0** — pass or warnings only. Safe to proceed.
|
|
109
|
+
**Exit code 1** — blocking findings. Fix before merging.
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
Finding severities: `critical` blocks merge, `warning` should fix, `note` informational.
|
|
98
112
|
|
|
99
|
-
|
|
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
|
-
|
|
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:
|
|
112
|
-
testCommand: npm test
|
|
113
|
-
protectedPaths:
|
|
114
|
-
-
|
|
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
|
-
|
|
137
|
+
Preset defaults at: `node_modules/@delegance/claude-autopilot/presets/<name>/autopilot.config.yaml`
|
|
121
138
|
|
|
122
|
-
##
|
|
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
|
-
|
|
149
|
+
## Integration with Development Pipeline
|
|
125
150
|
|
|
126
151
|
```bash
|
|
127
|
-
# After implementing feature
|
|
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;
|
|
64
|
-
files?: string[];
|
|
65
|
-
dryRun?: boolean;
|
|
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', '✓') :
|