@delegance/claude-autopilot 7.3.0 → 7.4.2

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.
@@ -1,60 +1,64 @@
1
- // v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
1
+ // v7.4.0 — `claude-autopilot scaffold --from-spec <path>` (per-stack).
2
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.
3
+ // History:
4
+ // v7.2.0 initial verb, Node ESM only.
5
+ // v7.4.0 split per-stack scaffolders into ./scaffold/{node,python}.ts.
6
+ // This file remains the public entry point + stack detector +
7
+ // dispatcher; src/index.ts continues to re-export `runScaffold`,
8
+ // `parseSpecFiles`, `buildStarterPackageJson`, plus the
9
+ // `ScaffoldOptions` / `ScaffoldResult` types from here so library
10
+ // consumers don't break.
9
11
  //
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.
12
+ // Stack detection lives in `detectStack()`. Per the spec ("Stack detection"
13
+ // section), precedence is:
14
+ // 1. explicit --stack flag (validated; unknown -> exit 3)
15
+ // 2. FastAPI (path + 'fastapi' mention) — checked BEFORE generic Python so
16
+ // a FastAPI spec listing pyproject.toml isn't mis-classified
17
+ // 3. Python (pyproject.toml or requirements.txt)
18
+ // 4. Node (package.json)
19
+ // 5. Detected-but-unsupported (go.mod / Cargo.toml / Gemfile) -> exit 3
20
+ // 6. Fallback: Node ESM (preserves v7.2.0 default for ambiguous specs)
16
21
  //
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).
22
+ // Polyglot guard: package.json AND pyproject.toml together without --stack
23
+ // -> exit 3 with "polyglot spec — pass --stack to disambiguate".
42
24
  //
43
25
  // 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
26
+ // 0 — scaffolded
27
+ // 1 — spec file missing
28
+ // 2 — spec missing `## Files` section
29
+ // 3 (NEW v7.4.0) — `--stack` value not recognized, detected-but-unsupported
30
+ // stack (Go/Rust/Ruby), or polyglot spec without --stack
47
31
  import * as fs from 'node:fs';
48
32
  import * as fsAsync from 'node:fs/promises';
49
33
  import * as path from 'node:path';
50
- const PASS = '\x1b[32m✓\x1b[0m';
51
- const SKIP = '\x1b[2m·\x1b[0m';
34
+ import { scaffoldNode, buildStarterPackageJson } from "./scaffold/node.js";
35
+ import { scaffoldPython } from "./scaffold/python.js";
52
36
  const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
53
37
  const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
38
+ // Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
39
+ // the existing tests/scaffold.test.ts (which imports from this module) keep
40
+ // compiling without changes.
41
+ export { buildStarterPackageJson };
42
+ /** Valid `--stack` argument values. v7.5+ adds 'go', 'rust', 'ruby'. */
43
+ export const SUPPORTED_STACKS = ['node', 'python', 'fastapi'];
44
+ /** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
45
+ export const UNSUPPORTED_STACK_FILES = {
46
+ go: 'go.mod',
47
+ rust: 'Cargo.toml',
48
+ ruby: 'Gemfile',
49
+ };
54
50
  /**
55
51
  * Parse the `## Files` (or `## files`) section of a spec markdown file.
56
52
  * Tolerant: missing section returns `null`; malformed bullets are skipped
57
53
  * silently. Returns extracted file paths + best-effort package-hint blob.
54
+ *
55
+ * v7.4.0 also extracts:
56
+ * - `stackHint` — first prose mention of `fastapi` / `python` / `node`
57
+ * (case-insensitive). Used as a tie-breaker when path heuristics are
58
+ * ambiguous between Python and FastAPI.
59
+ * - `pythonDeps` — narrow extraction per spec ("Dependency hint
60
+ * extraction"): explicit `dependencies: [...]` block, backticked
61
+ * package names with extras, and the phrase `depends on <name>`.
58
62
  */
