baldart 3.18.0 → 3.19.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 +73 -0
- package/README.md +2 -1
- package/VERSION +1 -1
- package/bin/baldart.js +30 -0
- package/framework/.claude/commands/design-review.md +129 -8
- package/framework/.claude/hooks/framework-edit-gate.js +5 -1
- package/framework/.claude/skills/overlay/SKILL.md +214 -0
- package/framework/templates/overlays/README.md +13 -0
- package/package.json +1 -1
- package/src/commands/overlay.js +381 -0
- package/src/utils/overlay-merger.js +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,79 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.19.0] - 2026-05-26
|
|
9
|
+
|
|
10
|
+
`.baldart/overlays/` smette di essere un percorso "andare al buio". Fino a v3.18.x, un consumer che voleva personalizzare uno skill / agent / command doveva ricordare a memoria tre canali distinti, due modelli di merging (concat per le skill, marker-based per agent/command), il path overlay corretto per ogni canale, lo schema frontmatter (`base_<kind>`, `base_<kind>_version`, `mode`) e la versione framework da targetare — il tutto senza nessun feedback su drift quando il base file cambiava upstream. L'unico touchpoint guida era il messaggio del hook `framework-edit-gate` quando bloccava un edit dentro `.framework/`, che diceva "metti la roba in `.baldart/overlays/<skill>.md`" senza spiegare *quale path esatto*, *quale schema*, *quale versione*. Risultato: overlay scritti raramente o male.
|
|
11
|
+
|
|
12
|
+
Questa release introduce uno skill `/overlay` + tre nuovi sotto-comandi CLI (`baldart overlay scaffold|validate|drift`) che colmano il gap end-to-end, seguendo il pattern stabilito da `/baldart-push` (skill = guidance, CLI = source of truth). Nessuna nuova chiave in `baldart.config.yml`: gli overlay vivono su path universali che non dipendono dalla configurazione del consumer.
|
|
13
|
+
|
|
14
|
+
### Added — Skill `/overlay`
|
|
15
|
+
|
|
16
|
+
- **Nuova skill [framework/.claude/skills/overlay/SKILL.md](framework/.claude/skills/overlay/SKILL.md)**. Thin conversational wrapper attorno al nuovo sotto-comando CLI. Sette fasi: pre-check (`.framework/` esiste?), discovery (skill/agent/command + nome), inspect base + overlay esistente, scaffold via CLI, author body con Edit, validate via CLI, drift maintenance. La skill non re-implementa mai la logica del merger — invoca sempre `npx baldart overlay <sub>`. Quattro hard rules: mai editare `.framework/` direttamente, mai editare file con marker `<!-- baldart-generated: -->`, mai inventare `base_file_sha` o `base_<kind>_version` (computati dalla CLI), mai re-implementare i marker sezionali (vivono in `overlay-merger.js`). Frontmatter porta `contamination_scan: skip` perché contiene esempi illustrativi con token di overlay reali.
|
|
17
|
+
|
|
18
|
+
### Added — Sotto-comando CLI `baldart overlay {scaffold,validate,drift}`
|
|
19
|
+
|
|
20
|
+
- **Nuovo file [src/commands/overlay.js](src/commands/overlay.js)** + dispatcher aggiunto in [bin/baldart.js](bin/baldart.js) come gruppo `overlay` (stesso pattern di `routines`). Tre sotto-comandi:
|
|
21
|
+
- `baldart overlay scaffold <type>/<name> [--mode extend|override]` — idempotente. Valida che il base esista in `.framework/`, computa `base_file_sha` (sha256 12-char) dal contenuto base, legge framework `VERSION`, scrive `.baldart/overlays/<path>` con frontmatter completo e body placeholder appropriato al kind (concat per skill, marker-based per agent/command). Refuse pulito se l'overlay esiste già — punta l'utente a `drift` o all'Edit diretto.
|
|
22
|
+
- `baldart overlay validate <path>` — dry-run del merger. Per agent/command invoca `mergeOverlay()` e riporta numero linee generate + warning (es. heading non matchato nel base, che diventa nuova sezione in coda). Per skill valida frontmatter YAML + esistenza base. Confronta `base_file_sha` overlay vs SHA attuale e segnala se il base è cambiato.
|
|
23
|
+
- `baldart overlay drift [<path>] [--all]` — drift report cross-overlay. SHA-based quando `base_file_sha` è presente nel frontmatter, version-based fallback altrimenti. Stampa summary `{clean, drifted, unknown}` su tutti gli overlay sotto `.baldart/overlays/` (root + `agents/` + `commands/`). Skip `*.example.md` e `README.md`.
|
|
24
|
+
- **Pre-check fail-safe**: ogni sotto-comando rifiuta con messaggio chiaro quando `.framework/` non esiste (consumer non installato). Non assume mai stato.
|
|
25
|
+
|
|
26
|
+
### Changed — `overlay-merger.js` arricchito (additivo, retro-compat)
|
|
27
|
+
|
|
28
|
+
- **[src/utils/overlay-merger.js](src/utils/overlay-merger.js)**: nuova funzione esportata `computeBaseFileSha(content): string` (12-char sha256 prefix) usata dal CLI scaffolder per scrivere `base_file_sha` nel frontmatter overlay. Il marker `<!-- baldart-generated: -->` ora include opzionalmente `base_sha=<sha>` come campo aggiuntivo; `readMarker()` lo legge se presente, altrimenti ritorna `null` per quel campo (retro-compatibile con file generati pre-v3.19.0). Il merger continua a produrre output bit-identico nei casi senza `base_file_sha` nel overlay frontmatter — l'arricchimento è opt-in via lo scaffolder.
|
|
29
|
+
- **YAML SHA quote**: il scaffolder quota il sha (`base_file_sha: "fd940fcc1786"`) perché un 12-char hex può essere tutto-digit, che YAML interpreterebbe come numero (`0` per `000000000000`). `parseFrontmatter()` lato CLI coerce sempre a string per difensività.
|
|
30
|
+
|
|
31
|
+
### Changed — Hook `framework-edit-gate` con handoff `/overlay`
|
|
32
|
+
|
|
33
|
+
- **[framework/.claude/hooks/framework-edit-gate.js](framework/.claude/hooks/framework-edit-gate.js)**: due piccoli edit testuali (logica del hook intatta). Nel messaggio "(B) Project-specific opinion" e nel messaggio "Cannot edit BALDART-generated file" la guida ora suggerisce di invocare `/overlay` per scaffolding guidato + drift check. La consistenza è garantita perché skill, CLI e hook vengono distribuiti insieme dal `baldart update`.
|
|
34
|
+
|
|
35
|
+
### Schema change propagation
|
|
36
|
+
|
|
37
|
+
- **Nessuna nuova chiave in `baldart.config.yml`** — gli overlay vivono su path universali (`.framework/`, `.baldart/overlays/`) non parametrizzati. La regola di propagazione schema (template + configure prompt + update detector + doctor) NON si applica a questa release.
|
|
38
|
+
- **Schema change additivo nel frontmatter overlay**: nuovo campo opzionale `base_file_sha`. Retro-compatibile — overlay scritti pre-v3.19.0 senza questo campo restano validi; `drift` li classifica come "unknown" (drift detection by version only) invece di errori. Documentato in `framework/templates/overlays/README.md` come campo opzionale via il body placeholder dello scaffolder.
|
|
39
|
+
|
|
40
|
+
### Versioning rationale
|
|
41
|
+
|
|
42
|
+
MINOR (3.18.1 → 3.19.0): added capability (nuova skill + nuovi sotto-comandi CLI). No breaking change: nessuna API esistente modificata semanticamente; `readMarker()` resta backward-compatible; il merger produce lo stesso output per ogni input pre-esistente. Decision tree CLAUDE.md: "Added an agent / command / skill / routine / template → MINOR" qualifica.
|
|
43
|
+
|
|
44
|
+
### Verification (manuale, no test suite)
|
|
45
|
+
|
|
46
|
+
Tested in sandbox `/tmp/overlay-test` con symlink `.framework/ → /Users/antoniobaldassarre/BALDART` per simulare un consumer:
|
|
47
|
+
|
|
48
|
+
1. `baldart overlay scaffold agents/coder` → overlay creato a `.baldart/overlays/agents/coder.md` con frontmatter completo (`base_agent: coder`, `base_agent_version: 3.19.0`, `base_file_sha: "<sha>"`, `mode: extend`).
|
|
49
|
+
2. `baldart overlay validate .baldart/overlays/agents/coder.md` → dry-run del merger, "Overlay valid (agent, marker model), 350 lines generated".
|
|
50
|
+
3. `baldart overlay drift --all` → cross-overlay drift report, summary corretto.
|
|
51
|
+
4. Drift positivo: alterazione manuale di `base_file_sha` nel frontmatter → `drift` segnala "drifted (sha mismatch)".
|
|
52
|
+
5. Scaffold idempotente: re-run con overlay esistente → "Overlay already exists, run drift to check it".
|
|
53
|
+
6. Skill scaffolding: `baldart overlay scaffold bug` → overlay skill creato a `.baldart/overlays/bug.md` con `base_skill: bug`, body concat-friendly.
|
|
54
|
+
7. Pre-check: `baldart overlay scaffold ...` in dir senza `.framework/` → exit 1 con messaggio chiaro.
|
|
55
|
+
|
|
56
|
+
## [3.18.1] - 2026-05-25
|
|
57
|
+
|
|
58
|
+
Chiusura del debito tecnico introdotto da v3.18.0. Il release v3.18.0 ha annunciato che `/design-review` supporta un programmatic JSON output mode invocabile da `/e2e-review` per gating uniforme — ma l'implementazione era una sola nota nel frontmatter ("se invocato con `mode: programmatic` ritorna JSON") senza body command che la concretizzasse, senza schema vincolato, senza mode-detection deterministica, senza taxonomy mapping tra le severity Markdown (Blockers/High/Medium/Nitpicks) e le severity programmatic (critical/major/minor) del `visual-fidelity-verifier`. Il risultato: una feature half-implemented, parsable only in teoria. Questo PATCH la rende reale.
|
|
59
|
+
|
|
60
|
+
L'adversarial review del piano v3.19.0 (multi-viewport + interactive states + Phase 4b design judgment pass) ha esplicitamente identificato il problema: "`/design-review` programmatic JSON è vaporware in v3.18.0 — va prima implementato bene come release a sé". Questa release fa esattamente quello, separando la chiusura del debito dall'aggiunta di nuove feature, in modo che v3.19.0 (quando arriverà, post-dogfooding) possa appoggiarsi a un contratto programmatic stabile invece di costruirsi sopra una stub.
|
|
61
|
+
|
|
62
|
+
### Fixed — `/design-review` dual-mode reale (Markdown ↔ JSON)
|
|
63
|
+
|
|
64
|
+
- **[framework/.claude/commands/design-review.md](framework/.claude/commands/design-review.md) riscritto** (frontmatter invariato, body completamente sostituito). Quattro sezioni nuove + workflow rivisto:
|
|
65
|
+
- **§ Invocation Modes**: documenta esplicitamente Mode A (interactive, Markdown — il comportamento storico) e Mode B (programmatic, JSON-only — il nuovo contratto). L'envelope JSON di input per Mode B è specificato verbatim: `{mode, card_id, route, viewport, dev_server_port, registry_paths: {index, tokens, components_dir}, ui_guidelines_path, features: {has_design_system, multi_tenant_theming}, tolerance}`. Il comando lo riceve passato dall'orchestratore `/e2e-review` quando Phase 4b sarà introdotta.
|
|
66
|
+
- **§ Programmatic output schema**: schema JSON di output vincolato, allineato verbatim a `framework/.claude/agents/visual-fidelity-verifier.md` § "Output Schema" per uniformità di parsing nell'aggregatore orchestratore. Campo `source: "design-review"` distingue le finding di questo command da quelle del verifier in modalità debug; i campi `findings[].severity`/`category`/`description`/`expected`/`actual`/`fix_hint`/`evidence`/`ds_drift_code`/`confidence` sono shape-identici. La deduplicazione cross-source avviene per `(route, category, ds_drift_code)` con precedenza al verifier quando duplicate (più strutturato).
|
|
67
|
+
- **§ Severity normalization (Markdown ↔ programmatic)**: tabella di mapping deterministica Blocker → `critical`, High → `major`, Medium → `minor`, Nitpick → DROP. Il drop dei Nitpick in modalità programmatic è deliberato (troppo noise per gating; restano nel report Markdown per uso interattivo).
|
|
68
|
+
- **§ Programmatic-mode category taxonomy**: enumera le 10 categorie ammesse in Mode B (`primitive-reinvented`, `primitive-missing-spec`, `token-bypass`, `component-stale`, `index-drift`, `authority-violation`, `brand-voice-drift`, `a11y-contrast`, `a11y-focus-visible`, `interactive-state-missing`) — sottoinsieme della tassonomia canonica del verifier adattato allo scope di design judgment di questo command. Categorie non in lista vanno emesse come stringhe libere in `gaps[]`, MAI come nuove categorie inventate (l'orchestratore non sa pesarle).
|
|
69
|
+
- **§ Workflow (both modes)**: passi 0–5 unificati per Mode A e Mode B, con la divergenza solo nel formato di output finale (passi 4 e 5). Mode B usa `dev_server_port` dall'envelope per costruire URL, salva screenshot in `/tmp/design-review/<route-slug>.png` per cross-reference dall'orchestratore.
|
|
70
|
+
- **§ Mode-detection contract**: regola deterministica anti-ambiguità — Mode B è triggered SOLO se l'input contiene la substring esatta `"mode": "programmatic"` (con quote, come appare nell'envelope JSON). Vietato auto-detection da contesto invocazione ("sto girando dentro a una skill") — produce silent Markdown leakage nel parser dell'orchestratore che rompe il gate. Su envelope malformato emette `{status: "error", source: "design-review", reason: "malformed_input_envelope", findings: []}` invece di fallback a Mode A (il fallback silenzioso bloccherebbe la card invece di segnalare).
|
|
71
|
+
|
|
72
|
+
### Notes
|
|
73
|
+
|
|
74
|
+
- **Nessun cambio framework payload oltre al command.** Nessun nuovo agente, nessuna nuova skill, nessuna nuova chiave config, nessuna modifica a `/new` o `/e2e-review`. Solo il command `/design-review` viene reso effettivamente dual-mode come annunciato in v3.18.0. `/e2e-review` non lo invoca ancora — quella connessione (Phase 4b) viene rimandata a una future release post-dogfooding, sulla base di dati reali su quanto valore aggiunge la judgment pass sopra il `visual-fidelity-verifier`.
|
|
75
|
+
- **Backward compatibility totale per uso interattivo**: utenti che invocano `/design-review /merchant/dashboard` continuano a ricevere il Markdown report identico a v3.17.x e prima. Mode A è il default; Mode B è opt-in via envelope JSON esplicito.
|
|
76
|
+
- **Severity taxonomy in lockstep**: il body del command include una nota esplicita "quando aggiorni la taxonomy qui, aggiorna in lockstep `framework/.claude/agents/visual-fidelity-verifier.md` § Severity Taxonomy". Le due fonti restano allineate per evitare drift cross-source nell'aggregatore.
|
|
77
|
+
- **Versioning rationale.** PATCH (3.18.0 → 3.18.1): no new capability. Il contratto Mode B era già annunciato in v3.18.0; questa release rende l'annuncio reale. Decision tree CLAUDE.md: "Doc fix, script bugfix, perf, security patch with no behaviour change → PATCH" — qualifica perché la behavior change è solo per chi sta ancora costruendo l'integrazione (nessun consumer esistente la invoca programmatically oggi).
|
|
78
|
+
- **Verification** (manuale, no test suite): (1) `/design-review /merchant/dashboard` interattivo → Markdown identico al pre-3.18.1 (regression check Mode A); (2) input contenente `"mode": "programmatic"` → JSON-only output, no preamble, parsable da `JSON.parse()`; (3) envelope malformato → emette `{status: "error", reason: "malformed_input_envelope"}` senza fallback a Markdown; (4) campi `route`/`dev_server_port` mancanti dall'envelope → stesso errore; (5) finding di severity Nitpick → presente in Mode A, assente da Mode B `findings[]`.
|
|
79
|
+
- **Path post-3.18.1**: dogfooding di v3.18.0 + 3.18.1 per 1–2 settimane su feature UI reali. Sulla base di falsi positivi/negativi raccolti, decidere data-driven quali estensioni includere in v3.19.0 tra: multi-viewport (mobile/desktop/tablet), interactive states (hover/focus/active/disabled + opt-in loading/empty/error), Phase 4b design judgment pass che invoca `/design-review` in Mode B all'interno di `/e2e-review`. L'adversarial review ha esplicitamente sconsigliato il bundle v3.19.0 senza dati di dogfooding — questa release rispetta quella raccomandazione.
|
|
80
|
+
|
|
8
81
|
## [3.18.0] - 2026-05-25
|
|
9
82
|
|
|
10
83
|
The end-to-end review fase post-implementazione diventa un **gate deterministico e BLOCKING** invece di un assortimento di check advisory. Until v3.17.x, la chiusura di una card via `/new` attraversava tre fasi eterogenee tutte non-bloccanti sul fronte UI: Phase 2.5 (Implementation Completeness Check) validava solo card AC / PRD alignment / code quality / schema / artifacts senza alcun gate visivo o funzionale runtime; Phase 2.6 (E2E Testing) era *condizionale* (solo con `test_plan.e2e_required: true` o QA profile BALANCED/DEEP) e invocava `qa-sentinel` che per charter (`qa-sentinel.md:151-161`) esegue solo gate meccanici (lint/type/test/build/audit) — non test browser-based né design; Phase 2.7 (Visual Design Review) invocava `ui-expert` ma era esplicitamente marcata "advisory, non-blocking" (`new/SKILL.md:897` — "findings logged but do NOT block commit"). Le skill `webapp-testing` e `playwright-skill` esistevano ma non erano mai auto-invocate da `/new`. La routine `ds-drift` (`framework/routines/ds-drift.routine.yml`) girava settimanalmente, catturando il drift visivo 1–7 giorni dopo il merge. **Conseguenza concreta** lamentata dall'utente: doveva rifare manualmente la review schermata-per-schermata di ogni card UI, e i dettagli grafici dei mockup venivano regolarmente ignorati.
|
package/README.md
CHANGED
|
@@ -208,7 +208,7 @@ Skills always-ask when required keys are missing — never silently default.
|
|
|
208
208
|
never overwrites your file. Full guide:
|
|
209
209
|
[`framework/docs/PROJECT-CONFIGURATION.md`](framework/docs/PROJECT-CONFIGURATION.md).
|
|
210
210
|
|
|
211
|
-
### Skills (
|
|
211
|
+
### Skills (28 portable skills)
|
|
212
212
|
|
|
213
213
|
Skills live under `.claude/skills/` and are auto-discovered by Claude Code.
|
|
214
214
|
Bundled skills:
|
|
@@ -219,6 +219,7 @@ Bundled skills:
|
|
|
219
219
|
- **Product**: `seo-audit`, `copywriting`, `api-design-principles`
|
|
220
220
|
- **Knowledge**: `doc-writing-for-rag`, `capture` (LLM wiki overlay)
|
|
221
221
|
- **Integration**: `kie-ai`, `remotion-best-practices`
|
|
222
|
+
- **Framework**: `baldart-push` (upstream contribution), `overlay` (v3.19.0 — guided overlay author)
|
|
222
223
|
|
|
223
224
|
### Registry-First UI Protocol (new in v3.11.0)
|
|
224
225
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.19.0
|
package/bin/baldart.js
CHANGED
|
@@ -115,6 +115,36 @@ program
|
|
|
115
115
|
await doctorCommand({ auto: !!options.auto, offline: !!options.offline });
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
const overlayGroup = program
|
|
119
|
+
.command('overlay')
|
|
120
|
+
.description('Author and check .baldart/overlays/ — scaffolds, validates, and detects drift on skill/agent/command overlays');
|
|
121
|
+
|
|
122
|
+
overlayGroup
|
|
123
|
+
.command('scaffold <target>')
|
|
124
|
+
.description('Create a new overlay file (e.g. `scaffold agents/coder`, `scaffold bug`)')
|
|
125
|
+
.option('--mode <mode>', 'Overlay mode: extend (default) or override (skill-only)', 'extend')
|
|
126
|
+
.action(async (target, options) => {
|
|
127
|
+
const overlay = require('../src/commands/overlay');
|
|
128
|
+
await overlay.scaffold(target, options);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
overlayGroup
|
|
132
|
+
.command('validate <path>')
|
|
133
|
+
.description('Dry-run the merger on an overlay and report any errors / warnings')
|
|
134
|
+
.action(async (overlayPath) => {
|
|
135
|
+
const overlay = require('../src/commands/overlay');
|
|
136
|
+
await overlay.validate(overlayPath);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
overlayGroup
|
|
140
|
+
.command('drift [path]')
|
|
141
|
+
.description('Check whether the base file changed since an overlay was authored. Without `path`, checks all overlays.')
|
|
142
|
+
.option('--all', 'Check every overlay under .baldart/overlays/ (default when no path is given)')
|
|
143
|
+
.action(async (overlayPath, options) => {
|
|
144
|
+
const overlay = require('../src/commands/overlay');
|
|
145
|
+
await overlay.drift(overlayPath, options);
|
|
146
|
+
});
|
|
147
|
+
|
|
118
148
|
const routinesGroup = program
|
|
119
149
|
.command('routines')
|
|
120
150
|
.description('Manage scheduled routines (wiki-review, doc-review, code-review, skill-improve, ds-drift, full-sweep)');
|
|
@@ -2,19 +2,140 @@
|
|
|
2
2
|
description: Trigger the design-review subagent with MCP Playwright for the specified route.
|
|
3
3
|
allowed-tools: Bash, Grep, LS, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash, ListMcpResourcesTool, ReadMcpResourceTool, mcp__playwright__browser_navigate, mcp__playwright__browser_click, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages
|
|
4
4
|
---
|
|
5
|
-
> **Invocation mode** — When invoked **interactively** by the user, produce the Markdown report described below (Blockers/High/Medium/Nitpicks). When invoked **programmatically** by the `/e2e-review` skill (input includes `"mode": "programmatic"`), return a single JSON object matching the `visual-fidelity-verifier` output schema (`framework/.claude/agents/visual-fidelity-verifier.md` § "Output Schema") so the orchestrator can parse it for gating. No prose preamble in programmatic mode.
|
|
6
5
|
|
|
7
|
-
You are the design-review subagent. Ensure `npm run design-review` is running before continuing so Playwright MCP can expose `localhost:9221` to Claude.
|
|
6
|
+
You are the design-review subagent. Ensure `npm run design-review` is running before continuing so Playwright MCP can expose `localhost:9221` to Claude.
|
|
7
|
+
|
|
8
|
+
## Invocation Modes (since v3.18.1)
|
|
9
|
+
|
|
10
|
+
This command supports two output modes, detected from the input prompt:
|
|
11
|
+
|
|
12
|
+
### Mode A — Interactive (default)
|
|
13
|
+
|
|
14
|
+
When the user invokes `/design-review` directly (input is a natural-language route reference, e.g. `/design-review /merchant/dashboard`), produce the **Markdown report** described in steps 1–5 below. Use the Blockers/High/Medium/Nitpicks template from `agents/design-review.md` § Report Template.
|
|
15
|
+
|
|
16
|
+
### Mode B — Programmatic (invoked by `/e2e-review`)
|
|
17
|
+
|
|
18
|
+
When the input contains a JSON envelope with `"mode": "programmatic"` at any position (typical payload from `/e2e-review` Phase 4b), switch to **JSON-only output**. No prose preamble, no Markdown, no closing remarks — a single JSON object as the final message, parseable by `JSON.parse()` on the orchestrator side.
|
|
19
|
+
|
|
20
|
+
**Programmatic input envelope** (passed verbatim by `/e2e-review`):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mode": "programmatic",
|
|
25
|
+
"card_id": "FEAT-XXXX",
|
|
26
|
+
"route": "/merchant/dashboard",
|
|
27
|
+
"viewport": { "width": 1440, "height": 900 },
|
|
28
|
+
"dev_server_port": 3000,
|
|
29
|
+
"registry_paths": {
|
|
30
|
+
"index": "docs/design-system/INDEX.md",
|
|
31
|
+
"tokens": "docs/design-system/tokens-reference.md",
|
|
32
|
+
"components_dir": "docs/design-system/components"
|
|
33
|
+
},
|
|
34
|
+
"ui_guidelines_path": "docs/references/ui-guidelines.md",
|
|
35
|
+
"features": {
|
|
36
|
+
"has_design_system": true,
|
|
37
|
+
"multi_tenant_theming": false
|
|
38
|
+
},
|
|
39
|
+
"tolerance": "strict"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Programmatic output schema** (MUST match this shape verbatim — `/e2e-review` Phase 5 aggregates this output alongside `visual-fidelity-verifier` findings, deduplicating by `(route, category, ds_drift_code)`):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"status": "completed" | "skipped" | "error",
|
|
48
|
+
"source": "design-review",
|
|
49
|
+
"route": "/merchant/dashboard",
|
|
50
|
+
"viewport": { "width": 1440, "height": 900 },
|
|
51
|
+
"findings": [
|
|
52
|
+
{
|
|
53
|
+
"severity": "critical" | "major" | "minor",
|
|
54
|
+
"category": "<category from taxonomy below>",
|
|
55
|
+
"description": "One-sentence description of what the design-review observed.",
|
|
56
|
+
"expected": "Brief reference to the design intent (token name, spec excerpt, guideline section).",
|
|
57
|
+
"actual": "Brief reference to what the rendered output shows.",
|
|
58
|
+
"fix_hint": "Actionable single-line hint. Never 'make it look right'.",
|
|
59
|
+
"evidence": {
|
|
60
|
+
"selector": "[data-testid='...']" | "css-selector",
|
|
61
|
+
"screenshot_path": "/tmp/design-review/<route-slug>.png"
|
|
62
|
+
},
|
|
63
|
+
"ds_drift_code": "DS_INDEX_DRIFT" | "DS_COMPONENT_STALE" | "DS_TOKENS_DRIFT" | null,
|
|
64
|
+
"confidence": "high" | "medium" | "low"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"ds_drift_codes": ["DS_INDEX_DRIFT", "DS_TOKENS_DRIFT"],
|
|
68
|
+
"gaps": [
|
|
69
|
+
"Could not find registry spec for primitive `Badge` — flagged as DS_INDEX_DRIFT instead of comparing against spec."
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Severity normalization (Markdown ↔ programmatic)
|
|
75
|
+
|
|
76
|
+
The interactive Markdown report uses Blockers/High/Medium/Nitpicks. In programmatic mode, normalize as follows for cross-source aggregation with `visual-fidelity-verifier`:
|
|
77
|
+
|
|
78
|
+
| Markdown tier | Programmatic `severity` | Notes |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Blocker | `critical` | Layout broken, primary CTA unreachable, accessibility blocker, security exposure |
|
|
81
|
+
| High | `major` | Token bypass, reinvented primitive, spec divergence on visible prop, contrast failure |
|
|
82
|
+
| Medium | `minor` | Stale spec doc, cosmetic spacing drift, micro-misalignments |
|
|
83
|
+
| Nitpick | DROP | Do not emit in programmatic output — too noisy for gating |
|
|
84
|
+
|
|
85
|
+
#### Programmatic-mode category taxonomy
|
|
86
|
+
|
|
87
|
+
Use these categories (subset of the canonical taxonomy in `framework/.claude/agents/visual-fidelity-verifier.md` § "Severity Taxonomy", adapted for the design-judgment scope of this command). The orchestrator deduplicates findings across sources by `(route, category)`, so categories MUST match the canonical names verbatim:
|
|
88
|
+
|
|
89
|
+
- `primitive-reinvented` (Major) — reimplements a primitive that already exists in `INDEX.md`
|
|
90
|
+
- `primitive-missing-spec` (Major) — new primitive shipped without `components/<Name>.md`
|
|
91
|
+
- `token-bypass` (Major) — hardcoded color/spacing/shadow/radius where a token exists
|
|
92
|
+
- `component-stale` (Minor) — `components/<Name>.md` out of sync with the implemented primitive
|
|
93
|
+
- `index-drift` (Minor) — primitive used but missing from `INDEX.md`
|
|
94
|
+
- `authority-violation` (Major) — local override of a registry-authoritative decision (per Authority Matrix)
|
|
95
|
+
- `brand-voice-drift` (Minor) — microcopy/UI language drift from `${paths.ui_guidelines}` brand voice
|
|
96
|
+
- `a11y-contrast` (Major) — body text < 4.5:1 or focus indicator < 3:1
|
|
97
|
+
- `a11y-focus-visible` (Major) — interactive element with no equivalent focus indicator
|
|
98
|
+
- `interactive-state-missing` (Major) — hover/focus/active visually identical to idle on interactive primitive
|
|
99
|
+
|
|
100
|
+
When a finding does not match any of these categories, drop it from programmatic output rather than inventing a new category — the orchestrator's aggregator does not know how to weigh unknown categories. Surface it in the `gaps[]` array as a free-text note instead.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Workflow (both modes)
|
|
105
|
+
|
|
106
|
+
When the user invokes `/design-review`, do the following (in both modes — only the final output format differs):
|
|
8
107
|
|
|
9
108
|
0. **BLOCKING when `features.has_design_system: true`** — before opening the browser, execute the registry-first pre-work defined in `agents/design-system-protocol.md`:
|
|
10
109
|
- Read `${paths.design_system}/INDEX.md` and the Authority Matrix for the surface under review.
|
|
11
110
|
- Read `${paths.design_system}/tokens-reference.md`.
|
|
12
111
|
- Read `${paths.design_system}/components/<Name>.md` for every primitive expected on the route.
|
|
13
|
-
When the flag is `false`, skip the registry reads
|
|
14
|
-
1. Start with the route or component specified (e.g., `/merchant/dashboard`) and open it in the MCP browser.
|
|
112
|
+
When the flag is `false`, skip the registry reads. In Mode A flag the gap in the final report and recommend `/design-system-init`. In Mode B add the gap to the `gaps[]` array of the JSON output.
|
|
113
|
+
1. Start with the route or component specified (e.g., `/merchant/dashboard`) and open it in the MCP browser. In Mode B, use the `dev_server_port` from the input envelope to construct `http://localhost:<port><route>`.
|
|
15
114
|
2. Follow the Quick Visual Check in `agents/design-review.md` and run Phases 0‑7 (interaction, responsiveness, polish, accessibility, robustness, code health, content/console).
|
|
16
|
-
3. Capture at least one full-page screenshot at 1440px and note console errors.
|
|
17
|
-
4.
|
|
18
|
-
5. **
|
|
115
|
+
3. Capture at least one full-page screenshot at the viewport from the input (default 1440px) and note console errors. In Mode B save the screenshot at `/tmp/design-review/<route-slug>.png` and cite it in `evidence.screenshot_path`.
|
|
116
|
+
4. **Mode A**: report findings using the Markdown template in `agents/design-review.md` (Blockers/High/Medium/Nitpicks). **Mode B**: assemble the JSON output described above, normalizing severity per the table.
|
|
117
|
+
5. **Design-System Conformance section** — primitives reused, primitives reinvented (HIGH→major), tokens bypassed (HIGH→major), spec drift (MEDIUM→minor), new primitives proposed.
|
|
118
|
+
- **Mode A**: include as a top-level section in the Markdown report per `agents/design-review.md` § Report Template.
|
|
119
|
+
- **Mode B**: emit as `findings[]` entries with the appropriate categories from the taxonomy above, and populate `ds_drift_codes[]` with the deduplicated set.
|
|
120
|
+
|
|
121
|
+
## Mode-detection contract (avoid silent misdetection)
|
|
122
|
+
|
|
123
|
+
The dual-mode design is deterministic only if the trigger is unambiguous. Apply these rules in order:
|
|
124
|
+
|
|
125
|
+
1. If the input prompt contains the exact substring `"mode": "programmatic"` (with quotes, as it appears in the JSON envelope) → **Mode B**, JSON only.
|
|
126
|
+
2. Else if the input is a route path or natural-language reference → **Mode A**, Markdown.
|
|
127
|
+
3. **Never auto-detect from invocation context** (e.g., "I'm running inside a skill") — that is unreliable and produces silent Markdown leakage into the orchestrator's JSON parser, which breaks the gate.
|
|
128
|
+
|
|
129
|
+
In Mode B, if the JSON envelope is malformed (cannot be parsed) or required fields (`route`, `dev_server_port`) are missing, emit:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "status": "error", "source": "design-review", "reason": "malformed_input_envelope", "findings": [] }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Do not fall back to Mode A on input-envelope error — the orchestrator expects JSON and will fail the parse on Markdown, blocking the card silently.
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
19
138
|
|
|
20
|
-
Document any blocking issues, regression risks, or accessibility gaps in the final response.
|
|
139
|
+
- Document any blocking issues, regression risks, or accessibility gaps in the final response (Mode A) or `findings[]` array (Mode B).
|
|
140
|
+
- Mode B is consumed by `/e2e-review` Phase 4b (when implemented in a future release) as the "design judgment pass" alongside the `visual-fidelity-verifier` agent's pixel-fidelity pass. The two are complementary: the verifier sees only the screenshot vs mockup; this command additionally reasons over the registry + Authority Matrix + brand voice.
|
|
141
|
+
- The output schema is kept aligned with `framework/.claude/agents/visual-fidelity-verifier.md` § "Output Schema" so the orchestrator can use a single parser for both sources. When updating the taxonomy here, update the verifier's taxonomy in lockstep.
|
|
@@ -133,6 +133,9 @@ function blockReason(realPath, decisions, blockings) {
|
|
|
133
133
|
` → Do not write to \`.framework/\`. Instead write to:`,
|
|
134
134
|
` .baldart/overlays/<skill-name>.md`,
|
|
135
135
|
` This file is consumer-owned, never pushed upstream.`,
|
|
136
|
+
` → Tip: invoke \`/overlay\` for a guided scaffolding flow (correct`,
|
|
137
|
+
` frontmatter, base_file_sha, drift check). See also`,
|
|
138
|
+
` \`npx baldart overlay scaffold <type>/<name>\`.`,
|
|
136
139
|
``,
|
|
137
140
|
`(C) Legitimate illustrative example (e.g. docs that quote the canonical`,
|
|
138
141
|
` paths as examples — \`PROJECT-CONFIGURATION.md\`, \`project-context.md\`)`,
|
|
@@ -177,7 +180,8 @@ function main() {
|
|
|
177
180
|
`This file is auto-generated by \`npx baldart update\` from a base ` +
|
|
178
181
|
`framework file plus an overlay. Manual edits will be overwritten.\n\n` +
|
|
179
182
|
`To customise: edit the corresponding overlay under .baldart/overlays/` +
|
|
180
|
-
`(or create it if absent), then re-run \`npx baldart update
|
|
183
|
+
`(or create it if absent), then re-run \`npx baldart update\`.\n` +
|
|
184
|
+
`→ Or invoke \`/overlay\` for a guided flow.`;
|
|
181
185
|
try {
|
|
182
186
|
process.stdout.write(JSON.stringify({
|
|
183
187
|
hookSpecificOutput: {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overlay
|
|
3
|
+
description: "Guided author of .baldart/overlays/ — the per-project customisation layer for BALDART skills, agents, and commands. Use when the user says /overlay, 'customizza skill', 'modifica agente upstream', 'personalizza comando', 'crea overlay', 'extend skill X', 'override agent Y', 'overlay drift', or when the framework-edit-gate hook blocked an edit and pointed here. The skill walks the user through choosing the right canal (skill / agent / command), scaffolds the overlay with correct frontmatter and section markers via `npx baldart overlay scaffold`, validates the merge via `npx baldart overlay validate`, and surfaces drift via `npx baldart overlay drift`. Never edits `.framework/` directly and never re-implements the merger logic — the CLI is the source of truth."
|
|
4
|
+
contamination_scan: skip
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /overlay — Customise the framework, the right way
|
|
8
|
+
|
|
9
|
+
Author an overlay under `.baldart/overlays/` for a BALDART skill, agent, or command, instead of editing `.framework/` (which gets overwritten on `baldart update` and blocked by the contamination scanner).
|
|
10
|
+
|
|
11
|
+
## Execution model
|
|
12
|
+
|
|
13
|
+
This skill is a **thin guidance wrapper** around `npx baldart overlay`. The CLI is non-interactive (no prompts), so the skill can invoke it via the Bash tool. The CLI is the source of truth for:
|
|
14
|
+
|
|
15
|
+
- Path conventions (`.baldart/overlays/<name>.md` for skills, `.baldart/overlays/agents/<name>.md`, `.baldart/overlays/commands/<name>.md`).
|
|
16
|
+
- Frontmatter schema (`base_<kind>`, `base_<kind>_version`, `base_file_sha`, `mode`).
|
|
17
|
+
- Merger validation (uses `src/utils/overlay-merger.js` internally).
|
|
18
|
+
- Drift detection (SHA-based when `base_file_sha` is present, version-based fallback).
|
|
19
|
+
|
|
20
|
+
The skill MUST NOT re-implement any of the above. If you find yourself writing path logic or YAML manipulation, stop — invoke the CLI.
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- The user invokes `/overlay`.
|
|
25
|
+
- The user says "customizza skill X", "personalizza il command Y", "modifica l'agente Z per questo progetto".
|
|
26
|
+
- The user wants a project-specific opinion to apply (brand voice, debug entry point, stack mandate, custom workflow) without contaminating the framework upstream.
|
|
27
|
+
- The user follows the handoff from the `framework-edit-gate` hook message ("invoke /overlay for a guided flow").
|
|
28
|
+
- The user asks "is my overlay still in sync with the framework?" or "drift overlay".
|
|
29
|
+
|
|
30
|
+
## When NOT to use
|
|
31
|
+
|
|
32
|
+
- The improvement is **generic** (useful for all consumers). Send it upstream via `/baldart-push` instead.
|
|
33
|
+
- The user wants to install / update the framework itself (use `npx baldart` / `update`).
|
|
34
|
+
- The user wants to author a brand-new skill that has no base in the framework — they create it directly under `.claude/skills/<name>/`. Overlays are for *modifying* a framework primitive, not for net-new ones.
|
|
35
|
+
- The user wants to modify a **hook** under `.claude/hooks/`. Hooks are installed as **plain copies** (consumer-owned, no auto-update) — they do not fit the overlay model. The consumer edits them directly in their repo. If the change should benefit every consumer, edit the `.template` file under `.framework/.claude/hooks/` and push upstream via `/baldart-push`. Overlay scope is limited to skills, agents, and commands.
|
|
36
|
+
|
|
37
|
+
## Project Context
|
|
38
|
+
|
|
39
|
+
**Reads from `baldart.config.yml`:** none (overlay paths are universal).
|
|
40
|
+
**Gated by features:** none.
|
|
41
|
+
**Overlay:** this skill has no overlay (it would be paradoxical).
|
|
42
|
+
**On missing/empty keys:** not applicable.
|
|
43
|
+
|
|
44
|
+
## Hard rules
|
|
45
|
+
|
|
46
|
+
1. **NEVER edit files under `.framework/` directly.** The contamination scanner will block the edit, and even if it didn't, `baldart update` would overwrite the change.
|
|
47
|
+
2. **NEVER edit a file with the marker `<!-- baldart-generated:` on line 1.** It is auto-regenerated from base + overlay. The hook blocks this. Edit the overlay instead.
|
|
48
|
+
3. **NEVER fabricate `base_<kind>_version` or `base_file_sha`.** Both are computed by the CLI at scaffold time. The skill never writes these by hand.
|
|
49
|
+
4. **NEVER write to `.baldart/overlays/` with the Edit/Write tools to *create* a new overlay.** Use `npx baldart overlay scaffold` so the frontmatter is correct. Editing the *body* of an already-scaffolded overlay with Edit/Write is fine.
|
|
50
|
+
5. **NEVER re-implement the section-merge logic** (`[OVERRIDE]/[APPEND]/[PREPEND]`). The merger in `overlay-merger.js` is the only place it lives.
|
|
51
|
+
6. **When the user's intent is clearly generic** ("we should add a Quality bar step to every coder workflow"), redirect them to `/baldart-push` instead of scaffolding an overlay.
|
|
52
|
+
|
|
53
|
+
## Workflow
|
|
54
|
+
|
|
55
|
+
### Step 0 — Pre-check
|
|
56
|
+
|
|
57
|
+
Before anything, confirm this is a consumer repo:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
[ -d .framework ] && echo OK
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If not OK, stop and say: "This repo is not a BALDART consumer. Run `npx baldart` to install the framework first."
|
|
64
|
+
|
|
65
|
+
Also confirm the overlays directory exists (it should, after `configure`):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
[ -d .baldart/overlays ] || mkdir -p .baldart/overlays
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Step 1 — Discovery
|
|
72
|
+
|
|
73
|
+
Ask the user:
|
|
74
|
+
|
|
75
|
+
1. **What do they want to customise?** A skill, an agent, or a command?
|
|
76
|
+
- **Skill**: shapes how a `/-command` workflow behaves (e.g. `/bug`, `/capture`, `/new`). Base lives at `.framework/framework/.claude/skills/<name>/SKILL.md`. Overlay is concat'd at runtime by the agent — section markers are textual conventions, not parsed.
|
|
77
|
+
- **Agent**: shapes how a specialised subagent reasons (e.g. `coder`, `code-reviewer`, `qa-sentinel`). Base lives at `.framework/framework/.claude/agents/<name>.md`. Overlay is merged section-by-section via markers, producing a generated file at `.claude/agents/<name>.md`.
|
|
78
|
+
- **Command**: shapes a top-level slash command (e.g. `/codexreview`, `/check`). Base lives at `.framework/framework/.claude/commands/<name>.md`. Same marker-based merge as agents.
|
|
79
|
+
|
|
80
|
+
2. **Which one?** List the available bases for the chosen kind:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# for skills
|
|
84
|
+
ls .framework/framework/.claude/skills/
|
|
85
|
+
# for agents
|
|
86
|
+
ls .framework/framework/.claude/agents/
|
|
87
|
+
# for commands
|
|
88
|
+
ls .framework/framework/.claude/commands/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
3. **What intent?**
|
|
92
|
+
- `extend` (default) — add to / tweak parts of the base.
|
|
93
|
+
- `override` (skills only) — replace the entire base skill with the overlay content. Rare; use only when the base does not fit your project at all.
|
|
94
|
+
|
|
95
|
+
### Step 2 — Inspect base + existing overlay
|
|
96
|
+
|
|
97
|
+
Read the base file with the Read tool. Show the user the H2 section list (`grep '^## ' <base-file>`). This is what they can target with `[OVERRIDE]/[APPEND]/[PREPEND]` (for agent/command) or as textual landmarks (for skill).
|
|
98
|
+
|
|
99
|
+
If `.baldart/overlays/<path>` already exists:
|
|
100
|
+
|
|
101
|
+
- Show its current frontmatter and section list.
|
|
102
|
+
- Run drift check: `npx baldart overlay drift .baldart/overlays/<path>`.
|
|
103
|
+
- Switch to "extend existing" mode: the skill will guide the user to add / modify sections by **editing the file directly** (Edit tool), not by re-scaffolding.
|
|
104
|
+
|
|
105
|
+
### Step 3 — Scaffold (new overlays only)
|
|
106
|
+
|
|
107
|
+
Invoke the CLI:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx baldart overlay scaffold <kind>/<name> [--mode override]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Where `<kind>` is `skill`, `agents`, or `commands` (plural for agent/command to match the overlay subdir convention). Examples:
|
|
114
|
+
|
|
115
|
+
- `npx baldart overlay scaffold agents/coder`
|
|
116
|
+
- `npx baldart overlay scaffold commands/codexreview`
|
|
117
|
+
- `npx baldart overlay scaffold bug` (no slash → skill)
|
|
118
|
+
- `npx baldart overlay scaffold skill/bug --mode override`
|
|
119
|
+
|
|
120
|
+
The CLI:
|
|
121
|
+
- Validates that the base exists in `.framework/`.
|
|
122
|
+
- Computes `base_file_sha` (sha256, 12 chars) from the base content.
|
|
123
|
+
- Reads framework `VERSION`.
|
|
124
|
+
- Writes `.baldart/overlays/<path>` with the correct frontmatter and a placeholder body.
|
|
125
|
+
|
|
126
|
+
If the overlay already exists, the CLI refuses (idempotent). Use Step 4 to extend it instead.
|
|
127
|
+
|
|
128
|
+
### Step 4 — Author the body
|
|
129
|
+
|
|
130
|
+
Open the scaffolded file with the Read tool, show the user the placeholder content, and ask what they want to put there. Use the Edit tool to insert their content. Examples of what each marker does:
|
|
131
|
+
|
|
132
|
+
**For agent / command overlays:**
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
## [APPEND] Quality bar
|
|
136
|
+
|
|
137
|
+
- Before closing any task, run `npm run test:e2e -- --grep <feature>`.
|
|
138
|
+
- Attach the Playwright screenshot to the commit body.
|
|
139
|
+
|
|
140
|
+
## [OVERRIDE] When to escalate
|
|
141
|
+
|
|
142
|
+
In this project, escalate to the human owner before touching `prisma/schema.prisma`.
|
|
143
|
+
|
|
144
|
+
## Project-only conventions
|
|
145
|
+
|
|
146
|
+
(New section, appended at the end of the merged file.)
|
|
147
|
+
- All API calls go through `src/lib/api-client.ts`.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**For skill overlays** (concat model — markers are textual conventions only):
|
|
151
|
+
|
|
152
|
+
```markdown
|
|
153
|
+
## [APPEND] Project-specific guidance
|
|
154
|
+
|
|
155
|
+
In this project, debug entry points are in `apps/web/`. Always start
|
|
156
|
+
Playwright in headed mode for visual bugs.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Reference examples already shipped with the framework:
|
|
160
|
+
- `.framework/framework/templates/overlays/ui-design.fidelity-example.md` (skill, fidelity-app brand voice)
|
|
161
|
+
- `.framework/framework/templates/overlays/copywriting.fidelity-example.md` (skill, 4-pillar brand voice)
|
|
162
|
+
- `.framework/framework/templates/overlays/agents/coder.example.md` (agent, marker-based)
|
|
163
|
+
- `.framework/framework/templates/overlays/commands/codexreview.example.md` (command, marker-based)
|
|
164
|
+
|
|
165
|
+
### Step 5 — Validate
|
|
166
|
+
|
|
167
|
+
Run:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx baldart overlay validate <path>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The CLI dry-runs the merger and reports:
|
|
174
|
+
- Frontmatter parse status.
|
|
175
|
+
- For agent/command: number of lines generated, any warnings (e.g. an overlay section whose heading does not match any base H2 — it will be appended as a new section, which may be intentional).
|
|
176
|
+
- Whether the base file changed since the overlay was authored (SHA comparison).
|
|
177
|
+
|
|
178
|
+
If `validate` reports errors, fix them with Edit and re-run. If it reports warnings about non-matching headings, decide with the user whether that is intentional (new custom section) or a typo to correct.
|
|
179
|
+
|
|
180
|
+
### Step 6 — Apply
|
|
181
|
+
|
|
182
|
+
- **For skills**: nothing to do. The overlay is loaded at runtime by Claude when the user invokes a `/-command` that uses the skill.
|
|
183
|
+
- **For agents / commands**: run `npx baldart update` to regenerate the file in `.claude/<kind>s/<name>.md` from base + overlay. The regenerated file carries the `<!-- baldart-generated: -->` marker on line 1 — do not edit it manually (the `framework-edit-gate` hook will block, anyway).
|
|
184
|
+
|
|
185
|
+
### Step 7 — Drift maintenance (periodic)
|
|
186
|
+
|
|
187
|
+
When the user updates the framework (`npx baldart update` bumps `VERSION`), the base files for their overlays may have changed. Tell them how to audit:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npx baldart overlay drift --all
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
For each drifted overlay, the user should:
|
|
194
|
+
1. Open the base file: `.framework/framework/.claude/<path>`.
|
|
195
|
+
2. Read the diff against what they expected (use `git -C .framework log -p -- framework/.claude/<path>` to see the upstream changes).
|
|
196
|
+
3. If the overlay still makes sense, refresh the `base_file_sha` by re-running scaffold (after backing up the overlay body) — or simply update `base_file_sha` by hand once they have confirmed the overlay still applies cleanly.
|
|
197
|
+
4. If the overlay no longer makes sense, edit it to align with the new base.
|
|
198
|
+
|
|
199
|
+
## Failure modes & remediation
|
|
200
|
+
|
|
201
|
+
| Symptom | Likely cause | Remediation |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `scaffold` says "already exists" | Overlay was authored previously | Edit the existing file directly. Run `overlay drift` first to check. |
|
|
204
|
+
| `validate` says "frontmatter mismatch" | `base_<kind>: <name>` does not match the overlay's filename | Either fix the frontmatter or rename the file. The two MUST agree. |
|
|
205
|
+
| `validate` warning "overlay section has no matching H2 in base" | The user used `[OVERRIDE]` / `[APPEND]` / `[PREPEND]` with a heading that does not exist in the base file | Either fix the heading to match an existing base H2, or drop the marker (the section will become a new custom section appended at the end). |
|
|
206
|
+
| `drift` says "drift unknown" | Overlay was authored before `base_file_sha` existed (pre-v3.19.0) | Re-run `scaffold` on a temp path to capture the current SHA, then merge that into the overlay frontmatter manually. Or just accept the version-based fallback. |
|
|
207
|
+
| Hook blocked an edit to a generated file | The user tried to edit `.claude/agents/<x>.md` directly, which is BALDART-managed | Edit the overlay at `.baldart/overlays/agents/<x>.md` instead. The hook message already says this. |
|
|
208
|
+
|
|
209
|
+
## Reference
|
|
210
|
+
|
|
211
|
+
- `framework/agents/project-context.md` § 5 — overlay protocol.
|
|
212
|
+
- `framework/templates/overlays/README.md` — conventions and pointer to example overlays.
|
|
213
|
+
- `src/utils/overlay-merger.js` — merger implementation (read-only; do not duplicate in the skill).
|
|
214
|
+
- `src/commands/overlay.js` — CLI implementation (skill calls this via Bash).
|
|
@@ -22,10 +22,23 @@ Every overlay file has YAML frontmatter:
|
|
|
22
22
|
---
|
|
23
23
|
base_skill: <skill-name> # MUST match the skill directory name
|
|
24
24
|
base_skill_version: <semver> # framework version when authored
|
|
25
|
+
base_file_sha: "<12-char-sha>" # optional (v3.19.0+) — base file content sha for accurate drift detection
|
|
25
26
|
mode: extend | override # default: extend
|
|
26
27
|
---
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
For agent overlays use `base_agent` / `base_agent_version`, and for command
|
|
31
|
+
overlays `base_command` / `base_command_version`.
|
|
32
|
+
|
|
33
|
+
The optional `base_file_sha` (v3.19.0+) records the sha256 prefix (12 chars)
|
|
34
|
+
of the base file at the moment the overlay was scaffolded. When present,
|
|
35
|
+
`npx baldart overlay drift` uses it to detect base-file changes precisely
|
|
36
|
+
instead of falling back to a version-string compare. The value MUST be
|
|
37
|
+
quoted because an all-digit hex like `000000000000` would otherwise be
|
|
38
|
+
parsed as a number by YAML. The `npx baldart overlay scaffold` CLI writes
|
|
39
|
+
this field automatically; hand-written overlays may omit it (drift detection
|
|
40
|
+
silently degrades to the version-only fallback — no error).
|
|
41
|
+
|
|
29
42
|
Sections marked `## [OVERRIDE] <topic>` replace the same `<topic>` from the
|
|
30
43
|
base skill. Everything else extends the base.
|
|
31
44
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const UI = require('../utils/ui');
|
|
5
|
+
const { mergeOverlay, computeBaseFileSha } = require('../utils/overlay-merger');
|
|
6
|
+
|
|
7
|
+
const FRAMEWORK_DIR = '.framework';
|
|
8
|
+
const FRAMEWORK_PAYLOAD = path.join(FRAMEWORK_DIR, 'framework');
|
|
9
|
+
const OVERLAYS_DIR = path.join('.baldart', 'overlays');
|
|
10
|
+
|
|
11
|
+
const KINDS = ['skill', 'agent', 'command'];
|
|
12
|
+
|
|
13
|
+
function frameworkRoot(cwd) {
|
|
14
|
+
return path.join(cwd, FRAMEWORK_DIR);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureConsumer(cwd) {
|
|
18
|
+
if (!fs.existsSync(frameworkRoot(cwd))) {
|
|
19
|
+
UI.error('Not a BALDART consumer repo — `.framework/` not found.');
|
|
20
|
+
UI.info('Run `npx baldart` to install the framework first.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readFrameworkVersion(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readFileSync(path.join(cwd, FRAMEWORK_DIR, 'VERSION'), 'utf8').trim();
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function baseFilePathFor(cwd, kind, name) {
|
|
34
|
+
if (kind === 'skill') {
|
|
35
|
+
return path.join(cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills', name, 'SKILL.md');
|
|
36
|
+
}
|
|
37
|
+
return path.join(cwd, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`, `${name}.md`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function overlayPathFor(cwd, kind, name) {
|
|
41
|
+
if (kind === 'skill') return path.join(cwd, OVERLAYS_DIR, `${name}.md`);
|
|
42
|
+
return path.join(cwd, OVERLAYS_DIR, `${kind}s`, `${name}.md`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTarget(target) {
|
|
46
|
+
// Accept "agents/coder", "commands/codexreview", "skill/bug", or bare name
|
|
47
|
+
// (skill assumed). Always normalise to { kind: 'agent'|'command'|'skill', name }.
|
|
48
|
+
if (!target || typeof target !== 'string') return null;
|
|
49
|
+
if (target.includes('/')) {
|
|
50
|
+
const [head, ...rest] = target.split('/');
|
|
51
|
+
const name = rest.join('/');
|
|
52
|
+
if (!name) return null;
|
|
53
|
+
if (head === 'skill' || head === 'skills') return { kind: 'skill', name };
|
|
54
|
+
if (head === 'agent' || head === 'agents') return { kind: 'agent', name };
|
|
55
|
+
if (head === 'command' || head === 'commands') return { kind: 'command', name };
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { kind: 'skill', name: target };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseFrontmatter(content) {
|
|
62
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
63
|
+
if (!m) return { fm: {}, found: false };
|
|
64
|
+
try {
|
|
65
|
+
const fm = yaml.load(m[1]) || {};
|
|
66
|
+
// Coerce `base_file_sha` to string — a 12-char hex SHA may be all digits,
|
|
67
|
+
// which YAML would otherwise parse as a number (0 → falsy → false drift).
|
|
68
|
+
if (fm.base_file_sha != null) fm.base_file_sha = String(fm.base_file_sha);
|
|
69
|
+
return { fm, found: true };
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return { fm: {}, found: true, malformed: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scaffoldBody(kind, name) {
|
|
76
|
+
if (kind === 'skill') {
|
|
77
|
+
return [
|
|
78
|
+
'',
|
|
79
|
+
`> Skill overlays are concatenated to the base SKILL.md at runtime by the Claude`,
|
|
80
|
+
`> agent. The merger does NOT parse [OVERRIDE]/[APPEND]/[PREPEND] markers here —`,
|
|
81
|
+
`> they are textual conventions for the agent. Use \`mode: override\` in`,
|
|
82
|
+
`> frontmatter to replace the base skill entirely.`,
|
|
83
|
+
'',
|
|
84
|
+
`## [APPEND] Project-specific guidance`,
|
|
85
|
+
'',
|
|
86
|
+
`<!-- Add the rules / vocabulary / opinions that apply to THIS project. -->`,
|
|
87
|
+
'',
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
return [
|
|
91
|
+
'',
|
|
92
|
+
`> Section markers (parsed by overlay-merger.js):`,
|
|
93
|
+
`> ## [OVERRIDE] <H2> — replace base section verbatim`,
|
|
94
|
+
`> ## [APPEND] <H2> — add overlay content after base section`,
|
|
95
|
+
`> ## [PREPEND] <H2> — add overlay content before base section`,
|
|
96
|
+
`> ## <H2> — appended as a new section at the end`,
|
|
97
|
+
'',
|
|
98
|
+
`## [APPEND] <heading from base ${kind} "${name}">`,
|
|
99
|
+
'',
|
|
100
|
+
`<!-- Replace the heading above with one that exists in the base file. -->`,
|
|
101
|
+
'',
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildFrontmatter({ kind, name, frameworkVersion, baseSha, mode }) {
|
|
106
|
+
const lines = ['---'];
|
|
107
|
+
lines.push(`base_${kind}: ${name}`);
|
|
108
|
+
lines.push(`base_${kind}_version: ${frameworkVersion || 'unknown'}`);
|
|
109
|
+
// Quote the SHA — a 12-char hex string can be all digits, which YAML would
|
|
110
|
+
// otherwise parse as a number (0 → falsy → false-negative in drift check).
|
|
111
|
+
if (baseSha) lines.push(`base_file_sha: "${baseSha}"`);
|
|
112
|
+
lines.push(`mode: ${mode || 'extend'}`);
|
|
113
|
+
lines.push('---');
|
|
114
|
+
return lines.join('\n') + '\n';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── scaffold ──────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
async function scaffold(target, options = {}) {
|
|
120
|
+
const cwd = process.cwd();
|
|
121
|
+
ensureConsumer(cwd);
|
|
122
|
+
|
|
123
|
+
const parsed = parseTarget(target);
|
|
124
|
+
if (!parsed) {
|
|
125
|
+
UI.error('Invalid target. Use: skill/<name>, agents/<name>, or commands/<name>.');
|
|
126
|
+
UI.info('Example: npx baldart overlay scaffold agents/coder');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const { kind, name } = parsed;
|
|
130
|
+
|
|
131
|
+
const baseAbs = baseFilePathFor(cwd, kind, name);
|
|
132
|
+
if (!fs.existsSync(baseAbs)) {
|
|
133
|
+
UI.error(`Base ${kind} "${name}" not found at ${path.relative(cwd, baseAbs)}.`);
|
|
134
|
+
UI.info(`Run \`ls ${path.relative(cwd, path.dirname(baseAbs))}\` to list available ${kind}s.`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const overlayAbs = overlayPathFor(cwd, kind, name);
|
|
139
|
+
const overlayRel = path.relative(cwd, overlayAbs);
|
|
140
|
+
|
|
141
|
+
if (fs.existsSync(overlayAbs)) {
|
|
142
|
+
UI.warning(`Overlay already exists: ${overlayRel}`);
|
|
143
|
+
UI.info('Run `npx baldart overlay drift ' + overlayRel + '` to check it, or edit it directly to extend.');
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
|
|
148
|
+
|
|
149
|
+
const baseContent = fs.readFileSync(baseAbs, 'utf8');
|
|
150
|
+
const baseSha = computeBaseFileSha(baseContent);
|
|
151
|
+
const fwVersion = readFrameworkVersion(cwd);
|
|
152
|
+
const mode = (options.mode || 'extend').trim();
|
|
153
|
+
if (!['extend', 'override'].includes(mode)) {
|
|
154
|
+
UI.error(`Invalid --mode "${mode}". Use "extend" or "override".`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const content =
|
|
159
|
+
buildFrontmatter({ kind, name, frameworkVersion: fwVersion, baseSha, mode }) +
|
|
160
|
+
scaffoldBody(kind, name);
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(overlayAbs, content);
|
|
163
|
+
UI.success(`Created ${overlayRel}`);
|
|
164
|
+
UI.info(`Base file: ${path.relative(cwd, baseAbs)}`);
|
|
165
|
+
UI.info(`Targets framework v${fwVersion || 'unknown'} (base_file_sha=${baseSha}).`);
|
|
166
|
+
UI.newline();
|
|
167
|
+
UI.list([
|
|
168
|
+
`Edit ${overlayRel} to add your customisations.`,
|
|
169
|
+
`Validate the merge with: npx baldart overlay validate ${overlayRel}`,
|
|
170
|
+
kind === 'skill'
|
|
171
|
+
? `Run any /-command that uses this skill to see the overlay applied at runtime.`
|
|
172
|
+
: `Apply with: npx baldart update (regenerates .claude/${kind}s/${name}.md from base + overlay).`,
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── validate ──────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function deriveTargetFromOverlayPath(cwd, overlayPath) {
|
|
179
|
+
const abs = path.isAbsolute(overlayPath) ? overlayPath : path.join(cwd, overlayPath);
|
|
180
|
+
const root = path.join(cwd, OVERLAYS_DIR);
|
|
181
|
+
const rel = path.relative(root, abs);
|
|
182
|
+
if (rel.startsWith('..')) return null;
|
|
183
|
+
const parts = rel.split(path.sep);
|
|
184
|
+
if (parts.length === 1) {
|
|
185
|
+
return { kind: 'skill', name: parts[0].replace(/\.md$/, ''), abs };
|
|
186
|
+
}
|
|
187
|
+
if (parts.length === 2) {
|
|
188
|
+
const sub = parts[0];
|
|
189
|
+
if (sub === 'agents') return { kind: 'agent', name: parts[1].replace(/\.md$/, ''), abs };
|
|
190
|
+
if (sub === 'commands') return { kind: 'command', name: parts[1].replace(/\.md$/, ''), abs };
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function validate(overlayPath) {
|
|
196
|
+
const cwd = process.cwd();
|
|
197
|
+
ensureConsumer(cwd);
|
|
198
|
+
|
|
199
|
+
if (!overlayPath) {
|
|
200
|
+
UI.error('Usage: npx baldart overlay validate <path>');
|
|
201
|
+
UI.info('Example: npx baldart overlay validate .baldart/overlays/agents/coder.md');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const t = deriveTargetFromOverlayPath(cwd, overlayPath);
|
|
206
|
+
if (!t) {
|
|
207
|
+
UI.error(`Path is not under ${OVERLAYS_DIR}/ or has unexpected shape: ${overlayPath}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
if (!fs.existsSync(t.abs)) {
|
|
211
|
+
UI.error(`Overlay file not found: ${path.relative(cwd, t.abs)}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const overlayContent = fs.readFileSync(t.abs, 'utf8');
|
|
216
|
+
const { fm, found, malformed } = parseFrontmatter(overlayContent);
|
|
217
|
+
if (!found) {
|
|
218
|
+
UI.error('Overlay has no YAML frontmatter.');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
if (malformed) {
|
|
222
|
+
UI.error('Overlay frontmatter is not valid YAML.');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const expectedKindField = `base_${t.kind}`;
|
|
226
|
+
if (fm[expectedKindField] !== t.name) {
|
|
227
|
+
UI.error(`Frontmatter mismatch: expected ${expectedKindField}: ${t.name} (got ${fm[expectedKindField] || 'missing'}).`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const baseAbs = baseFilePathFor(cwd, t.kind, t.name);
|
|
232
|
+
if (!fs.existsSync(baseAbs)) {
|
|
233
|
+
UI.error(`Base ${t.kind} "${t.name}" not found at ${path.relative(cwd, baseAbs)}.`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (t.kind === 'skill') {
|
|
238
|
+
UI.success(`Overlay valid (skill, concat model).`);
|
|
239
|
+
UI.info(`Targets v${fm.base_skill_version || 'unknown'}, mode=${fm.mode || 'extend'}.`);
|
|
240
|
+
if (fm.base_file_sha) {
|
|
241
|
+
const currentSha = computeBaseFileSha(fs.readFileSync(baseAbs, 'utf8'));
|
|
242
|
+
if (currentSha !== fm.base_file_sha) {
|
|
243
|
+
UI.warning(`Base file changed since overlay was authored (sha ${fm.base_file_sha} → ${currentSha}). Review for drift.`);
|
|
244
|
+
} else {
|
|
245
|
+
UI.success('Base file unchanged since overlay was authored.');
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
UI.info('No base_file_sha in frontmatter — drift check by version only.');
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Agent/command — dry-run the merger.
|
|
254
|
+
const baseContent = fs.readFileSync(baseAbs, 'utf8');
|
|
255
|
+
const fwVersion = readFrameworkVersion(cwd);
|
|
256
|
+
let result;
|
|
257
|
+
try {
|
|
258
|
+
result = mergeOverlay({
|
|
259
|
+
kind: t.kind,
|
|
260
|
+
name: t.name,
|
|
261
|
+
baseContent,
|
|
262
|
+
overlayContent,
|
|
263
|
+
frameworkVersion: fwVersion
|
|
264
|
+
});
|
|
265
|
+
} catch (err) {
|
|
266
|
+
UI.error(`Merger threw: ${err.message}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (result.warnings && result.warnings.length) {
|
|
271
|
+
result.warnings.forEach((w) => UI.warning(w));
|
|
272
|
+
}
|
|
273
|
+
UI.success(`Overlay valid (${t.kind}, marker model).`);
|
|
274
|
+
UI.info(`Targets v${result.baseVersionTargeted}, mode=${result.overlayMode}.`);
|
|
275
|
+
UI.info(`Generated output: ${result.generated.split('\n').length} lines.`);
|
|
276
|
+
if (fm.base_file_sha) {
|
|
277
|
+
const currentSha = computeBaseFileSha(baseContent);
|
|
278
|
+
if (currentSha !== fm.base_file_sha) {
|
|
279
|
+
UI.warning(`Base file changed since overlay was authored (sha ${fm.base_file_sha} → ${currentSha}). Review for drift.`);
|
|
280
|
+
} else {
|
|
281
|
+
UI.success('Base file unchanged since overlay was authored.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── drift ─────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
function collectOverlays(cwd) {
|
|
289
|
+
const root = path.join(cwd, OVERLAYS_DIR);
|
|
290
|
+
if (!fs.existsSync(root)) return [];
|
|
291
|
+
const out = [];
|
|
292
|
+
const pushFrom = (sub, kind) => {
|
|
293
|
+
const dir = sub ? path.join(root, sub) : root;
|
|
294
|
+
if (!fs.existsSync(dir)) return;
|
|
295
|
+
for (const f of fs.readdirSync(dir)) {
|
|
296
|
+
if (!f.endsWith('.md')) continue;
|
|
297
|
+
if (f.endsWith('.example.md')) continue;
|
|
298
|
+
if (sub === '' && (f === 'README.md')) continue;
|
|
299
|
+
out.push({ kind, name: f.replace(/\.md$/, ''), abs: path.join(dir, f), rel: path.join(OVERLAYS_DIR, sub || '', f) });
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
pushFrom('', 'skill');
|
|
303
|
+
pushFrom('agents', 'agent');
|
|
304
|
+
pushFrom('commands', 'command');
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function drift(target, options = {}) {
|
|
309
|
+
const cwd = process.cwd();
|
|
310
|
+
ensureConsumer(cwd);
|
|
311
|
+
|
|
312
|
+
const fwVersion = readFrameworkVersion(cwd);
|
|
313
|
+
const all = !!options.all || !target;
|
|
314
|
+
const targets = [];
|
|
315
|
+
if (all) {
|
|
316
|
+
targets.push(...collectOverlays(cwd));
|
|
317
|
+
} else {
|
|
318
|
+
const t = deriveTargetFromOverlayPath(cwd, target);
|
|
319
|
+
if (!t) {
|
|
320
|
+
UI.error(`Path is not under ${OVERLAYS_DIR}/ or has unexpected shape: ${target}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
if (!fs.existsSync(t.abs)) {
|
|
324
|
+
UI.error(`Overlay not found: ${path.relative(cwd, t.abs)}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
targets.push({ kind: t.kind, name: t.name, abs: t.abs, rel: path.relative(cwd, t.abs) });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (targets.length === 0) {
|
|
331
|
+
UI.info('No overlays authored.');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let drifted = 0;
|
|
336
|
+
let unknown = 0;
|
|
337
|
+
let clean = 0;
|
|
338
|
+
for (const t of targets) {
|
|
339
|
+
const content = fs.readFileSync(t.abs, 'utf8');
|
|
340
|
+
const { fm } = parseFrontmatter(content);
|
|
341
|
+
const versionField = `base_${t.kind}_version`;
|
|
342
|
+
const overlayVersion = fm[versionField];
|
|
343
|
+
const baseAbs = baseFilePathFor(cwd, t.kind, t.name);
|
|
344
|
+
const baseExists = fs.existsSync(baseAbs);
|
|
345
|
+
|
|
346
|
+
if (!baseExists) {
|
|
347
|
+
UI.error(`${t.rel}: base ${t.kind} "${t.name}" no longer exists in framework.`);
|
|
348
|
+
drifted++;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const baseSha = computeBaseFileSha(fs.readFileSync(baseAbs, 'utf8'));
|
|
353
|
+
let shaDrift = null;
|
|
354
|
+
if (fm.base_file_sha) {
|
|
355
|
+
shaDrift = fm.base_file_sha !== baseSha;
|
|
356
|
+
}
|
|
357
|
+
const versionDrift = overlayVersion && fwVersion && overlayVersion !== fwVersion;
|
|
358
|
+
|
|
359
|
+
if (shaDrift === true) {
|
|
360
|
+
UI.warning(`${t.rel}: base file changed since overlay was authored (sha ${fm.base_file_sha} → ${baseSha}).`);
|
|
361
|
+
drifted++;
|
|
362
|
+
} else if (shaDrift === false) {
|
|
363
|
+
UI.success(`${t.rel}: clean (sha matches base v${fwVersion}).`);
|
|
364
|
+
clean++;
|
|
365
|
+
} else if (versionDrift) {
|
|
366
|
+
UI.warning(`${t.rel}: targets v${overlayVersion}, installed v${fwVersion} — review (no base_file_sha to confirm).`);
|
|
367
|
+
unknown++;
|
|
368
|
+
} else if (overlayVersion) {
|
|
369
|
+
UI.info(`${t.rel}: targets v${overlayVersion} (no base_file_sha — drift unknown).`);
|
|
370
|
+
unknown++;
|
|
371
|
+
} else {
|
|
372
|
+
UI.warning(`${t.rel}: no base_${t.kind}_version in frontmatter.`);
|
|
373
|
+
unknown++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
UI.newline();
|
|
378
|
+
UI.info(`Summary: ${clean} clean, ${drifted} drifted, ${unknown} unknown — of ${targets.length} overlay(s).`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { scaffold, validate, drift };
|
|
@@ -34,8 +34,16 @@ function shortSha(content) {
|
|
|
34
34
|
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Public alias — the CLI scaffolder writes this value into the overlay
|
|
38
|
+
// frontmatter (as `base_file_sha`) so `baldart overlay drift` can detect
|
|
39
|
+
// when the base file changed upstream without re-parsing the merger logic.
|
|
40
|
+
function computeBaseFileSha(content) {
|
|
41
|
+
return shortSha(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildMarker({ kind, name, baseVersion, baseSha, overlaySha }) {
|
|
45
|
+
const baseShaField = baseSha ? ` base_sha=${baseSha}` : '';
|
|
46
|
+
return `${MARKER_PREFIX} kind=${kind} name=${name} base_version=${baseVersion}${baseShaField} overlay_sha=${overlaySha}
|
|
39
47
|
DO NOT EDIT MANUALLY. Edit .baldart/overlays/${kind}s/${name}.md instead;
|
|
40
48
|
this file is regenerated by \`npx baldart update\`. -->`;
|
|
41
49
|
}
|
|
@@ -44,13 +52,15 @@ function buildMarker({ kind, name, baseVersion, overlaySha }) {
|
|
|
44
52
|
* Returns the marker fields encoded in the first line if present, or null.
|
|
45
53
|
* Used by `update` to know "is this file ours (safe to regenerate) or did the
|
|
46
54
|
* user edit it manually?"
|
|
55
|
+
*
|
|
56
|
+
* `base_sha` is optional — older generated files (pre-v3.19.0) won't have it.
|
|
47
57
|
*/
|
|
48
58
|
function readMarker(content) {
|
|
49
59
|
const firstLine = content.split('\n').slice(0, 3).join('\n');
|
|
50
60
|
if (!firstLine.includes(MARKER_PREFIX)) return null;
|
|
51
|
-
const m = firstLine.match(/kind=(\S+) name=(\S+) base_version=(\S+) overlay_sha=(\S+)/);
|
|
61
|
+
const m = firstLine.match(/kind=(\S+) name=(\S+) base_version=(\S+)(?: base_sha=(\S+))? overlay_sha=(\S+)/);
|
|
52
62
|
if (!m) return null;
|
|
53
|
-
return { kind: m[1], name: m[2], baseVersion: m[3],
|
|
63
|
+
return { kind: m[1], name: m[2], baseVersion: m[3], baseSha: m[4] || null, overlaySha: m[5] };
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/**
|
|
@@ -170,10 +180,12 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
|
|
|
170
180
|
: '';
|
|
171
181
|
|
|
172
182
|
const overlaySha = shortSha(overlayContent);
|
|
183
|
+
const baseSha = shortSha(baseContent);
|
|
173
184
|
const marker = buildMarker({
|
|
174
185
|
kind,
|
|
175
186
|
name,
|
|
176
187
|
baseVersion: frameworkVersion || baseVersionTargeted,
|
|
188
|
+
baseSha,
|
|
177
189
|
overlaySha
|
|
178
190
|
});
|
|
179
191
|
|
|
@@ -202,5 +214,6 @@ module.exports = {
|
|
|
202
214
|
readMarker,
|
|
203
215
|
parseMarkdown,
|
|
204
216
|
shortSha,
|
|
217
|
+
computeBaseFileSha,
|
|
205
218
|
MARKER_PREFIX
|
|
206
219
|
};
|