@delegance/claude-autopilot 6.2.2 → 7.2.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/CHANGELOG.md +886 -0
- package/README.md +10 -1
- package/bin/_launcher.js +38 -23
- package/dist/src/cli/autopilot.d.ts +4 -0
- package/dist/src/cli/autopilot.js +15 -0
- package/dist/src/cli/dashboard/index.d.ts +5 -0
- package/dist/src/cli/dashboard/index.js +49 -0
- package/dist/src/cli/dashboard/login.d.ts +22 -0
- package/dist/src/cli/dashboard/login.js +260 -0
- package/dist/src/cli/dashboard/logout.d.ts +12 -0
- package/dist/src/cli/dashboard/logout.js +45 -0
- package/dist/src/cli/dashboard/status.d.ts +30 -0
- package/dist/src/cli/dashboard/status.js +65 -0
- package/dist/src/cli/dashboard/upload.d.ts +16 -0
- package/dist/src/cli/dashboard/upload.js +48 -0
- package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
- package/dist/src/cli/engine-flag-deprecation.js +20 -0
- package/dist/src/cli/help-text.d.ts +1 -1
- package/dist/src/cli/help-text.js +44 -28
- package/dist/src/cli/index.d.ts +2 -1
- package/dist/src/cli/index.js +72 -17
- package/dist/src/cli/scaffold.d.ts +39 -0
- package/dist/src/cli/scaffold.js +287 -0
- package/dist/src/cli/setup.d.ts +30 -0
- package/dist/src/cli/setup.js +137 -0
- package/dist/src/core/run-state/events.js +10 -2
- package/dist/src/core/run-state/resolve-engine.d.ts +26 -81
- package/dist/src/core/run-state/resolve-engine.js +39 -155
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +5 -9
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +26 -19
- package/dist/src/core/run-state/state.d.ts +1 -1
- package/dist/src/core/run-state/types.d.ts +8 -2
- package/dist/src/core/run-state/types.js +8 -2
- package/dist/src/dashboard/auto-upload.d.ts +26 -0
- package/dist/src/dashboard/auto-upload.js +107 -0
- package/dist/src/dashboard/config.d.ts +22 -0
- package/dist/src/dashboard/config.js +109 -0
- package/dist/src/dashboard/upload/canonical.d.ts +3 -0
- package/dist/src/dashboard/upload/canonical.js +16 -0
- package/dist/src/dashboard/upload/chain.d.ts +9 -0
- package/dist/src/dashboard/upload/chain.js +27 -0
- package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
- package/dist/src/dashboard/upload/snapshot.js +66 -0
- package/dist/src/dashboard/upload/uploader.d.ts +54 -0
- package/dist/src/dashboard/upload/uploader.js +330 -0
- package/package.json +18 -3
- package/scripts/test-runner.mjs +4 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
|
|
2
|
+
//
|
|
3
|
+
// Closes the biggest remaining day-1 friction the v7.1.6 blank-repo
|
|
4
|
+
// benchmark identified: even with auto-scaffolded CLAUDE.md and .gitignore
|
|
5
|
+
// (v7.1.7), a fresh repo still needs a hand-written package.json, tsconfig,
|
|
6
|
+
// and directory skeleton before any feature work happens. This verb reads
|
|
7
|
+
// a spec markdown file's `## Files` section and creates the listed
|
|
8
|
+
// directories + a starter package.json + tsconfig.json.
|
|
9
|
+
//
|
|
10
|
+
// Scope intentionally small:
|
|
11
|
+
// - Node ESM only (one-shot ship; per-stack expansion is v8 work).
|
|
12
|
+
// - Touches files, never overwrites (operator opted into autopilot, not
|
|
13
|
+
// into us nuking their package.json).
|
|
14
|
+
// - Inspects spec for `scripts:` / `dependencies:` / `devDependencies:`
|
|
15
|
+
// hints in plain prose; uses heuristics rather than a strict schema.
|
|
16
|
+
//
|
|
17
|
+
// Spec format expectations (matches v7.1.6 benchmark spec shape):
|
|
18
|
+
//
|
|
19
|
+
// ## Files
|
|
20
|
+
//
|
|
21
|
+
// * `package.json` — `type: module`, `bin: { foo: bin/foo.js }`,
|
|
22
|
+
// `dependencies: { @anthropic-ai/sdk: ^0.91 }`, ...
|
|
23
|
+
// * `bin/foo.js` — argv parser + main loop.
|
|
24
|
+
// * `src/baz.js` — pure function.
|
|
25
|
+
// * `tests/foo.test.js` — node:test cases.
|
|
26
|
+
// * `README.md` — usage + install.
|
|
27
|
+
//
|
|
28
|
+
// Heuristics:
|
|
29
|
+
// - Backtick-quoted paths in `## Files` bullets become directories
|
|
30
|
+
// (parent of the path) and empty placeholder files (the path itself).
|
|
31
|
+
// - JSON-ish tokens in the bullet description (`type: module`,
|
|
32
|
+
// `dependencies: { foo: ^1 }`) get parsed loosely and merged into
|
|
33
|
+
// a starter package.json.
|
|
34
|
+
// - tsconfig is a Node 22 ESM default with `allowJs+checkJs+noEmit`
|
|
35
|
+
// when the spec lists `.js` files (matches v7.1.6 benchmark project),
|
|
36
|
+
// or compiled NodeNext when it lists `.ts`.
|
|
37
|
+
//
|
|
38
|
+
// What this DELIBERATELY does NOT do:
|
|
39
|
+
// - Run `npm install`. The user can decide which package manager.
|
|
40
|
+
// - Pick a test runner if the spec doesn't say. Echoes `npm test`.
|
|
41
|
+
// - Generate the CLAUDE.md (that's v7.1.7's job).
|
|
42
|
+
//
|
|
43
|
+
// Exit codes:
|
|
44
|
+
// 0 — scaffolded (or all targets already existed; idempotent)
|
|
45
|
+
// 1 — spec file missing or not readable
|
|
46
|
+
// 2 — spec missing a `## Files` section
|
|
47
|
+
import * as fs from 'node:fs';
|
|
48
|
+
import * as fsAsync from 'node:fs/promises';
|
|
49
|
+
import * as path from 'node:path';
|
|
50
|
+
const PASS = '\x1b[32m✓\x1b[0m';
|
|
51
|
+
const SKIP = '\x1b[2m·\x1b[0m';
|
|
52
|
+
const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
|
|
53
|
+
const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
|
|
54
|
+
/**
|
|
55
|
+
* Parse the `## Files` (or `## files`) section of a spec markdown file.
|
|
56
|
+
* Tolerant: missing section returns `null`; malformed bullets are skipped
|
|
57
|
+
* silently. Returns extracted file paths + best-effort package-hint blob.
|
|
58
|
+
*/
|
|
59
|
+
export function parseSpecFiles(markdown) {
|
|
60
|
+
const filesSectionRe = /^##\s+files\s*$/im;
|
|
61
|
+
const m = filesSectionRe.exec(markdown);
|
|
62
|
+
if (!m)
|
|
63
|
+
return null;
|
|
64
|
+
const startIdx = m.index + m[0].length;
|
|
65
|
+
// Section ends at next heading or EOF.
|
|
66
|
+
const tail = markdown.slice(startIdx);
|
|
67
|
+
const nextHeadingMatch = /^#{1,6}\s+\S/m.exec(tail);
|
|
68
|
+
const sectionBody = nextHeadingMatch
|
|
69
|
+
? tail.slice(0, nextHeadingMatch.index)
|
|
70
|
+
: tail;
|
|
71
|
+
const paths = [];
|
|
72
|
+
// Bullet line: `* \`path\` — desc` or `- \`path\` — desc`.
|
|
73
|
+
const bulletRe = /^[*-]\s+`([^`]+)`/gm;
|
|
74
|
+
let bm;
|
|
75
|
+
while ((bm = bulletRe.exec(sectionBody)) !== null) {
|
|
76
|
+
const captured = bm[1];
|
|
77
|
+
if (!captured)
|
|
78
|
+
continue;
|
|
79
|
+
const raw = captured.trim();
|
|
80
|
+
// Skip prose-y entries by requiring path-shape: contains `/` or
|
|
81
|
+
// ends in known ext, OR is a known root-level file.
|
|
82
|
+
if (/[/.](?:js|ts|tsx|jsx|md|json|yaml|yml|sh|py|rs|go|rb|sql)$/i.test(raw) ||
|
|
83
|
+
raw === 'package.json' ||
|
|
84
|
+
raw === 'tsconfig.json' ||
|
|
85
|
+
raw === 'README.md' ||
|
|
86
|
+
raw === '.gitignore') {
|
|
87
|
+
paths.push(raw);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Loose package.json hint extraction. Look for inline tokens.
|
|
91
|
+
const packageHints = {};
|
|
92
|
+
if (/`?type\s*:\s*['"`]?module['"`]?/.test(sectionBody))
|
|
93
|
+
packageHints.type = 'module';
|
|
94
|
+
// bin: { foo: bin/foo.js }
|
|
95
|
+
const binMatch = /bin\s*:\s*\{\s*([^}]+)\s*\}/.exec(sectionBody);
|
|
96
|
+
const binBody = binMatch?.[1];
|
|
97
|
+
if (binBody) {
|
|
98
|
+
const entries = {};
|
|
99
|
+
for (const part of binBody.split(',')) {
|
|
100
|
+
const [name, target] = part.split(':').map((s) => s.trim().replace(/['"`]/g, ''));
|
|
101
|
+
if (name && target)
|
|
102
|
+
entries[name] = target;
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(entries).length > 0)
|
|
105
|
+
packageHints.bin = entries;
|
|
106
|
+
}
|
|
107
|
+
// dependencies: { foo: ^1 }
|
|
108
|
+
const depMatch = /dependencies\s*:\s*\{\s*([^}]+)\s*\}/i.exec(sectionBody);
|
|
109
|
+
const depBody = depMatch?.[1];
|
|
110
|
+
if (depBody) {
|
|
111
|
+
const entries = {};
|
|
112
|
+
for (const part of depBody.split(',')) {
|
|
113
|
+
const [name, version] = part.split(':').map((s) => s.trim().replace(/['"`]/g, ''));
|
|
114
|
+
if (name && version)
|
|
115
|
+
entries[name] = version;
|
|
116
|
+
}
|
|
117
|
+
if (Object.keys(entries).length > 0)
|
|
118
|
+
packageHints.dependencies = entries;
|
|
119
|
+
}
|
|
120
|
+
// scripts: { test: "..." } (handles quoted values via a 2nd pass)
|
|
121
|
+
const scriptsMatch = /scripts\s*:\s*\{\s*([^}]+)\s*\}/i.exec(sectionBody);
|
|
122
|
+
const scriptsBody = scriptsMatch?.[1];
|
|
123
|
+
if (scriptsBody) {
|
|
124
|
+
const entries = {};
|
|
125
|
+
// Use looser splitter — colon inside quoted values is fine.
|
|
126
|
+
const partRe = /([a-z_-]+)\s*:\s*["']([^"']+)["']/gi;
|
|
127
|
+
let pm;
|
|
128
|
+
while ((pm = partRe.exec(scriptsBody)) !== null) {
|
|
129
|
+
const [, key, value] = pm;
|
|
130
|
+
if (key && value)
|
|
131
|
+
entries[key] = value;
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(entries).length > 0)
|
|
134
|
+
packageHints.scripts = entries;
|
|
135
|
+
}
|
|
136
|
+
return { paths, packageHints };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build a minimal starter package.json. Caller passes in any explicit
|
|
140
|
+
* hints (parsed from spec); we layer Node 22 ESM defaults on top.
|
|
141
|
+
*/
|
|
142
|
+
export function buildStarterPackageJson(projectName, hints) {
|
|
143
|
+
const pkg = {
|
|
144
|
+
name: projectName,
|
|
145
|
+
version: '0.1.0',
|
|
146
|
+
private: true,
|
|
147
|
+
type: hints.type ?? 'module',
|
|
148
|
+
engines: { node: '>=22' },
|
|
149
|
+
scripts: {
|
|
150
|
+
test: 'node --test tests/*.test.js',
|
|
151
|
+
...hints.scripts,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
if (hints.bin)
|
|
155
|
+
pkg.bin = hints.bin;
|
|
156
|
+
if (hints.dependencies)
|
|
157
|
+
pkg.dependencies = hints.dependencies;
|
|
158
|
+
if (hints.devDependencies)
|
|
159
|
+
pkg.devDependencies = hints.devDependencies;
|
|
160
|
+
return pkg;
|
|
161
|
+
}
|
|
162
|
+
const STARTER_TSCONFIG_JS = {
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
target: 'ES2022',
|
|
165
|
+
module: 'NodeNext',
|
|
166
|
+
moduleResolution: 'NodeNext',
|
|
167
|
+
allowJs: true,
|
|
168
|
+
checkJs: true,
|
|
169
|
+
noEmit: true,
|
|
170
|
+
strict: true,
|
|
171
|
+
esModuleInterop: true,
|
|
172
|
+
skipLibCheck: true,
|
|
173
|
+
types: ['node'],
|
|
174
|
+
},
|
|
175
|
+
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
176
|
+
};
|
|
177
|
+
const STARTER_TSCONFIG_TS = {
|
|
178
|
+
compilerOptions: {
|
|
179
|
+
target: 'ES2022',
|
|
180
|
+
module: 'NodeNext',
|
|
181
|
+
moduleResolution: 'NodeNext',
|
|
182
|
+
outDir: 'dist',
|
|
183
|
+
strict: true,
|
|
184
|
+
esModuleInterop: true,
|
|
185
|
+
skipLibCheck: true,
|
|
186
|
+
types: ['node'],
|
|
187
|
+
},
|
|
188
|
+
include: ['bin/**/*', 'src/**/*', 'tests/**/*'],
|
|
189
|
+
};
|
|
190
|
+
export async function runScaffold(opts) {
|
|
191
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
192
|
+
const specAbs = path.isAbsolute(opts.specPath) ? opts.specPath : path.join(cwd, opts.specPath);
|
|
193
|
+
if (!fs.existsSync(specAbs)) {
|
|
194
|
+
process.stderr.write(`[scaffold] spec file not found: ${specAbs}\n`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const md = await fsAsync.readFile(specAbs, 'utf8');
|
|
198
|
+
const parsed = parseSpecFiles(md);
|
|
199
|
+
if (!parsed) {
|
|
200
|
+
process.stderr.write(`[scaffold] spec missing a "## Files" section: ${specAbs}\n`);
|
|
201
|
+
process.exit(2);
|
|
202
|
+
}
|
|
203
|
+
console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)}\n`);
|
|
204
|
+
const projectName = path.basename(cwd);
|
|
205
|
+
const filesCreated = [];
|
|
206
|
+
const filesSkippedExisting = [];
|
|
207
|
+
const dirsCreated = [];
|
|
208
|
+
let packageJsonAction = 'skipped-exists';
|
|
209
|
+
let tsconfigAction = 'skipped-no-ts';
|
|
210
|
+
// 1) Create directories first.
|
|
211
|
+
const dirs = new Set();
|
|
212
|
+
for (const p of parsed.paths) {
|
|
213
|
+
const d = path.dirname(p);
|
|
214
|
+
if (d && d !== '.')
|
|
215
|
+
dirs.add(d);
|
|
216
|
+
}
|
|
217
|
+
for (const d of dirs) {
|
|
218
|
+
const abs = path.join(cwd, d);
|
|
219
|
+
if (fs.existsSync(abs))
|
|
220
|
+
continue;
|
|
221
|
+
if (!opts.dryRun)
|
|
222
|
+
await fsAsync.mkdir(abs, { recursive: true });
|
|
223
|
+
dirsCreated.push(d);
|
|
224
|
+
console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
|
|
225
|
+
}
|
|
226
|
+
// 2) Create placeholder files (skip ones we'll handle specially).
|
|
227
|
+
const SPECIAL = new Set(['package.json', 'tsconfig.json']);
|
|
228
|
+
for (const p of parsed.paths) {
|
|
229
|
+
if (SPECIAL.has(p))
|
|
230
|
+
continue;
|
|
231
|
+
const abs = path.join(cwd, p);
|
|
232
|
+
if (fs.existsSync(abs)) {
|
|
233
|
+
filesSkippedExisting.push(p);
|
|
234
|
+
console.log(` ${SKIP} exists ${DIM(p)}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!opts.dryRun) {
|
|
238
|
+
await fsAsync.mkdir(path.dirname(abs), { recursive: true });
|
|
239
|
+
// Touch — empty file. Real content is the agent's job.
|
|
240
|
+
await fsAsync.writeFile(abs, '', 'utf8');
|
|
241
|
+
}
|
|
242
|
+
filesCreated.push(p);
|
|
243
|
+
console.log(` ${PASS} touch ${DIM(p)}`);
|
|
244
|
+
}
|
|
245
|
+
// 3) package.json — only if the spec lists it.
|
|
246
|
+
if (parsed.paths.includes('package.json')) {
|
|
247
|
+
const pkgAbs = path.join(cwd, 'package.json');
|
|
248
|
+
if (fs.existsSync(pkgAbs)) {
|
|
249
|
+
packageJsonAction = 'skipped-exists';
|
|
250
|
+
console.log(` ${SKIP} exists ${DIM('package.json (preserved)')}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const pkg = buildStarterPackageJson(projectName, parsed.packageHints);
|
|
254
|
+
if (!opts.dryRun) {
|
|
255
|
+
await fsAsync.writeFile(pkgAbs, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
256
|
+
}
|
|
257
|
+
packageJsonAction = 'created';
|
|
258
|
+
console.log(` ${PASS} write ${DIM('package.json (Node 22 ESM starter)')}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// 4) tsconfig.json — only if the spec lists it. JS-flavor when the
|
|
262
|
+
// other paths are predominantly .js, TS-flavor for .ts.
|
|
263
|
+
if (parsed.paths.includes('tsconfig.json')) {
|
|
264
|
+
const tsAbs = path.join(cwd, 'tsconfig.json');
|
|
265
|
+
if (fs.existsSync(tsAbs)) {
|
|
266
|
+
tsconfigAction = 'skipped-exists';
|
|
267
|
+
console.log(` ${SKIP} exists ${DIM('tsconfig.json (preserved)')}`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const otherPaths = parsed.paths.filter((p) => !SPECIAL.has(p));
|
|
271
|
+
const tsCount = otherPaths.filter((p) => /\.tsx?$/.test(p)).length;
|
|
272
|
+
const jsCount = otherPaths.filter((p) => /\.jsx?$/.test(p)).length;
|
|
273
|
+
const config = tsCount > jsCount ? STARTER_TSCONFIG_TS : STARTER_TSCONFIG_JS;
|
|
274
|
+
tsconfigAction = 'created';
|
|
275
|
+
if (!opts.dryRun) {
|
|
276
|
+
await fsAsync.writeFile(tsAbs, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
277
|
+
}
|
|
278
|
+
const flavor = config === STARTER_TSCONFIG_TS ? 'compiled TS to dist/' : 'JS w/ JSDoc + checkJs';
|
|
279
|
+
console.log(` ${PASS} write ${DIM(`tsconfig.json (${flavor})`)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
console.log(`\n${BOLD('Done.')} ${DIM(`${dirsCreated.length} dirs, ${filesCreated.length} files created, ${filesSkippedExisting.length} skipped.`)}\n`);
|
|
283
|
+
if (opts.dryRun)
|
|
284
|
+
console.log(DIM(`(--dry-run: no files were written)\n`));
|
|
285
|
+
return { filesCreated, dirsCreated, filesSkippedExisting, packageJsonAction, tsconfigAction };
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=scaffold.js.map
|
package/dist/src/cli/setup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DetectionResult } from './detector.ts';
|
|
1
2
|
export type ProfileName = 'security-strict' | 'team' | 'solo';
|
|
2
3
|
export interface SetupOptions {
|
|
3
4
|
cwd?: string;
|
|
@@ -6,4 +7,33 @@ export interface SetupOptions {
|
|
|
6
7
|
profile?: ProfileName;
|
|
7
8
|
}
|
|
8
9
|
export declare function runSetup(options?: SetupOptions): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Append `entries` to `<cwd>/.gitignore` if missing. Returns the entries
|
|
12
|
+
* actually added (empty array when all already present, .gitignore is empty
|
|
13
|
+
* + we don't want to create one, etc.).
|
|
14
|
+
*
|
|
15
|
+
* Behavior:
|
|
16
|
+
* - .gitignore exists: parse line-by-line, skip entries already present
|
|
17
|
+
* (exact match after trim, ignoring leading `!`), append the rest.
|
|
18
|
+
* - .gitignore missing: create it with the entries. Reasonable default
|
|
19
|
+
* for a fresh `setup` since the user is opting into autopilot's cache.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent: safe to call twice with the same entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ensureGitignoreEntries(cwd: string, entries: string[]): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Write a starter `<cwd>/CLAUDE.md` if none exists. Pulls stack-detection
|
|
26
|
+
* info from the same `detection` result that drove preset selection, so the
|
|
27
|
+
* scaffolded conventions match the actual project.
|
|
28
|
+
*
|
|
29
|
+
* The starter doc is intentionally short (~35 lines) — a real project will
|
|
30
|
+
* grow it. The goal is to give downstream agents an anchor for the most
|
|
31
|
+
* common "I had to guess" decisions the v7.1.6 benchmark agent reported:
|
|
32
|
+
* commit-message style, test command, error class shape, prompt location.
|
|
33
|
+
*
|
|
34
|
+
* Returns true when the file was written (false if it already exists; we
|
|
35
|
+
* never overwrite — operator opted into autopilot, not into us nuking
|
|
36
|
+
* their docs).
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureStarterClaudeMd(cwd: string, detection: DetectionResult): Promise<boolean>;
|
|
9
39
|
//# sourceMappingURL=setup.d.ts.map
|
package/dist/src/cli/setup.js
CHANGED
|
@@ -138,6 +138,26 @@ export async function runSetup(options = {}) {
|
|
|
138
138
|
for (const line of presetContent.trimEnd().split('\n')) {
|
|
139
139
|
console.log(` ${DIM(line)}`);
|
|
140
140
|
}
|
|
141
|
+
// v7.1.7 — Auto-add `.guardrail-cache/` and `node_modules/` to .gitignore.
|
|
142
|
+
// Per the v7.1.6 blank-repo benchmark, these are the two most common
|
|
143
|
+
// day-1 paper cuts: `setup` creates the cache dir on first run, and (for
|
|
144
|
+
// Node projects) `npm install` creates `node_modules` — neither belongs
|
|
145
|
+
// in git. Skipped silently if already present or no .gitignore exists
|
|
146
|
+
// and we don't want to create one without consent.
|
|
147
|
+
const gitignoreAdds = await ensureGitignoreEntries(cwd, [
|
|
148
|
+
'.guardrail-cache/',
|
|
149
|
+
'node_modules/',
|
|
150
|
+
]);
|
|
151
|
+
if (gitignoreAdds.length > 0) {
|
|
152
|
+
console.log(`\n ${PASS} Added to .gitignore: ${DIM(gitignoreAdds.join(', '))}`);
|
|
153
|
+
}
|
|
154
|
+
// v7.1.7 — Auto-scaffold a starter CLAUDE.md if none exists. Closes ~5 of
|
|
155
|
+
// 6 friction points the benchmark agent hit on a blank repo (commit
|
|
156
|
+
// style, error class shape, test runner choice, etc.).
|
|
157
|
+
const claudeMdAdded = await ensureStarterClaudeMd(cwd, detection);
|
|
158
|
+
if (claudeMdAdded) {
|
|
159
|
+
console.log(` ${PASS} Wrote starter CLAUDE.md`);
|
|
160
|
+
}
|
|
141
161
|
let hookInstalled = false;
|
|
142
162
|
if (!options.skipHook) {
|
|
143
163
|
const hookCode = await runHook('install', { cwd, silent: true });
|
|
@@ -152,6 +172,19 @@ export async function runSetup(options = {}) {
|
|
|
152
172
|
console.log('\nChecking prerequisites…');
|
|
153
173
|
await runDoctor();
|
|
154
174
|
console.log(`\n${BOLD('Next steps:')}\n`);
|
|
175
|
+
// v7.1.9 — Generic+low-confidence detection prompt. The v7.1.8 benchmark
|
|
176
|
+
// re-run on a truly blank repo (no package.json / go.mod / language signal)
|
|
177
|
+
// surfaced this: setup runs fine but downstream agents get a CLAUDE.md
|
|
178
|
+
// saying "Detected: Generic (low confidence)" with no concrete next step
|
|
179
|
+
// to improve detection. Surfacing the actionable "scaffold a stack file
|
|
180
|
+
// first" hint converts a paper-cut into a one-liner.
|
|
181
|
+
if (detection.preset === 'generic' && detection.confidence === 'low') {
|
|
182
|
+
console.log(` ${WARN} ${CYAN('Stack detection: Generic (low confidence).')}`);
|
|
183
|
+
console.log(` For higher-quality reviews + stack-specific presets, scaffold a`);
|
|
184
|
+
console.log(` package manifest first, then re-run setup:`);
|
|
185
|
+
console.log(` npm init -y ${DIM('# or: pnpm init, go mod init, cargo init')}`);
|
|
186
|
+
console.log(` npx claude-autopilot setup --force ${DIM('# re-detect with the new manifest')}\n`);
|
|
187
|
+
}
|
|
155
188
|
if (!hasKey) {
|
|
156
189
|
console.log(` 1. ${CYAN('Set an LLM API key')} — guardrail needs one to review code:`);
|
|
157
190
|
console.log(` export ANTHROPIC_API_KEY=sk-ant-... # https://console.anthropic.com/`);
|
|
@@ -175,4 +208,108 @@ export async function runSetup(options = {}) {
|
|
|
175
208
|
}
|
|
176
209
|
}
|
|
177
210
|
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// v7.1.7 — setup-verb day-1 polish helpers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* Append `entries` to `<cwd>/.gitignore` if missing. Returns the entries
|
|
216
|
+
* actually added (empty array when all already present, .gitignore is empty
|
|
217
|
+
* + we don't want to create one, etc.).
|
|
218
|
+
*
|
|
219
|
+
* Behavior:
|
|
220
|
+
* - .gitignore exists: parse line-by-line, skip entries already present
|
|
221
|
+
* (exact match after trim, ignoring leading `!`), append the rest.
|
|
222
|
+
* - .gitignore missing: create it with the entries. Reasonable default
|
|
223
|
+
* for a fresh `setup` since the user is opting into autopilot's cache.
|
|
224
|
+
*
|
|
225
|
+
* Idempotent: safe to call twice with the same entries.
|
|
226
|
+
*/
|
|
227
|
+
export async function ensureGitignoreEntries(cwd, entries) {
|
|
228
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
229
|
+
let existing = [];
|
|
230
|
+
let existingContent = '';
|
|
231
|
+
try {
|
|
232
|
+
existingContent = await fsAsync.readFile(gitignorePath, 'utf8');
|
|
233
|
+
existing = existingContent
|
|
234
|
+
.split('\n')
|
|
235
|
+
.map((l) => l.trim())
|
|
236
|
+
.filter((l) => l.length > 0 && !l.startsWith('#'));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// File doesn't exist — that's fine, we'll create it below.
|
|
240
|
+
}
|
|
241
|
+
const present = new Set(existing.map((l) => l.replace(/^!/, '')));
|
|
242
|
+
const missing = entries.filter((e) => !present.has(e.replace(/^!/, '')));
|
|
243
|
+
if (missing.length === 0)
|
|
244
|
+
return [];
|
|
245
|
+
// Build the appended block. Add a trailing newline first so we don't
|
|
246
|
+
// collide with a no-final-newline file.
|
|
247
|
+
const needsLeadingNewline = existingContent.length > 0 && !existingContent.endsWith('\n');
|
|
248
|
+
const block = (needsLeadingNewline ? '\n' : '') +
|
|
249
|
+
(existingContent.length > 0 ? '# claude-autopilot (v7.1.7+)\n' : '') +
|
|
250
|
+
missing.join('\n') +
|
|
251
|
+
'\n';
|
|
252
|
+
await fsAsync.writeFile(gitignorePath, existingContent + block, 'utf8');
|
|
253
|
+
return missing;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Write a starter `<cwd>/CLAUDE.md` if none exists. Pulls stack-detection
|
|
257
|
+
* info from the same `detection` result that drove preset selection, so the
|
|
258
|
+
* scaffolded conventions match the actual project.
|
|
259
|
+
*
|
|
260
|
+
* The starter doc is intentionally short (~35 lines) — a real project will
|
|
261
|
+
* grow it. The goal is to give downstream agents an anchor for the most
|
|
262
|
+
* common "I had to guess" decisions the v7.1.6 benchmark agent reported:
|
|
263
|
+
* commit-message style, test command, error class shape, prompt location.
|
|
264
|
+
*
|
|
265
|
+
* Returns true when the file was written (false if it already exists; we
|
|
266
|
+
* never overwrite — operator opted into autopilot, not into us nuking
|
|
267
|
+
* their docs).
|
|
268
|
+
*/
|
|
269
|
+
export async function ensureStarterClaudeMd(cwd, detection) {
|
|
270
|
+
const dest = path.join(cwd, 'CLAUDE.md');
|
|
271
|
+
if (fs.existsSync(dest))
|
|
272
|
+
return false;
|
|
273
|
+
const stackLabel = PRESET_LABELS[detection.preset] ?? detection.preset;
|
|
274
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
275
|
+
const body = [
|
|
276
|
+
`# CLAUDE.md`,
|
|
277
|
+
``,
|
|
278
|
+
`Project conventions for AI-assisted contributions. Auto-scaffolded by`,
|
|
279
|
+
`\`claude-autopilot setup\` on ${today}; edit freely.`,
|
|
280
|
+
``,
|
|
281
|
+
`## Stack`,
|
|
282
|
+
``,
|
|
283
|
+
`- **Detected:** ${stackLabel} (${detection.confidence} confidence)`,
|
|
284
|
+
`- **Test command:** \`${detection.testCommand}\``,
|
|
285
|
+
`- **Evidence:** ${detection.evidence}`,
|
|
286
|
+
``,
|
|
287
|
+
`## Conventions`,
|
|
288
|
+
``,
|
|
289
|
+
`- **Commit messages:** Conventional Commits (\`feat:\`, \`fix:\`,`,
|
|
290
|
+
` \`docs:\`, \`refactor:\`, \`test:\`, \`chore:\`). One sentence first`,
|
|
291
|
+
` line, optional body.`,
|
|
292
|
+
`- **Branches:** \`feat/<topic>\`, \`fix/<topic>\`, \`chore/<topic>\`.`,
|
|
293
|
+
`- **Errors:** prefer custom \`Error\` subclasses with a string \`code\``,
|
|
294
|
+
` field for programmatic handling. Example:`,
|
|
295
|
+
` \`\`\`ts`,
|
|
296
|
+
` class FetchFailed extends Error { code = 'fetch_failed' as const; }`,
|
|
297
|
+
` \`\`\``,
|
|
298
|
+
`- **Tests:** colocated with source under \`tests/\` or \`__tests__/\`.`,
|
|
299
|
+
` Run via \`${detection.testCommand}\`.`,
|
|
300
|
+
``,
|
|
301
|
+
`## Patterns to mimic`,
|
|
302
|
+
``,
|
|
303
|
+
`- TODO: as the project grows, list 2-3 example files agents should`,
|
|
304
|
+
` read first to learn local style.`,
|
|
305
|
+
``,
|
|
306
|
+
`## Common pitfalls`,
|
|
307
|
+
``,
|
|
308
|
+
`- TODO: list any non-obvious gotchas — env-var quirks, ordering`,
|
|
309
|
+
` requirements, footguns the test suite won't catch.`,
|
|
310
|
+
``,
|
|
311
|
+
].join('\n');
|
|
312
|
+
await fsAsync.writeFile(dest, body, 'utf8');
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
178
315
|
//# sourceMappingURL=setup.js.map
|
|
@@ -286,8 +286,16 @@ export function replayState(runDir) {
|
|
|
286
286
|
const observed = events[0].schema_version;
|
|
287
287
|
if (typeof observed === 'number' &&
|
|
288
288
|
(observed < minSupported || observed > maxSupported)) {
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
// v7.0 — when the observed version is HIGHER than this binary
|
|
290
|
+
// supports, the run was written by a newer Autopilot. Surface the
|
|
291
|
+
// "downgrade resume is not supported" hint so operators understand
|
|
292
|
+
// why a v6 binary can't pick up a v7-written run dir.
|
|
293
|
+
const downgradeHint = observed > maxSupported
|
|
294
|
+
? ` state was written by a newer Autopilot version (schema_version=${observed}; this binary supports [${minSupported}..${maxSupported}]); downgrade resume is not supported.`
|
|
295
|
+
: '';
|
|
296
|
+
throw new GuardrailError(`run dir at ${runDir} has schema_version ${observed}; this binary supports schema_version ${minSupported}..${maxSupported}.` +
|
|
297
|
+
downgradeHint +
|
|
298
|
+
` Use the version of claude-autopilot that created this run dir, or delete the run dir to start fresh.`, {
|
|
291
299
|
code: 'corrupted_state',
|
|
292
300
|
provider: 'run-state',
|
|
293
301
|
details: {
|
|
@@ -2,99 +2,44 @@
|
|
|
2
2
|
* callers which precedence layer won so they can surface it in diagnostics
|
|
3
3
|
* (`runs show`, `--json` envelopes, etc.). */
|
|
4
4
|
export interface ResolveEngineResult {
|
|
5
|
-
/** Final decision — whether the engine runs for this invocation.
|
|
5
|
+
/** Final decision — whether the engine runs for this invocation.
|
|
6
|
+
* v7.0+: always `true`. */
|
|
6
7
|
enabled: boolean;
|
|
7
|
-
/** Which precedence layer produced the decision.
|
|
8
|
+
/** Which precedence layer produced the decision. v7.0+: always
|
|
9
|
+
* `'default'` (engine is unconditionally on). */
|
|
8
10
|
source: 'cli' | 'env' | 'config' | 'default';
|
|
9
|
-
/** Human-readable explanation
|
|
10
|
-
* CLI output / debug logs. */
|
|
11
|
+
/** Human-readable explanation. */
|
|
11
12
|
reason: string;
|
|
12
|
-
/** When
|
|
13
|
-
* raw string so the caller
|
|
14
|
-
*
|
|
13
|
+
/** When the env value was malformed in pre-v7 callers, this carried
|
|
14
|
+
* the raw string so the caller could route a `run.warning`. v7.0+
|
|
15
|
+
* ignores env values entirely; field is left undefined. */
|
|
15
16
|
invalidEnvValue?: string;
|
|
16
17
|
}
|
|
17
18
|
export interface ResolveEngineOptions {
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
* the case where BOTH flags are passed before this function is called. */
|
|
19
|
+
/** Pre-v7 CLI flag override. v7.0+ ignores this — the engine is
|
|
20
|
+
* always on. */
|
|
21
21
|
cliEngine?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* variable is unset. Empty string is treated as unset (matches Node's
|
|
24
|
-
* convention). Case-insensitive parsing of the string value. */
|
|
22
|
+
/** Pre-v7 env value. v7.0+ ignores this. */
|
|
25
23
|
envValue?: string;
|
|
26
|
-
/**
|
|
27
|
-
* the config file is missing / does not declare the key. */
|
|
24
|
+
/** Pre-v7 config value. v7.0+ ignores this. */
|
|
28
25
|
configEnabled?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* the v6.1-flip behavior without needing to bump the constant globally. */
|
|
26
|
+
/** Pre-v7 built-in default override. v7.0+ ignores this. */
|
|
31
27
|
builtInDefault?: boolean;
|
|
32
28
|
}
|
|
33
|
-
/** v6.1+ ships with the engine ON by default — flipped from the v6.0
|
|
34
|
-
* default (`false`) per `docs/specs/v6.1-default-flip.md`. Exported so
|
|
35
|
-
* tests / future releases can pin a known value. */
|
|
36
|
-
export declare const ENGINE_DEFAULT_V6_1: true;
|
|
37
|
-
/** Historical v6.0 default. Preserved verbatim — its semantic meaning
|
|
38
|
-
* ("the v6.0 default was off") doesn't change just because the active
|
|
39
|
-
* default flipped. Out-of-tree consumers that pinned this constant get
|
|
40
|
-
* the value the name promises. Use `ENGINE_DEFAULT_V6_1` for the active
|
|
41
|
-
* default. Removed in v7.
|
|
42
|
-
* @deprecated Use `ENGINE_DEFAULT_V6_1` or omit `builtInDefault` to inherit
|
|
43
|
-
* the active default. */
|
|
44
|
-
export declare const ENGINE_DEFAULT_V6_0: false;
|
|
45
29
|
/** Parse a stringly-typed env value into a tri-state boolean.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* strings — that signals the caller to fall through to the next precedence
|
|
49
|
-
* layer. */
|
|
30
|
+
* Retained for back-compat with any out-of-tree callers; the v7
|
|
31
|
+
* resolver does not consult env values. */
|
|
50
32
|
export declare function parseEngineEnvValue(raw: string | undefined): boolean | undefined;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
export declare function resolveEngineEnabled(
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
* Exported for tests + downstream consumers (e.g. CI parsers) that want to
|
|
59
|
-
* match against the exact string. Kept on a single line so terminals don't
|
|
60
|
-
* wrap mid-message. */
|
|
61
|
-
export declare const ENGINE_OFF_DEPRECATION_MESSAGE = "[deprecation] --no-engine / engine.enabled: false will be removed in v7. Migrate to engine-on (default).";
|
|
62
|
-
/** Optional callback shape for the deprecation warner. Tests pass a capture
|
|
63
|
-
* function; production callers omit it and get the default `process.stderr`
|
|
64
|
-
* writer. Kept narrow (single-arg) so a `jest.fn` or a `(msg) => lines.push(msg)`
|
|
65
|
-
* array sink is trivially droppable. */
|
|
33
|
+
/** v7.0+ — engine is always on. Pure function; ignores all inputs.
|
|
34
|
+
* Source compatible with v6.x call sites. */
|
|
35
|
+
export declare function resolveEngineEnabled(_opts?: ResolveEngineOptions): ResolveEngineResult;
|
|
36
|
+
/** v6.1-era stable deprecation banner. v7.0+ never emits this string —
|
|
37
|
+
* the path is gone. Kept exported so out-of-tree consumers that imported
|
|
38
|
+
* it still type-check. */
|
|
39
|
+
export declare const ENGINE_OFF_DEPRECATION_MESSAGE = "[deprecation] --no-engine / engine.enabled: false were removed in v7.0. Migration: drop the flag/env/config.";
|
|
66
40
|
export type EngineDeprecationWarn = (message: string) => void;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* when the engine is actually on. Pure: takes the resolver result, returns
|
|
72
|
-
* a boolean.
|
|
73
|
-
*
|
|
74
|
-
* Why this is a separate predicate (not collapsed into the warner): the
|
|
75
|
-
* CLI dispatcher wants to ALSO emit a typed `run.warning` event into a
|
|
76
|
-
* ledger when the engine ends up on but the resolver came from a layer
|
|
77
|
-
* that's about to be removed — except today, on v6.1, the only path that
|
|
78
|
-
* warns IS the "engine off, explicit opt-out" path. So the predicate
|
|
79
|
-
* collapses cleanly to that single condition. v7 removes both. */
|
|
80
|
-
export declare function shouldWarnEngineOffDeprecation(resolved: Pick<ResolveEngineResult, 'enabled' | 'source'>): boolean;
|
|
81
|
-
/** Emit the v6.1 `--no-engine` deprecation warning to stderr (or the
|
|
82
|
-
* supplied `warn` callback) when the resolver result indicates the user
|
|
83
|
-
* explicitly opted out of the engine. No-op when:
|
|
84
|
-
* - the engine is on (no opt-out happened);
|
|
85
|
-
* - the source is `'default'` (v6.1's flipped default = on, so a default
|
|
86
|
-
* result with `enabled: false` is impossible without a custom
|
|
87
|
-
* `builtInDefault` override — and even that path doesn't warn since
|
|
88
|
-
* it's not a user-driven opt-out).
|
|
89
|
-
*
|
|
90
|
-
* Pure-ish: side-effect is captured behind the optional `warn` callback so
|
|
91
|
-
* tests can assert on the message without spawning a subprocess. The
|
|
92
|
-
* default warner writes to `process.stderr` with a trailing newline.
|
|
93
|
-
*
|
|
94
|
-
* Returns `true` when the warning fired, `false` when it was a no-op. The
|
|
95
|
-
* return value is purely informational — callers can use it to decide
|
|
96
|
-
* whether to also append a `run.warning` event into a run ledger (only
|
|
97
|
-
* meaningful on the engine-on path; the v6.1 deprecation only fires on
|
|
98
|
-
* engine-off, where there's no run dir to write into). */
|
|
99
|
-
export declare function emitEngineOffDeprecationWarning(resolved: Pick<ResolveEngineResult, 'enabled' | 'source'>, warn?: EngineDeprecationWarn): boolean;
|
|
41
|
+
/** v7.0+ no-op. Always returns false. */
|
|
42
|
+
export declare function shouldWarnEngineOffDeprecation(_resolved: Pick<ResolveEngineResult, 'enabled' | 'source'>): boolean;
|
|
43
|
+
/** v7.0+ no-op. Always returns false. */
|
|
44
|
+
export declare function emitEngineOffDeprecationWarning(_resolved: Pick<ResolveEngineResult, 'enabled' | 'source'>, _warn?: EngineDeprecationWarn): boolean;
|
|
100
45
|
//# sourceMappingURL=resolve-engine.d.ts.map
|