baldart 4.99.0 → 5.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +1 -0
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/CHANGELOG.md +82 -0
  5. package/framework/.claude/agents/REGISTRY.md +3 -1
  6. package/framework/.claude/agents/code-reviewer.md +84 -400
  7. package/framework/.claude/agents/codebase-architect.md +71 -356
  8. package/framework/.claude/agents/coder.md +131 -291
  9. package/framework/.claude/agents/doc-reviewer.md +126 -450
  10. package/framework/.claude/agents/plan-auditor.md +137 -436
  11. package/framework/.claude/agents/qa-sentinel.md +59 -313
  12. package/framework/.claude/agents/skill-improver.md +60 -4
  13. package/framework/.claude/agents/ui-expert.md +104 -591
  14. package/framework/.claude/hooks/agent-discovery-gate.js +2 -2
  15. package/framework/.claude/hooks/agent-discovery-info.sh +1 -0
  16. package/framework/.claude/skills/new/CHANGELOG.md +11 -0
  17. package/framework/.claude/skills/new/SKILL.md +1 -1
  18. package/framework/.claude/skills/new/references/codex-gate.md +25 -11
  19. package/framework/.claude/skills/new/references/implement.md +43 -3
  20. package/framework/.claude/skills/new/references/review-cycle.md +16 -4
  21. package/framework/.claude/skills/new/scripts/build-review-bundle.mjs +104 -0
  22. package/framework/.claude/skills/new/scripts/doc-invariants.mjs +163 -0
  23. package/framework/.claude/skills/new2/CHANGELOG.md +4 -0
  24. package/framework/.claude/skills/new2/SKILL.md +5 -1
  25. package/framework/.claude/workflows/new2-resolve.js +10 -3
  26. package/framework/.claude/workflows/new2.js +51 -3
  27. package/framework/agents/agent-operating-protocol.md +152 -0
  28. package/framework/agents/doc-audit-protocol.md +227 -0
  29. package/framework/agents/index.md +4 -0
  30. package/framework/agents/review-protocol.md +207 -0
  31. package/framework/routines/finding-mine.routine.yml +56 -0
  32. package/framework/routines/index.yml +11 -0
  33. package/package.json +1 -1
  34. package/src/commands/configure.js +15 -0
  35. package/src/commands/doctor.js +69 -2
  36. package/src/utils/agent-slots.js +109 -0
  37. package/src/utils/overlay-merger.js +17 -8
  38. package/src/utils/symlinks.js +93 -33
@@ -30,7 +30,10 @@ const CONFLICT_LOG = path.join('.baldart', 'skill-conflicts.json');
30
30
  // kind name doesn't pluralize to the on-disk dir — e.g. `outputStyle` lives in
31
31
  // the hyphenated `output-styles/` dir Claude Code requires, not `outputStyles/`.