59
63
  export function parseSpecFiles(markdown) {
60
64
  const filesSectionRe = /^##\s+files\s*$/im;
@@ -78,12 +82,20 @@ export function parseSpecFiles(markdown) {
78
82
  continue;
79
83
  const raw = captured.trim();
80
84
  // 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) ||
85
+ // ends in known ext, OR is a known root-level file. v7.4.0 adds
86
+ // `pyproject.toml`, `requirements.txt`, `Cargo.toml`, `Gemfile`,
87
+ // `go.mod` to the root-file allowlist so the stack detector can see
88
+ // them.
89
+ if (/[/.](?:js|ts|tsx|jsx|md|json|yaml|yml|sh|py|rs|go|rb|sql|toml)$/i.test(raw) ||
83
90
  raw === 'package.json' ||
84
91
  raw === 'tsconfig.json' ||
85
92
  raw === 'README.md' ||
86
- raw === '.gitignore') {
93
+ raw === '.gitignore' ||
94
+ raw === 'pyproject.toml' ||
95
+ raw === 'requirements.txt' ||
96
+ raw === 'Cargo.toml' ||
97
+ raw === 'Gemfile' ||
98
+ raw === 'go.mod') {
87
99
  paths.push(raw);
88
100
  }
89
101
  }
@@ -104,12 +116,14 @@ export function parseSpecFiles(markdown) {
104
116
  if (Object.keys(entries).length > 0)
105
117
  packageHints.bin = entries;
106
118
  }
107
- // dependencies: { foo: ^1 }
108
- const depMatch = /dependencies\s*:\s*\{\s*([^}]+)\s*\}/i.exec(sectionBody);
109
- const depBody = depMatch?.[1];
110
- if (depBody) {
119
+ // dependencies: { foo: ^1 } — Node-shape (object form).
120
+ // Skip if it looks like an array (Python-shape `dependencies: [...]`)
121
+ // that's handled below as a Python dep block.
122
+ const depObjMatch = /dependencies\s*:\s*\{\s*([^}]+)\s*\}/i.exec(sectionBody);
123
+ const depObjBody = depObjMatch?.[1];
124
+ if (depObjBody) {
111
125
  const entries = {};
112
- for (const part of depBody.split(',')) {
126
+ for (const part of depObjBody.split(',')) {
113
127
  const [name, version] = part.split(':').map((s) => s.trim().replace(/['"`]/g, ''));
114
128
  if (name && version)
115
129
  entries[name] = version;
@@ -133,60 +147,133 @@ export function parseSpecFiles(markdown) {
133
147
  if (Object.keys(entries).length > 0)
134
148
  packageHints.scripts = entries;
135
149
  }
150
+ // v7.4.0 — Python dep extraction (narrow contract, codex W6).
151
+ // Pattern 1: explicit `dependencies: [foo, bar, baz]` array form.
152
+ // We deliberately accept the Python-style array AFTER the Node-style
153
+ // object check above so a Node spec with `dependencies: { foo: ^1 }`
154
+ // still flows into packageHints.dependencies.
155
+ const pythonDeps = [];
156
+ const depArrayMatch = /dependencies\s*:\s*\[\s*([^\]]+)\s*\]/i.exec(sectionBody);
157
+ const depArrayBody = depArrayMatch?.[1];
158
+ if (depArrayBody) {
159
+ for (const raw of depArrayBody.split(',')) {
160
+ const cleaned = raw.trim().replace(/^[`'"]/, '').replace(/[`'"]$/, '');
161
+ if (cleaned)
162
+ pythonDeps.push(cleaned);
163
+ }
164
+ }
165
+ // Pattern 2: backticked package names with extras. We look for
166
+ // backticks containing `name[extra]` (with or without a version
167
+ // suffix). This is intentionally narrow — `foo` alone in backticks
168
+ // could be anything (filename, prose), so we require the `[extra]`
169
+ // shape to fire this pattern.
170
+ const extrasRe = /`([A-Za-z][A-Za-z0-9._-]*\[[^\]`]+\][^`]*)`/g;
171
+ let em;
172
+ while ((em = extrasRe.exec(sectionBody)) !== null) {
173
+ const value = em[1]?.trim();
174
+ if (value)
175
+ pythonDeps.push(value);
176
+ }
177
+ // Pattern 3: phrase `depends on <name>`. We capture the next
178
+ // identifier-shaped token (PEP 508 names: letters / digits / `._-`).
179
+ const dependsOnRe = /depends\s+on\s+`?([A-Za-z][A-Za-z0-9._-]*(?:\[[^\]]+\])?(?:[<>=!~][^\s`]+)?)`?/gi;
180
+ let dm;
181
+ while ((dm = dependsOnRe.exec(sectionBody)) !== null) {
182
+ const name = dm[1]?.trim();
183
+ if (name)
184
+ pythonDeps.push(name);
185
+ }
186
+ if (pythonDeps.length > 0)
187
+ packageHints.pythonDeps = pythonDeps;
188
+ // v7.4.0 — stack hint extraction. First-match wins, FastAPI checked
189
+ // before generic Python (codex C1) so prose like "FastAPI app on
190
+ // Python 3.12" classifies as fastapi, not python.
191
+ if (/\bfastapi\b/i.test(sectionBody)) {
192
+ packageHints.stackHint = 'fastapi';
193
+ }
194
+ else if (/\bpython\b/i.test(sectionBody)) {
195
+ packageHints.stackHint = 'python';
196
+ }
197
+ else if (/\bnode(?:\.js)?\s+\d+\b/i.test(sectionBody)) {
198
+ packageHints.stackHint = 'node';
199
+ }
136
200
  return { paths, packageHints };
137
201
  }
138
202
  /**
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.
203
+ * Apply the precedence ladder documented at the top of this file. Pure
204
+ * function no I/O so it's directly unit-testable.
205
+ */
206
+ export function detectStack(parsed, explicit) {
207
+ // Step 1: explicit override always wins.
208
+ if (explicit)
209
+ return { kind: 'resolved', stack: explicit };
210
+ const paths = parsed.paths;
211
+ const has = (name) => paths.includes(name);
212
+ const hasMainPy = paths.some(p => p === 'main.py' ||
213
+ p === 'app/main.py' ||
214
+ /^src\/[^/]+\/main\.py$/.test(p));
215
+ const hasFastapiMention = parsed.packageHints.stackHint === 'fastapi';
216
+ const hasPythonMarker = has('pyproject.toml') || has('requirements.txt');
217
+ const hasNodeMarker = has('package.json');
218
+ // Polyglot guard (codex W3) — Node + Python without --stack.
219
+ if (hasNodeMarker && hasPythonMarker) {
220
+ return {
221
+ kind: 'polyglot',
222
+ message: 'polyglot spec — pass --stack to disambiguate',
223
+ };
224
+ }
225
+ // Step 2: FastAPI (BEFORE generic Python — codex C1).
226
+ if (hasMainPy && hasFastapiMention) {
227
+ return { kind: 'resolved', stack: 'fastapi' };
228
+ }
229
+ // Edge: spec lists pyproject.toml AND mentions FastAPI in prose but
230
+ // doesn't list main.py — still classify as FastAPI; we generate
231
+ // main.py ourselves anyway.
232
+ if (hasPythonMarker && hasFastapiMention) {
233
+ return { kind: 'resolved', stack: 'fastapi' };
234
+ }
235
+ // Step 3: Python.
236
+ if (hasPythonMarker)
237
+ return { kind: 'resolved', stack: 'python' };
238
+ // Step 4: Node.
239
+ if (hasNodeMarker)
240
+ return { kind: 'resolved', stack: 'node' };
241
+ // Step 5: detected-but-unsupported (codex W2).
242
+ for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
243
+ if (has(file)) {
244
+ return {
245
+ kind: 'unsupported',
246
+ stack,
247
+ message: `${stack} detected but not supported until v7.5`,
248
+ };
249
+ }
250
+ }
251
+ // Step 6: fallback — Node ESM (preserves v7.2.0 default for ambiguous
252
+ // specs that listed only paths with no root-marker file).
253
+ return { kind: 'resolved', stack: 'node' };
254
+ }
255
+ /**
256
+ * Print the `--list-stacks` output (codex NOTE #2). Three sections:
257
+ * Supported, Auto-detected, Recognized-but-unsupported.
141
258
  */
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;
259
+ export function printStackList() {
260
+ console.log('');
261
+ console.log(BOLD('Supported (--stack accepts these):'));
262
+ console.log(' node Node 22 ESM (package.json + tsconfig.json)');
263
+ console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
264
+ console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
265
+ console.log('');
266
+ console.log(BOLD('Auto-detected from `## Files`:'));
267
+ console.log(' node when `package.json` is listed');
268
+ console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
269
+ console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
270
+ console.log('');
271
+ console.log(BOLD('Recognized-but-unsupported (exit 3):'));
272
+ console.log(' go v7.5 (would detect via go.mod)');
273
+ console.log(' rust v7.5 (would detect via Cargo.toml)');
274
+ console.log(' ruby v7.5+ (would detect via Gemfile)');
275
+ console.log('');
161
276
  }
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
277
  export async function runScaffold(opts) {
191
278
  const cwd = opts.cwd ?? process.cwd();
192
279
  const specAbs = path.isAbsolute(opts.specPath) ? opts.specPath : path.join(cwd, opts.specPath);
@@ -200,88 +287,72 @@ export async function runScaffold(opts) {
200
287
  process.stderr.write(`[scaffold] spec missing a "## Files" section: ${specAbs}\n`);
201
288
  process.exit(2);
202
289
  }
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);
290
+ // Validate explicit --stack value. The CLI dispatch in src/cli/index.ts
291
+ // also validates, but doing it here too means library consumers calling
292
+ // `runScaffold({ stack: 'python' })` get the same guard.
293
+ if (opts.stack && !SUPPORTED_STACKS.includes(opts.stack)) {
294
+ process.stderr.write(`[scaffold] --stack "${opts.stack}" not recognized — supported: ${SUPPORTED_STACKS.join(', ')}\n`);
295
+ process.exit(3);
216
296
  }
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 + '/')}`);
297
+ const detection = detectStack(parsed, opts.stack);
298
+ if (detection.kind === 'unsupported') {
299
+ process.stderr.write(`[scaffold] ${detection.message}\n`);
300
+ process.exit(3);
225
301
  }
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)}`);
302
+ if (detection.kind === 'polyglot') {
303
+ process.stderr.write(`[scaffold] ${detection.message}\n`);
304
+ process.exit(3);
244
305
  }
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)')}`);
306
+ const stack = detection.stack;
307
+ console.log(`\n${BOLD('[scaffold]')} ${DIM(specAbs)} ${DIM(`(stack: ${stack})`)}\n`);
308
+ // codex W5 when --stack <python|fastapi> is explicit and the spec
309
+ // ALSO lists Node files, warn + filter them out so the Python
310
+ // scaffolder doesn't try to touch them.
311
+ let ignoredOtherStackFiles;
312
+ let parsedForStack = parsed;
313
+ if (opts.stack && (stack === 'python' || stack === 'fastapi')) {
314
+ const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
315
+ const ignored = parsed.paths.filter(p => NODE_FILES.has(p));
316
+ if (ignored.length > 0) {
317
+ ignoredOtherStackFiles = ignored;
318
+ console.log(` ${DIM(`! ignoring Node files (--stack ${stack}): ${ignored.join(', ')}`)}`);
319
+ parsedForStack = {
320
+ ...parsed,
321
+ paths: parsed.paths.filter(p => !NODE_FILES.has(p)),
322
+ };
259
323
  }
260
324
  }
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})`)}`);
325
+ else if (opts.stack === 'node') {
326
+ // Symmetric: when --stack node is forced and the spec also lists
327
+ // Python markers, drop them so we don't touch them as placeholders.
328
+ const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
329
+ const ignored = parsed.paths.filter(p => PYTHON_FILES.has(p));
330
+ if (ignored.length > 0) {
331
+ ignoredOtherStackFiles = ignored;
332
+ console.log(` ${DIM(`! ignoring Python files (--stack node): ${ignored.join(', ')}`)}`);
333
+ parsedForStack = {
334
+ ...parsed,
335
+ paths: parsed.paths.filter(p => !PYTHON_FILES.has(p)),
336
+ };
280
337
  }
281
338
  }
