baldart 4.87.0 → 4.88.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,42 @@ 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.88.1] - 2026-07-01
9
+
10
+ **`baldart update` backfills hooks on the aligned/self-heal path too.** The "already up to date" branch of `update` reconciled newly-shipped payload (agents / skills / workflows / output-styles + root primitives) but did **not** backfill hooks. Same gap class the payload reconcile already fixed: a consumer who framework-updated with an **older CLI** (before a given hook shipped — e.g. the v4.88.0 Codex hooks) and then aligned the CLI would hit the "already current" early-exit and **never get the hooks registered** (recoverable only via `doctor`). Now the aligned path registers both Claude (`.claude/settings.json`) and — when `tools.enabled ∋ codex` — Codex (`.codex/hooks.json`) hooks, **idempotently and additively**: SILENT when nothing is missing, drift left to `doctor` (never prompts on the self-heal path), non-fatal on any hiccup. The `--json` payload gains `backfilled_hooks`. **PATCH** — robustness fix, no behavior change on a healthy install. Codex parity: adapter-generated (the same registrar). No new config key.
11
+
12
+ ### Changed
13
+
14
+ - **`src/commands/update.js`** — the aligned "already up to date" branch now backfills missing Claude + Codex hooks (silent when none missing) and reports `backfilled_hooks` in `--json`.
15
+
16
+ ## [4.88.0] - 2026-07-01
17
+
18
+ **Native Codex hooks — BALDART now manages `.codex/hooks.json` in parallel with `.claude/settings.json` (Codex-parity foundation, S1–S3).** Kicks off a phased program to make BALDART **universally tool-agnostic**: everything it ships must work natively on both Claude Code and Codex. Two governance rules are now binding on all future development (CLAUDE.md § "Cross-tool parity discipline"): (1) every NEW tool/surface is first checked for native Codex compatibility (portable / adapter-generated / inline-fallback / N/A, outcome recorded in the CHANGELOG); (2) every change travels in parallel across Codex and Claude in the same change. This release delivers the **hook foundation**:
19
+
20
+ - **S1 — hook registry split (no behavior change).** `src/utils/hooks.js` is split into a runtime-neutral registry (`hooks/registry.js`, one entry per logical hook with per-runtime bindings) + a Claude renderer (`hooks/claude.js`, `.claude/settings.json`); `hooks.js` becomes a thin compatibility shim. Verified **byte-identical** to the pre-split module across 7 fixtures (getStatus / verifyAll / registerAll keep+replace / unregisterAll + the written settings.json).
21
+ - **S2 — Codex hook registrar.** New `hooks/codex.js` renderer manages `.codex/hooks.json` (same schema as Claude, verified against the OpenAI Codex hooks spec, CLI 0.142.5). `CodexAdapter.supportsHooks()` is now `true`. Two hooks ship on Codex: `framework-edit-gate` (PreToolUse; the SAME script made `apply_patch`-aware, **fail-open on parse doubt**) and `agent-discovery-info` (SessionStart; the SAME script with `--runtime codex` → inspects `.codex/agents/*.toml`). Two are deliberately **not bound on Codex** with documented rationale + compensating mechanism: `agent-discovery-gate` (no observable Agent tool → the `/new`+`/prd` Codex preflight) and `overlay-telemetry` (no reliable Read event). Ownership is matched by a command **marker** (Codex entries carry no `id`); registration is **additive** (preserves Graphify + user hooks). **Hook trust is never faked** — BALDART writes `.codex/hooks.json` but never `~/.codex/config.toml`; `add`/`update` surface that Codex asks for one-time approval, and `doctor` reports presence and trust **separately** (untrusted-only is not a failure).
22
+ - **S3 — cross-tool delegation docs.** `skills-mapping.md` reframed for portable (Claude + Codex) skills, citing `AGENTS.md` as the delegation SSOT (the domain-ownership core — code→coder / UI→ui-expert, no-silent-fallback — already landed cross-tool in the AGENTS.md primitive), forward-referencing the Codex preflight, and noting that non-BALDART feature-entry skills (e.g. `nf`) must not drive BALDART backlog execution.
23
+
24
+ **MINOR** — additive capability (native Codex hook management). **Codex parity: adapter-generated** (one registry SSOT + a Codex renderer; the two edit/session hook scripts are single-source dual-runtime). **No new `baldart.config.yml` key** (rides on `tools.enabled` containing `codex`) → the schema-change propagation rule does NOT apply. **Must-measure-first (ships safe-by-default):** one live Codex hook fire + the trust-approval flow remain to confirm on a real Codex consumer — the gate fails open, registration is additive, and trust is never faked, so S2 cannot break a consumer even before that confirmation. Authoritative design: [`framework/docs/CODEX-HOOKS.md`](framework/docs/CODEX-HOOKS.md).
25
+
26
+ ### Added
27
+
28
+ - **`src/utils/hooks/registry.js`** — runtime-neutral hook set with per-runtime bindings (`runtimes.claude` / `runtimes.codex`) + `defsFor(runtime)`.
29
+ - **`src/utils/hooks/claude.js`** — the Claude Code renderer (`.claude/settings.json`).
30
+ - **`src/utils/hooks/codex.js`** — the Codex renderer (`.codex/hooks.json`) with additive register/verify/unregister/status/drift + a `~/.codex/config.toml` trust probe.
31
+ - **`framework/docs/CODEX-HOOKS.md`** — authoritative design + parity table + must-measure-first.
32
+
33
+ ### Changed
34
+
35
+ - **`src/utils/hooks.js`** — now a thin compatibility shim re-exporting the Claude renderer (historical `require('../utils/hooks')` surface preserved, byte-identical).
36
+ - **`src/utils/tool-adapters/{codex,claude}.js`** — `supportsHooks()` (Codex `false`→`true`) + `hookTargetPath()`.
37
+ - **`framework/.claude/hooks/framework-edit-gate.js`** — runtime-aware: parses the Codex `apply_patch` envelope (fail-open); Claude Edit/Write/MultiEdit/NotebookEdit path byte-unchanged.
38
+ - **`framework/.claude/hooks/agent-discovery-info.sh`** — accepts `--runtime codex` → inspects `.codex/agents/*.toml`.
39
+ - **`src/commands/{add,update,doctor}.js`** — register/backfill/report Codex hooks gated on `tools.enabled ∋ codex`; doctor reports presence and trust separately.
40
+ - **`framework/agents/skills-mapping.md`** — cross-tool framing + delegation SSOT note (cites `AGENTS.md`, `CODEX-HOOKS.md`).
41
+ - **`framework/docs/CODEX-AGENTS.md`** — cross-ref to `CODEX-HOOKS.md`.
42
+ - **`CLAUDE.md`** — new "Cross-tool parity discipline" + "Codex-native hooks" conventions; fixed the stale "hooks remain Claude-only" line.
43
+
8
44
  ## [4.87.0] - 2026-07-01
