baldart 4.87.0 → 4.88.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 +28 -0
- package/VERSION +1 -1
- package/framework/.claude/hooks/agent-discovery-info.sh +30 -3
- package/framework/.claude/hooks/framework-edit-gate.js +147 -14
- package/framework/agents/skills-mapping.md +30 -1
- package/framework/docs/CODEX-AGENTS.md +4 -0
- package/framework/docs/CODEX-HOOKS.md +142 -0
- package/package.json +1 -1
- package/src/commands/add.js +45 -0
- package/src/commands/doctor.js +104 -0
- package/src/commands/update.js +31 -0
- package/src/utils/hooks/claude.js +402 -0
- package/src/utils/hooks/codex.js +360 -0
- package/src/utils/hooks/registry.js +176 -0
- package/src/utils/hooks.js +17 -467
- package/src/utils/tool-adapters/claude.js +3 -0
- package/src/utils/tool-adapters/codex.js +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.0] - 2026-07-01
|
|
9
|
+
|
|
10
|
+
**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**:
|
|
11
|
+
|
|
12
|
+
- **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).
|
|
13
|
+
- **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).
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
16
|
+
**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).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`src/utils/hooks/registry.js`** — runtime-neutral hook set with per-runtime bindings (`runtimes.claude` / `runtimes.codex`) + `defsFor(runtime)`.
|
|
21
|
+
- **`src/utils/hooks/claude.js`** — the Claude Code renderer (`.claude/settings.json`).
|
|
22
|
+
- **`src/utils/hooks/codex.js`** — the Codex renderer (`.codex/hooks.json`) with additive register/verify/unregister/status/drift + a `~/.codex/config.toml` trust probe.
|
|
23
|
+
- **`framework/docs/CODEX-HOOKS.md`** — authoritative design + parity table + must-measure-first.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`src/utils/hooks.js`** — now a thin compatibility shim re-exporting the Claude renderer (historical `require('../utils/hooks')` surface preserved, byte-identical).
|
|
28
|
+
- **`src/utils/tool-adapters/{codex,claude}.js`** — `supportsHooks()` (Codex `false`→`true`) + `hookTargetPath()`.
|
|
29
|
+
- **`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.
|
|
30
|
+
- **`framework/.claude/hooks/agent-discovery-info.sh`** — accepts `--runtime codex` → inspects `.codex/agents/*.toml`.
|
|
31
|
+
- **`src/commands/{add,update,doctor}.js`** — register/backfill/report Codex hooks gated on `tools.enabled ∋ codex`; doctor reports presence and trust separately.
|
|
32
|
+
- **`framework/agents/skills-mapping.md`** — cross-tool framing + delegation SSOT note (cites `AGENTS.md`, `CODEX-HOOKS.md`).
|
|
33
|
+
- **`framework/docs/CODEX-AGENTS.md`** — cross-ref to `CODEX-HOOKS.md`.
|
|
34
|
+
- **`CLAUDE.md`** — new "Cross-tool parity discipline" + "Codex-native hooks" conventions; fixed the stale "hooks remain Claude-only" line.
|
|
35
|
+
|
|
8
36
|
## [4.87.0] - 2026-07-01
|
|
9
37
|
|
|
10
38
|
**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.
|
|
1
|
+
4.88.0
|
|
@@ -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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
|
3
|
+
* framework-edit-gate — cross-tool PreToolUse hook (Claude Code + Codex).
|
|
4
4
|
*
|
|
5
|
-
* Intercepts
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
18
|
-
* auto-register this hook in the consumer's
|
|
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
|
-
|
|
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
|
|
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
package/src/commands/add.js
CHANGED
|
@@ -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
|
|