@kernel.chat/kbot 3.3.1 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,646 @@
1
+ // kbot Bootstrap — Outer-loop optimizer for any project
2
+ //
3
+ // Most tools help you build. Bootstrap helps you be seen.
4
+ // It measures the gap between what your project IS and what the world PERCEIVES,
5
+ // then tells you exactly what to fix — highest impact first.
6
+ //
7
+ // The bootstrap pattern:
8
+ // 1. Sense — measure surfaces (README, npm, GitHub, docs)
9
+ // 2. Score — grade each dimension of visibility
10
+ // 3. Gap — identify the biggest delta between capability and perception
11
+ // 4. Act — recommend (or execute) the single highest-impact fix
12
+ // 5. Record — log the run so the next one starts from a higher floor
13
+ //
14
+ // This is not a feature tool. It's the meta-tool that makes features matter.
15
+ //
16
+ // Reference: Hernandez, I. (2026). "The Bootstrap Pattern: Outer-Loop
17
+ // Optimization for Open Source Projects." kernel.chat/bootstrap
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { join, basename } from 'node:path';
20
+ import { execSync } from 'node:child_process';
21
+ import chalk from 'chalk';
22
+ // ── Helpers ──
23
+ function execQuiet(cmd, timeoutMs = 5000) {
24
+ try {
25
+ return execSync(cmd, { encoding: 'utf-8', timeout: timeoutMs, stdio: 'pipe', cwd: process.cwd() }).trim();
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function fileExists(path) {
32
+ return existsSync(join(process.cwd(), path));
33
+ }
34
+ function readFile(path) {
35
+ const full = join(process.cwd(), path);
36
+ try {
37
+ return readFileSync(full, 'utf-8');
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function gradeFromPercent(pct) {
44
+ if (pct >= 90)
45
+ return 'A';
46
+ if (pct >= 80)
47
+ return 'B';
48
+ if (pct >= 70)
49
+ return 'C';
50
+ if (pct >= 60)
51
+ return 'D';
52
+ return 'F';
53
+ }
54
+ // ── GitHub API ──
55
+ async function githubRepoData(repo) {
56
+ try {
57
+ const res = await fetch(`https://api.github.com/repos/${repo}`, {
58
+ headers: { 'User-Agent': 'kbot-bootstrap/1.0', Accept: 'application/vnd.github.v3+json' },
59
+ });
60
+ if (!res.ok)
61
+ return null;
62
+ return res.json();
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ // ── npm API ──
69
+ async function npmData(pkg) {
70
+ try {
71
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
72
+ if (!res.ok)
73
+ return null;
74
+ return res.json();
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ async function npmDownloads(pkg) {
81
+ try {
82
+ const [weekRes, dayRes] = await Promise.all([
83
+ fetch(`https://api.npmjs.org/downloads/point/last-week/${pkg}`),
84
+ fetch(`https://api.npmjs.org/downloads/point/last-day/${pkg}`),
85
+ ]);
86
+ if (!weekRes.ok || !dayRes.ok)
87
+ return null;
88
+ const [week, day] = await Promise.all([weekRes.json(), dayRes.json()]);
89
+ return { weekly: week.downloads, daily: day.downloads };
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ // ── Sections ──
96
+ async function checkFirstImpression() {
97
+ const section = {
98
+ name: 'First Impression',
99
+ score: 0,
100
+ maxScore: 25,
101
+ findings: [],
102
+ status: 'pass',
103
+ };
104
+ // README exists and has substance
105
+ const readme = readFile('README.md');
106
+ if (!readme) {
107
+ section.findings.push('No README.md');
108
+ section.fix = 'Create a README.md — this is the first thing anyone sees';
109
+ section.status = 'fail';
110
+ return section;
111
+ }
112
+ section.score += 2;
113
+ section.findings.push('README.md exists');
114
+ // Length check
115
+ if (readme.length > 2000) {
116
+ section.score += 3;
117
+ section.findings.push(`README is substantial (${(readme.length / 1024).toFixed(1)}KB)`);
118
+ }
119
+ else if (readme.length > 500) {
120
+ section.score += 1;
121
+ section.findings.push('README is short — consider expanding');
122
+ }
123
+ else {
124
+ section.findings.push('README is too short (under 500 chars)');
125
+ section.fix = 'Expand your README — explain what this does and why someone should care';
126
+ }
127
+ // Has a GIF/image/screenshot
128
+ const hasImage = /!\[.*\]\(.*\.(gif|png|jpg|jpeg|svg|webp)/i.test(readme) ||
129
+ /<img\s+src=/i.test(readme);
130
+ if (hasImage) {
131
+ section.score += 5;
132
+ section.findings.push('Has visual demo (GIF/image)');
133
+ }
134
+ else {
135
+ section.findings.push('No visual demo — GIFs increase star conversion 5-10x');
136
+ if (!section.fix)
137
+ section.fix = 'Add a GIF or screenshot to your README — visual demos dramatically increase engagement';
138
+ }
139
+ // Install command
140
+ const hasInstall = /npm (install|i)\s|pip install|cargo install|brew install|go install|curl.*install/i.test(readme);
141
+ if (hasInstall) {
142
+ section.score += 3;
143
+ section.findings.push('Has install command');
144
+ }
145
+ else {
146
+ section.findings.push('No install command in README');
147
+ if (!section.fix)
148
+ section.fix = 'Add a one-line install command near the top of your README';
149
+ }
150
+ // Quick start / usage examples
151
+ const hasUsage = /quick\s*start|usage|example|getting\s*started/i.test(readme);
152
+ if (hasUsage) {
153
+ section.score += 3;
154
+ section.findings.push('Has usage/quickstart section');
155
+ }
156
+ else {
157
+ section.findings.push('No usage examples');
158
+ }
159
+ // Badges
160
+ const badgeCount = (readme.match(/\[!\[.*\]\(.*\)\]\(.*\)/g) || []).length +
161
+ (readme.match(/<img src="https:\/\/img\.shields\.io/g) || []).length;
162
+ if (badgeCount >= 3) {
163
+ section.score += 3;
164
+ section.findings.push(`${badgeCount} badges`);
165
+ }
166
+ else if (badgeCount > 0) {
167
+ section.score += 1;
168
+ section.findings.push(`Only ${badgeCount} badge(s) — consider adding version, license, downloads`);
169
+ }
170
+ else {
171
+ section.findings.push('No badges');
172
+ }
173
+ // Comparison table
174
+ const hasComparison = /\|.*\|.*\|.*\n\|.*---.*\|/m.test(readme) &&
175
+ /compar|vs\b|alternative/i.test(readme);
176
+ if (hasComparison) {
177
+ section.score += 3;
178
+ section.findings.push('Has comparison table');
179
+ }
180
+ // Architecture/how it works
181
+ const hasArchitecture = /architect|how it works|under the hood|design/i.test(readme);
182
+ if (hasArchitecture) {
183
+ section.score += 3;
184
+ section.findings.push('Has architecture/design section');
185
+ }
186
+ return section;
187
+ }
188
+ async function checkDistribution(packageJson) {
189
+ const section = {
190
+ name: 'Distribution',
191
+ score: 0,
192
+ maxScore: 25,
193
+ findings: [],
194
+ status: 'pass',
195
+ };
196
+ // package.json exists
197
+ if (!packageJson) {
198
+ const hasPkg = fileExists('package.json') || fileExists('Cargo.toml') ||
199
+ fileExists('pyproject.toml') || fileExists('go.mod');
200
+ if (hasPkg) {
201
+ section.score += 2;
202
+ section.findings.push('Has package manifest');
203
+ }
204
+ else {
205
+ section.findings.push('No package manifest found');
206
+ section.fix = 'Add a package manifest (package.json, Cargo.toml, etc.) to make your project installable';
207
+ section.status = 'warn';
208
+ }
209
+ return section;
210
+ }
211
+ section.score += 2;
212
+ section.findings.push('package.json exists');
213
+ const name = packageJson.name;
214
+ // Published to npm
215
+ if (name) {
216
+ const npm = await npmData(name);
217
+ if (npm) {
218
+ section.score += 5;
219
+ section.findings.push(`Published on npm: ${name}`);
220
+ // Downloads
221
+ const dl = await npmDownloads(name);
222
+ if (dl) {
223
+ if (dl.weekly > 1000) {
224
+ section.score += 5;
225
+ section.findings.push(`${dl.weekly.toLocaleString()}/week downloads`);
226
+ }
227
+ else if (dl.weekly > 100) {
228
+ section.score += 3;
229
+ section.findings.push(`${dl.weekly.toLocaleString()}/week downloads — growing`);
230
+ }
231
+ else {
232
+ section.score += 1;
233
+ section.findings.push(`${dl.weekly.toLocaleString()}/week downloads — low`);
234
+ if (!section.fix)
235
+ section.fix = 'Downloads are low — focus on launch posts (HN, Reddit, Twitter) to drive awareness';
236
+ }
237
+ }
238
+ }
239
+ else {
240
+ section.findings.push('Not published on npm');
241
+ if (!section.fix)
242
+ section.fix = 'Publish to npm — `npm publish --access public`';
243
+ }
244
+ }
245
+ // npm description
246
+ const desc = packageJson.description;
247
+ if (desc && desc.length > 50) {
248
+ section.score += 3;
249
+ section.findings.push('Has detailed npm description');
250
+ }
251
+ else if (desc) {
252
+ section.score += 1;
253
+ section.findings.push('npm description is short — expand it for better search ranking');
254
+ }
255
+ else {
256
+ section.findings.push('No npm description');
257
+ }
258
+ // Keywords
259
+ const keywords = packageJson.keywords;
260
+ if (keywords && keywords.length >= 10) {
261
+ section.score += 3;
262
+ section.findings.push(`${keywords.length} npm keywords`);
263
+ }
264
+ else if (keywords && keywords.length > 0) {
265
+ section.score += 1;
266
+ section.findings.push(`Only ${keywords.length} keywords — more = better npm search ranking`);
267
+ }
268
+ else {
269
+ section.findings.push('No npm keywords');
270
+ }
271
+ // Docker
272
+ if (fileExists('Dockerfile')) {
273
+ section.score += 3;
274
+ section.findings.push('Has Dockerfile');
275
+ }
276
+ // Install script
277
+ if (fileExists('install.sh') || fileExists('install.ps1')) {
278
+ section.score += 2;
279
+ section.findings.push('Has install script');
280
+ }
281
+ // Homebrew
282
+ const readme = readFile('README.md') || '';
283
+ if (/brew install/i.test(readme)) {
284
+ section.score += 2;
285
+ section.findings.push('Has Homebrew formula');
286
+ }
287
+ return section;
288
+ }
289
+ async function checkGitHubPresence() {
290
+ const section = {
291
+ name: 'GitHub Presence',
292
+ score: 0,
293
+ maxScore: 25,
294
+ findings: [],
295
+ status: 'pass',
296
+ };
297
+ // Detect GitHub repo
298
+ const remoteUrl = execQuiet('git remote get-url origin');
299
+ if (!remoteUrl) {
300
+ section.findings.push('No git remote — cannot check GitHub presence');
301
+ section.status = 'warn';
302
+ section.fix = 'Push your project to GitHub';
303
+ return section;
304
+ }
305
+ // Extract owner/repo
306
+ const match = remoteUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
307
+ if (!match) {
308
+ section.findings.push('Remote is not GitHub');
309
+ section.score += 2;
310
+ return section;
311
+ }
312
+ const repo = `${match[1]}/${match[2]}`;
313
+ section.findings.push(`GitHub: ${repo}`);
314
+ const data = await githubRepoData(repo);
315
+ if (!data) {
316
+ section.findings.push('Could not fetch GitHub data (rate limited or private)');
317
+ section.score += 2;
318
+ return section;
319
+ }
320
+ // Stars
321
+ const stars = data.stargazers_count || 0;
322
+ if (stars >= 100) {
323
+ section.score += 8;
324
+ section.findings.push(`${stars} stars — strong social proof`);
325
+ }
326
+ else if (stars >= 10) {
327
+ section.score += 4;
328
+ section.findings.push(`${stars} stars — building momentum`);
329
+ }
330
+ else {
331
+ section.score += 1;
332
+ section.findings.push(`${stars} star(s) — the first impression isn't converting to stars`);
333
+ if (!section.fix)
334
+ section.fix = 'Stars are low — improve README visual (add GIF), then post to HN/Reddit/Twitter';
335
+ }
336
+ // Description
337
+ if (data.description) {
338
+ section.score += 3;
339
+ section.findings.push('Has GitHub description');
340
+ }
341
+ else {
342
+ section.findings.push('No GitHub description');
343
+ if (!section.fix)
344
+ section.fix = 'Add a GitHub repo description — it appears in search results';
345
+ }
346
+ // Topics
347
+ if (data.topics?.length >= 5) {
348
+ section.score += 3;
349
+ section.findings.push(`${data.topics.length} topics`);
350
+ }
351
+ else if (data.topics?.length > 0) {
352
+ section.score += 1;
353
+ section.findings.push(`Only ${data.topics.length} topic(s) — add more for discoverability`);
354
+ }
355
+ else {
356
+ section.findings.push('No topics — these help people find your project');
357
+ }
358
+ // License
359
+ if (data.license) {
360
+ section.score += 2;
361
+ section.findings.push(`License: ${data.license.spdx_id}`);
362
+ }
363
+ else {
364
+ section.findings.push('No license — many developers won\'t use unlicensed projects');
365
+ if (!section.fix)
366
+ section.fix = 'Add a LICENSE file (MIT is most common for open source)';
367
+ }
368
+ // Activity
369
+ const daysAgo = (Date.now() - new Date(data.pushed_at).getTime()) / 86400000;
370
+ if (daysAgo < 7) {
371
+ section.score += 3;
372
+ section.findings.push(`Active — last push ${Math.floor(daysAgo)}d ago`);
373
+ }
374
+ else if (daysAgo < 30) {
375
+ section.score += 2;
376
+ section.findings.push(`Last push ${Math.floor(daysAgo)}d ago`);
377
+ }
378
+ else {
379
+ section.findings.push(`Stale — last push ${Math.floor(daysAgo)}d ago`);
380
+ section.status = 'warn';
381
+ }
382
+ // Community files
383
+ const communityFiles = ['CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', '.github/ISSUE_TEMPLATE'];
384
+ let communityCount = 0;
385
+ for (const f of communityFiles) {
386
+ if (fileExists(f))
387
+ communityCount++;
388
+ }
389
+ if (communityCount >= 2) {
390
+ section.score += 3;
391
+ section.findings.push(`${communityCount}/3 community files`);
392
+ }
393
+ else if (communityCount > 0) {
394
+ section.score += 1;
395
+ section.findings.push(`${communityCount}/3 community files — add CONTRIBUTING.md and CODE_OF_CONDUCT.md`);
396
+ }
397
+ // Forks and watchers
398
+ if (data.forks_count > 0) {
399
+ section.score += 2;
400
+ section.findings.push(`${data.forks_count} fork(s)`);
401
+ }
402
+ // Clone-to-star ratio
403
+ if (stars > 0 && data.forks_count > 0) {
404
+ const ratio = stars / (data.forks_count + stars);
405
+ section.findings.push(`Clone-to-star ratio: ${(ratio * 100).toFixed(1)}%`);
406
+ }
407
+ return section;
408
+ }
409
+ function checkSurfaceCoherence(packageJson) {
410
+ const section = {
411
+ name: 'Surface Coherence',
412
+ score: 0,
413
+ maxScore: 25,
414
+ findings: [],
415
+ status: 'pass',
416
+ };
417
+ const readme = readFile('README.md') || '';
418
+ const pkg = packageJson || {};
419
+ // Version consistency
420
+ const pkgVersion = pkg.version;
421
+ if (pkgVersion) {
422
+ const versionInReadme = readme.includes(pkgVersion);
423
+ if (versionInReadme) {
424
+ section.score += 3;
425
+ section.findings.push(`README mentions current version (${pkgVersion})`);
426
+ }
427
+ else {
428
+ section.findings.push(`README doesn't mention version ${pkgVersion}`);
429
+ }
430
+ }
431
+ // Description consistency
432
+ const pkgDesc = pkg.description;
433
+ if (pkgDesc && readme.length > 100) {
434
+ // Check if key phrases from npm description appear in README
435
+ const keyWords = pkgDesc.split(/\s+/).filter(w => w.length > 5).slice(0, 5);
436
+ const matchCount = keyWords.filter(w => readme.toLowerCase().includes(w.toLowerCase())).length;
437
+ if (matchCount >= 3) {
438
+ section.score += 3;
439
+ section.findings.push('npm description aligns with README');
440
+ }
441
+ else {
442
+ section.findings.push('npm description and README tell different stories');
443
+ if (!section.fix)
444
+ section.fix = 'Align your npm description with your README — they should tell the same story';
445
+ }
446
+ }
447
+ // Changelog / What's New
448
+ const hasChangelog = fileExists('CHANGELOG.md') || /what.s new|changelog|release/i.test(readme);
449
+ if (hasChangelog) {
450
+ section.score += 3;
451
+ section.findings.push('Has changelog or "What\'s New" section');
452
+ }
453
+ else {
454
+ section.findings.push('No changelog — users want to know what changed');
455
+ }
456
+ // ROADMAP
457
+ if (fileExists('ROADMAP.md') || /roadmap/i.test(readme)) {
458
+ section.score += 3;
459
+ section.findings.push('Has roadmap');
460
+ }
461
+ // Links consistency
462
+ const links = [];
463
+ if (/npmjs\.com/i.test(readme))
464
+ links.push('npm');
465
+ if (/github\.com/i.test(readme))
466
+ links.push('GitHub');
467
+ if (/discord/i.test(readme))
468
+ links.push('Discord');
469
+ if (/twitter\.com|x\.com/i.test(readme))
470
+ links.push('Twitter/X');
471
+ if (links.length >= 3) {
472
+ section.score += 3;
473
+ section.findings.push(`${links.length} links: ${links.join(', ')}`);
474
+ }
475
+ else if (links.length > 0) {
476
+ section.score += 1;
477
+ section.findings.push(`Only ${links.length} link(s) — add npm, GitHub, Discord, Twitter`);
478
+ }
479
+ else {
480
+ section.findings.push('No community links in README');
481
+ }
482
+ // SEO files
483
+ if (fileExists('robots.txt')) {
484
+ section.score += 1;
485
+ section.findings.push('Has robots.txt');
486
+ }
487
+ if (fileExists('sitemap.xml')) {
488
+ section.score += 1;
489
+ section.findings.push('Has sitemap.xml');
490
+ }
491
+ // Count surface files that mention the project name
492
+ const name = pkg.name;
493
+ if (name) {
494
+ const surfaceFiles = ['README.md', 'CONTRIBUTING.md', 'ROADMAP.md', 'package.json'];
495
+ const existing = surfaceFiles.filter(f => fileExists(f));
496
+ section.score += Math.min(4, existing.length);
497
+ section.findings.push(`${existing.length}/${surfaceFiles.length} surface files present`);
498
+ }
499
+ else {
500
+ section.score += 2;
501
+ }
502
+ // Staleness detection
503
+ const gitLog = execQuiet('git log -1 --format=%H -- README.md');
504
+ const lastCommit = execQuiet('git log -1 --format=%H');
505
+ if (gitLog && lastCommit) {
506
+ const readmeAge = execQuiet('git log -1 --format=%cr -- README.md');
507
+ if (readmeAge) {
508
+ section.findings.push(`README last updated: ${readmeAge}`);
509
+ if (/month|year/i.test(readmeAge)) {
510
+ section.findings.push('README may be stale — review for accuracy');
511
+ if (!section.fix)
512
+ section.fix = 'Your README hasn\'t been updated recently — review it for stale numbers, versions, and feature lists';
513
+ }
514
+ else {
515
+ section.score += 4;
516
+ }
517
+ }
518
+ }
519
+ return section;
520
+ }
521
+ // ── Main ──
522
+ export async function runBootstrap() {
523
+ const projectName = basename(process.cwd());
524
+ // Load package.json if it exists
525
+ let packageJson = null;
526
+ try {
527
+ packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
528
+ }
529
+ catch { /* not a Node project */ }
530
+ // Run all checks in parallel where possible
531
+ const [firstImpression, distribution, githubPresence] = await Promise.all([
532
+ checkFirstImpression(),
533
+ checkDistribution(packageJson),
534
+ checkGitHubPresence(),
535
+ ]);
536
+ // Surface coherence is sync (reads local files only)
537
+ const surfaceCoherence = checkSurfaceCoherence(packageJson);
538
+ const sections = [firstImpression, distribution, githubPresence, surfaceCoherence];
539
+ // Calculate totals
540
+ const totalScore = sections.reduce((sum, s) => sum + s.score, 0);
541
+ const totalMax = sections.reduce((sum, s) => sum + s.maxScore, 0);
542
+ const pct = Math.round((totalScore / totalMax) * 100);
543
+ const grade = gradeFromPercent(pct);
544
+ // Set section statuses
545
+ for (const s of sections) {
546
+ const sPct = s.maxScore > 0 ? (s.score / s.maxScore) * 100 : 0;
547
+ if (s.status !== 'fail') {
548
+ s.status = sPct >= 60 ? 'pass' : 'warn';
549
+ }
550
+ }
551
+ // Find the top fix — lowest-scoring section with a fix
552
+ const withFixes = sections.filter(s => s.fix).sort((a, b) => {
553
+ const aPct = a.score / a.maxScore;
554
+ const bPct = b.score / b.maxScore;
555
+ return aPct - bPct; // lowest percentage first = highest impact
556
+ });
557
+ const topFix = withFixes[0]?.fix || 'All sections look good — focus on sharing your project';
558
+ // Summary
559
+ const fails = sections.filter(s => s.status === 'fail');
560
+ const warns = sections.filter(s => s.status === 'warn');
561
+ const summary = fails.length > 0
562
+ ? `${fails.length} critical gap(s). Your project is invisible in ${fails.map(s => s.name.toLowerCase()).join(', ')}.`
563
+ : warns.length > 0
564
+ ? `${warns.length} area(s) need work. Fix the top recommendation to compound.`
565
+ : 'Strong visibility. Focus on distribution — post, share, submit to lists.';
566
+ return {
567
+ project: packageJson?.name || projectName,
568
+ score: totalScore,
569
+ maxScore: totalMax,
570
+ grade,
571
+ sections,
572
+ topFix,
573
+ summary,
574
+ timestamp: new Date().toISOString(),
575
+ };
576
+ }
577
+ // ── Formatting ──
578
+ export function formatBootstrapReport(report) {
579
+ const statusIcon = (s) => s === 'pass' ? '✅' : s === 'warn' ? '⚠️' : '❌';
580
+ const pct = Math.round((report.score / report.maxScore) * 100);
581
+ const lines = [];
582
+ // Header
583
+ lines.push('');
584
+ lines.push(chalk.bold(` kbot Bootstrap — ${report.project}`));
585
+ lines.push(chalk.dim(` ──────────────────────────────────────────────────`));
586
+ lines.push('');
587
+ lines.push(` ${chalk.bold('Score:')} ${report.score}/${report.maxScore} (${pct}%) — Grade ${chalk.bold(report.grade)}`);
588
+ lines.push(` ${chalk.dim(report.summary)}`);
589
+ lines.push('');
590
+ // Sections
591
+ for (const section of report.sections) {
592
+ const sPct = section.maxScore > 0 ? Math.round((section.score / section.maxScore) * 100) : 0;
593
+ const icon = section.status === 'pass' ? chalk.green('✓') : section.status === 'warn' ? chalk.yellow('!') : chalk.red('✗');
594
+ lines.push(` ${icon} ${chalk.bold(section.name)}${chalk.dim(` — ${section.score}/${section.maxScore} (${sPct}%)`)}`);
595
+ for (const f of section.findings) {
596
+ lines.push(` ${chalk.dim('·')} ${f}`);
597
+ }
598
+ if (section.fix && section.status !== 'pass') {
599
+ lines.push(` ${chalk.yellow('→')} ${chalk.yellow(section.fix)}`);
600
+ }
601
+ lines.push('');
602
+ }
603
+ // Top fix
604
+ lines.push(chalk.dim(` ──────────────────────────────────────────────────`));
605
+ lines.push(` ${chalk.bold('Top fix:')} ${report.topFix}`);
606
+ lines.push('');
607
+ lines.push(chalk.dim(` The bootstrap pattern: close the gap between what your project IS`));
608
+ lines.push(chalk.dim(` and what the world PERCEIVES. Fix one thing per run. Compound.`));
609
+ lines.push('');
610
+ return lines.join('\n');
611
+ }
612
+ export function formatBootstrapMarkdown(report) {
613
+ const statusIcon = (s) => s === 'pass' ? '✅' : s === 'warn' ? '⚠️' : '❌';
614
+ const pct = Math.round((report.score / report.maxScore) * 100);
615
+ const lines = [
616
+ `# Bootstrap Report: ${report.project}`,
617
+ '',
618
+ `> Generated by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — the outer-loop optimizer`,
619
+ '',
620
+ `## Score: ${report.score}/${report.maxScore} (${pct}%) — Grade ${report.grade}`,
621
+ '',
622
+ `**${report.summary}**`,
623
+ '',
624
+ ];
625
+ for (const section of report.sections) {
626
+ const sPct = section.maxScore > 0 ? Math.round((section.score / section.maxScore) * 100) : 0;
627
+ lines.push(`### ${statusIcon(section.status)} ${section.name} — ${section.score}/${section.maxScore} (${sPct}%)`);
628
+ for (const f of section.findings) {
629
+ lines.push(`- ${f}`);
630
+ }
631
+ if (section.fix && section.status !== 'pass') {
632
+ lines.push(`- **Fix:** ${section.fix}`);
633
+ }
634
+ lines.push('');
635
+ }
636
+ lines.push('---');
637
+ lines.push('');
638
+ lines.push(`**Top fix:** ${report.topFix}`);
639
+ lines.push('');
640
+ lines.push(`*The bootstrap pattern: close the gap between what your project IS and what the world PERCEIVES.*`);
641
+ lines.push(`*Fix one thing per run. Compound.*`);
642
+ lines.push('');
643
+ lines.push(`*[kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 22 agents, 284 tools, 20 providers*`);
644
+ return lines.join('\n');
645
+ }
646
+ //# sourceMappingURL=bootstrap.js.map