nexus-dev-toolkit 3.0.0__py3-none-any.whl
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_cli.py +292 -0
- nexus_dev_toolkit-3.0.0.dist-info/METADATA +170 -0
- nexus_dev_toolkit-3.0.0.dist-info/RECORD +21 -0
- nexus_dev_toolkit-3.0.0.dist-info/WHEEL +5 -0
- nexus_dev_toolkit-3.0.0.dist-info/entry_points.txt +3 -0
- nexus_dev_toolkit-3.0.0.dist-info/licenses/LICENSE +21 -0
- nexus_dev_toolkit-3.0.0.dist-info/top_level.txt +3 -0
- nexus_server.py +16 -0
- tools/__init__.py +0 -0
- tools/epav/__init__.py +14 -0
- tools/epav/arch_ingest.py +124 -0
- tools/epav/package_resolver.py +271 -0
- tools/epav/project_rules.py +208 -0
- tools/epav/skills/__init__.py +0 -0
- tools/epav/skills/apply.md +41 -0
- tools/epav/skills/epav.md +46 -0
- tools/epav/skills/evaluate.md +42 -0
- tools/epav/skills/plan.md +46 -0
- tools/epav/skills/scaffold.md +249 -0
- tools/epav/skills/validate.md +57 -0
- tools/epav/task_loader.py +140 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""
|
|
2
|
+
resolve_package_versions — MCP tool for Day 0 APPLY.
|
|
3
|
+
|
|
4
|
+
Given a package manager type and a list of packages with optional major-version
|
|
5
|
+
constraints (derived from the arch doc), spins up a temp directory, runs the
|
|
6
|
+
package manager's resolution command, reads the lock file, and returns exact
|
|
7
|
+
pinned versions. Never hardcodes versions or stack-specific logic beyond what
|
|
8
|
+
is needed to dispatch the right CLI.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
_PM_REGISTRY: dict[str, dict] = {
|
|
20
|
+
"npm": {
|
|
21
|
+
"detect": ["package.json", "package-lock.json", ".npmrc"],
|
|
22
|
+
"init": ["npm", "init", "-y"],
|
|
23
|
+
"resolve": ["npm", "install", "--package-lock-only", "--legacy-peer-deps"],
|
|
24
|
+
"lock_file": "package-lock.json",
|
|
25
|
+
},
|
|
26
|
+
"pnpm": {
|
|
27
|
+
"detect": ["pnpm-lock.yaml", "pnpm-workspace.yaml"],
|
|
28
|
+
"init": ["pnpm", "init"],
|
|
29
|
+
"resolve": ["pnpm", "install", "--lockfile-only"],
|
|
30
|
+
"lock_file": "pnpm-lock.yaml",
|
|
31
|
+
},
|
|
32
|
+
"yarn": {
|
|
33
|
+
"detect": ["yarn.lock", ".yarnrc.yml"],
|
|
34
|
+
"init": ["yarn", "init", "-y"],
|
|
35
|
+
"resolve": ["yarn", "install", "--frozen-lockfile"],
|
|
36
|
+
"lock_file": "yarn.lock",
|
|
37
|
+
},
|
|
38
|
+
"maven": {
|
|
39
|
+
"detect": ["pom.xml"],
|
|
40
|
+
"init": None,
|
|
41
|
+
"resolve": ["mvn", "dependency:resolve", "-q"],
|
|
42
|
+
"lock_file": None,
|
|
43
|
+
},
|
|
44
|
+
"gradle": {
|
|
45
|
+
"detect": ["build.gradle", "build.gradle.kts"],
|
|
46
|
+
"init": None,
|
|
47
|
+
"resolve": ["gradle", "dependencies", "--configuration", "runtimeClasspath"],
|
|
48
|
+
"lock_file": "gradle.lockfile",
|
|
49
|
+
},
|
|
50
|
+
"pub": {
|
|
51
|
+
"detect": ["pubspec.yaml"],
|
|
52
|
+
"init": None,
|
|
53
|
+
"resolve": ["flutter", "pub", "get"],
|
|
54
|
+
"lock_file": "pubspec.lock",
|
|
55
|
+
},
|
|
56
|
+
"go": {
|
|
57
|
+
"detect": ["go.mod"],
|
|
58
|
+
"init": None,
|
|
59
|
+
"resolve": ["go", "mod", "tidy"],
|
|
60
|
+
"lock_file": "go.sum",
|
|
61
|
+
},
|
|
62
|
+
"cargo": {
|
|
63
|
+
"detect": ["Cargo.toml"],
|
|
64
|
+
"init": None,
|
|
65
|
+
"resolve": ["cargo", "update"],
|
|
66
|
+
"lock_file": "Cargo.lock",
|
|
67
|
+
},
|
|
68
|
+
"pip": {
|
|
69
|
+
"detect": ["requirements.txt", "pyproject.toml"],
|
|
70
|
+
"init": None,
|
|
71
|
+
"resolve": ["pip", "install", "--dry-run", "--report", "-"],
|
|
72
|
+
"lock_file": None,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _detect_package_manager(hint: str | None) -> str:
|
|
78
|
+
if not hint:
|
|
79
|
+
return "npm"
|
|
80
|
+
hint_lower = hint.lower()
|
|
81
|
+
for pm in _PM_REGISTRY:
|
|
82
|
+
if pm in hint_lower:
|
|
83
|
+
return pm
|
|
84
|
+
if any(k in hint_lower for k in ["node", "next", "react", "vite", "typescript"]):
|
|
85
|
+
return "npm"
|
|
86
|
+
if any(k in hint_lower for k in ["flutter", "dart"]):
|
|
87
|
+
return "pub"
|
|
88
|
+
if any(k in hint_lower for k in ["java", "spring", "kotlin"]):
|
|
89
|
+
return "maven"
|
|
90
|
+
if "python" in hint_lower:
|
|
91
|
+
return "pip"
|
|
92
|
+
if "rust" in hint_lower:
|
|
93
|
+
return "cargo"
|
|
94
|
+
return "npm"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _strip_version_spec(pkg: str) -> str:
|
|
98
|
+
if pkg.startswith("@"):
|
|
99
|
+
rest = pkg[1:]
|
|
100
|
+
if "@" in rest:
|
|
101
|
+
return f"@{rest[:rest.index('@')]}"
|
|
102
|
+
return pkg
|
|
103
|
+
return pkg.split("@")[0] if "@" in pkg else pkg
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_npm_lock(lock_path: Path, packages: list[str]) -> dict[str, str]:
|
|
107
|
+
try:
|
|
108
|
+
lock = json.loads(lock_path.read_text())
|
|
109
|
+
deps = lock.get("packages", {})
|
|
110
|
+
return {
|
|
111
|
+
_strip_version_spec(pkg): deps.get(f"node_modules/{_strip_version_spec(pkg)}", {}).get("version", "unknown")
|
|
112
|
+
for pkg in packages
|
|
113
|
+
if f"node_modules/{_strip_version_spec(pkg)}" in deps
|
|
114
|
+
}
|
|
115
|
+
except Exception:
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_pubspec_lock(lock_path: Path, packages: list[str]) -> dict[str, str]:
|
|
120
|
+
versions: dict[str, str] = {}
|
|
121
|
+
try:
|
|
122
|
+
current_pkg = None
|
|
123
|
+
for line in lock_path.read_text().splitlines():
|
|
124
|
+
stripped = line.strip()
|
|
125
|
+
if stripped.endswith(":") and not stripped.startswith(" "):
|
|
126
|
+
current_pkg = stripped[:-1]
|
|
127
|
+
elif current_pkg and stripped.startswith("version:"):
|
|
128
|
+
ver = stripped.split(":", 1)[1].strip().strip('"')
|
|
129
|
+
if current_pkg in packages:
|
|
130
|
+
versions[current_pkg] = ver
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return versions
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _read_go_sum(lock_path: Path, packages: list[str]) -> dict[str, str]:
|
|
137
|
+
versions: dict[str, str] = {}
|
|
138
|
+
try:
|
|
139
|
+
for line in lock_path.read_text().splitlines():
|
|
140
|
+
parts = line.split()
|
|
141
|
+
if len(parts) >= 2:
|
|
142
|
+
mod, ver = parts[0], parts[1].split("/")[0]
|
|
143
|
+
for pkg in packages:
|
|
144
|
+
if pkg in mod and pkg not in versions:
|
|
145
|
+
versions[pkg] = ver.lstrip("v")
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
return versions
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _read_cargo_lock(lock_path: Path, packages: list[str]) -> dict[str, str]:
|
|
152
|
+
versions: dict[str, str] = {}
|
|
153
|
+
try:
|
|
154
|
+
current_name = None
|
|
155
|
+
for line in lock_path.read_text().splitlines():
|
|
156
|
+
line = line.strip()
|
|
157
|
+
if line.startswith("name ="):
|
|
158
|
+
current_name = line.split("=", 1)[1].strip().strip('"')
|
|
159
|
+
elif line.startswith("version =") and current_name:
|
|
160
|
+
ver = line.split("=", 1)[1].strip().strip('"')
|
|
161
|
+
if current_name in packages and current_name not in versions:
|
|
162
|
+
versions[current_name] = ver
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
return versions
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _read_lock_file(pm: str, lock_path: Path, packages: list[str]) -> dict[str, str]:
|
|
169
|
+
if pm in ("npm", "pnpm", "yarn"):
|
|
170
|
+
return _read_npm_lock(lock_path, packages)
|
|
171
|
+
if pm == "pub":
|
|
172
|
+
return _read_pubspec_lock(lock_path, packages)
|
|
173
|
+
if pm == "go":
|
|
174
|
+
return _read_go_sum(lock_path, packages)
|
|
175
|
+
if pm == "cargo":
|
|
176
|
+
return _read_cargo_lock(lock_path, packages)
|
|
177
|
+
return {}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _build_install_args(pm: str, packages: list[str]) -> list[str]:
|
|
181
|
+
if pm == "npm":
|
|
182
|
+
return ["npm", "install", "--package-lock-only", "--legacy-peer-deps"] + packages
|
|
183
|
+
if pm == "pnpm":
|
|
184
|
+
return ["pnpm", "add", "--lockfile-only"] + packages
|
|
185
|
+
if pm == "yarn":
|
|
186
|
+
return ["yarn", "add"] + packages
|
|
187
|
+
if pm == "pub":
|
|
188
|
+
return ["flutter", "pub", "add"] + packages
|
|
189
|
+
if pm == "go":
|
|
190
|
+
return ["go", "get"] + packages
|
|
191
|
+
if pm == "cargo":
|
|
192
|
+
return ["cargo", "add"] + packages
|
|
193
|
+
return _PM_REGISTRY[pm]["resolve"]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def register_package_resolver_tool(mcp: FastMCP) -> None:
|
|
197
|
+
@mcp.tool()
|
|
198
|
+
def resolve_package_versions(
|
|
199
|
+
packages: list[str],
|
|
200
|
+
package_manager: str = "",
|
|
201
|
+
stack_hint: str = "",
|
|
202
|
+
) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Resolve exact pinned package versions using the real package manager.
|
|
205
|
+
|
|
206
|
+
Runs in a temp directory — does not modify the project. Returns exact
|
|
207
|
+
versions so Day 0 APPLY can write deterministic package manifests.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
packages: List of packages with optional constraints,
|
|
211
|
+
e.g. ["next@16", "react", "@supabase/supabase-js@2"]
|
|
212
|
+
package_manager: npm | pnpm | yarn | pub | maven | gradle | go | cargo | pip.
|
|
213
|
+
Auto-detected from stack_hint if omitted.
|
|
214
|
+
stack_hint: Free-text hint from arch doc, e.g. "Next.js 16 + TypeScript".
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
JSON: { "versions": {"next": "16.3.2", ...}, "package_manager": "npm",
|
|
218
|
+
"lock_file": "package-lock.json", "errors": [...] }
|
|
219
|
+
"""
|
|
220
|
+
pm = package_manager.strip().lower() if package_manager.strip() else _detect_package_manager(stack_hint)
|
|
221
|
+
|
|
222
|
+
if pm not in _PM_REGISTRY:
|
|
223
|
+
return json.dumps({
|
|
224
|
+
"error": f"Unknown package manager: {pm}. Supported: {list(_PM_REGISTRY.keys())}"
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
pm_config = _PM_REGISTRY[pm]
|
|
228
|
+
tmpdir = tempfile.mkdtemp(prefix="nexus-resolve-")
|
|
229
|
+
errors: list[str] = []
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
if pm_config["init"]:
|
|
233
|
+
subprocess.run(pm_config["init"], cwd=tmpdir, capture_output=True, timeout=30)
|
|
234
|
+
|
|
235
|
+
if pm == "npm":
|
|
236
|
+
(Path(tmpdir) / "package.json").write_text(
|
|
237
|
+
json.dumps({"name": "resolve-temp", "version": "0.0.1", "private": True})
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
cmd = _build_install_args(pm, packages)
|
|
241
|
+
result = subprocess.run(cmd, cwd=tmpdir, capture_output=True, text=True, timeout=120)
|
|
242
|
+
|
|
243
|
+
if result.returncode != 0:
|
|
244
|
+
errors.append(result.stderr[:500])
|
|
245
|
+
|
|
246
|
+
lock_file = pm_config.get("lock_file")
|
|
247
|
+
versions: dict[str, str] = {}
|
|
248
|
+
if lock_file:
|
|
249
|
+
lock_path = Path(tmpdir) / lock_file
|
|
250
|
+
if lock_path.exists():
|
|
251
|
+
pkg_names = [
|
|
252
|
+
p.split("@")[0] if "@" in p and not p.startswith("@")
|
|
253
|
+
else p.rsplit("@", 1)[0] if p.count("@") > 1
|
|
254
|
+
else p
|
|
255
|
+
for p in packages
|
|
256
|
+
]
|
|
257
|
+
versions = _read_lock_file(pm, lock_path, pkg_names)
|
|
258
|
+
|
|
259
|
+
return json.dumps({
|
|
260
|
+
"versions": versions,
|
|
261
|
+
"package_manager": pm,
|
|
262
|
+
"lock_file": lock_file,
|
|
263
|
+
"errors": errors,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
except subprocess.TimeoutExpired:
|
|
267
|
+
return json.dumps({"error": "Resolution timed out after 120s", "package_manager": pm})
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return json.dumps({"error": str(e), "package_manager": pm})
|
|
270
|
+
finally:
|
|
271
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
_KNOWLEDGE_DIRS = [
|
|
10
|
+
"knowledge/rules",
|
|
11
|
+
"knowledge/patterns",
|
|
12
|
+
"knowledge/prompts/dev",
|
|
13
|
+
"knowledge/agents",
|
|
14
|
+
"knowledge/retros",
|
|
15
|
+
"knowledge/templates",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
_AGENTS_MD_TEMPLATE = """\
|
|
19
|
+
# AGENTS.md
|
|
20
|
+
|
|
21
|
+
> Cross-tool project rules. Every AI assistant working on this project must read this file first.
|
|
22
|
+
> Generated by nexus-dev-toolkit — update as the project evolves.
|
|
23
|
+
|
|
24
|
+
## Project
|
|
25
|
+
|
|
26
|
+
**Stack:** {stack}
|
|
27
|
+
**Repo:** {repo}
|
|
28
|
+
|
|
29
|
+
## Coding Standards
|
|
30
|
+
|
|
31
|
+
See `knowledge/rules/coding-standards.md` for the full ruleset.
|
|
32
|
+
|
|
33
|
+
### Non-negotiable rules
|
|
34
|
+
|
|
35
|
+
{rules}
|
|
36
|
+
|
|
37
|
+
## Git Conventions
|
|
38
|
+
|
|
39
|
+
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`
|
|
40
|
+
- One logical change per commit
|
|
41
|
+
- Branch naming: `feat/`, `fix/`, `chore/`
|
|
42
|
+
|
|
43
|
+
## Quality Gates
|
|
44
|
+
|
|
45
|
+
- All acceptance criteria from the task CSV must pass before marking done
|
|
46
|
+
- Run `/validate` after every `/apply`
|
|
47
|
+
- Contribute new patterns to `knowledge/patterns/` via PR
|
|
48
|
+
|
|
49
|
+
## E→P→A→V Cycle
|
|
50
|
+
|
|
51
|
+
Every dev task follows: `/evaluate` → `/plan` → `/apply` → `/validate`
|
|
52
|
+
The task CSV provides the prompt — do not write prompts from scratch.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_CODING_STANDARDS_TEMPLATE = """\
|
|
56
|
+
# Coding Standards
|
|
57
|
+
|
|
58
|
+
_Derived from architecture document. Update via PR when conventions change._
|
|
59
|
+
|
|
60
|
+
## Stack
|
|
61
|
+
|
|
62
|
+
{stack_detail}
|
|
63
|
+
|
|
64
|
+
## Rules
|
|
65
|
+
|
|
66
|
+
{rules_detail}
|
|
67
|
+
|
|
68
|
+
## Rationale
|
|
69
|
+
|
|
70
|
+
Rules without rationale get ignored. Every rule here links to the architecture
|
|
71
|
+
decision that produced it. When in doubt, read the arch doc in `docs/arch-docs/`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_arch_summary() -> dict:
|
|
76
|
+
summary_path = Path("knowledge/rules/arch-summary.md")
|
|
77
|
+
if summary_path.exists():
|
|
78
|
+
return {"source": str(summary_path), "text": summary_path.read_text(encoding="utf-8")}
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _infer_stack(arch_text: str) -> str:
|
|
83
|
+
text_lower = arch_text.lower()
|
|
84
|
+
parts = []
|
|
85
|
+
if "next.js" in text_lower or "nextjs" in text_lower:
|
|
86
|
+
parts.append("Next.js")
|
|
87
|
+
if "react native" in text_lower:
|
|
88
|
+
parts.append("React Native")
|
|
89
|
+
if "flutter" in text_lower:
|
|
90
|
+
parts.append("Flutter")
|
|
91
|
+
if "trpc" in text_lower or "t3" in text_lower:
|
|
92
|
+
parts.append("tRPC / T3")
|
|
93
|
+
if "prisma" in text_lower:
|
|
94
|
+
parts.append("Prisma")
|
|
95
|
+
if "postgres" in text_lower or "postgresql" in text_lower:
|
|
96
|
+
parts.append("PostgreSQL")
|
|
97
|
+
if "tailwind" in text_lower:
|
|
98
|
+
parts.append("Tailwind CSS")
|
|
99
|
+
if "supabase" in text_lower:
|
|
100
|
+
parts.append("Supabase")
|
|
101
|
+
return ", ".join(parts) if parts else "(update with your stack)"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def register_project_rules_tool(mcp: FastMCP) -> None:
|
|
105
|
+
|
|
106
|
+
@mcp.tool()
|
|
107
|
+
async def generate_project_rules(
|
|
108
|
+
arch_doc_path: str = "",
|
|
109
|
+
project_name: str = "",
|
|
110
|
+
overwrite: bool = False,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Generate AGENTS.md and knowledge/ directory structure from an architecture document.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
arch_doc_path: Path to the architecture doc (.md). If empty, uses
|
|
117
|
+
knowledge/rules/arch-summary.md if ingest_architecture_doc
|
|
118
|
+
has already run.
|
|
119
|
+
project_name: Name used in AGENTS.md header. Defaults to current dir name.
|
|
120
|
+
overwrite: If False (default), returns error if AGENTS.md already exists.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
JSON with list of files created/updated.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
agents_path = Path("AGENTS.md")
|
|
127
|
+
if agents_path.exists() and not overwrite:
|
|
128
|
+
return json.dumps({
|
|
129
|
+
"error": "AGENTS.md already exists. Pass overwrite=true to replace it.",
|
|
130
|
+
"existing_path": str(agents_path.resolve()),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
arch_text = ""
|
|
134
|
+
source_used = ""
|
|
135
|
+
|
|
136
|
+
if arch_doc_path and Path(arch_doc_path).exists():
|
|
137
|
+
arch_text = Path(arch_doc_path).read_text(encoding="utf-8")
|
|
138
|
+
source_used = arch_doc_path
|
|
139
|
+
else:
|
|
140
|
+
summary = _load_arch_summary()
|
|
141
|
+
if summary:
|
|
142
|
+
arch_text = summary["text"]
|
|
143
|
+
source_used = summary["source"]
|
|
144
|
+
|
|
145
|
+
if not arch_text:
|
|
146
|
+
return json.dumps({
|
|
147
|
+
"error": "No architecture doc found. Run ingest_architecture_doc first, "
|
|
148
|
+
"or provide arch_doc_path.",
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
repo = project_name or Path(".").resolve().name
|
|
152
|
+
stack = _infer_stack(arch_text)
|
|
153
|
+
rules = (
|
|
154
|
+
"- Follow the conventions in the architecture document\n"
|
|
155
|
+
"- No console.log in production code — use the project logger\n"
|
|
156
|
+
"- All secrets via environment variables — never hardcoded\n"
|
|
157
|
+
"- Pin exact dependency versions (no ^ or ~)\n"
|
|
158
|
+
"- Every task must pass /validate before moving on"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
dirs_created = []
|
|
162
|
+
for d in _KNOWLEDGE_DIRS:
|
|
163
|
+
p = Path(d)
|
|
164
|
+
if not p.exists():
|
|
165
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
dirs_created.append(d)
|
|
167
|
+
|
|
168
|
+
files_written = []
|
|
169
|
+
|
|
170
|
+
agents_path.write_text(
|
|
171
|
+
_AGENTS_MD_TEMPLATE.format(stack=stack, repo=repo, rules=rules),
|
|
172
|
+
encoding="utf-8",
|
|
173
|
+
)
|
|
174
|
+
files_written.append(str(agents_path))
|
|
175
|
+
|
|
176
|
+
standards_path = Path("knowledge/rules/coding-standards.md")
|
|
177
|
+
standards_path.write_text(
|
|
178
|
+
_CODING_STANDARDS_TEMPLATE.format(
|
|
179
|
+
stack_detail=f"**{stack}** — see arch doc for full decisions.",
|
|
180
|
+
rules_detail=rules,
|
|
181
|
+
),
|
|
182
|
+
encoding="utf-8",
|
|
183
|
+
)
|
|
184
|
+
files_written.append(str(standards_path))
|
|
185
|
+
|
|
186
|
+
pattern_path = Path("knowledge/patterns/implement-and-test.md")
|
|
187
|
+
if not pattern_path.exists():
|
|
188
|
+
pattern_path.write_text(
|
|
189
|
+
"# Implement and Test Pattern\n\n"
|
|
190
|
+
"Every dev task follows the E→P→A→V cycle:\n\n"
|
|
191
|
+
"1. `/evaluate <task>` — orient, load context, graphify blast radius\n"
|
|
192
|
+
"2. `/plan` — blueprint, wait for approval\n"
|
|
193
|
+
"3. `/apply` — implement, graphify auto-updates\n"
|
|
194
|
+
"4. `/validate` — acceptance criteria, classify issues, contribute patterns\n",
|
|
195
|
+
encoding="utf-8",
|
|
196
|
+
)
|
|
197
|
+
files_written.append(str(pattern_path))
|
|
198
|
+
|
|
199
|
+
return json.dumps({
|
|
200
|
+
"source_used": source_used,
|
|
201
|
+
"stack_detected": stack,
|
|
202
|
+
"dirs_created": dirs_created,
|
|
203
|
+
"files_written": files_written,
|
|
204
|
+
}, indent=2)
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.exception("Unexpected error in generate_project_rules")
|
|
208
|
+
return json.dumps({"error": f"Unexpected error: {e}"})
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# /apply
|
|
2
|
+
|
|
3
|
+
**APPLY** — Step 3 of the E→P→A→V cycle. Implement the approved plan.
|
|
4
|
+
|
|
5
|
+
## Prerequisite
|
|
6
|
+
|
|
7
|
+
A plan must be approved. If `/plan` has not run and been approved, stop and ask.
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
|
|
11
|
+
### 1 — Implement exactly what the plan says
|
|
12
|
+
|
|
13
|
+
- Follow the numbered steps from PLAN in order.
|
|
14
|
+
- Reference AGENTS.md coding standards for every file written.
|
|
15
|
+
- Do not add features, refactor unrelated code, or expand scope beyond the plan.
|
|
16
|
+
|
|
17
|
+
### 2 — graphify updates automatically
|
|
18
|
+
|
|
19
|
+
The `PostToolUse` hook runs `graphify update .` after every file edit — no manual step needed. The graph stays current as you write.
|
|
20
|
+
|
|
21
|
+
### 3 — Stay in scope
|
|
22
|
+
|
|
23
|
+
If you discover something unexpected that requires scope change:
|
|
24
|
+
- Stop implementing.
|
|
25
|
+
- Flag it: "Discovered: <X>. This is outside the plan scope. Revise plan before continuing?"
|
|
26
|
+
- Wait for direction.
|
|
27
|
+
|
|
28
|
+
### 4 — When done
|
|
29
|
+
|
|
30
|
+
State:
|
|
31
|
+
```
|
|
32
|
+
APPLY COMPLETE
|
|
33
|
+
──────────────
|
|
34
|
+
Created: <files>
|
|
35
|
+
Modified: <files>
|
|
36
|
+
Skipped: <anything intentionally deferred>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then prompt:
|
|
40
|
+
|
|
41
|
+
> "Ready to /validate. Type `/validate` to check against acceptance criteria."
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# /epav
|
|
2
|
+
|
|
3
|
+
**E→P→A→V** — Full cycle orchestrator. Runs all four steps in sequence with a gate before APPLY.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
`/epav <task description, CSV path, or feature name>`
|
|
8
|
+
|
|
9
|
+
## Cycle
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
EVALUATE → PLAN → [approval gate] → APPLY → VALIDATE
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
### Step 1 — EVALUATE
|
|
18
|
+
|
|
19
|
+
Follow the `/evaluate` skill exactly. Output the EVALUATE SUMMARY.
|
|
20
|
+
|
|
21
|
+
### Step 2 — PLAN
|
|
22
|
+
|
|
23
|
+
Follow the `/plan` skill exactly. Output the blueprint with blast radius.
|
|
24
|
+
|
|
25
|
+
**GATE: Stop here. Do not proceed to APPLY until the user explicitly approves.**
|
|
26
|
+
|
|
27
|
+
Say:
|
|
28
|
+
> "Plan ready. Reply **go** or `/apply` to implement, or give feedback to revise."
|
|
29
|
+
|
|
30
|
+
### Step 3 — APPLY (only after approval)
|
|
31
|
+
|
|
32
|
+
Follow the `/apply` skill exactly. Implement the plan step by step.
|
|
33
|
+
|
|
34
|
+
graphify updates automatically after every file edit via the PostToolUse hook.
|
|
35
|
+
|
|
36
|
+
### Step 4 — VALIDATE
|
|
37
|
+
|
|
38
|
+
Follow the `/validate` skill exactly. Check all acceptance criteria, fix all BLOCKERs and FIX NOWs, contribute patterns back to knowledge/.
|
|
39
|
+
|
|
40
|
+
## Abort at any step
|
|
41
|
+
|
|
42
|
+
If the user says "stop", "abort", or "cancel" at any point — stop immediately and summarize what was completed and what was not.
|
|
43
|
+
|
|
44
|
+
## Scope discipline
|
|
45
|
+
|
|
46
|
+
The EPAV cycle covers **one task at a time**. If additional work surfaces during APPLY, log it to `knowledge/retros/` and finish the current task first.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# /evaluate
|
|
2
|
+
|
|
3
|
+
**EVALUATE** — Step 1 of the E→P→A→V cycle. Orient fully before writing any plan or code.
|
|
4
|
+
|
|
5
|
+
## Arguments
|
|
6
|
+
|
|
7
|
+
`/evaluate <task description, CSV path, or feature name>`
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
|
|
11
|
+
### 1 — Orient with graphify (if graph exists)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
graphify query "<task context from args>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Note which communities and god nodes are in the blast radius. If no graph exists, suggest `/graphify .` then continue.
|
|
18
|
+
|
|
19
|
+
### 2 — Load context in priority order
|
|
20
|
+
|
|
21
|
+
1. **CSV task** — if `docs/dev-tasks/` exists, load the matching row. Fields `user_story`, `description`, `acceptance_criteria`, `dependencies` are the context.
|
|
22
|
+
2. **AGENTS.md** — load coding standards from project root if present.
|
|
23
|
+
3. **Architecture doc** — scan `docs/arch-docs/` for the relevant section.
|
|
24
|
+
4. **knowledge/** — check `knowledge/rules/`, `knowledge/patterns/`, `knowledge/prompts/dev/` for prior patterns.
|
|
25
|
+
|
|
26
|
+
### 3 — Output this summary, nothing else
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
EVALUATE SUMMARY
|
|
30
|
+
────────────────
|
|
31
|
+
Task: <what we are building>
|
|
32
|
+
Touches: <files / modules / graphify communities>
|
|
33
|
+
Depends on: <what must already exist>
|
|
34
|
+
Constraints: <from AGENTS.md, arch doc, acceptance criteria>
|
|
35
|
+
Risk: <god nodes or high-degree nodes in blast radius>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 4 — Stop
|
|
39
|
+
|
|
40
|
+
Do NOT plan. Do NOT write code. End with:
|
|
41
|
+
|
|
42
|
+
> "Ready to /plan. Type `/plan` when you want the implementation blueprint."
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# /plan
|
|
2
|
+
|
|
3
|
+
**PLAN** — Step 2 of the E→P→A→V cycle. Produce a blueprint. No code yet.
|
|
4
|
+
|
|
5
|
+
## Prerequisite
|
|
6
|
+
|
|
7
|
+
EVALUATE must have run first. If it hasn't, run `/evaluate <task>` before continuing.
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
|
|
11
|
+
### 1 — Blast radius check
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
graphify path "<primary file/module>" "<secondary file/module>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run for every significant file the plan will touch. State what else will be affected.
|
|
18
|
+
|
|
19
|
+
### 2 — Write the implementation blueprint
|
|
20
|
+
|
|
21
|
+
Structure the plan as numbered steps:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
PLAN
|
|
25
|
+
────
|
|
26
|
+
1. <file or module> — <what changes and why>
|
|
27
|
+
2. <file or module> — <what changes and why>
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
Files created: <list>
|
|
31
|
+
Files modified: <list>
|
|
32
|
+
Files deleted: <list>
|
|
33
|
+
|
|
34
|
+
Blast radius: <from graphify — what else references these>
|
|
35
|
+
God nodes touched: <list any with degree > 10>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 3 — State constraints
|
|
39
|
+
|
|
40
|
+
List which AGENTS.md rules and architecture decisions apply to this plan.
|
|
41
|
+
|
|
42
|
+
### 4 — Stop and wait
|
|
43
|
+
|
|
44
|
+
Do NOT write code. End with:
|
|
45
|
+
|
|
46
|
+
> "Waiting for approval. Reply `/apply` to implement, or give feedback to revise the plan."
|