Skip to content

Commit ece94ff

Browse files
tildesrcPanopticon Agentclaude
authored
Switch codex skills from deprecated custom prompts to ~/.agents/skills SKILL.md (#390)
Custom prompts (~/.codex/prompts/) are deprecated upstream and are only user-typed slash commands — the model never discovers them, so a codex task cannot self-advance its workflow. Switch to codex's model-discoverable skills mechanism: ~/.agents/skills/<name>/SKILL.md (user scope, so nothing reaches the working tree). Adds write_agent_skills / write_agent_operation_skills to container/skills.py with the name+description frontmatter the skills surface requires; updates CodexAgentCLI to call them; removes the now-unused PROMPTS_SUBDIR; updates test_codex.py path pins. Co-authored-by: Panopticon Agent <agent@panopticon.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4b61825 commit ece94ff

3 files changed

Lines changed: 91 additions & 31 deletions

File tree

src/panopticon/container/cli/codex.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Codex satisfies the same seams as claude against its own surface (ADR 0014 §5 mapping table):
44
55
- **config dir** ``~/.codex`` (``CODEX_HOME``), config file ``config.toml``;
6-
- **skills / operations** → custom prompts under ``~/.codex/prompts/<name>.md`` (same
7-
``---\\ndescription: …\\n---`` frontmatter claude uses, so the body renderers are shared);
6+
- **skills / operations** → ``~/.agents/skills/<name>/SKILL.md`` (codex's model-discoverable
7+
skills mechanism; user scope so nothing reaches the task's working tree; ``---\\nname: …\\n
8+
description: …\\n---`` frontmatter, body renderers shared with claude);
89
- **MCP** → a ``[mcp_servers.panopticon]`` table in ``config.toml`` over streamable **HTTP** (ADR
910
flag 1: codex supports remote HTTP MCP; older builds need ``experimental_use_rmcp_client``);
1011
- **workflow overview** → ``$CODEX_HOME/AGENTS.md`` (our config dir — *never* the repo's
@@ -37,7 +38,7 @@
3738
from panopticon.container.cli.base import AgentCLI, _Client
3839
from panopticon.container.config import update_toml_config
3940
from panopticon.container.hooks import HOOK_COMMAND
40-
from panopticon.container.skills import write_commands, write_operation_commands
41+
from panopticon.container.skills import write_agent_operation_skills, write_agent_skills
4142
from panopticon.core.models import Skill
4243

4344
#: The control plane's abstract model **tiers** mapped to codex's concrete model ids (ADR 0014 §3a).
@@ -67,23 +68,19 @@ class CodexAgentCLI(AgentCLI):
6768
#: codex's single config file, under the config dir. MCP, trust, and the unattended posture all
6869
#: merge into it (each adapter method touches only its own keys, via :func:`update_toml_config`).
6970
CONFIG_FILE: ClassVar[str] = "config.toml"
70-
#: Where the skill/operation custom prompts go, relative to the config home.
71-
PROMPTS_SUBDIR: ClassVar[tuple[str, ...]] = (".codex", "prompts")
7271
#: The workflow overview file inside the config dir — ``$CODEX_HOME/AGENTS.md`` (ADR 0014 §5).
7372
WORKFLOW_OVERVIEW_FILE: ClassVar[str] = "AGENTS.md"
7473
#: Session transcripts live here under the config dir; their presence means "resume" (§ launch).
7574
SESSIONS_DIRNAME: ClassVar[str] = "sessions"
7675

7776
def render_skills(self, client: _Client, task_id: str, home: Path) -> list[Path]:
78-
"""Render the workflow's skills to ``~/.codex/prompts/`` (codex's custom-prompt surface)."""
77+
"""Render the workflow's skills to ``~/.agents/skills/`` (codex's model-discoverable surface)."""
7978
skills = [Skill(**s) for s in client.list_skills(task_id)]
80-
return write_commands(skills, home, task_id, self.PROMPTS_SUBDIR)
79+
return write_agent_skills(skills, home, task_id)
8180

8281
def render_operations(self, client: _Client, task_id: str, home: Path) -> list[Path]:
83-
"""Render the workflow's declared core operations (advance/drop/…) as codex custom prompts."""
84-
return write_operation_commands(
85-
client.list_operations(task_id), home, task_id, self.PROMPTS_SUBDIR
86-
)
82+
"""Render the workflow's declared core operations (advance/drop/…) as codex agent skills."""
83+
return write_agent_operation_skills(client.list_operations(task_id), home, task_id)
8784

8885
def write_settings(self, home: Path) -> Path:
8986
"""Wire codex's turn-flip hooks into ``config.toml``; return the path (ADR 0014 §5, M3.6).

src/panopticon/container/skills.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Render a workflow's :class:`~panopticon.core.models.Skill` specs to an agent CLI's command surface.
22
3-
The Skill spec is agent-CLI-agnostic (core, ADR 0004); the rendered **body** (frontmatter + the
4-
agent procedure) is CLI-agnostic too — claude and codex both read a ``---\\ndescription: …\\n---``
5-
markdown file. Only the destination dir differs: claude's ``.claude/commands/<name>.md`` slash-command
6-
vs codex's ``.codex/prompts/<name>.md`` custom prompt. So the writers take the ``subdir`` under the
7-
config home (defaulting to claude's), letting both adapters share the text. Pure — no LLM; it just
8-
writes files. The in-container harness fetches the active workflow's skills (over REST) and renders
9-
them before launching the agent (Slice 6c).
3+
The Skill spec is agent-CLI-agnostic (core, ADR 0004). The rendered body (frontmatter + the agent
4+
procedure) is shared across CLIs; only the destination and frontmatter format differ:
5+
6+
- **claude** — ``.claude/commands/<name>.md``, ``---\\ndescription: …\\n---`` frontmatter.
7+
- **codex** — ``~/.agents/skills/<name>/SKILL.md``, ``---\\nname: …\\ndescription: …\\n---``
8+
frontmatter (codex's model-discoverable skills mechanism; written to user scope so nothing reaches
9+
the task's working tree).
10+
11+
Pure — no LLM; it just writes files. The in-container harness fetches the active workflow's skills
12+
(over REST) and renders them before launching the agent (Slice 6c).
1013
"""
1114

1215
from __future__ import annotations
@@ -16,8 +19,7 @@
1619

1720
from panopticon.core.models import Skill
1821

19-
#: The default destination (relative to the config home) — claude's slash-command dir. Codex passes
20-
#: its own (``(".codex", "prompts")``).
22+
#: The default destination (relative to the config home) — claude's slash-command dir.
2123
CLAUDE_COMMANDS_SUBDIR: tuple[str, ...] = (".claude", "commands")
2224

2325

@@ -81,9 +83,7 @@ def write_operation_commands(
8183
task_id: str,
8284
subdir: Sequence[str] = CLAUDE_COMMANDS_SUBDIR,
8385
) -> list[Path]:
84-
"""Write each core operation (verb → target state) to ``<root>/<subdir>/<verb>.md``.
85-
86-
``subdir`` defaults to claude's ``.claude/commands``; codex passes ``(".codex", "prompts")``."""
86+
"""Write each core operation (verb → target state) to ``<root>/<subdir>/<verb>.md``."""
8787
commands_dir = root.joinpath(*subdir)
8888
commands_dir.mkdir(parents=True, exist_ok=True)
8989
written = []
@@ -92,3 +92,59 @@ def write_operation_commands(
9292
path.write_text(render_operation(name, target_state, task_id))
9393
written.append(path)
9494
return written
95+
96+
97+
# -- codex skills surface (model-discoverable; ~/.agents/skills/<name>/SKILL.md) ----------------
98+
99+
100+
def render_agent_skill(skill: Skill, task_id: str) -> str:
101+
"""The rendered ``SKILL.md`` body for a skill on the codex agent-skills surface.
102+
103+
Adds ``name:`` to the frontmatter (required by the skills mechanism) alongside ``description:``.
104+
The instructions body and task-id note are the same as :func:`render_command`.
105+
"""
106+
return (
107+
f"---\nname: {skill.name}\ndescription: {skill.description}\n---\n"
108+
f"{skill.instructions}\n{_task_id_note(task_id)}"
109+
)
110+
111+
112+
def render_agent_operation(name: str, target_state: str, task_id: str) -> str:
113+
"""The rendered ``SKILL.md`` body for a core operation on the codex agent-skills surface."""
114+
return (
115+
f"---\nname: {name}\ndescription: Apply the workflow's '{name}' operation.\n---\n"
116+
f"Apply this workflow's `{name}` operation — it moves the task to **{target_state}**. "
117+
f'Invoke it with the `apply_operation` tool (`operation="{name}"`, `task_id="{task_id}"`); '
118+
f"don't edit the state directly. It's gated on the current state's responsibilities and "
119+
f"starts a new turn.\n"
120+
)
121+
122+
123+
def write_agent_skills(skills: Iterable[Skill], root: Path, task_id: str) -> list[Path]:
124+
"""Write each skill to ``<root>/.agents/skills/<name>/SKILL.md``; return the paths written.
125+
126+
Uses codex's model-discoverable skills mechanism. Written to user scope (``<root>`` is
127+
``~``), so nothing reaches the task's working tree.
128+
"""
129+
written = []
130+
for skill in skills:
131+
skill_dir = root / ".agents" / "skills" / skill.name
132+
skill_dir.mkdir(parents=True, exist_ok=True)
133+
path = skill_dir / "SKILL.md"
134+
path.write_text(render_agent_skill(skill, task_id))
135+
written.append(path)
136+
return written
137+
138+
139+
def write_agent_operation_skills(
140+
operations: Mapping[str, str], root: Path, task_id: str
141+
) -> list[Path]:
142+
"""Write each core operation to ``<root>/.agents/skills/<name>/SKILL.md``."""
143+
written = []
144+
for name, target_state in operations.items():
145+
skill_dir = root / ".agents" / "skills" / name
146+
skill_dir.mkdir(parents=True, exist_ok=True)
147+
path = skill_dir / "SKILL.md"
148+
path.write_text(render_agent_operation(name, target_state, task_id))
149+
written.append(path)
150+
return written

tests/container/test_codex.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,32 @@ def _load_config(cli: CodexAgentCLI, config_dir: Path) -> dict[str, object]:
3232
# -- skills + operations → custom prompts -------------------------------------------------------
3333

3434

35-
def test_render_skills_writes_prompt_files(tmp_path: Path) -> None:
35+
def test_render_skills_writes_skill_files(tmp_path: Path) -> None:
3636
client = _FakeClient(
3737
[{"name": "babysit-ci", "description": "Watch CI.", "instructions": "loop"}]
3838
)
3939
CodexAgentCLI().render_skills(client, "t1", tmp_path) # type: ignore[arg-type]
40-
body = (tmp_path / ".codex" / "prompts" / "babysit-ci.md").read_text()
41-
# Same frontmatter format codex + claude both read; the task id is injected for MCP calls.
42-
assert body.startswith("---\ndescription: Watch CI.")
40+
# Model-discoverable skills surface; user scope keeps the working tree clean.
41+
path = tmp_path / ".agents" / "skills" / "babysit-ci" / "SKILL.md"
42+
assert path.exists(), f"expected SKILL.md at {path}"
43+
body = path.read_text()
44+
assert body.startswith("---\nname: babysit-ci\ndescription: Watch CI.")
4345
assert 'task_id="t1"' in body
46+
# Old custom-prompt surface must not be written.
47+
assert not (tmp_path / ".codex" / "prompts").exists()
4448

4549

46-
def test_render_operations_writes_a_prompt_per_operation(tmp_path: Path) -> None:
50+
def test_render_operations_writes_a_skill_per_operation(tmp_path: Path) -> None:
4751
client = _FakeClient([], {"advance": "COMPLETE", "drop": "DROPPED"})
4852
CodexAgentCLI().render_operations(client, "t1", tmp_path) # type: ignore[arg-type]
49-
prompts = tmp_path / ".codex" / "prompts"
50-
assert {p.name for p in prompts.glob("*.md")} == {"advance.md", "drop.md"}
51-
body = (prompts / "advance.md").read_text()
53+
skills_dir = tmp_path / ".agents" / "skills"
54+
assert {p.parent.name for p in skills_dir.rglob("SKILL.md")} == {"advance", "drop"}
55+
body = (skills_dir / "advance" / "SKILL.md").read_text()
56+
assert body.startswith("---\nname: advance\n")
5257
assert "apply_operation" in body and "COMPLETE" in body
5358
assert 'task_id="t1"' in body
59+
# Old custom-prompt surface must not be written.
60+
assert not (tmp_path / ".codex" / "prompts").exists()
5461

5562

5663
# -- MCP config (config.toml [mcp_servers.panopticon]) ------------------------------------------

0 commit comments

Comments
 (0)