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.
- {nexus_dev_toolkit-3.1.9/nexus_dev_toolkit.egg-info → nexus_dev_toolkit-3.1.10}/PKG-INFO +18 -2
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/README.md +17 -1
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_cli.py +261 -55
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10/nexus_dev_toolkit.egg-info}/PKG-INFO +18 -2
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/pyproject.toml +1 -1
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tests/test_cli.py +172 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/code-reviewer.md +1 -1
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/graph_backend.py +7 -7
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/code-review.md +1 -1
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/LICENSE +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/SOURCES.txt +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/dependency_links.txt +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/entry_points.txt +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/requires.txt +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/top_level.txt +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_server.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/setup.cfg +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tests/test_server.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/database-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/deployment-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/monitoring-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/performance-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/agents/pr-verifier.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/arch_ingest.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/package_resolver.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/project_rules.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/apply.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/commit.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/create-pr.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/database-review.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/deployment-review.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/dev-tasks-planner.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/epav.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/evaluate.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/monitoring-review.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/performance-review.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/plan.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/pr-review.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/scaffold.md +0 -0
- {nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/validate.md +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 (
|
|
486
|
-
.claude/
|
|
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
|
|
504
|
-
|
|
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
|
|
516
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
@@ -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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
{nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nexus_dev_toolkit-3.1.9 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/performance-review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|