@delegance/claude-autopilot 7.5.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  - v5.6 Phase 7 (docs reconciliation) — pending.
4
4
 
5
+ ## 7.6.0 (2026-05-10)
6
+
7
+ **v7.6.0 — Go scaffold support.** Minor release. Promotes Go from
8
+ "detected-but-unsupported" (exit 3 in v7.4/v7.5) to a first-class
9
+ scaffold target, matching the Node + Python + FastAPI shape.
10
+
11
+ **New:** `claude-autopilot scaffold --from-spec <spec.md> --stack go`
12
+ (or auto-detected when the spec's `## Files` section lists `go.mod`
13
+ or `main.go`).
14
+
15
+ Generates for a basic spec:
16
+
17
+ - `go.mod` — `module <basename(cwd)>` + `go 1.22`, with an inline
18
+ comment documenting that the module path is the local-scaffold
19
+ default and should be replaced with the full hosted path
20
+ (e.g. `github.com/<user>/<name>`) before publishing.
21
+ - `main.go` — `package main` + Hello world (skipped when the spec
22
+ uses a `cmd/<name>/main.go` layout).
23
+ - `main_test.go` — `TestSmoke` stub for table-driven tests.
24
+ - `.gitignore` — idempotent augmentation: appends `vendor/`,
25
+ `*.exe`, `*.test` if not already present.
26
+
27
+ **Name normalization.** `basename(cwd)` lowercased, whitespace
28
+ collapsed to `-`. Dots + hyphens preserved (valid Go module path
29
+ chars). Path-invalid characters (`/`, `\`, control bytes) reject
30
+ with a clear error.
31
+
32
+ **Never overwrites.** `go.mod`, `main.go`, `main_test.go`, and
33
+ existing `.gitignore` entries are preserved — matches the Python
34
+ scaffolder pattern.
35
+
36
+ **Polyglot detection (codex CRITICAL pass-1).** `detectStack()` now
37
+ scans `## Files` for ALL supported stack signals (Node, Python, Go)
38
+ and exits 3 with `polyglot spec — pass --stack to disambiguate` when
39
+ more than one supported stack is present. Previously the check was
40
+ Node-vs-Python only; v7.6 closes the gap so e.g. `package.json` +
41
+ `go.mod` together correctly fail-loud instead of silently picking one.
42
+
43
+ **`--list-stacks`** now shows Go under Supported and drops it from
44
+ Recognized-but-unsupported. Rust + Ruby remain detection-only; the
45
+ Rust scaffolder is deferred to **v7.7.0** (out of scope here).
46
+
47
+ **Rust deferred.** `Cargo.toml` still exits 3 ("rust detected but
48
+ not supported until v7.7"). Targeted for v7.7.0 alongside the same
49
+ shape (Cargo.toml + `src/main.rs` + a smoke test).
50
+
5
51
  ## 7.5.0 (2026-05-10)
6
52
 
7
53
  **v7.5.0 — route-sensitivity-tiered membership revocation.** Minor
@@ -28,7 +28,7 @@ export const HELP_GROUPS = [
28
28
  verbs: [
29
29
  { verb: 'init', summary: 'Scaffold guardrail.config.yaml + auto-detect migrate stack (writes .autopilot/stack.md)' },
30
30
  { verb: 'setup', summary: 'Auto-detect stack, write config, install pre-push hook' },
31
- { verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi])' },
31
+ { verb: 'scaffold', summary: 'Scaffold project skeleton from a spec markdown (--from-spec <path> [--stack node|python|fastapi|go])' },
32
32
  { verb: 'autopilot', summary: 'Multi-phase orchestrator — run scan → spec → plan → implement under one runId (v6.2.0)' },
33
33
  { verb: 'brainstorm', summary: 'Pipeline entry point (Claude Code skill — see /brainstorm)' },
34
34
  { verb: 'spec', summary: 'Spec-writing pointer (Claude Code skill — see /brainstorm)' },
@@ -934,6 +934,7 @@ switch (subcommand) {
934
934
  case 'scaffold': {
935
935
  // v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
936
936
  // v7.4.0 — `--stack <node|python|fastapi>` + `--list-stacks`.
937
+ // v7.6.0 — `--stack go`.
937
938
  if (boolFlag('list-stacks')) {
938
939
  const { printStackList } = await import("./scaffold.js");
939
940
  printStackList();
@@ -942,8 +943,8 @@ switch (subcommand) {
942
943
  const fromSpec = flag('from-spec');
943
944
  const dryRun = boolFlag('dry-run');
944
945
  const stackArg = flag('stack');
945
- if (stackArg && !['node', 'python', 'fastapi'].includes(stackArg)) {
946
- console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi\x1b[0m`);
946
+ if (stackArg && !['node', 'python', 'fastapi', 'go'].includes(stackArg)) {
947
+ console.error(`\x1b[31m[claude-autopilot] --stack "${stackArg}" not recognized — supported: node, python, fastapi, go\x1b[0m`);
947
948
  console.error(` See: claude-autopilot scaffold --list-stacks`);
948
949
  process.exit(3);
949
950
  }
@@ -0,0 +1,27 @@
1
+ import type { ScaffoldResult, ScaffoldRunContext } from './types.ts';
2
+ /**
3
+ * Normalize a basename into a valid Go module name.
4
+ *
5
+ * - lowercased
6
+ * - whitespace runs collapse to a single `-`
7
+ * - dots + hyphens preserved (Go modules allow them)
8
+ * - empty result falls back to `app`
9
+ *
10
+ * Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
11
+ * the caller is expected to surface this as a scaffold error.
12
+ */
13
+ export declare function normalizeGoModuleName(raw: string): string;
14
+ /** Build the go.mod body. Inline comment documents the local-default. */
15
+ export declare function buildGoMod(moduleName: string): string;
16
+ /** Build main.go body — minimal Hello world package. */
17
+ export declare function buildMainGo(): string;
18
+ /** Build main_test.go body — table-test friendly smoke test. */
19
+ export declare function buildMainTestGo(): string;
20
+ /**
21
+ * Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
22
+ * already appears the second invocation leaves it alone. Creates the file
23
+ * if it doesn't exist.
24
+ */
25
+ export declare function augmentGitignore(existing: string | null): string;
26
+ export declare function scaffoldGo(ctx: ScaffoldRunContext): Promise<ScaffoldResult>;
27
+ //# sourceMappingURL=go.d.ts.map
@@ -0,0 +1,225 @@
1
+ // v7.6.0 — Go scaffolder.
2
+ //
3
+ // Mirrors the Python scaffolder shape:
4
+ // - single `scaffoldGo()` exported entrypoint
5
+ // - pure-function helpers (name normalization, builders) for unit tests
6
+ // - never overwrites existing files (matches `· exists` log pattern)
7
+ // - tracks filesCreated / dirsCreated / filesSkippedExisting for return
8
+ //
9
+ // Output for a basic spec (`## Files` listing go.mod + main.go + main_test.go):
10
+ // - go.mod with `module <basename(cwd)>` and `go 1.22`. Inline comment
11
+ // documents the local-scaffold-default per codex NOTE (not a real
12
+ // hosted module path — users override before publishing).
13
+ // - main.go: package main + Hello world (only when not under cmd/<name>/)
14
+ // - main_test.go: smoke test
15
+ // - .gitignore augmentation: appends `vendor/`, `*.exe`, `*.test`
16
+ // idempotently — if already present, leaves them alone.
17
+ //
18
+ // Name normalization: lowercased basename(cwd), whitespace -> `-`. Dots and
19
+ // hyphens preserved (Go module paths permit them). Path-invalid chars
20
+ // (`/`, `\`, control chars) are rejected with a clear error.
21
+ import * as fs from 'node:fs';
22
+ import * as fsAsync from 'node:fs/promises';
23
+ import * as path from 'node:path';
24
+ const PASS = '\x1b[32m✓\x1b[0m';
25
+ const SKIP = '\x1b[2m·\x1b[0m';
26
+ const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
27
+ /**
28
+ * Normalize a basename into a valid Go module name.
29
+ *
30
+ * - lowercased
31
+ * - whitespace runs collapse to a single `-`
32
+ * - dots + hyphens preserved (Go modules allow them)
33
+ * - empty result falls back to `app`
34
+ *
35
+ * Throws on path-invalid characters (`/`, `\`, NUL, other control chars) —
36
+ * the caller is expected to surface this as a scaffold error.
37
+ */
38
+ export function normalizeGoModuleName(raw) {
39
+ // eslint-disable-next-line no-control-regex
40
+ if (/[\/\\\x00-\x1f]/.test(raw)) {
41
+ throw new Error(`invalid Go module name "${raw}" — path/control characters not allowed`);
42
+ }
43
+ const lower = raw.toLowerCase();
44
+ const collapsed = lower.replace(/\s+/g, '-');
45
+ return collapsed.length > 0 ? collapsed : 'app';
46
+ }
47
+ /** Build the go.mod body. Inline comment documents the local-default. */
48
+ export function buildGoMod(moduleName) {
49
+ return `// NOTE: module name is the local-scaffold default (basename of cwd).
50
+ // Replace with your full module path (e.g. github.com/<user>/${moduleName})
51
+ // before publishing or running \`go install\`.
52
+ module ${moduleName}
53
+
54
+ go 1.22
55
+ `;
56
+ }
57
+ /** Build main.go body — minimal Hello world package. */
58
+ export function buildMainGo() {
59
+ return [
60
+ 'package main',
61
+ '',
62
+ 'import "fmt"',
63
+ '',
64
+ 'func main() {',
65
+ '\tfmt.Println("Hello, world!")',
66
+ '}',
67
+ '',
68
+ ].join('\n');
69
+ }
70
+ /** Build main_test.go body — table-test friendly smoke test. */
71
+ export function buildMainTestGo() {
72
+ return [
73
+ 'package main',
74
+ '',
75
+ 'import "testing"',
76
+ '',
77
+ 'func TestSmoke(t *testing.T) {',
78
+ '\t// Smoke test scaffolded by claude-autopilot. Replace with real',
79
+ '\t// table-driven cases for your package under test.',
80
+ '}',
81
+ '',
82
+ ].join('\n');
83
+ }
84
+ /** Lines to append to .gitignore. Idempotent — only adds missing entries. */
85
+ const GO_GITIGNORE_LINES = ['vendor/', '*.exe', '*.test'];
86
+ /**
87
+ * Augment `.gitignore` with Go-standard ignores. Idempotent: if `vendor/`
88
+ * already appears the second invocation leaves it alone. Creates the file
89
+ * if it doesn't exist.
90
+ */
91
+ export function augmentGitignore(existing) {
92
+ const lines = existing ? existing.split('\n') : [];
93
+ const present = new Set(lines.map(l => l.trim()));
94
+ const toAdd = GO_GITIGNORE_LINES.filter(l => !present.has(l));
95
+ if (toAdd.length === 0)
96
+ return existing ?? '';
97
+ const prefix = existing && !existing.endsWith('\n') ? existing + '\n' : (existing ?? '');
98
+ return prefix + toAdd.join('\n') + '\n';
99
+ }
100
+ /**
101
+ * True when the spec lists ONLY a `cmd/<name>/main.go` style entrypoint
102
+ * (no top-level main.go). In that case we skip generating the top-level
103
+ * main.go — the spec author intends the cmd/ layout.
104
+ */
105
+ function specHasCmdMainOnly(paths) {
106
+ const hasTopMain = paths.includes('main.go');
107
+ const hasCmdMain = paths.some(p => /^cmd\/[^/]+\/main\.go$/.test(p));
108
+ return hasCmdMain && !hasTopMain;
109
+ }
110
+ export async function scaffoldGo(ctx) {
111
+ const { cwd, parsed, dryRun } = ctx;
112
+ const moduleName = normalizeGoModuleName(path.basename(cwd));
113
+ const filesCreated = [];
114
+ const filesSkippedExisting = [];
115
+ const dirsCreated = [];
116
+ // Files we generate with content rather than empty placeholders.
117
+ const MANAGED_FILES = new Set(['go.mod', 'main.go', 'main_test.go', '.gitignore']);
118
+ // 1) Create directories implied by spec paths.
119
+ const dirs = new Set();
120
+ for (const p of parsed.paths) {
121
+ const d = path.dirname(p);
122
+ if (d && d !== '.')
123
+ dirs.add(d);
124
+ }
125
+ for (const d of dirs) {
126
+ const abs = path.join(cwd, d);
127
+ if (fs.existsSync(abs))
128
+ continue;
129
+ if (!dryRun)
130
+ await fsAsync.mkdir(abs, { recursive: true });
131
+ dirsCreated.push(d);
132
+ console.log(` ${PASS} mkdir ${DIM(d + '/')}`);
133
+ }
134
+ // 2) Empty-placeholder pass for spec paths we don't manage.
135
+ for (const p of parsed.paths) {
136
+ if (MANAGED_FILES.has(p))
137
+ continue;
138
+ const abs = path.join(cwd, p);
139
+ if (fs.existsSync(abs)) {
140
+ filesSkippedExisting.push(p);
141
+ console.log(` ${SKIP} exists ${DIM(p)}`);
142
+ continue;
143
+ }
144
+ if (!dryRun) {
145
+ await fsAsync.mkdir(path.dirname(abs), { recursive: true });
146
+ await fsAsync.writeFile(abs, '', 'utf8');
147
+ }
148
+ filesCreated.push(p);
149
+ console.log(` ${PASS} touch ${DIM(p)}`);
150
+ }
151
+ // 3) go.mod — never overwrite.
152
+ const goModAbs = path.join(cwd, 'go.mod');
153
+ if (fs.existsSync(goModAbs)) {
154
+ filesSkippedExisting.push('go.mod');
155
+ console.log(` ${SKIP} exists ${DIM('go.mod (preserved)')}`);
156
+ }
157
+ else {
158
+ if (!dryRun)
159
+ await fsAsync.writeFile(goModAbs, buildGoMod(moduleName), 'utf8');
160
+ filesCreated.push('go.mod');
161
+ console.log(` ${PASS} write ${DIM(`go.mod (module ${moduleName}, go 1.22)`)}`);
162
+ }
163
+ // 4) main.go — only if the spec doesn't push us to a cmd/<name>/ layout.
164
+ const cmdOnly = specHasCmdMainOnly(parsed.paths);
165
+ if (!cmdOnly) {
166
+ const mainAbs = path.join(cwd, 'main.go');
167
+ if (fs.existsSync(mainAbs)) {
168
+ filesSkippedExisting.push('main.go');
169
+ console.log(` ${SKIP} exists ${DIM('main.go (preserved)')}`);
170
+ }
171
+ else {
172
+ if (!dryRun)
173
+ await fsAsync.writeFile(mainAbs, buildMainGo(), 'utf8');
174
+ filesCreated.push('main.go');
175
+ console.log(` ${PASS} write ${DIM('main.go (package main + Hello)')}`);
176
+ }
177
+ }
178
+ // 5) main_test.go — only when we also wrote main.go (same cmd-only guard).
179
+ if (!cmdOnly) {
180
+ const testAbs = path.join(cwd, 'main_test.go');
181
+ if (fs.existsSync(testAbs)) {
182
+ filesSkippedExisting.push('main_test.go');
183
+ console.log(` ${SKIP} exists ${DIM('main_test.go (preserved)')}`);
184
+ }
185
+ else {
186
+ if (!dryRun)
187
+ await fsAsync.writeFile(testAbs, buildMainTestGo(), 'utf8');
188
+ filesCreated.push('main_test.go');
189
+ console.log(` ${PASS} write ${DIM('main_test.go (TestSmoke stub)')}`);
190
+ }
191
+ }
192
+ // 6) .gitignore — idempotent augmentation.
193
+ const giAbs = path.join(cwd, '.gitignore');
194
+ const existing = fs.existsSync(giAbs) ? await fsAsync.readFile(giAbs, 'utf8') : null;
195
+ const augmented = augmentGitignore(existing);
196
+ if (existing === null) {
197
+ if (!dryRun)
198
+ await fsAsync.writeFile(giAbs, augmented, 'utf8');
199
+ filesCreated.push('.gitignore');
200
+ console.log(` ${PASS} write ${DIM('.gitignore (vendor/, *.exe, *.test)')}`);
201
+ }
202
+ else if (augmented !== existing) {
203
+ if (!dryRun)
204
+ await fsAsync.writeFile(giAbs, augmented, 'utf8');
205
+ // Treat as "augmented" — not in skipped-existing (we modified it) and
206
+ // not in filesCreated (we didn't create a new file). For now we count
207
+ // it as filesCreated since the user sees a write. Tests assert
208
+ // idempotence on disk content, not on this return shape.
209
+ filesCreated.push('.gitignore');
210
+ console.log(` ${PASS} augment ${DIM('.gitignore (added Go ignores)')}`);
211
+ }
212
+ else {
213
+ filesSkippedExisting.push('.gitignore');
214
+ console.log(` ${SKIP} exists ${DIM('.gitignore (Go entries already present)')}`);
215
+ }
216
+ return {
217
+ filesCreated,
218
+ dirsCreated,
219
+ filesSkippedExisting,
220
+ // Node-shape fields — Go scaffolder doesn't touch package.json/tsconfig.
221
+ packageJsonAction: 'skipped-exists',
222
+ tsconfigAction: 'skipped-no-ts',
223
+ };
224
+ }
225
+ //# sourceMappingURL=go.js.map
@@ -1,11 +1,11 @@
1
- /** Supported `--stack` values. v7.5+ will add 'go', 'rust', 'ruby'. */
2
- export type Stack = 'node' | 'python' | 'fastapi';
1
+ /** Supported `--stack` values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
2
+ export type Stack = 'node' | 'python' | 'fastapi' | 'go';
3
3
  /**
4
4
  * Stacks we can DETECT but cannot scaffold yet. Detection still warns +
5
- * exits 3 so the operator gets a clear "v7.5" diagnostic instead of a
5
+ * exits 3 so the operator gets a clear "v7.7" diagnostic instead of a
6
6
  * silent fallback to Node, which would generate a wrong-language skeleton.
7
7
  */
8
- export type UnsupportedStack = 'go' | 'rust' | 'ruby';
8
+ export type UnsupportedStack = 'rust' | 'ruby';
9
9
  export interface ParsedFiles {
10
10
  /** Raw paths extracted from the `## Files` section bullets. */
11
11
  paths: string[];
@@ -2,7 +2,7 @@ import { buildStarterPackageJson } from './scaffold/node.ts';
2
2
  import type { ParsedFiles, ScaffoldOptions, ScaffoldResult, Stack, UnsupportedStack } from './scaffold/types.ts';
3
3
  export { buildStarterPackageJson };
4
4
  export type { ScaffoldOptions, ScaffoldResult, ParsedFiles, Stack };
5
- /** Valid `--stack` argument values. v7.5+ adds 'go', 'rust', 'ruby'. */
5
+ /** Valid `--stack` argument values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
6
6
  export declare const SUPPORTED_STACKS: readonly Stack[];
7
7
  /** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
8
8
  export declare const UNSUPPORTED_STACK_FILES: Record<UnsupportedStack, string>;
@@ -33,17 +33,17 @@ import * as fsAsync from 'node:fs/promises';
33
33
  import * as path from 'node:path';
34
34
  import { scaffoldNode, buildStarterPackageJson } from "./scaffold/node.js";
35
35
  import { scaffoldPython } from "./scaffold/python.js";
36
+ import { scaffoldGo } from "./scaffold/go.js";
36
37
  const BOLD = (t) => `\x1b[1m${t}\x1b[0m`;
37
38
  const DIM = (t) => `\x1b[2m${t}\x1b[0m`;
38
39
  // Re-export types + the legacy buildStarterPackageJson so `src/index.ts` and
39
40
  // the existing tests/scaffold.test.ts (which imports from this module) keep
40
41
  // compiling without changes.
41
42
  export { buildStarterPackageJson };
42
- /** Valid `--stack` argument values. v7.5+ adds 'go', 'rust', 'ruby'. */
43
- export const SUPPORTED_STACKS = ['node', 'python', 'fastapi'];
43
+ /** Valid `--stack` argument values. v7.6 adds 'go'; v7.7+ will add 'rust'. */
44
+ export const SUPPORTED_STACKS = ['node', 'python', 'fastapi', 'go'];
44
45
  /** Stacks we DETECT-but-don't-support yet. Mapped to spec exit-3 messages. */
45
46
  export const UNSUPPORTED_STACK_FILES = {
46
- go: 'go.mod',
47
47
  rust: 'Cargo.toml',
48
48
  ruby: 'Gemfile',
49
49
  };
@@ -215,8 +215,23 @@ export function detectStack(parsed, explicit) {
215
215
  const hasFastapiMention = parsed.packageHints.stackHint === 'fastapi';
216
216
  const hasPythonMarker = has('pyproject.toml') || has('requirements.txt');
217
217
  const hasNodeMarker = has('package.json');
218
- // Polyglot guard (codex W3) Node + Python without --stack.
219
- if (hasNodeMarker && hasPythonMarker) {
218
+ // v7.6 Go signal: `go.mod` OR a top-level / cmd-shaped `main.go`.
219
+ const hasGoMod = has('go.mod');
220
+ const hasMainGo = paths.some(p => p === 'main.go' || /^cmd\/[^/]+\/main\.go$/.test(p));
221
+ const hasGoMarker = hasGoMod || hasMainGo;
222
+ // v7.6 — Polyglot detection scans ALL supported stack signals at once.
223
+ // We collect each present stack and exit 3 if more than one supported
224
+ // stack is detected (e.g. Node + Go, Python + Go, Node + Python + Go).
225
+ // FastAPI is collapsed into Python for the polyglot count (they share
226
+ // pyproject.toml — not a real conflict).
227
+ const supportedSignals = [];
228
+ if (hasNodeMarker)
229
+ supportedSignals.push('node');
230
+ if (hasPythonMarker)
231
+ supportedSignals.push('python');
232
+ if (hasGoMarker)
233
+ supportedSignals.push('go');
234
+ if (supportedSignals.length > 1) {
220
235
  return {
221
236
  kind: 'polyglot',
222
237
  message: 'polyglot spec — pass --stack to disambiguate',
@@ -235,20 +250,23 @@ export function detectStack(parsed, explicit) {
235
250
  // Step 3: Python.
236
251
  if (hasPythonMarker)
237
252
  return { kind: 'resolved', stack: 'python' };
238
- // Step 4: Node.
253
+ // Step 4: Go (v7.6).
254
+ if (hasGoMarker)
255
+ return { kind: 'resolved', stack: 'go' };
256
+ // Step 5: Node.
239
257
  if (hasNodeMarker)
240
258
  return { kind: 'resolved', stack: 'node' };
241
- // Step 5: detected-but-unsupported (codex W2).
259
+ // Step 6: detected-but-unsupported (codex W2). Rust + Ruby still here.
242
260
  for (const [stack, file] of Object.entries(UNSUPPORTED_STACK_FILES)) {
243
261
  if (has(file)) {
244
262
  return {
245
263
  kind: 'unsupported',
246
264
  stack,
247
- message: `${stack} detected but not supported until v7.5`,
265
+ message: `${stack} detected but not supported until v7.7`,
248
266
  };
249
267
  }
250
268
  }
251
- // Step 6: fallback — Node ESM (preserves v7.2.0 default for ambiguous
269
+ // Step 7: fallback — Node ESM (preserves v7.2.0 default for ambiguous
252
270
  // specs that listed only paths with no root-marker file).
253
271
  return { kind: 'resolved', stack: 'node' };
254
272
  }
@@ -262,16 +280,17 @@ export function printStackList() {
262
280
  console.log(' node Node 22 ESM (package.json + tsconfig.json)');
263
281
  console.log(' python Python 3.11+ (pyproject.toml + hatchling + pytest)');
264
282
  console.log(' fastapi Python + FastAPI (auto-includes fastapi + uvicorn[standard])');
283
+ console.log(' go Go 1.22 (go.mod + main.go + main_test.go)');
265
284
  console.log('');
266
285
  console.log(BOLD('Auto-detected from `## Files`:'));
267
286
  console.log(' node when `package.json` is listed');
268
287
  console.log(' python when `pyproject.toml` or `requirements.txt` is listed');
269
288
  console.log(' fastapi when `main.py` is listed AND a bullet mentions `fastapi`');
289
+ console.log(' go when `go.mod` or `main.go` is listed');
270
290
  console.log('');
271
291
  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)');
292
+ console.log(' rust v7.7 (would detect via Cargo.toml)');
293
+ console.log(' ruby v7.7+ (would detect via Gemfile)');
275
294
  console.log('');
276
295
  }
277
296
  export async function runScaffold(opts) {
@@ -305,34 +324,47 @@ export async function runScaffold(opts) {
305
324
  }
306
325
  const stack = detection.stack;
307
326
  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.
327
+ // codex W5 — when an explicit --stack is passed against a polyglot spec,
328
+ // strip the OTHER stack's marker files so the chosen scaffolder doesn't
329
+ // touch them as empty placeholders. v7.6 extends this to Go.
311
330
  let ignoredOtherStackFiles;
312
331
  let parsedForStack = parsed;
332
+ const NODE_FILES = new Set(['package.json', 'tsconfig.json']);
333
+ const PYTHON_FILES = new Set(['pyproject.toml', 'requirements.txt']);
334
+ const GO_FILES = new Set(['go.mod']);
313
335
  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));
336
+ const toIgnore = new Set([...NODE_FILES, ...GO_FILES]);
337
+ const ignored = parsed.paths.filter(p => toIgnore.has(p));
316
338
  if (ignored.length > 0) {
317
339
  ignoredOtherStackFiles = ignored;
318
- console.log(` ${DIM(`! ignoring Node files (--stack ${stack}): ${ignored.join(', ')}`)}`);
340
+ console.log(` ${DIM(`! ignoring non-Python files (--stack ${stack}): ${ignored.join(', ')}`)}`);
319
341
  parsedForStack = {
320
342
  ...parsed,
321
- paths: parsed.paths.filter(p => !NODE_FILES.has(p)),
343
+ paths: parsed.paths.filter(p => !toIgnore.has(p)),
322
344
  };
323
345
  }
324
346
  }
325
347
  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));
348
+ const toIgnore = new Set([...PYTHON_FILES, ...GO_FILES]);
349
+ const ignored = parsed.paths.filter(p => toIgnore.has(p));
350
+ if (ignored.length > 0) {
351
+ ignoredOtherStackFiles = ignored;
352
+ console.log(` ${DIM(`! ignoring non-Node files (--stack node): ${ignored.join(', ')}`)}`);
353
+ parsedForStack = {
354
+ ...parsed,
355
+ paths: parsed.paths.filter(p => !toIgnore.has(p)),
356
+ };
357
+ }
358
+ }
359
+ else if (opts.stack === 'go') {
360
+ const toIgnore = new Set([...NODE_FILES, ...PYTHON_FILES]);
361
+ const ignored = parsed.paths.filter(p => toIgnore.has(p));
330
362
  if (ignored.length > 0) {
331
363
  ignoredOtherStackFiles = ignored;
332
- console.log(` ${DIM(`! ignoring Python files (--stack node): ${ignored.join(', ')}`)}`);
364
+ console.log(` ${DIM(`! ignoring non-Go files (--stack go): ${ignored.join(', ')}`)}`);
333
365
  parsedForStack = {
334
366
  ...parsed,
335
- paths: parsed.paths.filter(p => !PYTHON_FILES.has(p)),
367
+ paths: parsed.paths.filter(p => !toIgnore.has(p)),
336
368
  };
337
369
  }
338
370
  }
@@ -344,6 +376,9 @@ export async function runScaffold(opts) {
344
376
  else if (stack === 'fastapi') {
345
377
  result = await scaffoldPython(ctx, { isFastapi: true });
346
378
  }
379
+ else if (stack === 'go') {
380
+ result = await scaffoldGo(ctx);
381
+ }
347
382
  else {
348
383
  result = await scaffoldNode(ctx);
349
384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "tag": "next"