@delegance/claude-autopilot 1.0.0-alpha.4 → 1.0.0-alpha.5
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 +10 -0
- package/package.json +1 -1
- package/src/cli/index.ts +16 -1
- package/src/cli/run.ts +21 -0
- package/src/formatters/github-annotations.ts +36 -0
- package/src/formatters/index.ts +3 -0
- package/src/formatters/sarif.ts +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-alpha.5 (2026-04-21)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **`--format sarif --output <path>`** on `autopilot run` — serialises `RunResult` to SARIF 2.1.0; deduplicates rules by category; normalises URIs to repo-relative forward-slash; always emits `results: []` even on error so `upload-sarif` never fails on a missing file
|
|
8
|
+
- **Auto GitHub Actions annotations** — when `GITHUB_ACTIONS=true`, `emitAnnotations()` fires after every run and writes `::error`/`::warning`/`::notice` workflow commands to stdout; GitHub renders these as inline annotations on the PR diff
|
|
9
|
+
- **`src/formatters/`** — pure formatter modules (`sarif.ts`, `github-annotations.ts`) with full command-injection encoding (`%`, `\r`, `\n`, `:`, `,`) for annotation properties and data
|
|
10
|
+
- **`action.yml`** composite action — checkout → setup-node@v4 → npx autopilot run → upload-sarif@v3; inputs: `version`, `config`, `sarif-output`, `openai-api-key`; upload step runs `if: always()` so findings surface even when run exits 1
|
|
11
|
+
- 21 new formatter tests (11 SARIF + 10 annotations) → **95 total**
|
|
12
|
+
|
|
3
13
|
## 1.0.0-alpha.4 (2026-04-21)
|
|
4
14
|
|
|
5
15
|
### New Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delegance/claude-autopilot",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Code automation pipeline: spec → plan → implement → validate → PR",
|
|
6
6
|
"keywords": ["claude", "autopilot", "ai", "pipeline", "code-review", "cli"],
|
package/src/cli/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { runWatch } from './watch.ts';
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
|
|
19
19
|
const SUBCOMMANDS = ['init', 'run', 'preflight', 'help', '--help', '-h'] as const;
|
|
20
|
-
const VALUE_FLAGS = ['base', 'config', 'files'];
|
|
20
|
+
const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce'];
|
|
21
21
|
|
|
22
22
|
// Detect first non-flag arg as subcommand, default to 'run'
|
|
23
23
|
const subcommand = (args[0] && !args[0].startsWith('--')) ? args[0] : 'run';
|
|
@@ -53,6 +53,8 @@ Options (run):
|
|
|
53
53
|
--config <path> Path to config file (default: ./autopilot.config.yaml)
|
|
54
54
|
--files <a,b,c> Explicit comma-separated file list (skips git detection)
|
|
55
55
|
--dry-run Show what would run without executing
|
|
56
|
+
--format <text|sarif> Output format (default: text)
|
|
57
|
+
--output <path> Output file path (required with --format sarif)
|
|
56
58
|
|
|
57
59
|
Options (watch):
|
|
58
60
|
--config <path> Path to config file (default: ./autopilot.config.yaml)
|
|
@@ -92,12 +94,25 @@ switch (subcommand) {
|
|
|
92
94
|
const config = flag('config');
|
|
93
95
|
const filesArg = flag('files');
|
|
94
96
|
const dryRun = boolFlag('dry-run');
|
|
97
|
+
const formatArg = flag('format');
|
|
98
|
+
const outputPath = flag('output');
|
|
99
|
+
|
|
100
|
+
if (formatArg && formatArg !== 'text' && formatArg !== 'sarif') {
|
|
101
|
+
console.error(`\x1b[31m[autopilot] --format must be "text" or "sarif"\x1b[0m`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
if (formatArg === 'sarif' && !outputPath) {
|
|
105
|
+
console.error(`\x1b[31m[autopilot] --format sarif requires --output <path>\x1b[0m`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
95
108
|
|
|
96
109
|
const code = await runCommand({
|
|
97
110
|
base,
|
|
98
111
|
configPath: config,
|
|
99
112
|
files: filesArg ? filesArg.split(',').map(f => f.trim()) : undefined,
|
|
100
113
|
dryRun,
|
|
114
|
+
format: formatArg as 'text' | 'sarif' | undefined,
|
|
115
|
+
outputPath,
|
|
101
116
|
});
|
|
102
117
|
process.exit(code);
|
|
103
118
|
break;
|
package/src/cli/run.ts
CHANGED
|
@@ -10,6 +10,14 @@ import { resolveGitTouchedFiles } from '../core/git/touched-files.ts';
|
|
|
10
10
|
import type { RunInput } from '../core/pipeline/run.ts';
|
|
11
11
|
import type { ReviewEngine } from '../adapters/review-engine/types.ts';
|
|
12
12
|
import type { AutopilotConfig } from '../core/config/types.ts';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { toSarif } from '../formatters/sarif.ts';
|
|
15
|
+
import { emitAnnotations } from '../formatters/github-annotations.ts';
|
|
16
|
+
|
|
17
|
+
function readToolVersion(): string {
|
|
18
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
19
|
+
return (JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version: string }).version;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
const C = {
|
|
15
23
|
reset: '\x1b[0m',
|
|
@@ -31,6 +39,8 @@ export interface RunCommandOptions {
|
|
|
31
39
|
base?: string; // git base ref (default HEAD~1)
|
|
32
40
|
files?: string[]; // explicit file list (skips git detection)
|
|
33
41
|
dryRun?: boolean; // skip review, print what would run
|
|
42
|
+
format?: 'text' | 'sarif';
|
|
43
|
+
outputPath?: string;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
/**
|
|
@@ -109,6 +119,17 @@ export async function runCommand(options: RunCommandOptions = {}): Promise<numbe
|
|
|
109
119
|
console.log('');
|
|
110
120
|
const result = await runAutopilot(input);
|
|
111
121
|
|
|
122
|
+
// emitAnnotations is a no-op unless GITHUB_ACTIONS=true
|
|
123
|
+
emitAnnotations(result.allFindings);
|
|
124
|
+
|
|
125
|
+
// Write SARIF output if requested
|
|
126
|
+
if (options.format === 'sarif' && options.outputPath) {
|
|
127
|
+
const sarif = toSarif(result, { toolVersion: readToolVersion(), cwd });
|
|
128
|
+
fs.mkdirSync(path.dirname(path.resolve(options.outputPath)), { recursive: true });
|
|
129
|
+
fs.writeFileSync(options.outputPath, JSON.stringify(sarif, null, 2), 'utf8');
|
|
130
|
+
console.log(fmt('dim', `[run] SARIF written to ${options.outputPath}`));
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
// Print phase summaries
|
|
113
134
|
for (const phase of result.phases) {
|
|
114
135
|
const icon = phase.status === 'pass' ? fmt('green', '✓') :
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Finding } from '../core/findings/types.ts';
|
|
2
|
+
|
|
3
|
+
export function encodeAnnotationProperty(s: string): string {
|
|
4
|
+
return s
|
|
5
|
+
.replace(/%/g, '%25')
|
|
6
|
+
.replace(/\r/g, '%0D')
|
|
7
|
+
.replace(/\n/g, '%0A')
|
|
8
|
+
.replace(/:/g, '%3A')
|
|
9
|
+
.replace(/,/g, '%2C');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function encodeAnnotationData(s: string): string {
|
|
13
|
+
return s
|
|
14
|
+
.replace(/%/g, '%25')
|
|
15
|
+
.replace(/\r/g, '%0D')
|
|
16
|
+
.replace(/\n/g, '%0A');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function severityToCommand(s: Finding['severity']): 'error' | 'warning' | 'notice' {
|
|
20
|
+
if (s === 'critical') return 'error';
|
|
21
|
+
if (s === 'warning') return 'warning';
|
|
22
|
+
return 'notice';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function emitAnnotations(findings: Finding[]): void {
|
|
26
|
+
if (process.env.GITHUB_ACTIONS !== 'true') return;
|
|
27
|
+
for (const f of findings) {
|
|
28
|
+
const cmd = severityToCommand(f.severity);
|
|
29
|
+
const props: string[] = [`file=${encodeAnnotationProperty(f.file)}`];
|
|
30
|
+
if (f.line !== undefined) {
|
|
31
|
+
props.push(`line=${f.line}`, `endLine=${f.line}`);
|
|
32
|
+
}
|
|
33
|
+
props.push(`title=${encodeAnnotationProperty(f.category)}`);
|
|
34
|
+
process.stdout.write(`::${cmd} ${props.join(',')}::${encodeAnnotationData(f.message)}\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import type { RunResult } from '../core/pipeline/run.ts';
|
|
3
|
+
import type { Finding } from '../core/findings/types.ts';
|
|
4
|
+
|
|
5
|
+
interface SarifLog {
|
|
6
|
+
$schema: string;
|
|
7
|
+
version: '2.1.0';
|
|
8
|
+
runs: SarifRun[];
|
|
9
|
+
}
|
|
10
|
+
interface SarifRun {
|
|
11
|
+
tool: { driver: SarifDriver };
|
|
12
|
+
results: SarifResult[];
|
|
13
|
+
}
|
|
14
|
+
interface SarifDriver {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
informationUri: string;
|
|
18
|
+
rules: SarifRule[];
|
|
19
|
+
}
|
|
20
|
+
interface SarifRule {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
shortDescription: { text: string };
|
|
24
|
+
}
|
|
25
|
+
interface SarifResult {
|
|
26
|
+
ruleId: string;
|
|
27
|
+
level: 'error' | 'warning' | 'note';
|
|
28
|
+
message: { text: string };
|
|
29
|
+
locations: SarifLocation[];
|
|
30
|
+
fixes?: Array<{ description: { text: string } }>;
|
|
31
|
+
}
|
|
32
|
+
interface SarifLocation {
|
|
33
|
+
physicalLocation: {
|
|
34
|
+
artifactLocation: { uri: string; uriBaseId: string };
|
|
35
|
+
region?: { startLine: number };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type { SarifLog };
|
|
40
|
+
|
|
41
|
+
export function normalizeSarifUri(file: string, cwd: string): string {
|
|
42
|
+
let rel = path.isAbsolute(file) ? path.relative(cwd, file) : file;
|
|
43
|
+
rel = rel.replace(/\\/g, '/');
|
|
44
|
+
if (rel.startsWith('./')) rel = rel.slice(2);
|
|
45
|
+
if (rel.startsWith('../')) rel = file.replace(/\\/g, '/');
|
|
46
|
+
return rel;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function severityToLevel(s: Finding['severity']): 'error' | 'warning' | 'note' {
|
|
50
|
+
if (s === 'critical') return 'error';
|
|
51
|
+
if (s === 'warning') return 'warning';
|
|
52
|
+
return 'note';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toSarif(
|
|
56
|
+
result: RunResult,
|
|
57
|
+
opts: { toolVersion: string; cwd?: string },
|
|
58
|
+
): SarifLog {
|
|
59
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
60
|
+
|
|
61
|
+
const rulesMap = new Map<string, SarifRule>();
|
|
62
|
+
for (const f of result.allFindings) {
|
|
63
|
+
if (!rulesMap.has(f.category)) {
|
|
64
|
+
rulesMap.set(f.category, {
|
|
65
|
+
id: f.category,
|
|
66
|
+
name: f.category,
|
|
67
|
+
shortDescription: { text: f.category },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const results: SarifResult[] = result.allFindings.map(f => {
|
|
73
|
+
const r: SarifResult = {
|
|
74
|
+
ruleId: f.category,
|
|
75
|
+
level: severityToLevel(f.severity),
|
|
76
|
+
message: { text: f.message },
|
|
77
|
+
locations: [{
|
|
78
|
+
physicalLocation: {
|
|
79
|
+
artifactLocation: { uri: normalizeSarifUri(f.file, cwd), uriBaseId: '%SRCROOT%' },
|
|
80
|
+
...(f.line !== undefined ? { region: { startLine: f.line } } : {}),
|
|
81
|
+
},
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
if (f.suggestion) r.fixes = [{ description: { text: f.suggestion } }];
|
|
85
|
+
return r;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
90
|
+
version: '2.1.0',
|
|
91
|
+
runs: [{
|
|
92
|
+
tool: {
|
|
93
|
+
driver: {
|
|
94
|
+
name: 'claude-autopilot',
|
|
95
|
+
version: opts.toolVersion,
|
|
96
|
+
informationUri: 'https://github.com/axledbetter/claude-autopilot',
|
|
97
|
+
rules: [...rulesMap.values()],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
results,
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|