baldart 5.0.0 → 5.1.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 (51) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +3 -3
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/CHANGELOG.md +68 -0
  5. package/framework/.claude/agents/REGISTRY.md +3 -3
  6. package/framework/.claude/agents/coder.md +4 -5
  7. package/framework/.claude/agents/doc-reviewer.md +3 -2
  8. package/framework/.claude/agents/prd-card-writer.md +1 -1
  9. package/framework/.claude/agents/qa-sentinel.md +4 -4
  10. package/framework/.claude/agents/senior-researcher.md +166 -151
  11. package/framework/.claude/commands/codexreview.md +26 -4
  12. package/framework/.claude/skills/new/CHANGELOG.md +28 -0
  13. package/framework/.claude/skills/new/SKILL.md +1 -1
  14. package/framework/.claude/skills/new/references/implement.md +8 -4
  15. package/framework/.claude/skills/new/references/review-cycle.md +8 -5
  16. package/framework/.claude/skills/new/scripts/build-review-bundle.mjs +13 -5
  17. package/framework/.claude/skills/new/scripts/doc-invariants.mjs +6 -3
  18. package/framework/.claude/skills/new2/CHANGELOG.md +9 -0
  19. package/framework/.claude/skills/new2/SKILL.md +1 -1
  20. package/framework/.claude/skills/prd/CHANGELOG.md +16 -0
  21. package/framework/.claude/skills/prd/SKILL.md +1 -1
  22. package/framework/.claude/skills/prd/references/discovery-phase.md +5 -1
  23. package/framework/.claude/skills/prd/references/research-phase.md +32 -12
  24. package/framework/.claude/skills/research/CHANGELOG.md +9 -0
  25. package/framework/.claude/skills/research/SKILL.md +94 -0
  26. package/framework/.claude/skills/research/assets/report-compare.template.md +50 -0
  27. package/framework/.claude/skills/research/assets/report-decision.template.md +42 -0
  28. package/framework/.claude/skills/research/assets/report-deep.template.md +61 -0
  29. package/framework/.claude/skills/research/assets/report-regulatory.template.md +44 -0
  30. package/framework/.claude/skills/research/references/playbook.md +112 -0
  31. package/framework/.claude/skills/research/scripts/rebuild-research-index.mjs +77 -0
  32. package/framework/.claude/workflows/new-card-review.js +16 -2
  33. package/framework/.claude/workflows/new2-resolve.js +1 -1
  34. package/framework/.claude/workflows/new2.js +1 -1
  35. package/framework/agents/coding-standards.md +2 -2
  36. package/framework/agents/doc-audit-protocol.md +9 -4
  37. package/framework/agents/index.md +2 -0
  38. package/framework/agents/research-protocol.md +228 -0
  39. package/framework/agents/skills-mapping.md +17 -0
  40. package/framework/docs/PROJECT-CONFIGURATION.md +23 -0
  41. package/framework/docs/UPGRADE-3.12-UI-COHERENCE.md +1 -1
  42. package/framework/templates/baldart.config.template.yml +6 -0
  43. package/framework/templates/research-index.template.md +13 -0
  44. package/framework/templates/research-sources.CHANGELOG.md +14 -0
  45. package/framework/templates/research-sources.template.md +31 -0
  46. package/package.json +1 -1
  47. package/src/commands/configure.js +26 -0
  48. package/src/commands/doctor.js +82 -0
  49. package/src/commands/update.js +48 -1
  50. package/src/utils/research-library.js +92 -0
  51. package/src/utils/symlinks.js +3 -0