282
- console.log(`\n${BOLD('Done.')} ${DIM(`${dirsCreated.length} dirs, ${filesCreated.length} files created, ${filesSkippedExisting.length} skipped.`)}\n`);
339
+ const ctx = { cwd, parsed: parsedForStack, dryRun: !!opts.dryRun };
340
+ let result;
341
+ if (stack === 'python') {
342
+ result = await scaffoldPython(ctx, { isFastapi: false });
343
+ }
344
+ else if (stack === 'fastapi') {
345
+ result = await scaffoldPython(ctx, { isFastapi: true });
346
+ }
347
+ else {
348
+ result = await scaffoldNode(ctx);
349
+ }
350
+ result.stack = stack;
351
+ if (ignoredOtherStackFiles)
352
+ result.ignoredOtherStackFiles = ignoredOtherStackFiles;
353
+ console.log(`\n${BOLD('Done.')} ${DIM(`${result.dirsCreated.length} dirs, ${result.filesCreated.length} files created, ${result.filesSkippedExisting.length} skipped.`)}\n`);
283
354
  if (opts.dryRun)
284
355
  console.log(DIM(`(--dry-run: no files were written)\n`));
285
- return { filesCreated, dirsCreated, filesSkippedExisting, packageJsonAction, tsconfigAction };
356
+ return result;
286
357
  }
