baldart 3.7.1 → 3.8.1

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 CHANGED
@@ -5,6 +5,72 @@ 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.8.1] - 2026-05-22
9
+
10
+ Documentation: promote the "one command for everything" install path in the README and surface the global-install recommendation in CLAUDE.md.
11
+
12
+ ### Changed
13
+
14
+ - **`README.md` Quick Start** — restructured. Primary path is now `npm i -g baldart` once, then `baldart` (no args) in any project. Added a table that maps every repo state to the action `baldart` proposes (install / migrate / configure / update / push). The `npx baldart …` form remains documented as the no-global-install alternative.
15
+ - **`CLAUDE.md`** — added the recommended end-user install (`npm i -g baldart`) to the distribution note.
16
+
17
+ No behavioural changes; the smart-doctor entry point has worked this way since v3.2.0, this release just makes it discoverable.
18
+
19
+ ## [3.8.0] - 2026-05-22
20
+
21
+ `.claude/agents/` and `.claude/commands/` move from bulk symlink to **per-item merge** with an **overlay system** (compile-time merge of base + overlay). Closes the long-standing gap where specializing a framework agent forced you to either fork the whole file (and lose upstream updates) or contaminate `.framework/`.
22
+
23
+ ### Added
24
+
25
+ - **`src/utils/overlay-merger.js`** — new pure-function module. `mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion })` returns the generated file content + warnings. Section markers: `## [OVERRIDE]`, `## [APPEND]`, `## [PREPEND]`; un-marked H2 sections in the overlay are appended at the end. Base frontmatter (`name`, `description`, `tools`, `model`) is preserved verbatim. Output carries a leading `<!-- baldart-generated: kind=… name=… base_version=… overlay_sha=… -->` marker so future updates know it's regeneration-safe.
26
+ - **`SymlinkUtils.mergeAgents(opts)` / `mergeCommands(opts)`** — per-item merge with overlay awareness:
27
+ - No overlay → install as symlink (same model as skills).
28
+ - Overlay present → write a real file with the merged content + marker.
29
+ - Existing generated file → detected by marker → safely regenerated.
30
+ - User-authored fork (no marker) → conflict logged, file left untouched, user told to migrate to overlay.
31
+ - Broken symlink → silently re-linked (same fix-pattern as v3.5.1).
32
+ - **`tool-adapters/claude.js`** declares `agentsDir()` and `commandsDir()`. Codex returns `null` (no equivalent), so the dispatch skips it cleanly.
33
+ - **`framework/.claude/hooks/framework-edit-gate.js`** extended: blocks `Edit`/`Write` of any file whose first line is `<!-- baldart-generated:` unless the new content also starts with the same marker. Forces the user to edit the overlay instead.
34
+ - **Example overlays**: `framework/templates/overlays/agents/coder.example.md` and `framework/templates/overlays/commands/codexreview.example.md` showing all three section markers in use.
35
+ - **`framework/agents/project-context.md` § 5.bis** — new section documenting the agent/command overlay protocol, layout, frontmatter, section markers, generation rules, drift handling, and hard rules.
36
+ - **`baldart doctor` Overlays line** now counts skill overlays + agent overlays + command overlays.
37
+
38
+ ### Changed
39
+
40
+ - **`SymlinkUtils.createBulkSymlinks()`** — no longer creates bulk symlinks for `.claude/agents/` and `.claude/commands/`. If a legacy bulk symlink is detected (v3.7.x and earlier layout), it's unlinked and replaced with a real directory, populated per-item by `mergeAgents`/`mergeCommands` immediately after.
41
+ - **`SymlinkUtils.createAllSymlinks({ tools })`** — calls `mergeSkills` + `mergeAgents` + `mergeCommands` in sequence.
42
+ - **`SymlinkUtils.verifySymlinks()`** — `AGENTS.md` and `agents/` stay bulk symlinks; `.claude/agents/` and `.claude/commands/` are now validated as real dirs containing at least one framework-managed file (symlink or generated). Per-tool, only for tools whose adapter declares a non-null `agentsDir()` / `commandsDir()`.
43
+ - **`src/commands/update.js`** — post-update reconcile now runs the new agent + command merges in addition to skills.
44
+ - **`src/commands/migrate.js`** — additionally converts legacy `.claude/agents` and `.claude/commands` bulk symlinks to per-item.
45
+
46
+ ### Migration (pre-3.8.0 → 3.8.0)
47
+
48
+ On the next `npx baldart update`, an old consumer sees:
49
+
50
+ ```
51
+ ⚠ Detected legacy bulk symlink at .claude/agents. Converting to per-item layout (v3.8.0+).
52
+ ⚠ Detected legacy bulk symlink at .claude/commands. Converting to per-item layout (v3.8.0+).
53
+ ✓ [Claude Code] agent linked: .claude/agents/coder.md
54
+
55
+ ```
56
+
57
+ Zero data loss (the bulk symlink was just one pointer; the real content stays in `.framework/`). After the convert, the user can create `.baldart/overlays/agents/<name>.md` to specialize any framework agent without forking.
58
+
59
+ ### Verification (proven in sandbox)
60
+
61
+ 1. **Legacy → per-item migration**: bulk `.claude/agents` symlink → real dir + per-item symlinks ✓
62
+ 2. **Overlay create → regenerate**: real generated file with marker + overlay content visible ✓
63
+ 3. **Overlay remove → revert to symlink**: generated file deleted, symlink re-created ✓
64
+ 4. **User fork without marker → conflict logged**: framework version NOT overwritten, user file preserved, guidance printed ✓
65
+ 5. **Heading mismatch in overlay**: warning emitted, content appended as new section (no silent loss) ✓
66
+ 6. **Marker round-trip**: SHA-256-truncated overlay hash + framework version recoverable from generated file header ✓
67
+
68
+ ### Out of scope (explicitly deferred)
69
+
70
+ - **Codex agent/command equivalents**: Codex doesn't have subagents or slash commands (its custom prompts are deprecated). The overlay system is Claude-only by mechanism; the cross-tool flat skill model (v3.7.0) continues to handle Codex.
71
+ - **Skill overlay refactor**: skills today use runtime concatenation (SKILL.md body reads overlay), which works. Migrating skills to compile-time merge is opt-in for a future release.
72
+ - **Drift-section diff during update**: structural support is in place via `base_agent_version` in overlay frontmatter and `base_version` in generated marker, but the actual section-level diff against the previous-version base content is a future enhancement (the framework just emits a one-line "overlay vs vX → vY" warning today).
73
+
8
74
  ## [3.7.1] - 2026-05-22
