baldart 4.79.0 → 4.80.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,43 @@ 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
+ ## [4.80.1] - 2026-06-29
9
+
10
+ **Self-heal payload drift on an already-current install + a Codex-correct "agent not found" message.** Shipping v4.80.0 surfaced two gaps in the field, both from the same incident: a consumer's Codex session emitted the AGENTS.md STOP message for `codebase-architect`, yet the framework was already at v4.80.0. Two root causes:
11
+
12
+ 1. **CLI↔framework skew left the consumer stuck even after the framework reached 4.80.0.** The consumer's subtree was pulled to v4.80.0, but the `update` that pulled it ran with an **older global CLI** (4.76.x) whose `codex.agentsDir()` was still `null` — so the new Codex-agent payload was silently skipped, and `.codex/agents/` was never created. Worse, *re-running* `update` after aligning the CLI hit the **`Already up to date!` early-exit** (line ~893), which returned BEFORE the symlink/merge reconciliation block — so the version-aligned install never self-healed the missing payload (recoverable only via `baldart doctor`). Fix: the `isAligned` branch now runs the idempotent, additive per-item merges (skills/agents/commands/workflows/output-styles) **before** declaring "up to date". They stay silent when nothing is missing and emit a one-line "Reconciled N newly-shipped item(s)" when they heal real drift — so a CLI-skew victim self-heals on the next `update`, not only via `doctor`. This generalises: ANY future payload-kind addition (a new `MERGE_KINDS` entry) is now reconciled on version-aligned installs too.
13
+ 2. **The "agent not found" STOP message told a Codex user to "riavvia Claude Code".** The v4.80.0 AGENTS.md edit gave the Codex branch the Claude verbatim message. Fix: a **Codex-specific** message — *"Agent `<name>` non trovato tra i custom agent di Codex (`.codex/agents/<name>.toml`) — allinea la CLI (`npm i -g baldart@latest`), esegui `npx baldart update` (o `npx baldart doctor`), assicurati che il progetto sia trusted in Codex, poi riavvia la sessione Codex"* — encoding the exact remediation (CLI align → update → trust → restart; Codex loads `.codex/agents/` only in **trusted** projects, and discovers agents at session start).
14
+
15
+ **PATCH** — script bugfix (update self-heals payload drift) + AGENTS.md message correction; no behaviour change on the happy path, no new config key. Existing v4.80.0 consumers should `npm i -g baldart@latest` (kill the skew at the source) then `npx baldart update`; the Codex session must be restarted to discover the newly-installed agents.
16
+
17
+ ### Changed
18
+
19
+ - **`src/commands/update.js`** — the `Already up to date!` (`status.isAligned`) branch now runs the per-item merges before the early exit (self-heal of payload drift from a prior CLI-skew update), reports the healed count, and adds `reconciled_items` to the `already-current` JSON event.
20
+ - **`framework/AGENTS.md`** — the no-silent-substitution rule's Codex branch now carries its own verbatim "agent not found" remediation message (was reusing the Claude "riavvia Claude Code" text).
21
+
22
+ ## [4.80.0] - 2026-06-29
23
+
24
+ **Codex-native subagents — the 32 BALDART agents are now installed for Codex, not just Claude.** A portability bug surfaced in the field: a skill running on Codex delegated to `codebase-architect` (*"invoke the codebase-architect agent"*), but the agent did not exist in the Codex tree and there was no obvious mechanism to spawn it, so the model **improvised** a generic explorer sub-agent. Root cause (confirmed against the OpenAI Codex docs, June 2026): the framework treated subagents as Claude-only — the Codex tool adapter declared `supportsSubagents() === false` / `agentsDir() === null`, so the agent definitions under `framework/.claude/agents/` were **never installed into a Codex tree**. But **Codex now has custom agents** (subagents) — they were simply in a different shape than Claude's, and BALDART had not yet mapped them. This release closes that gap.
25
+
26
+ Because a Codex custom agent is a **different on-disk artifact** than a Claude subagent — a TOML file at `.codex/agents/<name>.toml` with `name` / `description` / `developer_instructions` (+ optional `model_reasoning_effort`), spawned **by name** — the skill-style "one source, two symlinks" model does not apply. Instead the same `framework/.claude/agents/<name>.md` stays the **single SSOT** and BALDART **transpiles** it to `.toml` on install/update (new `src/utils/codex-agent-transpiler.js`): frontmatter `name`/`description` → same, body → `developer_instructions`, `effort` (low|medium|high|xhigh|max) → `model_reasoning_effort` (xhigh/max clamp to high), `model` **omitted on purpose** (opus/sonnet/haiku have no 1:1 Codex model → inherit the parent session). The generated `.toml` is a real file (you cannot symlink `.md` → `.toml`) carrying a `# baldart-generated:` marker, so an `update` regenerates it idempotently and a user fork (no marker) is never clobbered. Overlays apply **first** (the markdown `## [OVERRIDE]/[APPEND]/[PREPEND]` overlay is merged, then transpiled) — Codex agents honour `.baldart/overlays/agents/<name>.md` exactly like Claude. The transpile rides on the **existing** `MERGE_KINDS` machinery via one new adapter hook, `transpilerFor(kind)` (`codex` returns a transpiler for `agent`; `claude` returns null), so nothing else in the merge path changed and Claude is byte-for-byte unaffected. Existing Codex consumers pick it up on the next `npx baldart update`. AGENTS.md's "MUST invoke codebase-architect" + no-silent-fallback rules are now tool-aware (the agent exists on both tools; spawn by name on Codex). Verified end-to-end: all 32 real agents transpile to valid TOML (parsed with `@iarna/toml`), idempotency / source-change-regen / user-fork-protection / overlay-aware-transpile all pass.
27
+
28
+ **Standing convention** (the user's explicit ask: *every agent from now on is natively Codex-compatible*): any new agent is auto-portable — drop a `.md` under `framework/.claude/agents/`, keep `name`+`description` frontmatter, put behaviour in the body (→ `developer_instructions`), and don't rely on an agent spawning a further agent (Codex `agents.max_depth: 1`, consistent with BALDART's `## Role boundary`). Skills were already Codex-native (identical format). Authoritative design: [`framework/docs/CODEX-AGENTS.md`](framework/docs/CODEX-AGENTS.md).
29
+
30
+ **MINOR** — additive capability (Codex consumers gain 32 agents; existing Claude installs unchanged). **No new `baldart.config.yml` key** — rides on `tools.enabled` containing `codex`, so the schema-change propagation rule does NOT apply. Slash commands / dynamic workflows / output styles stay Claude-only (`commandsDir`/`workflowsDir`/`outputStylesDir` remain null for Codex — no Codex equivalent).
31
+
32
+ ### Added
33
+
34
+ - **`src/utils/codex-agent-transpiler.js`** — pure `.md` → `.toml` transpiler (`mdAgentToToml`, `isGeneratedToml`, `mapEffort`). Emits `developer_instructions` as a TOML multi-line **literal** (`'''…'''`) so arbitrary markdown survives byte-for-byte, with an escaped `"""…"""` fallback only when the body itself contains `'''`.
35
+ - **`framework/docs/CODEX-AGENTS.md`** — design + field-mapping table + the per-agent authoring contract.
36
+
37
+ ### Changed
38
+
39
+ - **`src/utils/tool-adapters/codex.js`** — `agentsDir()` → `.codex/agents` (was null), `supportsSubagents()` → true (was false), new `transpilerFor(kind)` hook returning the TOML transpiler for `agent`. Header comment corrected (Codex DOES have custom agents now).
40
+ - **`src/utils/tool-adapters/claude.js`** — added `transpilerFor()` → null (explicit no-op: Claude consumes every payload in its native format).
41
+ - **`src/utils/symlinks.js`** — `_mergeBulkDir` routes a kind through the new `_installTranspiledFile` (generate tool-native file, overlay-merged first when present) when the adapter declares a transpiler; `verifySymlinks` recognises transpiled `.toml` (via `isGeneratedToml`) as framework-managed alongside symlinks and overlay-generated `.md`.
42
+ - **`framework/AGENTS.md`** — the "MUST invoke `codebase-architect`" and no-silent-substitution rules are now tool-aware: the agent exists on both tools (Claude → Task/`subagent_type`; Codex → spawn by name at `.codex/agents/codebase-architect.toml`); the #56869 specifics stay Claude-scoped, the principle stays universal.
43
+ - **`framework/.claude/agents/REGISTRY.md`** + **`CLAUDE.md`** — document the Codex-native subagent convention + the authoring contract.
44
+
8
45
  ## [4.79.0] - 2026-06-29
9
46
 
10
47
  **New skill `/ds-handoff` — the field-level, 1:1 Claude Design handoff brief.** When `/prd` reached the "hand off to Claude Design" branch, the prompt it produced described each screen only coarsely — *purpose / states / actions / data shown* — and never the exact fields. With no field-level contract, Claude Design **imagines** control types, required-ness, validation, defaults and enum values to cover the gap, so the result is never 1:1. The root cause: BALDART had **no mandatory phase** that enumerated a screen's real fields, and **no coverage gate** verifying them before the prompt was emitted (field discovery was manual discovery-questions). `/ds-handoff` fixes this end-to-end: it (1) identifies each screen's backing data entities, (2) **grounds the field inventory in the project's real schemas** (Zod / TS types / form schemas) by composing existing retrieval (code-graph → LSP → `codebase-architect` → Grep — **no new AST/Zod parser**, an over-scoped fragility the design refused), (3) reconciles fields↔screens (which fields show / are editable / group into which card), (4) runs a **coverage gate** that **BLOCKS** emission in interactive mode when a backed screen's `{control, required}` are unverified — degrading gracefully to user-confirmation when no formal schema exists (portable / Codex), (5) renders the **mandatory field-level template** (a 9-column field-inventory table per screen + actions / states / responsive). It becomes the **single SSOT** for the Claude Design prompt: `/prd` Step 3.0 Branch B now **delegates** to it (graceful inline-coarse fallback when the skill is unavailable), and the old inline `/prd` template is **retired**. Usable standalone via `/ds-handoff <screens-or-feature>` (e.g. an ad-hoc orchestrator + `ui-expert` run). A THIN orchestrator — it **cites** `design-system-protocol.md` (Functional Traceability Gate `backed/decorative/orphan` vocabulary + Reference Tables numbers) and **reads** `discovery-phase.md`'s `mockup_analysis` as input, never duplicating their rules (no dual-SSOT).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.79.0
1
+ 4.80.1
@@ -2,7 +2,9 @@
2
2
 
3
3
  Quick-reference for all custom agents. Use this to route tasks to the right specialist.
4
4
 
5
- **Location**: `.claude/agents/<name>.md` | **Invoke via**: `Task` tool with `subagent_type="<name>"`
5
+ **Location**: `.claude/agents/<name>.md` | **Invoke via**: `Task` tool with `subagent_type="<name>"` (Claude) — or **by name** as a Codex custom agent (Codex)
6
+
7
+ > **Codex-native (since v4.80.0)**: every agent here is portable to Codex. The same `<name>.md` is **transpiled** to `.codex/agents/<name>.toml` on install/update (`name`/`description` → same, body → `developer_instructions`, `effort` → `model_reasoning_effort`, `model` omitted). Authoring stays single-source: drop a `.md`, it auto-installs for both tools. Keep `name`+`description` frontmatter, put behaviour in the body, and don't rely on an agent spawning a further agent (Codex `agents.max_depth: 1`). Design: [`framework/docs/CODEX-AGENTS.md`](../../docs/CODEX-AGENTS.md).
6
8
 
7
9
  > **Machine-readable source of truth (since v3.20.0)**: the filesystem directory `framework/.claude/agents/` is authoritative for the list of BALDART agents. The `agent-discovery-gate.js` PreToolUse hook, the `agent-discovery-info.sh` SessionStart hook, and `baldart doctor` enumerate the directory directly (every `<name>.md` except this file is an agent — no manifest JSON is shipped). Adding a new agent means dropping `<name>.md` here; no other touchpoint.
8
10
 
@@ -103,8 +103,8 @@ Conflict steps (must follow in order):
103
103
  ### 4.a — Universal MUST (apply to every project)
104
104
 
105
105
  - MUST treat `AGENTS.md` as authoritative for agent rules.
106
- - MUST invoke `codebase-architect` agent (via Task tool) whenever you need to understand codebase structure, existing patterns, or code architecture before planning or implementing changes; do not proceed with planning or implementation without first understanding the existing system through codebase-architect.
107
- - MUST NOT silently fall back when an `Agent` (a.k.a. `Task`) tool call returns empty (`0 tool uses · Done`), times out, or fails with `permissionDecision: deny`. This pattern indicates a Claude Code sub-agent failure (see anthropics/claude-code#56869) or a discovery miss / blocked agent (see #20931 and BALDART's own `agent-discovery-gate` PreToolUse hook), NOT a signal to substitute another agent. STOP, report to the user verbatim: "Agent `<name>` non discoverato o fallito in questa sessione — riavvia Claude Code e verifica con `npx baldart doctor`", and wait for direction. Never auto-route to `feature-dev:*`, `general-purpose`, or any other marketplace/built-in agent as a replacement. **This rule is the ONLY defense against bug #56869 (which BALDART cannot patch from outside Claude Code); deviating from it reintroduces the exact failure mode the rule exists to prevent.**
106
+ - MUST invoke the `codebase-architect` agent whenever you need to understand codebase structure, existing patterns, or code architecture before planning or implementing changes; do not proceed with planning or implementation without first understanding the existing system through codebase-architect. **The agent exists on both tools** — Claude Code spawns it via the Task/Agent tool with `subagent_type: "codebase-architect"`; Codex spawns the `codebase-architect` custom agent **by name** (BALDART installs it at `.codex/agents/codebase-architect.toml`, transpiled from the same `.md` source). Only if your environment genuinely exposes NEITHER subagent mechanism (a stripped tool host) do you degrade — explicitly — to inline structural retrieval (code-graph → LSP → Grep → direct file reads per `agents/code-search-protocol.md`), and SAY SO; never silently skip the understanding step.
107
+ - MUST NOT silently substitute a *different* agent when an agent invocation returns empty (`0 tool uses · Done`), times out, or fails with `permissionDecision: deny`. On **Claude Code** this pattern indicates a sub-agent failure (see anthropics/claude-code#56869) or a discovery miss / blocked agent (see #20931 and BALDART's own `agent-discovery-gate` PreToolUse hook): STOP, report to the user verbatim: "Agent `<name>` non discoverato o fallito in questa sessione — riavvia Claude Code e verifica con `npx baldart doctor`", and wait for direction. Never auto-route to `feature-dev:*`, `general-purpose`, or any other marketplace/built-in agent as a replacement. **This rule is the ONLY defense against bug #56869 (which BALDART cannot patch from outside Claude Code); deviating from it reintroduces the exact failure mode the rule exists to prevent.** On **Codex** the equivalent is spawning the custom agent by name; the same no-silent-substitution rule holds — but the remediation differs, so STOP and report the **Codex-specific** message verbatim: "Agent `<name>` non trovato tra i custom agent di Codex (`.codex/agents/<name>.toml`) — allinea la CLI (`npm i -g baldart@latest`), esegui `npx baldart update` (o `npx baldart doctor`), assicurati che il progetto sia *trusted* in Codex, poi riavvia la sessione Codex", and wait for direction. (The #56869 specifics are Claude-only; the no-silent-substitution principle is universal.)
108
108
  - MUST NOT work on files/components already claimed by another agent; multiple agents allowed if working on independent areas.
109
109
  - MUST perform a mandatory clarity analysis before fixing any issue labeled `bug`; confirm that the issue description, proposed correct behavior, and edge cases are unambiguous, document any resolved doubts, and only start fix work once every zone of uncertainty is covered.
110
110
  - MUST mark missing info as UNKNOWN and ask the user; if blocked, set `BLOCKED` with a blocker.
@@ -0,0 +1,111 @@
1
+ # Codex-native subagents
2
+
3
+ > **Status:** shipped in v4.80.0. Claude + Codex. No new `baldart.config.yml` key —
4
+ > rides on `tools.enabled` containing `codex`.
5
+
6
+ ## The problem this fixes
7
+
8
+ BALDART's skills delegate code-understanding, review, and planning to named
9
+ **subagents** (`codebase-architect`, `code-reviewer`, `plan-auditor`, …). For a
10
+ long time those agents were treated as a Claude-only construct: the Codex tool
11
+ adapter declared `supportsSubagents() === false` and `agentsDir() === null`, so
12
+ the 32 agent definitions under `framework/.claude/agents/` were **never installed
13
+ into a Codex tree**.
14
+
15
+ The result, on Codex: a skill said *"invoke the `codebase-architect` agent"*, the
16
+ agent did not exist, there was no obvious mechanism to spawn it, and the model
17
+ **improvised** — e.g. falling back to a generic "explorer" sub-agent. Not a
18
+ comprehension failure; a missing portability contract.
19
+
20
+ Meanwhile Codex shipped **custom agents** (subagents). So the fix is not "degrade
21
+ on Codex" — it is "install the agents on Codex, in Codex's own format".
22
+
23
+ ## Why a transpiler, not a symlink
24
+
25
+ Skills are portable by **symlink** because the on-disk format is identical for
26
+ both tools (`SKILL.md` + frontmatter). One source, two symlinks, zero
27
+ duplication.
28
+
29
+ A subagent is **not** identical across tools:
30
+
31
+ | | Claude Code | Codex |
32
+ |---|---|---|
33
+ | Location | `.claude/agents/<name>.md` | `.codex/agents/<name>.toml` |
34
+ | Format | Markdown + YAML frontmatter | TOML |
35
+ | Required fields | `name`, `description` + body | `name`, `description`, `developer_instructions` |
36
+ | Effort knob | `effort: low\|…\|max` | `model_reasoning_effort: low\|medium\|high` |
37
+ | Invocation | Task/Agent tool, `subagent_type` | spawned **by `name`** |
38
+ | Nesting | (orchestrator spawns) | `agents.max_depth: 1` (no deeper nesting) |
39
+
40
+ Because the format differs, the same `framework/.claude/agents/<name>.md` stays
41
+ the **single source of truth** and BALDART **generates** the `.toml` from it on
42
+ install/update. The generated file is a real file (you cannot symlink a `.md`
43
+ source onto a `.toml` target), carrying a `# baldart-generated:` marker so:
44
+
45
+ - a later `update` regenerates it safely (idempotent — unchanged source → no rewrite),
46
+ - a user fork (a `.toml` without the marker) is **never clobbered** (recorded as a conflict).
47
+
48
+ ## Field mapping (`.md` → `.toml`)
49
+
50
+ Implemented in [`src/utils/codex-agent-transpiler.js`](../../src/utils/codex-agent-transpiler.js):
51
+
52
+ | Claude frontmatter / body | Codex TOML | Notes |
53
+ |---|---|---|
54
+ | `name` | `name` | spawn identity; source of truth |
55
+ | `description` | `description` | collapsed to a single line |
56
+ | markdown body | `developer_instructions` | the agent's system prompt; emitted as a TOML multi-line **literal** (`'''…'''`) so backslashes/quotes survive byte-for-byte (falls back to escaped `"""…"""` only if the body itself contains `'''`) |
57
+ | `effort: low\|medium\|high\|xhigh\|max` | `model_reasoning_effort: low\|medium\|high` | `xhigh`/`max` clamp to `high` |
58
+ | `model: opus\|sonnet\|haiku` | *(omitted)* | no 1:1 Codex model → inherit the parent session model (the safe default) |
59
+
60
+ Overlays apply **first**: when `.baldart/overlays/agents/<name>.md` exists the
61
+ markdown overlay is merged (the normal `## [OVERRIDE]/[APPEND]/[PREPEND]`
62
+ machinery), then the merged markdown is transpiled — so Codex agents honour
63
+ overlays exactly like Claude agents. The overlay's HTML-comment marker is
64
+ stripped before it can leak into `developer_instructions`.
65
+
66
+ ## How it is wired
67
+
68
+ The transpile rides on the **existing** per-item merge machinery
69
+ (`SymlinkUtils._mergeBulkDir`) through one new adapter hook:
70
+
71
+ ```js
72
+ // codex.js
73
+ agentsDir() { return '.codex/agents'; } // was null
74
+ supportsSubagents() { return true; } // was false
75
+ transpilerFor(kind) { // NEW
76
+ if (kind !== 'agent') return null;
77
+ const t = require('../codex-agent-transpiler');
78
+ return { outExt: '.toml', transform: t.mdAgentToToml, isGenerated: t.isGeneratedToml };
79
+ }
80
+ ```
81
+
82
+ When `_mergeBulkDir` sees a non-null `transpilerFor(kind)` it routes that kind
83
+ through `_installTranspiledFile` (generate `.toml`) instead of the symlink /
84
+ overlay path. `claude.transpilerFor()` returns null, so Claude is unchanged
85
+ (agents still symlinked `.md`). `commandsDir`/`workflowsDir`/`outputStylesDir`
86
+ stay null for Codex — slash commands, dynamic workflows, and output styles have
87
+ no Codex equivalent.
88
+
89
+ Existing Codex consumers pick this up automatically on the next
90
+ `npx baldart update` (which calls `mergeAgents`), and `baldart doctor`'s
91
+ `mergeAgents` backfill covers any drift.
92
+
93
+ ## The authoring contract (for every new agent, from now on)
94
+
95
+ Any new agent is **auto-portable** — just drop a `.md` under
96
+ `framework/.claude/agents/`. To keep it clean across both tools:
97
+
98
+ 1. **Keep `name` + `description` in the frontmatter.** `name` is the spawn
99
+ identity on both tools; `description` is how Codex chooses which agent to spawn.
100
+ 2. **Put the agent's behaviour in the body** — it becomes `developer_instructions`.
101
+ 3. **Set `effort:`** if the agent needs a non-default reasoning tier; it maps to
102
+ `model_reasoning_effort`.
103
+ 4. **Do not rely on a subagent spawning a further subagent.** Codex caps
104
+ `agents.max_depth` at 1 — consistent with BALDART's own `## Role boundary`
105
+ ("no Task/Agent tool") convention and the "nested subagents not available"
106
+ reality on Claude. Orchestration belongs to the skill, not the agent.
107
+ 5. **Avoid Claude-only tool references in the body that have no Codex analogue.**
108
+ Prefer the portable retrieval cascade (code-graph → LSP → Grep → reads).
109
+
110
+ Nothing else is required — no per-agent Codex file, no registry entry beyond the
111
+ usual `REGISTRY.md` row.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.79.0",
3
+ "version": "4.80.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"
@@ -891,11 +891,33 @@ async function update(options = {}, unknownArgs = []) {
891
891
  // VERSION-compare authority. Subtree-merge commit count is noise; never
892
892
  // use behind > 0 as the up-to-date signal (root cause of v3.24.x bug).
893
893
  if (status.isAligned) {
894
+ // Even when the VERSION is aligned, the per-item PAYLOAD may be stale.
895
+ // The failure class: a consumer updates with an OLDER CLI (CLI↔framework
896
+ // skew) → gets the new framework content but the old reconciliation logic,
897
+ // so a newly-added payload kind (e.g. Codex agents in v4.80.0, where
898
+ // `codex.agentsDir()` flipped from null to `.codex/agents`) is silently
899
+ // skipped. Later aligning the CLI and re-running `update` hit this early
900
+ // "already current" exit and NEVER reconciled — leaving the consumer
901
+ // stuck (recoverable only via `doctor`). The per-item merges are
902
+ // idempotent + additive and stay SILENT when nothing is missing, so run
903
+ // them here: an up-to-date install now self-heals payload drift too.
904
+ const alignedTools = readEnabledTools();
905
+ const reconciled = [
906
+ symlinks.mergeSkills({ tools: alignedTools }),
907
+ symlinks.mergeAgents({ tools: alignedTools }),
908
+ symlinks.mergeCommands({ tools: alignedTools }),
909
+ symlinks.mergeWorkflows({ tools: alignedTools }),
910
+ symlinks.mergeOutputStyles({ tools: alignedTools })
911
+ ].reduce((n, r) => n + ((r.linked && r.linked.length) || 0) + ((r.generated && r.generated.length) || 0), 0);
912
+ if (reconciled > 0) {
913
+ UI.info(`Reconciled ${reconciled} newly-shipped item(s) into per-tool dir(s) — payload drift from a prior CLI-skew update, now healed.`);
914
+ }
894
915
  UI.success(`Already up to date! (v${status.installedVersion})`);
895
916
  emitUpdateJson({ ok: true, action: 'already-current',
896
917
  installed_before: status.installedVersion,
897
918
  installed_after: status.installedVersion,
898
- remote_version: status.remoteVersion }, 0);
919
+ remote_version: status.remoteVersion,
920
+ reconciled_items: reconciled }, 0);
899
921
  }
900
922
 
901
923
  UI.warning(`Update available: v${status.installedVersion} → v${status.remoteVersion}`);
@@ -0,0 +1,139 @@
1
+ const yaml = require('js-yaml');
2
+
3
+ /**
4
+ * Codex agent transpiler — turns a BALDART Claude subagent definition
5
+ * (`.claude/agents/<name>.md`, YAML frontmatter + markdown body) into a Codex
6
+ * custom-agent definition (`.codex/agents/<name>.toml`).
7
+ *
8
+ * Why a transpiler and not a symlink (the model used for skills): a skill has
9
+ * the SAME on-disk format for Claude and Codex, so one source file is linked
10
+ * into two places. A subagent does NOT — Codex custom agents are TOML files
11
+ * with a different field set, living under a different root (`.codex/agents/`,
12
+ * not `.agents/skills/`). So the SAME source `.md` is the SSOT and we *generate*
13
+ * the `.toml` from it on install/update. Edit the `.md`; never hand-edit the
14
+ * generated `.toml`.
15
+ *
16
+ * Field mapping (Claude frontmatter → Codex TOML):
17
+ * name → name (source of truth for spawn identity)
18
+ * description → description (single-lined)
19
+ * <body> → developer_instructions (the agent's system prompt)
20
+ * effort → model_reasoning_effort (low|medium|high — see mapEffort)
21
+ * model → (OMITTED on purpose) opus/sonnet/haiku have no 1:1 Codex
22
+ * model; inheriting the parent session
23
+ * model is the safe, correct default.
24
+ *
25
+ * Codex docs: developers.openai.com/codex/subagents — required fields are
26
+ * `name`, `description`, `developer_instructions`; optional `model`,
27
+ * `model_reasoning_effort`, `sandbox_mode`, `nickname_candidates`,
28
+ * `mcp_servers`, `skills.config`. Agents are spawned by their `name`.
29
+ */
30
+
31
+ const TOML_MARKER_PREFIX = '# baldart-generated:';
32
+
33
+ /**
34
+ * Map a BALDART effort tier (low|medium|high|xhigh|max — see
35
+ * framework/agents/effort-protocol.md) onto Codex's `model_reasoning_effort`,
36
+ * which only accepts low|medium|high. xhigh/max clamp to high (the ceiling).
37
+ * Returns null when there's nothing to map (omit the key → inherit parent).
38
+ */
39
+ function mapEffort(effort) {
40
+ if (!effort) return null;
41
+ const e = String(effort).trim().toLowerCase();
42
+ if (e === 'low') return 'low';
43
+ if (e === 'medium') return 'medium';
44
+ if (e === 'high' || e === 'xhigh' || e === 'max') return 'high';
45
+ return null;
46
+ }
47
+
48
+ /** Split leading YAML frontmatter from the markdown body. */
49
+ function splitFrontmatter(md) {
50
+ let rest = String(md).replace(/^/, '');
51
+ const m = rest.match(/^---\n([\s\S]*?)\n---\n?/);
52
+ if (m) return { frontmatter: m[1], body: rest.slice(m[0].length) };
53
+ return { frontmatter: '', body: rest };
54
+ }
55
+
56
+ /**
57
+ * Strip a leading `<!-- baldart-generated: … -->` HTML-comment marker. Present
58
+ * only when the source markdown was overlay-merged (the marker sits right after
59
+ * the frontmatter); it must not leak into developer_instructions.
60
+ */
61
+ function stripHtmlMarker(body) {
62
+ return String(body).replace(/^\s*<!--\s*baldart-generated:[\s\S]*?-->\s*\n/, '');
63
+ }
64
+
65
+ /** Emit a TOML basic (single-line) string with the required escapes. */
66
+ function tomlBasicString(s) {
67
+ return '"' + String(s)
68
+ .replace(/\\/g, '\\\\')
69
+ .replace(/"/g, '\\"')
70
+ .replace(/[\r\n\t]+/g, ' ')
71
+ .trim() + '"';
72
+ }
73
+
74
+ /**
75
+ * Emit a TOML multi-line string for an arbitrary markdown body.
76
+ *
77
+ * Prefer a multi-line LITERAL string (`'''…'''`): it processes no escapes, so
78
+ * markdown full of backslashes and double-quotes survives byte-for-byte. The
79
+ * only thing it cannot contain is `'''`, so fall back to a multi-line BASIC
80
+ * string (`"""…"""`) with escaping when the body has a triple-single-quote.
81
+ * A leading newline (trimmed by the TOML spec) keeps the body starting exactly
82
+ * as authored; a trailing newline keeps a body-final quote from abutting the
83
+ * closing delimiter.
84
+ */
85
+ function tomlMultiline(s) {
86
+ const body = String(s).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
87
+ if (!body.includes("'''")) {
88
+ return "'''\n" + body + "\n'''";
89
+ }
90
+ const esc = body.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
91
+ return '"""\n' + esc + '\n"""';
92
+ }
93
+
94
+ /**
95
+ * Transpile one Claude agent `.md` into Codex `.toml` text.
96
+ * @param {string} markdown - raw `.md` content (frontmatter + body)
97
+ * @param {{name?: string, frameworkVersion?: string}} [opts]
98
+ * @returns {string} TOML content (ends with a trailing newline)
99
+ */
100
+ function mdAgentToToml(markdown, opts = {}) {
101
+ const { frontmatter, body } = splitFrontmatter(markdown);
102
+ let meta = {};
103
+ if (frontmatter) {
104
+ try { meta = yaml.load(frontmatter) || {}; }
105
+ catch (_) { meta = {}; }
106
+ }
107
+
108
+ const name = String(meta.name || opts.name || '').trim();
109
+ if (!name) {
110
+ throw new Error('codex-agent-transpiler: agent has no `name` (neither frontmatter nor filename provided one)');
111
+ }
112
+ const description = String(meta.description || '').replace(/\s+/g, ' ').trim();
113
+ const instructions = stripHtmlMarker(body).replace(/^\n+/, '').replace(/\n+$/, '\n');
114
+ const effort = mapEffort(meta.effort);
115
+
116
+ const lines = [];
117
+ lines.push(`${TOML_MARKER_PREFIX} kind=agent name=${name} v=${opts.frameworkVersion || 'unknown'}`);
118
+ lines.push(`# Transpiled from .claude/agents/${name}.md by BALDART — edit the source, not this file.`);
119
+ lines.push(`name = ${tomlBasicString(name)}`);
120
+ if (description) lines.push(`description = ${tomlBasicString(description)}`);
121
+ if (effort) lines.push(`model_reasoning_effort = ${tomlBasicString(effort)}`);
122
+ lines.push(`developer_instructions = ${tomlMultiline(instructions)}`);
123
+
124
+ return lines.join('\n') + '\n';
125
+ }
126
+
127
+ /** True when a TOML file was generated by BALDART (safe to overwrite/regenerate). */
128
+ function isGeneratedToml(content) {
129
+ return typeof content === 'string' && content.includes(TOML_MARKER_PREFIX);
130
+ }
131
+
132
+ module.exports = {
133
+ mdAgentToToml,
134
+ isGeneratedToml,
135
+ mapEffort,
136
+ splitFrontmatter,
137
+ tomlMultiline,
138
+ TOML_MARKER_PREFIX
139
+ };
@@ -425,6 +425,18 @@ class SymlinkUtils {
425
425
  const overlayAbs = path.join(this.cwd, overlayRel);
426
426
  const hasOverlay = cfg.overlay && fs.existsSync(overlayAbs);
427
427
 
428
+ // Transpile path: when the adapter declares a transpiler for this kind
429
+ // (e.g. Codex agents → TOML), do NOT symlink the `.md` source — generate
430
+ // the tool-native file from it (overlay-merged first when present).
431
+ const transpiler = (typeof adapter.transpilerFor === 'function') ? adapter.transpilerFor(kind) : null;
432
+ if (transpiler) {
433
+ this._installTranspiledFile({
434
+ kind, baseName, fwAbsolute, overlayAbs, hasOverlay,
435
+ targetRel, transpiler, adapter, frameworkVersion, aggregate
436
+ });
437
+ continue;
438
+ }
439
+
428
440
  // Inspect what currently lives at the link path (if anything).
429
441
  let lstat = null;
430
442
  try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }
@@ -525,6 +537,77 @@ class SymlinkUtils {
525
537
  });
526
538
  }
527
539
 
540
+ /**
541
+ * Install a transpiled payload for a tool whose on-disk format differs from
542
+ * the framework source (currently: Codex agents, `.md` → `.toml`).
543
+ *
544
+ * The framework `.md` stays the single SSOT; the `.toml` is *generated* (a
545
+ * real file, never a symlink — you can't symlink a `.md` source to a `.toml`
546
+ * target). When an overlay exists it is merged FIRST (reusing the markdown
547
+ * overlay machinery), so Codex agents honour overlays exactly like Claude
548
+ * agents do, then the merged markdown is transpiled. The output carries a
549
+ * `# baldart-generated:` marker so a later update can safely regenerate it
550
+ * and a user fork (no marker) is never clobbered.
551
+ */
552
+ _installTranspiledFile({ kind, baseName, fwAbsolute, overlayAbs, hasOverlay, targetRel, transpiler, adapter, frameworkVersion, aggregate }) {
553
+ const outName = `${baseName}${transpiler.outExt}`;
554
+ const outPath = path.join(this.cwd, targetRel, outName);
555
+ const relOut = path.join(targetRel, outName);
556
+
557
+ // Effective markdown = overlay-merged when an overlay exists, else the base.
558
+ const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
559
+ let effectiveMd = baseContent;
560
+ if (hasOverlay) {
561
+ try {
562
+ const { mergeOverlay } = require('./overlay-merger');
563
+ const overlayContent = fs.readFileSync(overlayAbs, 'utf8');
564
+ const merged = mergeOverlay({ kind, name: baseName, baseContent, overlayContent, frameworkVersion });
565
+ effectiveMd = merged.generated;
566
+ (merged.warnings || []).forEach((w) => {
567
+ UI.warning(`[${adapter.label}] ${kind} ${baseName}: ${w}`);
568
+ aggregate.warnings.push({ tool: adapter.name, kind, name: baseName, warning: w });
569
+ });
570
+ } catch (_) { effectiveMd = baseContent; /* fail-safe: transpile the base */ }
571
+ }
572
+
573
+ let outContent;
574
+ try {
575
+ outContent = transpiler.transform(effectiveMd, { name: baseName, frameworkVersion });
576
+ } catch (err) {
577
+ UI.warning(`[${adapter.label}] ${kind} ${baseName}: transpile failed (${err.message}) — skipped.`);
578
+ aggregate.warnings.push({ tool: adapter.name, kind, name: baseName, warning: `transpile-failed: ${err.message}` });
579
+ return;
580
+ }
581
+
582
+ let lstat = null;
583
+ try { lstat = fs.lstatSync(outPath); } catch (_) { /* absent */ }
584
+ if (lstat) {
585
+ if (lstat.isSymbolicLink()) {
586
+ // Stale symlink from an earlier layout — replace with the generated file.
587
+ fs.unlinkSync(outPath);
588
+ } else {
589
+ const existing = fs.readFileSync(outPath, 'utf8');
590
+ if (!transpiler.isGenerated(existing)) {
591
+ UI.warning(`[${adapter.label}] ${kind} ${baseName}: ${relOut} exists without a baldart-generated marker — NOT overwriting (user fork?).`);
592
+ aggregate.conflicts.push({
593
+ tool: adapter.name, kind, name: baseName,
594
+ local_path: relOut, reason: 'manual-edit-no-marker',
595
+ detected_at: new Date().toISOString()
596
+ });
597
+ return;
598
+ }
599
+ if (existing === outContent) {
600
+ aggregate.skipped.push({ tool: adapter.name, kind, name: baseName, reason: 'already-generated' });
601
+ return;
602
+ }
603
+ }
604
+ }
605
+
606
+ fs.writeFileSync(outPath, outContent);
607
+ UI.success(`[${adapter.label}] ${kind} transpiled: ${relOut}`);
608
+ aggregate.generated.push({ tool: adapter.name, kind, name: baseName });
609
+ }
610
+
528
611
  _generateOverlayedFile({ kind, name, fwAbsolute, overlayAbs, linkPath, lstat, frameworkVersion, aggregate, adapter, targetRel }) {
529
612
  const { mergeOverlay, isGeneratedFile, computeBaseFileSha, ensureBaseFileShaInOverlay, refreshOverlayVersionPin } = require('./overlay-merger');
530
613
  const baseContent = fs.readFileSync(fwAbsolute, 'utf8');
@@ -706,12 +789,16 @@ class SymlinkUtils {
706
789
  allValid = false;
707
790
  } else {
708
791
  const { isGeneratedFile } = require('./overlay-merger');
792
+ const { isGeneratedToml } = require('./codex-agent-transpiler');
709
793
  const entries = fs.readdirSync(full);
710
794
  const frameworkOwned = entries.filter((n) => {
711
795
  const p = path.join(full, n);
712
796
  try {
713
797
  if (fs.lstatSync(p).isSymbolicLink()) return true;
714
- return isGeneratedFile(fs.readFileSync(p, 'utf8'));
798
+ // Symlinked (Claude .md) OR transpiled-generated (Codex .toml) OR
799
+ // overlay-generated (Claude .md with HTML marker).
800
+ const c = fs.readFileSync(p, 'utf8');
801
+ return isGeneratedFile(c) || isGeneratedToml(c);
715
802
  } catch { return false; }
716
803
  });
717
804
  if (frameworkOwned.length === 0) {
@@ -35,6 +35,13 @@ class ClaudeAdapter {
35
35
  /** Where this tool reads output-style definitions from (null if unsupported). */
36
36
  outputStylesDir() { return '.claude/output-styles'; }
37
37
 
38
+ /**
39
+ * Per-kind transpiler hook. Claude consumes every payload in its native
40
+ * on-disk format, so nothing is transpiled — the merge machinery symlinks
41
+ * the source verbatim. (Codex returns a transpiler for `agent`.)
42
+ */
43
+ transpilerFor(/* kind */) { return null; }
44
+
38
45
  /** Whether this tool consumes subagent definitions (`.claude/agents/`). */
39
46
  supportsSubagents() { return true; }
40
47
 
@@ -19,9 +19,16 @@ const os = require('os');
19
19
  * Codex reads from `.agents/skills/<x>`, and both follow the symlink to
20
20
  * the single source under `.framework/framework/.claude/skills/<x>/`.
21
21
  *
22
- * Codex does NOT have an equivalent of Claude subagents (`.claude/agents/`)
23
- * or slash commands (custom prompts are deprecated in favor of skills).
24
- * AGENTS.md at the repo root IS read by Codex — same convention as Claude.
22
+ * Codex DOES have custom agents (since the subagents feature shipped), but in
23
+ * a different shape than Claude: TOML files under `.codex/agents/` (project) or
24
+ * `~/.codex/agents/` (personal), with fields name / description /
25
+ * developer_instructions (+ optional model_reasoning_effort, sandbox_mode, …),
26
+ * spawned by `name`. Because the format differs from Claude's markdown
27
+ * subagents, BALDART does not symlink them — it *transpiles* the same `.md`
28
+ * source into `.toml` via `codex-agent-transpiler` (see `transpilerFor`).
29
+ * Slash commands have no Codex equivalent (custom prompts are deprecated in
30
+ * favor of skills). AGENTS.md at the repo root IS read by Codex — same
31
+ * convention as Claude.
25
32
  */
26
33
  class CodexAdapter {
27
34
  constructor(cwd = process.cwd()) {
@@ -33,13 +40,27 @@ class CodexAdapter {
33
40
 
34
41
  skillsDir() { return '.agents/skills'; }
35
42
 
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; }
43
+ // Codex custom agents live under `.codex/agents/` as TOML files. They are
44
+ // NOT symlinked from the `.md` source (the format differs) the merge
45
+ // machinery routes this kind through `transpilerFor('agent')` to *generate*
46
+ // a `.toml` from each source `.md`. Slash commands have no Codex equivalent
47
+ // (custom prompts are deprecated in favor of skills) null disables that merge.
48
+ agentsDir() { return '.codex/agents'; }
41
49
  commandsDir() { return null; }
42
50
 
51
+ /**
52
+ * Per-kind transpiler. The merge machinery (`SymlinkUtils._mergeBulkDir`)
53
+ * calls this for every merge kind; a non-null return means "do NOT symlink
54
+ * the `.md` source — generate `outExt` content from it via `transform`".
55
+ * Only `agent` is transpiled (markdown subagent → Codex TOML); every other
56
+ * kind returns null (and Codex already disables them via a null dirGetter).
57
+ */
58
+ transpilerFor(kind) {
59
+ if (kind !== 'agent') return null;
60
+ const t = require('../codex-agent-transpiler');
61
+ return { outExt: '.toml', transform: t.mdAgentToToml, isGenerated: t.isGeneratedToml };
62
+ }
63
+
43
64
  // Codex has no equivalent of Claude Code dynamic workflows (the JS
44
65
  // orchestration runtime is Claude-only). Returning null disables the
45
66
  // per-item workflow merge for Codex — the same source workflow scripts are
@@ -51,7 +72,7 @@ class CodexAdapter {
51
72
  // Codex — the source style files are simply never linked into a Codex tree.
52
73
  outputStylesDir() { return null; }
53
74
 
54
- supportsSubagents() { return false; }
75
+ supportsSubagents() { return true; }
55
76
  supportsSlashCommands() { return false; }
56
77
  supportsHooks() { return false; }
57
78