bastard-framework 1.0.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/cli.js ADDED
@@ -0,0 +1,3142 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5 } from "fs";
7
+ import { basename, resolve as resolve3 } from "path";
8
+
9
+ // src/rounds.ts
10
+ var ROUNDS = [
11
+ {
12
+ id: 1,
13
+ name: "Vision & Product",
14
+ shortName: "product",
15
+ frameworks: ["bmad"],
16
+ description: "Transformer une idee en PRD actionnable. Zero code avant ca.",
17
+ gateChecks: [
18
+ {
19
+ id: "prd-content",
20
+ description: "PRD.md has real content (not just template)",
21
+ type: "content_validated",
22
+ schemaKey: "prd"
23
+ },
24
+ {
25
+ id: "personas-content",
26
+ description: "PERSONAS.md has real content",
27
+ type: "content_validated",
28
+ schemaKey: "personas"
29
+ },
30
+ {
31
+ id: "acceptance-content",
32
+ description: "ACCEPTANCE_CRITERIA.md has real content with AC-XXX format",
33
+ type: "content_validated",
34
+ schemaKey: "acceptance"
35
+ },
36
+ {
37
+ id: "round1-approval",
38
+ description: "Human approved Round 1 outputs",
39
+ type: "human_approval"
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ id: 2,
45
+ name: "Design System & UX",
46
+ shortName: "design",
47
+ frameworks: ["gstack", "turbo"],
48
+ description: "Poser l'identite visuelle avant de toucher au CSS.",
49
+ gateChecks: [
50
+ {
51
+ id: "design-content",
52
+ description: "DESIGN.md has real tokens (colors, typography, spacing)",
53
+ type: "content_validated",
54
+ schemaKey: "design"
55
+ },
56
+ {
57
+ id: "round2-approval",
58
+ description: "Human approved design system",
59
+ type: "human_approval"
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ id: 3,
65
+ name: "Architecture",
66
+ shortName: "architecture",
67
+ frameworks: ["bmad", "super"],
68
+ description: "Les decisions techniques ecrites avant d'etre codees.",
69
+ gateChecks: [
70
+ {
71
+ id: "arch-content",
72
+ description: "ARCHITECTURE.md has real content (stack, data model)",
73
+ type: "content_validated",
74
+ schemaKey: "architecture"
75
+ },
76
+ {
77
+ id: "adr-validated",
78
+ description: "At least 1 valid ADR (proper Status/Context/Decision/Consequences)",
79
+ type: "adr_validated"
80
+ },
81
+ {
82
+ id: "round3-approval",
83
+ description: "Human approved architecture",
84
+ type: "human_approval"
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ id: 4,
90
+ name: "Decomposition & Planification",
91
+ shortName: "planning",
92
+ frameworks: ["taskm", "gsd"],
93
+ description: "Transformer l'architecture en taches executables sans ambiguite.",
94
+ gateChecks: [
95
+ {
96
+ id: "context-exists",
97
+ description: "CONTEXT.md exists",
98
+ type: "file_exists",
99
+ path: "docs/planning/CONTEXT.md"
100
+ },
101
+ {
102
+ id: "tasks-exist",
103
+ description: "Task files exist",
104
+ type: "min_count",
105
+ path: "tasks",
106
+ minCount: 1
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ id: 5,
112
+ name: "Execution du Code",
113
+ shortName: "execution",
114
+ frameworks: ["gsd", "super"],
115
+ description: "Builder sans context rot, avec le bon cerveau au bon moment.",
116
+ gateChecks: [
117
+ {
118
+ id: "src-exists",
119
+ description: "Source code exists",
120
+ type: "min_count",
121
+ path: "src",
122
+ minCount: 1
123
+ },
124
+ {
125
+ id: "tests-exist",
126
+ description: "Tests exist",
127
+ type: "min_count",
128
+ path: "tests",
129
+ minCount: 1
130
+ }
131
+ ]
132
+ },
133
+ {
134
+ id: 6,
135
+ name: "Securite",
136
+ shortName: "security",
137
+ frameworks: ["tob", "super"],
138
+ description: "La securite est integree au build, pas ajoutee apres livraison.",
139
+ gateChecks: [
140
+ {
141
+ id: "threat-model-content",
142
+ description: "THREAT_MODEL.md has STRIDE analysis",
143
+ type: "content_validated",
144
+ schemaKey: "threat_model"
145
+ },
146
+ {
147
+ id: "security-review-content",
148
+ description: "SECURITY_REVIEW.md has OWASP checklist + findings",
149
+ type: "content_validated",
150
+ schemaKey: "security_review"
151
+ }
152
+ ]
153
+ },
154
+ {
155
+ id: 7,
156
+ name: "QA & Tests",
157
+ shortName: "qa",
158
+ frameworks: ["gstack"],
159
+ description: "Verifier que ce qu'on a bati marche vraiment.",
160
+ gateChecks: [
161
+ {
162
+ id: "round7-approval",
163
+ description: "QA passed \u2014 Design Score >= B, AI Slop Score = A",
164
+ type: "human_approval"
165
+ }
166
+ ]
167
+ },
168
+ {
169
+ id: 8,
170
+ name: "Review & Livraison",
171
+ shortName: "shipping",
172
+ frameworks: ["gstack", "gsd"],
173
+ description: "Shipper ce qu'on avait prevu de shipper, ni plus ni moins.",
174
+ gateChecks: [
175
+ {
176
+ id: "acceptance-pass",
177
+ description: "Acceptance criteria 100% green",
178
+ type: "human_approval"
179
+ }
180
+ ]
181
+ }
182
+ ];
183
+ function getRound(id) {
184
+ return ROUNDS.find((r) => r.id === id);
185
+ }
186
+
187
+ // src/state.ts
188
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
189
+ import { join } from "path";
190
+ var STATE_DIR = ".bastard";
191
+ var STATE_FILE = "state.json";
192
+ function getStatePath(projectRoot) {
193
+ return join(projectRoot, STATE_DIR, STATE_FILE);
194
+ }
195
+ function stateExists(projectRoot) {
196
+ return existsSync(getStatePath(projectRoot));
197
+ }
198
+ function createInitialState(projectName) {
199
+ const rounds = {};
200
+ for (const round of ROUNDS) {
201
+ rounds[String(round.id)] = {
202
+ status: round.id === 1 ? "active" : "locked",
203
+ gate: "pending",
204
+ gateResults: [],
205
+ startedAt: round.id === 1 ? (/* @__PURE__ */ new Date()).toISOString() : null,
206
+ completedAt: null
207
+ };
208
+ }
209
+ return {
210
+ version: "0.1.0",
211
+ project: projectName,
212
+ currentRound: 1,
213
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
214
+ rounds,
215
+ history: [
216
+ {
217
+ action: "init",
218
+ round: 1,
219
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
220
+ details: `Project "${projectName}" initialized`
221
+ }
222
+ ]
223
+ };
224
+ }
225
+ function saveState(projectRoot, state) {
226
+ const dir = join(projectRoot, STATE_DIR);
227
+ if (!existsSync(dir)) {
228
+ mkdirSync(dir, { recursive: true });
229
+ }
230
+ writeFileSync(getStatePath(projectRoot), JSON.stringify(state, null, 2) + "\n");
231
+ }
232
+ function loadState(projectRoot) {
233
+ const path = getStatePath(projectRoot);
234
+ if (!existsSync(path)) {
235
+ throw new Error("No BASTARD project found. Run `bastard init` first.");
236
+ }
237
+ try {
238
+ return JSON.parse(readFileSync(path, "utf-8"));
239
+ } catch {
240
+ throw new Error(
241
+ `Corrupted state file: ${path}
242
+ Run 'bastard reset' to reinitialize or fix the JSON manually.`
243
+ );
244
+ }
245
+ }
246
+ function addHistory(state, action, round, details) {
247
+ state.history.push({
248
+ action,
249
+ round,
250
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
251
+ details
252
+ });
253
+ }
254
+ function advanceRound(state) {
255
+ const current = state.currentRound;
256
+ const currentState = state.rounds[String(current)];
257
+ if (!currentState || currentState.gate !== "passed") {
258
+ return false;
259
+ }
260
+ currentState.status = "completed";
261
+ currentState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
262
+ const nextRound = current + 1;
263
+ if (nextRound > 8) {
264
+ addHistory(state, "project_complete", current, "All 8 rounds completed");
265
+ return true;
266
+ }
267
+ state.currentRound = nextRound;
268
+ state.rounds[String(nextRound)].status = "active";
269
+ state.rounds[String(nextRound)].startedAt = (/* @__PURE__ */ new Date()).toISOString();
270
+ addHistory(state, "advance", nextRound, `Advanced to Round ${nextRound}`);
271
+ return true;
272
+ }
273
+ function approveGate(state, roundId) {
274
+ const roundState = state.rounds[String(roundId)];
275
+ if (!roundState) return;
276
+ const round = ROUNDS.find((r) => r.id === roundId);
277
+ if (!round) return;
278
+ for (const check of round.gateChecks) {
279
+ if (check.type === "human_approval") {
280
+ const existing = roundState.gateResults.findIndex((r) => r.checkId === check.id);
281
+ const result = {
282
+ checkId: check.id,
283
+ passed: true,
284
+ message: "Approved by human",
285
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
286
+ };
287
+ if (existing >= 0) {
288
+ roundState.gateResults[existing] = result;
289
+ } else {
290
+ roundState.gateResults.push(result);
291
+ }
292
+ }
293
+ }
294
+ addHistory(state, "approve", roundId, `Human approved Round ${roundId} gate`);
295
+ }
296
+
297
+ // src/gates.ts
298
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
299
+ import { join as join3 } from "path";
300
+
301
+ // src/schemas.ts
302
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync } from "fs";
303
+ import { join as join2 } from "path";
304
+ var SCHEMAS = {
305
+ prd: {
306
+ path: "docs/product/PRD.md",
307
+ description: "Product Requirements Document",
308
+ sections: [
309
+ { heading: "## Problem", minContentLength: 100 },
310
+ { heading: "## Users", minContentLength: 50 },
311
+ { heading: "## User Stories", minContentLength: 80, pattern: /As a .+, I want .+ so that/, patternDescription: 'At least one user story in "As a..., I want... so that..." format' },
312
+ { heading: "## Success Metrics", minContentLength: 50 },
313
+ { heading: "## Scope", minContentLength: 30 }
314
+ ]
315
+ },
316
+ personas: {
317
+ path: "docs/product/PERSONAS.md",
318
+ description: "User Personas",
319
+ sections: [
320
+ { heading: "## Persona 1", minContentLength: 100 },
321
+ { heading: "## Persona 2", minContentLength: 100 }
322
+ ]
323
+ },
324
+ acceptance: {
325
+ path: "docs/product/ACCEPTANCE_CRITERIA.md",
326
+ description: "Acceptance Criteria",
327
+ sections: [
328
+ { heading: "## Core Criteria", minContentLength: 50, pattern: /AC-\d{3}/, patternDescription: "At least one criterion in AC-XXX format" }
329
+ ]
330
+ },
331
+ design: {
332
+ path: "docs/design/DESIGN.md",
333
+ description: "Design System",
334
+ sections: [
335
+ { heading: "## Color", minContentLength: 50, pattern: /--color-/, patternDescription: "At least one CSS custom property (--color-*)" },
336
+ { heading: "## Typography", minContentLength: 50, pattern: /--font-|--text-/, patternDescription: "At least one typography token" },
337
+ { heading: "## Spacing", minContentLength: 30 }
338
+ ]
339
+ },
340
+ architecture: {
341
+ path: "docs/architecture/ARCHITECTURE.md",
342
+ description: "Architecture Document",
343
+ sections: [
344
+ { heading: "## Stack", minContentLength: 50 },
345
+ { heading: "## Data Model", minContentLength: 30 }
346
+ ]
347
+ },
348
+ adr: {
349
+ path: "",
350
+ // validated per-file in ADR directory
351
+ description: "Architecture Decision Record",
352
+ sections: [
353
+ { heading: "## Status", minContentLength: 5, pattern: /Proposed|Accepted|Rejected|Superseded/, patternDescription: "Valid status: Proposed, Accepted, Rejected, or Superseded" },
354
+ { heading: "## Context", minContentLength: 50 },
355
+ { heading: "## Decision", minContentLength: 30 },
356
+ { heading: "## Consequences", minContentLength: 30 }
357
+ ]
358
+ },
359
+ threat_model: {
360
+ path: "docs/security/THREAT_MODEL.md",
361
+ description: "Threat Model",
362
+ sections: [
363
+ { heading: "## Assets", minContentLength: 30 },
364
+ { heading: "## STRIDE", minContentLength: 50 }
365
+ ]
366
+ },
367
+ security_review: {
368
+ path: "docs/security/SECURITY_REVIEW.md",
369
+ description: "Security Review",
370
+ sections: [
371
+ { heading: "## OWASP Top 10", minContentLength: 50 },
372
+ { heading: "## Findings", minContentLength: 10 }
373
+ ]
374
+ }
375
+ };
376
+ function extractSection(content, heading) {
377
+ const level = heading.match(/^#+/)?.[0].length ?? 2;
378
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
379
+ const pattern = new RegExp(`${escapedHeading}[^
380
+ ]*
381
+ ([\\s\\S]*?)(?=
382
+ #{1,${level}} |$)`);
383
+ const match = content.match(pattern);
384
+ if (!match) return null;
385
+ return match[1].trim();
386
+ }
387
+ function validateDocument(projectRoot, schema, filePath) {
388
+ const docPath = filePath ?? schema.path;
389
+ const fullPath = join2(projectRoot, docPath);
390
+ if (!existsSync2(fullPath)) {
391
+ return {
392
+ path: docPath,
393
+ description: schema.description,
394
+ fileExists: false,
395
+ sections: [],
396
+ allPassed: false
397
+ };
398
+ }
399
+ const content = readFileSync2(fullPath, "utf-8");
400
+ const sections = schema.sections.map((rule) => {
401
+ const sectionContent = extractSection(content, rule.heading);
402
+ const exists = sectionContent !== null;
403
+ const contentLength = sectionContent?.length ?? 0;
404
+ const strippedContent = sectionContent?.replace(/^[-*]\s*$/gm, "").replace(/\|[\s|]*\|/g, "").replace(/^\s*$/gm, "").trim() ?? "";
405
+ const realLength = strippedContent.length;
406
+ const meetsMinLength = realLength >= rule.minContentLength;
407
+ let patternMatched = null;
408
+ if (rule.pattern && sectionContent) {
409
+ patternMatched = rule.pattern.test(sectionContent);
410
+ } else if (rule.pattern) {
411
+ patternMatched = false;
412
+ }
413
+ return {
414
+ heading: rule.heading,
415
+ exists,
416
+ contentLength: realLength,
417
+ meetsMinLength,
418
+ patternMatched,
419
+ patternDescription: rule.patternDescription
420
+ };
421
+ });
422
+ const allPassed = sections.every(
423
+ (s) => s.exists && s.meetsMinLength && (s.patternMatched === null || s.patternMatched)
424
+ );
425
+ return {
426
+ path: docPath,
427
+ description: schema.description,
428
+ fileExists: true,
429
+ sections,
430
+ allPassed
431
+ };
432
+ }
433
+ function validateADRs(projectRoot) {
434
+ const adrDir = join2(projectRoot, "docs/architecture/ADR");
435
+ if (!existsSync2(adrDir)) {
436
+ return { count: 0, valid: 0, invalid: [] };
437
+ }
438
+ const files = readdirSync(adrDir).filter((f) => f.endsWith(".md") && f !== "ADR-000-template.md" && !f.startsWith("."));
439
+ const invalid = [];
440
+ let valid = 0;
441
+ for (const file of files) {
442
+ const result = validateDocument(projectRoot, SCHEMAS.adr, `docs/architecture/ADR/${file}`);
443
+ if (result.allPassed) {
444
+ valid++;
445
+ } else {
446
+ invalid.push(file);
447
+ }
448
+ }
449
+ return { count: files.length, valid, invalid };
450
+ }
451
+
452
+ // src/gates.ts
453
+ function checkFileExists(projectRoot, check) {
454
+ const fullPath = join3(projectRoot, check.path);
455
+ const exists = existsSync3(fullPath);
456
+ return {
457
+ checkId: check.id,
458
+ passed: exists,
459
+ message: exists ? `${check.path} exists` : `${check.path} not found`,
460
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
461
+ };
462
+ }
463
+ function checkFileSections(projectRoot, check) {
464
+ const fullPath = join3(projectRoot, check.path);
465
+ if (!existsSync3(fullPath)) {
466
+ return {
467
+ checkId: check.id,
468
+ passed: false,
469
+ message: `${check.path} not found \u2014 cannot check sections`,
470
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
471
+ };
472
+ }
473
+ const content = readFileSync3(fullPath, "utf-8");
474
+ const missing = (check.sections ?? []).filter((s) => !content.includes(s));
475
+ return {
476
+ checkId: check.id,
477
+ passed: missing.length === 0,
478
+ message: missing.length === 0 ? `All required sections present in ${check.path}` : `Missing sections in ${check.path}: ${missing.join(", ")}`,
479
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
480
+ };
481
+ }
482
+ function checkMinCount(projectRoot, check) {
483
+ const fullPath = join3(projectRoot, check.path);
484
+ if (!existsSync3(fullPath)) {
485
+ return {
486
+ checkId: check.id,
487
+ passed: false,
488
+ message: `Directory ${check.path} not found`,
489
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
490
+ };
491
+ }
492
+ const files = readdirSync2(fullPath).filter((f) => !f.startsWith("."));
493
+ const count = files.length;
494
+ const min = check.minCount ?? 1;
495
+ return {
496
+ checkId: check.id,
497
+ passed: count >= min,
498
+ message: count >= min ? `${check.path}: ${count} file(s) found (min: ${min})` : `${check.path}: ${count} file(s) found, need at least ${min}`,
499
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
500
+ };
501
+ }
502
+ function checkHumanApproval(state, roundId, check) {
503
+ const roundState = state.rounds[String(roundId)];
504
+ const existing = roundState?.gateResults.find((r) => r.checkId === check.id);
505
+ if (existing?.passed) {
506
+ return existing;
507
+ }
508
+ return {
509
+ checkId: check.id,
510
+ passed: false,
511
+ message: `Awaiting human approval. Run: bastard approve ${roundId}`,
512
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
513
+ };
514
+ }
515
+ function checkContentValidated(projectRoot, check, details) {
516
+ const schemaKey = check.schemaKey;
517
+ if (!schemaKey || !SCHEMAS[schemaKey]) {
518
+ return {
519
+ checkId: check.id,
520
+ passed: false,
521
+ message: `Schema "${schemaKey}" not found`,
522
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
523
+ };
524
+ }
525
+ const schema = SCHEMAS[schemaKey];
526
+ const validation = validateDocument(projectRoot, schema);
527
+ details.set(check.id, validation);
528
+ if (!validation.fileExists) {
529
+ return {
530
+ checkId: check.id,
531
+ passed: false,
532
+ message: `${schema.path} not found`,
533
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
534
+ };
535
+ }
536
+ if (validation.allPassed) {
537
+ return {
538
+ checkId: check.id,
539
+ passed: true,
540
+ message: `${schema.path} \u2014 all content validated`,
541
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
542
+ };
543
+ }
544
+ const failures = [];
545
+ for (const s of validation.sections) {
546
+ if (!s.exists) {
547
+ failures.push(`missing section "${s.heading}"`);
548
+ } else if (!s.meetsMinLength) {
549
+ failures.push(`"${s.heading}" too short (${s.contentLength}/${SCHEMAS[schemaKey].sections.find((r) => r.heading === s.heading)?.minContentLength ?? 0} chars)`);
550
+ } else if (s.patternMatched === false) {
551
+ failures.push(`"${s.heading}" missing pattern: ${s.patternDescription ?? "required format"}`);
552
+ }
553
+ }
554
+ return {
555
+ checkId: check.id,
556
+ passed: false,
557
+ message: `${schema.path} \u2014 ${failures.join("; ")}`,
558
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
559
+ };
560
+ }
561
+ function checkADRValidated(projectRoot, check, details) {
562
+ const result = validateADRs(projectRoot);
563
+ if (result.count === 0) {
564
+ return {
565
+ checkId: check.id,
566
+ passed: false,
567
+ message: "No ADRs found in docs/architecture/ADR/ (excluding template)",
568
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
569
+ };
570
+ }
571
+ if (result.invalid.length > 0) {
572
+ return {
573
+ checkId: check.id,
574
+ passed: false,
575
+ message: `${result.valid}/${result.count} ADRs valid \u2014 invalid: ${result.invalid.join(", ")}`,
576
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
577
+ };
578
+ }
579
+ return {
580
+ checkId: check.id,
581
+ passed: true,
582
+ message: `${result.count} ADR(s) validated (Status, Context, Decision, Consequences)`,
583
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
584
+ };
585
+ }
586
+ function runGate(projectRoot, state, roundId) {
587
+ const round = ROUNDS.find((r) => r.id === roundId);
588
+ if (!round) {
589
+ throw new Error(`Round ${roundId} not found`);
590
+ }
591
+ const details = /* @__PURE__ */ new Map();
592
+ const results = round.gateChecks.map((check) => {
593
+ switch (check.type) {
594
+ case "file_exists":
595
+ return checkFileExists(projectRoot, check);
596
+ case "file_has_sections":
597
+ return checkFileSections(projectRoot, check);
598
+ case "min_count":
599
+ return checkMinCount(projectRoot, check);
600
+ case "human_approval":
601
+ return checkHumanApproval(state, roundId, check);
602
+ case "content_validated":
603
+ return checkContentValidated(projectRoot, check, details);
604
+ case "adr_validated":
605
+ return checkADRValidated(projectRoot, check, details);
606
+ case "custom":
607
+ return {
608
+ checkId: check.id,
609
+ passed: false,
610
+ message: "Custom gate \u2014 not yet implemented",
611
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
612
+ };
613
+ }
614
+ });
615
+ const allPassed = results.every((r) => r.passed);
616
+ const roundState = state.rounds[String(roundId)];
617
+ if (roundState) {
618
+ roundState.gateResults = results;
619
+ roundState.gate = allPassed ? "passed" : "failed";
620
+ }
621
+ return { roundId, allPassed, results, details };
622
+ }
623
+
624
+ // src/templates.ts
625
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
626
+ import { join as join4, dirname } from "path";
627
+ function tpl(projectName) {
628
+ return [
629
+ {
630
+ path: "docs/product/PRD.md",
631
+ content: `# PRD \u2014 ${projectName}
632
+
633
+ > Generated by BASTARD. Fill every section before passing the Round 1 gate.
634
+
635
+ ## Problem
636
+
637
+ What problem are we solving? Who has this problem? How painful is it?
638
+
639
+ ## Users
640
+
641
+ Who are the target users? Be specific \u2014 "everyone" is not a user.
642
+
643
+ ### Primary user
644
+
645
+ ### Secondary user (if applicable)
646
+
647
+ ## User Stories
648
+
649
+ Format: As a [user type], I want [goal] so that [benefit].
650
+
651
+ ### Core stories (must-have)
652
+
653
+ 1.
654
+
655
+ ### Nice-to-have stories
656
+
657
+ 1.
658
+
659
+ ## Jobs to be Done
660
+
661
+ What job is the user hiring this product to do?
662
+
663
+ ## Competitive Landscape
664
+
665
+ What exists today? Why is it insufficient?
666
+
667
+ ## Solution Overview
668
+
669
+ High-level description of what we're building.
670
+
671
+ ## Success Metrics
672
+
673
+ How do we know this product is working?
674
+
675
+ | Metric | Target | How we measure |
676
+ |--------|--------|----------------|
677
+ | | | |
678
+
679
+ ## Scope
680
+
681
+ ### In scope (v1)
682
+
683
+ ### Out of scope (v1, future consideration)
684
+
685
+ ## Open Questions
686
+
687
+ 1.
688
+ `
689
+ },
690
+ {
691
+ path: "docs/product/PERSONAS.md",
692
+ content: `# Personas \u2014 ${projectName}
693
+
694
+ > Generated by BASTARD. Define at least 2 personas before passing the Round 1 gate.
695
+
696
+ ## Persona 1: [Name]
697
+
698
+ - **Role:**
699
+ - **Age range:**
700
+ - **Tech savviness:** Low / Medium / High
701
+ - **Goals:**
702
+ - **Pain points:**
703
+ - **Quote:** "[Something they would actually say]"
704
+
705
+ ### Typical day
706
+
707
+ ### How they discover us
708
+
709
+ ### What makes them stay
710
+
711
+ ### What makes them leave
712
+
713
+ ---
714
+
715
+ ## Persona 2: [Name]
716
+
717
+ - **Role:**
718
+ - **Age range:**
719
+ - **Tech savviness:** Low / Medium / High
720
+ - **Goals:**
721
+ - **Pain points:**
722
+ - **Quote:** "[Something they would actually say]"
723
+
724
+ ### Typical day
725
+
726
+ ### How they discover us
727
+
728
+ ### What makes them stay
729
+
730
+ ### What makes them leave
731
+ `
732
+ },
733
+ {
734
+ path: "docs/product/ACCEPTANCE_CRITERIA.md",
735
+ content: `# Acceptance Criteria \u2014 ${projectName}
736
+
737
+ > Generated by BASTARD. These criteria are verified in Round 8 by the acceptance-checker agent.
738
+ > A criterion is either GREEN (met) or RED (not met). No partial credit.
739
+
740
+ ## Format
741
+
742
+ \`\`\`
743
+ AC-XXX: [Short title]
744
+ Given [precondition]
745
+ When [action]
746
+ Then [expected result]
747
+ Priority: MUST | SHOULD | COULD
748
+ \`\`\`
749
+
750
+ ## Core Criteria
751
+
752
+ ### AC-001: [Title]
753
+ Given ...
754
+ When ...
755
+ Then ...
756
+ Priority: MUST
757
+
758
+ ### AC-002: [Title]
759
+ Given ...
760
+ When ...
761
+ Then ...
762
+ Priority: MUST
763
+
764
+ ## Secondary Criteria
765
+
766
+ ### AC-100: [Title]
767
+ Given ...
768
+ When ...
769
+ Then ...
770
+ Priority: SHOULD
771
+ `
772
+ },
773
+ {
774
+ path: "docs/design/DESIGN.md",
775
+ content: `# Design System \u2014 ${projectName}
776
+
777
+ > Generated by BASTARD. Define tokens before writing any CSS (Round 2 gate).
778
+
779
+ ## Brand Identity
780
+
781
+ ### Personality
782
+ -
783
+ ### Tone
784
+ -
785
+
786
+ ## Color
787
+
788
+ ### Primary palette
789
+
790
+ | Token | Value | Usage |
791
+ |-------|-------|-------|
792
+ | \`--color-primary\` | | Main actions, links |
793
+ | \`--color-primary-hover\` | | Hover states |
794
+ | \`--color-secondary\` | | Secondary actions |
795
+
796
+ ### Neutral palette
797
+
798
+ | Token | Value | Usage |
799
+ |-------|-------|-------|
800
+ | \`--color-bg\` | | Page background |
801
+ | \`--color-surface\` | | Card/panel background |
802
+ | \`--color-text\` | | Body text |
803
+ | \`--color-text-muted\` | | Secondary text |
804
+ | \`--color-border\` | | Borders, dividers |
805
+
806
+ ### Semantic palette
807
+
808
+ | Token | Value | Usage |
809
+ |-------|-------|-------|
810
+ | \`--color-success\` | | Success states |
811
+ | \`--color-warning\` | | Warning states |
812
+ | \`--color-error\` | | Error states |
813
+ | \`--color-info\` | | Info states |
814
+
815
+ ## Typography
816
+
817
+ ### Font families
818
+
819
+ | Token | Value | Usage |
820
+ |-------|-------|-------|
821
+ | \`--font-heading\` | | Headings |
822
+ | \`--font-body\` | | Body text |
823
+ | \`--font-mono\` | | Code, technical |
824
+
825
+ ### Type scale
826
+
827
+ | Token | Size | Line height | Weight | Usage |
828
+ |-------|------|-------------|--------|-------|
829
+ | \`--text-xs\` | | | | Captions |
830
+ | \`--text-sm\` | | | | Small text |
831
+ | \`--text-base\` | | | | Body |
832
+ | \`--text-lg\` | | | | Large body |
833
+ | \`--text-xl\` | | | | H4 |
834
+ | \`--text-2xl\` | | | | H3 |
835
+ | \`--text-3xl\` | | | | H2 |
836
+ | \`--text-4xl\` | | | | H1 |
837
+
838
+ ## Spacing
839
+
840
+ | Token | Value |
841
+ |-------|-------|
842
+ | \`--space-1\` | 4px |
843
+ | \`--space-2\` | 8px |
844
+ | \`--space-3\` | 12px |
845
+ | \`--space-4\` | 16px |
846
+ | \`--space-6\` | 24px |
847
+ | \`--space-8\` | 32px |
848
+ | \`--space-12\` | 48px |
849
+ | \`--space-16\` | 64px |
850
+
851
+ ## Border Radius
852
+
853
+ | Token | Value | Usage |
854
+ |-------|-------|-------|
855
+ | \`--radius-sm\` | | Buttons, inputs |
856
+ | \`--radius-md\` | | Cards |
857
+ | \`--radius-lg\` | | Modals, panels |
858
+ | \`--radius-full\` | 9999px | Avatars, pills |
859
+
860
+ ## Shadows
861
+
862
+ | Token | Value | Usage |
863
+ |-------|-------|-------|
864
+ | \`--shadow-sm\` | | Subtle elevation |
865
+ | \`--shadow-md\` | | Cards |
866
+ | \`--shadow-lg\` | | Modals, dropdowns |
867
+
868
+ ## Component Patterns
869
+
870
+ ### Buttons
871
+ ### Cards
872
+ ### Forms
873
+ ### Navigation
874
+ `
875
+ },
876
+ {
877
+ path: "docs/architecture/ARCHITECTURE.md",
878
+ content: `# Architecture \u2014 ${projectName}
879
+
880
+ > Generated by BASTARD. Document all major decisions here with corresponding ADRs (Round 3 gate).
881
+
882
+ ## System Overview
883
+
884
+ High-level architecture diagram (ASCII or link to diagram).
885
+
886
+ ## Stack
887
+
888
+ | Layer | Technology | Rationale |
889
+ |-------|-----------|-----------|
890
+ | Frontend | | |
891
+ | API | | |
892
+ | Database | | |
893
+ | Auth | | |
894
+ | Hosting | | |
895
+
896
+ ## Data Model
897
+
898
+ ### Core entities
899
+
900
+ ### Relationships
901
+
902
+ ## API Design
903
+
904
+ ### Endpoints overview
905
+
906
+ ### Authentication flow
907
+
908
+ ## Infrastructure
909
+
910
+ ### Deployment topology
911
+
912
+ ### Environments
913
+
914
+ | Env | Purpose | URL |
915
+ |-----|---------|-----|
916
+ | dev | | |
917
+ | staging | | |
918
+ | prod | | |
919
+
920
+ ## Security Architecture
921
+
922
+ ### Encryption at rest
923
+ ### Encryption in transit
924
+ ### Authentication
925
+ ### Authorization
926
+
927
+ ## ADRs
928
+
929
+ See \`docs/architecture/ADR/\` for all architecture decision records.
930
+ `
931
+ },
932
+ {
933
+ path: "docs/architecture/ADR/ADR-000-template.md",
934
+ content: `# ADR-000: Template
935
+
936
+ > Copy this template for each new architecture decision.
937
+
938
+ ## Status
939
+
940
+ Proposed
941
+
942
+ ## Date
943
+
944
+ YYYY-MM-DD
945
+
946
+ ## Context
947
+
948
+ Why is this decision necessary? What problem are we solving?
949
+ What constraints exist?
950
+
951
+ ## Options Considered
952
+
953
+ ### Option A: [Name]
954
+ - **Pros:** ...
955
+ - **Cons:** ...
956
+
957
+ ### Option B: [Name]
958
+ - **Pros:** ...
959
+ - **Cons:** ...
960
+
961
+ ## Decision
962
+
963
+ What we chose and why.
964
+
965
+ ## Consequences
966
+
967
+ ### Positive
968
+ -
969
+
970
+ ### Negative
971
+ -
972
+
973
+ ### Risks
974
+ -
975
+ `
976
+ },
977
+ {
978
+ path: "docs/security/THREAT_MODEL.md",
979
+ content: `# Threat Model \u2014 ${projectName}
980
+
981
+ > Generated by BASTARD. STRIDE analysis required before Round 6 gate.
982
+
983
+ ## Assets
984
+
985
+ What are we protecting?
986
+
987
+ | Asset | Sensitivity | Impact if compromised |
988
+ |-------|------------|----------------------|
989
+ | | | |
990
+
991
+ ## STRIDE Analysis
992
+
993
+ ### Spoofing
994
+
995
+ ### Tampering
996
+
997
+ ### Repudiation
998
+
999
+ ### Information Disclosure
1000
+
1001
+ ### Denial of Service
1002
+
1003
+ ### Elevation of Privilege
1004
+
1005
+ ## Attack Surface
1006
+
1007
+ ### External
1008
+ ### Internal
1009
+
1010
+ ## Mitigations
1011
+
1012
+ | Threat | Mitigation | Status |
1013
+ |--------|-----------|--------|
1014
+ | | | |
1015
+
1016
+ ## Risk Matrix
1017
+
1018
+ | Risk | Likelihood | Impact | DREAD Score | Priority |
1019
+ |------|-----------|--------|-------------|----------|
1020
+ | | | | | |
1021
+ `
1022
+ },
1023
+ {
1024
+ path: "docs/security/SECURITY_REVIEW.md",
1025
+ content: `# Security Review \u2014 ${projectName}
1026
+
1027
+ > Generated by BASTARD. Completed by the security-reviewer agent.
1028
+
1029
+ ## Review Date
1030
+
1031
+ YYYY-MM-DD
1032
+
1033
+ ## Scope
1034
+
1035
+ What was reviewed?
1036
+
1037
+ ## OWASP Top 10 Checklist
1038
+
1039
+ | # | Category | Status | Notes |
1040
+ |---|----------|--------|-------|
1041
+ | 1 | Injection | | |
1042
+ | 2 | Broken Authentication | | |
1043
+ | 3 | Sensitive Data Exposure | | |
1044
+ | 4 | XML External Entities | | |
1045
+ | 5 | Broken Access Control | | |
1046
+ | 6 | Security Misconfiguration | | |
1047
+ | 7 | XSS | | |
1048
+ | 8 | Insecure Deserialization | | |
1049
+ | 9 | Known Vulnerabilities | | |
1050
+ | 10 | Insufficient Logging | | |
1051
+
1052
+ ## Findings
1053
+
1054
+ ### Critical
1055
+
1056
+ ### High
1057
+
1058
+ ### Medium
1059
+
1060
+ ### Low
1061
+
1062
+ ## Recommendations
1063
+
1064
+ ## Sign-off
1065
+
1066
+ - [ ] 0 Critical findings
1067
+ - [ ] 0 High findings
1068
+ - [ ] All Medium findings have mitigation plan
1069
+ `
1070
+ },
1071
+ {
1072
+ path: "docs/planning/CONTEXT.md",
1073
+ content: `# Context \u2014 ${projectName}
1074
+
1075
+ > Generated by BASTARD. Filled during Round 4 to capture all ambiguities resolved.
1076
+
1077
+ ## Decisions Made
1078
+
1079
+ | # | Question | Decision | Rationale |
1080
+ |---|----------|----------|-----------|
1081
+ | 1 | | | |
1082
+
1083
+ ## Assumptions
1084
+
1085
+ | # | Assumption | Risk if wrong | Validation plan |
1086
+ |---|-----------|---------------|-----------------|
1087
+ | 1 | | | |
1088
+
1089
+ ## Dependencies
1090
+
1091
+ | # | Dependency | Owner | Status | Blocker? |
1092
+ |---|-----------|-------|--------|----------|
1093
+ | 1 | | | | |
1094
+
1095
+ ## Wave Plan
1096
+
1097
+ ### Wave 1 (Sequential \u2014 blocking)
1098
+
1099
+ ### Wave 2 (Parallel \u2014 independent)
1100
+
1101
+ ### Wave 3 (Parallel \u2014 independent)
1102
+ `
1103
+ }
1104
+ ];
1105
+ }
1106
+ function scaffoldTemplates(projectRoot, projectName) {
1107
+ const templates = tpl(projectName);
1108
+ const created = [];
1109
+ for (const template of templates) {
1110
+ const fullPath = join4(projectRoot, template.path);
1111
+ if (existsSync4(fullPath)) {
1112
+ continue;
1113
+ }
1114
+ const dir = dirname(fullPath);
1115
+ if (!existsSync4(dir)) {
1116
+ mkdirSync2(dir, { recursive: true });
1117
+ }
1118
+ writeFileSync2(fullPath, template.content);
1119
+ created.push(template.path);
1120
+ }
1121
+ return created;
1122
+ }
1123
+
1124
+ // src/guard.ts
1125
+ import { execSync } from "child_process";
1126
+ import { resolve } from "path";
1127
+ function isSourceCode(filePath) {
1128
+ const normalized = filePath.replace(/\\/g, "/");
1129
+ if (/\/(src|lib|app|components|pages|api|services|utils|hooks|stores)\//i.test(normalized)) return true;
1130
+ if (/\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|swift|kt)$/i.test(normalized) && !/\/docs\//.test(normalized)) return true;
1131
+ return false;
1132
+ }
1133
+ function isTestCode(filePath) {
1134
+ const normalized = filePath.replace(/\\/g, "/");
1135
+ if (/\/(tests?|__tests__|spec|e2e|cypress)\//i.test(normalized)) return true;
1136
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(normalized)) return true;
1137
+ return false;
1138
+ }
1139
+ function isStyling(filePath) {
1140
+ const normalized = filePath.replace(/\\/g, "/");
1141
+ return /\.(css|scss|sass|less|styl|styled\.(ts|js))$/i.test(normalized) || /tailwind\.config/i.test(normalized);
1142
+ }
1143
+ function isTaskFile(filePath) {
1144
+ const normalized = filePath.replace(/\\/g, "/");
1145
+ return /\/(tasks|\.planning)\//i.test(normalized);
1146
+ }
1147
+ function isSecuritySensitive(filePath) {
1148
+ const normalized = filePath.replace(/\\/g, "/");
1149
+ return /\/(auth|session|token|password|crypto|encrypt|payment|billing|checkout)\//i.test(normalized) || /\/(auth|session|token|password|crypto|encrypt|payment|billing|checkout)\.(ts|js|tsx|jsx)/i.test(normalized) || /middleware\.(ts|js)/i.test(normalized);
1150
+ }
1151
+ function evaluateGuard(projectRoot, tool) {
1152
+ if (!stateExists(projectRoot)) {
1153
+ return { allowed: true, reason: "No BASTARD project \u2014 guard inactive" };
1154
+ }
1155
+ const state = loadState(projectRoot);
1156
+ const round = state.currentRound;
1157
+ const toolName = tool.tool_name;
1158
+ const filePath = tool.tool_input.file_path ?? "";
1159
+ if (!["Write", "Edit", "NotebookEdit"].includes(toolName)) {
1160
+ return { allowed: true, reason: "Non-writing tool \u2014 allowed" };
1161
+ }
1162
+ const absPath = filePath.startsWith("/") ? filePath : resolve(projectRoot, filePath);
1163
+ const relPath = absPath.replace(projectRoot + "/", "");
1164
+ if (relPath.startsWith("docs/") || relPath.startsWith(".bastard/")) {
1165
+ return { allowed: true, reason: "Documentation/state write \u2014 always allowed" };
1166
+ }
1167
+ if (/^(CLAUDE|README|CONTRIBUTING|LICENSE|CHANGELOG)/i.test(relPath)) {
1168
+ return { allowed: true, reason: "Project root file \u2014 allowed" };
1169
+ }
1170
+ if (round < 5 && isSourceCode(relPath)) {
1171
+ return {
1172
+ allowed: false,
1173
+ reason: `BASTARD: No source code before Round 5 (currently Round ${round}). Complete Rounds 1-4 first: product vision \u2192 design \u2192 architecture \u2192 planning.`
1174
+ };
1175
+ }
1176
+ if (round < 5 && isTestCode(relPath)) {
1177
+ return {
1178
+ allowed: false,
1179
+ reason: `BASTARD: No test code before Round 5 (currently Round ${round}).`
1180
+ };
1181
+ }
1182
+ if (isStyling(relPath)) {
1183
+ const round2 = state.rounds["2"];
1184
+ if (!round2 || round2.gate !== "passed") {
1185
+ return {
1186
+ allowed: false,
1187
+ reason: `BASTARD: No CSS/styling before Round 2 gate passes. Approve the design system first: bastard approve 2`
1188
+ };
1189
+ }
1190
+ }
1191
+ if (round < 4 && isTaskFile(relPath)) {
1192
+ return {
1193
+ allowed: false,
1194
+ reason: `BASTARD: No task/planning files before Round 4 (currently Round ${round}).`
1195
+ };
1196
+ }
1197
+ if (isSecuritySensitive(relPath)) {
1198
+ const round6 = state.rounds["6"];
1199
+ if (round < 6 || !round6 || round6.gate !== "passed") {
1200
+ if (round > 5 && (!round6 || round6.gate !== "passed")) {
1201
+ return {
1202
+ allowed: false,
1203
+ reason: `BASTARD: Security-sensitive file detected. Complete Round 6 (security review) first.`
1204
+ };
1205
+ }
1206
+ }
1207
+ }
1208
+ return { allowed: true, reason: "Allowed by guard rules" };
1209
+ }
1210
+ function evaluateBashGuard(projectRoot, tool) {
1211
+ if (!stateExists(projectRoot)) {
1212
+ return { allowed: true, reason: "No BASTARD project \u2014 guard inactive" };
1213
+ }
1214
+ if (tool.tool_name !== "Bash") {
1215
+ return { allowed: true, reason: "Not a bash command" };
1216
+ }
1217
+ const command = tool.tool_input.command ?? "";
1218
+ const state = loadState(projectRoot);
1219
+ const round = state.currentRound;
1220
+ if (/git\s+add\s+(-A|\.)\s*$/.test(command)) {
1221
+ return {
1222
+ allowed: false,
1223
+ reason: "BASTARD: `git add .` and `git add -A` are forbidden. Stage files individually."
1224
+ };
1225
+ }
1226
+ if (/git\s+commit/.test(command) && !/-b\s/.test(command)) {
1227
+ try {
1228
+ const branch = execSync("git branch --show-current", { cwd: projectRoot, encoding: "utf-8" }).trim();
1229
+ if (branch === "main" || branch === "master") {
1230
+ return {
1231
+ allowed: false,
1232
+ reason: "BASTARD: No direct commits to main/master. Create a feature branch first."
1233
+ };
1234
+ }
1235
+ } catch {
1236
+ }
1237
+ }
1238
+ return { allowed: true, reason: "Bash command allowed" };
1239
+ }
1240
+ function guard(projectRoot, input) {
1241
+ let tool;
1242
+ try {
1243
+ tool = JSON.parse(input);
1244
+ } catch {
1245
+ return { allowed: true, reason: "Could not parse input \u2014 allowing" };
1246
+ }
1247
+ const writeResult = evaluateGuard(projectRoot, tool);
1248
+ if (!writeResult.allowed) return writeResult;
1249
+ const bashResult = evaluateBashGuard(projectRoot, tool);
1250
+ if (!bashResult.allowed) return bashResult;
1251
+ return { allowed: true, reason: "All guards passed" };
1252
+ }
1253
+ async function runGuardFromStdin(projectRoot) {
1254
+ const chunks = [];
1255
+ for await (const chunk of process.stdin) {
1256
+ chunks.push(chunk);
1257
+ }
1258
+ const input = Buffer.concat(chunks).toString("utf-8");
1259
+ const result = guard(projectRoot, input);
1260
+ if (!result.allowed) {
1261
+ process.stderr.write(result.reason + "\n");
1262
+ process.exit(2);
1263
+ }
1264
+ process.exit(0);
1265
+ }
1266
+
1267
+ // src/hooks.ts
1268
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1269
+ import { join as join5, resolve as resolve2 } from "path";
1270
+ function generateHooksConfig(projectRoot) {
1271
+ const bastardCli = resolve2(projectRoot, "node_modules/.bin/bastard");
1272
+ const fallbackCli = resolve2(projectRoot, "dist/cli.js");
1273
+ let cmd;
1274
+ if (existsSync5(bastardCli)) {
1275
+ cmd = bastardCli;
1276
+ } else if (existsSync5(fallbackCli)) {
1277
+ cmd = `node ${fallbackCli}`;
1278
+ } else {
1279
+ cmd = "npx bastard-framework";
1280
+ }
1281
+ return {
1282
+ hooks: {
1283
+ PreToolUse: [
1284
+ {
1285
+ matcher: "Write|Edit|NotebookEdit",
1286
+ command: `${cmd} guard`
1287
+ },
1288
+ {
1289
+ matcher: "Bash",
1290
+ command: `${cmd} guard`
1291
+ }
1292
+ ]
1293
+ }
1294
+ };
1295
+ }
1296
+ function installHooks(projectRoot) {
1297
+ const claudeDir = join5(projectRoot, ".claude");
1298
+ const settingsPath = join5(claudeDir, "settings.local.json");
1299
+ if (!existsSync5(claudeDir)) {
1300
+ mkdirSync3(claudeDir, { recursive: true });
1301
+ }
1302
+ const config = generateHooksConfig(projectRoot);
1303
+ let merged = false;
1304
+ if (existsSync5(settingsPath)) {
1305
+ try {
1306
+ const existing = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1307
+ if (!existing.hooks) existing.hooks = {};
1308
+ if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
1309
+ existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(
1310
+ (h) => !h.command.includes("bastard")
1311
+ );
1312
+ existing.hooks.PreToolUse.push(...config.hooks.PreToolUse);
1313
+ writeFileSync3(settingsPath, JSON.stringify(existing, null, 2) + "\n");
1314
+ merged = true;
1315
+ return { created: false, path: settingsPath, merged: true };
1316
+ } catch {
1317
+ }
1318
+ }
1319
+ writeFileSync3(settingsPath, JSON.stringify(config, null, 2) + "\n");
1320
+ return { created: true, path: settingsPath, merged: false };
1321
+ }
1322
+ function removeHooks(projectRoot) {
1323
+ const settingsPath = join5(projectRoot, ".claude", "settings.local.json");
1324
+ if (!existsSync5(settingsPath)) return false;
1325
+ try {
1326
+ const existing = JSON.parse(readFileSync4(settingsPath, "utf-8"));
1327
+ if (existing.hooks?.PreToolUse) {
1328
+ existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(
1329
+ (h) => !h.command.includes("bastard")
1330
+ );
1331
+ if (existing.hooks.PreToolUse.length === 0) {
1332
+ delete existing.hooks.PreToolUse;
1333
+ }
1334
+ if (Object.keys(existing.hooks).length === 0) {
1335
+ delete existing.hooks;
1336
+ }
1337
+ }
1338
+ writeFileSync3(settingsPath, JSON.stringify(existing, null, 2) + "\n");
1339
+ return true;
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+
1345
+ // src/parents.ts
1346
+ import { existsSync as existsSync6 } from "fs";
1347
+ import { join as join6 } from "path";
1348
+ import { execSync as execSync2 } from "child_process";
1349
+ var PARENTS = [
1350
+ {
1351
+ id: "bmad",
1352
+ name: "BMAD Method",
1353
+ role: "Vision produit, PRD, Architecture",
1354
+ rounds: [1, 3],
1355
+ installCmd: "npx bmad-method install",
1356
+ detectors: [
1357
+ { type: "dir", value: ".bmad" },
1358
+ { type: "npm", value: "bmad-method" },
1359
+ { type: "file", value: ".claude/agents/bmad-analyst.md" },
1360
+ { type: "file", value: ".claude/agents/bmad-pm.md" }
1361
+ ]
1362
+ },
1363
+ {
1364
+ id: "gsd",
1365
+ name: "Get Shit Done",
1366
+ role: "Context engineering, wave execution",
1367
+ rounds: [4, 5, 8],
1368
+ installCmd: "npx gsd@latest install",
1369
+ detectors: [
1370
+ { type: "dir", value: ".gsd" },
1371
+ { type: "npm", value: "gsd" },
1372
+ { type: "file", value: ".gsd/config.json" }
1373
+ ]
1374
+ },
1375
+ {
1376
+ id: "gstack",
1377
+ name: "gstack",
1378
+ role: "Design system, QA browser, livraison",
1379
+ rounds: [2, 7, 8],
1380
+ installCmd: "git clone https://github.com/garrytan/gstack .gstack && cd .gstack && ./install.sh",
1381
+ detectors: [
1382
+ { type: "dir", value: ".gstack" },
1383
+ { type: "file", value: ".claude/skills/design-consultation.md" },
1384
+ { type: "file", value: ".claude/skills/qa.md" }
1385
+ ]
1386
+ },
1387
+ {
1388
+ id: "super",
1389
+ name: "SuperClaude",
1390
+ role: "Cognitive personas, token efficiency",
1391
+ rounds: [3, 5, 6],
1392
+ installCmd: "git clone https://github.com/superclaude/superclaude .superclaude && cd .superclaude && ./install.sh",
1393
+ detectors: [
1394
+ { type: "dir", value: ".superclaude" },
1395
+ { type: "file", value: ".claude/personas" }
1396
+ ]
1397
+ },
1398
+ {
1399
+ id: "tob",
1400
+ name: "Trail of Bits",
1401
+ role: "Security review, threat modeling",
1402
+ rounds: [6],
1403
+ installCmd: "npx skills add trailofbits/skills",
1404
+ detectors: [
1405
+ { type: "file", value: ".claude/skills/security-review.md" },
1406
+ { type: "file", value: ".claude/skills/threat-modeling.md" }
1407
+ ]
1408
+ },
1409
+ {
1410
+ id: "turbo",
1411
+ name: "TurboDocx FD",
1412
+ role: "Frontend design anti-AI-slop",
1413
+ rounds: [2],
1414
+ installCmd: "npx skills add turbodocx/frontend-design",
1415
+ detectors: [
1416
+ { type: "file", value: ".claude/skills/frontend-design.md" }
1417
+ ]
1418
+ },
1419
+ {
1420
+ id: "taskm",
1421
+ name: "TaskMaster",
1422
+ role: "Task decomposition, dependency graph",
1423
+ rounds: [4],
1424
+ installCmd: "npx task-master-ai@latest init",
1425
+ detectors: [
1426
+ { type: "npm", value: "task-master-ai" },
1427
+ { type: "command", value: "task-master" },
1428
+ { type: "file", value: "tasks/tasks.json" }
1429
+ ]
1430
+ }
1431
+ ];
1432
+ function detect(projectRoot, detector) {
1433
+ switch (detector.type) {
1434
+ case "dir":
1435
+ return existsSync6(join6(projectRoot, detector.value));
1436
+ case "file":
1437
+ return existsSync6(join6(projectRoot, detector.value));
1438
+ case "npm":
1439
+ return existsSync6(join6(projectRoot, "node_modules", detector.value));
1440
+ case "command":
1441
+ try {
1442
+ execSync2(`which ${detector.value}`, { stdio: "ignore" });
1443
+ return true;
1444
+ } catch {
1445
+ return false;
1446
+ }
1447
+ case "glob":
1448
+ return existsSync6(join6(projectRoot, detector.value));
1449
+ }
1450
+ }
1451
+ function detectParents(projectRoot) {
1452
+ return PARENTS.map((framework) => {
1453
+ for (const detector of framework.detectors) {
1454
+ if (detect(projectRoot, detector)) {
1455
+ return {
1456
+ framework,
1457
+ installed: true,
1458
+ detectedVia: `${detector.type}: ${detector.value}`
1459
+ };
1460
+ }
1461
+ }
1462
+ return {
1463
+ framework,
1464
+ installed: false,
1465
+ detectedVia: null
1466
+ };
1467
+ });
1468
+ }
1469
+
1470
+ // src/installer.ts
1471
+ import { execSync as execSync3, spawn } from "child_process";
1472
+ import { existsSync as existsSync7 } from "fs";
1473
+ import { join as join7 } from "path";
1474
+ function getInstallSpecs(projectRoot) {
1475
+ return [
1476
+ {
1477
+ framework: PARENTS.find((p) => p.id === "bmad"),
1478
+ method: "npm",
1479
+ command: "npx bmad-method@latest install"
1480
+ },
1481
+ {
1482
+ framework: PARENTS.find((p) => p.id === "gsd"),
1483
+ method: "npm",
1484
+ command: "npx gsd@latest install"
1485
+ },
1486
+ {
1487
+ framework: PARENTS.find((p) => p.id === "gstack"),
1488
+ method: "git",
1489
+ command: "git clone --depth 1 https://github.com/garrytan/gstack.git .gstack"
1490
+ },
1491
+ {
1492
+ framework: PARENTS.find((p) => p.id === "super"),
1493
+ method: "git",
1494
+ command: "git clone --depth 1 https://github.com/superclaude/superclaude.git .superclaude"
1495
+ },
1496
+ {
1497
+ framework: PARENTS.find((p) => p.id === "tob"),
1498
+ method: "skill",
1499
+ command: "npx @anthropic-ai/claude-code-skills@latest add trailofbits/skills"
1500
+ },
1501
+ {
1502
+ framework: PARENTS.find((p) => p.id === "turbo"),
1503
+ method: "skill",
1504
+ command: "npx @anthropic-ai/claude-code-skills@latest add turbodocx/frontend-design"
1505
+ },
1506
+ {
1507
+ framework: PARENTS.find((p) => p.id === "taskm"),
1508
+ method: "npm",
1509
+ command: "npx task-master-ai@latest init"
1510
+ }
1511
+ ];
1512
+ }
1513
+ function runCommand(command, cwd) {
1514
+ return new Promise((resolve4) => {
1515
+ const parts = command.split(" ");
1516
+ const proc = spawn(parts[0], parts.slice(1), {
1517
+ cwd,
1518
+ stdio: ["ignore", "pipe", "pipe"],
1519
+ shell: true,
1520
+ env: { ...process.env, FORCE_COLOR: "0" }
1521
+ });
1522
+ let output = "";
1523
+ proc.stdout?.on("data", (d) => {
1524
+ output += d.toString();
1525
+ });
1526
+ proc.stderr?.on("data", (d) => {
1527
+ output += d.toString();
1528
+ });
1529
+ proc.on("close", (code) => {
1530
+ resolve4({ ok: code === 0, output });
1531
+ });
1532
+ proc.on("error", (err) => {
1533
+ resolve4({ ok: false, output: err.message });
1534
+ });
1535
+ setTimeout(() => {
1536
+ proc.kill("SIGTERM");
1537
+ resolve4({ ok: false, output: "Install timed out after 120s" });
1538
+ }, 12e4);
1539
+ });
1540
+ }
1541
+ async function installOne(spec, projectRoot, force, status) {
1542
+ const start = Date.now();
1543
+ const fw = spec.framework;
1544
+ if (status.installed && !force) {
1545
+ return {
1546
+ frameworkId: fw.id,
1547
+ name: fw.name,
1548
+ success: true,
1549
+ alreadyInstalled: true,
1550
+ updated: false,
1551
+ duration: 0
1552
+ };
1553
+ }
1554
+ if (spec.method === "git" && status.installed && force) {
1555
+ const repoDir = spec.command.split(" ").pop();
1556
+ const pullCmd = `git -C ${repoDir} pull --ff-only`;
1557
+ const result2 = await runCommand(pullCmd, projectRoot);
1558
+ return {
1559
+ frameworkId: fw.id,
1560
+ name: fw.name,
1561
+ success: result2.ok,
1562
+ alreadyInstalled: false,
1563
+ updated: true,
1564
+ error: result2.ok ? void 0 : result2.output.slice(0, 200),
1565
+ duration: Date.now() - start
1566
+ };
1567
+ }
1568
+ if (spec.method === "git" && force) {
1569
+ const repoDir = join7(projectRoot, spec.command.split(" ").pop());
1570
+ if (existsSync7(repoDir)) {
1571
+ execSync3(`rm -rf ${repoDir}`, { cwd: projectRoot });
1572
+ }
1573
+ }
1574
+ const result = await runCommand(spec.command, projectRoot);
1575
+ return {
1576
+ frameworkId: fw.id,
1577
+ name: fw.name,
1578
+ success: result.ok,
1579
+ alreadyInstalled: false,
1580
+ updated: false,
1581
+ error: result.ok ? void 0 : result.output.slice(0, 200),
1582
+ duration: Date.now() - start
1583
+ };
1584
+ }
1585
+ async function installParents(projectRoot, opts = {}, onProgress) {
1586
+ const specs = getInstallSpecs(projectRoot);
1587
+ const statuses = detectParents(projectRoot);
1588
+ const statusMap = new Map(statuses.map((s) => [s.framework.id, s]));
1589
+ let filtered = specs;
1590
+ if (opts.only && opts.only.length > 0) {
1591
+ filtered = specs.filter((s) => opts.only.includes(s.framework.id));
1592
+ }
1593
+ if (opts.skip && opts.skip.length > 0) {
1594
+ filtered = filtered.filter((s) => !opts.skip.includes(s.framework.id));
1595
+ }
1596
+ const results = [];
1597
+ for (let i = 0; i < filtered.length; i++) {
1598
+ const spec = filtered[i];
1599
+ const status = statusMap.get(spec.framework.id);
1600
+ const result = await installOne(spec, projectRoot, opts.force ?? false, status);
1601
+ results.push(result);
1602
+ onProgress?.(result, i + 1, filtered.length);
1603
+ }
1604
+ return results;
1605
+ }
1606
+
1607
+ // src/orchestrator.ts
1608
+ var WORKFLOWS = [
1609
+ {
1610
+ roundId: 1,
1611
+ name: "Vision & Product",
1612
+ steps: [
1613
+ {
1614
+ id: "r1-analysis",
1615
+ framework: "bmad",
1616
+ action: '/bmad-analyst "Describe your product idea"',
1617
+ description: "Product analysis \u2014 personas, pain points, jobs-to-be-done",
1618
+ output: "Initial brief",
1619
+ fallbackAction: "Manually write the product brief focusing on: target users, their pain points, and what job the product does for them."
1620
+ },
1621
+ {
1622
+ id: "r1-prd",
1623
+ framework: "bmad",
1624
+ action: "/bmad-pm",
1625
+ description: "Generate structured PRD with user stories and acceptance criteria",
1626
+ output: "docs/product/PRD.md",
1627
+ fallbackAction: "Fill docs/product/PRD.md template \u2014 every section must have real content (not just placeholders)."
1628
+ },
1629
+ {
1630
+ id: "r1-personas",
1631
+ framework: "bmad",
1632
+ action: "Fill docs/product/PERSONAS.md",
1633
+ description: "Define at least 2 detailed user personas",
1634
+ output: "docs/product/PERSONAS.md",
1635
+ isManual: true
1636
+ },
1637
+ {
1638
+ id: "r1-acceptance",
1639
+ framework: "bmad",
1640
+ action: "Fill docs/product/ACCEPTANCE_CRITERIA.md",
1641
+ description: "Write acceptance criteria in AC-XXX Given/When/Then format",
1642
+ output: "docs/product/ACCEPTANCE_CRITERIA.md",
1643
+ isManual: true
1644
+ }
1645
+ ],
1646
+ contextAdvice: "Round 1 is deep thinking. Use Opus 4.6 for best results.",
1647
+ completionSteps: [
1648
+ "bastard validate prd",
1649
+ "bastard gate",
1650
+ "bastard approve 1",
1651
+ "bastard next"
1652
+ ]
1653
+ },
1654
+ {
1655
+ roundId: 2,
1656
+ name: "Design System & UX",
1657
+ steps: [
1658
+ {
1659
+ id: "r2-consultation",
1660
+ framework: "gstack",
1661
+ action: "/design-consultation",
1662
+ description: "Design system from scratch \u2014 brand identity, tokens, scale",
1663
+ output: "docs/design/DESIGN.md",
1664
+ fallbackAction: "Fill docs/design/DESIGN.md with concrete values for every token (colors, typography, spacing). No empty cells."
1665
+ },
1666
+ {
1667
+ id: "r2-shotgun",
1668
+ framework: "gstack",
1669
+ action: "/design-shotgun",
1670
+ description: "3 visual variants \u2192 interactive comparison board",
1671
+ output: "docs/design/mockups/",
1672
+ fallbackAction: "Create 3 distinct visual directions (mood boards or quick mockups). Choose one."
1673
+ },
1674
+ {
1675
+ id: "r2-antiSlop",
1676
+ framework: "turbo",
1677
+ action: "/frontend-design with 4 dimensions: purpose, tone, constraints, differentiation",
1678
+ description: "Anti-AI-slop design review \u2014 kills generic layouts",
1679
+ fallbackAction: "Self-review DESIGN.md against the AI Slop Blacklist: no 3-column icon grids, no blue-purple gradients, no single font, no stock photo heroes."
1680
+ }
1681
+ ],
1682
+ contextAdvice: "/clear before starting Round 2 (different creative mode than Round 1).",
1683
+ completionSteps: [
1684
+ "bastard validate design",
1685
+ "bastard gate",
1686
+ "bastard approve 2",
1687
+ "bastard next"
1688
+ ]
1689
+ },
1690
+ {
1691
+ roundId: 3,
1692
+ name: "Architecture",
1693
+ steps: [
1694
+ {
1695
+ id: "r3-architect",
1696
+ framework: "bmad",
1697
+ action: "/bmad-architect",
1698
+ description: "Architecture decisions \u2014 stack, infra schema, ADRs",
1699
+ output: "docs/architecture/ARCHITECTURE.md",
1700
+ fallbackAction: "Fill ARCHITECTURE.md with stack choices, data model, API design, infra topology."
1701
+ },
1702
+ {
1703
+ id: "r3-tradeoffs",
1704
+ framework: "super",
1705
+ action: "--persona-architect to analyze trade-offs",
1706
+ description: "Detect over-engineering, YAGNI violations, complexity budget",
1707
+ fallbackAction: "Review each architectural decision: Is this the simplest thing that works? Would I regret this in 6 months?"
1708
+ },
1709
+ {
1710
+ id: "r3-adrs",
1711
+ framework: "bmad",
1712
+ action: "Write ADRs for each major decision",
1713
+ description: "One ADR per major decision \u2014 Status, Context, Options, Decision, Consequences",
1714
+ output: "docs/architecture/ADR/ADR-001.md, ...",
1715
+ isManual: true
1716
+ }
1717
+ ],
1718
+ contextAdvice: "Use Opus 4.6 for architecture decisions. This round defines everything downstream.",
1719
+ completionSteps: [
1720
+ "bastard validate architecture",
1721
+ "bastard gate",
1722
+ "bastard approve 3",
1723
+ "bastard next"
1724
+ ]
1725
+ },
1726
+ {
1727
+ roundId: 4,
1728
+ name: "Decomposition & Planification",
1729
+ steps: [
1730
+ {
1731
+ id: "r4-parse",
1732
+ framework: "taskm",
1733
+ action: "task-master parse-prd --input docs/product/PRD.md",
1734
+ description: "Parse PRD + architecture into weighted task graph",
1735
+ output: "tasks/",
1736
+ fallbackAction: "Manually break the PRD into tasks. One task = one responsibility = one commit. No task > 2h."
1737
+ },
1738
+ {
1739
+ id: "r4-analyze",
1740
+ framework: "taskm",
1741
+ action: "task-master analyze",
1742
+ description: "Complexity scores, dependencies, execution order",
1743
+ fallbackAction: "For each task: estimate complexity (1-5), list dependencies, assign to wave (blocking=Wave1, independent=Wave2+)."
1744
+ },
1745
+ {
1746
+ id: "r4-discuss",
1747
+ framework: "gsd",
1748
+ action: "/gsd:discuss-phase",
1749
+ description: "Kill all ambiguities before writing a single line of code",
1750
+ output: "docs/planning/CONTEXT.md",
1751
+ fallbackAction: "Fill docs/planning/CONTEXT.md with every decision made, assumption identified, and dependency tracked."
1752
+ },
1753
+ {
1754
+ id: "r4-plan",
1755
+ framework: "gsd",
1756
+ action: "/gsd:plan-phase",
1757
+ description: "Detailed execution plans organized by wave",
1758
+ output: ".planning/phases/",
1759
+ fallbackAction: "Create wave-based plan: Wave 1 (sequential blockers), Wave 2+ (parallelizable independent tasks)."
1760
+ }
1761
+ ],
1762
+ contextAdvice: "Sonnet 4.6 is fast enough for decomposition. Save Opus for the thinking rounds.",
1763
+ completionSteps: [
1764
+ "bastard gate",
1765
+ "bastard next"
1766
+ ]
1767
+ },
1768
+ {
1769
+ roundId: 5,
1770
+ name: "Execution du Code",
1771
+ steps: [
1772
+ {
1773
+ id: "r5-execute",
1774
+ framework: "gsd",
1775
+ action: "/gsd:execute-phase",
1776
+ description: "Wave-based execution with sub-contexts and atomic commits",
1777
+ output: "src/, tests/",
1778
+ fallbackAction: "Execute tasks wave by wave. Each completed task = immediate commit with conventional message."
1779
+ },
1780
+ {
1781
+ id: "r5-backend",
1782
+ framework: "super",
1783
+ action: "--persona-backend for APIs and business logic",
1784
+ description: "Switch to backend persona for service layer code",
1785
+ fallbackAction: "Focus: APIs, services, business logic. TypeScript strict, TDD, 80% coverage minimum."
1786
+ },
1787
+ {
1788
+ id: "r5-frontend",
1789
+ framework: "super",
1790
+ action: "--persona-frontend for UI components",
1791
+ description: "Switch to frontend persona for UI layer",
1792
+ fallbackAction: "Focus: components, state management, routing. Use design tokens from DESIGN.md."
1793
+ }
1794
+ ],
1795
+ contextAdvice: "/compact between phases. Context must stay under 40%. Use sub-agents for read-heavy tasks.",
1796
+ completionSteps: [
1797
+ "bastard gate",
1798
+ "bastard next"
1799
+ ]
1800
+ },
1801
+ {
1802
+ roundId: 6,
1803
+ name: "Securite",
1804
+ steps: [
1805
+ {
1806
+ id: "r6-threat",
1807
+ framework: "tob",
1808
+ action: "/threat-modeling",
1809
+ description: "STRIDE methodology \u2014 identify threats, DREAD/CVSS scoring",
1810
+ output: "docs/security/THREAT_MODEL.md",
1811
+ fallbackAction: "Fill THREAT_MODEL.md using STRIDE: Spoofing, Tampering, Repudiation, Info Disclosure, DoS, Elevation of Privilege."
1812
+ },
1813
+ {
1814
+ id: "r6-review",
1815
+ framework: "tob",
1816
+ action: "/security-review",
1817
+ description: "OWASP Top 10 code review, auth/crypto/input validation",
1818
+ output: "docs/security/SECURITY_REVIEW.md",
1819
+ fallbackAction: "Run through OWASP Top 10 checklist against codebase. Document findings with severity."
1820
+ },
1821
+ {
1822
+ id: "r6-persona",
1823
+ framework: "super",
1824
+ action: "--persona-security on auth/sessions/tokens code",
1825
+ description: "Deep review of security-sensitive code paths",
1826
+ fallbackAction: "Manual review: parameterized queries? Input validation? Secrets in code? Auth != authz separation?"
1827
+ }
1828
+ ],
1829
+ contextAdvice: "Use Opus 4.6 for security review. Missing a vulnerability here is catastrophic.",
1830
+ completionSteps: [
1831
+ "bastard validate threat_model",
1832
+ "bastard validate security_review",
1833
+ "bastard gate",
1834
+ "bastard next"
1835
+ ]
1836
+ },
1837
+ {
1838
+ roundId: 7,
1839
+ name: "QA & Tests",
1840
+ steps: [
1841
+ {
1842
+ id: "r7-qa",
1843
+ framework: "gstack",
1844
+ action: "/qa",
1845
+ description: "Analyze diff, test affected routes, generate regression tests",
1846
+ fallbackAction: "Run full test suite. Add regression tests for any fixed bugs. Check E2E flows."
1847
+ },
1848
+ {
1849
+ id: "r7-designReview",
1850
+ framework: "gstack",
1851
+ action: "/design-review [URL]",
1852
+ description: "80-point visual audit \u2014 Design Score + AI Slop Score",
1853
+ fallbackAction: "Manual audit: check every page against DESIGN.md tokens. Score: A-F for design quality and AI slop."
1854
+ }
1855
+ ],
1856
+ contextAdvice: "Sonnet 4.6 for QA. Fast iteration on test fixes.",
1857
+ completionSteps: [
1858
+ "bastard gate",
1859
+ "bastard approve 7",
1860
+ "bastard next"
1861
+ ]
1862
+ },
1863
+ {
1864
+ roundId: 8,
1865
+ name: "Review & Livraison",
1866
+ steps: [
1867
+ {
1868
+ id: "r8-engReview",
1869
+ framework: "gstack",
1870
+ action: "/plan-eng-review",
1871
+ description: "Architecture review \u2014 data flow, edge cases, failure modes",
1872
+ fallbackAction: "Review: data flows correct? Edge cases handled? Failure modes graceful?"
1873
+ },
1874
+ {
1875
+ id: "r8-review",
1876
+ framework: "gstack",
1877
+ action: "/review",
1878
+ description: "Adversarial review \u2014 two independent AI perspectives",
1879
+ fallbackAction: "Get a second opinion on the code. Review with fresh eyes."
1880
+ },
1881
+ {
1882
+ id: "r8-ship",
1883
+ framework: "gstack",
1884
+ action: "/ship",
1885
+ description: "Scope drift detection, sync main, full test suite, PR",
1886
+ fallbackAction: "Check: did we build what the PRD asked for? Run full tests. Create PR."
1887
+ },
1888
+ {
1889
+ id: "r8-verify",
1890
+ framework: "gsd",
1891
+ action: "/gsd:verify",
1892
+ description: "Verify against ACCEPTANCE_CRITERIA.md from Round 1",
1893
+ output: "Acceptance verification report",
1894
+ fallbackAction: "Go through ACCEPTANCE_CRITERIA.md line by line. Each AC-XXX must be demonstrably met in code + tests."
1895
+ }
1896
+ ],
1897
+ contextAdvice: "/clear before Round 8. Fresh context for final review.",
1898
+ completionSteps: [
1899
+ "bastard gate",
1900
+ "bastard approve 8",
1901
+ "bastard next"
1902
+ ]
1903
+ }
1904
+ ];
1905
+ function getWorkflow(roundId) {
1906
+ return WORKFLOWS.find((w) => w.roundId === roundId);
1907
+ }
1908
+ function generatePrompt(workflow, installedFrameworks) {
1909
+ const lines = [];
1910
+ lines.push(`# Round ${workflow.roundId} \u2014 ${workflow.name}`);
1911
+ lines.push("");
1912
+ lines.push(`> ${workflow.contextAdvice}`);
1913
+ lines.push("");
1914
+ lines.push("## Workflow");
1915
+ lines.push("");
1916
+ for (let i = 0; i < workflow.steps.length; i++) {
1917
+ const step = workflow.steps[i];
1918
+ const num = i + 1;
1919
+ const installed = installedFrameworks.has(step.framework);
1920
+ lines.push(`### Step ${num}: ${step.description}`);
1921
+ lines.push("");
1922
+ if (installed) {
1923
+ lines.push(`**Command:** \`${step.action}\``);
1924
+ } else if (step.fallbackAction) {
1925
+ lines.push(`**Action (${step.framework} not installed):** ${step.fallbackAction}`);
1926
+ }
1927
+ if (step.output) {
1928
+ lines.push(`**Expected output:** \`${step.output}\``);
1929
+ }
1930
+ lines.push("");
1931
+ }
1932
+ lines.push("## After completion");
1933
+ lines.push("");
1934
+ lines.push("Run these commands in order:");
1935
+ lines.push("");
1936
+ for (const cmd of workflow.completionSteps) {
1937
+ lines.push(`\`\`\`bash`);
1938
+ lines.push(cmd);
1939
+ lines.push(`\`\`\``);
1940
+ }
1941
+ return lines.join("\n");
1942
+ }
1943
+
1944
+ // src/slop.ts
1945
+ import { readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, existsSync as existsSync8 } from "fs";
1946
+ import { join as join8, extname } from "path";
1947
+ function findLines(content, pattern) {
1948
+ const results = [];
1949
+ const lines = content.split("\n");
1950
+ for (let i = 0; i < lines.length; i++) {
1951
+ if (pattern.test(lines[i])) {
1952
+ results.push({ line: i + 1, excerpt: lines[i].trim().slice(0, 120) });
1953
+ }
1954
+ }
1955
+ return results;
1956
+ }
1957
+ function findMultilinePattern(content, pattern) {
1958
+ const results = [];
1959
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g")));
1960
+ for (const match of matches) {
1961
+ if (match.index !== void 0) {
1962
+ const beforeMatch = content.slice(0, match.index);
1963
+ const line = beforeMatch.split("\n").length;
1964
+ results.push({ line, excerpt: match[0].replace(/\n/g, " ").trim().slice(0, 120) });
1965
+ }
1966
+ }
1967
+ return results;
1968
+ }
1969
+ var CSS_EXTENSIONS = [".css", ".scss", ".sass", ".less"];
1970
+ var JSX_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
1971
+ var ALL_UI = [...CSS_EXTENSIONS, ...JSX_EXTENSIONS];
1972
+ var PATTERNS = [
1973
+ // ── CSS Patterns ──────────────────────────────────────────────────────────
1974
+ {
1975
+ id: "blue-purple-gradient",
1976
+ name: "Blue-Purple Gradient",
1977
+ description: "The #1 tell of AI-generated UI. Every AI loves this gradient.",
1978
+ severity: "major",
1979
+ fileTypes: ALL_UI,
1980
+ detect(content, filePath) {
1981
+ const pattern = /linear-gradient\s*\([^)]*(?:blue|purple|violet|indigo|#6366f1|#8b5cf6|#7c3aed|#4f46e5|#818cf8|#a78bfa|#6c63ff|#9333ea|from-(?:blue|purple|violet|indigo))/gi;
1982
+ return findLines(content, pattern).map((m) => ({
1983
+ patternId: this.id,
1984
+ patternName: this.name,
1985
+ severity: this.severity,
1986
+ file: filePath,
1987
+ ...m
1988
+ }));
1989
+ }
1990
+ },
1991
+ {
1992
+ id: "gradient-text",
1993
+ name: "Gradient Text",
1994
+ description: "background-clip: text with gradients. Readability nightmare, AI favorite.",
1995
+ severity: "minor",
1996
+ fileTypes: ALL_UI,
1997
+ detect(content, filePath) {
1998
+ const pattern = /(?:background-clip:\s*text|-webkit-background-clip:\s*text|bg-clip-text|bg-gradient-to-.*text-transparent)/gi;
1999
+ return findLines(content, pattern).map((m) => ({
2000
+ patternId: this.id,
2001
+ patternName: this.name,
2002
+ severity: this.severity,
2003
+ file: filePath,
2004
+ ...m
2005
+ }));
2006
+ }
2007
+ },
2008
+ {
2009
+ id: "hero-oversized",
2010
+ name: "Oversized Hero Section",
2011
+ description: "Hero > 80vh with overlay. Stock photo territory.",
2012
+ severity: "minor",
2013
+ fileTypes: ALL_UI,
2014
+ detect(content, filePath) {
2015
+ const pattern = /(?:min-h-screen|h-screen|height:\s*100vh|min-height:\s*(?:9[0-9]|100)vh)/gi;
2016
+ const heroContext = /(?:hero|banner|landing|jumbotron)/i;
2017
+ const results = findLines(content, pattern);
2018
+ return results.filter((r) => {
2019
+ const lines = content.split("\n");
2020
+ const start = Math.max(0, r.line - 5);
2021
+ const end = Math.min(lines.length, r.line + 5);
2022
+ const context = lines.slice(start, end).join("\n");
2023
+ return heroContext.test(context);
2024
+ }).map((m) => ({
2025
+ patternId: this.id,
2026
+ patternName: this.name,
2027
+ severity: this.severity,
2028
+ file: filePath,
2029
+ ...m
2030
+ }));
2031
+ }
2032
+ },
2033
+ {
2034
+ id: "generic-card-grid",
2035
+ name: "Generic Card Grid",
2036
+ description: "Rounded cards with shadows in a 3-column grid. The AI default layout.",
2037
+ severity: "major",
2038
+ fileTypes: ALL_UI,
2039
+ detect(content, filePath) {
2040
+ const gridPattern = /(?:grid-cols-3|grid-template-columns:\s*repeat\(3)/gi;
2041
+ const roundedShadow = /(?:rounded-(?:lg|xl|2xl).*shadow|shadow.*rounded-(?:lg|xl|2xl)|border-radius.*box-shadow|box-shadow.*border-radius)/gi;
2042
+ const gridMatches = findLines(content, gridPattern);
2043
+ const shadowMatches = findLines(content, roundedShadow);
2044
+ if (gridMatches.length > 0 && shadowMatches.length > 0) {
2045
+ return gridMatches.map((m) => ({
2046
+ patternId: this.id,
2047
+ patternName: this.name,
2048
+ severity: this.severity,
2049
+ file: filePath,
2050
+ ...m
2051
+ }));
2052
+ }
2053
+ return [];
2054
+ }
2055
+ },
2056
+ // ── JSX/Component Patterns ────────────────────────────────────────────────
2057
+ {
2058
+ id: "icon-grid-3col",
2059
+ name: "3-Column Icon Grid",
2060
+ description: "Every AI landing page: 3 features with generic icons.",
2061
+ severity: "major",
2062
+ fileTypes: JSX_EXTENSIONS,
2063
+ detect(content, filePath) {
2064
+ const iconImport = /import\s*{[^}]*}\s*from\s*['"](?:lucide-react|@heroicons|react-icons)/i;
2065
+ const threeCol = /(?:grid-cols-3|md:grid-cols-3|lg:grid-cols-3)/i;
2066
+ if (iconImport.test(content) && threeCol.test(content)) {
2067
+ const matches = findLines(content, threeCol);
2068
+ return matches.map((m) => ({
2069
+ patternId: this.id,
2070
+ patternName: this.name,
2071
+ severity: this.severity,
2072
+ file: filePath,
2073
+ ...m
2074
+ }));
2075
+ }
2076
+ return [];
2077
+ }
2078
+ },
2079
+ {
2080
+ id: "generic-ctas",
2081
+ name: "Generic CTAs Only",
2082
+ description: '"Get Started" / "Learn More" \u2014 zero thought on conversion.',
2083
+ severity: "minor",
2084
+ fileTypes: JSX_EXTENSIONS,
2085
+ detect(content, filePath) {
2086
+ const genericCtas = /(?:>Get Started<|>Learn More<|>Sign Up Free<|>Start Free Trial<|>Get Started Now<|>Try for Free<)/gi;
2087
+ const allMatches = findLines(content, genericCtas);
2088
+ const uniqueCta = /(?:>(?!Get Started|Learn More|Sign Up|Start Free|Try for Free)[A-Z][^<]{3,30}<)/g;
2089
+ if (allMatches.length > 0 && !uniqueCta.test(content)) {
2090
+ return allMatches.map((m) => ({
2091
+ patternId: this.id,
2092
+ patternName: this.name,
2093
+ severity: this.severity,
2094
+ file: filePath,
2095
+ ...m
2096
+ }));
2097
+ }
2098
+ return [];
2099
+ }
2100
+ },
2101
+ {
2102
+ id: "generic-section-flow",
2103
+ name: "Generic Section Flow",
2104
+ description: "Hero \u2192 Features \u2192 Pricing \u2192 Testimonials \u2192 CTA \u2192 Footer. The AI playbook.",
2105
+ severity: "minor",
2106
+ fileTypes: JSX_EXTENSIONS,
2107
+ detect(content, filePath) {
2108
+ const sections = ["hero", "features", "pricing", "testimonial", "cta", "footer"];
2109
+ const found = sections.filter((s) => new RegExp(`(?:\\b${s}\\b|<${s}|id=["']${s})`, "i").test(content));
2110
+ if (found.length >= 4) {
2111
+ return [{ line: 1, excerpt: `Generic section flow: ${found.join(" \u2192 ")}` }].map((m) => ({
2112
+ patternId: this.id,
2113
+ patternName: this.name,
2114
+ severity: this.severity,
2115
+ file: filePath,
2116
+ ...m
2117
+ }));
2118
+ }
2119
+ return [];
2120
+ }
2121
+ },
2122
+ {
2123
+ id: "floating-shapes",
2124
+ name: "Floating Abstract Shapes",
2125
+ description: "Decorative blobs/circles positioned absolutely. Meaningless visual noise.",
2126
+ severity: "minor",
2127
+ fileTypes: ALL_UI,
2128
+ detect(content, filePath) {
2129
+ const pattern = /(?:absolute\s+[^"'<>]{0,200}?(?:rounded-full|border-radius:\s*50%)[^"'<>]{0,100}?blur|blur-(?:2xl|3xl)[^"'<>]{0,200}?rounded-full)/gi;
2130
+ return findMultilinePattern(content, pattern).map((m) => ({
2131
+ patternId: this.id,
2132
+ patternName: this.name,
2133
+ severity: this.severity,
2134
+ file: filePath,
2135
+ ...m
2136
+ }));
2137
+ }
2138
+ },
2139
+ {
2140
+ id: "single-font",
2141
+ name: "Single Font Family",
2142
+ description: "Real designers use typographic hierarchy. One font = no hierarchy.",
2143
+ severity: "minor",
2144
+ fileTypes: CSS_EXTENSIONS,
2145
+ detect(content, filePath) {
2146
+ const fontFamilies = /* @__PURE__ */ new Set();
2147
+ const pattern = /font-family:\s*["']?([^"';,\n]+)/gi;
2148
+ let match;
2149
+ while ((match = pattern.exec(content)) !== null) {
2150
+ const font = match[1].trim().toLowerCase();
2151
+ if (font !== "inherit" && font !== "sans-serif" && font !== "serif" && font !== "monospace") {
2152
+ fontFamilies.add(font);
2153
+ }
2154
+ }
2155
+ const tailwindFonts = content.match(/font-(?:sans|serif|mono|heading|body|display)/g);
2156
+ if (tailwindFonts) {
2157
+ for (const f of tailwindFonts) fontFamilies.add(f);
2158
+ }
2159
+ if (fontFamilies.size === 1 && content.length > 500) {
2160
+ return [{ line: 1, excerpt: `Only one font family: ${[...fontFamilies][0]}` }].map((m) => ({
2161
+ patternId: this.id,
2162
+ patternName: this.name,
2163
+ severity: this.severity,
2164
+ file: filePath,
2165
+ ...m
2166
+ }));
2167
+ }
2168
+ return [];
2169
+ }
2170
+ }
2171
+ ];
2172
+ function scanFiles(dir, extensions) {
2173
+ const results = [];
2174
+ if (!existsSync8(dir)) return results;
2175
+ function walk(current) {
2176
+ const entries = readdirSync3(current);
2177
+ for (const entry of entries) {
2178
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
2179
+ const fullPath = join8(current, entry);
2180
+ const stat = statSync(fullPath);
2181
+ if (stat.isDirectory()) {
2182
+ walk(fullPath);
2183
+ } else if (extensions.includes(extname(entry).toLowerCase())) {
2184
+ results.push(fullPath);
2185
+ }
2186
+ }
2187
+ }
2188
+ walk(dir);
2189
+ return results;
2190
+ }
2191
+ function runSlopScan(targetDir) {
2192
+ const allExtensions = [...new Set(PATTERNS.flatMap((p) => p.fileTypes))];
2193
+ const files = scanFiles(targetDir, allExtensions);
2194
+ const allMatches = [];
2195
+ for (const file of files) {
2196
+ const content = readFileSync5(file, "utf-8");
2197
+ const ext = extname(file).toLowerCase();
2198
+ for (const pattern of PATTERNS) {
2199
+ if (!pattern.fileTypes.includes(ext)) continue;
2200
+ const matches = pattern.detect(content, file.replace(targetDir + "/", ""));
2201
+ allMatches.push(...matches);
2202
+ }
2203
+ }
2204
+ const summary = {};
2205
+ for (const m of allMatches) {
2206
+ summary[m.patternName] = (summary[m.patternName] ?? 0) + 1;
2207
+ }
2208
+ const majorCount = allMatches.filter((m) => m.severity === "major").length;
2209
+ const minorCount = allMatches.filter((m) => m.severity === "minor").length;
2210
+ const totalScore = majorCount * 3 + minorCount;
2211
+ let score;
2212
+ if (totalScore === 0) score = "A";
2213
+ else if (totalScore <= 2 && majorCount === 0) score = "B";
2214
+ else if (totalScore <= 5 && majorCount <= 1) score = "C";
2215
+ else if (totalScore <= 10) score = "D";
2216
+ else score = "F";
2217
+ return {
2218
+ score,
2219
+ totalFiles: files.length,
2220
+ scannedFiles: files.length,
2221
+ matches: allMatches,
2222
+ summary
2223
+ };
2224
+ }
2225
+
2226
+ // src/design-score.ts
2227
+ import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2228
+ import { join as join9, extname as extname2 } from "path";
2229
+ function extractTokensFromDesign(projectRoot) {
2230
+ const designPath = join9(projectRoot, "docs/design/DESIGN.md");
2231
+ const tokens = { colors: [], typography: [], spacing: [], radii: [], shadows: [] };
2232
+ if (!existsSync9(designPath)) return tokens;
2233
+ const content = readFileSync6(designPath, "utf-8");
2234
+ const customProps = content.matchAll(/`(--[\w-]+)`/g);
2235
+ for (const match of customProps) {
2236
+ const prop = match[1];
2237
+ if (prop.startsWith("--color-")) tokens.colors.push(prop);
2238
+ else if (prop.startsWith("--font-") || prop.startsWith("--text-")) tokens.typography.push(prop);
2239
+ else if (prop.startsWith("--space-")) tokens.spacing.push(prop);
2240
+ else if (prop.startsWith("--radius-")) tokens.radii.push(prop);
2241
+ else if (prop.startsWith("--shadow-")) tokens.shadows.push(prop);
2242
+ }
2243
+ return tokens;
2244
+ }
2245
+ function extractTokensFromCSS(projectRoot) {
2246
+ const tokens = { colors: [], typography: [], spacing: [], radii: [], shadows: [] };
2247
+ const cssFiles = scanFiles2(projectRoot, [".css", ".scss"]);
2248
+ for (const file of cssFiles) {
2249
+ const content = readFileSync6(file, "utf-8");
2250
+ const customProps = content.matchAll(/(--[\w-]+)\s*:/g);
2251
+ for (const match of customProps) {
2252
+ const prop = match[1];
2253
+ if (prop.startsWith("--color-") && !tokens.colors.includes(prop)) tokens.colors.push(prop);
2254
+ else if ((prop.startsWith("--font-") || prop.startsWith("--text-")) && !tokens.typography.includes(prop)) tokens.typography.push(prop);
2255
+ else if (prop.startsWith("--space-") && !tokens.spacing.includes(prop)) tokens.spacing.push(prop);
2256
+ else if (prop.startsWith("--radius-") && !tokens.radii.includes(prop)) tokens.radii.push(prop);
2257
+ else if (prop.startsWith("--shadow-") && !tokens.shadows.includes(prop)) tokens.shadows.push(prop);
2258
+ }
2259
+ }
2260
+ return tokens;
2261
+ }
2262
+ function scanFiles2(dir, extensions) {
2263
+ const results = [];
2264
+ if (!existsSync9(dir)) return results;
2265
+ function walk(current) {
2266
+ const entries = readdirSync4(current);
2267
+ for (const entry of entries) {
2268
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
2269
+ const fullPath = join9(current, entry);
2270
+ const stat = statSync2(fullPath);
2271
+ if (stat.isDirectory()) {
2272
+ walk(fullPath);
2273
+ } else if (extensions.includes(extname2(entry).toLowerCase())) {
2274
+ results.push(fullPath);
2275
+ }
2276
+ }
2277
+ }
2278
+ walk(dir);
2279
+ return results;
2280
+ }
2281
+ var HARDCODED_COLORS = /(?:(?:color|background(?:-color)?|border(?:-color)?|fill|stroke)\s*:\s*)(#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|hsl\([^)]+\))/g;
2282
+ var TAILWIND_ARBITRARY_COLORS = /(?:bg|text|border|ring|fill|stroke)-\[#[0-9a-fA-F]{3,8}\]/g;
2283
+ var HARDCODED_FONTS = /font-size:\s*(\d+(?:\.\d+)?(?:px|rem|em))/g;
2284
+ var HARDCODED_SPACING = /(?:margin|padding|gap)(?:-(?:top|right|bottom|left|x|y))?\s*:\s*(\d+(?:\.\d+)?px)/g;
2285
+ function detectViolations(content, filePath, tokens) {
2286
+ const violations = [];
2287
+ const lines = content.split("\n");
2288
+ for (let i = 0; i < lines.length; i++) {
2289
+ const line = lines[i];
2290
+ const lineNum = i + 1;
2291
+ if (/var\(--/.test(line)) continue;
2292
+ let match;
2293
+ const colorRegex = new RegExp(HARDCODED_COLORS.source, "g");
2294
+ while ((match = colorRegex.exec(line)) !== null) {
2295
+ violations.push({
2296
+ type: "color",
2297
+ file: filePath,
2298
+ line: lineNum,
2299
+ value: match[1],
2300
+ suggestion: tokens.colors.length > 0 ? `Use a token: var(${tokens.colors[0]}), etc.` : "Define color tokens in DESIGN.md first"
2301
+ });
2302
+ }
2303
+ const twColorRegex = new RegExp(TAILWIND_ARBITRARY_COLORS.source, "g");
2304
+ while ((match = twColorRegex.exec(line)) !== null) {
2305
+ violations.push({
2306
+ type: "color",
2307
+ file: filePath,
2308
+ line: lineNum,
2309
+ value: match[0],
2310
+ suggestion: "Use Tailwind theme colors instead of arbitrary values"
2311
+ });
2312
+ }
2313
+ const fontRegex = new RegExp(HARDCODED_FONTS.source, "g");
2314
+ while ((match = fontRegex.exec(line)) !== null) {
2315
+ violations.push({
2316
+ type: "typography",
2317
+ file: filePath,
2318
+ line: lineNum,
2319
+ value: match[1],
2320
+ suggestion: tokens.typography.length > 0 ? `Use a token: var(${tokens.typography[0]}), etc.` : "Define typography tokens in DESIGN.md first"
2321
+ });
2322
+ }
2323
+ const spacingRegex = new RegExp(HARDCODED_SPACING.source, "g");
2324
+ while ((match = spacingRegex.exec(line)) !== null) {
2325
+ const px = parseInt(match[1]);
2326
+ const standardSpacing = [0, 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96];
2327
+ if (!standardSpacing.includes(px)) {
2328
+ violations.push({
2329
+ type: "spacing",
2330
+ file: filePath,
2331
+ line: lineNum,
2332
+ value: match[1],
2333
+ suggestion: `${px}px is not on the spacing scale. Nearest: ${standardSpacing.reduce((a, b) => Math.abs(b - px) < Math.abs(a - px) ? b : a)}px`
2334
+ });
2335
+ }
2336
+ }
2337
+ }
2338
+ return violations;
2339
+ }
2340
+ function runDesignScore(projectRoot, targetDir) {
2341
+ const scanDir = targetDir ?? join9(projectRoot, "src");
2342
+ const designTokens = extractTokensFromDesign(projectRoot);
2343
+ const cssTokens = extractTokensFromCSS(projectRoot);
2344
+ const tokens = {
2345
+ colors: [.../* @__PURE__ */ new Set([...designTokens.colors, ...cssTokens.colors])],
2346
+ typography: [.../* @__PURE__ */ new Set([...designTokens.typography, ...cssTokens.typography])],
2347
+ spacing: [.../* @__PURE__ */ new Set([...designTokens.spacing, ...cssTokens.spacing])],
2348
+ radii: [.../* @__PURE__ */ new Set([...designTokens.radii, ...cssTokens.radii])],
2349
+ shadows: [.../* @__PURE__ */ new Set([...designTokens.shadows, ...cssTokens.shadows])]
2350
+ };
2351
+ const files = scanFiles2(scanDir, [".css", ".scss", ".tsx", ".jsx"]);
2352
+ const allViolations = [];
2353
+ let tokenUsageCount = 0;
2354
+ for (const file of files) {
2355
+ const content = readFileSync6(file, "utf-8");
2356
+ const relPath = file.replace(projectRoot + "/", "");
2357
+ const tokenRefs = content.match(/var\(--/g);
2358
+ if (tokenRefs) tokenUsageCount += tokenRefs.length;
2359
+ const twThemeRefs = content.match(/(?:text|bg|border|ring|shadow|font|space|rounded)-(?![\[{])/g);
2360
+ if (twThemeRefs) tokenUsageCount += twThemeRefs.length;
2361
+ const violations = detectViolations(content, relPath, tokens);
2362
+ allViolations.push(...violations);
2363
+ }
2364
+ const checks = [
2365
+ {
2366
+ name: "Color tokens defined",
2367
+ passed: tokens.colors.length >= 3,
2368
+ details: tokens.colors.length >= 3 ? `${tokens.colors.length} color tokens found` : `Only ${tokens.colors.length} color tokens \u2014 need at least 3 (primary, secondary, neutral)`
2369
+ },
2370
+ {
2371
+ name: "Typography hierarchy",
2372
+ passed: tokens.typography.length >= 2,
2373
+ details: tokens.typography.length >= 2 ? `${tokens.typography.length} typography tokens \u2014 hierarchy established` : `Only ${tokens.typography.length} typography token(s) \u2014 need heading + body at minimum`
2374
+ },
2375
+ {
2376
+ name: "Spacing system",
2377
+ passed: tokens.spacing.length >= 3,
2378
+ details: tokens.spacing.length >= 3 ? `${tokens.spacing.length}-step spacing scale` : `Only ${tokens.spacing.length} spacing tokens \u2014 need a proper scale`
2379
+ },
2380
+ {
2381
+ name: "Token adoption",
2382
+ passed: tokenUsageCount > allViolations.length,
2383
+ details: tokenUsageCount > allViolations.length ? `${tokenUsageCount} token refs vs ${allViolations.length} hardcoded \u2014 tokens winning` : `${tokenUsageCount} token refs vs ${allViolations.length} hardcoded \u2014 hardcoded values dominate`
2384
+ },
2385
+ {
2386
+ name: "No excessive hardcoded colors",
2387
+ passed: allViolations.filter((v) => v.type === "color").length <= 5,
2388
+ details: `${allViolations.filter((v) => v.type === "color").length} hardcoded color(s) found`
2389
+ }
2390
+ ];
2391
+ const passedChecks = checks.filter((c) => c.passed).length;
2392
+ const violationPenalty = Math.min(allViolations.length, 20);
2393
+ let score;
2394
+ if (files.length === 0) {
2395
+ score = "-";
2396
+ } else if (passedChecks >= 5 && violationPenalty <= 2) {
2397
+ score = "A";
2398
+ } else if (passedChecks >= 4 && violationPenalty <= 5) {
2399
+ score = "B";
2400
+ } else if (passedChecks >= 3 && violationPenalty <= 10) {
2401
+ score = "C";
2402
+ } else if (passedChecks >= 2) {
2403
+ score = "D";
2404
+ } else {
2405
+ score = "F";
2406
+ }
2407
+ return {
2408
+ score,
2409
+ tokensFound: tokens,
2410
+ tokenUsageCount,
2411
+ hardcodedCount: allViolations.length,
2412
+ violations: allViolations.slice(0, 30),
2413
+ // Limit output
2414
+ checks
2415
+ };
2416
+ }
2417
+
2418
+ // src/display.ts
2419
+ import chalk from "chalk";
2420
+ var ICONS = {
2421
+ pass: chalk.green("\u2713"),
2422
+ fail: chalk.red("\u2717"),
2423
+ pending: chalk.yellow("\u25CB"),
2424
+ locked: chalk.dim("\u25CC"),
2425
+ active: chalk.cyan("\u25BA"),
2426
+ completed: chalk.green("\u25CF"),
2427
+ skipped: chalk.dim("\u25CB")
2428
+ };
2429
+ function displayBanner() {
2430
+ console.log(chalk.bold(`
2431
+ \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588
2432
+ \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588
2433
+ \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588
2434
+ \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588
2435
+ \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588
2436
+ `));
2437
+ console.log(chalk.dim(" Born from many. Better than all.\n"));
2438
+ }
2439
+ function displayStatus(state) {
2440
+ console.log(chalk.bold(`
2441
+ Project: ${state.project}`));
2442
+ console.log(chalk.dim(` Created: ${new Date(state.createdAt).toLocaleDateString()}`));
2443
+ console.log(chalk.bold(` Current: Round ${state.currentRound}
2444
+ `));
2445
+ console.log(chalk.bold(" Pipeline:\n"));
2446
+ for (const round of ROUNDS) {
2447
+ const rs = state.rounds[String(round.id)];
2448
+ if (!rs) continue;
2449
+ let icon;
2450
+ let style;
2451
+ switch (rs.status) {
2452
+ case "completed":
2453
+ icon = ICONS.completed;
2454
+ style = chalk.green;
2455
+ break;
2456
+ case "active":
2457
+ icon = ICONS.active;
2458
+ style = chalk.cyan.bold;
2459
+ break;
2460
+ case "locked":
2461
+ icon = ICONS.locked;
2462
+ style = chalk.dim;
2463
+ break;
2464
+ case "skipped":
2465
+ icon = ICONS.skipped;
2466
+ style = chalk.dim;
2467
+ break;
2468
+ }
2469
+ const gateIcon = rs.gate === "passed" ? chalk.green(" [GATE \u2713]") : rs.gate === "failed" ? chalk.red(" [GATE \u2717]") : "";
2470
+ const frameworks = chalk.dim(` [${round.frameworks.join(" + ")}]`);
2471
+ console.log(` ${icon} ${style(`Round ${round.id} \u2014 ${round.name}`)}${frameworks}${gateIcon}`);
2472
+ }
2473
+ console.log();
2474
+ }
2475
+ function displayGateReport(report) {
2476
+ const round = ROUNDS.find((r) => r.id === report.roundId);
2477
+ if (!round) return;
2478
+ const status = report.allPassed ? chalk.green.bold("PASSED") : chalk.red.bold("FAILED");
2479
+ console.log(chalk.bold(`
2480
+ Gate Check \u2014 Round ${round.id}: ${round.name} \u2014 ${status}
2481
+ `));
2482
+ for (const result of report.results) {
2483
+ const icon = result.passed ? ICONS.pass : ICONS.fail;
2484
+ console.log(` ${icon} ${result.message}`);
2485
+ }
2486
+ if (report.details.size > 0) {
2487
+ for (const [checkId, validation] of report.details) {
2488
+ if (!validation.fileExists || validation.allPassed) continue;
2489
+ console.log(chalk.dim(`
2490
+ Details for ${validation.path}:`));
2491
+ for (const s of validation.sections) {
2492
+ if (!s.exists) {
2493
+ console.log(chalk.dim(` ${ICONS.fail} Section "${s.heading}" not found`));
2494
+ } else if (!s.meetsMinLength) {
2495
+ console.log(chalk.dim(` ${ICONS.fail} "${s.heading}" \u2014 ${s.contentLength} chars (template content doesn't count)`));
2496
+ } else if (s.patternMatched === false) {
2497
+ console.log(chalk.dim(` ${ICONS.fail} "${s.heading}" \u2014 ${s.patternDescription}`));
2498
+ } else {
2499
+ console.log(chalk.dim(` ${ICONS.pass} "${s.heading}" \u2014 ${s.contentLength} chars`));
2500
+ }
2501
+ }
2502
+ }
2503
+ }
2504
+ if (report.allPassed) {
2505
+ console.log(chalk.green(`
2506
+ Gate passed. Run ${chalk.bold("bastard next")} to advance.
2507
+ `));
2508
+ } else {
2509
+ const failed = report.results.filter((r) => !r.passed);
2510
+ console.log(chalk.red(`
2511
+ ${failed.length} check(s) failed. Fix them and run ${chalk.bold("bastard gate")} again.
2512
+ `));
2513
+ }
2514
+ }
2515
+ function displayHooksInstalled(result) {
2516
+ if (result.created) {
2517
+ console.log(chalk.green(`
2518
+ Hooks installed at ${result.path}
2519
+ `));
2520
+ } else if (result.merged) {
2521
+ console.log(chalk.green(`
2522
+ Hooks merged into ${result.path}
2523
+ `));
2524
+ }
2525
+ console.log(chalk.bold(" Active guards:\n"));
2526
+ console.log(` ${ICONS.active} No source code before Round 5`);
2527
+ console.log(` ${ICONS.active} No CSS/styling before Round 2 gate passes`);
2528
+ console.log(` ${ICONS.active} No task files before Round 4`);
2529
+ console.log(` ${ICONS.active} Security-sensitive files flagged after Round 5`);
2530
+ console.log(` ${ICONS.active} git add . / git add -A blocked`);
2531
+ console.log(` ${ICONS.active} Direct commits to main/master blocked`);
2532
+ console.log();
2533
+ }
2534
+ function displayValidation(validation) {
2535
+ const status = validation.allPassed ? chalk.green.bold("VALID") : chalk.red.bold("INVALID");
2536
+ console.log(chalk.bold(`
2537
+ ${validation.description} \u2014 ${validation.path} \u2014 ${status}
2538
+ `));
2539
+ if (!validation.fileExists) {
2540
+ console.log(` ${ICONS.fail} File not found
2541
+ `);
2542
+ return;
2543
+ }
2544
+ for (const s of validation.sections) {
2545
+ const parts = [];
2546
+ if (!s.exists) {
2547
+ console.log(` ${ICONS.fail} ${s.heading} \u2014 section not found`);
2548
+ continue;
2549
+ }
2550
+ parts.push(`${s.contentLength} chars`);
2551
+ if (!s.meetsMinLength) parts.push(chalk.red("below minimum"));
2552
+ if (s.patternMatched === true) parts.push(chalk.green("pattern ok"));
2553
+ if (s.patternMatched === false) parts.push(chalk.red(`missing: ${s.patternDescription}`));
2554
+ const icon = s.meetsMinLength && s.patternMatched !== false ? ICONS.pass : ICONS.fail;
2555
+ console.log(` ${icon} ${s.heading} \u2014 ${parts.join(", ")}`);
2556
+ }
2557
+ console.log();
2558
+ }
2559
+ function displayHistory(state, limit = 10) {
2560
+ console.log(chalk.bold("\n History:\n"));
2561
+ const entries = state.history.slice(-limit).reverse();
2562
+ for (const entry of entries) {
2563
+ const date = new Date(entry.timestamp).toLocaleString();
2564
+ const action = chalk.cyan(entry.action.padEnd(16));
2565
+ const round = chalk.dim(`R${entry.round}`);
2566
+ const details = entry.details ? chalk.dim(` \u2014 ${entry.details}`) : "";
2567
+ console.log(` ${chalk.dim(date)} ${action} ${round}${details}`);
2568
+ }
2569
+ console.log();
2570
+ }
2571
+ function displayInit(projectName, created) {
2572
+ displayBanner();
2573
+ console.log(chalk.green.bold(` Project "${projectName}" initialized.
2574
+ `));
2575
+ if (created.length > 0) {
2576
+ console.log(chalk.bold(" Templates created:\n"));
2577
+ for (const path of created) {
2578
+ console.log(` ${chalk.green("+")} ${path}`);
2579
+ }
2580
+ }
2581
+ console.log(chalk.bold("\n Next steps:\n"));
2582
+ console.log(` 1. Run ${chalk.cyan("bastard hooks install")} to enforce pipeline rules`);
2583
+ console.log(` 2. Run ${chalk.cyan("bastard run")} to see the Round 1 workflow`);
2584
+ console.log(` 3. Fill the product docs (PRD, Personas, Acceptance Criteria)`);
2585
+ console.log(` 4. Run ${chalk.cyan("bastard gate")} \u2192 ${chalk.cyan("bastard approve 1")} \u2192 ${chalk.cyan("bastard next")}
2586
+ `);
2587
+ }
2588
+ function displayParents(statuses) {
2589
+ console.log(chalk.bold("\n Parent Frameworks:\n"));
2590
+ for (const s of statuses) {
2591
+ const icon = s.installed ? ICONS.pass : ICONS.fail;
2592
+ const name = s.installed ? chalk.green(s.framework.name.padEnd(18)) : chalk.red(s.framework.name.padEnd(18));
2593
+ const role = chalk.dim(s.framework.role);
2594
+ const rounds = chalk.dim(` (R${s.framework.rounds.join(",")})`);
2595
+ const via = s.detectedVia ? chalk.dim(` \u2014 via ${s.detectedVia}`) : "";
2596
+ console.log(` ${icon} ${name} ${role}${rounds}${via}`);
2597
+ }
2598
+ const missing = statuses.filter((s) => !s.installed);
2599
+ if (missing.length > 0) {
2600
+ console.log(chalk.bold("\n Install missing:\n"));
2601
+ console.log(` ${chalk.cyan("bastard parents install")} Install all missing`);
2602
+ console.log(` ${chalk.cyan("bastard parents install --force")} Update all to latest
2603
+ `);
2604
+ } else {
2605
+ console.log(chalk.green("\n All 7 parents installed.\n"));
2606
+ console.log(chalk.dim(` Run ${chalk.cyan("bastard parents install --force")} to update all to latest.
2607
+ `));
2608
+ }
2609
+ }
2610
+ function displayInstallProgress(result, index, total) {
2611
+ const progress = chalk.dim(`[${index}/${total}]`);
2612
+ if (result.alreadyInstalled) {
2613
+ console.log(` ${progress} ${ICONS.pass} ${result.name} \u2014 already installed`);
2614
+ } else if (result.success && result.updated) {
2615
+ console.log(` ${progress} ${chalk.cyan("\u2191")} ${result.name} \u2014 updated ${chalk.dim(`(${result.duration}ms)`)}`);
2616
+ } else if (result.success) {
2617
+ console.log(` ${progress} ${ICONS.pass} ${result.name} \u2014 installed ${chalk.dim(`(${result.duration}ms)`)}`);
2618
+ } else {
2619
+ console.log(` ${progress} ${ICONS.fail} ${result.name} \u2014 ${chalk.red("failed")}`);
2620
+ if (result.error) {
2621
+ console.log(chalk.dim(` ${result.error.split("\n")[0]}`));
2622
+ }
2623
+ }
2624
+ }
2625
+ function displayInstallSummary(results) {
2626
+ const installed = results.filter((r) => r.success && !r.alreadyInstalled && !r.updated);
2627
+ const updated = results.filter((r) => r.success && r.updated);
2628
+ const skipped = results.filter((r) => r.alreadyInstalled);
2629
+ const failed = results.filter((r) => !r.success);
2630
+ console.log();
2631
+ if (installed.length > 0) console.log(chalk.green(` ${installed.length} installed`));
2632
+ if (updated.length > 0) console.log(chalk.cyan(` ${updated.length} updated`));
2633
+ if (skipped.length > 0) console.log(chalk.dim(` ${skipped.length} already up to date`));
2634
+ if (failed.length > 0) {
2635
+ console.log(chalk.red(` ${failed.length} failed:`));
2636
+ for (const f of failed) {
2637
+ console.log(chalk.red(` - ${f.name}: ${f.error?.split("\n")[0] ?? "unknown error"}`));
2638
+ }
2639
+ }
2640
+ console.log();
2641
+ }
2642
+ function displayWorkflow(workflow, installedFrameworks) {
2643
+ console.log(chalk.bold(`
2644
+ Round ${workflow.roundId} \u2014 ${workflow.name}
2645
+ `));
2646
+ console.log(chalk.dim(` ${workflow.contextAdvice}
2647
+ `));
2648
+ for (let i = 0; i < workflow.steps.length; i++) {
2649
+ const step = workflow.steps[i];
2650
+ const num = i + 1;
2651
+ const installed = installedFrameworks.has(step.framework);
2652
+ const fwBadge = installed ? chalk.green(`[${step.framework}]`) : chalk.yellow(`[${step.framework}?]`);
2653
+ console.log(chalk.bold(` Step ${num}: ${step.description} ${fwBadge}`));
2654
+ if (installed) {
2655
+ console.log(` ${chalk.cyan("\u2192")} ${chalk.cyan(step.action)}`);
2656
+ } else if (step.fallbackAction) {
2657
+ console.log(` ${chalk.yellow("\u2192")} ${step.fallbackAction}`);
2658
+ }
2659
+ if (step.output) {
2660
+ console.log(chalk.dim(` Output: ${step.output}`));
2661
+ }
2662
+ if (step.isManual) {
2663
+ console.log(chalk.dim(" (manual step \u2014 requires human input)"));
2664
+ }
2665
+ console.log();
2666
+ }
2667
+ console.log(chalk.bold(" After completion:\n"));
2668
+ for (const cmd of workflow.completionSteps) {
2669
+ console.log(` ${chalk.cyan("$")} ${chalk.cyan(cmd)}`);
2670
+ }
2671
+ console.log();
2672
+ }
2673
+ function scoreColor(score) {
2674
+ switch (score) {
2675
+ case "A":
2676
+ return chalk.green.bold;
2677
+ case "B":
2678
+ return chalk.green;
2679
+ case "C":
2680
+ return chalk.yellow;
2681
+ case "D":
2682
+ return chalk.red;
2683
+ case "F":
2684
+ return chalk.red.bold;
2685
+ default:
2686
+ return chalk.dim;
2687
+ }
2688
+ }
2689
+ function displaySlopReport(report) {
2690
+ const color = scoreColor(report.score);
2691
+ console.log(chalk.bold("\n AI Slop Score: ") + color(report.score) + "\n");
2692
+ console.log(chalk.dim(` Scanned ${report.scannedFiles} file(s)
2693
+ `));
2694
+ if (report.matches.length === 0) {
2695
+ console.log(chalk.green(" No slop patterns detected. Clean design.\n"));
2696
+ return;
2697
+ }
2698
+ console.log(chalk.bold(" Patterns detected:\n"));
2699
+ for (const [name, count] of Object.entries(report.summary)) {
2700
+ const match = report.matches.find((m) => m.patternName === name);
2701
+ const severity = match?.severity === "major" ? chalk.red("[MAJOR]") : chalk.yellow("[minor]");
2702
+ console.log(` ${severity} ${name}: ${count} occurrence(s)`);
2703
+ }
2704
+ const shown = report.matches.slice(0, 15);
2705
+ if (shown.length > 0) {
2706
+ console.log(chalk.bold("\n Details:\n"));
2707
+ for (const m of shown) {
2708
+ const sev = m.severity === "major" ? chalk.red("!") : chalk.yellow("~");
2709
+ console.log(` ${sev} ${chalk.dim(m.file)}:${m.line}`);
2710
+ console.log(` ${chalk.dim(m.excerpt)}`);
2711
+ }
2712
+ if (report.matches.length > 15) {
2713
+ console.log(chalk.dim(`
2714
+ ... and ${report.matches.length - 15} more.`));
2715
+ }
2716
+ }
2717
+ console.log();
2718
+ if (report.score === "A") {
2719
+ console.log(chalk.green(" Distinctive design \u2014 no AI slop detected.\n"));
2720
+ } else if (report.score === "B") {
2721
+ console.log(chalk.green(" Minor generic elements. Acceptable for merge.\n"));
2722
+ } else {
2723
+ console.log(chalk.red(` Score ${report.score} \u2014 below merge threshold (need A). Fix the patterns above.
2724
+ `));
2725
+ }
2726
+ }
2727
+ function displayDesignScore(report) {
2728
+ const color = scoreColor(report.score);
2729
+ console.log(chalk.bold("\n Design Score: ") + color(report.score) + "\n");
2730
+ const t = report.tokensFound;
2731
+ const total = t.colors.length + t.typography.length + t.spacing.length + t.radii.length + t.shadows.length;
2732
+ console.log(chalk.bold(" Token inventory:"));
2733
+ console.log(` Colors: ${t.colors.length}`);
2734
+ console.log(` Typography: ${t.typography.length}`);
2735
+ console.log(` Spacing: ${t.spacing.length}`);
2736
+ console.log(` Radii: ${t.radii.length}`);
2737
+ console.log(` Shadows: ${t.shadows.length}`);
2738
+ console.log(chalk.dim(` Total: ${total} tokens
2739
+ `));
2740
+ console.log(chalk.bold(" Design checks:\n"));
2741
+ for (const check of report.checks) {
2742
+ const icon = check.passed ? ICONS.pass : ICONS.fail;
2743
+ console.log(` ${icon} ${check.name} \u2014 ${chalk.dim(check.details)}`);
2744
+ }
2745
+ console.log(chalk.bold(`
2746
+ Token usage: ${report.tokenUsageCount} refs | Hardcoded: ${report.hardcodedCount} violations
2747
+ `));
2748
+ if (report.violations.length > 0) {
2749
+ console.log(chalk.bold(" Violations:\n"));
2750
+ const shown = report.violations.slice(0, 15);
2751
+ for (const v of shown) {
2752
+ console.log(` ${ICONS.fail} ${chalk.dim(v.file)}:${v.line} \u2014 ${chalk.red(v.value)}`);
2753
+ console.log(` ${chalk.dim(v.suggestion)}`);
2754
+ }
2755
+ if (report.violations.length > 15) {
2756
+ console.log(chalk.dim(`
2757
+ ... and ${report.violations.length - 15} more. (showing first 15)`));
2758
+ }
2759
+ }
2760
+ console.log();
2761
+ if (report.score === "-") {
2762
+ console.log(chalk.dim(" No UI files found to score.\n"));
2763
+ } else if (["A", "B"].includes(report.score)) {
2764
+ console.log(chalk.green(` Design Score ${report.score} \u2014 meets merge threshold (>= B).
2765
+ `));
2766
+ } else {
2767
+ console.log(chalk.red(` Design Score ${report.score} \u2014 below merge threshold (need >= B).
2768
+ `));
2769
+ }
2770
+ }
2771
+ function displayAudit(slop, design) {
2772
+ displayBanner();
2773
+ console.log(chalk.bold(" Visual Audit Report\n"));
2774
+ const slopColor = scoreColor(slop.score);
2775
+ const designColor = scoreColor(design.score);
2776
+ console.log(` AI Slop Score: ${slopColor(slop.score)}`);
2777
+ console.log(` Design Score: ${designColor(design.score)}`);
2778
+ const canMerge = slop.score === "A" && (design.score === "A" || design.score === "B" || design.score === "-");
2779
+ console.log(`
2780
+ Merge verdict: ${canMerge ? chalk.green.bold("PASS") : chalk.red.bold("FAIL")}`);
2781
+ if (!canMerge) {
2782
+ if (slop.score !== "A") {
2783
+ console.log(chalk.red(` AI Slop must be A (currently ${slop.score})`));
2784
+ }
2785
+ if (design.score !== "-" && !["A", "B"].includes(design.score)) {
2786
+ console.log(chalk.red(` Design must be >= B (currently ${design.score})`));
2787
+ }
2788
+ }
2789
+ console.log();
2790
+ }
2791
+
2792
+ // src/cli.ts
2793
+ var program = new Command();
2794
+ program.name("bastard").description("Build Any SaaS Through Agent Roles & Discipline").version("1.0.0");
2795
+ program.command("init [name]").description("Initialize a new BASTARD project").option("--install-parents", "Also install all parent frameworks").option("--with-hooks", "Also install Claude Code guard hooks").action(async (name, opts) => {
2796
+ const projectRoot = process.cwd();
2797
+ if (stateExists(projectRoot)) {
2798
+ console.log(chalk2.yellow("\n BASTARD project already initialized here.\n"));
2799
+ console.log(` Run ${chalk2.cyan("bastard status")} to see current state.
2800
+ `);
2801
+ return;
2802
+ }
2803
+ const projectName = name ?? basename(projectRoot);
2804
+ const dirs = [
2805
+ "docs/product",
2806
+ "docs/design/mockups",
2807
+ "docs/architecture/ADR",
2808
+ "docs/security",
2809
+ "docs/planning",
2810
+ ".planning",
2811
+ "tasks",
2812
+ "src",
2813
+ "tests",
2814
+ ".bastard"
2815
+ ];
2816
+ for (const dir of dirs) {
2817
+ const fullPath = resolve3(projectRoot, dir);
2818
+ if (!existsSync10(fullPath)) {
2819
+ mkdirSync5(fullPath, { recursive: true });
2820
+ }
2821
+ }
2822
+ const state = createInitialState(projectName);
2823
+ saveState(projectRoot, state);
2824
+ const created = scaffoldTemplates(projectRoot, projectName);
2825
+ displayInit(projectName, created);
2826
+ if (opts.installParents) {
2827
+ console.log(chalk2.bold(" Installing parent frameworks...\n"));
2828
+ const results = await installParents(
2829
+ projectRoot,
2830
+ {},
2831
+ (result, index, total) => displayInstallProgress(result, index, total)
2832
+ );
2833
+ displayInstallSummary(results);
2834
+ }
2835
+ if (opts.withHooks) {
2836
+ const hookResult = installHooks(projectRoot);
2837
+ displayHooksInstalled(hookResult);
2838
+ }
2839
+ });
2840
+ program.command("status").description("Show current pipeline status").action(() => {
2841
+ const projectRoot = process.cwd();
2842
+ const state = loadState(projectRoot);
2843
+ displayBanner();
2844
+ displayStatus(state);
2845
+ });
2846
+ program.command("gate [round]").description("Run gate checks for a round (default: current)").action((roundArg) => {
2847
+ const projectRoot = process.cwd();
2848
+ const state = loadState(projectRoot);
2849
+ const roundId = roundArg ? parseInt(roundArg, 10) : state.currentRound;
2850
+ const round = getRound(roundId);
2851
+ if (!round) {
2852
+ console.log(chalk2.red(`
2853
+ Round ${roundId} does not exist.
2854
+ `));
2855
+ return;
2856
+ }
2857
+ const report = runGate(projectRoot, state, roundId);
2858
+ saveState(projectRoot, state);
2859
+ displayGateReport(report);
2860
+ });
2861
+ program.command("approve <round>").description("Human-approve a round gate").action((roundArg) => {
2862
+ const projectRoot = process.cwd();
2863
+ const state = loadState(projectRoot);
2864
+ const roundId = parseInt(roundArg, 10);
2865
+ const round = getRound(roundId);
2866
+ if (!round) {
2867
+ console.log(chalk2.red(`
2868
+ Round ${roundId} does not exist.
2869
+ `));
2870
+ return;
2871
+ }
2872
+ approveGate(state, roundId);
2873
+ const report = runGate(projectRoot, state, roundId);
2874
+ saveState(projectRoot, state);
2875
+ console.log(chalk2.green(`
2876
+ Round ${roundId} approved by human.
2877
+ `));
2878
+ displayGateReport(report);
2879
+ });
2880
+ program.command("next").description("Advance to the next round (if gate passes)").action(() => {
2881
+ const projectRoot = process.cwd();
2882
+ const state = loadState(projectRoot);
2883
+ const currentRound = state.currentRound;
2884
+ const report = runGate(projectRoot, state, currentRound);
2885
+ if (!report.allPassed) {
2886
+ saveState(projectRoot, state);
2887
+ displayGateReport(report);
2888
+ console.log(chalk2.red(` Cannot advance \u2014 Round ${currentRound} gate not passed.
2889
+ `));
2890
+ return;
2891
+ }
2892
+ const advanced = advanceRound(state);
2893
+ saveState(projectRoot, state);
2894
+ if (state.currentRound > 8 || currentRound === 8 && advanced) {
2895
+ displayBanner();
2896
+ console.log(chalk2.green.bold(" All 8 rounds completed. Ship it.\n"));
2897
+ displayStatus(state);
2898
+ return;
2899
+ }
2900
+ if (advanced) {
2901
+ const nextRound = getRound(state.currentRound);
2902
+ console.log(chalk2.green.bold(`
2903
+ Advanced to Round ${state.currentRound} \u2014 ${nextRound?.name}
2904
+ `));
2905
+ console.log(chalk2.dim(` Frameworks: [${nextRound?.frameworks.join(" + ")}]`));
2906
+ console.log(chalk2.dim(` ${nextRound?.description}
2907
+ `));
2908
+ console.log(chalk2.bold(" Gate requirements:\n"));
2909
+ for (const check of nextRound?.gateChecks ?? []) {
2910
+ console.log(` ${chalk2.yellow("\u25CB")} ${check.description}`);
2911
+ }
2912
+ console.log();
2913
+ } else {
2914
+ console.log(chalk2.red(`
2915
+ Could not advance from Round ${currentRound}.
2916
+ `));
2917
+ }
2918
+ });
2919
+ program.command("round <number>").description("Jump to a specific round (with warnings)").action((roundArg) => {
2920
+ const projectRoot = process.cwd();
2921
+ const state = loadState(projectRoot);
2922
+ const targetRound = parseInt(roundArg, 10);
2923
+ const round = getRound(targetRound);
2924
+ if (!round) {
2925
+ console.log(chalk2.red(`
2926
+ Round ${targetRound} does not exist.
2927
+ `));
2928
+ return;
2929
+ }
2930
+ const skipped = [];
2931
+ for (let i = 1; i < targetRound; i++) {
2932
+ const rs = state.rounds[String(i)];
2933
+ if (rs && rs.gate !== "passed") {
2934
+ skipped.push(i);
2935
+ }
2936
+ }
2937
+ if (skipped.length > 0) {
2938
+ console.log(chalk2.yellow(`
2939
+ \u26A0 WARNING: Skipping rounds with unpassed gates: ${skipped.join(", ")}`));
2940
+ console.log(chalk2.yellow(" This violates the BASTARD pipeline. Proceed at your own risk.\n"));
2941
+ for (const r of skipped) {
2942
+ state.rounds[String(r)].status = "skipped";
2943
+ }
2944
+ }
2945
+ state.currentRound = targetRound;
2946
+ state.rounds[String(targetRound)].status = "active";
2947
+ state.rounds[String(targetRound)].startedAt = (/* @__PURE__ */ new Date()).toISOString();
2948
+ state.history.push({
2949
+ action: "jump",
2950
+ round: targetRound,
2951
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2952
+ details: skipped.length > 0 ? `Jumped to Round ${targetRound}, skipping ${skipped.join(", ")}` : `Jumped to Round ${targetRound}`
2953
+ });
2954
+ saveState(projectRoot, state);
2955
+ console.log(chalk2.cyan(`
2956
+ Now on Round ${targetRound} \u2014 ${round.name}
2957
+ `));
2958
+ });
2959
+ program.command("history").description("Show action history").option("-n, --limit <number>", "Number of entries", "20").action((opts) => {
2960
+ const projectRoot = process.cwd();
2961
+ const state = loadState(projectRoot);
2962
+ displayHistory(state, parseInt(opts.limit, 10));
2963
+ });
2964
+ program.command("reset").description("Reset pipeline to Round 1 (keeps files)").action(() => {
2965
+ const projectRoot = process.cwd();
2966
+ const state = loadState(projectRoot);
2967
+ for (const [id, rs] of Object.entries(state.rounds)) {
2968
+ rs.status = id === "1" ? "active" : "locked";
2969
+ rs.gate = "pending";
2970
+ rs.gateResults = [];
2971
+ rs.completedAt = null;
2972
+ if (id === "1") {
2973
+ rs.startedAt = (/* @__PURE__ */ new Date()).toISOString();
2974
+ } else {
2975
+ rs.startedAt = null;
2976
+ }
2977
+ }
2978
+ state.currentRound = 1;
2979
+ state.history.push({
2980
+ action: "reset",
2981
+ round: 1,
2982
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2983
+ details: "Pipeline reset to Round 1"
2984
+ });
2985
+ saveState(projectRoot, state);
2986
+ console.log(chalk2.yellow("\n Pipeline reset to Round 1. Files preserved.\n"));
2987
+ });
2988
+ program.command("validate [schema]").description("Validate a document against its schema (prd, design, architecture, ...)").action((schemaArg) => {
2989
+ const projectRoot = process.cwd();
2990
+ if (schemaArg) {
2991
+ const schema = SCHEMAS[schemaArg];
2992
+ if (!schema) {
2993
+ console.log(chalk2.red(`
2994
+ Unknown schema: "${schemaArg}"`));
2995
+ console.log(` Available: ${Object.keys(SCHEMAS).join(", ")}
2996
+ `);
2997
+ return;
2998
+ }
2999
+ const result = validateDocument(projectRoot, schema);
3000
+ displayValidation(result);
3001
+ return;
3002
+ }
3003
+ console.log(chalk2.bold("\n Document Validation Report\n"));
3004
+ let allValid = true;
3005
+ for (const [key, schema] of Object.entries(SCHEMAS)) {
3006
+ if (!schema.path) continue;
3007
+ const result = validateDocument(projectRoot, schema);
3008
+ const icon = result.allPassed ? chalk2.green("\u2713") : result.fileExists ? chalk2.red("\u2717") : chalk2.dim("\u25CB");
3009
+ const status = result.allPassed ? chalk2.green("valid") : result.fileExists ? chalk2.red("invalid") : chalk2.dim("missing");
3010
+ console.log(` ${icon} ${key.padEnd(18)} ${schema.path.padEnd(45)} ${status}`);
3011
+ if (!result.allPassed) allValid = false;
3012
+ }
3013
+ console.log();
3014
+ if (!allValid) {
3015
+ console.log(chalk2.dim(` Run ${chalk2.cyan("bastard validate <schema>")} for details.
3016
+ `));
3017
+ }
3018
+ });
3019
+ program.command("guard").description("Guard for Claude Code hooks (reads tool JSON from stdin)").action(async () => {
3020
+ const projectRoot = process.cwd();
3021
+ await runGuardFromStdin(projectRoot);
3022
+ });
3023
+ var hooksCmd = program.command("hooks").description("Manage Claude Code hooks for pipeline enforcement");
3024
+ hooksCmd.command("install").description("Install guard hooks into .claude/settings.local.json").action(() => {
3025
+ const projectRoot = process.cwd();
3026
+ const result = installHooks(projectRoot);
3027
+ displayHooksInstalled(result);
3028
+ });
3029
+ hooksCmd.command("remove").description("Remove guard hooks from .claude/settings.local.json").action(() => {
3030
+ const projectRoot = process.cwd();
3031
+ const removed = removeHooks(projectRoot);
3032
+ if (removed) {
3033
+ console.log(chalk2.yellow("\n BASTARD hooks removed from .claude/settings.local.json\n"));
3034
+ } else {
3035
+ console.log(chalk2.dim("\n No hooks file found \u2014 nothing to remove.\n"));
3036
+ }
3037
+ });
3038
+ var parentsCmd = program.command("parents").description("Manage parent frameworks");
3039
+ parentsCmd.command("list", { isDefault: true }).description("Show installed/missing parent frameworks").action(() => {
3040
+ const projectRoot = process.cwd();
3041
+ const statuses = detectParents(projectRoot);
3042
+ displayParents(statuses);
3043
+ });
3044
+ parentsCmd.command("install").description("Install/update all parent frameworks (always fetches latest)").option("--force", "Reinstall and update all parents to latest version").option("--only <ids>", "Only install these frameworks (comma-separated: bmad,gsd,gstack,...)").option("--skip <ids>", "Skip these frameworks (comma-separated)").action(async (opts) => {
3045
+ const projectRoot = process.cwd();
3046
+ const force = opts.force ?? false;
3047
+ const only = opts.only ? opts.only.split(",").map((s) => s.trim()) : void 0;
3048
+ const skip = opts.skip ? opts.skip.split(",").map((s) => s.trim()) : void 0;
3049
+ console.log(chalk2.bold("\n Installing parent frameworks...\n"));
3050
+ if (force) {
3051
+ console.log(chalk2.cyan(" --force: updating all to latest\n"));
3052
+ }
3053
+ const results = await installParents(
3054
+ projectRoot,
3055
+ { force, only, skip },
3056
+ (result, index, total) => displayInstallProgress(result, index, total)
3057
+ );
3058
+ displayInstallSummary(results);
3059
+ const statuses = detectParents(projectRoot);
3060
+ displayParents(statuses);
3061
+ });
3062
+ program.command("run [round]").description("Show workflow for a round with framework-aware steps (default: current)").action((roundArg) => {
3063
+ const projectRoot = process.cwd();
3064
+ let roundId;
3065
+ if (roundArg) {
3066
+ roundId = parseInt(roundArg, 10);
3067
+ } else if (stateExists(projectRoot)) {
3068
+ const state = loadState(projectRoot);
3069
+ roundId = state.currentRound;
3070
+ } else {
3071
+ roundId = 1;
3072
+ }
3073
+ const workflow = getWorkflow(roundId);
3074
+ if (!workflow) {
3075
+ console.log(chalk2.red(`
3076
+ No workflow defined for Round ${roundId}.
3077
+ `));
3078
+ return;
3079
+ }
3080
+ const statuses = detectParents(projectRoot);
3081
+ const installed = new Set(statuses.filter((s) => s.installed).map((s) => s.framework.id));
3082
+ const needed = workflow.steps.map((s) => s.framework);
3083
+ const missingForRound = [...new Set(needed)].filter((fw) => !installed.has(fw));
3084
+ if (missingForRound.length > 0) {
3085
+ console.log(chalk2.yellow(`
3086
+ Missing frameworks for Round ${roundId}: ${missingForRound.join(", ")}`));
3087
+ console.log(chalk2.dim(" Fallback instructions will be shown for missing frameworks.\n"));
3088
+ }
3089
+ displayWorkflow(workflow, installed);
3090
+ });
3091
+ program.command("prompt [round]").description("Generate a copy-pasteable prompt for a round (default: current)").action((roundArg) => {
3092
+ const projectRoot = process.cwd();
3093
+ let roundId;
3094
+ if (roundArg) {
3095
+ roundId = parseInt(roundArg, 10);
3096
+ } else if (stateExists(projectRoot)) {
3097
+ const state = loadState(projectRoot);
3098
+ roundId = state.currentRound;
3099
+ } else {
3100
+ roundId = 1;
3101
+ }
3102
+ const workflow = getWorkflow(roundId);
3103
+ if (!workflow) {
3104
+ console.log(chalk2.red(`
3105
+ No workflow defined for Round ${roundId}.
3106
+ `));
3107
+ return;
3108
+ }
3109
+ const statuses = detectParents(projectRoot);
3110
+ const installed = new Set(statuses.filter((s) => s.installed).map((s) => s.framework.id));
3111
+ const prompt = generatePrompt(workflow, installed);
3112
+ console.log(prompt);
3113
+ });
3114
+ function resolveAuditTarget(pathArg) {
3115
+ const cwd = process.cwd();
3116
+ if (pathArg) return resolve3(cwd, pathArg);
3117
+ const srcDir = resolve3(cwd, "src");
3118
+ return existsSync10(srcDir) ? srcDir : cwd;
3119
+ }
3120
+ program.command("slop [path]").description("Scan for AI slop patterns \u2014 works on ANY project (default: src/ or cwd)").action((pathArg) => {
3121
+ const targetDir = resolveAuditTarget(pathArg);
3122
+ const report = runSlopScan(targetDir);
3123
+ displaySlopReport(report);
3124
+ });
3125
+ program.command("score [path]").description("Check design token compliance \u2014 works on ANY project").action((pathArg) => {
3126
+ const targetDir = resolveAuditTarget(pathArg);
3127
+ const report = runDesignScore(targetDir, targetDir);
3128
+ displayDesignScore(report);
3129
+ });
3130
+ program.command("audit [path]").description("AI Slop + Design Score audit \u2014 works on ANY project, no init needed").action((pathArg) => {
3131
+ const targetDir = resolveAuditTarget(pathArg);
3132
+ const slop = runSlopScan(targetDir);
3133
+ const design = runDesignScore(targetDir, targetDir);
3134
+ displayAudit(slop, design);
3135
+ if (slop.matches.length > 0) {
3136
+ displaySlopReport(slop);
3137
+ }
3138
+ if (design.violations.length > 0) {
3139
+ displayDesignScore(design);
3140
+ }
3141
+ });
3142
+ program.parse();