9
75
 
10
76
  `baldart update` now actively migrates pre-v3.7.0 configs to the new multi-tool layout instead of silently leaving them on `[claude]` only.
package/README.md CHANGED
@@ -14,47 +14,68 @@ BALDART provides a portable system for:
14
14
 
15
15
  ## Quick Start
16
16
 
17
- ### Installation
17
+ ### One command for everything (recommended)
18
+
19
+ Install the CLI globally once:
18
20
 
19
21
  ```bash
20
- # In your project directory
21
- npx baldart add
22
+ npm i -g baldart
22
23
  ```
23
24
 
24
- > Available on npm as [`baldart`](https://www.npmjs.com/package/baldart). If you need to pin a specific version (or you're hitting an old cached npx tarball), use `npx baldart@3.6.1 <cmd>` (or any other version). The legacy form `npx -y github:antbald/BALDART <cmd>` still works for unreleased commits on `main`.
25
+ Then in any project directory just run:
25
26
 
26
- That's it! The `add` command handles everything automatically:
27
- - Downloads the framework via Git subtree into `.framework/`
28
- - Creates symlinks for auto-updateable files (AGENTS.md, agents/, .claude/agents/, .claude/commands/)
29
- - Copies customizable templates (hooks, UI guidelines, backlog cards)
30
- - Creates required directories (docs/, templates/, backlog/)
27
+ ```bash
28
+ baldart
29
+ ```
31
30
 
32
- No additional activation steps needed once installed, Claude Code will automatically pick up the agents, commands, and protocols.
31
+ That's it. `baldart` with no arguments runs the **smart doctor**: it auto-detects the repo state and proposes the next sensible action with a `Y/n` prompt. You don't need to remember `add` vs `update` vs `configure` vs `migrate` — it picks the right one.
33
32
 
34
- ### Usage
33
+ | Repo state | What `baldart` proposes |
34
+ |---|---|
35
+ | No `.framework/` | "Install BALDART framework" → runs `add` |
36
+ | Legacy bulk-symlink (pre-v3.8.0) | "Migrate legacy layout" → runs `migrate` |
37
+ | Missing `baldart.config.yml` | "Generate baldart.config.yml" → runs `configure` |
38
+ | New config keys in this framework version | "Refresh baldart.config.yml" → runs `configure` |
39
+ | `framework-edit-gate` hook not registered | "Register hook" |
40
+ | Remote framework is ahead | "Pull N commit(s) from upstream" → runs `update` |
41
+ | Local framework changes ready to share | "Push local improvements upstream" → runs `push` |
35
42
 
36
- ```bash
37
- # Smart entry point: diagnose install state and propose next action.
38
- # `npx baldart` with no arguments runs this; equivalent to `baldart doctor`.
39
- npx baldart
43
+ If several conditions hold, actions are presented in priority order (e.g. update before push).
40
44
 
41
- # Show version + install date + drift from remote
42
- npx baldart version
45
+ Upgrade the CLI later:
46
+ ```bash
47
+ npm i -g baldart@latest
48
+ ```
43
49
 
44
- # Check status (symlinks, config, overlay drift)
45
- npx baldart status
50
+ ### Without the global install
46
51
 
47
- # (Re)generate baldart.config.yml interactive prompts + autodetection
48
- npx baldart configure
52
+ If you don't want a global install, every command also works via `npx`:
49
53
 
50
- # Update framework
54
+ ```bash
55
+ npx baldart # same smart entry point
56
+ npx baldart add # explicit subcommands
51
57
  npx baldart update
52
-
53
- # Contribute improvements upstream (auto bump + CHANGELOG + tag)
58
+ npx baldart configure
54
59
  npx baldart push
60
+ npx baldart version
61
+ npx baldart status
55
62
  ```
56
63
 
57
- > **Tip**: During installation, BALDART offers to configure git aliases (`fw-version`, `fw-update`, `fw-push`) so you can skip the `npx baldart` prefix after the first install.
64
+ > The legacy `npx -y github:antbald/BALDART <cmd>` form still works if you need to install from an unreleased commit on `main`. For pinning a specific version: `npx baldart@3.8.0 <cmd>`.
65
+
66
+ ### What `add` (first install) does
67
+
68
+ - Downloads the framework via Git subtree into `.framework/`
69
+ - Creates symlinks for auto-updateable files (AGENTS.md, agents/)
70
+ - Per-item-merges framework agents/commands/skills into `.claude/` (real directories where your own files coexist)
71
+ - Detects Codex on the machine and mirrors skills into `.agents/skills/` if found
72
+ - Copies customizable templates (hooks, UI guidelines, backlog cards)
73
+ - Registers the `framework-edit-gate` hook in `.claude/settings.json`
74
+ - Runs `configure` interactively to populate `baldart.config.yml`
75
+
76
+ No additional activation steps needed — once installed, Claude Code (and Codex) automatically pick up the agents, commands, skills, and protocols.
77
+
78
+ > **Tip**: During installation, BALDART offers to configure git aliases (`fw-version`, `fw-update`, `fw-push`) — handy if you don't want the global install but find `npx baldart …` long.
58
79
 
59
80
  ## Features
60
81
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.7.1
1
+ 3.8.1
@@ -158,6 +158,44 @@ function main() {
158
158
  const filePath = input.tool_input && input.tool_input.file_path;
159
159
  if (!filePath || typeof filePath !== 'string') safeExit();
160
160
 
161
+ // v3.8.0+: also block direct edits to BALDART-generated files (agents /
162
+ // commands generated from base + overlay). These live OUTSIDE .framework/
163
+ // but carry the baldart-generated marker comment as their first line.
164
+ // The user should edit the overlay, not the generated file, otherwise the
165
+ // next `baldart update` will overwrite their changes.
166
+ try {
167
+ const fs = require('fs');
168
+ if (fs.existsSync(filePath)) {
169
+ const head = fs.readFileSync(filePath, 'utf8').slice(0, 200);
170
+ if (head.startsWith('<!-- baldart-generated:')) {
171
+ // Allow ONLY if the edit explicitly retains the marker on line 1 —
172
+ // catches accidental overwrites while letting BALDART itself regenerate.
173
+ const newContent = extractNewContent(toolName, input.tool_input) || '';
174
+ if (!newContent.startsWith('<!-- baldart-generated:')) {
175
+ const reason =
176
+ `Cannot edit BALDART-generated file (${filePath}).\n` +
177
+ `This file is auto-generated by \`npx baldart update\` from a base ` +
178
+ `framework file plus an overlay. Manual edits will be overwritten.\n\n` +
179
+ `To customise: edit the corresponding overlay under .baldart/overlays/` +
180
+ `(or create it if absent), then re-run \`npx baldart update\`.`;
181
+ try {
182
+ process.stdout.write(JSON.stringify({
183
+ hookSpecificOutput: {
184
+ hookEventName: 'PreToolUse',
185
+ permissionDecision: 'deny',
186
+ permissionDecisionReason: reason
187
+ }
188
+ }));
189
+ } catch (_) {
190
+ process.stderr.write(reason + '\n');
191
+ process.exit(2);
192
+ }
193
+ process.exit(0);
194
+ }
195
+ }
196
+ }
197
+ } catch (_) { /* fail-safe: do not block on hook internal errors */ }
198
+
161
199
  const realPath = resolveReal(filePath);