@@ -479,6 +479,7 @@ function detect(cwd = process.cwd()) {
479
479
  backlog_dir: exists('backlog') ? 'backlog' : '',
480
480
  adrs_dir: countMatches('docs/decisions', /^ADR-.*\.md$/) > 0 ? 'docs/decisions' : '',
481
481
  prd_dir: exists('docs/prd') ? 'docs/prd' : '',
482
+ research_dir: exists('docs/research') ? 'docs/research' : '',
482
483
  docs_dir: exists('docs') ? 'docs' : '',
483
484
  references_dir: exists('docs/references') ? 'docs/references' : '',
484
485
  wiki_dir: exists('docs/wiki') ? 'docs/wiki' : '',
@@ -814,6 +815,7 @@ async function interactivePrompts(merged, detected) {
814
815
  ['backlog_dir', 'Backlog directory', () => merged.features.has_backlog],
815
816
  ['adrs_dir', 'ADR directory', () => merged.features.has_adrs],
816
817
  ['prd_dir', 'PRD directory', () => merged.features.has_prd_workflow],
818
+ ['research_dir', 'Research library directory (empty = disable)', () => true],
817
819
  ['docs_dir', 'Docs root directory (rg-search umbrella)', () => true],
818
820
  ['references_dir', 'References docs root', () => true],
819
821
  ['wiki_dir', 'Wiki overlay directory', () => merged.features.has_wiki_overlay],
@@ -1425,6 +1427,16 @@ async function configure(opts = {}) {
1425
1427
  // mergePreserving(target, source) keeps target where set, fills from source.
1426
1428
  let merged = mergePreserving(existing || {}, detected);
1427
1429
  merged = mergePreserving(merged, template);
1430
+
1431
+ // paths.research_dir (v5.1.0): default `docs/research` on FIRST encounter only
1432
+ // (key never declared in the consumer's config). An explicitly emptied key is
1433
+ // a deliberate opt-out and is respected everywhere — never re-proposed, never
1434
+ // resurrected (research-protocol.md § degrade-safe).
1435
+ const researchKeyDeclared = !!(existing && existing.paths && 'research_dir' in existing.paths);
1436
+ if (!researchKeyDeclared && merged.paths && !merged.paths.research_dir) {
1437
+ merged.paths.research_dir = 'docs/research';
1438
+ if (detected.paths) detected.paths.research_dir = 'docs/research';
1439
+ }
1428
1440
  merged.version = SCHEMA_VERSION;
1429
1441
 
1430
1442
  const dsSignals = detected.stack.design_system_signals || {};
@@ -1502,6 +1514,20 @@ async function configure(opts = {}) {
1502
1514
  UI.warning(`Agent re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
1503
1515
  }
1504
1516
 
1517
+ // v5.1.0 — research library: create + seed ${paths.research_dir} right after
1518
+ // the key is written. Creation ownership lives HERE (and in doctor repairs);
1519
+ // `update` only backfills seeds into an existing dir. Fail-safe: configure
1520
+ // must never die on a scaffolding hiccup — doctor heals later.
1521
+ try {
1522
+ const { ensureResearchLibrary } = require('../utils/research-library');
1523
+ const createdResearch = ensureResearchLibrary(cwd, merged, { createDir: true });
1524
+ if (createdResearch.length) {
1525
+ UI.success(`Research library initialized at ${merged.paths.research_dir}/`);
1526
+ }
1527
+ } catch (err) {
1528
+ UI.warning(`Research library setup failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
1529
+ }
1530
+
1505
1531
  // Ensure overlays dir exists (user-owned)
1506
1532
  const overlaysAbs = path.join(cwd, OVERLAYS_DIR);
1507
1533
  if (!fs.existsSync(overlaysAbs)) {
@@ -656,6 +656,39 @@ async function detectState(cwd, opts = {}) {
656
656
  }
657
657
  } catch (_) { /* never block doctor on design-system probe */ }
658
658
 
659
+ // ---- Research library (since v5.1.0) --------------------------------
660
+ // Gated on paths.research_dir being SET (empty = deliberate opt-out —
661
+ // never re-proposed, per research-protocol.md § degrade-safe). Three
662
+ // probes: dir missing (repair), seeds missing (backfill), source-matrix
663
+ // version behind the framework template (ADVISORY only — merging the
664
+ // matrix is a human decision, doctor never overwrites SOURCES.md).
665
+ state.researchDirMissing = false;
666
+ state.researchSeedMissing = [];
667
+ state.researchSourcesStale = null;
668
+ try {
669
+ const researchDir = (config && !config.__malformed && config.paths && config.paths.research_dir) || '';
670
+ if (typeof researchDir === 'string' && researchDir.trim()) {
671
+ const dirAbs = path.join(cwd, researchDir.trim());
672
+ if (!fs.existsSync(dirAbs)) {
673
+ state.researchDirMissing = true;
674
+ } else {
675
+ for (const seed of ['INDEX.md', 'SOURCES.md']) {
676
+ if (!fs.existsSync(path.join(dirAbs, seed))) state.researchSeedMissing.push(seed);
677
+ }
678
+ const { readMatrixVersion } = require('../utils/research-library');
679
+ const live = readMatrixVersion(path.join(dirAbs, 'SOURCES.md'));
680
+ const tpl = readMatrixVersion(path.join(cwd, '.framework', 'framework', 'templates', 'research-sources.template.md'));
681
+ if (live && tpl && live !== tpl) {
682
+ const toNum = (v) => v.split('.').map(Number);
683
+ const [lM, lm, lp] = toNum(live); const [tM, tm, tp] = toNum(tpl);
684
+ if (tM > lM || (tM === lM && (tm > lm || (tm === lm && tp > lp)))) {
685
+ state.researchSourcesStale = { live, template: tpl };
686
+ }
687
+ }
688
+ }
689
+ }
690
+ } catch (_) { /* never block doctor on research probe */ }
691
+
659
692
  // ---- Auto-deploy allowlist presence (since v4.59.0) ----------------
660
693
  // Purely informational: git.auto_deploy bounds what `/new -auto-ship`
661
694
  // may execute as an OUTWARD action. An empty allowlist is the SAFE
@@ -1343,6 +1376,55 @@ function planActions(state) {
1343
1376
  });
1344
1377
  }
1345
1378
 
1379
+ // Research library (since v5.1.0). Creation/repair lives HERE (and in
1380
+ // configure) — update only backfills seeds into an existing dir. autoOk:
1381
+ // unlike the i18n registry ("no safe auto-create without project context"),
1382
+ // the research seeds are fully GENERIC (an empty index + the framework
1383
+ // default source matrix) — zero project context needed, so unattended
1384
+ // creation is safe. Precedent for consumer-space writes: tokens-build.
1385
+ if (state.researchDirMissing) {
1386
+ actions.push({
1387
+ key: 'create-research-dir',
1388
+ label: 'Create the research library (paths.research_dir is set but the directory is missing)',
1389
+ why: 'baldart.config.yml declares a research library but the directory does not exist on disk. Creating it (with the INDEX.md + SOURCES.md seeds and the category subdirs) restores the reuse pre-flight for senior-researcher / /research / /prd Step 2.5. If you deleted it on purpose, empty paths.research_dir instead — an empty key is a respected opt-out.',
1390
+ autoOk: true, // generic seeds, idempotent, no project context needed
1391
+ run: async () => {
1392
+ const cfg = loadConfig(state.cwd);
1393
+ const { ensureResearchLibrary } = require('../utils/research-library');
1394
+ const created = ensureResearchLibrary(state.cwd, cfg, { createDir: true });
1395
+ if (created.length) UI.success(`Research library initialized (${created.join(', ')}).`);
1396
+ else UI.info('Nothing to create — library already present.');
1397
+ },
1398
+ });
1399
+ }
1400
+ if (state.researchSeedMissing && state.researchSeedMissing.length > 0) {
1401
+ actions.push({
1402
+ key: 'backfill-research-seeds',
1403
+ label: `Backfill research library seed file(s): ${state.researchSeedMissing.join(', ')}`,
1404
+ why: `The research library directory exists but ${state.researchSeedMissing.join(' and ')} are missing (pre-v5.1 install or manual cleanup). The seeds are generic framework templates (empty index / default source matrix); existing files are never overwritten.`,
1405
+ autoOk: true, // copy-if-absent only
1406
+ run: async () => {
1407
+ const cfg = loadConfig(state.cwd);
1408
+ const { ensureResearchLibrary } = require('../utils/research-library');
1409
+ const created = ensureResearchLibrary(state.cwd, cfg, { createDir: false });
1410
+ if (created.length) UI.success(`Seeded: ${created.join(', ')}.`);
1411
+ else UI.info('Nothing to backfill.');
1412
+ },
1413
+ });
1414
+ }
1415
+ if (state.researchSourcesStale) {
1416
+ actions.push({
1417
+ key: 'research-sources-advisory',
1418
+ label: `Research source matrix behind framework default (${state.researchSourcesStale.live} < ${state.researchSourcesStale.template})`,
1419
+ why: `Your SOURCES.md carries matrix_version ${state.researchSourcesStale.live} while the framework template ships ${state.researchSourcesStale.template}. ADVISORY only — SOURCES.md is consumer-owned and doctor never overwrites it. Compare with .framework/framework/templates/research-sources.template.md (its CHANGELOG sibling lists what changed) and merge what you want, keeping your "## Local additions".`,
1420
+ autoOk: true, // advisory print-only; run() does NOT mutate anything
1421
+ run: () => {
1422
+ UI.info(`Source matrix: local ${state.researchSourcesStale.live} vs framework ${state.researchSourcesStale.template}.`);
1423
+ UI.info('Diff against .framework/framework/templates/research-sources.template.md and merge manually (your `## Local additions` section is yours).');
1424
+ },
1425
+ });
1426
+ }
1427
+
1346
1428
  // Auto-deploy allowlist (since v4.59.0). Advisory print-only — surfaced ONLY
1347
1429
  // when the allowlist is non-empty (the safe default of empty/unset opens
1348
1430
  // nothing, so it warrants no nag). Reminds the user that `/new -auto-ship`
@@ -183,6 +183,28 @@ function isBaldartManagedPath(p, extra = []) {
183
183
  return BALDART_MANAGED_PATTERNS.some((rx) => rx.test(p)) || extra.some((rx) => rx.test(p));
184
184
  }
185
185
 
186
+ // Research-library seed paths (v5.1.0). The library CONTENT is consumer-owned
187
+ // (like docs/prd/) and never enters the managed patterns; only the two seed
188
+ // files BALDART itself creates are auto-commit candidates — and only while
189
+ // UNTRACKED (see postUpdateAutoCommit), so a consumer-edited tracked
190
+ // SOURCES.md/INDEX.md is never swept into a reconcile commit.
191
+ function researchSeedPaths(cwd = process.cwd()) {
192
+ try {
193
+ const fs = require('fs');
194
+ const path = require('path');
195
+ const yaml = require('js-yaml');
196
+ const cfgPath = path.join(cwd, 'baldart.config.yml');
197
+ if (!fs.existsSync(cfgPath)) return [];
198
+ const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
199
+ const dir = cfg && cfg.paths && cfg.paths.research_dir;
200
+ if (typeof dir !== 'string' || !dir.trim()) return [];
201
+ const norm = dir.trim().replace(/\\/g, '/').replace(/\/+$/, '');
202
+ return [`${norm}/INDEX.md`, `${norm}/SOURCES.md`];
203
+ } catch (_) {
204
+ return [];
205
+ }
206
+ }
207
+
186
208
  // Re-generate the root primitives (AGENTS.md + CLAUDE.md) from the versioned
187
209
  // skeletons + baldart.config.yml + overlays. Idempotent + byte-stable, so it is
188
210
  // safe to call on both the aligned-install and post-pull paths. Never throws —
@@ -252,10 +274,14 @@ async function postUpdateAutoCommit(git, newVersion, options) {
252
274
  const managed = [];
253
275
  const userOwned = [];
254
276
  const extraPatterns = i18nRegistryPatterns();
277
+ // Research seeds: managed ONLY while untracked (created by BALDART, never yet
278
+ // the consumer's). Tracked-and-modified seeds are user content — untouched.
279
+ const untracked = new Set([...status.not_added, ...status.created]);
280
+ const researchSeedNew = researchSeedPaths().filter((p) => untracked.has(p));
255
281
  for (const p of allDirty) {
256
282
  if (!p || seen.has(p)) continue;
257
283
  seen.add(p);
258
- (isBaldartManagedPath(p, extraPatterns) ? managed : userOwned).push(p);
284
+ (isBaldartManagedPath(p, extraPatterns) || researchSeedNew.includes(p) ? managed : userOwned).push(p);
259
285
  }
260
286
  if (managed.length === 0) {
261
287
  return;
@@ -986,6 +1012,17 @@ async function update(options = {}, unknownArgs = []) {
986
1012
  // --json is programmatic → force autonomous so a handwritten file is never mutated.
987
1013
  reconcileRootPrimitives(process.cwd(), alignedTools, options && options.json === true ? true : undefined);
988
1014
 
1015
+ // Research-library seed backfill (v5.1.0) — seeds only, into an EXISTING
1016
+ // dir. Never creates the dir here (creation ownership: configure/doctor;
1017
+ // update must not resurrect a deliberately deleted library). Silent + idempotent.
1018
+ try {
1019
+ const fsl = require('fs');
1020
+ const pathl = require('path');
1021
+ const yaml = require('js-yaml');
1022
+ const cfg = yaml.load(fsl.readFileSync(pathl.join(process.cwd(), 'baldart.config.yml'), 'utf8'));
1023
+ require('../utils/research-library').ensureResearchLibrary(process.cwd(), cfg, { createDir: false });
1024
+ } catch (_) { /* doctor heals */ }
1025
+
989
1026
  // Backfill hooks on the aligned/self-heal path too. Same gap class as the
990
1027
  // payload reconcile above: a consumer who framework-updated with an OLDER
991
1028
  // CLI (before a given hook shipped — e.g. the S2 Codex hooks) then aligns
@@ -1351,6 +1388,16 @@ async function update(options = {}, unknownArgs = []) {
1351
1388
  // a primitive/slot/overlay change lands. Idempotent + byte-stable.
1352
1389
  reconcileRootPrimitives(process.cwd(), enabledTools, options && options.json === true ? true : undefined);
1353
1390
 
1391
+ // Research-library seed backfill (v5.1.0) — seeds only, into an EXISTING dir
1392
+ // (creation ownership: configure/doctor; never resurrect a deleted library).
1393
+ try {
1394
+ const fsl = require('fs');
1395
+ const pathl = require('path');
1396
+ const yaml = require('js-yaml');
1397
+ const cfg = yaml.load(fsl.readFileSync(pathl.join(process.cwd(), 'baldart.config.yml'), 'utf8'));
1398
+ require('../utils/research-library').ensureResearchLibrary(process.cwd(), cfg, { createDir: false });
1399
+ } catch (_) { /* doctor heals */ }
1400
+
1354
1401
  // Routines wizard (since v2.1.0) — surfaces routines added in the new framework version
1355
1402
  try {
1356
1403
  const routinesCmd = require('./routines');
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Research library scaffolding (v5.1.0).
3
+ *
4
+ * Creates/seeds the consumer-owned research library at `paths.research_dir`:
5
+ * category subdirectories + two seed files copied from framework-internal
6
+ * templates (INDEX.md — the library map; SOURCES.md — the live source matrix).
7
+ *
8
+ * Lifecycle ownership (deliberate, see research-protocol.md):
9
+ * - `configure` CREATES (right after writing the key) — createDir: true
10
+ * - `doctor` REPAIRS (confirmable actions) — createDir: true
11
+ * - `update` only BACKFILLS seeds into an EXISTING dir — createDir: false
12
+ * (never resurrects a deliberately deleted library)
13
+ *
14
+ * Idempotent and silent on already-existing artifacts (no per-run warnings).
15
+ * Returns the repo-relative paths it created THIS run, so `update` can scope
16
+ * its auto-commit to exactly these artifacts (v4.89.2 lesson) while the rest
17
+ * of the library stays consumer-owned and untouched.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const FRAMEWORK_TEMPLATES = path.join('.framework', 'framework', 'templates');
24
+
25
+ const CATEGORY_DIRS = [
26
+ 'architecture',
27
+ 'integrations',
28
+ 'regulatory',
29
+ 'ux-patterns',
30
+ 'algorithms',
31
+ 'tooling',
32
+ '_archive',
33
+ ];
34
+
35
+ const SEEDS = [
36
+ { template: 'research-index.template.md', dest: 'INDEX.md' },
37
+ { template: 'research-sources.template.md', dest: 'SOURCES.md' },
38
+ ];
39
+
40
+ /**
41
+ * @param {string} cwd - consumer repo root
42
+ * @param {object|null} config - parsed baldart.config.yml
43
+ * @param {{createDir?: boolean}} [opts]
44
+ * @returns {string[]} repo-relative paths created this run
45
+ */
46
+ function ensureResearchLibrary(cwd, config, opts = {}) {
47
+ const createDir = opts.createDir !== false;
48
+ const created = [];
49
+
50
+ const key = config && config.paths && config.paths.research_dir;
51
+ if (!key || typeof key !== 'string') return created; // opt-out / unconfigured — no-op
52
+
53
+ const dirAbs = path.join(cwd, key);
54
+ if (!fs.existsSync(dirAbs)) {
55
+ if (!createDir) return created; // update path: never resurrect a deleted library
56
+ fs.mkdirSync(dirAbs, { recursive: true });
57
+ created.push(key);
58
+ }
59
+
60
+ for (const sub of CATEGORY_DIRS) {
61
+ const subAbs = path.join(dirAbs, sub);
62
+ if (!fs.existsSync(subAbs)) fs.mkdirSync(subAbs, { recursive: true });
63
+ }
64
+
65
+ for (const seed of SEEDS) {
66
+ const destAbs = path.join(dirAbs, seed.dest);
67
+ if (fs.existsSync(destAbs)) continue; // consumer-owned — never overwrite, never warn
68
+ const srcAbs = path.join(cwd, FRAMEWORK_TEMPLATES, seed.template);
69
+ if (!fs.existsSync(srcAbs)) continue; // older framework payload — doctor backfills later
70
+ fs.copyFileSync(srcAbs, destAbs);
71
+ created.push(path.join(key, seed.dest));
72
+ }
73
+
74
+ return created;
75
+ }
76
+
77
+ /**
78
+ * Parse the `matrix_version:` frontmatter value from a source-matrix file.
79
+ * Returns null when the file is missing/unreadable or carries no version —
80
+ * callers treat null as "cannot compare" (never a failure).
81
+ */
82
+ function readMatrixVersion(fileAbs) {
83
+ try {
84
+ const head = fs.readFileSync(fileAbs, 'utf8').slice(0, 500);
85
+ const m = head.match(/^matrix_version:\s*["']?(\d+\.\d+\.\d+)["']?\s*$/m);
86
+ return m ? m[1] : null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ module.exports = { ensureResearchLibrary, readMatrixVersion, CATEGORY_DIRS };
@@ -948,6 +948,9 @@ class SymlinkUtils {
948
948
  'skill-project-context.snippet.md',
949
949
  'component-spec.template.md', // canonical prose-body template — consumed by serialize-spec.mjs, not hand-edited
950
950
  'overlays', // example overlays directory — consumers reference, don't copy wholesale
951
+ 'research-index.template.md', // research library seed — copied to ${paths.research_dir}/INDEX.md by ensureResearchLibrary
952
+ 'research-sources.template.md', // source matrix seed + default-matrix SSOT — copied to ${paths.research_dir}/SOURCES.md
953
+ 'research-sources.CHANGELOG.md', // matrix version history — framework metadata, never installed
951
954
  ]);
952
955
 
953
956
  this.ensureDirectory('templates');