code-yeongyu / code-yeongyu/lazycodex

Bundled Hephaestus rule shows spawn_agent examples the LazyCodex spawn guard denies

Open
#164 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
3.5k
Forks
216
PR merge metrics
No merged PRs in 30d

Description

## 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 . ...","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":""`, 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
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 . ...","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":"","message":"TASK: act as a . ...","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":"","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":"","message":"TASK: ...","agent_type":"","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 . 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":"","message":"TASK: act as a . 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":"","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":"","message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"","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 . 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":"","message":"TASK: act as a . 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":"","fork_context":false})`. If the tool list instead exposes a flat `spawn_agent` requiring `task_name` (`multi_agent_v2`), use `spawn_agent({"task_name":"","message":"TASK: ... GOAL: ... STOP WHEN: ... EVIDENCE: ...","agent_type":"","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[] {
+ const text = readFileSync(join(process.cwd(), "bundled-rules", "hephaestus", variant), "utf8");
+ const found: Record[] = [];
+ for (const match of text.matchAll(EXAMPLE)) {
+ const raw = (match[1] ?? "")
+ .replace(/\.\.\./g, "x")
+ .replace(//g, "explorer")
+ .replace(//g, "task");
+ found.push(JSON.parse(raw) as Record);
+ }
+ 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](https://github.com/code-yeongyu/lazycodex).
Tag: lazycodex-generated

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.