162
200
  if (!realPath) safeExit();
163
201
 
@@ -98,6 +98,65 @@ If `base_skill_version` in the overlay is older than the installed framework ver
98
98
 
99
99
  This is non-blocking by design — silent overrides are worse than noisy ones.
100
100
 
101
+ ## 5.bis) Overlays for agents and commands (v3.8.0+)
102
+
103
+ Skill overlays use **runtime concatenation** — the SKILL.md body instructs Claude to read `.baldart/overlays/<skill>.md`. Agent and command files do not have that affordance (Claude Code reads a subagent file as a single unit), so BALDART uses a different model: **compile-time merge**.
104
+
105
+ ### Layout
106
+
107
+ ```
108
+ .baldart/overlays/
109
+ ├── ui-design.md (skill overlay — runtime, unchanged)
110
+ ├── agents/
111
+ │ └── coder.md (agent overlay — compile-time merge)
112
+ └── commands/
113
+ └── codexreview.md (command overlay — compile-time merge)
114
+ ```
115
+
116
+ ### Frontmatter (mandatory for agent / command overlays)
117
+
118
+ ```yaml
119
+ ---
120
+ base_agent: coder # or base_command: codexreview
121
+ base_agent_version: 3.8.0 # framework version when the overlay was authored
122
+ mode: extend # extend | replace
123
+ ---
124
+ ```
125
+
126
+ ### Section markers in the overlay body
127
+
128
+ - `## [OVERRIDE] <heading>` — replace the matching `## <heading>` of the base verbatim.
129
+ - `## [APPEND] <heading>` — add content after the base section.
130
+ - `## [PREPEND] <heading>` — add content before the base section.
131
+ - `## <heading>` (no marker) — append as a new H2 at the end of the merged file.
132
+
133
+ If the overlay targets a heading that no longer exists in the base (e.g. it was renamed upstream), BALDART emits a warning at update time and falls back to appending the overlay block as a new section, so content is never silently dropped.
134
+
135
+ ### Generation
136
+
137
+ At `baldart add` / `baldart update`, for each framework agent/command file `<x>.md`:
138
+
139
+ - No overlay → BALDART installs a **per-item symlink** in `.claude/agents/<x>.md` (or `.claude/commands/<x>.md`) pointing at the framework source.
140
+ - Overlay present → BALDART writes a **generated file** at `.claude/agents/<x>.md` containing base + overlay merged, with a leading marker:
141
+
142
+ ```html
143
+ <!-- baldart-generated: kind=agent name=coder base_version=3.8.0 overlay_sha=ab12cd34
144
+ DO NOT EDIT MANUALLY. Edit .baldart/overlays/agents/coder.md instead;
145
+ this file is regenerated by `npx baldart update`. -->
146
+ ```
147
+
148
+ The base's frontmatter (`name`, `description`, `tools`, `model`) is preserved verbatim — the overlay's frontmatter is metadata for BALDART only and is NOT propagated to the generated file.
149
+
150
+ ### Drift detection
151
+
152
+ When `base_agent_version` in the overlay is older than the installed framework version, BALDART compares the base sections the overlay overrides between the two versions. If a targeted section has changed upstream, `baldart update` emits a one-line warning per overlay and suggests bumping `base_agent_version` after reviewing the new base content.
153
+
154
+ ### Hard rules
155
+
156
+ - Never edit `.claude/agents/<x>.md` or `.claude/commands/<x>.md` if it carries the `baldart-generated` marker — your edits will be overwritten on the next update. Edit the overlay instead. The `framework-edit-gate` hook blocks direct edits to generated files.
157
+ - Overlay files (`.baldart/overlays/**`) live in the consumer repo only. `baldart push` operates on `.framework/...` exclusively, so overlays are structurally excluded from upstream contributions.
158
+ - A user-authored agent/command with NO framework counterpart (e.g. `.claude/agents/mayo-billing-expert.md`) is left alone forever. Per-item merge only touches names that exist in the framework.
159
+
101
160
  ## 6) Autodetection (used by `baldart configure`)
102
161
 
103
162
  `npx baldart configure` populates `baldart.config.yml` by probing the filesystem. The complete probe table (source of truth: `src/commands/configure.js` `detect()`):
