nexus-dev-toolkit 3.1.9__tar.gz → 3.1.10__tar.gz

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.
Files changed (45) hide show
  1. {nexus_dev_toolkit-3.1.9/nexus_dev_toolkit.egg-info → nexus_dev_toolkit-3.1.10}/PKG-INFO +18 -2
  2. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/README.md +17 -1
  3. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_cli.py +261 -55
  4. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10/nexus_dev_toolkit.egg-info}/PKG-INFO +18 -2
  5. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/pyproject.toml +1 -1
  6. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tests/test_cli.py +172 -0
  7. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/code-reviewer.md +1 -1
  8. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/graph_backend.py +7 -7
  9. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/code-review.md +1 -1
  10. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/LICENSE +0 -0
  11. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/SOURCES.txt +0 -0
  12. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/dependency_links.txt +0 -0
  13. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/entry_points.txt +0 -0
  14. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/requires.txt +0 -0
  15. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/top_level.txt +0 -0
  16. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_server.py +0 -0
  17. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/setup.cfg +0 -0
  18. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tests/test_server.py +0 -0
  19. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/__init__.py +0 -0
  20. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/__init__.py +0 -0
  21. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/database-reviewer.md +0 -0
  22. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/deployment-reviewer.md +0 -0
  23. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/monitoring-reviewer.md +0 -0
  24. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/performance-reviewer.md +0 -0
  25. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/pr-verifier.md +0 -0
  26. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/__init__.py +0 -0
  27. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/arch_ingest.py +0 -0
  28. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/package_resolver.py +0 -0
  29. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/project_rules.py +0 -0
  30. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/__init__.py +0 -0
  31. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/apply.md +0 -0
  32. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/commit.md +0 -0
  33. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/create-pr.md +0 -0
  34. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/database-review.md +0 -0
  35. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/deployment-review.md +0 -0
  36. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/dev-tasks-planner.md +0 -0
  37. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/epav.md +0 -0
  38. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/evaluate.md +0 -0
  39. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/monitoring-review.md +0 -0
  40. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/performance-review.md +0 -0
  41. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/plan.md +0 -0
  42. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/pr-review.md +0 -0
  43. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/scaffold.md +0 -0
  44. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/validate.md +0 -0
  45. {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/task_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexus-dev-toolkit
3
- Version: 3.1.9
3
+ Version: 3.1.10
4
4
  Summary: Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV
5
5
  Author-email: Ronald dela Cruz <rcdelacruz@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -22,7 +22,7 @@ Dynamic: license-file
22
22
  # nexus-dev-toolkit
23
23
 
24
24
 
25
- Developer workflow toolkit for Claude Code and OpenCode. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
25
+ Developer workflow toolkit for Claude Code, OpenCode and Codex. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
26
26
 
27
27
  **[Documentation](https://nexus.coderstudio.co/docs)**
28
28
 
@@ -89,6 +89,7 @@ uv tool install nexus-dev-toolkit
89
89
  cd my-project
90
90
  nexus init . # Claude Code (default), prompts for graphify/codegraph/none
91
91
  nexus init . --tool opencode # OpenCode
92
+ nexus init . --tool codex # Codex
92
93
  nexus init . --graph-backend codegraph # skip the prompt, use codegraph
93
94
  ```
94
95
 
@@ -174,6 +175,20 @@ knowledge/ ← same structure
174
175
  opencode.json ← MCP server config
175
176
  ```
176
177
 
178
+ **Codex** (`nexus init . --tool codex`):
179
+ ```
180
+ .agents/skills/<name>/
181
+ ├── SKILL.md ← same EPAV + reviewer skills — invoke with $evaluate, $plan, …
182
+ └── agents/openai.yaml ← EPAV steps only: blocks implicit invocation, so Codex never starts $apply on its own
183
+ .codex/
184
+ ├── agents/*.toml ← same reviewer subagents (converted to Codex TOML)
185
+ ├── config.toml ← [mcp_servers.nexus] (appended — your existing config is never rewritten)
186
+ └── hooks.json ← PostToolUse: graphify auto-update (graphify backend only)
187
+ knowledge/ ← same structure
188
+ ```
189
+
190
+ Codex only loads project `.codex/` config and hooks for **trusted** projects — trust the project when Codex asks.
191
+
177
192
  ---
178
193
 
179
194
  ## Commands
@@ -181,6 +196,7 @@ opencode.json ← MCP server config
181
196
  ```bash
182
197
  nexus init . # Claude Code — set up .claude/ + knowledge/ + .mcp.json
183
198
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
199
+ nexus init . --tool codex # Codex — set up .agents/skills/ + .codex/ + knowledge/
184
200
  nexus --version # show installed version
185
201
  nexus update # update the CLI itself; asks whether to also sync in a real terminal
186
202
  nexus update --sync # skip the prompt, always sync the current project's skills/agents too
@@ -1,7 +1,7 @@
1
1
  # nexus-dev-toolkit
2
2
 
3
3
 
4
- Developer workflow toolkit for Claude Code and OpenCode. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
4
+ Developer workflow toolkit for Claude Code, OpenCode and Codex. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
5
5
 
6
6
  **[Documentation](https://nexus.coderstudio.co/docs)**
7
7
 
@@ -68,6 +68,7 @@ uv tool install nexus-dev-toolkit
68
68
  cd my-project
69
69
  nexus init . # Claude Code (default), prompts for graphify/codegraph/none
70
70
  nexus init . --tool opencode # OpenCode
71
+ nexus init . --tool codex # Codex
71
72
  nexus init . --graph-backend codegraph # skip the prompt, use codegraph
72
73
  ```
73
74
 
@@ -153,6 +154,20 @@ knowledge/ ← same structure
153
154
  opencode.json ← MCP server config
154
155
  ```
155
156
 
157
+ **Codex** (`nexus init . --tool codex`):
158
+ ```
159
+ .agents/skills/<name>/
160
+ ├── SKILL.md ← same EPAV + reviewer skills — invoke with $evaluate, $plan, …
161
+ └── agents/openai.yaml ← EPAV steps only: blocks implicit invocation, so Codex never starts $apply on its own
162
+ .codex/
163
+ ├── agents/*.toml ← same reviewer subagents (converted to Codex TOML)
164
+ ├── config.toml ← [mcp_servers.nexus] (appended — your existing config is never rewritten)
165
+ └── hooks.json ← PostToolUse: graphify auto-update (graphify backend only)
166
+ knowledge/ ← same structure
167
+ ```
168
+
169
+ Codex only loads project `.codex/` config and hooks for **trusted** projects — trust the project when Codex asks.
170
+
156
171
  ---
157
172
 
158
173
  ## Commands
@@ -160,6 +175,7 @@ opencode.json ← MCP server config
160
175
  ```bash
161
176
  nexus init . # Claude Code — set up .claude/ + knowledge/ + .mcp.json
162
177
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
178
+ nexus init . --tool codex # Codex — set up .agents/skills/ + .codex/ + knowledge/
163
179
  nexus --version # show installed version
164
180
  nexus update # update the CLI itself; asks whether to also sync in a real terminal
165
181
  nexus update --sync # skip the prompt, always sync the current project's skills/agents too
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import re
2
3
  import shutil
3
4
  import subprocess
4
5
  import sys
@@ -12,7 +13,7 @@ from rich.table import Table
12
13
 
13
14
  from tools.epav import graph_backend
14
15
 
15
- app = typer.Typer(name="nexus", no_args_is_help=False, help="nexus-dev-toolkit — Day 0 scaffold + Day 1 EPAV workflow for Claude Code and OpenCode")
16
+ app = typer.Typer(name="nexus", no_args_is_help=False, help="nexus-dev-toolkit — Day 0 scaffold + Day 1 EPAV workflow for Claude Code, OpenCode and Codex")
16
17
  skill_app = typer.Typer(name="skill", no_args_is_help=True, help="Manage skills in .claude/commands/")
17
18
  rule_app = typer.Typer(name="rule", no_args_is_help=True, help="Manage rules in knowledge/rules/")
18
19
  agent_app = typer.Typer(name="agent", no_args_is_help=True, help="Manage subagents in .claude/agents/")
@@ -22,7 +23,7 @@ app.add_typer(agent_app, name="agent")
22
23
 
23
24
  console = Console()
24
25
 
25
- _VERSION = "3.1.9"
26
+ _VERSION = "3.1.10"
26
27
 
27
28
 
28
29
  def _fetch_latest_pypi_version() -> str | None:
@@ -174,6 +175,37 @@ export const GraphifyPlugin = async ({ directory }) => {
174
175
  """
175
176
 
176
177
 
178
+ # ── Codex ─────────────────────────────────────────────────────────────────────
179
+
180
+ _CODEX_MCP_TOML = """\
181
+ [mcp_servers.nexus]
182
+ command = "uvx"
183
+ args = ["--refresh", "--from", "nexus-dev-toolkit", "nexus-mcp"]
184
+ """
185
+
186
+ _CODEX_HOOKS = {
187
+ "hooks": {
188
+ "PostToolUse": [
189
+ {
190
+ "matcher": "Edit|Write",
191
+ "hooks": [
192
+ {
193
+ "type": "command",
194
+ "command": "graphify update . --force 2>/dev/null || true"
195
+ }
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+ }
201
+
202
+ # EPAV steps are user-driven — Codex must never start /apply or /commit on its own.
203
+ _CODEX_EXPLICIT_ONLY_YAML = """\
204
+ policy:
205
+ allow_implicit_invocation: false
206
+ """
207
+
208
+
177
209
  def _init_project(project_dir: Path, backend: str = "graphify") -> list[str]:
178
210
  """
179
211
  nexus init — sets up:
@@ -297,6 +329,140 @@ def _init_opencode(project_dir: Path, backend: str = "graphify") -> list[str]:
297
329
  return created
298
330
 
299
331
 
332
+ def _split_frontmatter(content: str) -> tuple[dict[str, str], str]:
333
+ """Parse a simple `key: value` frontmatter block. ({}, content) if there isn't one."""
334
+ if not content.startswith("---"):
335
+ return {}, content
336
+ parts = content.split("---", 2)
337
+ if len(parts) < 3:
338
+ return {}, content
339
+ fields = {}
340
+ for line in parts[1].splitlines():
341
+ key, sep, value = line.partition(":")
342
+ if sep:
343
+ fields[key.strip()] = value.strip()
344
+ return fields, parts[2].lstrip("\n")
345
+
346
+
347
+ # "/plan" → "$plan": Codex invokes skills with $, and "/" belongs to its own built-in commands.
348
+ # The lookbehind skips paths like docs/scaffold or ./plan.
349
+ _SKILL_REF_RE = re.compile(r"(?<![\w/.])/(" + "|".join(re.escape(Path(s).stem) for s in _BUILTIN_SKILLS) + r")\b")
350
+
351
+
352
+ def _to_codex_skill(skill_name: str, content: str) -> tuple[str, str | None]:
353
+ """Convert a nexus skill to a Codex SKILL.md. Skills without frontmatter are the user-driven
354
+ EPAV steps — they get generated frontmatter plus an openai.yaml that blocks implicit invocation."""
355
+ content = _SKILL_REF_RE.sub(r"$\1", content)
356
+ fields, body = _split_frontmatter(content)
357
+ if fields.get("name") and fields.get("description"):
358
+ return content, None
359
+ name = Path(skill_name).stem
360
+ description = next(
361
+ (line.replace("**", "").strip() for line in body.splitlines() if line.strip() and not line.startswith("#")),
362
+ f"nexus {name} skill",
363
+ )
364
+ frontmatter = f"---\nname: {name}\ndescription: {json.dumps(description, ensure_ascii=False)}\n---\n\n"
365
+ return frontmatter + body, _CODEX_EXPLICIT_ONLY_YAML
366
+
367
+
368
+ def _to_codex_agent(agent_name: str, content: str) -> str:
369
+ """Convert a nexus subagent (markdown + frontmatter) to a Codex agent TOML. Claude Code-only
370
+ fields (tools, model) are dropped — the agent inherits the parent session's model."""
371
+ fields, body = _split_frontmatter(content)
372
+ name = fields.get("name") or Path(agent_name).stem
373
+ # Multi-line literal string keeps the prompt readable; fall back to an escaped basic string
374
+ # in the unlikely case the body itself contains '''.
375
+ instructions = json.dumps(body, ensure_ascii=False) if "'''" in body else f"'''\n{body}'''"
376
+ return (
377
+ f"name = {json.dumps(name, ensure_ascii=False)}\n"
378
+ f"description = {json.dumps(fields.get('description', ''), ensure_ascii=False)}\n"
379
+ f"developer_instructions = {instructions}\n"
380
+ )
381
+
382
+
383
+ def _write_codex_mcp_config(project_dir: Path) -> bool:
384
+ """Append the nexus MCP block to .codex/config.toml. Text-level only — Python 3.10 has no
385
+ tomllib, and appending never rewrites the user's TOML. True if the block was added."""
386
+ config = project_dir / ".codex" / "config.toml"
387
+ config.parent.mkdir(parents=True, exist_ok=True)
388
+ existing = config.read_text(encoding="utf-8") if config.exists() else ""
389
+ if "[mcp_servers.nexus]" in existing:
390
+ return False
391
+ if existing and not existing.endswith("\n"):
392
+ existing += "\n"
393
+ if existing:
394
+ existing += "\n"
395
+ config.write_text(existing + _CODEX_MCP_TOML, encoding="utf-8")
396
+ return True
397
+
398
+
399
+ def _write_codex_hooks(project_dir: Path) -> bool:
400
+ """Merge the graphify PostToolUse hook into .codex/hooks.json. True if it was added."""
401
+ hooks_path = project_dir / ".codex" / "hooks.json"
402
+ hooks_path.parent.mkdir(parents=True, exist_ok=True)
403
+ existing: dict = {}
404
+ if hooks_path.exists():
405
+ try:
406
+ existing = json.loads(hooks_path.read_text())
407
+ except Exception:
408
+ return False # never clobber a hooks file we can't parse
409
+ if "graphify update" in json.dumps(existing):
410
+ return False
411
+ existing.setdefault("hooks", {}).setdefault("PostToolUse", []).extend(_CODEX_HOOKS["hooks"]["PostToolUse"])
412
+ hooks_path.write_text(json.dumps(existing, indent=2))
413
+ return True
414
+
415
+
416
+ def _init_codex(project_dir: Path, backend: str = "graphify") -> list[str]:
417
+ """
418
+ nexus init --tool codex — sets up:
419
+ .agents/skills/<name>/ ← built-in skills (SKILL.md, plus agents/openai.yaml for EPAV steps)
420
+ .codex/agents/ ← built-in subagents as TOML
421
+ .codex/config.toml ← MCP server config (appended, never rewritten)
422
+ .codex/hooks.json ← graphify PostToolUse hook (only when backend == "graphify")
423
+ knowledge/ ← empty scaffold
424
+ """
425
+ created = []
426
+
427
+ # .agents/skills/<name>/SKILL.md — Codex's project skill location
428
+ skills_dir = project_dir / ".agents" / "skills"
429
+ for skill_name in _BUILTIN_SKILLS:
430
+ src = _SKILLS_SRC / skill_name
431
+ dest_dir = skills_dir / Path(skill_name).stem
432
+ dest = dest_dir / "SKILL.md"
433
+ if src.exists() and not dest.exists():
434
+ skill_md, openai_yaml = _to_codex_skill(skill_name, src.read_text(encoding="utf-8"))
435
+ dest_dir.mkdir(parents=True, exist_ok=True)
436
+ dest.write_text(skill_md, encoding="utf-8")
437
+ if openai_yaml:
438
+ (dest_dir / "agents").mkdir(exist_ok=True)
439
+ (dest_dir / "agents" / "openai.yaml").write_text(openai_yaml, encoding="utf-8")
440
+ created.append(f".agents/skills/{dest_dir.name}/SKILL.md")
441
+
442
+ # .codex/agents/<name>.toml — built-in subagents converted to Codex's TOML format
443
+ agents_dir = project_dir / ".codex" / "agents"
444
+ agents_dir.mkdir(parents=True, exist_ok=True)
445
+ for agent_name in _BUILTIN_AGENTS:
446
+ src = _AGENTS_SRC / agent_name
447
+ dest = agents_dir / f"{Path(agent_name).stem}.toml"
448
+ if src.exists() and not dest.exists():
449
+ dest.write_text(_to_codex_agent(agent_name, src.read_text(encoding="utf-8")), encoding="utf-8")
450
+ created.append(f".codex/agents/{dest.name}")
451
+
452
+ if _write_codex_mcp_config(project_dir):
453
+ created.append(".codex/config.toml")
454
+
455
+ # .codex/hooks.json — graphify hook (codegraph syncs itself, no hook needed)
456
+ if backend == "graphify" and _write_codex_hooks(project_dir):
457
+ created.append(".codex/hooks.json")
458
+
459
+ # knowledge/ scaffold
460
+ for d in _KNOWLEDGE_DIRS:
461
+ (project_dir / d).mkdir(parents=True, exist_ok=True)
462
+
463
+ return created
464
+
465
+
300
466
  def _write_mcp_config(project_dir: Path) -> str:
301
467
  mcp_path = project_dir / ".mcp.json"
302
468
  existing: dict = {}
@@ -312,7 +478,7 @@ def _write_mcp_config(project_dir: Path) -> str:
312
478
 
313
479
  # ── Commands ──────────────────────────────────────────────────────────────────
314
480
 
315
- _SUPPORTED_TOOLS = ["claude", "opencode"]
481
+ _SUPPORTED_TOOLS = ["claude", "opencode", "codex"]
316
482
 
317
483
  _TOOL_CHECK = {
318
484
  "claude": {
@@ -327,6 +493,12 @@ _TOOL_CHECK = {
327
493
  "install": "curl -fsSL https://opencode.ai/install | bash",
328
494
  "install_cmd": None, # pipe install — run via shell
329
495
  },
496
+ "codex": {
497
+ "bin": "codex",
498
+ "name": "Codex",
499
+ "install": "npm install -g @openai/codex",
500
+ "install_cmd": ["npm", "install", "-g", "@openai/codex"],
501
+ },
330
502
  }
331
503
 
332
504
 
@@ -409,6 +581,14 @@ def init(
409
581
  console.print(" [yellow]·[/yellow] Already initialized — nothing to do")
410
582
  return
411
583
  console.print(f"\n [bold green]Done.[/bold green] Open [bold]{root}[/bold] in OpenCode and type [cyan]/scaffold[/cyan]\n")
584
+ elif tool == "codex":
585
+ created = _init_codex(root, backend)
586
+ for f in created:
587
+ console.print(f" [green]✓[/green] {f}")
588
+ if not created:
589
+ console.print(" [yellow]·[/yellow] Already initialized — nothing to do")
590
+ return
591
+ console.print(f"\n [bold green]Done.[/bold green] Run [bold]codex[/bold] in [bold]{root}[/bold], trust the project when asked (.codex/ config and hooks only load in trusted projects), and type [cyan]$scaffold[/cyan]\n")
412
592
  else:
413
593
  created = _init_project(root, backend)
414
594
  for f in created:
@@ -481,13 +661,24 @@ def update(
481
661
  console.print()
482
662
 
483
663
 
664
+ def _sync_file(table: Table, dst: Path, text: str, label: str) -> None:
665
+ """Write text to dst unless it's already identical, and record the result in table."""
666
+ dst.parent.mkdir(parents=True, exist_ok=True)
667
+ if dst.exists() and dst.read_text(encoding="utf-8") == text:
668
+ table.add_row(label, "[dim green]ok[/dim green]")
669
+ else:
670
+ dst.write_text(text, encoding="utf-8")
671
+ table.add_row(label, "[cyan]updated[/cyan]")
672
+
673
+
484
674
  def _sync_project(root: Path) -> Table | None:
485
- """Sync built-in skills/agents into root. None if root isn't a nexus project (neither
486
- .claude/ nor .opencode/ present) — callers decide how to handle that case."""
675
+ """Sync built-in skills/agents into root. None if root isn't a nexus project (none of
676
+ .claude/, .opencode/ or .codex/ present) — callers decide how to handle that case."""
487
677
  has_claude = (root / ".claude").is_dir()
488
678
  has_opencode = (root / ".opencode").is_dir()
679
+ has_codex = (root / ".codex").is_dir()
489
680
 
490
- if not has_claude and not has_opencode:
681
+ if not has_claude and not has_opencode and not has_codex:
491
682
  return None
492
683
 
493
684
  table = Table(show_header=True, header_style="dim")
@@ -495,68 +686,52 @@ def _sync_project(root: Path) -> Table | None:
495
686
  table.add_column("Status")
496
687
 
497
688
  if has_claude:
498
- (root / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
499
- (root / ".claude" / "agents").mkdir(parents=True, exist_ok=True)
500
-
501
689
  for fname in _BUILTIN_SKILLS:
502
690
  src = _SKILLS_SRC / fname
503
- if not src.exists():
504
- continue
505
- dst = root / ".claude" / "commands" / fname
506
- src_text = src.read_text(encoding="utf-8")
507
- if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
508
- table.add_row(f".claude/commands/{fname}", "[dim green]ok[/dim green]")
509
- else:
510
- dst.write_text(src_text, encoding="utf-8")
511
- table.add_row(f".claude/commands/{fname}", "[cyan]updated[/cyan]")
691
+ if src.exists():
692
+ _sync_file(table, root / ".claude" / "commands" / fname, src.read_text(encoding="utf-8"), f".claude/commands/{fname}")
512
693
 
513
694
  for fname in _BUILTIN_AGENTS:
514
695
  src = _AGENTS_SRC / fname
515
- if not src.exists():
516
- continue
517
- dst = root / ".claude" / "agents" / fname
518
- src_text = src.read_text(encoding="utf-8")
519
- if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
520
- table.add_row(f".claude/agents/{fname}", "[dim green]ok[/dim green]")
521
- else:
522
- dst.write_text(src_text, encoding="utf-8")
523
- table.add_row(f".claude/agents/{fname}", "[cyan]updated[/cyan]")
696
+ if src.exists():
697
+ _sync_file(table, root / ".claude" / "agents" / fname, src.read_text(encoding="utf-8"), f".claude/agents/{fname}")
524
698
 
525
699
  if has_opencode:
526
- (root / ".opencode" / "commands").mkdir(parents=True, exist_ok=True)
527
- (root / ".opencode" / "agents").mkdir(parents=True, exist_ok=True)
528
- (root / ".opencode" / "plugins").mkdir(parents=True, exist_ok=True)
700
+ for fname in _BUILTIN_SKILLS:
701
+ src = _SKILLS_SRC / fname
702
+ if src.exists():
703
+ _sync_file(table, root / ".opencode" / "commands" / fname, src.read_text(encoding="utf-8"), f".opencode/commands/{fname}")
704
+
705
+ for fname in _BUILTIN_AGENTS:
706
+ src = _AGENTS_SRC / fname
707
+ if src.exists():
708
+ text = _strip_claude_frontmatter(src.read_text(encoding="utf-8"))
709
+ _sync_file(table, root / ".opencode" / "agents" / fname, text, f".opencode/agents/{fname}")
710
+
711
+ _sync_file(table, root / ".opencode" / "plugins" / "graphify.js", _OPENCODE_GRAPHIFY_PLUGIN, ".opencode/plugins/graphify.js")
529
712
 
713
+ if has_codex:
530
714
  for fname in _BUILTIN_SKILLS:
531
715
  src = _SKILLS_SRC / fname
532
716
  if not src.exists():
533
717
  continue
534
- dst = root / ".opencode" / "commands" / fname
535
- src_text = src.read_text(encoding="utf-8")
536
- if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
537
- table.add_row(f".opencode/commands/{fname}", "[dim green]ok[/dim green]")
538
- else:
539
- dst.write_text(src_text, encoding="utf-8")
540
- table.add_row(f".opencode/commands/{fname}", "[cyan]updated[/cyan]")
718
+ stem = Path(fname).stem
719
+ skill_md, openai_yaml = _to_codex_skill(fname, src.read_text(encoding="utf-8"))
720
+ _sync_file(table, root / ".agents" / "skills" / stem / "SKILL.md", skill_md, f".agents/skills/{stem}/SKILL.md")
721
+ if openai_yaml:
722
+ _sync_file(table, root / ".agents" / "skills" / stem / "agents" / "openai.yaml", openai_yaml, f".agents/skills/{stem}/agents/openai.yaml")
541
723
 
542
724
  for fname in _BUILTIN_AGENTS:
543
725
  src = _AGENTS_SRC / fname
544
- if not src.exists():
545
- continue
546
- dst = root / ".opencode" / "agents" / fname
547
- src_text = _strip_claude_frontmatter(src.read_text(encoding="utf-8"))
548
- if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
549
- table.add_row(f".opencode/agents/{fname}", "[dim green]ok[/dim green]")
550
- else:
551
- dst.write_text(src_text, encoding="utf-8")
552
- table.add_row(f".opencode/agents/{fname}", "[cyan]updated[/cyan]")
553
-
554
- plugin_dst = root / ".opencode" / "plugins" / "graphify.js"
555
- if plugin_dst.exists() and plugin_dst.read_text(encoding="utf-8") == _OPENCODE_GRAPHIFY_PLUGIN:
556
- table.add_row(".opencode/plugins/graphify.js", "[dim green]ok[/dim green]")
557
- else:
558
- plugin_dst.write_text(_OPENCODE_GRAPHIFY_PLUGIN, encoding="utf-8")
559
- table.add_row(".opencode/plugins/graphify.js", "[cyan]updated[/cyan]")
726
+ if src.exists():
727
+ stem = Path(fname).stem
728
+ text = _to_codex_agent(fname, src.read_text(encoding="utf-8"))
729
+ _sync_file(table, root / ".codex" / "agents" / f"{stem}.toml", text, f".codex/agents/{stem}.toml")
730
+
731
+ # Only re-add the graphify hook where the project already opted into one.
732
+ if (root / ".codex" / "hooks.json").exists():
733
+ added = _write_codex_hooks(root)
734
+ table.add_row(".codex/hooks.json", "[cyan]updated[/cyan]" if added else "[dim green]ok[/dim green]")
560
735
 
561
736
  return table
562
737
 
@@ -612,16 +787,20 @@ def doctor(
612
787
  # ── Tool ─────────────────────────────────────────────────────────────────
613
788
  has_claude_bin = bool(shutil.which("claude"))
614
789
  has_opencode_bin = bool(shutil.which("opencode"))
790
+ has_codex_bin = bool(shutil.which("codex"))
615
791
  table.add_row("Claude Code", ok("installed") if has_claude_bin else warn("not found — install: npm install -g @anthropic-ai/claude-code"))
616
792
  table.add_row("OpenCode", ok("installed") if has_opencode_bin else warn("not found — install: curl -fsSL https://opencode.ai/install | bash"))
793
+ table.add_row("Codex", ok("installed") if has_codex_bin else warn("not found — install: npm install -g @openai/codex"))
617
794
 
618
795
  # ── Project ──────────────────────────────────────────────────────────────
619
796
  has_claude_dir = (root / ".claude").is_dir()
620
797
  has_opencode_dir = (root / ".opencode").is_dir()
621
- initialized = has_claude_dir or has_opencode_dir
798
+ has_codex_dir = (root / ".codex").is_dir()
799
+ initialized = has_claude_dir or has_opencode_dir or has_codex_dir
800
+ tools_found = ['claude' if has_claude_dir else '', 'opencode' if has_opencode_dir else '', 'codex' if has_codex_dir else '']
622
801
  table.add_row(
623
802
  "nexus initialized",
624
- ok(f"yes ({', '.join(filter(None, ['claude' if has_claude_dir else '', 'opencode' if has_opencode_dir else '']))})")
803
+ ok(f"yes ({', '.join(filter(None, tools_found))})")
625
804
  if initialized else fail("no — run nexus init")
626
805
  )
627
806
 
@@ -655,6 +834,16 @@ def doctor(
655
834
  except Exception:
656
835
  table.add_row("opencode.json", warn("invalid JSON"))
657
836
 
837
+ # MCP config — Codex (text check: no tomllib on Python 3.10)
838
+ if has_codex_dir:
839
+ codex_config = root / ".codex" / "config.toml"
840
+ if not codex_config.exists():
841
+ table.add_row(".codex/config.toml", fail("missing"))
842
+ elif "[mcp_servers.nexus]" in codex_config.read_text(encoding="utf-8"):
843
+ table.add_row(".codex/config.toml", ok("nexus entry present"))
844
+ else:
845
+ table.add_row(".codex/config.toml", warn("exists but no [mcp_servers.nexus] entry"))
846
+
658
847
  # ── Skills & Agents ──────────────────────────────────────────────────────
659
848
  if has_claude_dir:
660
849
  cmd_dir = root / ".claude" / "commands"
@@ -690,6 +879,23 @@ def doctor(
690
879
  else:
691
880
  table.add_row("Built-in agents (OpenCode)", warn(f"{missing_oc_ag} missing — run nexus sync"))
692
881
 
882
+ if has_codex_dir:
883
+ cx_skills_dir = root / ".agents" / "skills"
884
+ present_cx = [f for f in _BUILTIN_SKILLS if (cx_skills_dir / Path(f).stem / "SKILL.md").exists()]
885
+ missing_cx = len(_BUILTIN_SKILLS) - len(present_cx)
886
+ if missing_cx == 0:
887
+ table.add_row("Built-in skills (Codex)", ok(f"all {len(_BUILTIN_SKILLS)} present"))
888
+ else:
889
+ table.add_row("Built-in skills (Codex)", warn(f"{missing_cx} missing — run nexus sync"))
890
+
891
+ cx_ag_dir = root / ".codex" / "agents"
892
+ present_cx_ag = [f for f in _BUILTIN_AGENTS if (cx_ag_dir / f"{Path(f).stem}.toml").exists()]
893
+ missing_cx_ag = len(_BUILTIN_AGENTS) - len(present_cx_ag)
894
+ if missing_cx_ag == 0:
895
+ table.add_row("Built-in agents (Codex)", ok(f"all {len(_BUILTIN_AGENTS)} present"))
896
+ else:
897
+ table.add_row("Built-in agents (Codex)", warn(f"{missing_cx_ag} missing — run nexus sync"))
898
+
693
899
  # ── Knowledge ────────────────────────────────────────────────────────────
694
900
  missing_dirs = [d for d in _KNOWLEDGE_DIRS if not (root / d).is_dir()]
695
901
  if not missing_dirs:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexus-dev-toolkit
3
- Version: 3.1.9
3
+ Version: 3.1.10
4
4
  Summary: Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV
5
5
  Author-email: Ronald dela Cruz <rcdelacruz@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -22,7 +22,7 @@ Dynamic: license-file
22
22
  # nexus-dev-toolkit
23
23
 
24
24
 
25
- Developer workflow toolkit for Claude Code and OpenCode. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
25
+ Developer workflow toolkit for Claude Code, OpenCode and Codex. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
26
26
 
27
27
  **[Documentation](https://nexus.coderstudio.co/docs)**
28
28
 
@@ -89,6 +89,7 @@ uv tool install nexus-dev-toolkit
89
89
  cd my-project
90
90
  nexus init . # Claude Code (default), prompts for graphify/codegraph/none
91
91
  nexus init . --tool opencode # OpenCode
92
+ nexus init . --tool codex # Codex
92
93
  nexus init . --graph-backend codegraph # skip the prompt, use codegraph
93
94
  ```
94
95
 
@@ -174,6 +175,20 @@ knowledge/ ← same structure
174
175
  opencode.json ← MCP server config
175
176
  ```
176
177
 
178
+ **Codex** (`nexus init . --tool codex`):
179
+ ```
180
+ .agents/skills/<name>/
181
+ ├── SKILL.md ← same EPAV + reviewer skills — invoke with $evaluate, $plan, …
182
+ └── agents/openai.yaml ← EPAV steps only: blocks implicit invocation, so Codex never starts $apply on its own
183
+ .codex/
184
+ ├── agents/*.toml ← same reviewer subagents (converted to Codex TOML)
185
+ ├── config.toml ← [mcp_servers.nexus] (appended — your existing config is never rewritten)
186
+ └── hooks.json ← PostToolUse: graphify auto-update (graphify backend only)
187
+ knowledge/ ← same structure
188
+ ```
189
+
190
+ Codex only loads project `.codex/` config and hooks for **trusted** projects — trust the project when Codex asks.
191
+
177
192
  ---
178
193
 
179
194
  ## Commands
@@ -181,6 +196,7 @@ opencode.json ← MCP server config
181
196
  ```bash
182
197
  nexus init . # Claude Code — set up .claude/ + knowledge/ + .mcp.json
183
198
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
199
+ nexus init . --tool codex # Codex — set up .agents/skills/ + .codex/ + knowledge/
184
200
  nexus --version # show installed version
185
201
  nexus update # update the CLI itself; asks whether to also sync in a real terminal
186
202
  nexus update --sync # skip the prompt, always sync the current project's skills/agents too
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nexus-dev-toolkit"
3
- version = "3.1.9"
3
+ version = "3.1.10"
4
4
  description = "Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV"
5
5
  authors = [
6
6
  {name = "Ronald dela Cruz", email = "rcdelacruz@users.noreply.github.com"}
@@ -310,6 +310,31 @@ def test_doctor_skips_version_row_when_pypi_unreachable(tmp_project):
310
310
  assert "nexus-dev-toolkit" not in result.output
311
311
 
312
312
 
313
+ # ── built-in registration ────────────────────────────────────────────────────
314
+ # A skill/agent .md file living under tools/epav/skills/ or tools/agents/ but
315
+ # missing from _BUILTIN_SKILLS/_BUILTIN_AGENTS is exactly the bug that shipped
316
+ # in v3.1.5 (fixed in v3.1.7) and that update --sync's stale-process bug (fixed
317
+ # in v3.1.9) piggybacked on. These tests make "added the file, forgot the list
318
+ # entry" fail CI instead of silently shipping.
319
+
320
+ def test_all_skill_files_are_registered_as_builtin():
321
+ on_disk = {p.name for p in Path("tools/epav/skills").glob("*.md")}
322
+ assert on_disk == set(_BUILTIN_SKILLS), (
323
+ f"tools/epav/skills/*.md and _BUILTIN_SKILLS disagree — "
324
+ f"on disk but not registered: {on_disk - set(_BUILTIN_SKILLS)}; "
325
+ f"registered but missing from disk: {set(_BUILTIN_SKILLS) - on_disk}"
326
+ )
327
+
328
+
329
+ def test_all_agent_files_are_registered_as_builtin():
330
+ on_disk = {p.name for p in Path("tools/agents").glob("*.md")}
331
+ assert on_disk == set(_BUILTIN_AGENTS), (
332
+ f"tools/agents/*.md and _BUILTIN_AGENTS disagree — "
333
+ f"on disk but not registered: {on_disk - set(_BUILTIN_AGENTS)}; "
334
+ f"registered but missing from disk: {set(_BUILTIN_AGENTS) - on_disk}"
335
+ )
336
+
337
+
313
338
  # ── update ────────────────────────────────────────────────────────────────────
314
339
 
315
340
  def test_update_already_up_to_date_skips_upgrade(monkeypatch):
@@ -524,3 +549,150 @@ def test_doctor_stale_graphify_hook_warns(tmp_project, monkeypatch):
524
549
  assert result.exit_code == 0
525
550
  assert "Stale hook" in output
526
551
  assert "codegraph is the active backend" in output
552
+
553
+
554
+ # ── init --tool codex ────────────────────────────────────────────────────────
555
+
556
+ import tomllib # noqa: E402 — test-only; runtime code stays 3.10-compatible
557
+
558
+ _EPAV_SKILLS_WITHOUT_FRONTMATTER = [
559
+ s for s in _BUILTIN_SKILLS
560
+ if not (Path(__file__).parent.parent / "tools" / "epav" / "skills" / s).read_text().startswith("---")
561
+ ]
562
+
563
+
564
+ def _frontmatter(text: str) -> dict:
565
+ assert text.startswith("---"), "SKILL.md must start with frontmatter"
566
+ fields = {}
567
+ for line in text.split("---", 2)[1].splitlines():
568
+ key, sep, value = line.partition(":")
569
+ if sep:
570
+ fields[key.strip()] = value.strip().strip('"')
571
+ return fields
572
+
573
+
574
+ def test_init_codex_creates_skills(tmp_project):
575
+ result = runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
576
+ assert result.exit_code == 0
577
+ for skill in _BUILTIN_SKILLS:
578
+ stem = Path(skill).stem
579
+ skill_md = tmp_project / ".agents" / "skills" / stem / "SKILL.md"
580
+ assert skill_md.exists(), f"Missing skill: {stem}"
581
+ fields = _frontmatter(skill_md.read_text())
582
+ assert fields.get("name") == stem
583
+ assert fields.get("description"), f"{stem} has an empty description"
584
+
585
+
586
+ def test_init_codex_epav_skills_are_explicit_only(tmp_project):
587
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
588
+ assert _EPAV_SKILLS_WITHOUT_FRONTMATTER, "expected some frontmatter-less EPAV skills"
589
+ for skill in _BUILTIN_SKILLS:
590
+ yaml_path = tmp_project / ".agents" / "skills" / Path(skill).stem / "agents" / "openai.yaml"
591
+ if skill in _EPAV_SKILLS_WITHOUT_FRONTMATTER:
592
+ assert "allow_implicit_invocation: false" in yaml_path.read_text()
593
+ else:
594
+ assert not yaml_path.exists(), f"{skill} should stay implicitly invocable"
595
+
596
+
597
+ def test_init_codex_creates_agent_tomls(tmp_project):
598
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
599
+ for agent in _BUILTIN_AGENTS:
600
+ toml_path = tmp_project / ".codex" / "agents" / f"{Path(agent).stem}.toml"
601
+ assert toml_path.exists(), f"Missing agent: {agent}"
602
+ data = tomllib.loads(toml_path.read_text())
603
+ assert data["name"] and data["description"] and data["developer_instructions"]
604
+ assert "model" not in data and "tools" not in data
605
+
606
+
607
+ def test_init_codex_creates_config_toml(tmp_project):
608
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
609
+ data = tomllib.loads((tmp_project / ".codex" / "config.toml").read_text())
610
+ assert data["mcp_servers"]["nexus"]["command"] == "uvx"
611
+ assert data["mcp_servers"]["nexus"]["args"][-1] == "nexus-mcp"
612
+
613
+
614
+ def test_init_codex_preserves_existing_config_toml(tmp_project):
615
+ existing = 'model = "gpt-5"\n\n[mcp_servers.other]\ncommand = "other"'
616
+ (tmp_project / ".codex").mkdir()
617
+ (tmp_project / ".codex" / "config.toml").write_text(existing)
618
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
619
+ text = (tmp_project / ".codex" / "config.toml").read_text()
620
+ assert text.startswith(existing)
621
+ data = tomllib.loads(text)
622
+ assert set(data["mcp_servers"]) == {"other", "nexus"}
623
+
624
+
625
+ def test_init_codex_does_not_duplicate_nexus_mcp_entry(tmp_project):
626
+ (tmp_project / ".codex").mkdir()
627
+ (tmp_project / ".codex" / "config.toml").write_text('[mcp_servers.nexus]\ncommand = "custom"\n')
628
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
629
+ text = (tmp_project / ".codex" / "config.toml").read_text()
630
+ assert text.count("[mcp_servers.nexus]") == 1
631
+ assert 'command = "custom"' in text
632
+
633
+
634
+ def test_init_codex_creates_graphify_hook(tmp_project):
635
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
636
+ data = json.loads((tmp_project / ".codex" / "hooks.json").read_text())
637
+ entry = data["hooks"]["PostToolUse"][0]
638
+ assert entry["matcher"] == "Edit|Write"
639
+ assert "graphify update" in entry["hooks"][0]["command"]
640
+
641
+
642
+ def test_init_codex_merges_existing_hooks(tmp_project):
643
+ (tmp_project / ".codex").mkdir()
644
+ mine = {"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "echo done"}]}]}}
645
+ (tmp_project / ".codex" / "hooks.json").write_text(json.dumps(mine))
646
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
647
+ data = json.loads((tmp_project / ".codex" / "hooks.json").read_text())
648
+ assert data["hooks"]["Stop"] == mine["hooks"]["Stop"]
649
+ assert "PostToolUse" in data["hooks"]
650
+
651
+
652
+ def test_init_codex_codegraph_backend_skips_hook(tmp_project):
653
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex", "--graph-backend", "codegraph"])
654
+ assert not (tmp_project / ".codex" / "hooks.json").exists()
655
+ assert (tmp_project / ".codex" / "config.toml").exists()
656
+
657
+
658
+ def test_init_codex_idempotent(tmp_project):
659
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
660
+ result = runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
661
+ assert result.exit_code == 0
662
+ assert "Already initialized" in result.output
663
+
664
+
665
+ def test_sync_codex_updates_modified_skill(tmp_project):
666
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
667
+ skill_md = tmp_project / ".agents" / "skills" / "evaluate" / "SKILL.md"
668
+ skill_md.write_text("stale")
669
+ result = runner.invoke(app, ["sync", str(tmp_project)])
670
+ assert result.exit_code == 0
671
+ assert "updated" in result.output
672
+ assert skill_md.read_text() != "stale"
673
+ assert _frontmatter(skill_md.read_text())["name"] == "evaluate"
674
+
675
+
676
+ def test_doctor_codex_project(tmp_project):
677
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
678
+ result = runner.invoke(app, ["doctor", str(tmp_project)])
679
+ out = _flat(result.output)
680
+ assert "yes (codex)" in out
681
+ assert "nexus entry present" in out
682
+ assert f"all {len(_BUILTIN_SKILLS)} present" in out
683
+ assert f"all {len(_BUILTIN_AGENTS)} present" in out
684
+
685
+
686
+ def test_codex_hook_counts_as_graphify_hook(tmp_project):
687
+ from tools.epav import graph_backend
688
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
689
+ assert graph_backend.has_graphify_hook(tmp_project)
690
+
691
+
692
+ def test_init_codex_rewrites_skill_refs_to_dollar_syntax(tmp_project):
693
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "codex"])
694
+ apply_md = (tmp_project / ".agents" / "skills" / "apply" / "SKILL.md").read_text()
695
+ assert "$validate" in apply_md and "`/validate`" not in apply_md
696
+ # Claude Code output must keep the slash syntax
697
+ runner.invoke(app, ["init", str(tmp_project)])
698
+ assert "`/validate`" in (tmp_project / ".claude" / "commands" / "apply.md").read_text()
@@ -80,7 +80,7 @@ For every candidate finding, before keeping it:
80
80
 
81
81
  ## Step 5: Report
82
82
 
83
- Print the `## Repo Context` block as your only text output, then call `ReportFindings` with the verified findings (most severe first; empty array if none survived). Do not also print findings as text, and do not add prose after the tool call.
83
+ Print the `## Repo Context` block as your only text output, then call `ReportFindings` with the verified findings (most severe first; empty array if none survived). Do not also print findings as text, and do not add prose after the tool call. If the `ReportFindings` tool isn't available, print the verified findings as a numbered list instead (most severe first, each with file:line, summary and failure scenario).
84
84
 
85
85
  - `category`: one of `bug`, `error-handling`, `complexity`, `naming`, `duplication`, `type-safety`, `performance`, `maintainability`, `security`
86
86
  - `file`/`line`: omit if not applicable
@@ -78,11 +78,11 @@ def run_query(text: str, backend: str | None = None, root: Path = Path(".")) ->
78
78
 
79
79
  def has_graphify_hook(root: Path = Path(".")) -> bool:
80
80
  """True if this project has graphify's PostToolUse automation wired in."""
81
- settings_path = root / ".claude" / "settings.json"
82
- if settings_path.exists():
83
- try:
84
- if _GRAPHIFY_HOOK_MARKER in settings_path.read_text():
85
- return True
86
- except Exception:
87
- pass
81
+ for settings_path in (root / ".claude" / "settings.json", root / ".codex" / "hooks.json"):
82
+ if settings_path.exists():
83
+ try:
84
+ if _GRAPHIFY_HOOK_MARKER in settings_path.read_text():
85
+ return True
86
+ except Exception:
87
+ pass
88
88
  return (root / ".opencode" / "plugins" / "graphify.js").exists()
@@ -17,7 +17,7 @@ Default scope is "what changed": uncommitted/staged changes plus any commits ahe
17
17
 
18
18
  ### 2 — Delegate to the code-reviewer agent
19
19
 
20
- Launch the `code-reviewer` agent (Agent tool, `subagent_type: "code-reviewer"`) with a prompt stating the scope from Step 1 and any focus areas the user mentioned (e.g. "focus on the auth changes"). The agent discovers repo context itself, reviews read-only, and reports findings via `ReportFindings` — do not ask it to edit code.
20
+ Launch the `code-reviewer` agent (Agent tool, `subagent_type: "code-reviewer"`) with a prompt stating the scope from Step 1 and any focus areas the user mentioned (e.g. "focus on the auth changes"). The agent discovers repo context itself, reviews read-only, and reports findings via `ReportFindings` — do not ask it to edit code. If the `ReportFindings` tool isn't available (non-Claude Code tools), the agent prints the findings as a numbered list instead.
21
21
 
22
22
  ### 3 — Output
23
23