@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.
- package/package.json +3 -3
- package/src/aide/discovery.ts +88 -0
- package/src/aide/index.ts +9 -0
- package/src/cli.ts +133 -11
- package/src/commands/aide.ts +432 -48
- package/src/commands/check.ts +9 -4
- package/src/commands/ci.ts +228 -0
- package/src/commands/diff.ts +387 -0
- package/src/commands/init.ts +38 -1
- package/src/commands/status.ts +311 -0
- package/src/commands/tasks.ts +220 -0
- package/src/export/all.ts +5 -1
- package/src/export/claude.ts +8 -5
- package/src/export/codex.ts +72 -0
- package/src/export/cursor.ts +5 -3
- package/src/export/index.ts +1 -0
- package/src/export/invariants.ts +7 -5
- package/src/generators/claude-commands.ts +61 -0
- package/src/templates/commands/bantay-check.md +58 -0
- package/src/templates/commands/bantay-interview.md +207 -0
- package/src/templates/commands/bantay-orchestrate.md +164 -0
- package/src/templates/commands/bantay-status.md +39 -0
|
@@ -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
|
}
|
package/src/export/claude.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
+
}
|
package/src/export/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
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 = "";
|
package/src/export/index.ts
CHANGED
|
@@ -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 {
|