@bantay/cli 0.2.0 → 0.3.1

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,387 @@
1
+ /**
2
+ * diff.ts — bantay diff command
3
+ *
4
+ * Wraps bantay aide diff with entity type classification based on parent chain.
5
+ * bantay aide diff stays unchanged (raw structural output).
6
+ * bantay diff adds classification for human-readable output.
7
+ *
8
+ * @scenario sc_diff_classified
9
+ */
10
+
11
+ import { existsSync } from "fs";
12
+ import { readFile, readdir } from "fs/promises";
13
+ import { read, type AideTree } from "../aide";
14
+
15
+ /**
16
+ * Container parents that define entity types
17
+ */
18
+ const PARENT_TYPE_MAP: Record<string, string> = {
19
+ cujs: "scenario",
20
+ invariants: "invariant",
21
+ constraints: "constraint",
22
+ foundations: "foundation",
23
+ wisdom: "wisdom",
24
+ };
25
+
26
+ const CUJ_PREFIX = "cuj_";
27
+
28
+ /**
29
+ * Classified change entry
30
+ */
31
+ export interface ClassifiedChange {
32
+ action: "ADDED" | "MODIFIED" | "REMOVED";
33
+ type: string;
34
+ entity_id: string;
35
+ parent?: string;
36
+ from?: string;
37
+ to?: string;
38
+ relationship_type?: string;
39
+ }
40
+
41
+ /**
42
+ * Result from diff command
43
+ */
44
+ export interface DiffResult {
45
+ hasChanges: boolean;
46
+ changes: ClassifiedChange[];
47
+ }
48
+
49
+ /**
50
+ * Discover aide file in directory
51
+ */
52
+ async function discoverAideFile(cwd: string): Promise<string | null> {
53
+ try {
54
+ const files = await readdir(cwd);
55
+ const aideFiles = files.filter((f) => f.endsWith(".aide"));
56
+ if (aideFiles.length === 1) {
57
+ return `${cwd}/${aideFiles[0]}`;
58
+ }
59
+ return null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get entity type by walking parent chain
67
+ */
68
+ function getEntityTypeByParent(id: string, parent: string | undefined, tree: AideTree): string {
69
+ if (!parent) {
70
+ return "entity";
71
+ }
72
+
73
+ // Direct mapping
74
+ if (parent in PARENT_TYPE_MAP) {
75
+ return PARENT_TYPE_MAP[parent];
76
+ }
77
+
78
+ // If parent starts with cuj_, child is a scenario
79
+ if (parent.startsWith(CUJ_PREFIX)) {
80
+ return "scenario";
81
+ }
82
+
83
+ // Walk up the parent chain
84
+ const parentEntity = tree.entities[parent];
85
+ if (parentEntity && parentEntity.parent) {
86
+ return getEntityTypeByParent(id, parentEntity.parent, tree);
87
+ }
88
+
89
+ return "entity";
90
+ }
91
+
92
+ /**
93
+ * Get entity type from parent ID (for removed entities)
94
+ */
95
+ function getEntityTypeFromParentId(parentId: string): string {
96
+ if (parentId in PARENT_TYPE_MAP) {
97
+ return PARENT_TYPE_MAP[parentId];
98
+ }
99
+ if (parentId.startsWith(CUJ_PREFIX)) {
100
+ return "scenario";
101
+ }
102
+ return "entity";
103
+ }
104
+
105
+ /**
106
+ * Fallback: get entity type from ID prefix
107
+ */
108
+ function getEntityTypeByIdPrefix(id: string): string {
109
+ const prefixes: Record<string, string> = {
110
+ cuj_: "cuj",
111
+ sc_: "scenario",
112
+ inv_: "invariant",
113
+ con_: "constraint",
114
+ found_: "foundation",
115
+ wis_: "wisdom",
116
+ };
117
+ for (const [prefix, type] of Object.entries(prefixes)) {
118
+ if (id.startsWith(prefix)) {
119
+ return type;
120
+ }
121
+ }
122
+ return "entity";
123
+ }
124
+
125
+ /**
126
+ * Lock file entity with parent info
127
+ */
128
+ interface LockFileEntity {
129
+ hash: string;
130
+ parent?: string;
131
+ }
132
+
133
+ interface LockFile {
134
+ entities: Record<string, LockFileEntity>;
135
+ relationships: string[];
136
+ }
137
+
138
+ /**
139
+ * Parse lock file content
140
+ */
141
+ function parseLockFile(content: string): LockFile {
142
+ const result: LockFile = {
143
+ entities: {},
144
+ relationships: [],
145
+ };
146
+
147
+ let section = "";
148
+ const lines = content.split("\n");
149
+
150
+ for (const line of lines) {
151
+ const trimmed = line.trim();
152
+
153
+ if (trimmed.startsWith("#") || trimmed === "") {
154
+ continue;
155
+ }
156
+
157
+ if (trimmed === "entities:") {
158
+ section = "entities";
159
+ continue;
160
+ }
161
+ if (trimmed === "relationships:") {
162
+ section = "relationships";
163
+ continue;
164
+ }
165
+
166
+ if (section === "entities") {
167
+ const matchWithParent = trimmed.match(/^(\w+):\s*(\w+)\s+parent:(\w+)$/);
168
+ if (matchWithParent) {
169
+ result.entities[matchWithParent[1]] = {
170
+ hash: matchWithParent[2],
171
+ parent: matchWithParent[3],
172
+ };
173
+ } else {
174
+ const match = trimmed.match(/^(\w+):\s*(\w+)$/);
175
+ if (match) {
176
+ result.entities[match[1]] = {
177
+ hash: match[2],
178
+ };
179
+ }
180
+ }
181
+ } else if (section === "relationships") {
182
+ const match = trimmed.match(/^-\s*(\w+):(\w+):(\w+):\s*\w+$/);
183
+ if (match) {
184
+ result.relationships.push(`${match[1]}:${match[2]}:${match[3]}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ /**
193
+ * Compute entity hash (same as aide.ts)
194
+ */
195
+ function computeEntityHash(
196
+ id: string,
197
+ entity: { display?: string; parent?: string; props?: Record<string, unknown> }
198
+ ): string {
199
+ const str = JSON.stringify({ id, ...entity });
200
+ let hash = 0;
201
+ for (let i = 0; i < str.length; i++) {
202
+ const char = str.charCodeAt(i);
203
+ hash = ((hash << 5) - hash) + char;
204
+ hash = hash & hash;
205
+ }
206
+ return Math.abs(hash).toString(16).padStart(8, "0");
207
+ }
208
+
209
+ /**
210
+ * Run diff and classify changes
211
+ */
212
+ export async function runDiff(projectPath: string): Promise<DiffResult> {
213
+ const aidePath = await discoverAideFile(projectPath);
214
+
215
+ if (!aidePath) {
216
+ throw new Error("No .aide file found. Run 'bantay aide init' to create one.");
217
+ }
218
+
219
+ const lockPath = `${aidePath}.lock`;
220
+
221
+ if (!existsSync(lockPath)) {
222
+ throw new Error(`Lock file not found: ${lockPath}. Run 'bantay aide lock' first.`);
223
+ }
224
+
225
+ // Read current aide tree
226
+ const tree = await read(aidePath);
227
+
228
+ // Read and parse lock file
229
+ const lockContent = await readFile(lockPath, "utf-8");
230
+ const lock = parseLockFile(lockContent);
231
+
232
+ // Compute current hashes
233
+ const currentHashes: Record<string, string> = {};
234
+ for (const [id, entity] of Object.entries(tree.entities)) {
235
+ currentHashes[id] = computeEntityHash(id, entity);
236
+ }
237
+
238
+ // Compute current relationships
239
+ const currentRelationships = new Set<string>();
240
+ for (const rel of tree.relationships) {
241
+ currentRelationships.add(`${rel.from}:${rel.to}:${rel.type}`);
242
+ }
243
+
244
+ const lockRelationships = new Set<string>(lock.relationships);
245
+
246
+ // Build classified changes
247
+ const changes: ClassifiedChange[] = [];
248
+
249
+ // Added entities
250
+ for (const id of Object.keys(currentHashes)) {
251
+ if (!(id in lock.entities)) {
252
+ const entity = tree.entities[id];
253
+ const parent = entity.parent;
254
+ const entityType = getEntityTypeByParent(id, parent, tree);
255
+ changes.push({
256
+ action: "ADDED",
257
+ type: entityType,
258
+ entity_id: id,
259
+ parent,
260
+ });
261
+ } else if (lock.entities[id].hash !== currentHashes[id]) {
262
+ const entity = tree.entities[id];
263
+ const parent = entity.parent;
264
+ const entityType = getEntityTypeByParent(id, parent, tree);
265
+ changes.push({
266
+ action: "MODIFIED",
267
+ type: entityType,
268
+ entity_id: id,
269
+ parent,
270
+ });
271
+ }
272
+ }
273
+
274
+ // Removed entities
275
+ for (const id of Object.keys(lock.entities)) {
276
+ if (!(id in currentHashes)) {
277
+ const lockEntity = lock.entities[id];
278
+ let entityType: string;
279
+ if (lockEntity.parent) {
280
+ entityType = getEntityTypeFromParentId(lockEntity.parent);
281
+ } else {
282
+ entityType = getEntityTypeByIdPrefix(id);
283
+ }
284
+ changes.push({
285
+ action: "REMOVED",
286
+ type: entityType,
287
+ entity_id: id,
288
+ parent: lockEntity.parent,
289
+ });
290
+ }
291
+ }
292
+
293
+ // Added relationships
294
+ for (const rel of currentRelationships) {
295
+ if (!lockRelationships.has(rel)) {
296
+ const [from, to, relType] = rel.split(":");
297
+ changes.push({
298
+ action: "ADDED",
299
+ type: "relationship",
300
+ entity_id: `${from}:${to}`,
301
+ from,
302
+ to,
303
+ relationship_type: relType,
304
+ });
305
+ }
306
+ }
307
+
308
+ // Removed relationships
309
+ for (const rel of lockRelationships) {
310
+ if (!currentRelationships.has(rel)) {
311
+ const [from, to, relType] = rel.split(":");
312
+ changes.push({
313
+ action: "REMOVED",
314
+ type: "relationship",
315
+ entity_id: `${from}:${to}`,
316
+ from,
317
+ to,
318
+ relationship_type: relType,
319
+ });
320
+ }
321
+ }
322
+
323
+ return {
324
+ hasChanges: changes.length > 0,
325
+ changes,
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Format diff result as human-readable output
331
+ */
332
+ export function formatDiff(result: DiffResult): string {
333
+ if (!result.hasChanges) {
334
+ return "No changes since last lock.";
335
+ }
336
+
337
+ const lines: string[] = [];
338
+
339
+ // Sort by action then by entity_id
340
+ const sorted = [...result.changes].sort((a, b) => {
341
+ const actionOrder = { ADDED: 0, MODIFIED: 1, REMOVED: 2 };
342
+ const actionDiff = actionOrder[a.action] - actionOrder[b.action];
343
+ if (actionDiff !== 0) return actionDiff;
344
+ return a.entity_id.localeCompare(b.entity_id);
345
+ });
346
+
347
+ for (const change of sorted) {
348
+ if (change.type === "relationship") {
349
+ lines.push(`${change.action} relationship: ${change.from} → ${change.to}`);
350
+ } else {
351
+ const parentStr = change.parent ? ` (parent: ${change.parent})` : "";
352
+ lines.push(`${change.action} ${change.type}: ${change.entity_id}${parentStr}`);
353
+ }
354
+ }
355
+
356
+ return lines.join("\n");
357
+ }
358
+
359
+ /**
360
+ * Format diff result as JSON
361
+ */
362
+ export function formatDiffJson(result: DiffResult): string {
363
+ return JSON.stringify(result, null, 2);
364
+ }
365
+
366
+ /**
367
+ * Handle bantay diff command
368
+ */
369
+ export async function handleDiff(args: string[]): Promise<void> {
370
+ const projectPath = process.cwd();
371
+ const jsonOutput = args.includes("--json");
372
+
373
+ try {
374
+ const result = await runDiff(projectPath);
375
+
376
+ if (jsonOutput) {
377
+ console.log(formatDiffJson(result));
378
+ } else {
379
+ console.log(formatDiff(result));
380
+ }
381
+
382
+ process.exit(result.hasChanges ? 1 : 0);
383
+ } catch (error) {
384
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
385
+ process.exit(1);
386
+ }
387
+ }
@@ -1,11 +1,18 @@
1
- import { writeFile, access } from "fs/promises";
1
+ import { writeFile, access, mkdir } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { detectStack, type StackDetectionResult } from "../detectors";
4
4
  import { generateInvariants } from "../generators/invariants";
5
5
  import { generateConfig, configToYaml } from "../generators/config";
6
+ import {
7
+ generateInterviewCommand,
8
+ generateStatusCommand,
9
+ generateCheckCommand,
10
+ generateOrchestrateCommand,
11
+ } from "../generators/claude-commands";
6
12
 
7
13
  export interface InitOptions {
8
14
  regenerateConfig?: boolean;
15
+ force?: boolean;
9
16
  }
10
17
 
11
18
  export interface InitResult {
@@ -66,6 +73,36 @@ export async function runInit(
66
73
  filesCreated.push("bantay.config.yml");
67
74
  }
68
75
 
76
+ // Generate Claude Code slash commands
77
+ const claudeCommandsDir = join(projectPath, ".claude", "commands");
78
+ await mkdir(claudeCommandsDir, { recursive: true });
79
+
80
+ const interviewPath = join(claudeCommandsDir, "bantay-interview.md");
81
+ const statusPath = join(claudeCommandsDir, "bantay-status.md");
82
+ const checkPath = join(claudeCommandsDir, "bantay-check.md");
83
+ const orchestratePath = join(claudeCommandsDir, "bantay-orchestrate.md");
84
+
85
+ // Only create if they don't exist (don't overwrite user customizations) unless --force
86
+ if (options?.force || !(await fileExists(interviewPath))) {
87
+ await writeFile(interviewPath, generateInterviewCommand());
88
+ filesCreated.push(".claude/commands/bantay-interview.md");
89
+ }
90
+
91
+ if (options?.force || !(await fileExists(statusPath))) {
92
+ await writeFile(statusPath, generateStatusCommand());
93
+ filesCreated.push(".claude/commands/bantay-status.md");
94
+ }
95
+
96
+ if (options?.force || !(await fileExists(checkPath))) {
97
+ await writeFile(checkPath, generateCheckCommand());
98
+ filesCreated.push(".claude/commands/bantay-check.md");
99
+ }
100
+
101
+ if (options?.force || !(await fileExists(orchestratePath))) {
102
+ await writeFile(orchestratePath, generateOrchestrateCommand());
103
+ filesCreated.push(".claude/commands/bantay-orchestrate.md");
104
+ }
105
+
69
106
  return {
70
107
  success: true,
71
108
  filesCreated,
@@ -1,6 +1,7 @@
1
1
  import { readFile, readdir, access } from "fs/promises";
2
2
  import { join, relative } from "path";
3
3
  import * as yaml from "js-yaml";
4
+ import { tryResolveAidePath } from "../aide";
4
5
 
5
6
  export interface StatusOptions {
6
7
  json?: boolean;
@@ -146,17 +147,18 @@ export async function runStatus(
146
147
  projectPath: string,
147
148
  options?: StatusOptions
148
149
  ): Promise<StatusResult> {
149
- const aidePath = join(projectPath, "bantay.aide");
150
-
151
- // Check if aide file exists
152
- if (!(await fileExists(aidePath))) {
150
+ // Discover aide file
151
+ const resolved = await tryResolveAidePath(projectPath);
152
+ if (!resolved) {
153
153
  return {
154
154
  scenarios: [],
155
155
  summary: { total: 0, implemented: 0, missing: 0 },
156
- error: "bantay.aide not found",
156
+ error: "No .aide file found. Run 'bantay aide init' to create one.",
157
157
  };
158
158
  }
159
159
 
160
+ const aidePath = resolved.path;
161
+
160
162
  // Parse aide file
161
163
  let aideContent: AideFile;
162
164
  try {
@@ -166,7 +168,7 @@ export async function runStatus(
166
168
  return {
167
169
  scenarios: [],
168
170
  summary: { total: 0, implemented: 0, missing: 0 },
169
- error: `Failed to parse bantay.aide: ${e instanceof Error ? e.message : String(e)}`,
171
+ error: `Failed to parse ${resolved.filename}: ${e instanceof Error ? e.message : String(e)}`,
170
172
  };
171
173
  }
172
174
 
@@ -174,7 +176,7 @@ export async function runStatus(
174
176
  return {
175
177
  scenarios: [],
176
178
  summary: { total: 0, implemented: 0, missing: 0 },
177
- error: "bantay.aide has no entities",
179
+ error: `${resolved.filename} has no entities`,
178
180
  };
179
181
  }
180
182
 
@@ -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
+ }