baldart 3.26.0 → 3.27.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 +42 -0
- package/VERSION +1 -1
- package/framework/.claude/hooks/agent-discovery-info.sh +30 -8
- package/framework/.claude/hooks/framework-edit-gate.js +13 -7
- package/framework/.claude/skills/baldart-update/SKILL.md +16 -4
- package/package.json +1 -1
- package/src/utils/overlay-merger.js +36 -12
- package/src/utils/symlinks.js +58 -11
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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.27.0] - 2026-05-28
|
|
9
|
+
|
|
10
|
+
Fix critico al meccanismo di overlay-merge degli agent. Sintomo riproducibile osservato in `mayo` durante `/new FEAT-0009 -full` (team mode, claude-opus-4-7): gli agent overlay-merged (`coder`, `ui-expert`, `code-reviewer`, `doc-reviewer`, `qa-sentinel`, `security-reviewer` quando hanno un overlay) **non comparivano nella lista "Available agent types"** del Claude Code session reminder. Sequenza concreta: `codebase-architect` (symlink) parte e completa; i 4 background agent (`coder×2` + `ui-expert×2`) lanciati subito dopo falliscono con `InputValidationError: subagent_type not recognized`; l'orchestratore halta con un AskUserQuestion fuori-protocollo. Repro: `ls -la .claude/agents/` mostra 21 symlink + 6 file regolari (i 6 overlay-merged); CC discoverizza i 21 symlink e omette i 6 file regolari. Confermato su sessioni indipendenti del 2026-05-27 e 2026-05-28 → bug strutturale, non transiente. Workflow `/new` in team mode, `/check`, `/qa`, `/codexreview` e skill `prd` erano **bloccati end-to-end** su qualsiasi consumer con overlay agent.
|
|
11
|
+
|
|
12
|
+
### Root cause — Claude Code upstream bug #20931
|
|
13
|
+
|
|
14
|
+
Il session-start discovery scanner di Claude Code che costruisce la registry di `subagent_type` validi **skippa silenziosamente i file regolari** in `.claude/agents/` (solo i symlink vengono caricati). Il bug è già documentato in `framework/.claude/hooks/agent-discovery-gate.js:10-15`:
|
|
15
|
+
|
|
16
|
+
> #20931 — file-based discovery of `.claude/agents/*.md` is documented as BROKEN in some scenarios (files ignored at session start)
|
|
17
|
+
|
|
18
|
+
Dalla v3.8.0 in poi BALDART ha generato i file overlay-merged come **file regolari** direttamente in `.claude/agents/<name>.md`, intersecando esattamente la zona di rottura di CC. Tutti gli overlay agent attivi nei consumer sono stati silenziosamente invisibili per ~3 mesi, e il sintomo emergeva solo quando una skill provava ad invocare lo `subagent_type` (es. team-mode `/new`).
|
|
19
|
+
|
|
20
|
+
Diagnosi precedente erroneamente attribuita a "HTML comment prima del frontmatter rompe il parser YAML" è stata rivista: il riordino del marker post-frontmatter resta una pulizia di formato corretta ma **non era la causa primaria** del sintomo.
|
|
21
|
+
|
|
22
|
+
### Fixed — Symlink indirection per agent/command overlay-merged (root cause fix)
|
|
23
|
+
|
|
24
|
+
- **[src/utils/symlinks.js](src/utils/symlinks.js)** `_generateOverlayedFile()`: il content merged ora vive in `.baldart/generated/<kind>s/<name>.md` (file regolare), e `.claude/agents/<name>.md` (o `.claude/commands/<name>.md`) viene creato come **symlink relativo** verso quel file. Claude Code vede il path consumer come un symlink come tutti gli altri e lo discoverizza correttamente. Stesso pattern per Codex (`.agents/skills/`) tramite il tool adapter.
|
|
25
|
+
- **[src/utils/symlinks.js](src/utils/symlinks.js)** `_installPerItemSymlink()`: quando `hasOverlay` viene rimosso e il path consumer è un symlink che punta in `.baldart/generated/`, il CLI relinka al framework base e cancella lo stale generated file. Coerenza forte tra "overlay esiste" e "esiste un generated file".
|
|
26
|
+
- **[src/utils/symlinks.js](src/utils/symlinks.js)**: migrazione automatica del legacy layout. Se il consumer ha un file regolare baldart-generated direttamente in `.claude/agents/<name>.md` (pre-v3.27.0), la prossima `npx baldart update` lo riconosce dal marker, sposta il content in `.baldart/generated/agents/<name>.md` e crea il symlink. Nessun intervento manuale richiesto. User-fork (file regolare senza marker baldart) restano protetti.
|
|
27
|
+
|
|
28
|
+
### Added — Safety net SessionStart hook
|
|
29
|
+
|
|
30
|
+
- **[framework/.claude/hooks/agent-discovery-info.sh](framework/.claude/hooks/agent-discovery-info.sh)**: oltre a rilevare gli agent **mancanti** (file assente), il hook ora rileva anche gli agent in **stato degradato** — ovvero file regolari con marker `<!-- baldart-generated: -->` presenti in `.claude/agents/` (pre-v3.27.0 layout). Quando rilevati, emette `additionalContext` esplicito al modello con il nome degli agent affected, la causa (upstream bug #20931) e l'azione richiesta (`npx baldart update`). Difesa-in-profondità: il `mergeAgents` migra automaticamente e questo hook copre l'edge case in cui un consumer non lanci `update` subito dopo l'upgrade.
|
|
31
|
+
|
|
32
|
+
### Fixed — Formato file generato (cleanup secondario, non root cause)
|
|
33
|
+
|
|
34
|
+
- **[src/utils/overlay-merger.js](src/utils/overlay-merger.js)**: i file generati hanno il frontmatter YAML su riga 1 e il marker `<!-- baldart-generated: -->` subito dopo il `---` di chiusura (era prima invertito). Allinea il formato alla convenzione standard CommonMark + YAML frontmatter, indipendentemente dal fatto che CC sia o meno strict sul parser.
|
|
35
|
+
- **[src/utils/overlay-merger.js](src/utils/overlay-merger.js)**: nuovo export `isGeneratedFile(content)` posizione-agnostico. `readMarker()` cerca il marker in tutto il file (cap 4KB) per leggere sia file v3.27.0+ sia legacy pre-v3.27.0 (retrocompat fondamentale per la migration).
|
|
36
|
+
- **[framework/.claude/hooks/framework-edit-gate.js](framework/.claude/hooks/framework-edit-gate.js)**: il PreToolUse hook controlla il marker via `head.includes` su slice 5KB, così intercetta edit ai file generati sia che il marker sia su riga 1 (legacy) sia dopo il frontmatter (v3.27.0+).
|
|
37
|
+
|
|
38
|
+
### Migration path per i consumer
|
|
39
|
+
|
|
40
|
+
Su un consumer con overlay agent in layout legacy (es. `mayo`):
|
|
41
|
+
|
|
42
|
+
1. `npx baldart update` rileva la nuova VERSION e lancia `mergeAgents`.
|
|
43
|
+
2. Per ogni file regolare baldart-generated in `.claude/agents/`: marker letto → content spostato in `.baldart/generated/agents/<name>.md` → simlink creato.
|
|
44
|
+
3. `git status` mostra: `.claude/agents/<name>.md` modificato (file → symlink), `.baldart/generated/agents/<name>.md` nuovo. La `_autoCommitFn` del CLI committa con `chore(baldart): post-update reconcile to v3.27.0`.
|
|
45
|
+
4. **Restart della Claude Code session** richiesto perché la registry di `subagent_type` viene letta al session-start.
|
|
46
|
+
5. Verifica: `Available agent types` ora include tutti i nomi (sia symlink che generated-symlink). `/new` in team mode funziona.
|
|
47
|
+
|
|
48
|
+
Nessuna perdita di overlay: i file in `.baldart/overlays/agents/*.md` sono consumer-owned e non vengono toccati. Lo skill `/baldart-update` cita esplicitamente nel report post-flight gli agent migrati e ricorda di restartare la sessione CC.
|
|
49
|
+
|
|
8
50
|
## [3.26.0] - 2026-05-27
|
|
9
51
|
|
|
10
52
|
Fix definitivo del processo `baldart update` dopo che un'esecuzione reale su un consumer (`mayo`) ha esposto un workflow strutturalmente rotto. Sintomi: `update --yes` stampava "Already up to date!" mentre `version`/`doctor` riportavano correttamente "Remote: 51 commit ahead"; CLI globale (v3.18.1) silenzioso vs npm latest (v3.24.0); divergenza subtree (19 commit locali + 51 upstream) mai menzionata; output sporcato da `ExperimentalWarning` di npm a ogni invocazione; skill `/baldart-update` che narrava il workflow rotto senza accorgersene. Root cause: tre superfici (`update`, `version`, `doctor`) implementavano TRE check diversi per la stessa domanda, e nessuna era giusta — `update.js:193` usava `hasChangesToPush()` (commit LOCALI non pushati al CONSUMER's origin, niente a che vedere con BALDART upstream); `version`/`doctor` usavano `HEAD...FETCH_HEAD` full-repo che include sempre subtree-merge noise → falso positivo perenne "52 commit ahead".
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.27.0
|
|
@@ -62,9 +62,19 @@ CONSUMER_AGENTS_DIR=".claude/agents"
|
|
|
62
62
|
|
|
63
63
|
[ -d "$FRAMEWORK_AGENTS_DIR" ] || exit 0 # consumer not installed — silent.
|
|
64
64
|
|
|
65
|
-
# --- compute missing
|
|
65
|
+
# --- compute missing + degraded sets --------------------------------------
|
|
66
|
+
#
|
|
67
|
+
# MISSING — file does not exist (or symlink is broken). agent-discovery-gate
|
|
68
|
+
# will hard-deny calls to these names.
|
|
69
|
+
# DEGRADED — file exists as a *regular file* with the baldart-generated marker.
|
|
70
|
+
# Pre-v3.27.0 layout produced these, but Claude Code upstream bug
|
|
71
|
+
# #20931 skips regular files at session-start discovery, so the
|
|
72
|
+
# `subagent_type` value is silently unavailable to the orchestrator.
|
|
73
|
+
# The fix is `npx baldart update` (migrates them to a symlink into
|
|
74
|
+
# .baldart/generated/<kind>s/).
|
|
66
75
|
|
|
67
76
|
MISSING=""
|
|
77
|
+
DEGRADED=""
|
|
68
78
|
while IFS= read -r f; do
|
|
69
79
|
[ -z "$f" ] && continue
|
|
70
80
|
name="$(basename "$f" .md)"
|
|
@@ -72,16 +82,21 @@ while IFS= read -r f; do
|
|
|
72
82
|
target="$CONSUMER_AGENTS_DIR/${name}.md"
|
|
73
83
|
# `test -e` returns false for broken symlinks too — exactly what we want.
|
|
74
84
|
if [ ! -e "$target" ]; then
|
|
75
|
-
if [ -z "$MISSING" ]; then
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
if [ -z "$MISSING" ]; then MISSING="$name"; else MISSING="$MISSING, $name"; fi
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
# File exists. If it's a symlink → CC discovery sees it, healthy.
|
|
89
|
+
# If it's a regular file with the baldart-generated marker → degraded
|
|
90
|
+
# state (legacy overlay layout before v3.27.0).
|
|
91
|
+
if [ ! -L "$target" ] && [ -f "$target" ]; then
|
|
92
|
+
if head -c 4096 "$target" 2>/dev/null | grep -q "<!-- baldart-generated:" ; then
|
|
93
|
+
if [ -z "$DEGRADED" ]; then DEGRADED="$name"; else DEGRADED="$DEGRADED, $name"; fi
|
|
79
94
|
fi
|
|
80
95
|
fi
|
|
81
96
|
done < <(find "$FRAMEWORK_AGENTS_DIR" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sort)
|
|
82
97
|
|
|
83
|
-
# Nothing
|
|
84
|
-
[ -z "$MISSING" ] && exit 0
|
|
98
|
+
# Nothing to report — exit silently. No noise when everything is healthy.
|
|
99
|
+
[ -z "$MISSING" ] && [ -z "$DEGRADED" ] && exit 0
|
|
85
100
|
|
|
86
101
|
# --- emit additionalContext ----------------------------------------------
|
|
87
102
|
|
|
@@ -90,7 +105,14 @@ escape_json() {
|
|
|
90
105
|
printf '%s' "$1" | awk 'BEGIN{ORS=""} {gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); gsub(/\r/,""); gsub(/\n/,"\\n"); print}'
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
MSG="
|
|
108
|
+
MSG=""
|
|
109
|
+
if [ -n "$MISSING" ]; then
|
|
110
|
+
MSG="BALDART expects the following sub-agents to be reachable via the Agent tool in this session, but their backing files are missing from .claude/agents/: ${MISSING}. Calling Agent with one of these subagent_type values will be denied by the agent-discovery-gate PreToolUse hook. Run \`npx baldart doctor\` to repair, then restart this session."
|
|
111
|
+
fi
|
|
112
|
+
if [ -n "$DEGRADED" ]; then
|
|
113
|
+
DEG_MSG="BALDART detected overlay-merged agents stored in the legacy pre-v3.27.0 layout (regular files in .claude/agents/ with the baldart-generated marker): ${DEGRADED}. Claude Code upstream bug #20931 silently skips regular files at session-start discovery, so calling Agent with one of these subagent_type values will fail with InputValidationError even though the file is on disk. Run \`npx baldart update\` to migrate to the v3.27.0+ symlink-indirection layout (the file is moved to .baldart/generated/<kind>s/<name>.md and .claude/agents/<name>.md becomes a symlink to it), then restart this session."
|
|
114
|
+
if [ -n "$MSG" ]; then MSG="${MSG} | ${DEG_MSG}"; else MSG="$DEG_MSG"; fi
|
|
115
|
+
fi
|
|
94
116
|
|
|
95
117
|
cat <<JSON
|
|
96
118
|
{
|
|
@@ -163,18 +163,24 @@ function main() {
|
|
|
163
163
|
|
|
164
164
|
// v3.8.0+: also block direct edits to BALDART-generated files (agents /
|
|
165
165
|
// commands generated from base + overlay). These live OUTSIDE .framework/
|
|
166
|
-
// but carry the baldart-generated marker comment
|
|
167
|
-
//
|
|
168
|
-
//
|
|
166
|
+
// but carry the baldart-generated marker comment in their head. Pre-v3.27.0
|
|
167
|
+
// the marker was on line 1; v3.27.0+ places it after the YAML frontmatter
|
|
168
|
+
// and the actual file lives in .baldart/generated/<kind>s/<name>.md with a
|
|
169
|
+
// symlink from .claude/agents/<name>.md. Match both layouts. The structural
|
|
170
|
+
// regex (requires kind=…name=…overlay_sha=…) avoids false-positives on
|
|
171
|
+
// framework source files that mention the marker as a string literal
|
|
172
|
+
// (e.g. this hook itself, agent-discovery-info.sh, overlay-merger.js).
|
|
173
|
+
const GENERATED_MARKER_RE =
|
|
174
|
+
/<!-- baldart-generated:[\s\S]{0,400}?kind=\S+[\s\S]{0,400}?name=\S+[\s\S]{0,400}?overlay_sha=\S+/;
|
|
169
175
|
try {
|
|
170
176
|
const fs = require('fs');
|
|
171
177
|
if (fs.existsSync(filePath)) {
|
|
172
|
-
const head = fs.readFileSync(filePath, 'utf8').slice(0,
|
|
173
|
-
if (
|
|
174
|
-
// Allow ONLY if the edit explicitly retains the marker
|
|
178
|
+
const head = fs.readFileSync(filePath, 'utf8').slice(0, 5000);
|
|
179
|
+
if (GENERATED_MARKER_RE.test(head)) {
|
|
180
|
+
// Allow ONLY if the edit explicitly retains the marker —
|
|
175
181
|
// catches accidental overwrites while letting BALDART itself regenerate.
|
|
176
182
|
const newContent = extractNewContent(toolName, input.tool_input) || '';
|
|
177
|
-
if (!
|
|
183
|
+
if (!GENERATED_MARKER_RE.test(newContent)) {
|
|
178
184
|
const reason =
|
|
179
185
|
`Cannot edit BALDART-generated file (${filePath}).\n` +
|
|
180
186
|
`This file is auto-generated by \`npx baldart update\` from a base ` +
|
|
@@ -312,11 +312,23 @@ Report to the user, in order:
|
|
|
312
312
|
baldart@…", surface it once so the user knows the CLI bumped too.
|
|
313
313
|
4. **Hooks reconciled**: any hook backfilled (parse "Auto-registered N
|
|
314
314
|
missing hook(s)" or per-line "Registered hook" from stdout).
|
|
315
|
-
5. **
|
|
315
|
+
5. **Agent layout migration (v3.27.0+)**: parse stdout for lines matching
|
|
316
|
+
`agent generated (overlay applied): .claude/agents/<name>.md → ../../.baldart/generated/agents/<name>.md`.
|
|
317
|
+
Pre-v3.27.0 overlay-merged agents (`coder`, `ui-expert`, `code-reviewer`,
|
|
318
|
+
`doc-reviewer`, `qa-sentinel`, `security-reviewer` when overlayed) lived
|
|
319
|
+
as regular files in `.claude/agents/` and were silently invisible to
|
|
320
|
+
Claude Code session-start discovery (upstream bug #20931). The CLI now
|
|
321
|
+
stores merged content in `.baldart/generated/<kind>s/<name>.md` and
|
|
322
|
+
places a symlink at `.claude/agents/<name>.md`. Report the migrated
|
|
323
|
+
names so the user knows they should restart their CC session for the
|
|
324
|
+
new layout to take effect on `subagent_type` resolution.
|
|
325
|
+
6. **Schema notes**: any missing key still flagged → suggest
|
|
316
326
|
`npx baldart configure`.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
doctor checks fired, suggest `npx baldart` (smart doctor).
|
|
327
|
+
7. **Stash-pop status**: clean / conflicts present (with file list).
|
|
328
|
+
8. **Suggested next step**: if schema drift residual or other non-update
|
|
329
|
+
doctor checks fired, suggest `npx baldart` (smart doctor). Also remind:
|
|
330
|
+
if any agent was migrated (step 5), **restart the Claude Code session**
|
|
331
|
+
so the discovery scanner sees the new symlinks.
|
|
320
332
|
|
|
321
333
|
## Failure modes
|
|
322
334
|
|
package/package.json
CHANGED
|
@@ -49,20 +49,38 @@ function buildMarker({ kind, name, baseVersion, baseSha, overlaySha }) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Returns the marker fields encoded in the
|
|
53
|
-
* Used by `update` to know "is this file ours (safe to regenerate) or
|
|
54
|
-
* user edit it manually?"
|
|
52
|
+
* Returns the marker fields encoded in the generated-marker block if present,
|
|
53
|
+
* or null. Used by `update` to know "is this file ours (safe to regenerate) or
|
|
54
|
+
* did the user edit it manually?"
|
|
55
|
+
*
|
|
56
|
+
* The marker block can appear in two positions:
|
|
57
|
+
* - line 1 (pre-v3.26.1 generated files — kept for back-compat reading)
|
|
58
|
+
* - immediately after the closing `---` of the YAML frontmatter (v3.26.1+)
|
|
55
59
|
*
|
|
56
60
|
* `base_sha` is optional — older generated files (pre-v3.19.0) won't have it.
|
|
57
61
|
*/
|
|
58
62
|
function readMarker(content) {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
63
|
+
const idx = content.indexOf(MARKER_PREFIX);
|
|
64
|
+
if (idx === -1) return null;
|
|
65
|
+
// The marker block ends at the next `-->`. Cap the search to the first 4KB
|
|
66
|
+
// to avoid scanning huge files when the marker isn't actually there.
|
|
67
|
+
const block = content.slice(idx, idx + 4000);
|
|
68
|
+
const end = block.indexOf('-->');
|
|
69
|
+
if (end === -1) return null;
|
|
70
|
+
const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)\s+base_version=(\S+)(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)/);
|
|
62
71
|
if (!m) return null;
|
|
63
72
|
return { kind: m[1], name: m[2], baseVersion: m[3], baseSha: m[4] || null, overlaySha: m[5] };
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Tells whether `content` is a BALDART-generated file (agent or command).
|
|
77
|
+
* Position-agnostic — accepts both pre-v3.26.1 (marker on line 1) and v3.26.1+
|
|
78
|
+
* (marker after the frontmatter) layouts.
|
|
79
|
+
*/
|
|
80
|
+
function isGeneratedFile(content) {
|
|
81
|
+
return readMarker(content) !== null;
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
/**
|
|
67
85
|
* Split markdown text into:
|
|
68
86
|
* - frontmatter: the raw text between leading `---` fences (or '')
|
|
@@ -193,12 +211,17 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
|
|
|
193
211
|
? `---\n${baseParsed.frontmatter}\n---\n`
|
|
194
212
|
: '';
|
|
195
213
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
// v3.26.1+ layout: frontmatter MUST be the first thing in the file —
|
|
215
|
+
// Claude Code's agent discovery uses a strict YAML-frontmatter parser that
|
|
216
|
+
// skips files where `---` is not on line 1. Pre-v3.26.1 placed the marker
|
|
217
|
+
// first, which silently de-registered every overlay-merged agent (no
|
|
218
|
+
// `subagent_type` exposed → spawns failed with "agent not found"). The
|
|
219
|
+
// marker now sits as a CommonMark HTML comment immediately after the
|
|
220
|
+
// closing `---`, which `readMarker` still finds via position-agnostic
|
|
221
|
+
// scan. If the base has no frontmatter (unusual), marker still goes first.
|
|
222
|
+
const generated = fmBlock
|
|
223
|
+
? fmBlock + `${marker}\n` + (baseParsed.preamble || '') + sectionsOut + trailingOut
|
|
224
|
+
: `${marker}\n` + (baseParsed.preamble || '') + sectionsOut + trailingOut;
|
|
202
225
|
|
|
203
226
|
return {
|
|
204
227
|
generated,
|
|
@@ -212,6 +235,7 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
|
|
|
212
235
|
module.exports = {
|
|
213
236
|
mergeOverlay,
|
|
214
237
|
readMarker,
|
|
238
|
+
isGeneratedFile,
|
|
215
239
|
parseMarkdown,
|
|
216
240
|
shortSha,
|
|
217
241
|
computeBaseFileSha,
|
package/src/utils/symlinks.js
CHANGED
|
@@ -423,6 +423,20 @@ class SymlinkUtils {
|
|
|
423
423
|
aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'already-linked' });
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
|
+
// Symlink pointing into .baldart/generated/ → BALDART-managed from a
|
|
427
|
+
// previous overlay state. Overlay has just been removed, so revert to
|
|
428
|
+
// the framework-base symlink and clean up the now-stale generated file.
|
|
429
|
+
if (current.split(path.sep).includes('generated') && current.includes('.baldart')) {
|
|
430
|
+
fs.unlinkSync(linkPath);
|
|
431
|
+
fs.symlinkSync(target, linkPath);
|
|
432
|
+
try {
|
|
433
|
+
const staleGenerated = path.join(this.cwd, '.baldart', 'generated', `${kind}s`, filename);
|
|
434
|
+
if (fs.existsSync(staleGenerated)) fs.unlinkSync(staleGenerated);
|
|
435
|
+
} catch (_) { /* best effort */ }
|
|
436
|
+
UI.info(`[${adapter.label}] ${kind} overlay removed — relinking ${path.join(targetRel, filename)} → framework base`);
|
|
437
|
+
aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
426
440
|
// Broken or pointing elsewhere — replace if broken, else respect user override
|
|
427
441
|
if (!fs.existsSync(linkPath)) {
|
|
428
442
|
fs.unlinkSync(linkPath);
|
|
@@ -438,6 +452,10 @@ class SymlinkUtils {
|
|
|
438
452
|
// Real file. Could be:
|
|
439
453
|
// (a) A baldart-generated file from a previous run where overlay USED to
|
|
440
454
|
// exist but now doesn't — safe to replace with symlink.
|
|
455
|
+
// Pre-v3.27.0 layout: marker file directly at linkPath. v3.27.0+
|
|
456
|
+
// stores the merged file under .baldart/generated/ and points a
|
|
457
|
+
// symlink at it, so the "real file at linkPath" case here is the
|
|
458
|
+
// legacy layout that no longer regenerates — just unlink and resymlink.
|
|
441
459
|
// (b) A user-authored fork — leave alone.
|
|
442
460
|
const { readMarker } = require('./overlay-merger');
|
|
443
461
|
const content = fs.readFileSync(linkPath, 'utf8');
|
|
@@ -447,6 +465,12 @@ class SymlinkUtils {
|
|
|
447
465
|
UI.info(`[${adapter.label}] ${kind} overlay removed — replacing generated file with symlink: ${path.join(targetRel, filename)}`);
|
|
448
466
|
fs.unlinkSync(linkPath);
|
|
449
467
|
fs.symlinkSync(target, linkPath);
|
|
468
|
+
// Also clean up any stale .baldart/generated/<kind>s/<name>.md from
|
|
469
|
+
// the v3.27.0+ indirect layout (no overlay means no generated file).
|
|
470
|
+
try {
|
|
471
|
+
const staleGenerated = path.join(this.cwd, '.baldart', 'generated', `${kind}s`, filename);
|
|
472
|
+
if (fs.existsSync(staleGenerated)) fs.unlinkSync(staleGenerated);
|
|
473
|
+
} catch (_) { /* best effort */ }
|
|
450
474
|
aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
|
|
451
475
|
return;
|
|
452
476
|
}
|
|
@@ -460,7 +484,7 @@ class SymlinkUtils {
|
|
|
460
484
|
}
|
|
461
485
|
|
|
462
486
|
_generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
|
|
463
|
-
const { mergeOverlay, readMarker } = require('./overlay-merger');
|
|
487
|
+
const { mergeOverlay, readMarker, isGeneratedFile } = require('./overlay-merger');
|
|
464
488
|
const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
|
|
465
489
|
const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
|
|
466
490
|
const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion });
|
|
@@ -472,13 +496,28 @@ class SymlinkUtils {
|
|
|
472
496
|
});
|
|
473
497
|
}
|
|
474
498
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
499
|
+
// v3.27.0+ layout: the merged content lives at
|
|
500
|
+
// .baldart/generated/<kind>s/<name>.md (real file)
|
|
501
|
+
// and the consumer-visible path is a symlink:
|
|
502
|
+
// .claude/agents/<name>.md → ../../.baldart/generated/agents/<name>.md
|
|
503
|
+
// This is a workaround for Claude Code upstream bug #20931, where the
|
|
504
|
+
// session-start scanner that builds the `subagent_type` registry skips
|
|
505
|
+
// regular files in .claude/agents/ (only symlinks are picked up). The
|
|
506
|
+
// pre-v3.27.0 layout wrote the merged file directly at linkPath as a
|
|
507
|
+
// real file, which made every overlay-merged agent silently invisible
|
|
508
|
+
// to the orchestrator — see CHANGELOG 3.27.0 root cause analysis.
|
|
509
|
+
const generatedDir = path.join(this.cwd, '.baldart', 'generated', `${kind}s`);
|
|
510
|
+
const generatedPath = path.join(generatedDir, `${name}.md`);
|
|
511
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
512
|
+
|
|
513
|
+
// Inspect what currently sits at linkPath. Three legal states:
|
|
514
|
+
// (1) symlink we already manage (resolves into .baldart/generated/) → stale, replace
|
|
515
|
+
// (2) symlink to the framework base (no overlay last time) → upgrade to indirect symlink
|
|
516
|
+
// (3) regular file with baldart-generated marker (legacy pre-v3.27.0 layout) → migrate
|
|
517
|
+
// (4) regular file without marker → user fork, refuse to touch
|
|
478
518
|
if (lstat && !lstat.isSymbolicLink()) {
|
|
479
519
|
const existing = fs.readFileSync(linkPath, 'utf8');
|
|
480
|
-
|
|
481
|
-
if (!m) {
|
|
520
|
+
if (!isGeneratedFile(existing)) {
|
|
482
521
|
UI.warning(`[${adapter.label}] ${kind} ${name}: existing file at ${path.join(targetRel, name)}.md has no baldart-generated marker — NOT overwriting (user fork?). Move it aside (or merge it into the overlay) and re-run update.`);
|
|
483
522
|
aggregate.conflicts.push({
|
|
484
523
|
tool: adapter.name, kind, name,
|
|
@@ -488,15 +527,23 @@ class SymlinkUtils {
|
|
|
488
527
|
});
|
|
489
528
|
return;
|
|
490
529
|
}
|
|
530
|
+
// Legacy generated file at linkPath — migrate (delete in-place, rewrite at generatedPath, symlink).
|
|
491
531
|
}
|
|
492
532
|
|
|
493
|
-
//
|
|
533
|
+
// Write merged content to the indirect location.
|
|
534
|
+
fs.writeFileSync(generatedPath, merged.generated);
|
|
535
|
+
|
|
536
|
+
// Compute the relative symlink target (portable across moves of the
|
|
537
|
+
// project root).
|
|
538
|
+
const relativeTarget = path.relative(path.dirname(linkPath), generatedPath);
|
|
539
|
+
|
|
540
|
+
// Replace whatever currently lives at linkPath with the new symlink.
|
|
494
541
|
if (lstat) {
|
|
495
542
|
try { fs.unlinkSync(linkPath); }
|
|
496
543
|
catch (_) { try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch (_) { /* noop */ } }
|
|
497
544
|
}
|
|
498
|
-
fs.
|
|
499
|
-
UI.success(`[${adapter.label}] ${kind} generated (overlay applied): ${path.join(targetRel, name)}.md`);
|
|
545
|
+
fs.symlinkSync(relativeTarget, linkPath);
|
|
546
|
+
UI.success(`[${adapter.label}] ${kind} generated (overlay applied): ${path.join(targetRel, name)}.md → ${relativeTarget}`);
|
|
500
547
|
aggregate.generated.push({ tool: adapter.name, kind, name, overlaySha: merged.overlaySha });
|
|
501
548
|
}
|
|
502
549
|
|
|
@@ -598,13 +645,13 @@ class SymlinkUtils {
|
|
|
598
645
|
UI.warning(`[${adapter.label}] Legacy bulk symlink at ${rel}. Run \`npx baldart update\` to convert to per-item layout.`);
|
|
599
646
|
allValid = false;
|
|
600
647
|
} else {
|
|
648
|
+
const { isGeneratedFile } = require('./overlay-merger');
|
|
601
649
|
const entries = fs.readdirSync(full);
|
|
602
650
|
const frameworkOwned = entries.filter((n) => {
|
|
603
651
|
const p = path.join(full, n);
|
|
604
652
|
try {
|
|
605
653
|
if (fs.lstatSync(p).isSymbolicLink()) return true;
|
|
606
|
-
|
|
607
|
-
return content.startsWith('<!-- baldart-generated:');
|
|
654
|
+
return isGeneratedFile(fs.readFileSync(p, 'utf8'));
|
|
608
655
|
} catch { return false; }
|
|
609
656
|
});
|
|
610
657
|
if (frameworkOwned.length === 0) {
|