9
45
 
10
46
  **Plan-review gate promoted to the cross-tool SSOT (AGENTS.md primitive `1.3.0`).** The "have the plan reviewed by `plan-auditor` + `doc-reviewer` before presenting it" rule lived only in the `CLAUDE.md` stub's Plan Mode — so Codex never ran it. Now that both agents exist on Codex (transpiled to `.codex/agents/`), the tool-agnostic principle is promoted into `AGENTS.md`: **MUST have any non-trivial plan reviewed by `plan-auditor` + `doc-reviewer` (in parallel), fold in their feedback, note the review gate in the plan, and present the ALREADY-reviewed plan — never a raw one.** The Claude-only `EnterPlanMode`/`ExitPlanMode` mechanics stay in `CLAUDE.md`; `AGENTS.md` carries the principle so both tools honor it.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.87.0
1
+ 4.88.1
@@ -34,6 +34,20 @@
34
34
  set -u
35
35
  exec 2>/dev/null # silence stderr; we are not user-visible
36
36
 
37
+ # --- runtime selection ----------------------------------------------------
38
+ # Default is Claude (inspects .claude/agents/<name>.md symlinks). Codex passes
39
+ # `--runtime codex`, which inspects the generated .codex/agents/<name>.toml
40
+ # files instead — Codex custom agents are transpiled TOML, never symlinks, so
41
+ # the Claude-specific "degraded regular-file" concept does not apply there.
42
+ RUNTIME="claude"
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ --runtime) RUNTIME="${2:-claude}"; shift 2 ;;
46
+ --runtime=*) RUNTIME="${1#*=}"; shift ;;
47
+ *) shift ;;
48
+ esac
49
+ done
50
+
37
51
  # --- read envelope --------------------------------------------------------
38
52
 
39
53
  INPUT=""
@@ -58,7 +72,13 @@ cd "$CWD" 2>/dev/null || exit 0
58
72
  # --- locate the framework agent directory --------------------------------
59
73
 
60
74
  FRAMEWORK_AGENTS_DIR=".framework/framework/.claude/agents"
61
- CONSUMER_AGENTS_DIR=".claude/agents"
75
+ if [ "$RUNTIME" = "codex" ]; then
76
+ CONSUMER_AGENTS_DIR=".codex/agents"
77
+ CONSUMER_AGENT_EXT="toml"
78
+ else
79
+ CONSUMER_AGENTS_DIR=".claude/agents"
80
+ CONSUMER_AGENT_EXT="md"
81
+ fi
62
82
 
63
83
  [ -d "$FRAMEWORK_AGENTS_DIR" ] || exit 0 # consumer not installed — silent.
64
84
 
@@ -79,12 +99,15 @@ while IFS= read -r f; do
79
99
  [ -z "$f" ] && continue
80
100
  name="$(basename "$f" .md)"
81
101
  [ "$name" = "REGISTRY" ] && continue
