baldart 3.18.1 → 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 +48 -0
- package/README.md +2 -1
- package/VERSION +1 -1
- package/bin/baldart.js +30 -0
- 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,54 @@ 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
|
+
|
|
8
56
|
## [3.18.1] - 2026-05-25
|
|
9
57
|
|
|
10
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.
|
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)');
|
|
@@ -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
|
};
|