baldart 5.6.0 → 5.8.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 +98 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/prd-card-writer.md +23 -0
- 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 +19 -0
- package/framework/.claude/skills/new/SKILL.md +2 -1
- package/framework/.claude/skills/new/references/final-review.md +36 -1
- package/framework/.claude/skills/new2/CHANGELOG.md +15 -0
- package/framework/.claude/skills/new2/SKILL.md +2 -1
- package/framework/.claude/skills/prd/CHANGELOG.md +32 -0
- package/framework/.claude/skills/prd/SKILL.md +2 -1
- package/framework/.claude/skills/prd/assets/epic-template.yml +6 -0
- package/framework/.claude/skills/prd/references/backlog-phase.md +36 -0
- package/framework/.claude/skills/prd/references/validation-phase.md +30 -0
- 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/workflows/new2.js +24 -3
- package/framework/agents/card-schema.md +50 -0
- 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
|
@@ -290,6 +290,42 @@ The `prd-card-writer` agent owns the entire card writing pipeline:
|
|
|
290
290
|
- Presenting results to the user (the agent returns summary, main context displays it)
|
|
291
291
|
- Proceeding to Step 6 (validation phase)
|
|
292
292
|
|
|
293
|
+
## Launch-wave partition (orchestrator judgment — YOU compute it, not the card-writer)
|
|
294
|
+
|
|
295
|
+
After the card-writer returns the Parallel Execution Map, **you** (the `/prd` orchestrator)
|
|
296
|
+
decide how the epic should be **launched** across `/new` runs — a judgment the mechanical
|
|
297
|
+
card-writer cannot make because it needs the discovery context you hold (Tier, review-profile
|
|
298
|
+
mix, migration, UI/server split). This is the durable half of the "Piano di implementazione
|
|
299
|
+
suggerito" the final message renders (validation-phase.md § Final output).
|
|
300
|
+
|
|
301
|
+
**Decide single-run vs waves (scoped — default single-run).** Keep the epic as ONE run unless
|
|
302
|
+
at least one split trigger fires; if none fires, do NOT tag any wave (single-run is the explicit
|
|
303
|
+
else branch — never fabricate a wave grouping just to have one):
|
|
304
|
+
- **Large batch** — many children over several dependency levels (a long single run drifts the
|
|
305
|
+
trunk mid-batch → merge conflicts instead of fast-forward; keep this rationale generic, do not
|
|
306
|
+
name a stack).
|
|
307
|
+
- **Migration / schema card present** — land it in an EARLY wave so its migration/deploy gate runs
|
|
308
|
+
once and later waves build on the real schema.
|
|
309
|
+
- **Review pressure** — many `review_profile: deep` children; splitting keeps the orchestrator
|
|
310
|
+
context lucid on exactly the cards that need lucid review.
|
|
311
|
+
- **Clear server↔UI cut** — server/data foundation vs UI surfaces is a natural wave boundary
|
|
312
|
+
(UI/E2E cards exercise the server, so they belong in the later wave).
|
|
313
|
+
- **Cross-domain file conflicts** — `execution_strategy.file_conflicts` that straddle a domain
|
|
314
|
+
boundary force sequencing.
|
|
315
|
+
|
|
316
|
+
**Assign waves over the EXISTING groups (one partition, no re-listing of card IDs).** Waves are a
|
|
317
|
+
contiguous grouping of `execution_strategy.groups[]` in `level` order. For each group in a wave,
|
|
318
|
+
set `wave` (1-based, ascending), `wave_name` (a short, unique, human-memorable name derived from
|
|
319
|
+
the wave's dominant domain/role — e.g. "Fondamenta server", "Superfici UI & closer"), and
|
|
320
|
+
`wave_rationale` (one line). Persist them by **editing the epic card** (`backlog/<slug>-00.yml`)
|
|
321
|
+
in the worktree — a targeted Edit adding the three sub-keys to the matching `groups[]` entries.
|
|
322
|
+
Do NOT re-spawn the card-writer; do NOT create a parallel `waves[]` block. Schema:
|
|
323
|
+
`framework/agents/card-schema.md` § "`execution_strategy.groups[].wave*`".
|
|
324
|
+
|
|
325
|
+
**Do NOT persist the engine (`/new` vs `/new2`) or the hard rules** — those are render-time,
|
|
326
|
+
runtime-aware (validation-phase.md § Final output). On a single-run epic, skip this step entirely
|
|
327
|
+
(no wave keys written).
|
|
328
|
+
|
|
293
329
|
## Present and Proceed
|
|
294
330
|
|
|
295
331
|
Present the plan to the user (traceability matrix + parallel execution map) for
|
|
@@ -410,4 +410,34 @@ A run that prints "local `git.trunk_branch` did not fast-forward, handle the fil
|
|
|
410
410
|
yourself and then pull" has FAILED Step 7.5 — go back and resolve it before
|
|
411
411
|
printing the summary.
|
|
412
412
|
|
|
413
|
+
Then, ALWAYS, append the launch plan:
|
|
414
|
+
|
|
415
|
+
### 🚀 Piano di implementazione suggerito (MANDATORY — always render)
|
|
416
|
+
|
|
417
|
+
Derive it from the epic card's `execution_strategy` (the wave partition you persisted in
|
|
418
|
+
backlog-phase.md § "Launch-wave partition"); nothing here is recomputed from scratch. Rules +
|
|
419
|
+
schema: `framework/agents/card-schema.md` § "`execution_strategy.groups[].wave*`" (Launch waves).
|
|
420
|
+
|
|
421
|
+
- **Single-run** (≤1 distinct `wave_name`, e.g. a light-lane 1-child epic): render ONE line —
|
|
422
|
+
`**Lancia in un'unica run:** \`/new <all card IDs>\`` — no table. Stop here.
|
|
423
|
+
- **Waves** (≥2 distinct `wave_name`): render a table, one row per wave in `wave` order:
|
|
424
|
+
|
|
425
|
+
| Wave | Nome | Card | Contenuto |
|
|
426
|
+
|------|------|------|-----------|
|
|
427
|
+
| 1 | Fondamenta server | FEAT-XXXX-01…06 | <wave_rationale> |
|
|
428
|
+
| 2 | Superfici UI & closer | FEAT-XXXX-07…13 | <wave_rationale> |
|
|
429
|
+
|
|
430
|
+
Then, below the table:
|
|
431
|
+
- **Engine (runtime-aware — computed here, never read from the card).** On **Claude** (the
|
|
432
|
+
`Workflow` tool is available) recommend `/new2` for a high-volume wave (off-context batch, no
|
|
433
|
+
interactive gates) and `/new` (classic) for a wave that wants human gates (migration / risky
|
|
434
|
+
review) — a hybrid (`/new` on the risky early wave, `/new2` on the rest) is a valid call. On
|
|
435
|
+
**Codex** recommend `/new` only — `/new2` needs the `Workflow` tool and does not exist there
|
|
436
|
+
(`framework/agents/runtime-portability-protocol.md` § "Workflow acceleration"). Give the exact
|
|
437
|
+
command per wave, e.g. `/new2 FEAT-XXXX-01-FEAT-XXXX-06`.
|
|
438
|
+
- **Hard rule:** niente altro lavoro in parallelo sugli stessi file tra una wave e l'altra.
|
|
439
|
+
- **Post-wave gates:** if `.baldart/overlays/prd.md` declares project-specific between-wave
|
|
440
|
+
cautions (e.g. a migration/deploy gate, a compile check, a stash check), surface them after the
|
|
441
|
+
relevant wave. Omit this line when the overlay declares none — never invent stack-specific steps.
|
|
442
|
+
|
|
413
443
|
Display completed Progress Bar.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 1.1.1 — 2026-07-06
|
|
6
|
+
|
|
7
|
+
- **Feature-gate**: added `requires_feature: has_prd_workflow` frontmatter — this skill is now linked into a consumer only when `features.has_prd_workflow` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
|
|
8
|
+
|
|
5
9
|
## 1.1.0 — 2026-07-02
|
|
6
10
|
|
|
7
11
|
- **Analysis-profile contract (framework v4.94.0)**: PATCH-mode re-discovery passes `PROFILE=impact` to `codebase-architect` (blast-radius scoping per `framework/agents/analysis-profiles.md`).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prd-add
|
|
3
3
|
effort: medium
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
|
+
requires_feature: has_prd_workflow
|
|
5
6
|
description: "Change Request skill for active PRD sessions. Use when the user says /prd-add, 'aggiungi requisito', 'serve anche', 'manca un endpoint', or describes a new requirement that impacts an existing PRD. Also auto-triggered by /prd during discovery when the user's answer reveals a new sub-feature not covered by the original scope. Runs ICIAS impact analysis (semantic scan + structural propagation + scoring) to determine which PRD phases need SKIP/PATCH/REDO, then executes only affected phases."
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -27,13 +27,42 @@ when the skill needs them** — no empty placeholder dirs (that was upstream
|
|
|
27
27
|
name: <kebab-case> # matches the directory name
|
|
28
28
|
effort: medium # low | medium | high | xhigh | max (effort-protocol.md)
|
|
29
29
|
version: 1.0.0 # the skill's OWN SemVer
|
|
30
|
+
requires_feature: has_x # OPTIONAL — install-time feature-gate (see below)
|
|
30
31
|
description: > # triggers + what/when; NO angle brackets
|
|
31
32
|
...
|
|
32
33
|
---
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Order: `name` → `effort` → `version` → `
|
|
36
|
-
tolerated only on imported third-party skills; do not
|
|
36
|
+
Order: `name` → `effort` → `version` → (`requires_feature`) → `description`.
|
|
37
|
+
`license` / `metadata` are tolerated only on imported third-party skills; do not
|
|
38
|
+
add other keys.
|
|
39
|
+
|
|
40
|
+
### `requires_feature` — install-time feature-gate (OPTIONAL)
|
|
41
|
+
|
|
42
|
+
A skill that is only meaningful when a `baldart.config.yml` `features.*` flag is
|
|
43
|
+
on (and already REFUSES at runtime otherwise) declares that dependency here, so
|
|
44
|
+
the installer links it into a consumer ONLY when the feature is enabled. This
|
|
45
|
+
trims Codex's ~2% skill-description budget on projects that don't use the feature.
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
requires_feature: has_design_system # string → link iff true
|
|
49
|
+
requires_feature: [has_design_system, has_e2e_review] # list = AND (ALL true)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **Omit it** for any skill that should always be linked (the default). The five
|
|
53
|
+
bootstrap skills that ENABLE a feature (`design-system-init`, `i18n-adopt`,
|
|
54
|
+
`graphify-bootstrap`, `lsp-bootstrap`, `toolchain-bootstrap`) MUST stay ungated
|
|
55
|
+
— otherwise the feature could never be turned on.
|
|
56
|
+
- The gate is **install-time only** and **portable**: both Claude (`.claude/skills/`)
|
|
57
|
+
and Codex (`.agents/skills/`) simply see fewer directories. Neither runtime reads
|
|
58
|
+
the field — it is ignored exactly like `version`/`effort`. Resolution lives in
|
|
59
|
+
`src/utils/skill-gate.js`; it reads `features.*` directly and **fails open** (a
|
|
60
|
+
missing config, or a malformed frontmatter, links the skill rather than hiding it).
|
|
61
|
+
- **NOT a `baldart.config.yml` key** — it consumes existing `features.*`, so the
|
|
62
|
+
schema-change propagation rule does NOT apply. It is skill-frontmatter metadata.
|
|
63
|
+
- **Invariant** (latent, for future Codex `agents/openai.yaml` metadata): a gated
|
|
64
|
+
skill MUST NOT be declared as a `dependencies:` target of an always-linked skill's
|
|
65
|
+
`openai.yaml` — pruning the gated skill would leave a dangling dependency.
|
|
37
66
|
|
|
38
67
|
## Body headers (in order, when applicable)
|
|
39
68
|
|
|
@@ -39,10 +39,22 @@ def validate_skill(skill_path):
|
|
|
39
39
|
return False, f"Invalid YAML in frontmatter: {e}"
|
|
40
40
|
|
|
41
41
|
# Define allowed properties
|
|
42
|
-
# BALDART additions: `effort` (per-skill reasoning baseline — effort-protocol.md)
|
|
43
|
-
#
|
|
42
|
+
# BALDART additions: `effort` (per-skill reasoning baseline — effort-protocol.md),
|
|
43
|
+
# `version` (per-skill SemVer — canonical structure, see references/skill-structure.md)
|
|
44
|
+
# and `requires_feature` (install-time feature-gate — a skill is linked into a
|
|
45
|
+
# consumer only when this `features.*` flag is on; see references/skill-structure.md
|
|
46
|
+
# and src/utils/skill-gate.js). All three are OPTIONAL frontmatter metadata,
|
|
47
|
+
# ignored by the Claude/Codex skill loaders (they only read name+description).
|
|
44
48
|
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata',
|
|
45
|
-
'effort', 'version'}
|
|
49
|
+
'effort', 'version', 'requires_feature'}
|
|
50
|
+
|
|
51
|
+
# Known feature flags a `requires_feature` value may reference (mirrors the
|
|
52
|
+
# features.* block of baldart.config.template.yml). Used for a SOFT warning
|
|
53
|
+
# only — an unknown value is not fatal (fail-open: the gate links the skill).
|
|
54
|
+
KNOWN_FEATURES = {'has_design_system', 'multi_tenant_theming', 'has_api_docs',
|
|
55
|
+
'has_backlog', 'has_adrs', 'has_prd_workflow', 'has_wiki_overlay',
|
|
56
|
+
'has_lsp_layer', 'has_code_graph', 'has_e2e_review',
|
|
57
|
+
'has_toolchain', 'has_i18n'}
|
|
46
58
|
|
|
47
59
|
# Check for unexpected properties (excluding nested keys under metadata)
|
|
48
60
|
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
|
|
@@ -90,6 +102,16 @@ def validate_skill(skill_path):
|
|
|
90
102
|
# `version` + a per-skill CHANGELOG.md are REQUIRED for new skills but WARNED (not
|
|
91
103
|
# hard-failed) here for backwards-compat with skills authored before the convention.
|
|
92
104
|
warnings = []
|
|
105
|
+
# Soft-validate requires_feature values against the known features.* set.
|
|
106
|
+
if 'requires_feature' in frontmatter:
|
|
107
|
+
req = frontmatter['requires_feature']
|
|
108
|
+
req_list = req if isinstance(req, list) else [req]
|
|
109
|
+
unknown = [str(f) for f in req_list if str(f) not in KNOWN_FEATURES]
|
|
110
|
+
if unknown:
|
|
111
|
+
warnings.append(
|
|
112
|
+
f"`requires_feature` references unknown flag(s): {', '.join(sorted(unknown))}. "
|
|
113
|
+
f"The gate fails open (skill still linked). Known: {', '.join(sorted(KNOWN_FEATURES))}"
|
|
114
|
+
)
|
|
93
115
|
if 'version' not in frontmatter:
|
|
94
116
|
warnings.append("missing `version:` in frontmatter (add `version: 1.0.0`)")
|
|
95
117
|
if 'effort' not in frontmatter:
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 2.0.1 — 2026-07-06
|
|
6
|
+
|
|
7
|
+
- **Feature-gate**: added `requires_feature: has_design_system` frontmatter — this skill is now linked into a consumer only when `features.has_design_system` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
|
|
8
|
+
|
|
5
9
|
## 2.0.0 — 2026-07-03
|
|
6
10
|
|
|
7
11
|
**The local design studio.** Root-level refactor (framework v5.2.0): the
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ui-design
|
|
3
3
|
effort: medium
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.1
|
|
5
|
+
requires_feature: has_design_system
|
|
5
6
|
description: "The local design studio — THE framework skill for designing, prototyping and restyling UI directly in the repo (the internal twin of the Claude Design handoff). Produces context-aware, token-compliant HTML mockups in 3 distinct directions, with a locked Design Read, a deterministic anti-slop craft gate and separated generator/evaluator verification. Use when: (1) designing new pages, screens or components, (2) the /prd skill reaches its UI design phase (Step 3), (3) redesigning/restyling existing pages ('rifai la UI', 'migliora il design', 'restyle'), (4) exploring design options ('3 opzioni', 'opzioni di design', 'direzioni estetiche'), (5) prototyping a page/landing/dashboard locally ('prototipo', 'mockup', 'progetta interfaccia', 'design UI'). SUPERSEDES frontend-design (now a router) and ui-ux-pro-max for ALL design/prototyping work. Not for: implementing an already-approved mockup (→ /ui-implement), in-code UI fixes with no design decision (→ ui-expert agent), motion specs (→ /motion-design)."
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 1.1.2 — 2026-07-06
|
|
6
|
+
|
|
7
|
+
- **Feature-gate**: added `requires_feature: has_design_system` frontmatter — this skill is now linked into a consumer only when `features.has_design_system` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
|
|
8
|
+
|
|
5
9
|
## 1.1.1 — 2026-07-02
|
|
6
10
|
|
|
7
11
|
- Return Contract (SKILL.md): documentati i campi `bindings` + `lane_coverage`/`lanes`
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ui-implement
|
|
3
3
|
effort: medium
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
|
+
requires_feature: has_design_system
|
|
5
6
|
description: >
|
|
6
7
|
Implement an ALREADY-APPROVED mockup into the codebase end-to-end and
|
|
7
8
|
self-verify its fidelity. Takes a mockup in any channel — a Claude Design
|
|
@@ -219,6 +219,11 @@ const PREFLIGHT_SCHEMA = {
|
|
|
219
219
|
// Drives the epic-closure blocklist (an epic with open scope residuals/follow-ups must
|
|
220
220
|
// not close) and the Epic goal section of the final report.
|
|
221
221
|
parentEpic: { type: 'string', description: "parent epic id from group.parent or parent: ('' when none)" },
|
|
222
|
+
// v5.8.0 — the launch-wave this card belongs to, IFF /prd persisted a wave partition on
|
|
223
|
+
// the epic card (optional wave_name on execution_strategy.groups[] whose .cards includes
|
|
224
|
+
// this id — schema: agents/card-schema.md § execution_strategy.groups[].wave*). '' when the
|
|
225
|
+
// epic has no wave tags (single-run / legacy). Drives the "Prossime wave" report line.
|
|
226
|
+
waveName: { type: 'string', description: "launch wave_name from the epic's execution_strategy.groups[] ('' when none)" },
|
|
222
227
|
},
|
|
223
228
|
},
|
|
224
229
|
},
|
|
@@ -305,7 +310,7 @@ try {
|
|
|
305
310
|
g3Bullet +
|
|
306
311
|
`• G4 card-field validation (setup.md 1b/1c, card-schema.md consumer contract): card missing acceptance_criteria/files_likely_touched/scope → EXCLUDE (excluded[] + reason). \`requirements\` is CONDITIONAL, not an auto-exclude: if it is missing BUT acceptance_criteria + scope are both present and non-empty, FAITHFULLY DERIVE it (a concrete restatement/decomposition of the existing AC + scope — faithful, never inventing new scope) and WRITE it back to the card YAML on disk in ${MAIN}/${paths.backlog_dir || 'backlog'} (F-040: main repo, not the worktree copy; the card artifact is NOT a source/doc file, so this is allowed despite the role boundary), logging \`[BACKFILL] <id>: requirements=<N items, AC+scope-derived>\`. Only EXCLUDE for missing requirements when acceptance_criteria OR scope is ALSO empty (a genuinely thin card). Never HALT for one bad card.\n` +
|
|
307
312
|
`• G5 depends-on: a card whose depends_on names a non-DONE card NOT in this batch → EXCLUDE it AND every in-batch card that transitively depends on it.\n` +
|
|
308
|
-
`• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card BUILDS the mocked UI: links.design_src non-empty, OR links.design non-empty AND (component_bindings non-empty OR files_likely_touched intersects component/style/UI source paths). A bare links.design on a docs/test/meta-only card is a POINTER to the epic's explainer, not a build target — v5.6.0, FEAT-0068: the bare test clamped a docs+ADR card to ui-expert; drives the mockup-first build override — implement.md §6a), parentEpic (the card's parent epic id from group.parent or a top-level parent: field; '' when none — drives the epic-closure blocklist + the Epic goal report section), designSrcDir (ABS path of links.design_src / the PRD mockups/_src dir; '' when absent), designHtml (ABS path when links.design is an .html file; '' otherwise), hasBindings (BOOLEAN — non-empty component_bindings), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
|
|
313
|
+
`• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card BUILDS the mocked UI: links.design_src non-empty, OR links.design non-empty AND (component_bindings non-empty OR files_likely_touched intersects component/style/UI source paths). A bare links.design on a docs/test/meta-only card is a POINTER to the epic's explainer, not a build target — v5.6.0, FEAT-0068: the bare test clamped a docs+ADR card to ui-expert; drives the mockup-first build override — implement.md §6a), parentEpic (the card's parent epic id from group.parent or a top-level parent: field; '' when none — drives the epic-closure blocklist + the Epic goal report section), waveName (v5.8.0 — when the parent epic card carries a launch-wave partition, the wave_name of the execution_strategy.groups[] entry whose .cards includes this id; '' when the epic has no wave tags — single-run/legacy; drives the "Prossime wave" report line), designSrcDir (ABS path of links.design_src / the PRD mockups/_src dir; '' when absent), designHtml (ABS path when links.design is an .html file; '' otherwise), hasBindings (BOOLEAN — non-empty component_bindings), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
|
|
309
314
|
`• B1/F-026 idempotency (per card, AFTER the worktree exists): set alreadyCommitted:true (+ alreadyCommittedSha) IFF ALL hold: (a) a commit referencing the card id exists in ${TRUNK}..HEAD of the worktree; (b) the card's validation_commands re-run GREEN right now; (c) NO open follow-up card for it exists in ${paths.backlog_dir || 'backlog'}. On a FRESH worktree ${TRUNK}..HEAD is empty → all false, zero extra work.\n` +
|
|
310
315
|
`• F-016 AC↔ownership consistency: for each acceptance_criterion, derive the file(s) it requires editing. If those files are NOT a subset of the card's MAY-EDIT/files_likely_touched → add the AC to policyDeferredACs:[{n,text,owningCard|owningFile,reason}] (it will become ONE follow-up, never a resolve). Do the same for any AC whose remedy is an owner-gated infra action (remote db push / deploy / secret / DNS).\n` +
|
|
311
316
|
(migrationApplied
|
|
@@ -634,7 +639,11 @@ async function runCard(cardId, cardPath) {
|
|
|
634
639
|
: `Phase 1 fallback: the architect specialist was unavailable — do the Phase 1 claim+architect exploration yourself per ${REF}/implement.md and persist the baseline to ${baselinePath} before coding.`
|
|
635
640
|
// T1.A (v4.90.0) — mockup cards: binding-compliance is verified deterministically by the
|
|
636
641
|
// check-bindings.mjs script (ui-implement Step 5b is the SSOT; this is the new2 parallel).
|
|
637
|
-
|
|
642
|
+
// Guard on has_design_system: component_bindings only exist when a /prd design
|
|
643
|
+
// phase (which requires the design system) produced them, so absent-DS + bindings
|
|
644
|
+
// is a malformed hand-authored card — don't emit a gate whose script path could
|
|
645
|
+
// resolve empty. Mirrors the has_design_system ds-gate guard above.
|
|
646
|
+
const bindingBit = features.has_design_system && node.hasMockup && node.hasBindings
|
|
638
647
|
? `\nBINDING GATE (deterministic — ui-implement Step 5b parallel): after building, run \`node "$(ls .claude/skills/ui-implement/scripts/check-bindings.mjs .framework/framework/.claude/skills/ui-implement/scripts/check-bindings.mjs 2>/dev/null | head -1)" --bindings-json '<the card component_bindings as JSON>' --files <your touched implementation files>\`. Exit 3 = a reuse/reuse-variant binding is NOT used in the code: the map is AUTHORITATIVE — fix it before returning (one pass). Include the script's one-line JSON output as bindingCheck in your return.\n`
|
|
639
648
|
: ''
|
|
640
649
|
let impl
|
|
@@ -1683,7 +1692,19 @@ function buildReport(o) {
|
|
|
1683
1692
|
const epicHigh = residuals.filter((r) => ((graphById[r.card] && graphById[r.card].parentEpic) === e || r.card === e) && (sevRank[residualSeverity(r)] || 0) >= 2)
|
|
1684
1693
|
if (closedEpics.includes(e) && !closureBlockedEpics.has(e)) L.push(`- **${e}: CHIUSA** (children DONE + AC epic verificati dal merge agent)`)
|
|
1685
1694
|
else if (closedEpics.includes(e)) L.push(`- **${e}: ⚠️ CHIUSA CONTRO BLOCKLIST (false DONE)** — riaprire: ${epicHigh.length} residuo/i HIGH aperti`)
|
|
1686
|
-
else
|
|
1695
|
+
else {
|
|
1696
|
+
L.push(`- **${e}: APERTA** — ${reason || (closureBlockedEpics.has(e) ? 'blocklist: residui/follow-up aperti' : 'merge non eseguito o children aperti')}${epicHigh.length ? ` · ${epicHigh.length} residuo/i HIGH` : ''}`)
|
|
1697
|
+
// v5.8.0 — remaining launch waves by NAME (parity with /new final-review F.6). Uses the
|
|
1698
|
+
// waveName threaded through cardGraph (buildReport has no fs access to re-read the epic).
|
|
1699
|
+
// Honest scope: only waves whose cards are IN THIS BATCH are visible — waves entirely
|
|
1700
|
+
// outside the batch aren't loaded, so this reflects the batch, not the whole epic.
|
|
1701
|
+
const openWaves = dedupe(
|
|
1702
|
+
cardGraph
|
|
1703
|
+
.filter((n) => (n.parentEpic || '') === e && n.waveName && (perCardResults.find((r) => r.card === n.id) || {}).status !== 'DONE')
|
|
1704
|
+
.map((n) => n.waveName)
|
|
1705
|
+
)
|
|
1706
|
+
if (openWaves.length) L.push(` - Prossime wave (batch): ${openWaves.map((w) => `**${w}**`).join(' · ')}`)
|
|
1707
|
+
}
|
|
1687
1708
|
}
|
|
1688
1709
|
}
|
|
1689
1710
|
if (finalSummary) {
|
|
@@ -230,6 +230,56 @@ Rules:
|
|
|
230
230
|
- Omit any sub-field whose session id is unavailable (env var unset) rather than writing a
|
|
231
231
|
placeholder.
|
|
232
232
|
|
|
233
|
+
### `execution_strategy.groups[].wave*` — launch-wave annotation (epic-only, optional, orchestrator-written)
|
|
234
|
+
|
|
235
|
+
The epic's `execution_strategy.groups[]` is the **single card-ID partition** consumed by `/new`
|
|
236
|
+
for parallelism (`groups[].level` + `.cards` — the machine schedule). A **launch wave** is a
|
|
237
|
+
coarser, human-facing partition layered ON TOP of those same groups so the user can sequence the
|
|
238
|
+
epic across multiple `/new` runs and refer to each run by a stable NAME. It is expressed as three
|
|
239
|
+
**optional sub-fields on the existing `groups[]` entries** — never a parallel block that re-lists
|
|
240
|
+
card IDs (that would be a second SSOT that drifts from `groups[].cards`):
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
execution_strategy:
|
|
244
|
+
recommended_mode: team | sequential # existing — scheduling shape, runtime-neutral
|
|
245
|
+
groups:
|
|
246
|
+
- level: 0
|
|
247
|
+
cards: [FEAT-0069-01, FEAT-0069-02] # existing — the ONLY card-ID partition
|
|
248
|
+
description: "Foundation/scaffold" # existing
|
|
249
|
+
wave: 1 # opt — 1-based wave index (contiguous over groups)
|
|
250
|
+
wave_name: "Fondamenta server" # opt — short, unique human NAME for the wave
|
|
251
|
+
wave_rationale: "migration + config + resolver + enforcement server" # opt — why this wave
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Rules:
|
|
255
|
+
- **Optional (`C`)**, epic-only. They are **sub-keys of `execution_strategy`** (already `R`), NOT
|
|
256
|
+
top-level fields — the field-state matrix carries **no new row** and the baseline validator (which
|
|
257
|
+
checks only top-level presence/non-emptiness) never sees them. Legacy epics lack them → no HALT,
|
|
258
|
+
no migration.
|
|
259
|
+
- **Ownership split (same as [`provenance`](#provenance-session-id-traceability--optional-skill-written)):**
|
|
260
|
+
the `/prd` **orchestrator** COMPUTES the wave partition + names + rationale (a launch judgment that
|
|
261
|
+
needs discovery context — migration-early, review-`deep` pressure, server↔UI cut — which the
|
|
262
|
+
mechanical `prd-card-writer` does not hold), and it is PERSISTED onto the epic card. The card-writer
|
|
263
|
+
emits these keys ONLY when handed them; otherwise it **omits** them.
|
|
264
|
+
- **Contiguity invariant:** waves partition the groups in `level` order — a wave is a contiguous run
|
|
265
|
+
of groups. `wave` numbers ascend 1,2,3…; every group in the same wave shares one `wave_name`.
|
|
266
|
+
- **Single-run:** ≤1 distinct `wave_name` across all groups (e.g. a light-lane 1-child epic) means
|
|
267
|
+
"launch in one run" — the orchestrator leaves the wave keys ABSENT rather than tagging everything
|
|
268
|
+
`wave: 1`.
|
|
269
|
+
|
|
270
|
+
**Launch waves (render-time — NOT persisted):** the human-facing launch plan is derived, never
|
|
271
|
+
stored beyond the three keys above:
|
|
272
|
+
- The **wave view** = group the epic's `groups[]` by `wave_name`, ordered by `wave`; each wave's
|
|
273
|
+
cards = the union of its groups' `.cards`. Consumers: `/prd` final message ("Piano di
|
|
274
|
+
implementazione suggerito") and `/new`/`/new2` final message ("prossime wave rimanenti", joining
|
|
275
|
+
wave membership with each card's on-disk `status`).
|
|
276
|
+
- The **engine recommendation** (`/new` vs `/new2`) is computed at render time and is **runtime-aware**
|
|
277
|
+
— `/new2` needs the Workflow tool and is **Claude-only** (see `framework/agents/runtime-portability-protocol.md`
|
|
278
|
+
§ "Workflow acceleration" — *no Codex equivalent*). It is **never persisted** on the card: a durable,
|
|
279
|
+
cross-tool artifact must not carry a runtime-capability recommendation a Codex consumer cannot execute.
|
|
280
|
+
- The anti-parallel hard rule ("no other work on the same files between waves") and any project-specific
|
|
281
|
+
post-wave gates (surfaced from `.baldart/overlays/prd.md`) are render-time constants, likewise not persisted.
|
|
282
|
+
|
|
233
283
|
## Acceptance-criteria grammar (EARS) + per-AC oracles (since the EARS+verify wave)
|
|
234
284
|
|
|
235
285
|
**Why**: a prose AC passes gates without being verifiable ("handle it correctly"), and
|
package/package.json
CHANGED
|
@@ -1508,10 +1508,17 @@ async function configure(opts = {}) {
|
|
|
1508
1508
|
const enabledTools = (merged.tools && Array.isArray(merged.tools.enabled) && merged.tools.enabled.length)
|
|
1509
1509
|
? merged.tools.enabled : ['claude'];
|
|
1510
1510
|
if (fs.existsSync(path.join(cwd, '.framework'))) {
|
|
1511
|
-
new SymlinkUtils(cwd)
|
|
1511
|
+
const symlinks = new SymlinkUtils(cwd);
|
|
1512
|
+
symlinks.mergeAgents({ tools: enabledTools });
|
|
1513
|
+
// Also re-merge SKILLS: the feature-gate (src/utils/skill-gate.js) depends
|
|
1514
|
+
// on the `features.*` just written. On first install `mergeSkills` ran
|
|
1515
|
+
// fail-open BEFORE this config existed (linking everything), so this pass
|
|
1516
|
+
// is what actually prunes the gated-off skills. Without it, gating never
|
|
1517
|
+
// takes effect until the next `baldart update`.
|
|
1518
|
+
symlinks.mergeSkills({ tools: enabledTools });
|
|
1512
1519
|
}
|
|
1513
1520
|
} catch (err) {
|
|
1514
|
-
UI.warning(`Agent re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
|
|
1521
|
+
UI.warning(`Agent/skill re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
|
|
1515
1522
|
}
|
|
1516
1523
|
|
|
1517
1524
|
// v5.1.0 — research library: create + seed ${paths.research_dir} right after
|
package/src/commands/doctor.js
CHANGED
|
@@ -409,6 +409,47 @@ async function detectState(cwd, opts = {}) {
|
|
|
409
409
|
}
|
|
410
410
|
} catch (_) { /* never block doctor on probe */ }
|
|
411
411
|
|
|
412
|
+
// ---- Feature-gated skill drift (skill-gate) ------------------------
|
|
413
|
+
// A skill whose `requires_feature` frontmatter is no longer satisfied
|
|
414
|
+
// (feature flipped off in baldart.config.yml) may still be linked from a
|
|
415
|
+
// prior install. `configure` and `update` both re-merge (which prunes), but
|
|
416
|
+
// a user who edits the config and runs ONLY `doctor` should be told. The
|
|
417
|
+
// reverse (a feature just turned ON, gated skill not yet linked) is covered
|
|
418
|
+
// by the missing-skills path. Advisory only (fix = `reconcile-skills`, a
|
|
419
|
+
// plain re-merge); never blocks. Fully fail-safe.
|
|
420
|
+
state.gatedSkillsLinked = [];
|
|
421
|
+
try {
|
|
422
|
+
if (state.frameworkPresent) {
|
|
423
|
+
const { resolveSkillGate } = require('../utils/skill-gate');
|
|
424
|
+
const { getAdapter } = require('../utils/tool-adapters');
|
|
425
|
+
const frameworkSkillsDir = path.join(cwd, '.framework', 'framework', '.claude', 'skills');
|
|
426
|
+
const gate = resolveSkillGate(frameworkSkillsDir, cwd);
|
|
427
|
+
if (!gate.failOpen && gate.gatedOff.length) {
|
|
428
|
+
const toolsEnabled = (config && !config.__malformed && config.tools && Array.isArray(config.tools.enabled))
|
|
429
|
+
? config.tools.enabled : ['claude'];
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
for (const toolName of toolsEnabled) {
|
|
432
|
+
let adapter;
|
|
433
|
+
try { adapter = getAdapter(toolName, cwd); } catch (_) { continue; }
|
|
434
|
+
const skillsRel = adapter.skillsDir();
|
|
435
|
+
for (const name of gate.gatedOff) {
|
|
436
|
+
if (seen.has(name)) continue;
|
|
437
|
+
const linkPath = path.join(cwd, skillsRel, name);
|
|
438
|
+
try {
|
|
439
|
+
const lst = fs.lstatSync(linkPath);
|
|
440
|
+
if (!lst.isSymbolicLink()) continue;
|
|
441
|
+
const resolved = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
442
|
+
if (resolved === path.join(frameworkSkillsDir, name)) {
|
|
443
|
+
seen.add(name);
|
|
444
|
+
state.gatedSkillsLinked.push(name);
|
|
445
|
+
}
|
|
446
|
+
} catch (_) { /* absent */ }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch (_) { /* never block doctor on probe */ }
|
|
452
|
+
|
|
412
453
|
// ---- Workflow symlink integrity (since v4.14.0) --------------------
|
|
413
454
|
// Dynamic workflows (.js) are Claude-only and per-item symlinked into
|
|
414
455
|
// `.claude/workflows/`. Unlike agents there is NO hard discovery gate —
|
|
@@ -1086,6 +1127,30 @@ function planActions(state) {
|
|
|
1086
1127
|
});
|
|
1087
1128
|
}
|
|
1088
1129
|
|
|
1130
|
+
// Feature-gated skill drift (skill-gate). Advisory — these skills' gating
|
|
1131
|
+
// feature is now off, but they're still linked from a prior install; they'd
|
|
1132
|
+
// only refuse at runtime and waste the Codex skill-description budget. A plain
|
|
1133
|
+
// re-merge prunes them (same rationale as the agent/workflow repairs).
|
|
1134
|
+
if (state.gatedSkillsLinked && state.gatedSkillsLinked.length > 0) {
|
|
1135
|
+
actions.push({
|
|
1136
|
+
key: 'reconcile-skills',
|
|
1137
|
+
label: `Unlink ${state.gatedSkillsLinked.length} feature-gated skill(s)`,
|
|
1138
|
+
why: `These skills are still linked but their gating feature is now off in baldart.config.yml: ${state.gatedSkillsLinked.join(', ')}. They refuse at runtime and consume Codex's ~2% skill-description budget. Re-merging prunes them (re-enable the feature to bring them back).`,
|
|
1139
|
+
autoOk: true,
|
|
1140
|
+
run: async () => {
|
|
1141
|
+
const SymlinkUtils = require('../utils/symlinks');
|
|
1142
|
+
const cfg = loadConfig(process.cwd());
|
|
1143
|
+
const enabledTools = (cfg && !cfg.__malformed
|
|
1144
|
+
&& cfg.tools && Array.isArray(cfg.tools.enabled))
|
|
1145
|
+
? cfg.tools.enabled
|
|
1146
|
+
: ['claude'];
|
|
1147
|
+
const symlinks = new SymlinkUtils();
|
|
1148
|
+
symlinks.mergeSkills({ tools: enabledTools });
|
|
1149
|
+
UI.success(`Reconciled feature-gated skills for tools: ${enabledTools.join(', ')}`);
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1089
1154
|
// Root primitives integrity (since v4.83.0). Regenerates AGENTS.md / CLAUDE.md
|
|
1090
1155
|
// from the versioned skeleton + config + overlay when missing, still a legacy
|
|
1091
1156
|
// symlink, or drifted. Offline + idempotent. A handwritten file is NOT touched
|
|
@@ -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 };
|