82
- target="$CONSUMER_AGENTS_DIR/${name}.md"
102
+ target="$CONSUMER_AGENTS_DIR/${name}.${CONSUMER_AGENT_EXT}"
83
103
  # `test -e` returns false for broken symlinks too — exactly what we want.
84
104
  if [ ! -e "$target" ]; then
85
105
  if [ -z "$MISSING" ]; then MISSING="$name"; else MISSING="$MISSING, $name"; fi
86
106
  continue
87
107
  fi
108
+ # Codex agents are generated TOML regular files — that IS the healthy state,
109
+ # so the degraded-regular-file check below applies to Claude only.
110
+ [ "$RUNTIME" = "codex" ] && continue
88
111
  # File exists. If it's a symlink → CC discovery sees it, healthy.
89
112
  # If it's a regular file with the baldart-generated marker → degraded
90
113
  # state (legacy overlay layout before v3.27.0).
@@ -107,7 +130,11 @@ escape_json() {
107
130
 
108
131
  MSG=""
109
132
  if [ -n "$MISSING" ]; then
110
- MSG="BALDART expects the following sub-agents to be reachable via the Agent tool in this session, but their backing files are missing from .claude/agents/: ${MISSING}. Calling Agent with one of these subagent_type values will be denied by the agent-discovery-gate PreToolUse hook. Run \`npx baldart doctor\` to repair, then restart this session."
133
+ if [ "$RUNTIME" = "codex" ]; then
134
+ MSG="BALDART expects the following custom agents to be spawnable by name in this Codex session, but their generated definitions are missing from .codex/agents/ (expected <name>.toml): ${MISSING}. Do NOT silently substitute a generic agent for these — per AGENTS.md the run must stop with the missing-agent remediation. Run \`npx baldart update\` (regenerates the TOML from the .md source) or \`npx baldart doctor\`, then restart this session."
135
+ else
136
+ MSG="BALDART expects the following sub-agents to be reachable via the Agent tool in this session, but their backing files are missing from .claude/agents/: ${MISSING}. Calling Agent with one of these subagent_type values will be denied by the agent-discovery-gate PreToolUse hook. Run \`npx baldart doctor\` to repair, then restart this session."
137
+ fi
111
138
  fi
112
139
  if [ -n "$DEGRADED" ]; then
113
140
  DEG_MSG="BALDART detected overlay-merged agents stored in the legacy pre-v3.27.0 layout (regular files in .claude/agents/ with the baldart-generated marker): ${DEGRADED}. Claude Code upstream bug #20931 silently skips regular files at session-start discovery, so calling Agent with one of these subagent_type values will fail with InputValidationError even though the file is on disk. Run \`npx baldart update\` to migrate to the v3.27.0+ symlink-indirection layout (the file is moved to .baldart/generated/<kind>s/<name>.md and .claude/agents/<name>.md becomes a symlink to it), then restart this session."
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * framework-edit-gate — Claude Code PreToolUse hook.
3
+ * framework-edit-gate — cross-tool PreToolUse hook (Claude Code + Codex).
4
4
  *
5
- * Intercepts Edit / Write / MultiEdit calls whose target resolves (via
6
- * symlink) to a path inside `.framework/` — i.e. the BALDART subtree that
7
- * flows upstream via `baldart push`. Runs the contamination scanner on the
8
- * new content and blocks the call with an informative `reason` if any
9
- * project-specific tokens are present (Neo-Brutalism, merchant, fidelity,
10
- * Recharts, hardcoded paths, etc.).
5
+ * Intercepts file-edit calls whose target resolves (via symlink) to a path
6
+ * inside `.framework/` — i.e. the BALDART subtree that flows upstream via
7
+ * `baldart push`. Runs the contamination scanner on the new content and blocks
8
+ * the call with an informative `reason` if any project-specific tokens are
9
+ * present (Neo-Brutalism, merchant, fidelity, Recharts, hardcoded paths, etc.).
11
10
  *
12
- * Effect: Claude sees the block reason and either reformulates the content
11
+ * Runtime-aware payloads:
12
+ * - Claude Code: `Edit` / `Write` / `MultiEdit` / `NotebookEdit`, each with a
13
+ * single `file_path` + new content.
14
+ * - Codex: `apply_patch`, whose input is the OpenAI patch envelope covering
15
+ * one or more files; `handleApplyPatch` parses it (fail-open on any doubt).
16
+ * The deny format (`hookSpecificOutput.permissionDecision`) is identical for
17
+ * both runtimes.
18
+ *
19
+ * Effect: the agent sees the block reason and either reformulates the content
13
20
  * generically (referencing `${paths.X}` / `identity.X`) or moves the change
14
- * into `.baldart/overlays/<skill>.md`. Project-specific tokens never reach
15
- * the framework subtree silently.
21
+ * into `.baldart/overlays/<skill>.md`. Project-specific tokens never reach the
22
+ * framework subtree silently.
16
23
  *
17
- * Registration: see src/utils/hooks.js — `baldart add` and `baldart update`
18
- * auto-register this hook in the consumer's `.claude/settings.json`.
24
+ * Registration: see src/utils/hooks/registry.js — `baldart add` and
25
+ * `baldart update` auto-register this hook in the consumer's
26
+ * `.claude/settings.json` (Claude) and/or `.codex/hooks.json` (Codex).
19
27
  *
20
28
  * Fail-safe contract: this hook MUST NOT block legitimate work. If anything
21
29
  * unexpected happens (missing scanner, parse error, IO error), it exits 0
@@ -150,12 +158,138 @@ function blockReason(realPath, decisions, blockings) {
150
158
  ].join('\n');
151
159
  }
