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.
- package/CHANGELOG.md +119 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/CHANGELOG.md +16 -0
- package/framework/.claude/agents/prd-card-writer.md +37 -3
- package/framework/.claude/skills/capture/CHANGELOG.md +4 -0
- package/framework/.claude/skills/capture/SKILL.md +2 -1
- package/framework/.claude/skills/design-sync/CHANGELOG.md +4 -0
- package/framework/.claude/skills/design-sync/SKILL.md +2 -1
- package/framework/.claude/skills/ds-edit/CHANGELOG.md +5 -0
- package/framework/.claude/skills/ds-edit/SKILL.md +9 -10
- package/framework/.claude/skills/ds-handoff/CHANGELOG.md +5 -0
- package/framework/.claude/skills/ds-handoff/SKILL.md +3 -2
- package/framework/.claude/skills/ds-new/CHANGELOG.md +4 -0
- package/framework/.claude/skills/ds-new/SKILL.md +2 -1
- package/framework/.claude/skills/ds-render/CHANGELOG.md +4 -0
- package/framework/.claude/skills/ds-render/SKILL.md +2 -1
- package/framework/.claude/skills/e2e-review/CHANGELOG.md +4 -0
- package/framework/.claude/skills/e2e-review/SKILL.md +2 -1
- package/framework/.claude/skills/gamification-design/CHANGELOG.md +4 -0
- package/framework/.claude/skills/gamification-design/SKILL.md +2 -1
- package/framework/.claude/skills/graph-align/CHANGELOG.md +4 -0
- package/framework/.claude/skills/graph-align/SKILL.md +2 -1
- package/framework/.claude/skills/i18n/CHANGELOG.md +4 -0
- package/framework/.claude/skills/i18n/SKILL.md +2 -1
- package/framework/.claude/skills/motion-design/CHANGELOG.md +4 -0
- package/framework/.claude/skills/motion-design/SKILL.md +2 -1
- package/framework/.claude/skills/new/CHANGELOG.md +27 -0
- package/framework/.claude/skills/new/SKILL.md +2 -1
- package/framework/.claude/skills/new/references/commit.md +12 -8
- package/framework/.claude/skills/new/references/completeness.md +10 -0
- package/framework/.claude/skills/new/references/final-review.md +23 -0
- package/framework/.claude/skills/new/references/implement.md +14 -4
- package/framework/.claude/skills/new/references/merge-cleanup.md +35 -14
- package/framework/.claude/skills/new2/CHANGELOG.md +40 -0
- package/framework/.claude/skills/new2/SKILL.md +53 -12
- package/framework/.claude/skills/prd/CHANGELOG.md +40 -0
- package/framework/.claude/skills/prd/SKILL.md +18 -5
- package/framework/.claude/skills/prd/assets/state-template.md +8 -1
- package/framework/.claude/skills/prd/references/audit-phase.md +13 -1
- package/framework/.claude/skills/prd/references/backlog-phase.md +20 -0
- package/framework/.claude/skills/prd/references/discovery-phase.md +35 -0
- package/framework/.claude/skills/prd/references/ui-design-phase.md +28 -0
- package/framework/.claude/skills/prd/references/validation-phase.md +45 -1
- package/framework/.claude/skills/prd-add/CHANGELOG.md +4 -0
- package/framework/.claude/skills/prd-add/SKILL.md +2 -1
- package/framework/.claude/skills/skill-creator/references/skill-structure.md +31 -2
- package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +25 -3
- package/framework/.claude/skills/ui-design/CHANGELOG.md +4 -0
- package/framework/.claude/skills/ui-design/SKILL.md +2 -1
- package/framework/.claude/skills/ui-implement/CHANGELOG.md +4 -0
- package/framework/.claude/skills/ui-implement/SKILL.md +2 -1
- package/framework/.claude/skills/worktree-manager/CHANGELOG.md +13 -0
- package/framework/.claude/skills/worktree-manager/SKILL.md +11 -1
- package/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh +82 -6
- package/framework/.claude/workflows/new-final-review.js +9 -7
- package/framework/.claude/workflows/new2-resolve.js +29 -1
- package/framework/.claude/workflows/new2.js +158 -18
- package/framework/agents/card-schema.md +18 -1
- package/framework/agents/research-protocol.md +6 -1
- package/framework/scripts/stamp-holistic-audit.js +53 -10
- package/package.json +1 -1
- package/src/commands/configure.js +9 -2
- package/src/commands/doctor.js +65 -0
- package/src/utils/skill-gate.js +139 -0
- 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 };
|
package/src/utils/symlinks.js
CHANGED
|
@@ -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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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);
|