nexus-dev-toolkit 3.1.8__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.8/nexus_dev_toolkit.egg-info → nexus_dev_toolkit-3.1.10}/PKG-INFO +18 -2
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/README.md +17 -1
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_cli.py +278 -55
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10/nexus_dev_toolkit.egg-info}/PKG-INFO +18 -2
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/pyproject.toml +1 -1
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tests/test_cli.py +209 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/code-reviewer.md +1 -1
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/graph_backend.py +7 -7
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/code-review.md +1 -1
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/LICENSE +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/SOURCES.txt +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/dependency_links.txt +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/entry_points.txt +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/requires.txt +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/top_level.txt +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_server.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/setup.cfg +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tests/test_server.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/database-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/deployment-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/monitoring-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/performance-reviewer.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/agents/pr-verifier.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/arch_ingest.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/package_resolver.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/project_rules.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/__init__.py +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/apply.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/commit.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/create-pr.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/database-review.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/deployment-review.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/dev-tasks-planner.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/epav.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/evaluate.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/monitoring-review.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/performance-review.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/plan.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/pr-review.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/scaffold.md +0 -0
- {nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/tools/epav/skills/validate.md +0 -0
- {nexus_dev_toolkit-3.1.8 → 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:
|
|
@@ -430,6 +610,7 @@ def update(
|
|
|
430
610
|
) -> None:
|
|
431
611
|
"""Update nexus-dev-toolkit to the latest version."""
|
|
432
612
|
latest_version = _fetch_latest_pypi_version()
|
|
613
|
+
upgraded = False
|
|
433
614
|
if latest_version and not _is_outdated(_VERSION, latest_version):
|
|
434
615
|
console.print(f"\n [green]✓[/green] Already up to date (v{_VERSION}).\n")
|
|
435
616
|
else:
|
|
@@ -439,6 +620,7 @@ def update(
|
|
|
439
620
|
else:
|
|
440
621
|
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "nexus-dev-toolkit"])
|
|
441
622
|
console.print("\n [green]✓[/green] Done.\n")
|
|
623
|
+
upgraded = True
|
|
442
624
|
|
|
443
625
|
if not also_sync:
|
|
444
626
|
if sys.stdin.isatty():
|
|
@@ -454,6 +636,21 @@ def update(
|
|
|
454
636
|
return
|
|
455
637
|
|
|
456
638
|
root = Path(".").resolve()
|
|
639
|
+
|
|
640
|
+
if upgraded:
|
|
641
|
+
# This process's own _BUILTIN_SKILLS/_BUILTIN_AGENTS were imported before the upgrade
|
|
642
|
+
# subprocess above ran, so syncing in-process here would silently miss anything newly
|
|
643
|
+
# added in the version just installed. Re-exec `nexus sync` as a fresh process instead.
|
|
644
|
+
nexus_bin = shutil.which("nexus")
|
|
645
|
+
if nexus_bin:
|
|
646
|
+
subprocess.run([nexus_bin, "sync", str(root)])
|
|
647
|
+
return
|
|
648
|
+
console.print(
|
|
649
|
+
" [yellow]⚠[/yellow] Couldn't find [bold]nexus[/bold] on PATH to re-run sync in a fresh "
|
|
650
|
+
"process — syncing in-process instead, which may miss anything added in this exact "
|
|
651
|
+
"upgrade. Run [cyan]nexus sync[/cyan] again afterward to be sure.\n"
|
|
652
|
+
)
|
|
653
|
+
|
|
457
654
|
table = _sync_project(root)
|
|
458
655
|
if table is None:
|
|
459
656
|
console.print(" [yellow]⚠[/yellow] Current directory isn't a nexus project — skipping sync.\n")
|
|
@@ -464,13 +661,24 @@ def update(
|
|
|
464
661
|
console.print()
|
|
465
662
|
|
|
466
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
|
+
|
|
467
674
|
def _sync_project(root: Path) -> Table | None:
|
|
468
|
-
"""Sync built-in skills/agents into root. None if root isn't a nexus project (
|
|
469
|
-
.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."""
|
|
470
677
|
has_claude = (root / ".claude").is_dir()
|
|
471
678
|
has_opencode = (root / ".opencode").is_dir()
|
|
679
|
+
has_codex = (root / ".codex").is_dir()
|
|
472
680
|
|
|
473
|
-
if not has_claude and not has_opencode:
|
|
681
|
+
if not has_claude and not has_opencode and not has_codex:
|
|
474
682
|
return None
|
|
475
683
|
|
|
476
684
|
table = Table(show_header=True, header_style="dim")
|
|
@@ -478,68 +686,52 @@ def _sync_project(root: Path) -> Table | None:
|
|
|
478
686
|
table.add_column("Status")
|
|
479
687
|
|
|
480
688
|
if has_claude:
|
|
481
|
-
(root / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
|
|
482
|
-
(root / ".claude" / "agents").mkdir(parents=True, exist_ok=True)
|
|
483
|
-
|
|
484
689
|
for fname in _BUILTIN_SKILLS:
|
|
485
690
|
src = _SKILLS_SRC / fname
|
|
486
|
-
if
|
|
487
|
-
|
|
488
|
-
dst = root / ".claude" / "commands" / fname
|
|
489
|
-
src_text = src.read_text(encoding="utf-8")
|
|
490
|
-
if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
|
|
491
|
-
table.add_row(f".claude/commands/{fname}", "[dim green]ok[/dim green]")
|
|
492
|
-
else:
|
|
493
|
-
dst.write_text(src_text, encoding="utf-8")
|
|
494
|
-
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}")
|
|
495
693
|
|
|
496
694
|
for fname in _BUILTIN_AGENTS:
|
|
497
695
|
src = _AGENTS_SRC / fname
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
dst = root / ".claude" / "agents" / fname
|
|
501
|
-
src_text = src.read_text(encoding="utf-8")
|
|
502
|
-
if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
|
|
503
|
-
table.add_row(f".claude/agents/{fname}", "[dim green]ok[/dim green]")
|
|
504
|
-
else:
|
|
505
|
-
dst.write_text(src_text, encoding="utf-8")
|
|
506
|
-
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}")
|
|
507
698
|
|
|
508
699
|
if has_opencode:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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")
|
|
512
712
|
|
|
713
|
+
if has_codex:
|
|
513
714
|
for fname in _BUILTIN_SKILLS:
|
|
514
715
|
src = _SKILLS_SRC / fname
|
|
515
716
|
if not src.exists():
|
|
516
717
|
continue
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
dst.write_text(src_text, encoding="utf-8")
|
|
523
|
-
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")
|
|
524
723
|
|
|
525
724
|
for fname in _BUILTIN_AGENTS:
|
|
526
725
|
src = _AGENTS_SRC / fname
|
|
527
|
-
if
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
plugin_dst = root / ".opencode" / "plugins" / "graphify.js"
|
|
538
|
-
if plugin_dst.exists() and plugin_dst.read_text(encoding="utf-8") == _OPENCODE_GRAPHIFY_PLUGIN:
|
|
539
|
-
table.add_row(".opencode/plugins/graphify.js", "[dim green]ok[/dim green]")
|
|
540
|
-
else:
|
|
541
|
-
plugin_dst.write_text(_OPENCODE_GRAPHIFY_PLUGIN, encoding="utf-8")
|
|
542
|
-
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]")
|
|
543
735
|
|
|
544
736
|
return table
|
|
545
737
|
|
|
@@ -595,16 +787,20 @@ def doctor(
|
|
|
595
787
|
# ── Tool ─────────────────────────────────────────────────────────────────
|
|
596
788
|
has_claude_bin = bool(shutil.which("claude"))
|
|
597
789
|
has_opencode_bin = bool(shutil.which("opencode"))
|
|
790
|
+
has_codex_bin = bool(shutil.which("codex"))
|
|
598
791
|
table.add_row("Claude Code", ok("installed") if has_claude_bin else warn("not found — install: npm install -g @anthropic-ai/claude-code"))
|
|
599
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"))
|
|
600
794
|
|
|
601
795
|
# ── Project ──────────────────────────────────────────────────────────────
|
|
602
796
|
has_claude_dir = (root / ".claude").is_dir()
|
|
603
797
|
has_opencode_dir = (root / ".opencode").is_dir()
|
|
604
|
-
|
|
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 '']
|
|
605
801
|
table.add_row(
|
|
606
802
|
"nexus initialized",
|
|
607
|
-
ok(f"yes ({', '.join(filter(None,
|
|
803
|
+
ok(f"yes ({', '.join(filter(None, tools_found))})")
|
|
608
804
|
if initialized else fail("no — run nexus init")
|
|
609
805
|
)
|
|
610
806
|
|
|
@@ -638,6 +834,16 @@ def doctor(
|
|
|
638
834
|
except Exception:
|
|
639
835
|
table.add_row("opencode.json", warn("invalid JSON"))
|
|
640
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
|
+
|
|
641
847
|
# ── Skills & Agents ──────────────────────────────────────────────────────
|
|
642
848
|
if has_claude_dir:
|
|
643
849
|
cmd_dir = root / ".claude" / "commands"
|
|
@@ -673,6 +879,23 @@ def doctor(
|
|
|
673
879
|
else:
|
|
674
880
|
table.add_row("Built-in agents (OpenCode)", warn(f"{missing_oc_ag} missing — run nexus sync"))
|
|
675
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
|
+
|
|
676
899
|
# ── Knowledge ────────────────────────────────────────────────────────────
|
|
677
900
|
missing_dirs = [d for d in _KNOWLEDGE_DIRS if not (root / d).is_dir()]
|
|
678
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):
|
|
@@ -367,6 +392,43 @@ def test_update_with_sync_flag_syncs_current_project(monkeypatch, tmp_project):
|
|
|
367
392
|
assert "scaffold.md" in output
|
|
368
393
|
|
|
369
394
|
|
|
395
|
+
def test_update_with_sync_flag_reexecs_sync_after_real_upgrade(monkeypatch, tmp_project):
|
|
396
|
+
"""After an actual upgrade, sync must run as a fresh `nexus sync` process, not in-process —
|
|
397
|
+
this process's own _BUILTIN_SKILLS/_BUILTIN_AGENTS were imported before the upgrade ran, so
|
|
398
|
+
calling _sync_project() directly here would silently miss anything the upgrade just added."""
|
|
399
|
+
import nexus_cli
|
|
400
|
+
monkeypatch.setattr("nexus_cli._fetch_latest_pypi_version", lambda: "99.0.0")
|
|
401
|
+
calls = []
|
|
402
|
+
monkeypatch.setattr("nexus_cli.subprocess.run", lambda cmd, *a, **k: calls.append(cmd))
|
|
403
|
+
monkeypatch.setattr("nexus_cli.shutil.which", lambda name: f"/usr/bin/{name}")
|
|
404
|
+
|
|
405
|
+
def _boom(root):
|
|
406
|
+
raise AssertionError("_sync_project must not run in-process after a real upgrade")
|
|
407
|
+
monkeypatch.setattr("nexus_cli._sync_project", _boom)
|
|
408
|
+
|
|
409
|
+
runner.invoke(app, ["init", str(tmp_project)])
|
|
410
|
+
monkeypatch.chdir(tmp_project)
|
|
411
|
+
result = runner.invoke(app, ["update", "--sync"])
|
|
412
|
+
|
|
413
|
+
assert result.exit_code == 0
|
|
414
|
+
assert calls[0] == ["uv", "tool", "upgrade", "nexus-dev-toolkit"]
|
|
415
|
+
assert calls[1] == ["/usr/bin/nexus", "sync", str(tmp_project.resolve())]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_update_sync_falls_back_in_process_when_nexus_not_on_path(monkeypatch, tmp_project):
|
|
419
|
+
import nexus_cli
|
|
420
|
+
monkeypatch.setattr("nexus_cli._fetch_latest_pypi_version", lambda: "99.0.0")
|
|
421
|
+
monkeypatch.setattr("nexus_cli.subprocess.run", lambda *a, **k: None)
|
|
422
|
+
monkeypatch.setattr("nexus_cli.shutil.which", lambda name: "/usr/bin/uv" if name == "uv" else None)
|
|
423
|
+
runner.invoke(app, ["init", str(tmp_project)])
|
|
424
|
+
monkeypatch.chdir(tmp_project)
|
|
425
|
+
result = runner.invoke(app, ["update", "--sync"])
|
|
426
|
+
assert result.exit_code == 0
|
|
427
|
+
output = _flat(result.output)
|
|
428
|
+
assert "Syncing built-ins" in output
|
|
429
|
+
assert "scaffold.md" in output
|
|
430
|
+
|
|
431
|
+
|
|
370
432
|
class _FakeTTY:
|
|
371
433
|
def isatty(self):
|
|
372
434
|
return True
|
|
@@ -487,3 +549,150 @@ def test_doctor_stale_graphify_hook_warns(tmp_project, monkeypatch):
|
|
|
487
549
|
assert result.exit_code == 0
|
|
488
550
|
assert "Stale hook" in output
|
|
489
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.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.8 → nexus_dev_toolkit-3.1.10}/nexus_dev_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{nexus_dev_toolkit-3.1.8 → 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.8 → 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
|