@shrkcrft/cli 0.1.0-alpha.12 → 0.1.0-alpha.13

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.
Files changed (50) hide show
  1. package/dist/audit/knowledge-audit-llm.d.ts +19 -0
  2. package/dist/audit/knowledge-audit-llm.d.ts.map +1 -0
  3. package/dist/audit/knowledge-audit-llm.js +164 -0
  4. package/dist/audit/knowledge-audit.d.ts +61 -0
  5. package/dist/audit/knowledge-audit.d.ts.map +1 -0
  6. package/dist/audit/knowledge-audit.js +203 -0
  7. package/dist/audit/knowledge-fix-plan-llm.d.ts +11 -0
  8. package/dist/audit/knowledge-fix-plan-llm.d.ts.map +1 -0
  9. package/dist/audit/knowledge-fix-plan-llm.js +141 -0
  10. package/dist/audit/knowledge-fix-plan.d.ts +41 -0
  11. package/dist/audit/knowledge-fix-plan.d.ts.map +1 -0
  12. package/dist/audit/knowledge-fix-plan.js +125 -0
  13. package/dist/audit/pipeline-audit-llm.d.ts +11 -0
  14. package/dist/audit/pipeline-audit-llm.d.ts.map +1 -0
  15. package/dist/audit/pipeline-audit-llm.js +134 -0
  16. package/dist/audit/pipeline-audit.d.ts +69 -0
  17. package/dist/audit/pipeline-audit.d.ts.map +1 -0
  18. package/dist/audit/pipeline-audit.js +166 -0
  19. package/dist/audit/templates-audit-llm.d.ts +19 -0
  20. package/dist/audit/templates-audit-llm.d.ts.map +1 -0
  21. package/dist/audit/templates-audit-llm.js +207 -0
  22. package/dist/audit/templates-audit.d.ts +63 -0
  23. package/dist/audit/templates-audit.d.ts.map +1 -0
  24. package/dist/audit/templates-audit.js +171 -0
  25. package/dist/audit/templates-fix-plan-llm.d.ts +19 -0
  26. package/dist/audit/templates-fix-plan-llm.d.ts.map +1 -0
  27. package/dist/audit/templates-fix-plan-llm.js +162 -0
  28. package/dist/audit/templates-fix-plan.d.ts +37 -0
  29. package/dist/audit/templates-fix-plan.d.ts.map +1 -0
  30. package/dist/audit/templates-fix-plan.js +174 -0
  31. package/dist/commands/ai-status.command.d.ts +19 -0
  32. package/dist/commands/ai-status.command.d.ts.map +1 -0
  33. package/dist/commands/ai-status.command.js +94 -0
  34. package/dist/commands/command-catalog.d.ts.map +1 -1
  35. package/dist/commands/command-catalog.js +10 -0
  36. package/dist/commands/doctor.command.d.ts.map +1 -1
  37. package/dist/commands/doctor.command.js +40 -2
  38. package/dist/commands/smart-context.command.d.ts +28 -0
  39. package/dist/commands/smart-context.command.d.ts.map +1 -1
  40. package/dist/commands/smart-context.command.js +762 -1
  41. package/dist/commands/surface.command.d.ts +1 -0
  42. package/dist/commands/surface.command.d.ts.map +1 -1
  43. package/dist/commands/surface.command.js +10 -3
  44. package/dist/commands/template-quality.command.d.ts.map +1 -1
  45. package/dist/commands/template-quality.command.js +39 -3
  46. package/dist/commands/templates.command.d.ts.map +1 -1
  47. package/dist/commands/templates.command.js +37 -2
  48. package/dist/main.d.ts.map +1 -1
  49. package/dist/main.js +40 -18
  50. package/package.json +32 -32
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { spawn, spawnSync } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  import * as nodePath from 'node:path';
4
4
  import { AiMessageRole, buildPromptMessages, EnhancementPipeline, EnhancementStageKind, OllamaProvider, buildDefaultEnhancementStages, selectAiProvider, } from '@shrkcrft/ai';
@@ -10,6 +10,17 @@ import { DeclarationKind, PLAN_CACHE_SCHEMA, PlanCache, SemanticIndex, TaskType,
10
10
  import { SmartContextDetailedPlanSchema, SmartContextExpansionRequestSchema, } from "../schemas/json-schemas.js";
11
11
  import { asJson, header, kv } from "../output/format-output.js";
12
12
  import { printError } from "../output/print-error.js";
13
+ import { buildTemplateAudit, } from "../audit/templates-audit.js";
14
+ import { enrichAuditWithLlm } from "../audit/templates-audit-llm.js";
15
+ import { buildFixPlan, } from "../audit/templates-fix-plan.js";
16
+ import { enrichFixPlanWithLlm } from "../audit/templates-fix-plan-llm.js";
17
+ import { buildAiBlock, renderAiBlockMarkdown } from '@shrkcrft/ai';
18
+ import { buildKnowledgeAudit, } from "../audit/knowledge-audit.js";
19
+ import { enrichKnowledgeAuditWithLlm } from "../audit/knowledge-audit-llm.js";
20
+ import { buildKnowledgeFixPlan, } from "../audit/knowledge-fix-plan.js";
21
+ import { enrichKnowledgeFixPlanWithLlm } from "../audit/knowledge-fix-plan-llm.js";
22
+ import { buildPipelineAudit, buildPipelineFixPlan, } from "../audit/pipeline-audit.js";
23
+ import { enrichPipelineAuditWithLlm } from "../audit/pipeline-audit-llm.js";
13
24
  const SMART_CONTEXT_DIR = nodePath.join('.sharkcraft', 'smart-context');
14
25
  /**
15
26
  * Gemini-backed context enrichment.
@@ -318,12 +329,762 @@ export const smartContextShowCommand = {
318
329
  return 0;
319
330
  },
320
331
  };
332
+ /**
333
+ * `shrk smart-context audit-templates` — local-LLM template audit.
334
+ *
335
+ * Orchestrates the existing deterministic template inspectors
336
+ * (`templates lint` + `templates drift`), dedupes their overlap by
337
+ * (category + message), and — when a local provider is reachable —
338
+ * runs an LLM critique pass per template. Always report-only: no edits
339
+ * to template sources, no plan emission. See
340
+ * docs/smart-context-audit-templates.md for the report contract.
341
+ */
342
+ export const smartContextAuditTemplatesCommand = {
343
+ name: 'audit-templates',
344
+ description: 'Audit user templates with the deterministic inspectors and (when reachable) a local LLM critique pass. Report-only — no edits. `--fix-plan` adds a Claude-targetable fix plan derived from the report.',
345
+ usage: 'shrk smart-context audit-templates [--id <templateId>] [--no-enhance] [--provider auto|ollama|llamacpp] [--model <id>] [--save] [--json] [--fix-plan] [--only-plan]',
346
+ async run(args) {
347
+ const cwd = resolveCwd(args);
348
+ const json = flagBool(args, 'json');
349
+ const save = flagBool(args, 'save');
350
+ const noEnhance = flagBool(args, 'no-enhance');
351
+ const templateId = flagString(args, 'id');
352
+ const providerKind = flagString(args, 'provider');
353
+ const model = flagString(args, 'model');
354
+ const wantFixPlan = flagBool(args, 'fix-plan') || flagBool(args, 'only-plan');
355
+ const onlyPlan = flagBool(args, 'only-plan');
356
+ const inspection = await inspectSharkcraft({ cwd });
357
+ let report = buildTemplateAudit(inspection, templateId ? { templateId } : {});
358
+ if (report.templates.length === 0) {
359
+ if (json) {
360
+ process.stdout.write(asJson(report) + '\n');
361
+ }
362
+ else {
363
+ process.stdout.write(templateId
364
+ ? `No user template with id "${templateId}".\n`
365
+ : 'No user templates registered.\n');
366
+ }
367
+ return templateId ? 1 : 0;
368
+ }
369
+ const selection = noEnhance ? null : selectAiProvider(providerKind);
370
+ if (selection?.provider) {
371
+ if (model)
372
+ selection.provider.configure({ model });
373
+ if (!json) {
374
+ process.stderr.write(`[audit-templates] enriching with provider ${selection.provider.id}…\n`);
375
+ }
376
+ report = await enrichAuditWithLlm(report, {
377
+ provider: selection.provider,
378
+ inspection,
379
+ onPerTemplateError: (id, err) => {
380
+ if (!json) {
381
+ process.stderr.write(`[audit-templates] LLM pass failed for ${id}: ${err.message.slice(0, 120)} — keeping deterministic findings only.\n`);
382
+ }
383
+ },
384
+ });
385
+ }
386
+ else if (!noEnhance && !json) {
387
+ process.stderr.write('[audit-templates] no local LLM reachable — running deterministic-only audit. See `ai.hints` in the output for setup steps.\n');
388
+ }
389
+ report = { ...report, ai: buildAiBlock({ selection, userOptedOut: noEnhance }) };
390
+ let fixPlan = wantFixPlan ? buildFixPlan(report) : null;
391
+ if (fixPlan && selection?.provider) {
392
+ if (!json) {
393
+ process.stderr.write('[audit-templates] sharpening fix plan with LLM suggestions…\n');
394
+ }
395
+ fixPlan = await enrichFixPlanWithLlm(fixPlan, {
396
+ provider: selection.provider,
397
+ inspection,
398
+ onPerTemplateError: (id, err) => {
399
+ if (!json) {
400
+ process.stderr.write(`[audit-templates] LLM fix-plan pass failed for ${id}: ${err.message.slice(0, 120)} — keeping deterministic prompts.\n`);
401
+ }
402
+ },
403
+ });
404
+ }
405
+ if (save) {
406
+ const saved = saveAuditReport(cwd, report, fixPlan);
407
+ if (json) {
408
+ process.stdout.write(asJson({ saved, report, ...(fixPlan ? { fixPlan } : {}) }) + '\n');
409
+ }
410
+ else {
411
+ process.stdout.write(`Audit saved → ${saved.mdPath}\n`);
412
+ process.stdout.write(` → ${saved.jsonPath}\n`);
413
+ if (saved.planMdPath && saved.planJsonPath) {
414
+ process.stdout.write(`Plan saved → ${saved.planMdPath}\n`);
415
+ process.stdout.write(` → ${saved.planJsonPath}\n`);
416
+ }
417
+ }
418
+ return exitCodeForAudit(report);
419
+ }
420
+ if (json) {
421
+ if (onlyPlan && fixPlan) {
422
+ process.stdout.write(asJson(fixPlan) + '\n');
423
+ }
424
+ else if (fixPlan) {
425
+ process.stdout.write(asJson({ report, fixPlan }) + '\n');
426
+ }
427
+ else {
428
+ process.stdout.write(asJson(report) + '\n');
429
+ }
430
+ return exitCodeForAudit(report);
431
+ }
432
+ if (!onlyPlan) {
433
+ process.stdout.write(renderAuditMarkdown(report));
434
+ }
435
+ if (fixPlan) {
436
+ if (!onlyPlan)
437
+ process.stdout.write('\n');
438
+ process.stdout.write(renderFixPlanMarkdown(fixPlan));
439
+ }
440
+ return exitCodeForAudit(report);
441
+ },
442
+ };
443
+ function exitCodeForAudit(report) {
444
+ if (report.summary.broken > 0)
445
+ return 1;
446
+ return 0;
447
+ }
448
+ function saveAuditReport(cwd, report, fixPlan) {
449
+ const dir = nodePath.join(cwd, SMART_CONTEXT_DIR);
450
+ mkdirSync(dir, { recursive: true });
451
+ const slug = report.auditId;
452
+ const mdPath = nodePath.join(dir, `${slug}.md`);
453
+ const jsonPath = nodePath.join(dir, `${slug}.json`);
454
+ writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
455
+ writeFileSync(mdPath, renderAuditMarkdown(report), 'utf8');
456
+ if (!fixPlan)
457
+ return { slug, mdPath, jsonPath };
458
+ const planSlug = fixPlan.fixPlanId;
459
+ const planMdPath = nodePath.join(dir, `${planSlug}.md`);
460
+ const planJsonPath = nodePath.join(dir, `${planSlug}.json`);
461
+ writeFileSync(planJsonPath, JSON.stringify(fixPlan, null, 2), 'utf8');
462
+ writeFileSync(planMdPath, renderFixPlanMarkdown(fixPlan), 'utf8');
463
+ return { slug, mdPath, jsonPath, planMdPath, planJsonPath };
464
+ }
465
+ function renderAuditMarkdown(report) {
466
+ const out = [];
467
+ out.push(`# Template audit — ${report.auditId}`);
468
+ out.push('');
469
+ out.push(`- generated: ${report.generatedAt}`);
470
+ out.push(`- llm enriched: ${report.llmEnriched ? `yes (${report.llmProviderId ?? 'unknown'})` : 'no — deterministic only'}`);
471
+ out.push(`- summary: ok=${report.summary.ok}, minor=${report.summary.minor}, stale=${report.summary.stale}, broken=${report.summary.broken} (total ${report.summary.total})`);
472
+ if (report.skipped.length > 0) {
473
+ out.push(`- skipped: ${report.skipped.length} (${report.skipped.map((s) => s.templateId).join(', ')})`);
474
+ }
475
+ out.push('');
476
+ const order = ['broken', 'stale', 'minor', 'ok'];
477
+ for (const verdict of order) {
478
+ const inGroup = report.templates.filter((t) => t.verdict === verdict);
479
+ if (inGroup.length === 0)
480
+ continue;
481
+ out.push(`## ${verdict.toUpperCase()} (${inGroup.length})`);
482
+ out.push('');
483
+ for (const entry of inGroup) {
484
+ out.push(`### \`${entry.templateId}\` — ${entry.templateName}`);
485
+ out.push(`usage: ${entry.usage}`);
486
+ if (entry.deterministicFindings.length === 0 && entry.llmFindings.length === 0) {
487
+ out.push('No findings.');
488
+ out.push('');
489
+ continue;
490
+ }
491
+ if (entry.deterministicFindings.length > 0) {
492
+ out.push('');
493
+ out.push('Findings:');
494
+ for (const f of entry.deterministicFindings) {
495
+ out.push(`- **[deterministic]** ${f.severity} \`${f.category}\` — ${f.message} _(sources: ${f.sources.join(', ')})_`);
496
+ if (f.suggestion)
497
+ out.push(` - ↳ ${f.suggestion}`);
498
+ }
499
+ }
500
+ if (entry.llmFindings.length > 0) {
501
+ out.push('');
502
+ out.push('LLM-flagged (advisory):');
503
+ for (const f of entry.llmFindings) {
504
+ out.push(`- **[llm]** ${f.severity} \`${f.category}\` (confidence ${f.confidence.toFixed(2)}) — ${f.message}`);
505
+ }
506
+ }
507
+ if (entry.suggestedActions.length > 0) {
508
+ out.push('');
509
+ out.push('Suggested actions:');
510
+ for (const a of entry.suggestedActions) {
511
+ out.push(`- \`${a.kind}\` ${a.target} — ${a.note}`);
512
+ }
513
+ }
514
+ out.push('');
515
+ }
516
+ }
517
+ if (report.ai) {
518
+ out.push(renderAiBlockMarkdown(report.ai));
519
+ }
520
+ return out.join('\n') + '\n';
521
+ }
522
+ function renderFixPlanMarkdown(plan) {
523
+ const out = [];
524
+ out.push(`# Template fix plan — ${plan.fixPlanId}`);
525
+ out.push('');
526
+ out.push(`- generated: ${plan.generatedAt}`);
527
+ out.push(`- derived from audit: ${plan.auditId}`);
528
+ out.push(`- source files Claude will edit: ${plan.sourceFiles.join(', ')}`);
529
+ out.push(`- summary: ${plan.summary.fixCount} fix(es) — high=${plan.summary.highConfidence}, medium=${plan.summary.mediumConfidence}, low=${plan.summary.lowConfidence}; skipped=${plan.summary.skipped}`);
530
+ out.push('');
531
+ if (plan.fixes.length === 0) {
532
+ out.push('No fix instructions emitted.');
533
+ out.push('');
534
+ }
535
+ else {
536
+ const order = ['high', 'medium', 'low'];
537
+ for (const confidence of order) {
538
+ const inGroup = plan.fixes.filter((f) => f.confidence === confidence);
539
+ if (inGroup.length === 0)
540
+ continue;
541
+ out.push(`## Confidence: ${confidence.toUpperCase()} (${inGroup.length})`);
542
+ out.push('');
543
+ for (const fix of inGroup) {
544
+ out.push(`### \`${fix.templateId}\` — \`${fix.findingCategory}\` _(${fix.source}, ${fix.severity})_`);
545
+ out.push(`**Intent.** ${fix.intent}`);
546
+ out.push('');
547
+ out.push(`Original finding: ${fix.finding}`);
548
+ out.push('');
549
+ out.push('Agent prompt:');
550
+ out.push('```');
551
+ out.push(fix.agentPrompt);
552
+ out.push('```');
553
+ if (fix.llmSuggestion) {
554
+ out.push('');
555
+ out.push('LLM suggestion (advisory):');
556
+ out.push('> ' + fix.llmSuggestion.split('\n').join('\n> '));
557
+ }
558
+ out.push('');
559
+ }
560
+ }
561
+ }
562
+ if (plan.skipped.length > 0) {
563
+ out.push(`## Skipped (${plan.skipped.length})`);
564
+ out.push('');
565
+ for (const s of plan.skipped) {
566
+ out.push(`- \`${s.templateId}\` / \`${s.findingCategory}\` — ${s.reason}`);
567
+ out.push(` - finding: ${s.finding}`);
568
+ }
569
+ out.push('');
570
+ }
571
+ return out.join('\n') + '\n';
572
+ }
573
+ /**
574
+ * `shrk smart-context audit-knowledge` — local-LLM knowledge audit.
575
+ *
576
+ * Wraps `lintKnowledge` + `buildKnowledgeStaleReport` from `@shrkcrft/inspector`,
577
+ * then layers LLM critique (when a provider is reachable) and emits a
578
+ * Claude-targetable fix plan. Report-only — no writes to knowledge sources.
579
+ * See docs/smart-context-audit-templates.md for the shared report contract.
580
+ */
581
+ export const smartContextAuditKnowledgeCommand = {
582
+ name: 'audit-knowledge',
583
+ description: 'Audit user knowledge entries with the deterministic inspectors (lint + stale-reference check) and (when reachable) a local LLM critique pass. Report-only — no edits.',
584
+ usage: 'shrk smart-context audit-knowledge [--id <entryId>] [--no-enhance] [--no-stale-check] [--provider auto|ollama|llamacpp] [--model <id>] [--save] [--json] [--fix-plan] [--only-plan]',
585
+ async run(args) {
586
+ const cwd = resolveCwd(args);
587
+ const json = flagBool(args, 'json');
588
+ const save = flagBool(args, 'save');
589
+ const noEnhance = flagBool(args, 'no-enhance');
590
+ const noStaleCheck = flagBool(args, 'no-stale-check');
591
+ const entryId = flagString(args, 'id');
592
+ const providerKind = flagString(args, 'provider');
593
+ const model = flagString(args, 'model');
594
+ const wantFixPlan = flagBool(args, 'fix-plan') || flagBool(args, 'only-plan');
595
+ const onlyPlan = flagBool(args, 'only-plan');
596
+ const inspection = await inspectSharkcraft({ cwd });
597
+ let report = buildKnowledgeAudit(inspection, {
598
+ ...(entryId ? { entryId } : {}),
599
+ ...(noStaleCheck ? { skipStaleCheck: true } : {}),
600
+ });
601
+ if (report.entries.length === 0) {
602
+ if (json) {
603
+ process.stdout.write(asJson(report) + '\n');
604
+ }
605
+ else {
606
+ process.stdout.write(entryId
607
+ ? `No user knowledge entry with id "${entryId}".\n`
608
+ : 'No user knowledge entries registered.\n');
609
+ }
610
+ return entryId ? 1 : 0;
611
+ }
612
+ const selection = noEnhance ? null : selectAiProvider(providerKind);
613
+ if (selection?.provider) {
614
+ if (model)
615
+ selection.provider.configure({ model });
616
+ if (!json) {
617
+ process.stderr.write(`[audit-knowledge] enriching with provider ${selection.provider.id}…\n`);
618
+ }
619
+ report = await enrichKnowledgeAuditWithLlm(report, {
620
+ provider: selection.provider,
621
+ inspection,
622
+ onPerEntryError: (id, err) => {
623
+ if (!json) {
624
+ process.stderr.write(`[audit-knowledge] LLM pass failed for ${id}: ${err.message.slice(0, 120)} — keeping deterministic findings only.\n`);
625
+ }
626
+ },
627
+ });
628
+ }
629
+ else if (!noEnhance && !json) {
630
+ process.stderr.write('[audit-knowledge] no local LLM reachable — running deterministic-only audit. See `ai.hints` in the output for setup steps.\n');
631
+ }
632
+ report = { ...report, ai: buildAiBlock({ selection, userOptedOut: noEnhance }) };
633
+ let fixPlan = wantFixPlan ? buildKnowledgeFixPlan(report) : null;
634
+ if (fixPlan && selection?.provider) {
635
+ if (!json) {
636
+ process.stderr.write('[audit-knowledge] sharpening fix plan with LLM suggestions…\n');
637
+ }
638
+ fixPlan = await enrichKnowledgeFixPlanWithLlm(fixPlan, {
639
+ provider: selection.provider,
640
+ inspection,
641
+ onPerEntryError: (id, err) => {
642
+ if (!json) {
643
+ process.stderr.write(`[audit-knowledge] LLM fix-plan pass failed for ${id}: ${err.message.slice(0, 120)} — keeping deterministic prompts.\n`);
644
+ }
645
+ },
646
+ });
647
+ }
648
+ if (save) {
649
+ const saved = saveKnowledgeAuditReport(cwd, report, fixPlan);
650
+ if (json) {
651
+ process.stdout.write(asJson({ saved, report, ...(fixPlan ? { fixPlan } : {}) }) + '\n');
652
+ }
653
+ else {
654
+ process.stdout.write(`Audit saved → ${saved.mdPath}\n`);
655
+ process.stdout.write(` → ${saved.jsonPath}\n`);
656
+ if (saved.planMdPath && saved.planJsonPath) {
657
+ process.stdout.write(`Plan saved → ${saved.planMdPath}\n`);
658
+ process.stdout.write(` → ${saved.planJsonPath}\n`);
659
+ }
660
+ }
661
+ return exitCodeForKnowledgeAudit(report);
662
+ }
663
+ if (json) {
664
+ if (onlyPlan && fixPlan) {
665
+ process.stdout.write(asJson(fixPlan) + '\n');
666
+ }
667
+ else if (fixPlan) {
668
+ process.stdout.write(asJson({ report, fixPlan }) + '\n');
669
+ }
670
+ else {
671
+ process.stdout.write(asJson(report) + '\n');
672
+ }
673
+ return exitCodeForKnowledgeAudit(report);
674
+ }
675
+ if (!onlyPlan) {
676
+ process.stdout.write(renderKnowledgeAuditMarkdown(report));
677
+ }
678
+ if (fixPlan) {
679
+ if (!onlyPlan)
680
+ process.stdout.write('\n');
681
+ process.stdout.write(renderKnowledgeFixPlanMarkdown(fixPlan));
682
+ }
683
+ return exitCodeForKnowledgeAudit(report);
684
+ },
685
+ };
686
+ function exitCodeForKnowledgeAudit(report) {
687
+ if (report.summary.broken > 0)
688
+ return 1;
689
+ return 0;
690
+ }
691
+ function saveKnowledgeAuditReport(cwd, report, fixPlan) {
692
+ const dir = nodePath.join(cwd, SMART_CONTEXT_DIR);
693
+ mkdirSync(dir, { recursive: true });
694
+ const slug = `knowledge-${report.auditId}`;
695
+ const mdPath = nodePath.join(dir, `${slug}.md`);
696
+ const jsonPath = nodePath.join(dir, `${slug}.json`);
697
+ writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
698
+ writeFileSync(mdPath, renderKnowledgeAuditMarkdown(report), 'utf8');
699
+ if (!fixPlan)
700
+ return { slug, mdPath, jsonPath };
701
+ const planSlug = `knowledge-${fixPlan.fixPlanId}`;
702
+ const planMdPath = nodePath.join(dir, `${planSlug}.md`);
703
+ const planJsonPath = nodePath.join(dir, `${planSlug}.json`);
704
+ writeFileSync(planJsonPath, JSON.stringify(fixPlan, null, 2), 'utf8');
705
+ writeFileSync(planMdPath, renderKnowledgeFixPlanMarkdown(fixPlan), 'utf8');
706
+ return { slug, mdPath, jsonPath, planMdPath, planJsonPath };
707
+ }
708
+ function renderKnowledgeAuditMarkdown(report) {
709
+ const out = [];
710
+ out.push(`# Knowledge audit — ${report.auditId}`);
711
+ out.push('');
712
+ out.push(`- generated: ${report.generatedAt}`);
713
+ out.push(`- llm enriched: ${report.llmEnriched ? `yes (${report.llmProviderId ?? 'unknown'})` : 'no — deterministic only'}`);
714
+ out.push(`- summary: ok=${report.summary.ok}, minor=${report.summary.minor}, stale=${report.summary.stale}, broken=${report.summary.broken} (total ${report.summary.total})`);
715
+ if (report.skipped.length > 0) {
716
+ out.push(`- skipped: ${report.skipped.length} (pack-contributed)`);
717
+ }
718
+ out.push('');
719
+ const order = ['broken', 'stale', 'minor', 'ok'];
720
+ for (const verdict of order) {
721
+ const inGroup = report.entries.filter((t) => t.verdict === verdict);
722
+ if (inGroup.length === 0)
723
+ continue;
724
+ out.push(`## ${verdict.toUpperCase()} (${inGroup.length})`);
725
+ out.push('');
726
+ for (const entry of inGroup) {
727
+ out.push(`### \`${entry.entryId}\` (${entry.entryType}) — ${entry.title}`);
728
+ if (entry.deterministicFindings.length === 0 && entry.llmFindings.length === 0) {
729
+ out.push('No findings.');
730
+ out.push('');
731
+ continue;
732
+ }
733
+ if (entry.deterministicFindings.length > 0) {
734
+ out.push('');
735
+ out.push('Findings:');
736
+ for (const f of entry.deterministicFindings) {
737
+ out.push(`- **[deterministic]** ${f.severity} \`${f.category}\` (${f.field}) — ${f.message} _(sources: ${f.sources.join(', ')})_`);
738
+ if (f.fixSuggestion)
739
+ out.push(` - ↳ ${f.fixSuggestion}`);
740
+ if (f.stubSuggestion)
741
+ out.push(` - stub: ${f.stubSuggestion}`);
742
+ }
743
+ }
744
+ if (entry.llmFindings.length > 0) {
745
+ out.push('');
746
+ out.push('LLM-flagged (advisory):');
747
+ for (const f of entry.llmFindings) {
748
+ out.push(`- **[llm]** ${f.severity} \`${f.category}\` (confidence ${f.confidence.toFixed(2)}) — ${f.message}`);
749
+ }
750
+ }
751
+ if (entry.suggestedActions.length > 0) {
752
+ out.push('');
753
+ out.push('Suggested actions:');
754
+ for (const a of entry.suggestedActions) {
755
+ out.push(`- \`${a.kind}\` ${a.target} — ${a.note}`);
756
+ }
757
+ }
758
+ out.push('');
759
+ }
760
+ }
761
+ if (report.ai) {
762
+ out.push(renderAiBlockMarkdown(report.ai));
763
+ }
764
+ return out.join('\n') + '\n';
765
+ }
766
+ function renderKnowledgeFixPlanMarkdown(plan) {
767
+ const out = [];
768
+ out.push(`# Knowledge fix plan — ${plan.fixPlanId}`);
769
+ out.push('');
770
+ out.push(`- generated: ${plan.generatedAt}`);
771
+ out.push(`- derived from audit: ${plan.auditId}`);
772
+ out.push(`- source hint: ${plan.sourceHint}`);
773
+ out.push(`- summary: ${plan.summary.fixCount} fix(es) — high=${plan.summary.highConfidence}, medium=${plan.summary.mediumConfidence}, low=${plan.summary.lowConfidence}; skipped=${plan.summary.skipped}`);
774
+ out.push('');
775
+ if (plan.fixes.length === 0) {
776
+ out.push('No fix instructions emitted.');
777
+ out.push('');
778
+ }
779
+ else {
780
+ const order = ['high', 'medium', 'low'];
781
+ for (const confidence of order) {
782
+ const inGroup = plan.fixes.filter((f) => f.confidence === confidence);
783
+ if (inGroup.length === 0)
784
+ continue;
785
+ out.push(`## Confidence: ${confidence.toUpperCase()} (${inGroup.length})`);
786
+ out.push('');
787
+ for (const fix of inGroup) {
788
+ out.push(`### \`${fix.entryId}\` — \`${fix.findingCategory}\` _(${fix.source}, ${fix.severity})_`);
789
+ out.push(`**Intent.** ${fix.intent}`);
790
+ out.push('');
791
+ out.push(`Original finding: ${fix.finding}`);
792
+ out.push('');
793
+ out.push('Agent prompt:');
794
+ out.push('```');
795
+ out.push(fix.agentPrompt);
796
+ out.push('```');
797
+ if (fix.llmSuggestion) {
798
+ out.push('');
799
+ out.push('LLM suggestion (advisory):');
800
+ out.push('> ' + fix.llmSuggestion.split('\n').join('\n> '));
801
+ }
802
+ out.push('');
803
+ }
804
+ }
805
+ }
806
+ if (plan.skipped.length > 0) {
807
+ out.push(`## Skipped (${plan.skipped.length})`);
808
+ out.push('');
809
+ for (const s of plan.skipped) {
810
+ out.push(`- \`${s.entryId}\` / \`${s.findingCategory}\` — ${s.reason}`);
811
+ out.push(` - finding: ${s.finding}`);
812
+ }
813
+ out.push('');
814
+ }
815
+ return out.join('\n') + '\n';
816
+ }
817
+ /**
818
+ * `shrk smart-context audit-pipelines` — local-LLM pipeline audit.
819
+ *
820
+ * Wraps `lintPipelines` from `@shrkcrft/inspector`, optionally layers
821
+ * LLM critique, and emits a Claude-targetable fix plan. Same report-only
822
+ * contract as audit-templates / audit-knowledge.
823
+ */
824
+ export const smartContextAuditPipelinesCommand = {
825
+ name: 'audit-pipelines',
826
+ description: 'Audit registered pipelines with the deterministic inspector and (when reachable) a local LLM critique pass. Report-only — no edits.',
827
+ usage: 'shrk smart-context audit-pipelines [--id <pipelineId>] [--no-enhance] [--provider auto|ollama|llamacpp] [--model <id>] [--save] [--json] [--fix-plan] [--only-plan]',
828
+ async run(args) {
829
+ const cwd = resolveCwd(args);
830
+ const json = flagBool(args, 'json');
831
+ const save = flagBool(args, 'save');
832
+ const noEnhance = flagBool(args, 'no-enhance');
833
+ const pipelineId = flagString(args, 'id');
834
+ const providerKind = flagString(args, 'provider');
835
+ const model = flagString(args, 'model');
836
+ const wantFixPlan = flagBool(args, 'fix-plan') || flagBool(args, 'only-plan');
837
+ const onlyPlan = flagBool(args, 'only-plan');
838
+ const inspection = await inspectSharkcraft({ cwd });
839
+ let report = buildPipelineAudit(inspection, pipelineId ? { pipelineId } : {});
840
+ if (report.pipelines.length === 0) {
841
+ if (json) {
842
+ process.stdout.write(asJson(report) + '\n');
843
+ }
844
+ else {
845
+ process.stdout.write(pipelineId
846
+ ? `No pipeline with id "${pipelineId}".\n`
847
+ : 'No pipelines registered.\n');
848
+ }
849
+ return pipelineId ? 1 : 0;
850
+ }
851
+ const selection = noEnhance ? null : selectAiProvider(providerKind);
852
+ if (selection?.provider) {
853
+ if (model)
854
+ selection.provider.configure({ model });
855
+ if (!json) {
856
+ process.stderr.write(`[audit-pipelines] enriching with provider ${selection.provider.id}…\n`);
857
+ }
858
+ report = await enrichPipelineAuditWithLlm(report, {
859
+ provider: selection.provider,
860
+ inspection,
861
+ onPerPipelineError: (id, err) => {
862
+ if (!json) {
863
+ process.stderr.write(`[audit-pipelines] LLM pass failed for ${id}: ${err.message.slice(0, 120)} — keeping deterministic findings only.\n`);
864
+ }
865
+ },
866
+ });
867
+ }
868
+ else if (!noEnhance && !json) {
869
+ process.stderr.write('[audit-pipelines] no local LLM reachable — running deterministic-only audit. See `ai.hints` in the output for setup steps.\n');
870
+ }
871
+ report = { ...report, ai: buildAiBlock({ selection, userOptedOut: noEnhance }) };
872
+ const fixPlan = wantFixPlan ? buildPipelineFixPlan(report) : null;
873
+ if (save) {
874
+ const saved = savePipelineAuditReport(cwd, report, fixPlan);
875
+ if (json) {
876
+ process.stdout.write(asJson({ saved, report, ...(fixPlan ? { fixPlan } : {}) }) + '\n');
877
+ }
878
+ else {
879
+ process.stdout.write(`Audit saved → ${saved.mdPath}\n`);
880
+ process.stdout.write(` → ${saved.jsonPath}\n`);
881
+ if (saved.planMdPath && saved.planJsonPath) {
882
+ process.stdout.write(`Plan saved → ${saved.planMdPath}\n`);
883
+ process.stdout.write(` → ${saved.planJsonPath}\n`);
884
+ }
885
+ }
886
+ return exitCodeForPipelineAudit(report);
887
+ }
888
+ if (json) {
889
+ if (onlyPlan && fixPlan) {
890
+ process.stdout.write(asJson(fixPlan) + '\n');
891
+ }
892
+ else if (fixPlan) {
893
+ process.stdout.write(asJson({ report, fixPlan }) + '\n');
894
+ }
895
+ else {
896
+ process.stdout.write(asJson(report) + '\n');
897
+ }
898
+ return exitCodeForPipelineAudit(report);
899
+ }
900
+ if (!onlyPlan) {
901
+ process.stdout.write(renderPipelineAuditMarkdown(report));
902
+ }
903
+ if (fixPlan) {
904
+ if (!onlyPlan)
905
+ process.stdout.write('\n');
906
+ process.stdout.write(renderPipelineFixPlanMarkdown(fixPlan));
907
+ }
908
+ return exitCodeForPipelineAudit(report);
909
+ },
910
+ };
911
+ function exitCodeForPipelineAudit(report) {
912
+ if (report.summary.broken > 0)
913
+ return 1;
914
+ return 0;
915
+ }
916
+ function savePipelineAuditReport(cwd, report, fixPlan) {
917
+ const dir = nodePath.join(cwd, SMART_CONTEXT_DIR);
918
+ mkdirSync(dir, { recursive: true });
919
+ const slug = `pipelines-${report.auditId}`;
920
+ const mdPath = nodePath.join(dir, `${slug}.md`);
921
+ const jsonPath = nodePath.join(dir, `${slug}.json`);
922
+ writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
923
+ writeFileSync(mdPath, renderPipelineAuditMarkdown(report), 'utf8');
924
+ if (!fixPlan)
925
+ return { slug, mdPath, jsonPath };
926
+ const planSlug = `pipelines-${fixPlan.fixPlanId}`;
927
+ const planMdPath = nodePath.join(dir, `${planSlug}.md`);
928
+ const planJsonPath = nodePath.join(dir, `${planSlug}.json`);
929
+ writeFileSync(planJsonPath, JSON.stringify(fixPlan, null, 2), 'utf8');
930
+ writeFileSync(planMdPath, renderPipelineFixPlanMarkdown(fixPlan), 'utf8');
931
+ return { slug, mdPath, jsonPath, planMdPath, planJsonPath };
932
+ }
933
+ function renderPipelineAuditMarkdown(report) {
934
+ const out = [];
935
+ out.push(`# Pipeline audit — ${report.auditId}`);
936
+ out.push('');
937
+ out.push(`- generated: ${report.generatedAt}`);
938
+ out.push(`- llm enriched: ${report.llmEnriched ? `yes (${report.llmProviderId ?? 'unknown'})` : 'no — deterministic only'}`);
939
+ out.push(`- summary: ok=${report.summary.ok}, minor=${report.summary.minor}, stale=${report.summary.stale}, broken=${report.summary.broken} (total ${report.summary.total})`);
940
+ out.push('');
941
+ const order = ['broken', 'stale', 'minor', 'ok'];
942
+ for (const verdict of order) {
943
+ const inGroup = report.pipelines.filter((p) => p.verdict === verdict);
944
+ if (inGroup.length === 0)
945
+ continue;
946
+ out.push(`## ${verdict.toUpperCase()} (${inGroup.length})`);
947
+ out.push('');
948
+ for (const entry of inGroup) {
949
+ out.push(`### \`${entry.pipelineId}\``);
950
+ if (entry.deterministicFindings.length === 0 && entry.llmFindings.length === 0) {
951
+ out.push('No findings.');
952
+ out.push('');
953
+ continue;
954
+ }
955
+ if (entry.deterministicFindings.length > 0) {
956
+ out.push('');
957
+ out.push('Findings:');
958
+ for (const f of entry.deterministicFindings) {
959
+ out.push(`- **[deterministic]** ${f.severity} \`${f.category}\`${f.stepId ? ` (step "${f.stepId}")` : ''} — ${f.message} _(sources: ${f.sources.join(', ')})_`);
960
+ }
961
+ }
962
+ if (entry.llmFindings.length > 0) {
963
+ out.push('');
964
+ out.push('LLM-flagged (advisory):');
965
+ for (const f of entry.llmFindings) {
966
+ out.push(`- **[llm]** ${f.severity} \`${f.category}\` (confidence ${f.confidence.toFixed(2)}) — ${f.message}`);
967
+ }
968
+ }
969
+ out.push('');
970
+ }
971
+ }
972
+ if (report.ai) {
973
+ out.push(renderAiBlockMarkdown(report.ai));
974
+ }
975
+ return out.join('\n') + '\n';
976
+ }
977
+ function renderPipelineFixPlanMarkdown(plan) {
978
+ const out = [];
979
+ out.push(`# Pipeline fix plan — ${plan.fixPlanId}`);
980
+ out.push('');
981
+ out.push(`- generated: ${plan.generatedAt}`);
982
+ out.push(`- derived from audit: ${plan.auditId}`);
983
+ out.push(`- source hint: ${plan.sourceHint}`);
984
+ out.push(`- summary: ${plan.summary.fixCount} fix(es) — high=${plan.summary.highConfidence}, medium=${plan.summary.mediumConfidence}, low=${plan.summary.lowConfidence}`);
985
+ out.push('');
986
+ if (plan.fixes.length === 0) {
987
+ out.push('No fix instructions emitted.');
988
+ out.push('');
989
+ return out.join('\n') + '\n';
990
+ }
991
+ const order = ['high', 'medium', 'low'];
992
+ for (const confidence of order) {
993
+ const inGroup = plan.fixes.filter((f) => f.confidence === confidence);
994
+ if (inGroup.length === 0)
995
+ continue;
996
+ out.push(`## Confidence: ${confidence.toUpperCase()} (${inGroup.length})`);
997
+ out.push('');
998
+ for (const fix of inGroup) {
999
+ out.push(`### \`${fix.pipelineId}\` — \`${fix.findingCategory}\` _(${fix.source}, ${fix.severity})_`);
1000
+ out.push(`**Intent.** ${fix.intent}`);
1001
+ out.push('');
1002
+ out.push(`Original finding: ${fix.finding}`);
1003
+ out.push('');
1004
+ out.push('Agent prompt:');
1005
+ out.push('```');
1006
+ out.push(fix.agentPrompt);
1007
+ out.push('```');
1008
+ out.push('');
1009
+ }
1010
+ }
1011
+ return out.join('\n') + '\n';
1012
+ }
1013
+ // Patterns matching ONNX worker-thread teardown noise that surfaces
1014
+ // AFTER a successful embeddings-build. `pipeline.dispose()` returns
1015
+ // cleanly but `onnxruntime-node`'s worker pool isn't actually joined;
1016
+ // when the main thread exits, the workers briefly outlive it and hit a
1017
+ // pthread mutex teardown race. The libc++abi message is the user-visible
1018
+ // symptom. Filtered here so the child's exit doesn't pollute the user's
1019
+ // terminal — exit code is preserved.
1020
+ const EMBEDDINGS_NOISE_PATTERNS = [
1021
+ /^libc\+\+abi: terminating due to uncaught exception of type std::__1::system_error: mutex lock failed/,
1022
+ ];
1023
+ function isEmbeddingsCleanupNoise(line) {
1024
+ return EMBEDDINGS_NOISE_PATTERNS.some((p) => p.test(line));
1025
+ }
1026
+ /**
1027
+ * Run `embeddings-build` in an isolated child process and filter
1028
+ * known cleanup noise from its stderr. See EMBEDDINGS_NOISE_PATTERNS
1029
+ * for the rationale.
1030
+ *
1031
+ * Implementation: re-exec the same CLI binary (`process.execPath` +
1032
+ * `process.argv.slice(1)`) with `SHRK_EMBEDDINGS_WORKER=1` in the env.
1033
+ * The child sees the env flag, skips this wrapper, and runs the
1034
+ * indexing inline. The parent pipes child stdout through unchanged
1035
+ * (so JSON output + result line flow as-is) and filters child stderr
1036
+ * line-by-line before forwarding.
1037
+ *
1038
+ * Trust model: the child's exit code is the source of truth. Even if
1039
+ * the child aborts during cleanup, `reallyExit(code)` in main.ts has
1040
+ * already set the kernel-visible exit code before the abort. We
1041
+ * surface that code verbatim.
1042
+ */
1043
+ function runEmbeddingsBuildInChild() {
1044
+ return new Promise((resolve) => {
1045
+ const child = spawn(process.execPath, process.argv.slice(1), {
1046
+ env: { ...process.env, SHRK_EMBEDDINGS_WORKER: '1' },
1047
+ stdio: ['inherit', 'pipe', 'pipe'],
1048
+ });
1049
+ child.stdout.pipe(process.stdout);
1050
+ let stderrBuf = '';
1051
+ const flushLine = (line) => {
1052
+ if (isEmbeddingsCleanupNoise(line))
1053
+ return;
1054
+ process.stderr.write(line + '\n');
1055
+ };
1056
+ child.stderr.on('data', (chunk) => {
1057
+ stderrBuf += chunk.toString('utf8');
1058
+ let idx;
1059
+ while ((idx = stderrBuf.indexOf('\n')) !== -1) {
1060
+ flushLine(stderrBuf.slice(0, idx));
1061
+ stderrBuf = stderrBuf.slice(idx + 1);
1062
+ }
1063
+ });
1064
+ child.on('error', (err) => {
1065
+ process.stderr.write(`Failed to spawn embeddings worker: ${err.message}\n`);
1066
+ resolve(1);
1067
+ });
1068
+ child.on('close', (code) => {
1069
+ if (stderrBuf.length > 0 && !isEmbeddingsCleanupNoise(stderrBuf)) {
1070
+ process.stderr.write(stderrBuf);
1071
+ }
1072
+ resolve(typeof code === 'number' ? code : 1);
1073
+ });
1074
+ });
1075
+ }
321
1076
  /** `shrk smart-context embeddings build` — (re)build the semantic index. */
322
1077
  export const smartContextEmbeddingsBuildCommand = {
323
1078
  name: 'embeddings-build',
324
1079
  description: 'Build or incrementally refresh the semantic index. Defaults to incremental updates when an index already exists; pass --rebuild for a full rebuild.',
325
1080
  usage: 'shrk smart-context embeddings-build [--model <hf-id>] [--root <dir>]... [--max-files N] [--rebuild] [--json]',
326
1081
  async run(args) {
1082
+ // Top-level: if we're the parent, re-exec ourselves in worker mode
1083
+ // and filter the resulting cleanup noise. The child path (env flag
1084
+ // set) runs the original code below inline.
1085
+ if (process.env.SHRK_EMBEDDINGS_WORKER !== '1') {
1086
+ return runEmbeddingsBuildInChild();
1087
+ }
327
1088
  const cwd = resolveCwd(args);
328
1089
  const model = flagString(args, 'model');
329
1090
  const maxFiles = flagNumber(args, 'max-files') ?? 5000;