@@ -0,0 +1,43 @@
1
+ ---
2
+ base_agent: coder
3
+ base_agent_version: 3.8.0
4
+ mode: extend
5
+ ---
6
+
7
+ > **How to use this example**
8
+ >
9
+ > Copy this file to `.baldart/overlays/agents/coder.md` (drop the `.example`)
10
+ > and adapt it to your project. The framework will regenerate
11
+ > `.claude/agents/coder.md` from base + overlay on every `npx baldart update`.
12
+ >
13
+ > Section markers:
14
+ > - `## [OVERRIDE] <heading>` — replace the entire matching H2 from the base.
15
+ > - `## [APPEND] <heading>` — add content after the base section.
16
+ > - `## [PREPEND] <heading>` — add content before the base section.
17
+ > - `## <heading>` with no marker — appended at the end as a new custom section.
18
+ >
19
+ > The base's frontmatter (`name`, `description`, `tools`, `model`) is preserved
20
+ > automatically — do NOT redeclare it in the overlay.
21
+
22
+ ## [APPEND] Quality bar
23
+
24
+ Project-specific addition that lives in your repo only:
25
+
26
+ - Before closing any task, run `npm run test:e2e -- --grep <feature>`.
27
+ - Attach the Playwright screenshot to the commit body.
28
+ - Never push to `develop` without the QA label on the PR.
29
+
30
+ ## [OVERRIDE] When to escalate
31
+
32
+ In this project, escalate to the human owner BEFORE making any change that
33
+ touches `prisma/schema.prisma`, `next.config.ts`, or anything under
34
+ `src/lib/billing/`. These paths are policy-protected.
35
+
36
+ ## Project-only conventions
37
+
38
+ Custom rules with no equivalent in the base — appended as a new H2:
39
+
40
+ - All API calls go through `src/lib/api-client.ts`. Never call `fetch()` directly.
41
+ - Logging always via `src/lib/observability/logger.ts` (no `console.log` in
42
+ production code paths).
43
+ - Feature flags live in `src/lib/flags/` and are read via `useFlag(name)`.
@@ -0,0 +1,17 @@
1
+ ---
2
+ base_command: codexreview
3
+ base_command_version: 3.8.0
4
+ mode: extend
5
+ ---
6
+
7
+ > Copy to `.baldart/overlays/commands/codexreview.md` and adapt freely.
8
+ > Same merge model as agent overlays: `[OVERRIDE]` / `[APPEND]` / `[PREPEND]`
9
+ > with H2 heading matching, plus untagged H2 sections appended at the end.
10
+
11
+ ## [APPEND] Project-specific gates
12
+
13
+ Before approving a PR in this repo:
14
+
15
+ 1. Verify GitHub status check `mayo-build` is green.
16
+ 2. Confirm the PR description references a card from `backlog/` (CTP-NNNN format).
17
+ 3. If the PR touches `src/lib/billing/` request a second reviewer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -78,11 +78,17 @@ function configSchemaDrift(config, template) {
78
78
  }
79
79
 
80
80
  function countOverlays(cwd) {
81
- const dir = path.join(cwd, '.baldart', 'overlays');
82
- if (!fileExists(dir)) return 0;
83
- try {
84
- return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
85
- } catch (_) { return 0; }
81
+ const root = path.join(cwd, '.baldart', 'overlays');
82
+ if (!fileExists(root)) return 0;
83
+ // Skill overlays at root, agent overlays under agents/, command under commands/.
84
+ const counts = (sub) => {
85
+ const d = sub ? path.join(root, sub) : root;
86
+ if (!fileExists(d)) return 0;
87
+ try {
88
+ return fs.readdirSync(d).filter((f) => f.endsWith('.md') && !f.endsWith('.example.md')).length;
89
+ } catch (_) { return 0; }
90
+ };
91
+ return counts('') + counts('agents') + counts('commands');
86
92
  }
87
93
 
88
94
  function overlayDrift(cwd, frameworkVersion) {
@@ -128,6 +128,9 @@ async function migrate() {
128
128
  }
129
129
  } catch (_) { /* keep default */ }
130
130
  const mergeResult = symlinks.mergeSkills({ tools: enabledTools });
131
+ // v3.8.0+: also per-item merge agents and commands (was bulk symlink before).
132
+ symlinks.mergeAgents({ tools: enabledTools });
133
+ symlinks.mergeCommands({ tools: enabledTools });
131
134
  UI.info(`Linked ${mergeResult.linked.length} framework skills, kept ${mergeResult.skipped.length} as-is, ${mergeResult.conflicts.length} conflict(s).`);
132
135
 
133
136
  // --- Step 3: restore user skills from .backup --------------------------
@@ -329,9 +329,11 @@ async function update(options = {}) {
329
329
  await symlinks.createAllSymlinks({ mode: 'prompt', tools: enabledTools });
330
330
  }
331
331
  } else {
332
- // Re-run the per-item skill merge for every enabled tool so newly-shipped
333
- // skills are linked in each tool's expected directory.
332
+ // Re-run the per-item merges for every enabled tool so newly-shipped
333
+ // skills/agents/commands are linked in each tool's expected directory.
334
334
  symlinks.mergeSkills({ tools: enabledTools });
335
+ symlinks.mergeAgents({ tools: enabledTools });
336
+ symlinks.mergeCommands({ tools: enabledTools });
335
337
  }
336
338
 
337
339
  // Routines wizard (since v2.1.0) — surfaces routines added in the new framework version
