baldart 4.78.2 → 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 +42 -0
- package/README.md +2 -2
- package/VERSION +1 -1
- package/framework/.claude/agents/REGISTRY.md +3 -1
- package/framework/.claude/skills/ds-handoff/SKILL.md +183 -0
- package/framework/.claude/skills/ds-handoff/assets/claude-design-handoff-prompt.template.md +197 -0
- package/framework/.claude/skills/ds-handoff/references/coverage-gate.md +109 -0
- package/framework/.claude/skills/ds-handoff/references/field-extraction.md +78 -0
- package/framework/.claude/skills/prd/references/ui-design-phase.md +33 -18
- package/framework/AGENTS.md +2 -2
- package/framework/agents/skills-mapping.md +409 -7
- 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/framework/.claude/skills/prd/assets/claude-design-handoff-prompt.template.md +0 -137
|
@@ -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
|
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Claude Design Handoff Prompt — Template
|
|
2
|
-
|
|
3
|
-
> Template populated at runtime by **Step 3.0 (Mockup Source Decision)** in
|
|
4
|
-
> [../references/ui-design-phase.md](../references/ui-design-phase.md) when the
|
|
5
|
-
> user picks the **handoff** branch. All `{{placeholders}}` are resolved from the
|
|
6
|
-
> PRD state file (`${paths.prd_dir}/sessions/YYYY-MM-DD-<slug>-state.md`),
|
|
7
|
-
> `baldart.config.yml`, and optional overlays.
|
|
8
|
-
>
|
|
9
|
-
> Render rules:
|
|
10
|
-
> - Conditional sections (marked `{{#if …}} … {{/if}}`) are emitted only when the
|
|
11
|
-
> condition is true. Skip the entire section (including its heading) when false.
|
|
12
|
-
> - Lists (marked `{{#each …}}`) render one bullet per item; emit "—" if empty.
|
|
13
|
-
> - The rendered output is presented to the user inside a fenced markdown
|
|
14
|
-
> code-block so they can copy-paste verbatim into Claude Design.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
# Feature: {{feature_title}}
|
|
19
|
-
|
|
20
|
-
## 1. Obiettivo
|
|
21
|
-
|
|
22
|
-
{{feature_objective_one_sentence}}
|
|
23
|
-
|
|
24
|
-
## 2. User flow
|
|
25
|
-
|
|
26
|
-
{{user_flow_summary}}
|
|
27
|
-
|
|
28
|
-
{{#if primary_personas}}
|
|
29
|
-
**Personas target**:
|
|
30
|
-
{{#each primary_personas}}
|
|
31
|
-
- {{this.name}} — {{this.context}}
|
|
32
|
-
{{/each}}
|
|
33
|
-
{{/if}}
|
|
34
|
-
|
|
35
|
-
## 3. Schermate in scope
|
|
36
|
-
|
|
37
|
-
> **Vincolo di tracciabilità funzionale.** Inserisci SOLO controlli (bottoni,
|
|
38
|
-
> icone-azione, voci di menu, tab, toggle) legati a una funzione elencata qui
|
|
39
|
-
> sotto. Non aggiungere chrome interattivo "per completezza visiva": ogni
|
|
40
|
-
> affordance senza funzione diventa codice morto in implementazione. Se proponi
|
|
41
|
-
> un elemento interattivo non presente nelle azioni primarie, segnalalo in
|
|
42
|
-
> consegna come *proposta da validare*, non come requisito. Gli elementi
|
|
43
|
-
> puramente decorativi (icone di sezione, marchi) marcali esplicitamente come
|
|
44
|
-
> **decorativi**.
|
|
45
|
-
|
|
46
|
-
{{#each screens_in_scope}}
|
|
47
|
-
### {{this.name}}
|
|
48
|
-
|
|
49
|
-
- **Scopo**: {{this.purpose}}
|
|
50
|
-
- **Stati richiesti**: {{this.states_required}} <!-- empty, loading, populated, error, success, ecc. -->
|
|
51
|
-
- **Azioni primarie**: {{this.primary_actions}}
|
|
52
|
-
- **Dati visualizzati**: {{this.data_shown}}
|
|
53
|
-
{{#if this.notes}}
|
|
54
|
-
- **Note**: {{this.notes}}
|
|
55
|
-
{{/if}}
|
|
56
|
-
|
|
57
|
-
{{/each}}
|
|
58
|
-
> **Nota per l'export.** Dai al canvas di questa feature un titolo che contenga il
|
|
59
|
-
> nome feature («{{feature_title}}»), e numera gli artboard in modo coerente con gli
|
|
60
|
-
> ID schermata qui sopra (es. schermata 3.1 → artboard `s31`). Scaricherò il bundle
|
|
61
|
-
> completo del workspace: questo mi permette di ritrovare esattamente le TUE schermate
|
|
62
|
-
> ed evitare ambiguità con le altre feature.
|
|
63
|
-
|
|
64
|
-
{{#if brand_voice}}
|
|
65
|
-
## 4. Brand & voice
|
|
66
|
-
|
|
67
|
-
- **Tono**: {{brand_voice.tone}}
|
|
68
|
-
- **Personalità**: {{brand_voice.personality}}
|
|
69
|
-
- **Do**: {{brand_voice.do}}
|
|
70
|
-
- **Don't**: {{brand_voice.dont}}
|
|
71
|
-
{{#if brand_voice.references}}
|
|
72
|
-
- **Riferimenti visivi**: {{brand_voice.references}}
|
|
73
|
-
{{/if}}
|
|
74
|
-
{{/if}}
|
|
75
|
-
|
|
76
|
-
{{#if has_design_system}}
|
|
77
|
-
## 5. Design system constraints
|
|
78
|
-
|
|
79
|
-
> Riusa SOLO i token e le primitive elencate. Se devi proporre qualcosa di nuovo,
|
|
80
|
-
> segnalalo esplicitamente nella consegna così posso aggiornare il registro.
|
|
81
|
-
|
|
82
|
-
### Token chiave
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
{{design_tokens_excerpt}}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Primitive da riusare
|
|
89
|
-
|
|
90
|
-
{{#each registered_primitives}}
|
|
91
|
-
- **{{this.name}}** — {{this.purpose}} ({{this.path}})
|
|
92
|
-
{{/each}}
|
|
93
|
-
|
|
94
|
-
### Vincoli numerici (da `design-system-protocol.md`)
|
|
95
|
-
|
|
96
|
-
- Type scale: {{tokens.type_scale}}
|
|
97
|
-
- Spacing scale: {{tokens.spacing_scale}}
|
|
98
|
-
- Radius: {{tokens.radius_scale}}
|
|
99
|
-
- Contrast minimo: {{tokens.contrast_target}} (WCAG AA + APCA Lc ≥ {{tokens.apca_target}})
|
|
100
|
-
- Motion durations: {{tokens.motion_durations}}
|
|
101
|
-
{{/if}}
|
|
102
|
-
|
|
103
|
-
## 6. Stack target
|
|
104
|
-
|
|
105
|
-
- **Framework**: {{stack.framework}}
|
|
106
|
-
- **Output preferito**: {{output_format_preference}} <!-- "HTML/CSS standalone" | "React component (Tailwind)" | "Figma frame" -->
|
|
107
|
-
{{#if stack.charting}}
|
|
108
|
-
- **Charting library**: {{stack.charting}}
|
|
109
|
-
{{/if}}
|
|
110
|
-
{{#if stack.animation}}
|
|
111
|
-
- **Animation library**: {{stack.animation}}
|
|
112
|
-
{{/if}}
|
|
113
|
-
|
|
114
|
-
## 7. Cosa restituirmi
|
|
115
|
-
|
|
116
|
-
Per ogni schermata elencata in § 3:
|
|
117
|
-
|
|
118
|
-
1. **PNG export** ad alta risoluzione (≥ 2x) di OGNI stato richiesto (es. `lista-ordini__empty.png`, `lista-ordini__populated.png`).
|
|
119
|
-
2. {{output_format_preference_block}} <!-- "Codice HTML/CSS in un file unico" | "Codice React (.tsx) con import Tailwind" | "Link Figma alla frame" -->
|
|
120
|
-
3. **Mappa di tracciabilità** — per OGNI elemento interattivo o iconografico
|
|
121
|
-
della schermata, una riga: `elemento → funzione che lo motiva` (l'azione
|
|
122
|
-
primaria / il dato di § 3 che serve), oppure marcalo **decorativo** se non ha
|
|
123
|
-
comportamento. Qualunque elemento che non riesci a tracciare a una funzione,
|
|
124
|
-
elencalo separatamente come *orphan da decidere* — così posso scegliere se
|
|
125
|
-
dargli una funzione, renderlo decorativo o rimuoverlo, invece di
|
|
126
|
-
implementarlo a vuoto.
|
|
127
|
-
4. **Note inline** su:
|
|
128
|
-
- Componenti nuovi (non presenti nel registry sopra) che hai introdotto e perché.
|
|
129
|
-
- Token deviation: qualunque colore/spacing/radius fuori dalla palette sopra, con motivazione.
|
|
130
|
-
- Decisioni di layout non ovvie (es. priority sort, hierarchy visiva, density).
|
|
131
|
-
|
|
132
|
-
Quando hai pronto, dimmi i **path locali** dei file (PNG/HTML/TSX) — li importerò in questa sessione PRD per analisi e validazione contro il design system.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
<!-- baldart-handoff: do-not-edit-manually -->
|
|
137
|
-
<!-- Generated from claude-design-handoff-prompt.template.md by /prd Step 3.0 -->
|