32
32
  const MERGE_KINDS = {
33
- agent: { dirGetter: 'agentsDir', ext: '.md', overlay: true, exclude: new Set(['REGISTRY.md']) },
33
+ // - slots: `.md` sources may carry `{{#flag}}` conditionals resolved at
34
+ // install-time from baldart.config.yml (v5.0.0 — see agent-slots.js).
35
+ // A slot-bearing source is GENERATED per-consumer even without an overlay.
36
+ agent: { dirGetter: 'agentsDir', ext: '.md', overlay: true, slots: true, exclude: new Set(['REGISTRY.md', 'CHANGELOG.md']) },
34
37
  command: { dirGetter: 'commandsDir', ext: '.md', overlay: true, exclude: new Set(['REGISTRY.md']) },
35
38
  workflow: { dirGetter: 'workflowsDir', ext: '.js', overlay: false, exclude: new Set() },
36
39
  outputStyle: { dirGetter: 'outputStylesDir', ext: '.md', overlay: false, exclude: new Set(), srcDir: 'output-styles' },
@@ -415,6 +418,28 @@ class SymlinkUtils {
415
418
 
416
419
  const frameworkVersion = this._readFrameworkVersion();
417
420
 
421
+ // v5.0.0 — install-time slot resolution (kinds with cfg.slots). Flags are
422
+ // resolved ONCE per merge run from baldart.config.yml; only sources that
423
+ // actually carry `{{#flag}}` syntax pay the fill (others stay symlinks).
424
+ let slotEnv = null;
425
+ const slotFillFor = (fwAbsolute) => {
426
+ if (!cfg.slots) return null;
427
+ const raw = fs.readFileSync(fwAbsolute, 'utf8');
428
+ const slots = require('./agent-slots');
429
+ if (!slots.hasAgentSlots(raw)) return null;
430
+ if (!slotEnv) {
431
+ const flags = slots.resolveAgentFlags(slots.loadAgentConfig(this.cwd));
432
+ slotEnv = { flags, configSha: slots.computeAgentConfigSha(flags) };
433
+ }
434
+ const filled = slots.fillAgentSlots(raw, slotEnv.flags);
435
+ return {
436
+ content: filled.content,
437
+ residual: filled.residual,
438
+ configSha: slotEnv.configSha,
439
+ rawBaseSha: require('./overlay-merger').shortSha(raw),
440
+ };
441
+ };
442
+
418
443
  for (const tool of tools) {
419
444
  const adapter = getAdapter(tool, this.cwd);
420
445
  const dirGetter = adapter[cfg.dirGetter];
@@ -438,14 +463,25 @@ class SymlinkUtils {
438
463
  const overlayAbs = path.join(this.cwd, overlayRel);
439
464
  const hasOverlay = cfg.overlay && fs.existsSync(overlayAbs);
440
465
 
466
+ // Slot fill (v5.0.0): resolved BEFORE overlay merge and BEFORE transpile,
467
+ // so both consume the config-effective markdown. A leftover `{{…}}` after
468
+ // the fill is an authoring error — surfaced, never silently stripped.
469
+ const slotFill = slotFillFor(fwAbsolute);
470
+ if (slotFill && slotFill.residual.length) {
471
+ const w = `unresolved slot token(s) after fill: ${slotFill.residual.join(' ')} — check the flag names in the framework source`;
472
+ UI.warning(`[${adapter.label}] ${kind} ${baseName}: ${w}`);
473
+ aggregate.warnings.push({ tool: adapter.name, kind, name: baseName, warning: w });
474
+ }
475
+
441
476
  // Transpile path: when the adapter declares a transpiler for this kind
442
477
  // (e.g. Codex agents → TOML), do NOT symlink the `.md` source — generate
443
- // the tool-native file from it (overlay-merged first when present).
478
+ // the tool-native file from it (slot-filled + overlay-merged first).
444
479
  const transpiler = (typeof adapter.transpilerFor === 'function') ? adapter.transpilerFor(kind) : null;
445
480
  if (transpiler) {
446
481
  this._installTranspiledFile({
447
482
  kind, baseName, fwAbsolute, overlayAbs, hasOverlay,
448
- targetRel, transpiler, adapter, frameworkVersion, aggregate
483
+ targetRel, transpiler, adapter, frameworkVersion, aggregate,
484
+ baseContentOverride: slotFill ? slotFill.content : null,
449
485
  });
450
486
  continue;
451
487
  }
@@ -454,11 +490,14 @@ class SymlinkUtils {
454
490
  let lstat = null;
455
491
  try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }
456
492
 
457
- if (hasOverlay) {
458
- // Generate a real file from base + overlay.
493
+ if (hasOverlay || slotFill) {
494
+ // Generate a real file: slot-filled base + (optional) overlay.
459
495
  this._generateOverlayedFile({
460
496
  kind, name: baseName, fwAbsolute, overlayAbs, linkPath, lstat,
461
- frameworkVersion, aggregate, adapter, targetRel
497
+ frameworkVersion, aggregate, adapter, targetRel, hasOverlay,
498
+ baseContentOverride: slotFill ? slotFill.content : null,
499
+ configSha: slotFill ? slotFill.configSha : null,
500
+ baseShaOverride: slotFill ? slotFill.rawBaseSha : null,
462
501
  });
463
502
  } else {
464
503
  // Plain symlink, with broken-symlink recovery (same fix-pattern as v3.5.1).
@@ -562,13 +601,14 @@ class SymlinkUtils {
562
601
  * `# baldart-generated:` marker so a later update can safely regenerate it
563
602
  * and a user fork (no marker) is never clobbered.
564
603
  */
565
- _installTranspiledFile({ kind, baseName, fwAbsolute, overlayAbs, hasOverlay, targetRel, transpiler, adapter, frameworkVersion, aggregate }) {
604
+ _installTranspiledFile({ kind, baseName, fwAbsolute, overlayAbs, hasOverlay, targetRel, transpiler, adapter, frameworkVersion, aggregate, baseContentOverride = null }) {
566
605
  const outName = `${baseName}${transpiler.outExt}`;
567
606
  const outPath = path.join(this.cwd, targetRel, outName);
568
607
  const relOut = path.join(targetRel, outName);
569
608
 
570
- // Effective markdown = overlay-merged when an overlay exists, else the base.
571
- const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
609
+ // Effective markdown = slot-filled (v5.0.0, when the source carries {{#flag}})
610
+ // then overlay-merged when an overlay exists, else the base.
611
+ const baseContent = baseContentOverride != null ? baseContentOverride : fs.readFileSync(fwAbsolute, 'utf8');
572
612
  let effectiveMd = baseContent;
573
613
  if (hasOverlay) {
574
614
  try {
@@ -621,27 +661,33 @@ class SymlinkUtils {
621
661
  aggregate.generated.push({ tool: adapter.name, kind, name: baseName });
622
662
  }
623
663
 
624
- _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
664
+ _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel, hasOverlay = true, baseContentOverride = null, configSha = null, baseShaOverride = null }) {
625
665
  const { mergeOverlay, isGeneratedFile, computeBaseFileSha, ensureBaseFileShaInOverlay, refreshOverlayVersionPin } = require('./overlay-merger');
626
- const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
627
- const baseSha = computeBaseFileSha(baseContent);
628
- // Backfill base_file_sha into the overlay frontmatter so `overlay drift` is
629
- // deterministic for EVERY overlay, not just freshly-scaffolded ones
630
- // (additive-only, never overwrites). Done BEFORE reading overlayContent so
631
- // the marker's overlay_sha already reflects it — no one-time regen churn.
632
- // (v3.33.0)
633
- try {
634
- const bf = ensureBaseFileShaInOverlay(overlayAbs, baseSha);
635
- if (bf.changed) UI.info(`[${adapter.label}] ${kind} ${name}: backfilled base_file_sha into overlay frontmatter.`);
636
- // Keep the informational base_<kind>_version pin truthfulbut ONLY when
637
- // the base content is unchanged (sha matches). When the base HAS changed
638
- // the sha differs and the pin is left stale on purpose, so it stays a
639
- // legitimate drift signal to reconcile. (v4.11.0)
640
- const vp = refreshOverlayVersionPin(overlayAbs, kind, frameworkVersion, baseSha);
641
- if (vp.changed) UI.info(`[${adapter.label}] ${kind} ${name}: refreshed base_${kind}_version pin to v${frameworkVersion} (base unchanged).`);
642
- } catch (_) { /* non-fatal never block a merge on backfill */ }
643
- const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
644
- const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion });
666
+ // baseContentOverride = the slot-filled markdown (v5.0.0). The overlay pin
667
+ // and the marker's base_sha stay anchored to the RAW framework source
668
+ // (baseShaOverride) so upstream drift and config drift are separate signals.
669
+ const baseContent = baseContentOverride != null ? baseContentOverride : fs.readFileSync(fwAbsolute, 'utf8');
670
+ const baseSha = baseShaOverride || computeBaseFileSha(baseContent);
671
+ let overlayContent = '';
672
+ if (hasOverlay) {
673
+ // Backfill base_file_sha into the overlay frontmatter so `overlay drift` is
674
+ // deterministic for EVERY overlay, not just freshly-scaffolded ones
675
+ // (additive-only, never overwrites). Done BEFORE reading overlayContent so
676
+ // the marker's overlay_sha already reflects it no one-time regen churn.
677
+ // (v3.33.0)
678
+ try {
679
+ const bf = ensureBaseFileShaInOverlay(overlayAbs, baseSha);
680
+ if (bf.changed) UI.info(`[${adapter.label}] ${kind} ${name}: backfilled base_file_sha into overlay frontmatter.`);
681
+ // Keep the informational base_<kind>_version pin truthful but ONLY when
682
+ // the base content is unchanged (sha matches). When the base HAS changed
683
+ // the sha differs and the pin is left stale on purpose, so it stays a
684
+ // legitimate drift signal to reconcile. (v4.11.0)
685
+ const vp = refreshOverlayVersionPin(overlayAbs, kind, frameworkVersion, baseSha);
686
+ if (vp.changed) UI.info(`[${adapter.label}] ${kind} ${name}: refreshed base_${kind}_version pin to v${frameworkVersion} (base unchanged).`);
687
+ } catch (_) { /* non-fatal — never block a merge on backfill */ }
688
+ overlayContent = fs.readFileSync(overlayAbs, 'utf8');
689
+ }
690
+ const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion, configSha, baseShaOverride: baseSha });
645
691
 
646
692
  if (merged.warnings && merged.warnings.length) {
647
693
  merged.warnings.forEach((w) => {
@@ -684,20 +730,34 @@ class SymlinkUtils {
684
730
  // Legacy generated file at linkPath — migrate (delete in-place, rewrite at generatedPath, symlink).
685
731
  }
686
732
 
687
- // Write merged content to the indirect location.
688
- fs.writeFileSync(generatedPath, merged.generated);
689
-
690
733
  // Compute the relative symlink target (portable across moves of the
691
734
  // project root).
692
735
  const relativeTarget = path.relative(path.dirname(linkPath), generatedPath);
693
736
 
737
+ // Byte-stability (v5.0.0): when the generated content is unchanged AND the
738
+ // consumer-visible symlink already points at it, this is a no-op — skip
739
+ // silently so repeated `baldart update` runs produce zero churn.
740
+ try {
741
+ if (fs.existsSync(generatedPath)
742
+ && fs.readFileSync(generatedPath, 'utf8') === merged.generated
743
+ && lstat && lstat.isSymbolicLink()
744
+ && path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath)) === generatedPath) {
745
+ aggregate.skipped.push({ tool: adapter.name, kind, name, reason: 'already-generated' });
746
+ return;
747
+ }
748
+ } catch (_) { /* fall through to the full write */ }
749
+
750
+ // Write merged content to the indirect location.
751
+ fs.writeFileSync(generatedPath, merged.generated);
752
+
694
753
  // Replace whatever currently lives at linkPath with the new symlink.
695
754
  if (lstat) {
696
755
  try { fs.unlinkSync(linkPath); }
697
756
  catch (_) { try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch (_) { /* noop */ } }
698
757
  }
699
758
  fs.symlinkSync(relativeTarget, linkPath);
700
- UI.success(`[${adapter.label}] ${kind} generated (overlay applied): ${path.join(targetRel, name)}.md ${relativeTarget}`);
759
+ const mode = hasOverlay && configSha ? 'slots + overlay' : (hasOverlay ? 'overlay applied' : 'slots resolved');
760
+ UI.success(`[${adapter.label}] ${kind} generated (${mode}): ${path.join(targetRel, name)}.md → ${relativeTarget}`);
701
761
  aggregate.generated.push({ tool: adapter.name, kind, name, overlaySha: merged.overlaySha });
702
762
  }
703
763