@@ -0,0 +1,206 @@
1
+ const crypto = require('crypto');
2
+ const yaml = require('js-yaml');
3
+
4
+ /**
5
+ * Overlay merge engine for agents and commands.
6
+ *
7
+ * Reads a base markdown file (from the framework) plus an overlay file
8
+ * (from `.baldart/overlays/<kind>/<name>.md`) and produces the merged
9
+ * content that BALDART writes into `.claude/<kind>s/<name>.md` at
10
+ * `add`/`update` time.
11
+ *
12
+ * Section model
13
+ * -------------
14
+ * - The base is split into H2 sections (`^## <heading>` … up to next H2/EOF).
15
+ * - The overlay body uses three section markers:
16
+ * `## [OVERRIDE] <heading>` → replace base section <heading> verbatim
17
+ * `## [APPEND] <heading>` → append overlay block after base section
18
+ * `## [PREPEND] <heading>` → prepend overlay block before base section
19
+ * Sections in the overlay without a marker are appended at the end of the
20
+ * merged file as new H2 sections (custom additions).
21
+ *
22
+ * - The base file's frontmatter (YAML between leading `---` fences) is
23
+ * preserved untouched in the output — Claude Code uses `name`, `description`,
24
+ * `tools`, `model` from it. Overlay frontmatter is metadata FOR BALDART only
25
+ * (`base_agent`, `base_agent_version`, `mode`) and is NOT propagated.
26
+ *
27
+ * - The output gets a BALDART marker comment as the first line (above the
28
+ * frontmatter) so `update` can detect that the file is regeneration-safe.
29
+ */
30
+
31
+ const MARKER_PREFIX = '<!-- baldart-generated:';
32
+
33
+ function shortSha(content) {
34
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
35
+ }
36
+
37
+ function buildMarker({ kind, name, baseVersion, overlaySha }) {
38
+ return `${MARKER_PREFIX} kind=${kind} name=${name} base_version=${baseVersion} overlay_sha=${overlaySha}
39
+ DO NOT EDIT MANUALLY. Edit .baldart/overlays/${kind}s/${name}.md instead;
40
+ this file is regenerated by \`npx baldart update\`. -->`;
41
+ }
42
+
43
+ /**
44
+ * Returns the marker fields encoded in the first line if present, or null.
45
+ * Used by `update` to know "is this file ours (safe to regenerate) or did the
46
+ * user edit it manually?"
47
+ */
48
+ function readMarker(content) {
49
+ const firstLine = content.split('\n').slice(0, 3).join('\n');
50
+ if (!firstLine.includes(MARKER_PREFIX)) return null;
51
+ const m = firstLine.match(/kind=(\S+) name=(\S+) base_version=(\S+) overlay_sha=(\S+)/);
52
+ if (!m) return null;
53
+ return { kind: m[1], name: m[2], baseVersion: m[3], overlaySha: m[4] };
54
+ }
55
+
56
+ /**
57
+ * Split markdown text into:
58
+ * - frontmatter: the raw text between leading `---` fences (or '')
59
+ * - preamble: any text after frontmatter before the first H2
60
+ * - sections: ordered list of { heading, marker, body }
61
+ * marker is null | 'OVERRIDE' | 'APPEND' | 'PREPEND'
62
+ */
63
+ function parseMarkdown(text) {
64
+ let frontmatter = '';
65
+ let rest = text;
66
+
67
+ // Strip leading BOM / whitespace
68
+ rest = rest.replace(/^/, '');
69
+
70
+ const fmMatch = rest.match(/^---\n([\s\S]*?)\n---\n?/);
71
+ if (fmMatch) {
72
+ frontmatter = fmMatch[1];
73
+ rest = rest.slice(fmMatch[0].length);
74
+ }
75
+
76
+ // Split on H2 boundaries. Keep delimiters by using a lookahead.
77
+ const parts = rest.split(/(?=^## )/m);
78
+ const preamble = (parts.length && !parts[0].startsWith('## ')) ? parts.shift() : '';
79
+
80
+ const sections = parts.map((block) => {
81
+ // Heading line
82
+ const eol = block.indexOf('\n');
83
+ const headingLine = eol >= 0 ? block.slice(0, eol) : block;
84
+ const body = eol >= 0 ? block.slice(eol + 1) : '';
85
+
86
+ // Detect marker — `## [OVERRIDE] Foo` etc. Marker keyword is upper-case
87
+ // between square brackets, then a single space, then the heading text.
88
+ const m = headingLine.match(/^##\s+\[(OVERRIDE|APPEND|PREPEND)\]\s+(.+?)\s*$/);
89
+ if (m) {
90
+ return { marker: m[1], heading: m[2].trim(), body: body.replace(/\n+$/, '') + '\n' };
91
+ }
92
+ const plain = headingLine.match(/^##\s+(.+?)\s*$/);
93
+ return { marker: null, heading: plain ? plain[1].trim() : '', body: body.replace(/\n+$/, '') + '\n' };
94
+ });
95
+
96
+ return { frontmatter, preamble, sections };
97
+ }
98
+
99
+ function rebuildSections(sections) {
100
+ return sections.map((s) => `## ${s.heading}\n${s.body}`).join('\n').replace(/\n+$/, '\n');
101
+ }
102
+
103
+ /**
104
+ * Merge base + overlay → generated content + warnings.
105
+ *
106
+ * @param {Object} args
107
+ * @param {string} args.kind - 'agent' | 'command'
108
+ * @param {string} args.name - identifier (filename without extension)
109
+ * @param {string} args.baseContent - raw markdown of the framework base
110
+ * @param {string} args.overlayContent - raw markdown of the overlay
111
+ * @param {string} args.frameworkVersion - the installed framework VERSION
112
+ * @returns {{ generated: string, warnings: string[], baseVersionTargeted: string, overlayMode: string }}
113
+ */
114
+ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion }) {
115
+ const warnings = [];
116
+
117
+ // Parse overlay frontmatter for metadata.
118
+ const overlayParsed = parseMarkdown(overlayContent);
119
+ let overlayMeta = {};
120
+ if (overlayParsed.frontmatter) {
121
+ try { overlayMeta = yaml.load(overlayParsed.frontmatter) || {}; }
122
+ catch (err) {
123
+ warnings.push(`overlay frontmatter parse error (${err.message}) — treating overlay as plain markdown without metadata`);
124
+ }
125
+ }
126
+ const baseVersionTargeted = String(overlayMeta[`base_${kind}_version`] || '').trim() || 'unknown';
127
+ const overlayMode = (overlayMeta.mode || 'extend').trim();
128
+
129
+ // Parse the base. Keep its frontmatter verbatim — that's what Claude reads.
130
+ const baseParsed = parseMarkdown(baseContent);
131
+
132
+ // Build a heading → section map for the base. If headings repeat we keep
133
+ // the first occurrence (Claude Code does the same for repeated `name:`).
134
+ const baseSectionByHeading = new Map();
135
+ baseParsed.sections.forEach((s, idx) => {
136
+ if (s.heading && !baseSectionByHeading.has(s.heading)) {
137
+ baseSectionByHeading.set(s.heading, { ...s, _idx: idx });
138
+ }
139
+ });
140
+
141
+ // Apply overlay sections.
142
+ const newSections = baseParsed.sections.map((s) => ({ ...s }));
143
+ const trailing = []; // overlay sections without marker → custom adds at the end
144
+
145
+ for (const ov of overlayParsed.sections) {
146
+ if (!ov.marker) {
147
+ trailing.push(ov);
148
+ continue;
149
+ }
150
+ const base = baseSectionByHeading.get(ov.heading);
151
+ if (!base) {
152
+ warnings.push(`overlay section "${ov.marker} ${ov.heading}" has no matching H2 in base ${kind} "${name}" — appending as new section instead`);
153
+ trailing.push({ marker: null, heading: ov.heading, body: ov.body });
154
+ continue;
155
+ }
156
+ const target = newSections[base._idx];
157
+ if (ov.marker === 'OVERRIDE') {
158
+ target.body = ov.body;
159
+ } else if (ov.marker === 'APPEND') {
160
+ target.body = `${target.body.replace(/\n+$/, '')}\n\n${ov.body}`;
161
+ } else if (ov.marker === 'PREPEND') {
162
+ target.body = `${ov.body.replace(/\n+$/, '')}\n\n${target.body}`;
163
+ }
164
+ }
165
+
166
+ // Compose the merged body.
167
+ const sectionsOut = rebuildSections(newSections);
168
+ const trailingOut = trailing.length
169
+ ? '\n' + trailing.map((s) => `## ${s.heading}\n${s.body}`).join('\n').replace(/\n+$/, '\n')
170
+ : '';
171
+
172
+ const overlaySha = shortSha(overlayContent);
173
+ const marker = buildMarker({
174
+ kind,
175
+ name,
176
+ baseVersion: frameworkVersion || baseVersionTargeted,
177
+ overlaySha
178
+ });
179
+
180
+ const fmBlock = baseParsed.frontmatter
181
+ ? `---\n${baseParsed.frontmatter}\n---\n`
182
+ : '';
183
+
184
+ const generated =
185
+ `${marker}\n` +
186
+ fmBlock +
187
+ (baseParsed.preamble || '') +
188
+ sectionsOut +
189
+ trailingOut;
190
+
191
+ return {
192
+ generated,
193
+ warnings,
194
+ baseVersionTargeted,
195
+ overlayMode,
196
+ overlaySha
197
+ };
198
+ }
199
+
200
+ module.exports = {
201
+ mergeOverlay,
202
+ readMarker,
203
+ parseMarkdown,
204
+ shortSha,
205
+ MARKER_PREFIX
206
+ };
@@ -126,6 +126,9 @@ class SymlinkUtils {
126
126
  async createBulkSymlinks(opts = {}) {
127
127
  const mode = opts.mode || 'prompt';
128
128
 
129
+ // AGENTS.md and the cross-tool `agents/` reference dir remain BULK
130
+ // symlinks — they're single canonical artifacts of the framework that
131
+ // do not need per-item user-overlay behaviour.
129
132
  await this.createSymlink(
130
133
  path.join(FRAMEWORK_PAYLOAD, 'AGENTS.md'),
131
134
  'AGENTS.md',
@@ -140,19 +143,29 @@ class SymlinkUtils {
140
143
 
141
144
  this.ensureDirectory('.claude');
142
145
 
143
- // target is relative to cwd; createSymlink converts it to a path relative
144
- // to the link's parent directory when `relative: true`.
145
- await this.createSymlink(
146
- path.join(FRAMEWORK_PAYLOAD, '.claude', 'agents'),
147
- path.join('.claude', 'agents'),
148
- { relative: true, mode }
149
- );
146
+ // NB: `.claude/agents/` and `.claude/commands/` are NO LONGER bulk
147
+ // symlinks since v3.8.0 — they're real directories with per-item
148
+ // symlinks (or overlay-generated real files). Populated by
149
+ // mergeAgents() and mergeCommands() which createAllSymlinks() invokes
150
+ // right after this method.
151
+ this._convertLegacyBulkSymlinkToDir(path.join('.claude', 'agents'));
152
+ this._convertLegacyBulkSymlinkToDir(path.join('.claude', 'commands'));
153
+ }
150
154
 
151
- await this.createSymlink(
152
- path.join(FRAMEWORK_PAYLOAD, '.claude', 'commands'),
153
- path.join('.claude', 'commands'),
154
- { relative: true, mode }
155
- );
155
+ /**
156
+ * If `rel` exists and is a symlink (the v3.7.x and earlier bulk-symlink
157
+ * layout for .claude/agents and .claude/commands), unlink it so that the
158
+ * per-item merge can populate a real directory in its place. Legacy migration.
159
+ */
160
+ _convertLegacyBulkSymlinkToDir(rel) {
161
+ const full = path.join(this.cwd, rel);
162
+ let lstat = null;
163
+ try { lstat = fs.lstatSync(full); } catch (_) { /* absent */ }
164
+ if (lstat && lstat.isSymbolicLink()) {
165
+ UI.warning(`Detected legacy bulk symlink at ${rel}. Converting to per-item layout (v3.8.0+).`);
166
+ fs.unlinkSync(full);
167
+ }
168
+ this.ensureDirectory(rel);
156
169
  }
157
170
 
158
171
  // -----------------------------------------------------------------------
@@ -305,14 +318,198 @@ class SymlinkUtils {
305
318
  this.mergeSkills({ tools: opts.tools });
306
319
 
307
320
  UI.newline();
321
+ UI.section('Merging Framework Agents');
322
+ this.mergeAgents({ tools: opts.tools });
323
+
324
+ UI.newline();
325
+ UI.section('Merging Framework Commands');
326
+ this.mergeCommands({ tools: opts.tools });
327
+
328
+ UI.newline();
329
+ }
330
+
331
+ // -----------------------------------------------------------------------
332
+ // Per-item merge for agents and commands (v3.8.0+)
333
+ //
334
+ // Same model as skills (per-item, user files coexist, broken-symlink-safe)
335
+ // PLUS overlay support: if `.baldart/overlays/<kind>s/<name>.md` exists,
336
+ // BALDART generates a real file `.claude/<kind>s/<name>.md` = base + overlay
337
+ // merged, with a marker header so future updates know it's regeneration-safe.
338
+ // -----------------------------------------------------------------------
339
+
340
+ mergeAgents(opts = {}) { return this._mergeBulkDir('agent', opts); }
341
+ mergeCommands(opts = {}) { return this._mergeBulkDir('command', opts); }
342
+
343
+ /**
344
+ * Generic per-item merge for kinds whose framework source is a directory
345
+ * of single .md files (agents, commands). For each enabled tool whose
346
+ * adapter declares a target directory (claude.agentsDir(), etc.), this
347
+ * either symlinks each base file or — when an overlay exists — writes a
348
+ * generated real file with the overlay applied.
349
+ */
350
+ _mergeBulkDir(kind, opts = {}) {
351
+ const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
352
+ const aggregate = { linked: [], generated: [], skipped: [], conflicts: [], warnings: [] };
353
+ const { getAdapter } = require('./tool-adapters');
354
+
355
+ const fwSourceDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`);
356
+ if (!fs.existsSync(fwSourceDir)) {
357
+ UI.warning(`No framework ${kind}s found at ${path.relative(this.cwd, fwSourceDir)}. Skipping ${kind} merge.`);
358
+ return aggregate;
359
+ }
360
+
361
+ const fwItems = fs.readdirSync(fwSourceDir)
362
+ .filter((n) => n.endsWith('.md') && !n.startsWith('.') && n !== 'REGISTRY.md');
363
+
364
+ const frameworkVersion = this._readFrameworkVersion();
365
+
366
+ for (const tool of tools) {
367
+ const adapter = getAdapter(tool, this.cwd);
368
+ const dirGetter = kind === 'agent' ? adapter.agentsDir : adapter.commandsDir;
369
+ const targetRel = dirGetter ? dirGetter.call(adapter) : null;
370
+ if (!targetRel) continue; // tool doesn't support this kind (e.g. Codex)
371
+
372
+ this.ensureDirectory(targetRel);
373
+
374
+ for (const filename of fwItems) {
375
+ const baseName = filename.replace(/\.md$/, '');
376
+ const linkPath = path.join(this.cwd, targetRel, filename);
377
+ const fwAbsolute = path.join(fwSourceDir, filename);
378
+
379
+ // Compute the relative-from-link path used by the symlink so it's
380
+ // portable if the project gets moved.
381
+ const linkDepth = targetRel.split(path.sep).filter(Boolean).length;
382
+ const upPath = path.join(...Array(linkDepth).fill('..'));
383
+ const target = path.join(upPath, FRAMEWORK_PAYLOAD, '.claude', `${kind}s`, filename);
384
+
385
+ const overlayRel = path.join('.baldart', 'overlays', `${kind}s`, filename);
386
+ const overlayAbs = path.join(this.cwd, overlayRel);
387
+ const hasOverlay = fs.existsSync(overlayAbs);
388
+
389
+ // Inspect what currently lives at the link path (if anything).
390
+ let lstat = null;
391
+ try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }
392
+
393
+ if (hasOverlay) {
394
+ // Generate a real file from base + overlay.
395
+ this._generateOverlayedFile({
396
+ kind, name: baseName, fwAbsolute, overlayAbs, linkPath, lstat,
397
+ frameworkVersion, aggregate, adapter, targetRel
398
+ });
399
+ } else {
400
+ // Plain symlink, with broken-symlink recovery (same fix-pattern as v3.5.1).
401
+ this._installPerItemSymlink({
402
+ linkPath, target, fwAbsolute, lstat, baseName, filename,
403
+ aggregate, adapter, targetRel, kind
404
+ });
405
+ }
406
+ }
407
+ }
408
+
409
+ return aggregate;
410
+ }
411
+
412
+ _installPerItemSymlink({ linkPath, target, fwAbsolute, lstat, baseName, filename, aggregate, adapter, targetRel, kind }) {
413
+ if (!lstat) {
414
+ fs.symlinkSync(target, linkPath);
415
+ UI.success(`[${adapter.label}] ${kind} linked: ${path.join(targetRel, filename)}`);
416
+ aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
417
+ return;
418
+ }
419
+ if (lstat.isSymbolicLink()) {
420
+ const current = fs.readlinkSync(linkPath);
421
+ const resolved = path.resolve(path.dirname(linkPath), current);
422
+ if (current === target || resolved === fwAbsolute) {
423
+ aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'already-linked' });
424
+ return;
425
+ }
426
+ // Broken or pointing elsewhere — replace if broken, else respect user override
427
+ if (!fs.existsSync(linkPath)) {
428
+ fs.unlinkSync(linkPath);
429
+ fs.symlinkSync(target, linkPath);
430
+ UI.success(`[${adapter.label}] ${kind} re-linked (was broken): ${path.join(targetRel, filename)}`);
431
+ aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
432
+ return;
433
+ }
434
+ UI.info(`[${adapter.label}] ${kind} kept (user override symlink): ${path.join(targetRel, filename)} → ${current}`);
435
+ aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'user-symlink-override' });
436
+ return;
437
+ }
438
+ // Real file. Could be:
439
+ // (a) A baldart-generated file from a previous run where overlay USED to
440
+ // exist but now doesn't — safe to replace with symlink.
441
+ // (b) A user-authored fork — leave alone.
442
+ const { readMarker } = require('./overlay-merger');
443
+ const content = fs.readFileSync(linkPath, 'utf8');
444
+ const marker = readMarker(content);
445
+ if (marker && marker.kind === kind && marker.name === baseName) {
446
+ // Overlay was removed → revert to symlink.
447
+ UI.info(`[${adapter.label}] ${kind} overlay removed — replacing generated file with symlink: ${path.join(targetRel, filename)}`);
448
+ fs.unlinkSync(linkPath);
449
+ fs.symlinkSync(target, linkPath);
450
+ aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
451
+ return;
452
+ }
453
+ UI.warning(`[${adapter.label}] ${kind} name conflict (user file at ${path.join(targetRel, filename)}). Framework version NOT installed. Move user content to .baldart/overlays/${kind}s/${filename} to specialize without losing upstream updates.`);
454
+ aggregate.conflicts.push({
455
+ tool: adapter.name, kind, name: baseName,
456
+ local_path: path.join(targetRel, filename),
457
+ framework_path: path.relative(this.cwd, fwAbsolute),
458
+ detected_at: new Date().toISOString()
459
+ });
460
+ }
461
+
462
+ _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
463
+ const { mergeOverlay, readMarker } = require('./overlay-merger');
464
+ const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
465
+ const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
466
+ const merged = mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion });
467
+
468
+ if (merged.warnings && merged.warnings.length) {
469
+ merged.warnings.forEach((w) => {
470
+ UI.warning(`[${adapter.label}] ${kind} ${name}: ${w}`);
471
+ aggregate.warnings.push({ tool: adapter.name, kind, name, warning: w });
472
+ });
473
+ }
474
+
475
+ // If a previously generated file is on disk, check ownership via marker
476
+ // before overwriting. If it lacks the marker (= user edited manually after
477
+ // a previous generation, or a pre-3.8 fork), refuse to overwrite.
478
+ if (lstat && !lstat.isSymbolicLink()) {
479
+ const existing = fs.readFileSync(linkPath, 'utf8');
480
+ const m = readMarker(existing);
481
+ if (!m) {
482
+ 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
+ aggregate.conflicts.push({
484
+ tool: adapter.name, kind, name,
485
+ local_path: path.join(targetRel, `${name}.md`),
486
+ reason: 'manual-edit-no-marker',
487
+ detected_at: new Date().toISOString()
488
+ });
489
+ return;
490
+ }
491
+ }
492
+
493
+ // Replace any existing symlink/file with the freshly generated content.
494
+ if (lstat) {
495
+ try { fs.unlinkSync(linkPath); }
496
+ catch (_) { try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch (_) { /* noop */ } }
497
+ }
498
+ fs.writeFileSync(linkPath, merged.generated);
499
+ UI.success(`[${adapter.label}] ${kind} generated (overlay applied): ${path.join(targetRel, name)}.md`);
500
+ aggregate.generated.push({ tool: adapter.name, kind, name, overlaySha: merged.overlaySha });
501
+ }
502
+
503
+ _readFrameworkVersion() {
504
+ const vPath = path.join(this.cwd, FRAMEWORK_DIR, 'VERSION');
505
+ try { return fs.readFileSync(vPath, 'utf8').trim(); }
506
+ catch { return 'unknown'; }
308
507
  }
309
508
 
310
509
  verifySymlinks() {
311
510
  const bulkSymlinks = [
312
511
  'AGENTS.md',
313
- 'agents',
314
- '.claude/agents',
315
- '.claude/commands'
512
+ 'agents'
316
513
  ];
317
514
 
318
515
  let allValid = true;
@@ -385,6 +582,39 @@ class SymlinkUtils {
385
582
  UI.warning(`[${adapter.label}] Missing: ${skillsRel}/ (the framework would merge skills here)`);
386
583
  allValid = false;
387
584
  }
585
+
586
+ // v3.8.0+: agents and commands also use per-item merge with overlay support.
587
+ for (const [kindGetter, kindLabel] of [['agentsDir', 'agents'], ['commandsDir', 'commands']]) {
588
+ if (typeof adapter[kindGetter] !== 'function') continue;
589
+ const rel = adapter[kindGetter]();
590
+ if (!rel) continue;
591
+ const full = path.join(this.cwd, rel);
592
+ let s = null;
593
+ try { s = fs.lstatSync(full); } catch (_) { /* absent */ }
594
+ if (!s) {
595
+ UI.warning(`[${adapter.label}] Missing: ${rel}/ (framework ${kindLabel} would be merged here)`);
596
+ allValid = false;
597
+ } else if (s.isSymbolicLink()) {
598
+ UI.warning(`[${adapter.label}] Legacy bulk symlink at ${rel}. Run \`npx baldart update\` to convert to per-item layout.`);
599
+ allValid = false;
600
+ } else {
601
+ const entries = fs.readdirSync(full);
602
+ const frameworkOwned = entries.filter((n) => {
603
+ const p = path.join(full, n);
604
+ try {
605
+ if (fs.lstatSync(p).isSymbolicLink()) return true;
606
+ const content = fs.readFileSync(p, 'utf8');
607
+ return content.startsWith('<!-- baldart-generated:');
608
+ } catch { return false; }
609
+ });
610
+ if (frameworkOwned.length === 0) {
611
+ UI.warning(`[${adapter.label}] ${rel}/ has no framework-managed ${kindLabel}. Run \`npx baldart update\`.`);
612
+ allValid = false;
613
+ } else {
614
+ UI.success(`[${adapter.label}] Valid: ${rel}/ (${frameworkOwned.length} framework-managed ${kindLabel})`);
615
+ }
616
+ }
617
+ }
388
618
  }
389
619
 
390
620
  // Project configuration (v3.0.0+)
@@ -21,6 +21,12 @@ class ClaudeAdapter {
21
21
  /** Where this tool reads skill bundles from. */
22
22
  skillsDir() { return '.claude/skills'; }
23
23
 
24
+ /** Where this tool reads subagent definitions from (null if unsupported). */
25
+ agentsDir() { return '.claude/agents'; }
26
+
27
+ /** Where this tool reads slash commands from (null if unsupported). */
28
+ commandsDir() { return '.claude/commands'; }
29
+
24
30
  /** Whether this tool consumes subagent definitions (`.claude/agents/`). */
25
31
  supportsSubagents() { return true; }
26
32
 
@@ -33,6 +33,13 @@ class CodexAdapter {
33
33
 
34
34
  skillsDir() { return '.agents/skills'; }
35
35
 
36
+ // Codex has no equivalent of Claude subagents or slash commands (custom
37
+ // prompts are deprecated in favor of skills). Returning null disables the
38
+ // per-item merge for those kinds — adapters that DO support them return
39
+ // the target directory string.
40
+ agentsDir() { return null; }
41
+ commandsDir() { return null; }
42
+
36
43
  supportsSubagents() { return false; }
37
44
  supportsSlashCommands() { return false; }
38
45
  supportsHooks() { return false; }