baldart 4.79.0 → 4.80.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 +23 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/REGISTRY.md +3 -1
- package/framework/AGENTS.md +2 -2
- package/framework/docs/CODEX-AGENTS.md +111 -0
- package/package.json +1 -1
- package/src/utils/codex-agent-transpiler.js +139 -0
- package/src/utils/symlinks.js +88 -1
- package/src/utils/tool-adapters/claude.js +7 -0
- package/src/utils/tool-adapters/codex.js +30 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.0] - 2026-06-29
|
|
9
|
+
|
|
10
|
+
**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.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
**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).
|
|
15
|
+
|
|
16
|
+
**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).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`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 `'''`.
|
|
21
|
+
- **`framework/docs/CODEX-AGENTS.md`** — design + field-mapping table + the per-agent authoring contract.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`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).
|
|
26
|
+
- **`src/utils/tool-adapters/claude.js`** — added `transpilerFor()` → null (explicit no-op: Claude consumes every payload in its native format).
|
|
27
|
+
- **`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`.
|
|
28
|
+
- **`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.
|
|
29
|
+
- **`framework/.claude/agents/REGISTRY.md`** + **`CLAUDE.md`** — document the Codex-native subagent convention + the authoring contract.
|
|
30
|
+
|
|
8
31
|
## [4.79.0] - 2026-06-29
|
|
9
32
|
|
|
10
33
|
**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.
|
|
1
|
+
4.80.0
|
|
@@ -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
|
|
package/framework/AGENTS.md
CHANGED
|
@@ -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
|
|
107
|
-
- MUST NOT silently
|
|
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 named-agent spawn is the equivalent; the same no-silent-substitution rule holds (the #56869 specifics are Claude-only, the 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
|
@@ -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
|
+
};
|
package/src/utils/symlinks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
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
|
|
75
|
+
supportsSubagents() { return true; }
|
|
55
76
|
supportsSlashCommands() { return false; }
|
|
56
77
|
supportsHooks() { return false; }
|
|
57
78
|
|