baldart 5.5.0 → 5.7.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 (65) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/VERSION +1 -1
  3. package/framework/.claude/agents/CHANGELOG.md +16 -0
  4. package/framework/.claude/agents/prd-card-writer.md +37 -3
  5. package/framework/.claude/skills/capture/CHANGELOG.md +4 -0
  6. package/framework/.claude/skills/capture/SKILL.md +2 -1
  7. package/framework/.claude/skills/design-sync/CHANGELOG.md +4 -0
  8. package/framework/.claude/skills/design-sync/SKILL.md +2 -1
  9. package/framework/.claude/skills/ds-edit/CHANGELOG.md +5 -0
  10. package/framework/.claude/skills/ds-edit/SKILL.md +9 -10
  11. package/framework/.claude/skills/ds-handoff/CHANGELOG.md +5 -0
  12. package/framework/.claude/skills/ds-handoff/SKILL.md +3 -2
  13. package/framework/.claude/skills/ds-new/CHANGELOG.md +4 -0
  14. package/framework/.claude/skills/ds-new/SKILL.md +2 -1
  15. package/framework/.claude/skills/ds-render/CHANGELOG.md +4 -0
  16. package/framework/.claude/skills/ds-render/SKILL.md +2 -1
  17. package/framework/.claude/skills/e2e-review/CHANGELOG.md +4 -0
  18. package/framework/.claude/skills/e2e-review/SKILL.md +2 -1
  19. package/framework/.claude/skills/gamification-design/CHANGELOG.md +4 -0
  20. package/framework/.claude/skills/gamification-design/SKILL.md +2 -1
  21. package/framework/.claude/skills/graph-align/CHANGELOG.md +4 -0
  22. package/framework/.claude/skills/graph-align/SKILL.md +2 -1
  23. package/framework/.claude/skills/i18n/CHANGELOG.md +4 -0
  24. package/framework/.claude/skills/i18n/SKILL.md +2 -1
  25. package/framework/.claude/skills/motion-design/CHANGELOG.md +4 -0
  26. package/framework/.claude/skills/motion-design/SKILL.md +2 -1
  27. package/framework/.claude/skills/new/CHANGELOG.md +27 -0
  28. package/framework/.claude/skills/new/SKILL.md +2 -1
  29. package/framework/.claude/skills/new/references/commit.md +12 -8
  30. package/framework/.claude/skills/new/references/completeness.md +10 -0
  31. package/framework/.claude/skills/new/references/final-review.md +23 -0
  32. package/framework/.claude/skills/new/references/implement.md +14 -4
  33. package/framework/.claude/skills/new/references/merge-cleanup.md +35 -14
  34. package/framework/.claude/skills/new2/CHANGELOG.md +40 -0
  35. package/framework/.claude/skills/new2/SKILL.md +53 -12
  36. package/framework/.claude/skills/prd/CHANGELOG.md +40 -0
  37. package/framework/.claude/skills/prd/SKILL.md +18 -5
  38. package/framework/.claude/skills/prd/assets/state-template.md +8 -1
  39. package/framework/.claude/skills/prd/references/audit-phase.md +13 -1
  40. package/framework/.claude/skills/prd/references/backlog-phase.md +20 -0
  41. package/framework/.claude/skills/prd/references/discovery-phase.md +35 -0
  42. package/framework/.claude/skills/prd/references/ui-design-phase.md +28 -0
  43. package/framework/.claude/skills/prd/references/validation-phase.md +45 -1
  44. package/framework/.claude/skills/prd-add/CHANGELOG.md +4 -0
  45. package/framework/.claude/skills/prd-add/SKILL.md +2 -1
  46. package/framework/.claude/skills/skill-creator/references/skill-structure.md +31 -2
  47. package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +25 -3
  48. package/framework/.claude/skills/ui-design/CHANGELOG.md +4 -0
  49. package/framework/.claude/skills/ui-design/SKILL.md +2 -1
  50. package/framework/.claude/skills/ui-implement/CHANGELOG.md +4 -0
  51. package/framework/.claude/skills/ui-implement/SKILL.md +2 -1
  52. package/framework/.claude/skills/worktree-manager/CHANGELOG.md +13 -0
  53. package/framework/.claude/skills/worktree-manager/SKILL.md +11 -1
  54. package/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh +82 -6
  55. package/framework/.claude/workflows/new-final-review.js +9 -7
  56. package/framework/.claude/workflows/new2-resolve.js +29 -1
  57. package/framework/.claude/workflows/new2.js +158 -18
  58. package/framework/agents/card-schema.md +18 -1
  59. package/framework/agents/research-protocol.md +6 -1
  60. package/framework/scripts/stamp-holistic-audit.js +53 -10
  61. package/package.json +1 -1
  62. package/src/commands/configure.js +9 -2
  63. package/src/commands/doctor.js +65 -0
  64. package/src/utils/skill-gate.js +139 -0
  65. package/src/utils/symlinks.js +72 -7