152
160
 
161
+ // Structural marker that identifies a BALDART-generated file (agent/command
162
+ // generated from base + overlay). Hoisted to module scope so both the Claude
163
+ // single-file path and the Codex apply_patch path can reuse it. The regex
164
+ // requires kind=…name=…overlay_sha= so a mere string-literal mention of the
165
+ // marker (e.g. in this hook, overlay-merger.js) is not a false positive.
166
+ const GENERATED_MARKER_RE =
167
+ /<!-- baldart-generated:[\s\S]{0,400}?kind=\S+[\s\S]{0,400}?name=\S+[\s\S]{0,400}?overlay_sha=\S+/;
168
+
169
+ // Emit a deny decision in the shape both Claude Code and Codex accept
170
+ // (`hookSpecificOutput.permissionDecision`), falling back to the legacy
171
+ // exit-2 + stderr convention if stdout is unavailable. Never returns.
172
+ function emitDeny(reason) {
173
+ try {
174
+ process.stdout.write(JSON.stringify({
175
+ hookSpecificOutput: {
176
+ hookEventName: 'PreToolUse',
177
+ permissionDecision: 'deny',
178
+ permissionDecisionReason: reason,
179
+ },
180
+ }));
181
+ } catch (_) {
182
+ process.stderr.write(reason + '\n');
183
+ process.exit(2);
184
+ }
185
+ process.exit(0);
186
+ }
187
+
188
+ // --- Codex apply_patch support -------------------------------------------
189
+ //
190
+ // Codex normalizes file edits to a single `apply_patch` tool whose input is the
191
+ // OpenAI patch envelope (`*** Begin Patch` / `*** Add File:` / `*** Update File:`
192
+ // / `*** Delete File:` / `+added` / `-removed`). The field carrying the patch
193
+ // text is not guaranteed by the spec, so we probe the common shapes and, on any
194
+ // doubt, fail OPEN (return no targets → the caller exits 0). This gate is a
195
+ // safety net; a missed edit is caught later by the `baldart push` scanner, but
196
+ // a FALSE block would disrupt legitimate work — so uncertainty always fails open.
197
+
198
+ function extractPatchText(toolInput) {
199
+ if (!toolInput) return '';
200
+ if (typeof toolInput === 'string') return toolInput;
201
+ for (const key of ['input', 'patch', 'content', 'text', 'diff', 'apply_patch']) {
202
+ if (typeof toolInput[key] === 'string' && toolInput[key].includes('*** ')) return toolInput[key];
203
+ }
204
+ // Last resort: any string value that looks like a patch envelope.
205
+ for (const v of Object.values(toolInput)) {
206
+ if (typeof v === 'string' && v.includes('*** Begin Patch')) return v;
207
+ }
208
+ return '';
209
+ }
210
+
211
+ // Parse the patch text into per-file `{ path, added }` targets. `added` is the
212
+ // concatenation of inserted lines (leading `+` stripped) for that file.
213
+ function collectApplyPatchTargets(toolInput) {
214
+ const text = extractPatchText(toolInput);
215
+ if (!text) return [];
216
+ const targets = [];
217
+ let cur = null;
218
+ const fileHdr = /^\*\*\* (Add|Update|Delete) File: (.+?)\s*$/;
219
+ for (const line of text.split('\n')) {
220
+ const m = line.match(fileHdr);
221
+ if (m) {
222
+ if (cur) targets.push(cur);
223
+ cur = { path: m[2], added: [] };
224
+ continue;
225
+ }
226
+ if (!cur) continue;
227
+ if (line.startsWith('*** ')) { targets.push(cur); cur = null; continue; } // End Patch / next marker
228
+ if (line.startsWith('+') && !line.startsWith('+++')) cur.added.push(line.slice(1));
229
+ }
230
+ if (cur) targets.push(cur);
231
+ return targets.map((t) => ({ path: t.path, added: t.added.join('\n') }));
232
+ }
233
+
234
+ // Codex PreToolUse(apply_patch) handler — the multi-file analogue of the Claude
235
+ // single-file flow below. Reuses the same generated-file guard and the same
236
+ // `.framework/` contamination scan. Fail-safe throughout.
237
+ function handleApplyPatch(input) {
238
+ let targets;
239
+ try { targets = collectApplyPatchTargets(input.tool_input); }
240
+ catch (_) { safeExit(); }
241
+ if (!targets || targets.length === 0) safeExit();
242
+
243
+ for (const t of targets) {
244
+ if (!t.path) continue;
245
+
246
+ // Guard 1: overwriting a BALDART-generated file without retaining the marker.
247
+ try {
248
+ if (fs.existsSync(t.path)) {
249
+ const head = fs.readFileSync(t.path, 'utf8').slice(0, 5000);
250
+ if (GENERATED_MARKER_RE.test(head) && !GENERATED_MARKER_RE.test(t.added)) {
251
+ emitDeny(
252
+ `Cannot edit BALDART-generated file (${t.path}).\n` +
253
+ `This file is auto-generated by \`npx baldart update\` from a base ` +
254
+ `framework file plus an overlay. Manual edits will be overwritten.\n\n` +
255
+ `To customise: edit the corresponding overlay under .baldart/overlays/ ` +
256
+ `(or create it if absent), then re-run \`npx baldart update\`.`);
257
+ }
258
+ }
259
+ } catch (_) { /* fail-safe */ }
260
+
261
+ // Guard 2: contamination inside the .framework/ subtree.
262
+ const realPath = resolveReal(t.path);
263
+ if (!realPath) continue;
264
+ if (realPath.includes(`${path.sep}.baldart${path.sep}overlays${path.sep}`)) continue;
265
+ const frameworkRoot = findFrameworkRoot(realPath);
266
+ if (!frameworkRoot) continue;
267
+ if (!t.added) continue;
268
+ const scanner = loadScanner(frameworkRoot);
269
+ if (!scanner) continue;
270
+ let findings;
271
+ try {
272
+ const r = scanner.scan(t.added);
273
+ if (r.optedOut) continue;
274
+ findings = r.findings || [];
275
+ } catch (_) { continue; }
276
+ const decisions = findings.filter((f) => f.severity === 'requires-decision');
277
+ const blockings = findings.filter((f) => f.severity === 'block');
278
+ if (decisions.length === 0 && blockings.length === 0) continue;
279
+ emitDeny(blockReason(realPath, decisions, blockings));
280
+ }
281
+
282
+ safeExit();
283
+ }
284
+
153
285
  function main() {
154
286
  const raw = readStdin();
155
287
  const input = parseInput(raw);
156
288
  if (!input) safeExit();
157
289
 
158
290
  const toolName = input.tool_name;
291
+ // Codex routes all file edits through `apply_patch`; handle that shape first.
292
+ if (toolName === 'apply_patch') return handleApplyPatch(input);
159
293
  if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) safeExit();
