@nathapp/nax 0.38.2 → 0.39.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/dist/nax.js CHANGED
@@ -20793,7 +20793,7 @@ var package_default;
20793
20793
  var init_package = __esm(() => {
20794
20794
  package_default = {
20795
20795
  name: "@nathapp/nax",
20796
- version: "0.38.2",
20796
+ version: "0.39.0",
20797
20797
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20798
20798
  type: "module",
20799
20799
  bin: {
@@ -20857,8 +20857,8 @@ var init_version = __esm(() => {
20857
20857
  NAX_VERSION = package_default.version;
20858
20858
  NAX_COMMIT = (() => {
20859
20859
  try {
20860
- if (/^[0-9a-f]{6,10}$/.test("a4df623"))
20861
- return "a4df623";
20860
+ if (/^[0-9a-f]{6,10}$/.test("e6f293e"))
20861
+ return "e6f293e";
20862
20862
  } catch {}
20863
20863
  try {
20864
20864
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -65180,6 +65180,9 @@ async function exportPromptCommand(options) {
65180
65180
  // src/cli/init.ts
65181
65181
  init_paths();
65182
65182
  init_logger2();
65183
+
65184
+ // src/cli/init-context.ts
65185
+ init_logger2();
65183
65186
  // src/cli/plugins.ts
65184
65187
  init_loader5();
65185
65188
  import * as os2 from "os";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.38.2",
4
- "description": "AI Coding Agent Orchestrator loops until done",
3
+ "version": "0.39.0",
4
+ "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nax": "./dist/nax.js"
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Context.md Generation (INIT-002)
3
+ *
4
+ * Generates context.md from filesystem scan with optional LLM enhancement.
5
+ * Default mode: template from scan (zero LLM cost)
6
+ * AI mode (--ai flag): LLM-powered narrative context
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { mkdir } from "node:fs/promises";
11
+ import { basename, join } from "node:path";
12
+ import { getLogger } from "../logger";
13
+
14
+ /** Project scan results */
15
+ export interface ProjectScan {
16
+ projectName: string;
17
+ fileTree: string[];
18
+ packageManifest: {
19
+ name?: string;
20
+ description?: string;
21
+ scripts?: Record<string, string>;
22
+ dependencies?: Record<string, string>;
23
+ } | null;
24
+ readmeSnippet: string | null;
25
+ entryPoints: string[];
26
+ configFiles: string[];
27
+ }
28
+
29
+ /** Package manifest structure */
30
+ interface PackageManifest {
31
+ name?: string;
32
+ description?: string;
33
+ scripts?: Record<string, string>;
34
+ dependencies?: Record<string, string>;
35
+ }
36
+
37
+ /** initContext options */
38
+ export interface InitContextOptions {
39
+ ai?: boolean;
40
+ force?: boolean;
41
+ }
42
+
43
+ /** Dependency injection for testing */
44
+ export const _deps = {
45
+ callLLM: async (_prompt: string): Promise<string> => {
46
+ // Placeholder implementation
47
+ // In production, this would call the nax LLM infrastructure
48
+ throw new Error("callLLM not implemented");
49
+ },
50
+ };
51
+
52
+ /**
53
+ * Recursively find all files in a directory, excluding certain paths.
54
+ * Returns relative paths, limited to maxFiles entries.
55
+ */
56
+ async function findFiles(dir: string, maxFiles = 200): Promise<string[]> {
57
+ // Use find command to locate files, excluding common directories
58
+ try {
59
+ const proc = Bun.spawnSync(
60
+ [
61
+ "find",
62
+ dir,
63
+ "-type",
64
+ "f",
65
+ "-not",
66
+ "-path",
67
+ "*/node_modules/*",
68
+ "-not",
69
+ "-path",
70
+ "*/.git/*",
71
+ "-not",
72
+ "-path",
73
+ "*/dist/*",
74
+ ],
75
+ { stdio: ["pipe", "pipe", "pipe"] },
76
+ );
77
+
78
+ if (proc.success) {
79
+ const output = new TextDecoder().decode(proc.stdout);
80
+ const files = output
81
+ .trim()
82
+ .split("\n")
83
+ .filter((f) => f.length > 0)
84
+ .map((f) => f.replace(`${dir}/`, ""))
85
+ .slice(0, maxFiles);
86
+ return files;
87
+ }
88
+ } catch {
89
+ // find command failed, use fallback
90
+ }
91
+
92
+ return [];
93
+ }
94
+
95
+ /**
96
+ * Read and parse package.json if it exists
97
+ */
98
+ async function readPackageManifest(projectRoot: string): Promise<PackageManifest | null> {
99
+ const packageJsonPath = join(projectRoot, "package.json");
100
+
101
+ if (!existsSync(packageJsonPath)) {
102
+ return null;
103
+ }
104
+
105
+ try {
106
+ const content = await Bun.file(packageJsonPath).text();
107
+ const manifest = JSON.parse(content) as PackageManifest;
108
+ return {
109
+ name: manifest.name,
110
+ description: manifest.description,
111
+ scripts: manifest.scripts,
112
+ dependencies: manifest.dependencies,
113
+ };
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Read first 100 lines of README.md if it exists
121
+ */
122
+ async function readReadmeSnippet(projectRoot: string): Promise<string | null> {
123
+ const readmePath = join(projectRoot, "README.md");
124
+
125
+ if (!existsSync(readmePath)) {
126
+ return null;
127
+ }
128
+
129
+ try {
130
+ const content = await Bun.file(readmePath).text();
131
+ const lines = content.split("\n");
132
+ return lines.slice(0, 100).join("\n");
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Detect entry points in the project
140
+ */
141
+ async function detectEntryPoints(projectRoot: string): Promise<string[]> {
142
+ const candidates = ["src/index.ts", "src/main.ts", "main.go", "src/lib.rs"];
143
+ const found: string[] = [];
144
+
145
+ for (const candidate of candidates) {
146
+ const path = join(projectRoot, candidate);
147
+ if (existsSync(path)) {
148
+ found.push(candidate);
149
+ }
150
+ }
151
+
152
+ return found;
153
+ }
154
+
155
+ /**
156
+ * Detect config files in the project
157
+ */
158
+ async function detectConfigFiles(projectRoot: string): Promise<string[]> {
159
+ const candidates = ["tsconfig.json", "biome.json", "turbo.json", ".env.example"];
160
+ const found: string[] = [];
161
+
162
+ for (const candidate of candidates) {
163
+ const path = join(projectRoot, candidate);
164
+ if (existsSync(path)) {
165
+ found.push(candidate);
166
+ }
167
+ }
168
+
169
+ return found;
170
+ }
171
+
172
+ /**
173
+ * Scan a project for context information
174
+ */
175
+ export async function scanProject(projectRoot: string): Promise<ProjectScan> {
176
+ const fileTree = await findFiles(projectRoot, 200);
177
+ const packageManifest = await readPackageManifest(projectRoot);
178
+ const readmeSnippet = await readReadmeSnippet(projectRoot);
179
+ const entryPoints = await detectEntryPoints(projectRoot);
180
+ const configFiles = await detectConfigFiles(projectRoot);
181
+
182
+ // Determine project name from package.json or directory basename
183
+ const projectName = packageManifest?.name || basename(projectRoot);
184
+
185
+ return {
186
+ projectName,
187
+ fileTree,
188
+ packageManifest,
189
+ readmeSnippet,
190
+ entryPoints,
191
+ configFiles,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Generate a markdown template for context.md from scan results
197
+ */
198
+ export function generateContextTemplate(scan: ProjectScan): string {
199
+ const lines: string[] = [];
200
+
201
+ lines.push(`# ${scan.projectName}\n`);
202
+
203
+ if (scan.packageManifest?.description) {
204
+ lines.push(`${scan.packageManifest.description}\n`);
205
+ } else {
206
+ lines.push("<!-- TODO: Add project description -->\n");
207
+ }
208
+
209
+ if (scan.entryPoints.length > 0) {
210
+ lines.push("## Entry Points\n");
211
+ for (const ep of scan.entryPoints) {
212
+ lines.push(`- ${ep}`);
213
+ }
214
+ lines.push("");
215
+ } else {
216
+ lines.push("## Entry Points\n");
217
+ lines.push("<!-- TODO: Document entry points -->\n");
218
+ }
219
+
220
+ if (scan.fileTree.length > 0) {
221
+ lines.push("## Project Structure\n");
222
+ lines.push("```");
223
+ for (const file of scan.fileTree.slice(0, 20)) {
224
+ lines.push(file);
225
+ }
226
+ if (scan.fileTree.length > 20) {
227
+ lines.push(`... and ${scan.fileTree.length - 20} more files`);
228
+ }
229
+ lines.push("```\n");
230
+ } else {
231
+ lines.push("## Project Structure\n");
232
+ lines.push("<!-- TODO: Document project structure -->\n");
233
+ }
234
+
235
+ if (scan.configFiles.length > 0) {
236
+ lines.push("## Configuration Files\n");
237
+ for (const cf of scan.configFiles) {
238
+ lines.push(`- ${cf}`);
239
+ }
240
+ lines.push("");
241
+ } else {
242
+ lines.push("## Configuration Files\n");
243
+ lines.push("<!-- TODO: Document configuration files -->\n");
244
+ }
245
+
246
+ if (scan.packageManifest?.scripts) {
247
+ const hasScripts = Object.keys(scan.packageManifest.scripts).length > 0;
248
+ if (hasScripts) {
249
+ lines.push("## Scripts\n");
250
+ for (const [name, command] of Object.entries(scan.packageManifest.scripts)) {
251
+ lines.push(`- **${name}**: \`${command}\``);
252
+ }
253
+ lines.push("");
254
+ }
255
+ }
256
+
257
+ if (scan.packageManifest?.dependencies) {
258
+ const deps = Object.keys(scan.packageManifest.dependencies);
259
+ if (deps.length > 0) {
260
+ lines.push("## Dependencies\n");
261
+ lines.push("<!-- TODO: Document key dependencies and their purpose -->\n");
262
+ }
263
+ }
264
+
265
+ lines.push("## Development Guidelines\n");
266
+ lines.push("<!-- TODO: Document development guidelines and conventions -->\n");
267
+
268
+ return `${lines.join("\n").trim()}\n`;
269
+ }
270
+
271
+ /**
272
+ * Generate context.md with LLM enhancement
273
+ */
274
+ async function generateContextWithLLM(scan: ProjectScan): Promise<string> {
275
+ const logger = getLogger();
276
+
277
+ // Build LLM prompt from scan results
278
+ const scanSummary = `
279
+ Project: ${scan.projectName}
280
+ Entry Points: ${scan.entryPoints.join(", ") || "None detected"}
281
+ Config Files: ${scan.configFiles.join(", ") || "None detected"}
282
+ Total Files: ${scan.fileTree.length}
283
+ Description: ${scan.packageManifest?.description || "Not provided"}
284
+ `;
285
+
286
+ const prompt = `
287
+ You are a technical documentation expert. Generate a concise, well-structured context.md file for a software project based on this scan:
288
+
289
+ ${scanSummary}
290
+
291
+ The context.md should include:
292
+ 1. Project overview (name, purpose, key technologies)
293
+ 2. Entry points and main modules
294
+ 3. Key dependencies and why they're used
295
+ 4. Development setup and common commands
296
+ 5. Architecture overview (brief)
297
+ 6. Development guidelines
298
+
299
+ Keep it under 2000 tokens. Use markdown formatting. Be specific to the detected stack and structure.
300
+ `;
301
+
302
+ try {
303
+ const result = await _deps.callLLM(prompt);
304
+ logger.info("init", "Generated context.md with LLM");
305
+ return result;
306
+ } catch (err) {
307
+ logger.warn(
308
+ "init",
309
+ `LLM context generation failed, falling back to template: ${err instanceof Error ? err.message : String(err)}`,
310
+ );
311
+ return generateContextTemplate(scan);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Initialize context.md for a project
317
+ */
318
+ export async function initContext(projectRoot: string, options: InitContextOptions = {}): Promise<void> {
319
+ const logger = getLogger();
320
+ const naxDir = join(projectRoot, "nax");
321
+ const contextPath = join(naxDir, "context.md");
322
+
323
+ // Check if context.md already exists
324
+ if (existsSync(contextPath) && !options.force) {
325
+ logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
326
+ return;
327
+ }
328
+
329
+ // Create nax directory if needed
330
+ if (!existsSync(naxDir)) {
331
+ await mkdir(naxDir, { recursive: true });
332
+ }
333
+
334
+ // Scan the project
335
+ const scan = await scanProject(projectRoot);
336
+
337
+ // Generate content (template or LLM-enhanced)
338
+ let content: string;
339
+ if (options.ai) {
340
+ content = await generateContextWithLLM(scan);
341
+ } else {
342
+ content = generateContextTemplate(scan);
343
+ }
344
+
345
+ // Write context.md
346
+ await Bun.write(contextPath, content);
347
+ logger.info("init", "Generated nax/context.md template from project scan", { path: contextPath });
348
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Project Stack Detection for nax init
3
+ *
4
+ * Scans the project root for stack indicators and builds quality.commands
5
+ * for nax/config.json.
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ /** Detected project runtime */
12
+ export type Runtime = "bun" | "node" | "unknown";
13
+
14
+ /** Detected project language */
15
+ export type Language = "typescript" | "python" | "rust" | "go" | "unknown";
16
+
17
+ /** Detected linter */
18
+ export type Linter = "biome" | "eslint" | "ruff" | "clippy" | "golangci-lint" | "unknown";
19
+
20
+ /** Detected monorepo tooling */
21
+ export type Monorepo = "turborepo" | "none";
22
+
23
+ /** Full detected project stack */
24
+ export interface ProjectStack {
25
+ runtime: Runtime;
26
+ language: Language;
27
+ linter: Linter;
28
+ monorepo: Monorepo;
29
+ }
30
+
31
+ /** Quality commands derived from stack detection */
32
+ export interface QualityCommands {
33
+ typecheck?: string;
34
+ lint?: string;
35
+ test?: string;
36
+ }
37
+
38
+ function detectRuntime(projectRoot: string): Runtime {
39
+ if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bunfig.toml"))) {
40
+ return "bun";
41
+ }
42
+ if (
43
+ existsSync(join(projectRoot, "package-lock.json")) ||
44
+ existsSync(join(projectRoot, "yarn.lock")) ||
45
+ existsSync(join(projectRoot, "pnpm-lock.yaml"))
46
+ ) {
47
+ return "node";
48
+ }
49
+ return "unknown";
50
+ }
51
+
52
+ function detectLanguage(projectRoot: string): Language {
53
+ if (existsSync(join(projectRoot, "tsconfig.json"))) return "typescript";
54
+ if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "setup.py"))) {
55
+ return "python";
56
+ }
57
+ if (existsSync(join(projectRoot, "Cargo.toml"))) return "rust";
58
+ if (existsSync(join(projectRoot, "go.mod"))) return "go";
59
+ return "unknown";
60
+ }
61
+
62
+ function detectLinter(projectRoot: string): Linter {
63
+ if (existsSync(join(projectRoot, "biome.json")) || existsSync(join(projectRoot, "biome.jsonc"))) {
64
+ return "biome";
65
+ }
66
+ if (
67
+ existsSync(join(projectRoot, ".eslintrc.json")) ||
68
+ existsSync(join(projectRoot, ".eslintrc.js")) ||
69
+ existsSync(join(projectRoot, "eslint.config.js"))
70
+ ) {
71
+ return "eslint";
72
+ }
73
+ return "unknown";
74
+ }
75
+
76
+ function detectMonorepo(projectRoot: string): Monorepo {
77
+ if (existsSync(join(projectRoot, "turbo.json"))) return "turborepo";
78
+ return "none";
79
+ }
80
+
81
+ /**
82
+ * Detect the project stack by scanning for indicator files.
83
+ */
84
+ export function detectProjectStack(projectRoot: string): ProjectStack {
85
+ return {
86
+ runtime: detectRuntime(projectRoot),
87
+ language: detectLanguage(projectRoot),
88
+ linter: detectLinter(projectRoot),
89
+ monorepo: detectMonorepo(projectRoot),
90
+ };
91
+ }
92
+
93
+ function resolveLintCommand(stack: ProjectStack, fallback: string): string {
94
+ if (stack.linter === "biome") return "biome check .";
95
+ if (stack.linter === "eslint") return "eslint .";
96
+ return fallback;
97
+ }
98
+
99
+ /**
100
+ * Build quality.commands from a detected project stack.
101
+ */
102
+ export function buildQualityCommands(stack: ProjectStack): QualityCommands {
103
+ if (stack.runtime === "bun" && stack.language === "typescript") {
104
+ return {
105
+ typecheck: "bun run tsc --noEmit",
106
+ lint: resolveLintCommand(stack, "bun run lint"),
107
+ test: "bun test",
108
+ };
109
+ }
110
+
111
+ if (stack.runtime === "node" && stack.language === "typescript") {
112
+ return {
113
+ typecheck: "npx tsc --noEmit",
114
+ lint: resolveLintCommand(stack, "npm run lint"),
115
+ test: "npm test",
116
+ };
117
+ }
118
+
119
+ if (stack.language === "python") {
120
+ return {
121
+ lint: "ruff check .",
122
+ test: "pytest",
123
+ };
124
+ }
125
+
126
+ if (stack.language === "rust") {
127
+ return {
128
+ typecheck: "cargo check",
129
+ lint: "cargo clippy",
130
+ test: "cargo test",
131
+ };
132
+ }
133
+
134
+ if (stack.language === "go") {
135
+ return {
136
+ typecheck: "go vet ./...",
137
+ lint: "golangci-lint run",
138
+ test: "go test ./...",
139
+ };
140
+ }
141
+
142
+ return {};
143
+ }
144
+
145
+ function isStackDetected(stack: ProjectStack): boolean {
146
+ return stack.runtime !== "unknown" || stack.language !== "unknown";
147
+ }
148
+
149
+ /**
150
+ * Build the full init config object from a detected project stack.
151
+ * Falls back to minimal config when stack is undetected.
152
+ */
153
+ export function buildInitConfig(stack: ProjectStack): object {
154
+ if (!isStackDetected(stack)) {
155
+ return { version: 1 };
156
+ }
157
+
158
+ const commands = buildQualityCommands(stack);
159
+ const hasCommands = Object.keys(commands).length > 0;
160
+
161
+ if (!hasCommands) {
162
+ return { version: 1 };
163
+ }
164
+
165
+ return {
166
+ version: 1,
167
+ quality: { commands },
168
+ };
169
+ }
package/src/cli/init.ts CHANGED
@@ -8,8 +8,10 @@ import { existsSync } from "node:fs";
8
8
  import { mkdir } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
  import { globalConfigDir, projectConfigDir } from "../config/paths";
11
- import { DEFAULT_CONFIG } from "../config/schema";
12
11
  import { getLogger } from "../logger";
12
+ import { initContext } from "./init-context";
13
+ import { buildInitConfig, detectProjectStack } from "./init-detect";
14
+ import type { ProjectStack } from "./init-detect";
13
15
  import { promptsInitCommand } from "./prompts";
14
16
 
15
17
  /** Init command options */
@@ -20,10 +22,18 @@ export interface InitOptions {
20
22
  projectRoot?: string;
21
23
  }
22
24
 
25
+ /** Options for initProject */
26
+ export interface InitProjectOptions {
27
+ /** Use LLM to generate context.md (--ai flag) */
28
+ ai?: boolean;
29
+ /** Force overwrite of existing files */
30
+ force?: boolean;
31
+ }
32
+
23
33
  /**
24
34
  * Gitignore entries added by nax init
25
35
  */
26
- const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json"];
36
+ const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json", "nax.lock", "nax/**/runs/", "nax/metrics.json"];
27
37
 
28
38
  /**
29
39
  * Add nax-specific entries to .gitignore if not already present.
@@ -55,33 +65,58 @@ async function updateGitignore(projectRoot: string): Promise<void> {
55
65
  }
56
66
 
57
67
  /**
58
- * Template for default constitution.md
68
+ * Build a stack-aware constitution.md from the detected project stack.
59
69
  */
60
- const DEFAULT_CONSTITUTION = `# Project Constitution
70
+ function buildConstitution(stack: ProjectStack): string {
71
+ const sections: string[] = [];
61
72
 
62
- ## Goals
63
- - Deliver high-quality, maintainable code
64
- - Follow project conventions and best practices
65
- - Maintain comprehensive test coverage
73
+ sections.push("# Project Constitution\n");
66
74
 
67
- ## Constraints
68
- - Use Bun-native APIs only
69
- - Follow functional style for pure logic
70
- - Keep files focused and under 400 lines
75
+ sections.push("## Goals");
76
+ sections.push("- Deliver high-quality, maintainable code");
77
+ sections.push("- Follow project conventions and best practices");
78
+ sections.push("- Maintain comprehensive test coverage\n");
71
79
 
72
- ## Preferences
73
- - Prefer immutability over mutation
74
- - Write tests first (TDD approach)
75
- - Clear, descriptive naming
76
- `;
80
+ sections.push("## Constraints");
81
+ sections.push("- Follow functional style for pure logic");
82
+ sections.push("- Keep files focused and under 400 lines\n");
77
83
 
78
- /**
79
- * Template for minimal config.json (references defaults, only overrides)
80
- */
81
- const MINIMAL_PROJECT_CONFIG = {
82
- version: 1,
83
- // Add project-specific overrides here
84
- };
84
+ if (stack.runtime === "bun") {
85
+ sections.push("## Bun-Native APIs");
86
+ sections.push("- Use `Bun.file()` for file reads, `Bun.write()` for file writes");
87
+ sections.push("- Use `Bun.spawn()` for subprocesses (never `child_process`)");
88
+ sections.push("- Use `Bun.sleep()` for delays");
89
+ sections.push("- Use `bun test` for running tests\n");
90
+ }
91
+
92
+ if (stack.language === "typescript") {
93
+ sections.push("## strict TypeScript");
94
+ sections.push("- Enable strict mode in tsconfig.json");
95
+ sections.push("- No `any` in public APIs — use `unknown` + type guards");
96
+ sections.push("- Explicit return types on all exported functions\n");
97
+ }
98
+
99
+ if (stack.language === "python") {
100
+ sections.push("## Python Standards");
101
+ sections.push("- Follow PEP 8 style guide for formatting and naming");
102
+ sections.push("- Add type hints to all function signatures");
103
+ sections.push("- Use type annotations for variables where non-obvious\n");
104
+ }
105
+
106
+ if (stack.monorepo === "turborepo") {
107
+ sections.push("## Monorepo Conventions");
108
+ sections.push("- Respect package boundaries — do not import across packages without explicit dependency");
109
+ sections.push("- Each package should be independently buildable and testable");
110
+ sections.push("- Shared utilities go in a dedicated `packages/shared` (or equivalent) package\n");
111
+ }
112
+
113
+ sections.push("## Preferences");
114
+ sections.push("- Prefer immutability over mutation");
115
+ sections.push("- Write tests first (TDD approach)");
116
+ sections.push("- Clear, descriptive naming");
117
+
118
+ return `${sections.join("\n")}\n`;
119
+ }
85
120
 
86
121
  const MINIMAL_GLOBAL_CONFIG = {
87
122
  version: 1,
@@ -113,7 +148,10 @@ async function initGlobal(): Promise<void> {
113
148
  // Create ~/.nax/constitution.md if it doesn't exist
114
149
  const constitutionPath = join(globalDir, "constitution.md");
115
150
  if (!existsSync(constitutionPath)) {
116
- await Bun.write(constitutionPath, DEFAULT_CONSTITUTION);
151
+ await Bun.write(
152
+ constitutionPath,
153
+ buildConstitution({ runtime: "unknown", language: "unknown", linter: "unknown", monorepo: "none" }),
154
+ );
117
155
  logger.info("init", "Created global constitution", { path: constitutionPath });
118
156
  } else {
119
157
  logger.info("init", "Global constitution already exists", { path: constitutionPath });
@@ -134,7 +172,7 @@ async function initGlobal(): Promise<void> {
134
172
  /**
135
173
  * Initialize project nax directory (nax/)
136
174
  */
137
- export async function initProject(projectRoot: string): Promise<void> {
175
+ export async function initProject(projectRoot: string, options?: InitProjectOptions): Promise<void> {
138
176
  const logger = getLogger();
139
177
  const projectDir = projectConfigDir(projectRoot);
140
178
 
@@ -144,19 +182,32 @@ export async function initProject(projectRoot: string): Promise<void> {
144
182
  logger.info("init", "Created project config directory", { path: projectDir });
145
183
  }
146
184
 
185
+ // Detect project stack and build config
186
+ const stack = detectProjectStack(projectRoot);
187
+ const projectConfig = buildInitConfig(stack);
188
+ logger.info("init", "Detected project stack", {
189
+ runtime: stack.runtime,
190
+ language: stack.language,
191
+ linter: stack.linter,
192
+ monorepo: stack.monorepo,
193
+ });
194
+
147
195
  // Create nax/config.json if it doesn't exist
148
196
  const configPath = join(projectDir, "config.json");
149
197
  if (!existsSync(configPath)) {
150
- await Bun.write(configPath, `${JSON.stringify(MINIMAL_PROJECT_CONFIG, null, 2)}\n`);
198
+ await Bun.write(configPath, `${JSON.stringify(projectConfig, null, 2)}\n`);
151
199
  logger.info("init", "Created project config", { path: configPath });
152
200
  } else {
153
201
  logger.info("init", "Project config already exists", { path: configPath });
154
202
  }
155
203
 
156
- // Create nax/constitution.md if it doesn't exist
204
+ // Generate context.md (template or LLM-enhanced with --ai flag)
205
+ await initContext(projectRoot, { ai: options?.ai, force: options?.force });
206
+
207
+ // Create nax/constitution.md with stack-aware content
157
208
  const constitutionPath = join(projectDir, "constitution.md");
158
- if (!existsSync(constitutionPath)) {
159
- await Bun.write(constitutionPath, DEFAULT_CONSTITUTION);
209
+ if (!existsSync(constitutionPath) || options?.force) {
210
+ await Bun.write(constitutionPath, buildConstitution(stack));
160
211
  logger.info("init", "Created project constitution", { path: constitutionPath });
161
212
  } else {
162
213
  logger.info("init", "Project constitution already exists", { path: constitutionPath });
@@ -174,11 +225,25 @@ export async function initProject(projectRoot: string): Promise<void> {
174
225
  // Update .gitignore to include nax-specific entries
175
226
  await updateGitignore(projectRoot);
176
227
 
177
- // Create prompt templates (final step)
228
+ // Create prompt templates
178
229
  // Pass autoWireConfig: false to prevent auto-wiring prompts.overrides
179
230
  // Templates are created but not activated until user explicitly configures them
180
231
  await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
181
232
 
233
+ // Print summary
234
+ console.log("\n[OK] nax init complete. Created files:");
235
+ console.log(" - nax/config.json");
236
+ console.log(" - nax/context.md");
237
+ console.log(" - nax/constitution.md");
238
+ console.log(" - nax/hooks/");
239
+ console.log(" - nax/templates/");
240
+ console.log("\nNext steps:");
241
+ console.log(" 1. Review nax/context.md and fill in TODOs");
242
+ console.log(" 2. Review nax/config.json and adjust quality commands");
243
+ console.log(" 3. Run: nax generate");
244
+ console.log(" 4. Run: nax plan");
245
+ console.log(" 5. Run: nax run");
246
+
182
247
  logger.info("init", "Project config initialized successfully", { path: projectDir });
183
248
  }
184
249