@bantay/cli 0.1.1 → 0.3.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.
@@ -0,0 +1,311 @@
1
+ import { readFile, readdir, access } from "fs/promises";
2
+ import { join, relative } from "path";
3
+ import * as yaml from "js-yaml";
4
+ import { tryResolveAidePath } from "../aide";
5
+
6
+ export interface StatusOptions {
7
+ json?: boolean;
8
+ }
9
+
10
+ export interface ScenarioStatus {
11
+ id: string;
12
+ name: string;
13
+ parentCuj: string;
14
+ status: "implemented" | "missing";
15
+ testFile?: string;
16
+ line?: number;
17
+ }
18
+
19
+ export interface StatusSummary {
20
+ total: number;
21
+ implemented: number;
22
+ missing: number;
23
+ }
24
+
25
+ export interface StatusResult {
26
+ scenarios: ScenarioStatus[];
27
+ summary: StatusSummary;
28
+ cujs?: Record<string, string>;
29
+ error?: string;
30
+ }
31
+
32
+ interface AideEntity {
33
+ parent?: string;
34
+ props?: {
35
+ name?: string;
36
+ feature?: string;
37
+ [key: string]: unknown;
38
+ };
39
+ }
40
+
41
+ interface AideFile {
42
+ entities: Record<string, AideEntity>;
43
+ }
44
+
45
+ async function fileExists(path: string): Promise<boolean> {
46
+ try {
47
+ await access(path);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ async function findTestFiles(testsDir: string): Promise<string[]> {
55
+ const files: string[] = [];
56
+
57
+ async function walk(dir: string): Promise<void> {
58
+ try {
59
+ const entries = await readdir(dir, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const fullPath = join(dir, entry.name);
62
+ if (entry.isDirectory()) {
63
+ await walk(fullPath);
64
+ } else if (entry.name.endsWith(".test.ts") || entry.name.endsWith(".test.js")) {
65
+ files.push(fullPath);
66
+ }
67
+ }
68
+ } catch {
69
+ // Directory doesn't exist or isn't readable
70
+ }
71
+ }
72
+
73
+ await walk(testsDir);
74
+ return files;
75
+ }
76
+
77
+ async function searchTestFileForScenario(
78
+ testFilePath: string,
79
+ scenarioId: string
80
+ ): Promise<{ found: boolean; line?: number }> {
81
+ try {
82
+ const content = await readFile(testFilePath, "utf-8");
83
+ const lines = content.split("\n");
84
+
85
+ // Priority 1: Look for explicit scenario marker (highest priority)
86
+ // Formats: @scenario sc_xxx, // sc_xxx:, // sc_xxx, * @scenario sc_xxx
87
+ for (let i = 0; i < lines.length; i++) {
88
+ const line = lines[i];
89
+ if (
90
+ line.includes(`@scenario ${scenarioId}`) ||
91
+ line.includes(`@scenario: ${scenarioId}`) ||
92
+ line.includes(`// ${scenarioId}:`) ||
93
+ line.includes(`// ${scenarioId} `) ||
94
+ line.match(new RegExp(`\\*\\s*@scenario\\s+${scenarioId}\\b`))
95
+ ) {
96
+ return { found: true, line: i + 1 };
97
+ }
98
+ }
99
+
100
+ // Priority 2: Look for describe/test block containing exact scenario ID
101
+ for (let i = 0; i < lines.length; i++) {
102
+ const line = lines[i];
103
+ // Match scenario ID in describe block or test name
104
+ const isDescribeOrTest = /^\s*(describe|test|it)\s*\(/.test(line);
105
+ if (isDescribeOrTest && (line.includes(`"${scenarioId}"`) || line.includes(`'${scenarioId}'`))) {
106
+ return { found: true, line: i + 1 };
107
+ }
108
+ }
109
+
110
+ return { found: false };
111
+ } catch {
112
+ return { found: false };
113
+ }
114
+ }
115
+
116
+ function scenarioIdToTestFileName(scenarioId: string): string[] {
117
+ // Convert sc_init_prerequisites to possible test file names
118
+ // sc_init_prerequisites -> [prerequisites, init-prerequisites, init]
119
+ const withoutPrefix = scenarioId.replace(/^sc_/, "");
120
+ const parts = withoutPrefix.split("_");
121
+
122
+ const candidates: string[] = [];
123
+
124
+ // Full name with dashes: init-prerequisites
125
+ candidates.push(parts.join("-"));
126
+
127
+ // Last part only: prerequisites
128
+ if (parts.length > 1) {
129
+ candidates.push(parts[parts.length - 1]);
130
+ candidates.push(parts.slice(1).join("-"));
131
+ }
132
+
133
+ // First part only: init
134
+ candidates.push(parts[0]);
135
+
136
+ return candidates;
137
+ }
138
+
139
+ function testFileMatchesScenario(testFileName: string, scenarioId: string): boolean {
140
+ const baseName = testFileName.replace(/\.test\.(ts|js)$/, "");
141
+ const candidates = scenarioIdToTestFileName(scenarioId);
142
+
143
+ return candidates.some(candidate => baseName === candidate);
144
+ }
145
+
146
+ export async function runStatus(
147
+ projectPath: string,
148
+ options?: StatusOptions
149
+ ): Promise<StatusResult> {
150
+ // Discover aide file
151
+ const resolved = await tryResolveAidePath(projectPath);
152
+ if (!resolved) {
153
+ return {
154
+ scenarios: [],
155
+ summary: { total: 0, implemented: 0, missing: 0 },
156
+ error: "No .aide file found. Run 'bantay aide init' to create one.",
157
+ };
158
+ }
159
+
160
+ const aidePath = resolved.path;
161
+
162
+ // Parse aide file
163
+ let aideContent: AideFile;
164
+ try {
165
+ const rawContent = await readFile(aidePath, "utf-8");
166
+ aideContent = yaml.load(rawContent) as AideFile;
167
+ } catch (e) {
168
+ return {
169
+ scenarios: [],
170
+ summary: { total: 0, implemented: 0, missing: 0 },
171
+ error: `Failed to parse ${resolved.filename}: ${e instanceof Error ? e.message : String(e)}`,
172
+ };
173
+ }
174
+
175
+ if (!aideContent?.entities) {
176
+ return {
177
+ scenarios: [],
178
+ summary: { total: 0, implemented: 0, missing: 0 },
179
+ error: `${resolved.filename} has no entities`,
180
+ };
181
+ }
182
+
183
+ // Extract CUJ information
184
+ const cujs: Record<string, string> = {};
185
+ for (const [id, entity] of Object.entries(aideContent.entities)) {
186
+ if (id.startsWith("cuj_") && entity.props?.feature) {
187
+ cujs[id] = entity.props.feature;
188
+ }
189
+ }
190
+
191
+ // Extract all sc_* entities
192
+ const scenarios: ScenarioStatus[] = [];
193
+ for (const [id, entity] of Object.entries(aideContent.entities)) {
194
+ if (id.startsWith("sc_")) {
195
+ scenarios.push({
196
+ id,
197
+ name: entity.props?.name || id,
198
+ parentCuj: entity.parent || "unknown",
199
+ status: "missing",
200
+ });
201
+ }
202
+ }
203
+
204
+ // Find test files
205
+ const testsDir = join(projectPath, "tests");
206
+ const testFiles = await findTestFiles(testsDir);
207
+
208
+ // Match scenarios to test files
209
+ for (const scenario of scenarios) {
210
+ // Priority 1: Explicit scenario ID in test file
211
+ for (const testFile of testFiles) {
212
+ // Skip status-command.test.ts as it's a meta test that contains scenario IDs as test data
213
+ if (testFile.includes("status-command.test.ts")) {
214
+ continue;
215
+ }
216
+
217
+ const result = await searchTestFileForScenario(testFile, scenario.id);
218
+ if (result.found) {
219
+ scenario.status = "implemented";
220
+ scenario.testFile = relative(projectPath, testFile);
221
+ scenario.line = result.line;
222
+ break;
223
+ }
224
+ }
225
+
226
+ // Priority 2: Test file name matches scenario (if not already matched)
227
+ if (scenario.status === "missing") {
228
+ for (const testFile of testFiles) {
229
+ if (testFile.includes("status-command.test.ts")) {
230
+ continue;
231
+ }
232
+
233
+ const fileName = testFile.split("/").pop() || "";
234
+ if (testFileMatchesScenario(fileName, scenario.id)) {
235
+ // Found a file that matches by name, but verify it has relevant tests
236
+ try {
237
+ const content = await readFile(testFile, "utf-8");
238
+ // Ensure it's not an empty or stub test file
239
+ if (content.includes("describe(") || content.includes("test(")) {
240
+ scenario.status = "implemented";
241
+ scenario.testFile = relative(projectPath, testFile);
242
+ scenario.line = 1; // Start of file when matched by name
243
+ break;
244
+ }
245
+ } catch {
246
+ // Skip if can't read file
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ // Calculate summary
254
+ const implemented = scenarios.filter((s) => s.status === "implemented").length;
255
+ const missing = scenarios.filter((s) => s.status === "missing").length;
256
+
257
+ return {
258
+ scenarios,
259
+ summary: {
260
+ total: scenarios.length,
261
+ implemented,
262
+ missing,
263
+ },
264
+ cujs,
265
+ };
266
+ }
267
+
268
+ export function formatStatus(result: StatusResult): string {
269
+ if (result.error) {
270
+ return `Error: ${result.error}\n`;
271
+ }
272
+
273
+ const lines: string[] = [];
274
+ lines.push("# Scenario Implementation Status\n");
275
+
276
+ // Group scenarios by CUJ
277
+ const byParent: Record<string, ScenarioStatus[]> = {};
278
+ for (const scenario of result.scenarios) {
279
+ if (!byParent[scenario.parentCuj]) {
280
+ byParent[scenario.parentCuj] = [];
281
+ }
282
+ byParent[scenario.parentCuj].push(scenario);
283
+ }
284
+
285
+ // Output by CUJ
286
+ for (const [cuj, scenarios] of Object.entries(byParent)) {
287
+ const cujName = result.cujs?.[cuj] || cuj;
288
+ lines.push(`\n## ${cujName}\n`);
289
+
290
+ for (const scenario of scenarios) {
291
+ const icon = scenario.status === "implemented" ? "✓" : "○";
292
+ const location = scenario.testFile
293
+ ? `${scenario.testFile}${scenario.line ? `:${scenario.line}` : ""}`
294
+ : "";
295
+
296
+ lines.push(`${icon} ${scenario.id}: ${scenario.name}`);
297
+ if (location) {
298
+ lines.push(` → ${location}`);
299
+ }
300
+ }
301
+ }
302
+
303
+ // Summary
304
+ lines.push(`\n---`);
305
+ lines.push(
306
+ `Implemented: ${result.summary.implemented}/${result.summary.total} (${Math.round((result.summary.implemented / result.summary.total) * 100)}%)`
307
+ );
308
+ lines.push(`Missing: ${result.summary.missing}`);
309
+
310
+ return lines.join("\n");
311
+ }
@@ -0,0 +1,220 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import * as yaml from "js-yaml";
5
+ import { resolveAidePath } from "../aide/discovery";
6
+
7
+ interface Entity {
8
+ id: string;
9
+ parent?: string;
10
+ display?: string;
11
+ props?: Record<string, string>;
12
+ }
13
+
14
+ interface Relationship {
15
+ from: string;
16
+ to: string;
17
+ type: string;
18
+ cardinality?: string;
19
+ }
20
+
21
+ interface AideData {
22
+ entities: Record<string, Entity>;
23
+ relationships: Relationship[];
24
+ }
25
+
26
+ interface CUJ {
27
+ id: string;
28
+ feature: string;
29
+ tier?: string;
30
+ area?: string;
31
+ scenarios: Scenario[];
32
+ dependsOn: string[];
33
+ }
34
+
35
+ interface Scenario {
36
+ id: string;
37
+ name: string;
38
+ given?: string;
39
+ when?: string;
40
+ then?: string;
41
+ }
42
+
43
+ interface TasksOptions {
44
+ all?: boolean;
45
+ aide?: string;
46
+ }
47
+
48
+ export async function runTasks(
49
+ projectPath: string,
50
+ options: TasksOptions = {}
51
+ ): Promise<{ outputPath: string; cujs: CUJ[] }> {
52
+ // Resolve aide file path
53
+ const { path: aidePath, filename } = await resolveAidePath(
54
+ projectPath,
55
+ options.aide
56
+ );
57
+
58
+ if (!existsSync(aidePath)) {
59
+ throw new Error("No .aide file found. Run 'bantay aide init' to create one.");
60
+ }
61
+
62
+ // Parse the aide file
63
+ const aideContent = await readFile(aidePath, "utf-8");
64
+ const aideData = yaml.load(aideContent) as AideData;
65
+
66
+ // Get CUJs to process
67
+ let cujsToProcess: string[];
68
+
69
+ if (options.all) {
70
+ // All CUJs
71
+ cujsToProcess = Object.keys(aideData.entities).filter((id) =>
72
+ id.startsWith("cuj_")
73
+ );
74
+ } else {
75
+ // Diff mode - only added/modified CUJs
76
+ const lockPath = aidePath + ".lock";
77
+ if (!existsSync(lockPath)) {
78
+ throw new Error(
79
+ "No lock file found. Run 'bantay aide lock' first, or use --all for full generation."
80
+ );
81
+ }
82
+
83
+ const lockContent = await readFile(lockPath, "utf-8");
84
+ const lockData = yaml.load(lockContent) as { entities: Record<string, string> };
85
+
86
+ // Find CUJs that are new or modified
87
+ cujsToProcess = Object.keys(aideData.entities).filter((id) => {
88
+ if (!id.startsWith("cuj_")) return false;
89
+ // CUJ is new if not in lock file
90
+ if (!lockData.entities[id]) return true;
91
+ // CUJ is modified if hash differs (we'd need to compute hash, but for now check existence)
92
+ return false;
93
+ });
94
+ }
95
+
96
+ // Build CUJ objects with scenarios and dependencies
97
+ const cujs: CUJ[] = cujsToProcess.map((id) => {
98
+ const entity = aideData.entities[id];
99
+ const props = entity.props || {};
100
+
101
+ // Find scenarios that are children of this CUJ
102
+ const scenarios: Scenario[] = Object.entries(aideData.entities)
103
+ .filter(([scId, scEntity]) =>
104
+ scId.startsWith("sc_") && scEntity.parent === id
105
+ )
106
+ .map(([scId, scEntity]) => ({
107
+ id: scId,
108
+ name: scEntity.props?.name || scId,
109
+ given: scEntity.props?.given,
110
+ when: scEntity.props?.when,
111
+ then: scEntity.props?.then,
112
+ }));
113
+
114
+ // Find dependencies (depends_on relationships where this CUJ is the 'from')
115
+ const dependsOn: string[] = aideData.relationships
116
+ .filter((rel) => rel.from === id && rel.type === "depends_on")
117
+ .map((rel) => rel.to);
118
+
119
+ return {
120
+ id,
121
+ feature: props.feature || id,
122
+ tier: props.tier,
123
+ area: props.area,
124
+ scenarios,
125
+ dependsOn,
126
+ };
127
+ });
128
+
129
+ // Order CUJs into phases using topological sort
130
+ const phases = topologicalSort(cujs);
131
+
132
+ // Generate tasks.md content
133
+ const content = generateTasksMarkdown(phases);
134
+
135
+ // Write to tasks.md
136
+ const outputPath = join(projectPath, "tasks.md");
137
+ await writeFile(outputPath, content, "utf-8");
138
+
139
+ return { outputPath, cujs };
140
+ }
141
+
142
+ function topologicalSort(cujs: CUJ[]): CUJ[][] {
143
+ const cujMap = new Map(cujs.map((c) => [c.id, c]));
144
+ const phases: CUJ[][] = [];
145
+ const processed = new Set<string>();
146
+
147
+ // Keep going until all CUJs are processed
148
+ while (processed.size < cujs.length) {
149
+ const phase: CUJ[] = [];
150
+
151
+ for (const cuj of cujs) {
152
+ if (processed.has(cuj.id)) continue;
153
+
154
+ // Check if all dependencies are already processed
155
+ const allDepsProcessed = cuj.dependsOn.every(
156
+ (dep) => !cujMap.has(dep) || processed.has(dep)
157
+ );
158
+
159
+ if (allDepsProcessed) {
160
+ phase.push(cuj);
161
+ }
162
+ }
163
+
164
+ if (phase.length === 0 && processed.size < cujs.length) {
165
+ // Circular dependency - just add remaining
166
+ for (const cuj of cujs) {
167
+ if (!processed.has(cuj.id)) {
168
+ phase.push(cuj);
169
+ }
170
+ }
171
+ }
172
+
173
+ for (const cuj of phase) {
174
+ processed.add(cuj.id);
175
+ }
176
+
177
+ if (phase.length > 0) {
178
+ phases.push(phase);
179
+ }
180
+ }
181
+
182
+ return phases;
183
+ }
184
+
185
+ function generateTasksMarkdown(phases: CUJ[][]): string {
186
+ const lines: string[] = ["# Tasks", ""];
187
+
188
+ for (let i = 0; i < phases.length; i++) {
189
+ const phase = phases[i];
190
+ lines.push(`## Phase ${i + 1}`);
191
+ lines.push("");
192
+
193
+ for (const cuj of phase) {
194
+ lines.push(`### ${cuj.id}: ${cuj.feature}`);
195
+ lines.push("");
196
+ lines.push(`- [ ] Implement ${cuj.feature}`);
197
+ lines.push("");
198
+
199
+ if (cuj.scenarios.length > 0) {
200
+ lines.push("**Acceptance Criteria:**");
201
+ lines.push("");
202
+ for (const sc of cuj.scenarios) {
203
+ lines.push(`- [ ] ${sc.name}`);
204
+ if (sc.given || sc.when || sc.then) {
205
+ if (sc.given) lines.push(` - Given: ${sc.given}`);
206
+ if (sc.when) lines.push(` - When: ${sc.when}`);
207
+ if (sc.then) lines.push(` - Then: ${sc.then}`);
208
+ }
209
+ }
210
+ lines.push("");
211
+ }
212
+ }
213
+ }
214
+
215
+ return lines.join("\n");
216
+ }
217
+
218
+ export function formatTasks(result: { outputPath: string; cujs: CUJ[] }): string {
219
+ return `Generated ${result.outputPath} with ${result.cujs.length} tasks`;
220
+ }
package/src/export/all.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  import { exportInvariants } from "./invariants";
6
6
  import { exportClaude } from "./claude";
7
7
  import { exportCursor } from "./cursor";
8
+ import { exportCodex } from "./codex";
8
9
  import type { ExportOptions, ExportResult } from "./types";
9
10
 
10
11
  /**
11
- * Export all targets: invariants.md, CLAUDE.md, .cursorrules
12
+ * Export all targets: invariants.md, CLAUDE.md, .cursorrules, AGENTS.md
12
13
  */
13
14
  export async function exportAll(
14
15
  projectPath: string,
@@ -25,5 +26,8 @@ export async function exportAll(
25
26
  // Export .cursorrules
26
27
  results.push(await exportCursor(projectPath, options));
27
28
 
29
+ // Export AGENTS.md
30
+ results.push(await exportCodex(projectPath, options));
31
+
28
32
  return results;
29
33
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { readFile, writeFile, access } from "fs/promises";
6
6
  import { join } from "path";
7
- import { read as readAide } from "../aide";
7
+ import { read as readAide, resolveAidePath } from "../aide";
8
8
  import {
9
9
  extractConstraints,
10
10
  extractFoundations,
@@ -27,7 +27,8 @@ import {
27
27
  export function generateClaudeSection(
28
28
  constraints: ExtractedConstraint[],
29
29
  foundations: ExtractedFoundation[],
30
- invariants: ExtractedInvariant[]
30
+ invariants: ExtractedInvariant[],
31
+ aideFilename: string = "*.aide"
31
32
  ): string {
32
33
  const lines: string[] = [];
33
34
 
@@ -35,7 +36,7 @@ export function generateClaudeSection(
35
36
  lines.push("");
36
37
  lines.push("## Bantay Project Rules");
37
38
  lines.push("");
38
- lines.push("*Auto-generated from bantay.aide. Do not edit manually.*");
39
+ lines.push(`*Auto-generated from ${aideFilename}. Do not edit manually.*`);
39
40
  lines.push("");
40
41
 
41
42
  // Foundations as principles
@@ -184,7 +185,9 @@ export async function exportClaude(
184
185
  projectPath: string,
185
186
  options: ExportOptions = {}
186
187
  ): Promise<ExportResult> {
187
- const aidePath = options.aidePath || join(projectPath, "bantay.aide");
188
+ // Discover aide file if not explicitly provided
189
+ const resolved = await resolveAidePath(projectPath, options.aidePath);
190
+ const aidePath = resolved.path;
188
191
  const outputPath = options.outputPath || join(projectPath, "CLAUDE.md");
189
192
 
190
193
  // Read the aide tree
@@ -196,7 +199,7 @@ export async function exportClaude(
196
199
  const invariants = extractInvariants(tree);
197
200
 
198
201
  // Generate section content
199
- const section = generateClaudeSection(constraints, foundations, invariants);
202
+ const section = generateClaudeSection(constraints, foundations, invariants, resolved.filename);
200
203
 
201
204
  // Read existing file if it exists
202
205
  let existingContent = "";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Export to AGENTS.md with section markers
3
+ * Works for Codex, Copilot, and any agent that reads AGENTS.md
4
+ */
5
+
6
+ import { readFile, writeFile, access } from "fs/promises";
7
+ import { join } from "path";
8
+ import { read as readAide, resolveAidePath } from "../aide";
9
+ import {
10
+ extractConstraints,
11
+ extractFoundations,
12
+ extractInvariants,
13
+ } from "./aide-reader";
14
+ import { generateClaudeSection, insertSection } from "./claude";
15
+ import type { ExportOptions, ExportResult } from "./types";
16
+
17
+ /**
18
+ * Check if a file exists
19
+ */
20
+ async function fileExists(path: string): Promise<boolean> {
21
+ try {
22
+ await access(path);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Export to AGENTS.md
31
+ */
32
+ export async function exportCodex(
33
+ projectPath: string,
34
+ options: ExportOptions = {}
35
+ ): Promise<ExportResult> {
36
+ // Discover aide file if not explicitly provided
37
+ const resolved = await resolveAidePath(projectPath, options.aidePath);
38
+ const aidePath = resolved.path;
39
+ const outputPath = options.outputPath || join(projectPath, "AGENTS.md");
40
+
41
+ // Read the aide tree
42
+ const tree = await readAide(aidePath);
43
+
44
+ // Extract entities
45
+ const constraints = extractConstraints(tree);
46
+ const foundations = extractFoundations(tree);
47
+ const invariants = extractInvariants(tree);
48
+
49
+ // Generate section content (same format as claude)
50
+ const section = generateClaudeSection(constraints, foundations, invariants, resolved.filename);
51
+
52
+ // Read existing file if it exists
53
+ let existingContent = "";
54
+ if (await fileExists(outputPath)) {
55
+ existingContent = await readFile(outputPath, "utf-8");
56
+ }
57
+
58
+ // Insert or replace section
59
+ const content = insertSection(existingContent, section);
60
+
61
+ // Write unless dry run
62
+ if (!options.dryRun) {
63
+ await writeFile(outputPath, content, "utf-8");
64
+ }
65
+
66
+ return {
67
+ target: "codex",
68
+ outputPath,
69
+ content,
70
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
71
+ };
72
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { readFile, writeFile, access } from "fs/promises";
6
6
  import { join } from "path";
7
- import { read as readAide } from "../aide";
7
+ import { read as readAide, resolveAidePath } from "../aide";
8
8
  import {
9
9
  extractConstraints,
10
10
  extractFoundations,
@@ -32,7 +32,9 @@ export async function exportCursor(
32
32
  projectPath: string,
33
33
  options: ExportOptions = {}
34
34
  ): Promise<ExportResult> {
35
- const aidePath = options.aidePath || join(projectPath, "bantay.aide");
35
+ // Discover aide file if not explicitly provided
36
+ const resolved = await resolveAidePath(projectPath, options.aidePath);
37
+ const aidePath = resolved.path;
36
38
  const outputPath = options.outputPath || join(projectPath, ".cursorrules");
37
39
 
38
40
  // Read the aide tree
@@ -44,7 +46,7 @@ export async function exportCursor(
44
46
  const invariants = extractInvariants(tree);
45
47
 
46
48
  // Generate section content (same format as claude)
47
- const section = generateClaudeSection(constraints, foundations, invariants);
49
+ const section = generateClaudeSection(constraints, foundations, invariants, resolved.filename);
48
50
 
49
51
  // Read existing file if it exists
50
52
  let existingContent = "";
@@ -5,6 +5,7 @@
5
5
  export { exportInvariants, generateInvariantsMd } from "./invariants";
6
6
  export { exportClaude, generateClaudeSection, insertSection } from "./claude";
7
7
  export { exportCursor } from "./cursor";
8
+ export { exportCodex } from "./codex";
8
9
  export { exportAll } from "./all";
9
10
 
10
11
  export type {