287
358
  //# sourceMappingURL=scaffold.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "7.3.0",
3
+ "version": "7.4.2",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "tag": "next"
@@ -25,6 +25,58 @@ The ONLY time you stop is if a step **fails and cannot be recovered**. Otherwise
25
25
 
26
26
  Brief status lines like `[autopilot] Step 3: Executing plan...` are fine. Full summaries, questions, or check-ins are not.
27
27
 
28
+ ## Codex pass policy (risk-tiered)
29
+
30
+ > Adopted from the v7.4.1 strategic review (see
31
+ > `docs/strategy/2026-05-11-codex-pivot.md`, codex finding N2).
32
+ >
33
+ > The v8 spec pass-2 finding 3 CRITICALs the original spec missed
34
+ > (especially sandbox / credential exfiltration) was concrete evidence
35
+ > that 1 codex pass is insufficient for security-sensitive architecture.
36
+ > But running 3 passes on every CLI polish spec adds latency without
37
+ > proportional value.
38
+
39
+ **Tier the spec by risk; pass count follows.**
40
+
41
+ | Spec risk | Triggers | # of codex passes |
42
+ |---|---|---|
43
+ | **Low** | CLI UX changes, doc-only PRs, scaffolding extensions, config polish, CI workflow tweaks | **1 pass** (this skill's existing pattern — codex on the committed spec) |
44
+ | **Medium** | New execution modes, auth changes, billing flows, data-access patterns, new env vars, API contracts | **2 passes** (1 on the draft spec, 1 on the merged spec after edits) |
45
+ | **High** | Sandboxing, multi-tenancy, auto-merge, anything that mutates user repos, new secrets-handling, RPC/SECURITY DEFINER changes | **3 passes** + external review (1 draft, 1 post-edit, 1 on the impl PR diff) |
46
+
47
+ **How to apply.** Spec docs declare risk in their frontmatter:
48
+
49
+ ```markdown
50
+ ---
51
+ title: <topic>
52
+ risk: low | medium | high
53
+ ---
54
+ ```
55
+
56
+ If the spec's `risk:` is omitted, default to **medium** (safer than
57
+ defaulting to low; matches the v8 spec pattern where pass-1 was
58
+ clearly insufficient).
59
+
60
+ The brainstorming skill's per-step codex pass (approach selection,
61
+ architecture, components, error handling, implementation prep) is
62
+ ALWAYS run — it's how we get a draft spec good enough to merge.
63
+ This tier policy applies to the **post-brainstorm** passes and to
64
+ the codex PR review at Step 7 below.
65
+
66
+ **Examples from v7.x:**
67
+
68
+ * v7.1.7 (setup polish — CLAUDE.md scaffold + .gitignore + dedup): low.
69
+ 1 pass on the committed spec. Caught zero CRITICALs in practice.
70
+ * v7.4.0 (Python/FastAPI scaffold extension): low. 1 pass. Found
71
+ 2 CRITICALs (FastAPI precedence, dangling entrypoint) — both
72
+ fixed pre-impl, no PR-pass surprises.
73
+ * v7.0 Phase 6 (engine-off removal + middleware revocation): high.
74
+ 3 passes (spec, post-edit, PR diff). Each pass surfaced new
75
+ trust-boundary issues; without all three the launch would have
76
+ shipped with the credential-exfiltration vector C3.
77
+ * v8.0 spec (standalone daemon): high. 2 passes so far + needs a
78
+ 3rd before any v8 alpha implementation.
79
+
28
80
  ## Pipeline
29
81
 
30
82
  Execute these steps in order. Do NOT pause between steps unless a step fails.