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.
- package/CHANGELOG.md +75 -0
- package/README.md +1 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/CHANGELOG.md +82 -0
- package/framework/.claude/agents/REGISTRY.md +3 -1
- package/framework/.claude/agents/code-reviewer.md +84 -400
- package/framework/.claude/agents/codebase-architect.md +71 -356
- package/framework/.claude/agents/coder.md +131 -291
- package/framework/.claude/agents/doc-reviewer.md +126 -450
- package/framework/.claude/agents/plan-auditor.md +137 -436
- package/framework/.claude/agents/qa-sentinel.md +59 -313
- package/framework/.claude/agents/skill-improver.md +60 -4
- package/framework/.claude/agents/ui-expert.md +104 -591
- package/framework/.claude/hooks/agent-discovery-gate.js +2 -2
- package/framework/.claude/hooks/agent-discovery-info.sh +1 -0
- package/framework/.claude/skills/new/CHANGELOG.md +11 -0
- package/framework/.claude/skills/new/SKILL.md +1 -1
- package/framework/.claude/skills/new/references/codex-gate.md +25 -11
- package/framework/.claude/skills/new/references/implement.md +43 -3
- package/framework/.claude/skills/new/references/review-cycle.md +16 -4
- package/framework/.claude/skills/new/scripts/build-review-bundle.mjs +104 -0
- package/framework/.claude/skills/new/scripts/doc-invariants.mjs +163 -0
- package/framework/.claude/skills/new2/CHANGELOG.md +4 -0
- package/framework/.claude/skills/new2/SKILL.md +5 -1
- package/framework/.claude/workflows/new2-resolve.js +10 -3
- package/framework/.claude/workflows/new2.js +51 -3
- package/framework/agents/agent-operating-protocol.md +152 -0
- package/framework/agents/doc-audit-protocol.md +227 -0
- package/framework/agents/index.md +4 -0
- package/framework/agents/review-protocol.md +207 -0
- package/framework/routines/finding-mine.routine.yml +56 -0
- package/framework/routines/index.yml +11 -0
- package/package.json +1 -1
- package/src/commands/configure.js +15 -0
- package/src/commands/doctor.js +69 -2
- package/src/utils/agent-slots.js +109 -0
- package/src/utils/overlay-merger.js +17 -8
- package/src/utils/symlinks.js +93 -33
package/src/utils/symlinks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
571
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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
|
|