160
294
 
161
295
  const filePath = input.tool_input && input.tool_input.file_path;
@@ -170,8 +304,7 @@ function main() {
170
304
  // regex (requires kind=…name=…overlay_sha=…) avoids false-positives on
171
305
  // framework source files that mention the marker as a string literal
172
306
  // (e.g. this hook itself, agent-discovery-info.sh, overlay-merger.js).
173
- const GENERATED_MARKER_RE =
174
- /<!-- baldart-generated:[\s\S]{0,400}?kind=\S+[\s\S]{0,400}?name=\S+[\s\S]{0,400}?overlay_sha=\S+/;
307
+ // GENERATED_MARKER_RE is defined at module scope (shared with handleApplyPatch).
175
308
  try {
176
309
  const fs = require('fs');
177
310
  if (fs.existsSync(filePath)) {
@@ -2,7 +2,11 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Map Claude Code skills to project domains and provide guidance on when to use each skill.
5
+ Map the framework's **portable skills** (Claude Code + Codex) to project domains
6
+ and provide guidance on when to use each skill. The same skill source under
7
+ `framework/.claude/skills/<x>/` is installed for every enabled tool — Claude
8
+ reads it from `.claude/skills/<x>`, Codex from `.agents/skills/<x>` — so skill
9
+ selection is runtime-agnostic; only the invocation mechanics differ.
6
10
 
7
11
  ## Scope
8
12
 
@@ -29,6 +33,31 @@ Map Claude Code skills to project domains and provide guidance on when to use ea
29
33
  > (noted per entry). When the flag is `false` the skill REFUSES — do not route to
30
34
  > it; resolve the gate first (e.g. `/design-system-init` for `has_design_system`).
31
35
 
36
+ ### Cross-tool delegation (SSOT: `AGENTS.md`)
37
+
38
+ Skill selection is portable, but **who does the work** is governed cross-tool by
39
+ `AGENTS.md` § Non-negotiables — the single SSOT (read by Claude Code and Codex
40
+ alike), not restated here:
41
+
42
+ - Understand-before-implement (`codebase-architect` first), plan-review gate,
43
+ and **domain delegation — code/logic/tests → `coder`, UI → `ui-expert`** —
44
+ hold on both runtimes. The invocation differs only mechanically: Claude uses
45
+ the Task tool (`subagent_type`), Codex spawns the custom agent **by name**
46
+ from `.codex/agents/<name>.toml`.
47
+ - **No silent fallback.** If a named agent is missing or an invocation returns
48
+ empty, STOP and report — never improvise a generic agent. On Claude this is
49
+ also guarded by the `agent-discovery-gate` hook; on Codex, where agents are
50
+ spawned by name (no observable Agent tool), the guarantee is enforced by the
51
+ `/new` + `/prd` **Codex preflight** (validates `.codex/agents/<name>.toml`
52
+ before spawning) — see [`CODEX-HOOKS.md`](../docs/CODEX-HOOKS.md).
53
+ - Branch/worktree creation is the orchestrator's job (`/nw`, `/new`), **never**
54
+ an implementer agent's — same rule on both runtimes.
55
+
56
+ **Non-BALDART feature-entry skills** (e.g. the OpenAI-curated `nf`) describe
57
+ their own delegation model with non-BALDART agent names and must **not** be used
58
+ to drive BALDART backlog execution — route backlog work through `/prd` + `/new`,
59
+ which enforce the contract above.
60
+
32
61
  ## Core Development Skills
33
62
 
34
63
  ### prd
@@ -2,6 +2,10 @@
2
2
 
3
3
  > **Status:** shipped in v4.80.0. Claude + Codex. No new `baldart.config.yml` key —
4
4
  > rides on `tools.enabled` containing `codex`.
5
+ >
6
+ > **Related:** Codex also has a native **hook** surface, managed by BALDART in
7
+ > `.codex/hooks.json` — see [`CODEX-HOOKS.md`](CODEX-HOOKS.md). This doc covers
8
+ > agents (transpiled TOML); that one covers hooks.
5
9
 
6
10
  ## The problem this fixes
7
11
 
@@ -0,0 +1,142 @@
1
+ # Codex Hooks
2
+
3
+ BALDART manages **native Codex hooks** in `.codex/hooks.json`, in parallel with
4
+ the Claude Code hooks it has always managed in `.claude/settings.json`. This is
5
+ the Codex half of the cross-tool hook parity delivered by the Codex-parity
6
+ foundation (slice S2). It is **additive** — Claude hook behavior is unchanged.
7
+
8
+ > Authoritative for agents: **read [`CODEX-AGENTS.md`](CODEX-AGENTS.md)** for the
9
+ > agent-transpilation story and this file for hooks. The runtime-neutral hook
10
+ > **set** lives in one place: [`src/utils/hooks/registry.js`](../../src/utils/hooks/registry.js).
11
+
12
+ ## Why this exists
13
+
14
+ Codex now has a native hook surface using the **same JSON schema as Claude
15
+ Code** (verified against the OpenAI Codex hooks spec, CLI 0.142.5):
16
+
17
+ ```json
18
+ {
19
+ "hooks": {
20
+ "PreToolUse": [
21
+ { "matcher": "Edit|Write|apply_patch",
22
+ "hooks": [ { "type": "command", "command": "node …/framework-edit-gate.js" } ] }
23
+ ],
24
+ "SessionStart": [
25
+ { "hooks": [ { "type": "command", "command": "bash …/agent-discovery-info.sh --runtime codex" } ] }
26
+ ]
27
+ }
28
+ }
29
+ ```
30
+
31
+ Before S2, `CodexAdapter.supportsHooks()` returned `false` and the hook
32
+ registrar was Claude-only, so a Codex consumer got **none** of BALDART's hook
33
+ protections (framework-contamination gate, agent-discovery signal). Now they do.
34
+
35
+ ## Architecture — one registry, two renderers
36
+
37
+ ```
38
+ src/utils/hooks/
39
+ registry.js ← runtime-neutral hook SET (one entry per logical hook,
40
+ with per-runtime bindings under `runtimes.claude` / `runtimes.codex`)
41
+ claude.js ← Claude renderer → .claude/settings.json
42
+ codex.js ← Codex renderer → .codex/hooks.json
43
+ src/utils/hooks.js ← compatibility shim → re-exports claude.js
44
+ ```
45
+
46
+ A logical hook declares an identity (`id`, `order`, `purpose`) and a binding
47
+ per runtime that can express it. `defsFor('claude')` / `defsFor('codex')`
48
+ project the registry onto each renderer. A hook with no binding for a runtime is
49
+ simply not registered there.
50
+
51
+ ### Key differences the Codex renderer handles
52
+
53
+ | Concern | Claude Code | Codex |
54
+ |---|---|---|
55
+ | Settings file | `.claude/settings.json` (many keys) | `.codex/hooks.json` (hooks only) |
56
+ | Schema | `hooks[event][].{matcher, hooks:[{type,command}]}` | **identical** |
57
+ | Per-entry `id` | yes (BALDART stamps + matches by `id`) | **no** — Codex addresses by position, trusts by content hash, and rejects unknown fields. Ownership is matched by a **command substring `marker`**, and entries carry **no `id`**. |
58
+ | Edit tool | `Edit` / `Write` / `MultiEdit` / `NotebookEdit` (has `file_path`) | `apply_patch` (patch envelope; matcher also accepts `Edit\|Write`) |
59
+ | Session context | `SessionStart` (inject `additionalContext`) | `SessionStart` (same) |
60
+ | Deny format | `hookSpecificOutput.permissionDecision:"deny"` or exit 2 | **identical** |
61
+ | Trust | none | **hook trust**: a new/changed hook is skipped until the user approves it; recorded in `~/.codex/config.toml`. |
62
+
63
+ ## The hooks and their Codex status
64
+
65
+ | BALDART hook | Claude | Codex | Rationale |
66
+ |---|---|---|---|
67
+ | `baldart-framework-edit-gate` | `PreToolUse` on `Edit\|Write\|MultiEdit\|NotebookEdit` | **`PreToolUse` on `Edit\|Write\|apply_patch`** | Same script, made runtime-aware: it parses the Codex `apply_patch` envelope (`handleApplyPatch`) and scans added lines. **Fail-open on any parse doubt** — a missed edit is caught later by the `baldart push` scanner, but a false block would disrupt work. |
68
+ | `baldart-agent-discovery-info` | `SessionStart` | **`SessionStart --runtime codex`** | Same script, told to inspect `.codex/agents/<name>.toml` (generated TOML) instead of `.claude/agents/<name>.md` (symlinks). Injects `additionalContext` only. |
69
+ | `baldart-agent-discovery-gate` | `PreToolUse` on `Agent` | **not bound (deferred)** | Codex spawns subagents by name, not via an observable `Agent` PreToolUse tool; its `SubagentStart` event is session-scoped with unconfirmed deny semantics. The no-silent-fallback guarantee is enforced Codex-side by the **skill preflight** (the `/new` + `/prd` Codex runtime modules validate `.codex/agents/<name>.toml` before spawning). |
70
+ | `baldart-overlay-telemetry` | `PostToolUse` on `Read` | **not bound (unsupported)** | Codex exposes no reliable file-`Read` tool event to hook on. Overlay-load telemetry stays Claude-only (observation-only, no functional impact). |
71
+
72
+ The two deferred/unsupported hooks satisfy acceptance criterion 12 of the
73
+ porting report: every unsupported hook is explicitly documented with a
74
+ compensating mechanism (preflight for the gate; N/A for telemetry).
75
+
76
+ ## Hook trust — BALDART never fakes it
77
+
78
+ Codex records trust against a hook's content; a new or changed hook is
79
+ **inactive until the user approves it once** (or the session is run with
80
+ `--dangerously-bypass-hook-trust`, which BALDART never does). The approval is
81
+ stored in `~/.codex/config.toml`:
82
+
83
+ ```toml
84
+ [hooks.state."/abs/path/.codex/hooks.json:pre_tool_use:0:0"]
85
+ trusted_hash = "sha256:…"
86
+ ```
87
+
88
+ BALDART **writes `.codex/hooks.json` but never `config.toml`** — faking trust
89
+ would defeat the security model. Instead:
90
+
91
+ - `add` / `update` register the hooks and **tell the user** that Codex will
92
+ prompt for approval on the next session ("until approved, they stay inactive").
93
+ - `doctor` reports **presence and trust separately**: `Codex hooks: 2/2
94
+ registered & trusted`, or `2/2 registered, 2 untrusted` with a
95
+ `• untrusted: … (approve once on the next Codex session)` line. Untrusted-only
96
+ is **not** a failure (it needs an interactive step BALDART cannot perform).
97
+
98
+ The trust probe reads `config.toml` and checks whether a `trusted_hash` entry
99
+ exists for each hook's position key (`<hooks.json abspath>:<event_snake>:<i>:<j>`).
100
+ It reports *presence of a trust entry*, not hash currency (Codex's hash
101
+ algorithm is unspecified).
102
+
103
+ ## Lifecycle wiring
104
+
105
+ - **`baldart add`** — after Claude hook registration, if `codex` ∈ enabled
106
+ tools, register the Codex hooks (additive) and surface the trust nudge. The
107
+ post-flight is **non-fatal** for Codex (a missing Codex hook warns + points to
108
+ `doctor`; it does not fail the install the way a missing Claude hook does).
109
+ - **`baldart update`** — backfills Codex hooks for consumers that predate S2,
110
+ idempotently, preserving foreign entries.
111
+ - **`baldart doctor`** — probes `.codex/hooks.json`, offers
112
+ `register-codex-hooks` (missing) and `reconcile-codex-hooks-drift` (drift)
113
+ actions, and reports trust state.
114
+
115
+ ## Additivity guarantee
116
+
117
+ The Codex renderer only ever touches entries whose command carries a BALDART
118
+ `marker`. Foreign hooks — notably a project's **Graphify** `graphify hook-check`
119
+ entry, or any user-authored hook — are preserved verbatim, in place. `.codex/
120
+ hooks.json` is never rewritten wholesale.
121
+
122
+ ## Must-measure-first
123
+
124
+ The Codex hook schema, events, deny format, and trust key format were verified
125
+ against the OpenAI Codex hooks spec and the real local `.codex/hooks.json` +
126
+ `config.toml`. The renderer, the `apply_patch` gate, and the trust probe are
127
+ unit-tested. What remains to confirm on a **live Codex consumer**:
128
+
129
+ 1. one BALDART Codex hook actually **fires** in a real Codex session, and
130
+ 2. the **trust-approval prompt** flow behaves as documented.
131
+
132
+ Both fail safe: the gate fails open, registration is additive, and trust is
133
+ never faked — so shipping S2 cannot break a Codex consumer even before the live
134
+ confirmation. Report findings back into this doc.
135
+
136
+ ## Adding or changing a hook
137
+
138
+ Edit exactly one place — [`src/utils/hooks/registry.js`](../../src/utils/hooks/registry.js):
139
+ add a neutral entry with the `runtimes.claude` and/or `runtimes.codex` binding.
140
+ The renderers, `add`/`update`/`doctor` wiring, and `doctor` reporting pick it up
141
+ automatically. Per the cross-tool parity discipline, a new hook MUST state its
142
+ Codex binding (or an explicit rationale for having none).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.87.0",
3
+ "version": "4.88.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"
@@ -226,6 +226,35 @@ async function add(repo, options) {
226
226
  UI.warning(`Hook registration skipped: ${err.message}`);
227
227
  }
228
228
 
229
+ // Codex hooks (S2) — register the framework-edit-gate (PreToolUse/apply_patch)
230
+ // + agent-discovery-info (SessionStart) hooks into .codex/hooks.json when
231
+ // Codex is an enabled tool. Additive (preserves Graphify + user entries),
232
+ // never fakes hook trust — Codex asks the user to approve them on the next
233
+ // session; we surface that here so the inactivity is expected, not a bug.
234
+ if (Array.isArray(enabledTools) && enabledTools.includes('codex')) {
235
+ try {
236
+ const CodexHooks = require('../utils/hooks/codex');
237
+ const res = await CodexHooks.registerAll(process.cwd(), {
238
+ onDrift: CodexHooks.createDriftPrompt(UI, { autoYes: nonInteractive }),
239
+ });
240
+ if (res.malformed) {
241
+ UI.warning('.codex/hooks.json is malformed; skipped Codex hook registration. Run `npx baldart doctor` after fixing it.');
242
+ } else {
243
+ for (const r of res.results) {
244
+ if (r.status === 'created') UI.success(`Registered Codex hook \`${r.id}\` in .codex/hooks.json`);
245
+ else if (r.status === 'drift-replaced') UI.success(`Replaced drifted Codex hook \`${r.id}\` (was: ${r.previousCommand})`);
246
+ else if (r.status === 'drift-kept') UI.info(`Kept user-customized Codex hook \`${r.id}\` as-is.`);
247
+ }
248
+ const verify = CodexHooks.verifyAll(process.cwd());
249
+ if (verify.untrusted && verify.untrusted.length) {
250
+ UI.info(`Codex will prompt you to approve ${verify.untrusted.length} new hook(s) on the next session (hook trust): ${verify.untrusted.join(', ')}. Until approved they stay inactive.`);
251
+ }
252
+ }
253
+ } catch (err) {
254
+ UI.warning(`Codex hook registration skipped: ${err.message}`);
255
+ }
256
+ }
257
+
229
258
  // Configure Git aliases
230
259
  UI.section('Configuring Git Aliases');
231
260
 
@@ -343,6 +372,22 @@ async function add(repo, options) {
343
372
  process.exit(1);
344
373
  }
345
374
 
375
+ // Codex hook post-flight — non-fatal (Codex is a secondary target and hook
376
+ // trust is an interactive step BALDART cannot complete during install). A
377
+ // MISSING Codex hook is surfaced as a warning + a doctor pointer, but it
378
+ // does not fail the install the way a missing Claude hook does.
379
+ if (Array.isArray(enabledTools) && enabledTools.includes('codex')) {
380
+ try {
381
+ const CodexHooks = require('../utils/hooks/codex');
382
+ const cv = CodexHooks.verifyAll(process.cwd());
383
+ if (cv.malformed) {
384
+ UI.warning('.codex/hooks.json is malformed — Codex hooks unverifiable. Run `npx baldart doctor`.');
385
+ } else if (!cv.ok) {
386
+ UI.warning(`Codex hooks missing: ${cv.missing.join(', ')}. Run \`npx baldart doctor\` to repair.`);
387
+ }
388
+ } catch (_) { /* non-fatal */ }
389
+ }
390
+
346
391
  // Success summary
347
392
  UI.header('✓ INSTALLATION COMPLETE');
348
393