@@ -0,0 +1,139 @@
1
+ /**
2
+ * skill-gate.js — install-time feature-gating for framework skills.
3
+ *
4
+ * BALDART ships ~40 skills; Codex reserves only ~2% of its context window for
5
+ * skill descriptions, so linking every skill (including ones whose gating
6
+ * `features.*` flag is OFF, which then refuse at runtime anyway) wastes that
7
+ * budget. This module decides, per install/update, WHICH framework skills to
8
+ * link into a consumer, based on an OPTIONAL `requires_feature` frontmatter
9
+ * field in each skill's `SKILL.md`.
10
+ *
11
+ * The gate is PURELY install-time (resolved here by the Node installer) — both
12
+ * Claude (`.claude/skills/`) and Codex (`.agents/skills/`) simply see fewer
13
+ * directories. Neither runtime interprets `requires_feature`; it is a BALDART
14
+ * authoring construct, ignored by the skill loaders exactly like `version:` /
15
+ * `effort:`. So the filter is natively portable across both tools and applied
16
+ * from a single code path (the gate is shared SEMANTICS, not runtime mechanics).
17
+ *
18
+ * Frontmatter contract (all OPTIONAL — a skill with no `requires_feature` is
19
+ * ALWAYS linked, backwards-compatible default):
20
+ * requires_feature: has_design_system # string → link iff true
21
+ * requires_feature: [has_design_system, has_e2e_review] # list = AND (all true)
22
+ *
23
+ * Fail-open everywhere (never hide a skill by accident — the safety net must
24
+ * not become a tribunal):
25
+ * - config absent / unreadable / malformed → link EVERYTHING.
26
+ * - a single skill's frontmatter malformed → link THAT skill (per-skill),
27
+ * never crash the whole merge.
28
+ *
29
+ * NOTE: we read `features.*` DIRECTLY from baldart.config.yml rather than reuse
30
+ * `agent-slots.resolveAgentFlags()` — that flag set intentionally covers only
31
+ * the keys the agent SLOT sources reference and omits `has_prd_workflow` /
32
+ * `has_e2e_review`, which ARE skill gates. Reading the raw block keeps the gate
33
+ * correct for every existing (and future) `features.*` key with no coupling.
34
+ *
35
+ * This introduces NO new `baldart.config.yml` key (it consumes existing
36
+ * `features.*`) — the schema-change propagation rule does NOT apply. It is
37
+ * skill-frontmatter metadata, like `version`/`effort`.
38
+ */
39
+
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+
43
+ /**
44
+ * Load the `features` block from baldart.config.yml.
45
+ * @returns {{ features: Object|null }} `features: null` signals "config
46
+ * unavailable" → callers must fail-open (link everything).
47
+ */
48
+ function loadFeatures(cwd) {
49
+ try {
50
+ const yaml = require('js-yaml');
51
+ const cfgPath = path.join(cwd, 'baldart.config.yml');
52
+ if (!fs.existsSync(cfgPath)) return { features: null };
53
+ const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
54
+ if (!cfg || typeof cfg !== 'object') return { features: null };
55
+ const features = (cfg.features && typeof cfg.features === 'object') ? cfg.features : {};
56
+ return { features };
57
+ } catch (_) {
58
+ return { features: null };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Extract the OPTIONAL `requires_feature` from a skill's SKILL.md frontmatter.
64
+ * @returns {string[]|null} normalized list of required feature keys, or null
65
+ * when the skill is ungated / has no SKILL.md / frontmatter is unparseable
66
+ * (fail-open: null ⇒ always link).
67
+ */
68
+ function readRequiredFeatures(skillDir) {
69
+ try {
70
+ const skillMd = path.join(skillDir, 'SKILL.md');
71
+ if (!fs.existsSync(skillMd)) return null;
72
+ const raw = fs.readFileSync(skillMd, 'utf8');
73
+ if (!raw.startsWith('---')) return null;
74
+ // Frontmatter is between the first two `---` fences.
75
+ const end = raw.indexOf('\n---', 3);
76
+ if (end === -1) return null;
77
+ const fmText = raw.slice(3, end);
78
+ const yaml = require('js-yaml');
79
+ const fm = yaml.load(fmText);
80
+ if (!fm || typeof fm !== 'object') return null;
81
+ const req = fm.requires_feature;
82
+ if (req == null) return null;
83
+ const list = (Array.isArray(req) ? req : [req])
84
+ .map(x => String(x).trim())
85
+ .filter(Boolean);
86
+ return list.length ? list : null;
87
+ } catch (_) {
88
+ // Malformed frontmatter for THIS skill → fail-open (link it).
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Decide which framework skills to link vs skip for this consumer.
95
+ *
96
+ * @param {string} frameworkSkillsDir absolute path to
97
+ * `.framework/framework/.claude/skills/`
98
+ * @param {string} cwd consumer repo root (where baldart.config.yml lives)
99
+ * @returns {{ toLink: string[], gatedOff: string[], failOpen: boolean }}
100
+ * `toLink` = skill dir names to link; `gatedOff` = skill dir names to skip
101
+ * (and prune if already linked); `failOpen` = true when config was
102
+ * unavailable so nothing was gated.
103
+ */
104
+ function resolveSkillGate(frameworkSkillsDir, cwd) {
105
+ const result = { toLink: [], gatedOff: [], failOpen: false };
106
+
107
+ let names = [];
108
+ try {
109
+ names = fs.readdirSync(frameworkSkillsDir).filter(name => {
110
+ if (name.startsWith('.')) return false;
111
+ try { return fs.lstatSync(path.join(frameworkSkillsDir, name)).isDirectory(); }
112
+ catch (_) { return false; }
113
+ });
114
+ } catch (_) {
115
+ return result; // no framework skills dir → empty sets, caller handles
116
+ }
117
+
118
+ const { features } = loadFeatures(cwd);
119
+
120
+ // Config unavailable → fail-open: link everything, gate nothing.
121
+ if (features === null) {
122
+ result.failOpen = true;
123
+ result.toLink = names.slice();
124
+ return result;
125
+ }
126
+
127
+ for (const name of names) {
128
+ const required = readRequiredFeatures(path.join(frameworkSkillsDir, name));
129
+ if (!required) { result.toLink.push(name); continue; }
130
+ // AND semantics: link iff EVERY required feature is explicitly true.
131
+ const allOn = required.every(f => features[f] === true);
132
+ if (allOn) result.toLink.push(name);
133
+ else result.gatedOff.push(name);
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ module.exports = { resolveSkillGate, readRequiredFeatures, loadFeatures };
@@ -210,7 +210,7 @@ class SymlinkUtils {
210
210
  */
211
211
  mergeSkills(opts = {}) {
212
212
  const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
213
- const aggregate = { linked: [], skipped: [], conflicts: [] };
213
+ const aggregate = { linked: [], skipped: [], conflicts: [], pruned: [], gatedOff: [] };
214
214
 
215
215
  const frameworkSkillsDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills');
216
216
  if (!fs.existsSync(frameworkSkillsDir)) {
@@ -218,17 +218,31 @@ class SymlinkUtils {
218
218
  return aggregate;
219
219
  }
220
220
 
221
- const frameworkSkills = fs.readdirSync(frameworkSkillsDir).filter(name => {
222
- if (name.startsWith('.')) return false;
223
- const full = path.join(frameworkSkillsDir, name);
224
- return fs.lstatSync(full).isDirectory();
225
- });
221
+ // Feature-gate: link only the skills whose `requires_feature` frontmatter is
222
+ // satisfied by this consumer's `features.*` (fail-open when config is
223
+ // unavailable link everything). Resolved ONCE and applied to EVERY tool
224
+ // from this single code path — the gate is shared semantics, so Claude and
225
+ // Codex stay identical by construction. See src/utils/skill-gate.js.
226
+ const gate = require('./skill-gate').resolveSkillGate(frameworkSkillsDir, this.cwd);
227
+ const frameworkSkills = gate.toLink;
228
+ aggregate.gatedOff = gate.gatedOff.slice();
229
+ if (gate.gatedOff.length) {
230
+ UI.info(`Feature-gate: ${gate.gatedOff.length} skill(s) skipped (gating feature off): ${gate.gatedOff.join(', ')}`);
231
+ }
226
232
 
227
233
  for (const tool of tools) {
228
234
  const result = this._mergeSkillsForTool(tool, frameworkSkills, frameworkSkillsDir);
229
235
  aggregate.linked.push(...result.linked);
230
236
  aggregate.skipped.push(...result.skipped);
231
237
  aggregate.conflicts.push(...result.conflicts);
238
+
239
+ // Prune direction: remove any pre-existing framework symlink for a skill
240
+ // that is now gated-off (e.g. a feature flipped false after a prior
241
+ // install, or a fail-open install that linked everything before config
242
+ // existed). Only OUR framework symlinks are removed — never a real user
243
+ // skill dir, never a user-override symlink pointing elsewhere.
244
+ const pruneResult = this._pruneGatedSkillsForTool(tool, gate.gatedOff, frameworkSkillsDir);
245
+ aggregate.pruned.push(...pruneResult.pruned);
232
246
  }
233
247
 
234
248
  if (aggregate.conflicts.length > 0) {
@@ -329,6 +343,54 @@ class SymlinkUtils {
329
343
  return result;
330
344
  }
331
345
 
346
+ /**
347
+ * Prune framework skill symlinks that are now gated-off — internal.
348
+ *
349
+ * This is the reconcile (removal) direction of the feature-gate: turning a
350
+ * `features.*` flag off (or a fail-open first install that linked everything
351
+ * before config existed) must actually REMOVE the stale symlinks, or the
352
+ * skill-description budget never shrinks. Each `name` in `gatedOff` is a
353
+ * framework skill name by construction (it came from the framework catalog),
354
+ * so basename∈catalog is guaranteed; we still verify the link RESOLVES to our
355
+ * framework source for that exact skill before unlinking. Safety invariants:
356
+ * - never touch a real directory (a user-authored skill of the same name);
357
+ * - never touch a symlink pointing ANYWHERE ELSE (a user override, cf. the
358
+ * `user-symlink-override` branch in _mergeSkillsForTool).
359
+ */
360
+ _pruneGatedSkillsForTool(toolName, gatedOff, frameworkSkillsDir) {
361
+ const { getAdapter } = require('./tool-adapters');
362
+ const adapter = getAdapter(toolName, this.cwd);
363
+ const result = { pruned: [] };
364
+ if (!gatedOff || !gatedOff.length) return result;
365
+
366
+ const skillsRel = adapter.skillsDir();
367
+ const userSkillsDir = path.join(this.cwd, skillsRel);
368
+ if (!fs.existsSync(userSkillsDir)) return result;
369
+
370
+ for (const name of gatedOff) {
371
+ const linkPath = path.join(userSkillsDir, name);
372
+ let lstat = null;
373
+ try { lstat = fs.lstatSync(linkPath); } catch (_) { continue; /* absent */ }
374
+
375
+ // Only ever remove a symlink — a real dir here is a user-authored skill.
376
+ if (!lstat.isSymbolicLink()) continue;
377
+
378
+ const current = fs.readlinkSync(linkPath);
379
+ const resolved = path.resolve(path.dirname(linkPath), current);
380
+ const frameworkTarget = path.join(frameworkSkillsDir, name);
381
+
382
+ // Unlink ONLY when it resolves to OUR framework source for this skill.
383
+ // A symlink pointing elsewhere is a deliberate user override → leave it.
384
+ if (resolved === frameworkTarget) {
385
+ fs.unlinkSync(linkPath);
386
+ UI.info(`[${adapter.label}] Skill unlinked (feature gated off): ${path.join(skillsRel, name)}`);
387
+ result.pruned.push({ tool: toolName, name });
388
+ }
389
+ }
390
+
391
+ return result;
392
+ }
393
+
332
394
  // -----------------------------------------------------------------------
333
395
  // Public composite operations
334
396
  // -----------------------------------------------------------------------
@@ -827,7 +889,10 @@ class SymlinkUtils {
827
889
  UI.warning(`[${adapter.label}] Legacy bulk skills symlink at ${skillsRel}. Run \`npx baldart update\` to convert to per-item.`);
828
890
  allValid = false;
829
891
  } else {
830
- const sample = ['skill-creator', 'frontend-design', 'bug', 'prd', 'capture'];
892
+ // Sample ONLY always-linked skills — never feature-gated ones (e.g.
893
+ // prd/capture), which are legitimately absent when their feature is
894
+ // off and would make this probe brittle / falsely alarming.
895
+ const sample = ['skill-creator', 'bug', 'simplify', 'overlay', 'find-skills'];
831
896
  let frameworkLinks = 0;
832
897
  sample.forEach(name => {
833
898
  const p = path.join(skillsDir, name);