Problem Situation
A Codex session whose only spawn guidance is the bundled Hephaestus rule (components/rules/bundled-rules/hephaestus/gpt-5.5.md, injected by the rules hook at SessionStart) copies the rule's own spawn_agent example, and the LazyCodex spawn guard added in 5.0.0-beta.61 (components/ulw-loop/src/spawn-role-guard.ts) denies it. All six examples across the three variants (gpt-5.5.md, gpt-5.6.md, gpt-6.md) spell the call as {"message":"TASK: act as a <role>. ...","fork_context":false} with no agent_type, which is exactly the shape the guard rejects ("Describing a role in message does not select its TOML"). A long orchestration session that had been spawning workers this way for two days on beta.53 lost every spawn the moment beta.62 loaded, mid-lap.
The ultrawork and ulw-execute skills already say every spawn must pass an exact agent_type; the bundled rule is the one document that still teaches the denied shape, and it is the one every session receives without asking for a skill.
Reproduction Logs
Live denial, Codex CLI with omo 5.0.0-beta.62, multi_agent_v1 surface, a spawn written from the bundled rule's example:
Tool call blocked by PreToolUse hook: LazyCodex requires an explicit registered agent_type: explorer, lazycodex-clone-fidelity-reviewer, lazycodex-code-reviewer, lazycodex-gate-reviewer, lazycodex-qa-executor, lazycodex-worker-high, lazycodex-worker-low, lazycodex-worker-medium, librarian, metis, momus, plan. Received no agent_type. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.. Tool: spawn_agent
The rule's examples fed straight into the shipped guard (spawnRoleDenial from components/ulw-loop/dist/spawn-role-guard.js, placeholders filled with explorer / task):
HEAD gpt-5.5.md example 1 DENIED {"message":"TASK: act as a explorer. x","fork_context":false}
HEAD gpt-5.5.md example 2 DENIED {"task_name":"t","message":"TASK: act as a explorer. x","fork_turns":"none"}
HEAD gpt-5.6.md example 1 DENIED {"message":"TASK: act as a explorer. GOAL: x STOP WHEN: x EVIDENCE: x","fork_context":false}
HEAD gpt-5.6.md example 2 DENIED {"task_name":"t","message":"TASK: act as a explorer. GOAL: x STOP WHEN: x EVIDENCE: x","fork_turns":"none"}
HEAD gpt-6.md example 1 DENIED {"message":"TASK: act as a explorer. GOAL: x STOP WHEN: x EVIDENCE: x","fork_context":false}
HEAD gpt-6.md example 2 DENIED {"task_name":"t","message":"TASK: act as a explorer. GOAL: x STOP WHEN: x EVIDENCE: x","fork_turns":"none"}
Root Cause
spawnRoleDenial admits a spawn only when tool_input.agent_type is one of LAZYCODEX_SPAWN_ROLES; it cannot see the tool schema, so it fails closed on any unnamed call. The bundled Hephaestus rule predates that guard (its spawn line last moved in 7124986 / ba0383d, July, in oh-my-openagent) and still shows the role in message with no agent_type, so a model following the rule verbatim produces a call the same plugin denies. The two documents shipped in one plugin disagree, and the guard wins.
Verified Fix
Rewrite the spawn sentence in the three variants so both examples carry "agent_type":"<role>", state that the role is selected by agent_type and that the guard rejects an unnamed spawn, and point implementation work at the three worker roles; the role bullets, GOAL/STOP WHEN/EVIDENCE labels and V1/V2 distinction are untouched. A test in components/rules/test parses every spawn_agent({...}) example in the three files as tool_input and requires a registered agent_type and no full-history fork, so the rule cannot drift away from the guard again. The role set in the test mirrors LAZYCODEX_SPAWN_ROLES; the ulw-loop matrix test already pins that set to the installed TOMLs.
Patch against e6be07d (chore: sync Codex marketplace v5.0.0-beta.62); in the source tree the same files live under packages/omo-codex/plugin/components/rules/.
diff --git a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.5.md b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.5.md
index 204ac6d..c334289 100644
--- a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.5.md
+++ b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.5.md
@@ -41,7 +41,7 @@ omo-codex auto-runs LSP diagnostics after every edit and injects the result: any
# Subagents
-Read-only Codex subagent roles live in `CODEX_HOME/agents/`. Spawn: `multi_agent_v1.spawn_agent({"message":"TASK: act as a <role>. ...","fork_context":false})`. If your tool list instead has a flat `spawn_agent` with a required `task_name` (`multi_agent_v2`): `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: act as a <role>. ...","fork_turns":"none"})` — finished agents end on their own; `wait_agent` takes only `timeout_ms`.
+Read-only Codex subagent roles live in `CODEX_HOME/agents/`. Every spawn selects one of them by exact name in `agent_type`; describing the role in `message` does not select its TOML, and the LazyCodex spawn guard rejects a spawn without a registered `agent_type`. Spawn: `multi_agent_v1.spawn_agent({"message":"TASK: ...","agent_type":"<role>","fork_context":false})`. If your tool list instead has a flat `spawn_agent` with a required `task_name` (`multi_agent_v2`): `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: ...","agent_type":"<role>","fork_turns":"none"})` — finished agents end on their own; `wait_agent` takes only `timeout_ms`. Implementation work selects `lazycodex-worker-low`, `lazycodex-worker-medium`, or `lazycodex-worker-high` by difficulty.
- `explorer` - codebase search
- `librarian` - external docs, OSS code, API contracts
diff --git a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.6.md b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.6.md
index 6e1ef69..6f660e9 100644
--- a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.6.md
+++ b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-5.6.md
@@ -31,7 +31,7 @@ Waiting is not free: a status poll replays the whole accumulated context through
# Subagents
-Read-only Codex subagent roles live in `CODEX_HOME/agents/`. Spawn: `multi_agent_v1.spawn_agent({"message":"TASK: act as a <role>. GOAL: ... STOP WHEN: ... EVIDENCE: ...","fork_context":false})`. If your tool list instead has a flat `spawn_agent` with a required `task_name` (`multi_agent_v2`): `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: act as a <role>. GOAL: ... STOP WHEN: ... EVIDENCE: ...","fork_turns":"none"})` - finished agents end on their own; `wait_agent` takes only `timeout_ms`.
+Read-only Codex subagent roles live in `CODEX_HOME/agents/`. Every spawn selects one of them by exact name in `agent_type`; describing the role in `message` does not select its TOML, and the LazyCodex spawn guard rejects a spawn without a registered `agent_type`. Spawn: `multi_agent_v1.spawn_agent({"message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"<role>","fork_context":false})`. If your tool list instead has a flat `spawn_agent` with a required `task_name` (`multi_agent_v2`): `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"<role>","fork_turns":"none"})` - finished agents end on their own; `wait_agent` takes only `timeout_ms`. Implementation work selects `lazycodex-worker-low`, `lazycodex-worker-medium`, or `lazycodex-worker-high` by difficulty.
- `explorer` - codebase search
- `librarian` - external docs, OSS code, API contracts
diff --git a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-6.md b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-6.md
index e921103..924219a 100644
--- a/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-6.md
+++ b/plugins/omo/components/rules/bundled-rules/hephaestus/gpt-6.md
@@ -78,7 +78,7 @@ When an approach fails, change something material - an algorithm, library, or pa
## Codex tool and skills notes
-The actual Codex tool list and schemas determine the route. Read-only subagent roles live in `CODEX_HOME/agents/`. For `multi_agent_v1`, use `multi_agent_v1.spawn_agent({"message":"TASK: act as a <role>. GOAL: ... STOP WHEN: ... EVIDENCE: ...","fork_context":false})`. If the tool list instead exposes a flat `spawn_agent` requiring `task_name` (`multi_agent_v2`), use `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: act as a <role>. GOAL: ... STOP WHEN: ... EVIDENCE: ...","fork_turns":"none"})`. Finished agents end on their own; `wait_agent` takes only `timeout_ms`. Keep the two payloads distinct and do not send v1 fields to v2 or vice versa.
+The actual Codex tool list and schemas determine the route. Read-only subagent roles live in `CODEX_HOME/agents/`, and every spawn selects one of them by exact name in `agent_type`; describing the role in `message` does not select its TOML, and the LazyCodex spawn guard rejects a spawn without a registered `agent_type`. For `multi_agent_v1`, use `multi_agent_v1.spawn_agent({"message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"<role>","fork_context":false})`. If the tool list instead exposes a flat `spawn_agent` requiring `task_name` (`multi_agent_v2`), use `spawn_agent({"task_name":"<lowercase_digits_underscores>","message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"<role>","fork_turns":"none"})`. Finished agents end on their own; `wait_agent` takes only `timeout_ms`. Keep the two payloads distinct and do not send v1 fields to v2 or vice versa. Implementation work selects `lazycodex-worker-low`, `lazycodex-worker-medium`, or `lazycodex-worker-high` by difficulty.
- `explorer`: codebase search.
- `librarian`: external docs, OSS code, and API contracts.
diff --git a/plugins/omo/components/rules/test/hephaestus-spawn-examples.test.ts b/plugins/omo/components/rules/test/hephaestus-spawn-examples.test.ts
new file mode 100644
index 0000000..73d2a26
--- /dev/null
+++ b/plugins/omo/components/rules/test/hephaestus-spawn-examples.test.ts
@@ -0,0 +1,53 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+
+// The bundled Hephaestus rules are the only spawn_agent examples a fresh session reads, and the
+// LazyCodex spawn guard (ulw-loop spawn-role-guard) denies any spawn whose tool_input carries no
+// registered agent_type. An example without agent_type teaches the model a call the guard rejects.
+// The role set below mirrors LAZYCODEX_SPAWN_ROLES; the ulw-loop test pins that set to the TOMLs.
+const REGISTERED_ROLES = new Set([
+ "explorer",
+ "lazycodex-clone-fidelity-reviewer",
+ "lazycodex-code-reviewer",
+ "lazycodex-gate-reviewer",
+ "lazycodex-qa-executor",
+ "lazycodex-worker-high",
+ "lazycodex-worker-low",
+ "lazycodex-worker-medium",
+ "librarian",
+ "metis",
+ "momus",
+ "plan",
+]);
+const VARIANTS = ["gpt-5.5.md", "gpt-5.6.md", "gpt-6.md"] as const;
+const EXAMPLE = /spawn_agent\((\{[^)]*\})\)/g;
+
+function spawnExamples(variant: string): Record<string, unknown>[] {
+ const text = readFileSync(join(process.cwd(), "bundled-rules", "hephaestus", variant), "utf8");
+ const found: Record<string, unknown>[] = [];
+ for (const match of text.matchAll(EXAMPLE)) {
+ const raw = (match[1] ?? "")
+ .replace(/\.\.\./g, "x")
+ .replace(/<role>/g, "explorer")
+ .replace(/<lowercase_digits_underscores>/g, "task");
+ found.push(JSON.parse(raw) as Record<string, unknown>);
+ }
+ return found;
+}
+
+describe("Hephaestus bundled rule spawn examples", () => {
+ for (const variant of VARIANTS) {
+ it(`#given the ${variant} variant #when its spawn_agent examples are read as tool_input #then each names a registered agent_type and no full-history fork`, () => {
+ const examples = spawnExamples(variant);
+ expect(examples.length).toBeGreaterThanOrEqual(2);
+ for (const example of examples) {
+ const role = example["agent_type"];
+ expect(typeof role).toBe("string");
+ expect(REGISTERED_ROLES.has(role as string)).toBe(true);
+ if ("fork_context" in example) expect(example["fork_context"]).toBe(false);
+ if ("fork_turns" in example) expect(example["fork_turns"]).toBe("none");
+ }
+ });
+ }
+});
Verification
- RED, new test on unpatched rules (
vitest --run test/hephaestus-spawn-examples.test.ts): Tests 3 failed (3), each at expect(typeof role).toBe("string") with Received: "undefined".
- GREEN, same test on patched rules:
Tests 3 passed (3).
- Guard replay on the patched examples: all six
admitted ({"message":"TASK: x","agent_type":"explorer","fork_context":false} and the V2 shape with fork_turns:"none").
- Manual QA: the same session that was denied above spawned
multi_agent_v1.spawn_agent({"message":"TASK: ...","agent_type":"lazycodex-gate-reviewer","fork_context":false}) and the hook admitted it.
- Not run here: the sibling suites
hephaestus-model-variant and windows-git-bash-bundled-rule need the @oh-my-opencode/rules-engine workspace link that the mirror does not carry; the diff does not touch the discovery paths they read.
This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated
Problem Situation
A Codex session whose only spawn guidance is the bundled Hephaestus rule (
components/rules/bundled-rules/hephaestus/gpt-5.5.md, injected by the rules hook at SessionStart) copies the rule's ownspawn_agentexample, and the LazyCodex spawn guard added in 5.0.0-beta.61 (components/ulw-loop/src/spawn-role-guard.ts) denies it. All six examples across the three variants (gpt-5.5.md,gpt-5.6.md,gpt-6.md) spell the call as{"message":"TASK: act as a <role>. ...","fork_context":false}with noagent_type, which is exactly the shape the guard rejects ("Describing a role in message does not select its TOML"). A long orchestration session that had been spawning workers this way for two days on beta.53 lost every spawn the moment beta.62 loaded, mid-lap.The
ultraworkandulw-executeskills already say every spawn must pass an exactagent_type; the bundled rule is the one document that still teaches the denied shape, and it is the one every session receives without asking for a skill.Reproduction Logs
Live denial, Codex CLI with omo 5.0.0-beta.62,
multi_agent_v1surface, a spawn written from the bundled rule's example:The rule's examples fed straight into the shipped guard (
spawnRoleDenialfromcomponents/ulw-loop/dist/spawn-role-guard.js, placeholders filled withexplorer/task):Root Cause
spawnRoleDenialadmits a spawn only whentool_input.agent_typeis one ofLAZYCODEX_SPAWN_ROLES; it cannot see the tool schema, so it fails closed on any unnamed call. The bundled Hephaestus rule predates that guard (its spawn line last moved in7124986/ba0383d, July, in oh-my-openagent) and still shows the role inmessagewith noagent_type, so a model following the rule verbatim produces a call the same plugin denies. The two documents shipped in one plugin disagree, and the guard wins.Verified Fix
Rewrite the spawn sentence in the three variants so both examples carry
"agent_type":"<role>", state that the role is selected byagent_typeand that the guard rejects an unnamed spawn, and point implementation work at the three worker roles; the role bullets, GOAL/STOP WHEN/EVIDENCE labels and V1/V2 distinction are untouched. A test incomponents/rules/testparses everyspawn_agent({...})example in the three files astool_inputand requires a registeredagent_typeand no full-history fork, so the rule cannot drift away from the guard again. The role set in the test mirrorsLAZYCODEX_SPAWN_ROLES; the ulw-loop matrix test already pins that set to the installed TOMLs.Patch against
e6be07d(chore: sync Codex marketplace v5.0.0-beta.62); in the source tree the same files live underpackages/omo-codex/plugin/components/rules/.Verification
vitest --run test/hephaestus-spawn-examples.test.ts):Tests 3 failed (3), each atexpect(typeof role).toBe("string")withReceived: "undefined".Tests 3 passed (3).admitted({"message":"TASK: x","agent_type":"explorer","fork_context":false}and the V2 shape withfork_turns:"none").multi_agent_v1.spawn_agent({"message":"TASK: ...","agent_type":"lazycodex-gate-reviewer","fork_context":false})and the hook admitted it.hephaestus-model-variantandwindows-git-bash-bundled-ruleneed the@oh-my-opencode/rules-engineworkspace link that the mirror does not carry; the diff does not touch the discovery paths they read.This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated