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.
@@ -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."