@delegance/claude-autopilot 6.2.2 → 7.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +841 -0
  2. package/README.md +10 -1
  3. package/bin/_launcher.js +38 -23
  4. package/dist/src/cli/autopilot.d.ts +4 -0
  5. package/dist/src/cli/autopilot.js +15 -0
  6. package/dist/src/cli/dashboard/index.d.ts +5 -0
  7. package/dist/src/cli/dashboard/index.js +49 -0
  8. package/dist/src/cli/dashboard/login.d.ts +22 -0
  9. package/dist/src/cli/dashboard/login.js +260 -0
  10. package/dist/src/cli/dashboard/logout.d.ts +12 -0
  11. package/dist/src/cli/dashboard/logout.js +45 -0
  12. package/dist/src/cli/dashboard/status.d.ts +30 -0
  13. package/dist/src/cli/dashboard/status.js +65 -0
  14. package/dist/src/cli/dashboard/upload.d.ts +16 -0
  15. package/dist/src/cli/dashboard/upload.js +48 -0
  16. package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
  17. package/dist/src/cli/engine-flag-deprecation.js +20 -0
  18. package/dist/src/cli/help-text.d.ts +1 -1
  19. package/dist/src/cli/help-text.js +44 -28
  20. package/dist/src/cli/index.d.ts +2 -1
  21. package/dist/src/cli/index.js +72 -17
  22. package/dist/src/cli/scaffold.d.ts +39 -0
  23. package/dist/src/cli/scaffold.js +287 -0
  24. package/dist/src/cli/setup.d.ts +30 -0
  25. package/dist/src/cli/setup.js +137 -0
  26. package/dist/src/core/run-state/events.js +10 -2
  27. package/dist/src/core/run-state/resolve-engine.d.ts +26 -81
  28. package/dist/src/core/run-state/resolve-engine.js +39 -155
  29. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +5 -9
  30. package/dist/src/core/run-state/run-phase-with-lifecycle.js +26 -19
  31. package/dist/src/core/run-state/state.d.ts +1 -1
  32. package/dist/src/core/run-state/types.d.ts +8 -2
  33. package/dist/src/core/run-state/types.js +8 -2
  34. package/dist/src/dashboard/auto-upload.d.ts +26 -0
  35. package/dist/src/dashboard/auto-upload.js +107 -0
  36. package/dist/src/dashboard/config.d.ts +22 -0
  37. package/dist/src/dashboard/config.js +109 -0
  38. package/dist/src/dashboard/upload/canonical.d.ts +3 -0
  39. package/dist/src/dashboard/upload/canonical.js +16 -0
  40. package/dist/src/dashboard/upload/chain.d.ts +9 -0
  41. package/dist/src/dashboard/upload/chain.js +27 -0
  42. package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
  43. package/dist/src/dashboard/upload/snapshot.js +66 -0
  44. package/dist/src/dashboard/upload/uploader.d.ts +54 -0
  45. package/dist/src/dashboard/upload/uploader.js +330 -0
  46. package/package.json +18 -3
  47. 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
@@ -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
@@ -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
- throw new GuardrailError(`run dir at ${runDir} has schema_version ${observed}; this binary supports schema_version ${minSupported}..${maxSupported}. ` +
290
- `Use the version of claude-autopilot that created this run dir, or delete the run dir to start fresh.`, {
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, suitable for run.warning details / verbose
10
- * CLI output / debug logs. */
11
+ /** Human-readable explanation. */
11
12
  reason: string;
12
- /** When `source === 'env'` and the env value was malformed, this carries the
13
- * raw string so the caller can route a `run.warning` describing the
14
- * fallthrough. Absent on the happy path. */
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
- /** True if `--engine` was passed; false if `--no-engine`; undefined if
19
- * neither flag was present. The CLI parser is responsible for rejecting
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
- /** Raw value of `process.env.CLAUDE_AUTOPILOT_ENGINE`. Undefined if the
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
- /** Value of `engine.enabled` from guardrail.config.yaml, or undefined if
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
- /** Built-in default. v6.0: false. Tests pin this explicitly to exercise
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
- * Accepts (case-insensitive): on, off, true, false, 1, 0, yes, no.
47
- * Returns undefined for any other input INCLUDING empty / whitespace-only
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
- /** Resolve whether the Run State Engine should run for this invocation.
52
- * Pure function does not touch process.env, fs, or anything I/O. */
53
- export declare function resolveEngineEnabled(opts?: ResolveEngineOptions): ResolveEngineResult;
54
- /** Stable copy emitted on stderr when a user explicitly opts out of the
55
- * engine via `--no-engine`, `CLAUDE_AUTOPILOT_ENGINE=off`, or
56
- * `engine.enabled: false`. v7 removes the escape hatch entirely.
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
- /** Decide whether v6.1's `--no-engine` deprecation warning applies for a
68
- * given resolver result. Returns `true` ONLY when the user explicitly
69
- * opted out (via CLI flag, env var, or config) — never on the v6.1 default
70
- * (which is `enabled: true`, so it can't trigger here anyway) and never
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