pan-wizard 2.8.1 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +4 -2
  2. package/bin/install.js +23 -0
  3. package/commands/pan/assumptions.md +38 -3
  4. package/commands/pan/audit-deployment.md +6 -0
  5. package/commands/pan/debug.md +71 -2
  6. package/commands/pan/exec-phase.md +90 -0
  7. package/commands/pan/focus-auto.md +181 -18
  8. package/commands/pan/focus-design.md +302 -14
  9. package/commands/pan/focus-doc-audit.md +530 -0
  10. package/commands/pan/focus-drift-walking.md +525 -0
  11. package/commands/pan/focus-exec.md +168 -46
  12. package/commands/pan/focus-plan.md +204 -12
  13. package/commands/pan/focus-scan.md +17 -5
  14. package/commands/pan/map-codebase.md +32 -6
  15. package/commands/pan/milestone-audit.md +23 -0
  16. package/commands/pan/new-project.md +64 -0
  17. package/commands/pan/pause.md +42 -1
  18. package/commands/pan/plan-phase.md +84 -0
  19. package/commands/pan/profile.md +2 -1
  20. package/commands/pan/quick.md +15 -0
  21. package/commands/pan/resume.md +62 -2
  22. package/commands/pan/verify-phase.md +42 -0
  23. package/package.json +1 -1
  24. package/pan-wizard-core/bin/lib/commands.cjs +29 -7
  25. package/pan-wizard-core/bin/lib/config.cjs +10 -0
  26. package/pan-wizard-core/bin/lib/constants.cjs +3 -1
  27. package/pan-wizard-core/bin/lib/core.cjs +168 -21
  28. package/pan-wizard-core/bin/lib/focus.cjs +5 -0
  29. package/pan-wizard-core/bin/lib/verify.cjs +283 -4
  30. package/pan-wizard-core/bin/pan-tools.cjs +11 -2
  31. package/pan-wizard-core/references/model-profiles.md +191 -62
  32. package/pan-wizard-core/workflows/help.md +11 -1
  33. package/pan-wizard-core/workflows/profile.md +8 -1
  34. package/pan-wizard-core/workflows/settings.md +14 -0
  35. package/scripts/generate-skills-docs.py +560 -0
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate PAN Wizard skills documentation from source command files.
4
+
5
+ Reads all shipped skills (commands/pan/*.md) and dev skills (.claude/commands/*.md),
6
+ parses their YAML frontmatter and content, and produces two documents:
7
+
8
+ docs/SKILLS-REFERENCE.md — Organized summary with tables, descriptions, tool matrix
9
+ docs/SKILLS-FULL-TEXT.md — Complete unabridged prompt text of every skill
10
+
11
+ Usage:
12
+ python scripts/generate-skills-docs.py # from repo root
13
+ python scripts/generate-skills-docs.py --dry-run # preview without writing
14
+ python scripts/generate-skills-docs.py --full-only # only generate SKILLS-FULL-TEXT.md
15
+ python scripts/generate-skills-docs.py --ref-only # only generate SKILLS-REFERENCE.md
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Configuration
26
+ # ---------------------------------------------------------------------------
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parent.parent
29
+ SHIPPED_DIR = REPO_ROOT / "commands" / "pan"
30
+ DEV_DIR = REPO_ROOT / ".claude" / "commands"
31
+ PACKAGE_JSON = REPO_ROOT / "package.json"
32
+ OUT_REFERENCE = REPO_ROOT / "docs" / "SKILLS-REFERENCE.md"
33
+ OUT_FULL_TEXT = REPO_ROOT / "docs" / "SKILLS-FULL-TEXT.md"
34
+
35
+ # Group ordering for the reference doc (shipped skills)
36
+ GROUP_ORDER = [
37
+ "Getting Started",
38
+ "Phase Lifecycle",
39
+ "Phase Management",
40
+ "Focus",
41
+ "Milestone",
42
+ "Milestone Lifecycle",
43
+ "Session & Progress",
44
+ "System",
45
+ "Community",
46
+ ]
47
+
48
+ # Dev skill categorization (filename -> category)
49
+ DEV_CATEGORIES = {
50
+ "Development Workflow": [
51
+ "pandev", "execplan", "superplan", "featureAI", "review",
52
+ ],
53
+ "Testing & Verification": [
54
+ "test", "quick", "pantest", "check", "check-platform", "auditai",
55
+ ],
56
+ "Documentation & Audit": [
57
+ "doc-audit", "docs", "sync",
58
+ ],
59
+ "Build & Deploy": [
60
+ "build", "run", "commit",
61
+ ],
62
+ "Session Management": [
63
+ "session-start", "session-end",
64
+ ],
65
+ }
66
+
67
+ # All tools that shipped skills can reference
68
+ ALL_TOOLS = [
69
+ "Read", "Write", "Edit", "Bash", "Grep", "Glob",
70
+ "Agent", "Task", "TodoWrite", "AskUserQuestion",
71
+ "SlashCommand", "WebSearch", "WebFetch",
72
+ ]
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Frontmatter parser
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def parse_frontmatter(content: str) -> tuple[dict, str]:
79
+ """Parse YAML frontmatter from markdown content.
80
+
81
+ Returns (metadata_dict, body_after_frontmatter).
82
+ If no frontmatter found, returns (empty dict, full content).
83
+ """
84
+ if not content.startswith("---"):
85
+ return {}, content
86
+
87
+ end = content.find("---", 3)
88
+ if end == -1:
89
+ return {}, content
90
+
91
+ raw = content[3:end].strip()
92
+ body = content[end + 3:].strip()
93
+ meta = {}
94
+
95
+ current_key = None
96
+ current_list = None
97
+
98
+ for line in raw.split("\n"):
99
+ stripped = line.strip()
100
+ if not stripped:
101
+ continue
102
+
103
+ # List item under a key
104
+ if stripped.startswith("- ") and current_key:
105
+ if current_list is None:
106
+ current_list = []
107
+ current_list.append(stripped[2:].strip())
108
+ meta[current_key] = current_list
109
+ continue
110
+
111
+ # Key: value pair
112
+ if ":" in stripped:
113
+ # Save previous list if any
114
+ if current_list is not None:
115
+ current_list = None
116
+
117
+ colon_idx = stripped.index(":")
118
+ key = stripped[:colon_idx].strip()
119
+ value = stripped[colon_idx + 1:].strip()
120
+
121
+ current_key = key
122
+
123
+ if value:
124
+ # Strip quotes
125
+ if value.startswith('"') and value.endswith('"'):
126
+ value = value[1:-1]
127
+ elif value.startswith("'") and value.endswith("'"):
128
+ value = value[1:-1]
129
+ meta[key] = value
130
+ current_list = None
131
+ else:
132
+ # Value might be a list on following lines
133
+ current_list = []
134
+ meta[key] = current_list
135
+
136
+ return meta, body
137
+
138
+
139
+ def extract_first_heading(body: str) -> str:
140
+ """Extract the first # heading from the body."""
141
+ for line in body.split("\n"):
142
+ line = line.strip()
143
+ if line.startswith("# "):
144
+ return line[2:].strip()
145
+ return ""
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Skill loading
150
+ # ---------------------------------------------------------------------------
151
+
152
+ class Skill:
153
+ def __init__(self, filepath: Path, is_dev: bool = False):
154
+ self.filepath = filepath
155
+ self.is_dev = is_dev
156
+ self.filename = filepath.stem # e.g., "focus-auto"
157
+ self.content = filepath.read_text(encoding="utf-8")
158
+ self.line_count = self.content.count("\n") + (1 if not self.content.endswith("\n") else 0)
159
+ self.meta, self.body = parse_frontmatter(self.content)
160
+
161
+ # Derive fields
162
+ if is_dev:
163
+ self.name = self.meta.get("name", f"/{self.filename}")
164
+ self.command = f"/{self.filename}"
165
+ else:
166
+ raw_name = self.meta.get("name", f"pan:{self.filename}")
167
+ # Normalize to /pan: prefix
168
+ if not raw_name.startswith("pan:") and not raw_name.startswith("/pan:"):
169
+ self.name = f"/pan:{raw_name}"
170
+ elif raw_name.startswith("pan:"):
171
+ self.name = f"/{raw_name}"
172
+ else:
173
+ self.name = raw_name
174
+ self.command = self.name
175
+
176
+ self.group = self.meta.get("group", "System")
177
+ self.description = self.meta.get("description", "")
178
+ raw_tools = self.meta.get("allowed-tools", [])
179
+ if isinstance(raw_tools, str):
180
+ # Handle comma-separated format: "Read, Write, Edit"
181
+ self.tools = [t.strip() for t in raw_tools.split(",") if t.strip()]
182
+ elif isinstance(raw_tools, list):
183
+ self.tools = raw_tools
184
+ else:
185
+ self.tools = []
186
+ self.argument_hint = self.meta.get("argument-hint", "")
187
+ self.heading = extract_first_heading(self.body)
188
+
189
+
190
+ def load_shipped_skills() -> list[Skill]:
191
+ """Load all shipped skills from commands/pan/."""
192
+ if not SHIPPED_DIR.exists():
193
+ print(f"Warning: {SHIPPED_DIR} not found", file=sys.stderr)
194
+ return []
195
+ skills = []
196
+ for f in sorted(SHIPPED_DIR.glob("*.md")):
197
+ skills.append(Skill(f, is_dev=False))
198
+ return skills
199
+
200
+
201
+ def load_dev_skills() -> list[Skill]:
202
+ """Load all dev skills from .claude/commands/."""
203
+ if not DEV_DIR.exists():
204
+ print(f"Warning: {DEV_DIR} not found", file=sys.stderr)
205
+ return []
206
+ skills = []
207
+ for f in sorted(DEV_DIR.glob("*.md")):
208
+ skills.append(Skill(f, is_dev=True))
209
+ return skills
210
+
211
+
212
+ def get_version() -> str:
213
+ """Read version from package.json."""
214
+ try:
215
+ with open(PACKAGE_JSON, encoding="utf-8") as f:
216
+ return json.load(f)["version"]
217
+ except (FileNotFoundError, KeyError):
218
+ return "unknown"
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # SKILLS-FULL-TEXT.md generator
223
+ # ---------------------------------------------------------------------------
224
+
225
+ def generate_full_text(shipped: list[Skill], dev: list[Skill], version: str) -> str:
226
+ """Generate the complete full-text document."""
227
+ lines = []
228
+ w = lines.append
229
+
230
+ w("# PAN Wizard — Complete Skills Full Text")
231
+ w("")
232
+ w("Every skill (slash command) available in PAN Wizard, reproduced in full.")
233
+ w("This is the actual prompt text that Claude receives when a skill is invoked.")
234
+ w("")
235
+ w(f"**Version:** {version} | **Shipped:** {len(shipped)} skills | **Dev:** {len(dev)} skills | **Total:** {len(shipped) + len(dev)}")
236
+ w("")
237
+ w("> Auto-generated by `scripts/generate-skills-docs.py` — do not edit manually.")
238
+ w("")
239
+ w("---")
240
+ w("")
241
+ w(f"## Part 1: Shipped Skills ({len(shipped)})")
242
+ w("")
243
+ w("These are installed into host projects via the PAN installer.")
244
+
245
+ for skill in shipped:
246
+ w("")
247
+ w("---")
248
+ w("")
249
+ w(f"### {skill.command} ({skill.line_count} lines)")
250
+ w("")
251
+ w("```markdown")
252
+ w(skill.content.rstrip())
253
+ w("```")
254
+ w("")
255
+
256
+ w("")
257
+ w("---")
258
+ w("")
259
+ w(f"## Part 2: Dev Skills ({len(dev)})")
260
+ w("")
261
+ w("These exist only in the PAN source repository and are NOT shipped to end users.")
262
+
263
+ for skill in dev:
264
+ w("")
265
+ w("---")
266
+ w("")
267
+ w(f"### {skill.command} ({skill.line_count} lines)")
268
+ w("")
269
+ w("```markdown")
270
+ w(skill.content.rstrip())
271
+ w("```")
272
+ w("")
273
+
274
+ return "\n".join(lines)
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # SKILLS-REFERENCE.md generator
279
+ # ---------------------------------------------------------------------------
280
+
281
+ def group_skills_by(skills: list[Skill]) -> dict[str, list[Skill]]:
282
+ """Group shipped skills by their group field."""
283
+ groups: dict[str, list[Skill]] = {}
284
+ for s in skills:
285
+ groups.setdefault(s.group, []).append(s)
286
+ return groups
287
+
288
+
289
+ def dev_category_for(filename: str) -> str:
290
+ """Find which dev category a filename belongs to."""
291
+ for cat, members in DEV_CATEGORIES.items():
292
+ if filename in members:
293
+ return cat
294
+ return "Other"
295
+
296
+
297
+ def tools_cell(tools: list[str]) -> str:
298
+ """Format tools list for a table cell."""
299
+ if not tools:
300
+ return "*(none)*"
301
+ return ", ".join(tools)
302
+
303
+
304
+ def tool_matrix_row(skill: Skill, all_tools: list[str]) -> str:
305
+ """Generate a tool access matrix row."""
306
+ cells = []
307
+ for t in all_tools:
308
+ # Handle mcp__context7__* wildcard
309
+ if t in skill.tools:
310
+ cells.append("x")
311
+ else:
312
+ # Check for mcp prefix tools
313
+ has_mcp = any(tool.startswith("mcp__") for tool in skill.tools)
314
+ if t == "mcp" and has_mcp:
315
+ cells.append("x")
316
+ else:
317
+ cells.append("")
318
+ name = skill.command.replace("/pan:", "")
319
+ return f"| {name} | " + " | ".join(cells) + " |"
320
+
321
+
322
+ def generate_reference(shipped: list[Skill], dev: list[Skill], version: str) -> str:
323
+ """Generate the reference summary document."""
324
+ lines = []
325
+ w = lines.append
326
+
327
+ w("# PAN Wizard — Skills Reference")
328
+ w("")
329
+ w("Complete catalog of every skill (slash command) available in PAN Wizard, organized by purpose.")
330
+ w("Each entry shows the command, what it does, what tools it uses, and when to reach for it.")
331
+ w("")
332
+ w(f"**Version:** {version} | **Total:** {len(shipped)} shipped skills + {len(dev)} dev skills = {len(shipped) + len(dev)}")
333
+ w("")
334
+ w("> Auto-generated by `scripts/generate-skills-docs.py` — do not edit manually.")
335
+ w("")
336
+ w("---")
337
+ w("")
338
+
339
+ # ── Table of Contents ──
340
+ grouped = group_skills_by(shipped)
341
+ ordered_groups = [g for g in GROUP_ORDER if g in grouped]
342
+ # Add any groups not in GROUP_ORDER
343
+ for g in grouped:
344
+ if g not in ordered_groups:
345
+ ordered_groups.append(g)
346
+
347
+ w("## Table of Contents")
348
+ w("")
349
+ w(f"- [Shipped Skills ({len(shipped)})](#shipped-skills-{len(shipped)}) — installed into host projects")
350
+ for g in ordered_groups:
351
+ anchor = g.lower().replace(" & ", "--").replace(" ", "-")
352
+ w(f" - [{g}](#{anchor})")
353
+ w(f"- [Dev Skills ({len(dev)})](#dev-skills-{len(dev)}) — PAN source repo only")
354
+ for cat in DEV_CATEGORIES:
355
+ anchor = cat.lower().replace(" & ", "--").replace(" ", "-")
356
+ w(f" - [{cat}](#{anchor})")
357
+ w("")
358
+ w("---")
359
+ w("")
360
+
361
+ # ── Shipped Skills ──
362
+ w(f"## Shipped Skills ({len(shipped)})")
363
+ w("")
364
+ w("These are installed into host projects via the PAN installer and available to end users.")
365
+ w("")
366
+
367
+ for group_name in ordered_groups:
368
+ group_skills = grouped[group_name]
369
+ w(f"### {group_name}")
370
+ w("")
371
+
372
+ # Summary table
373
+ w("| Skill | Tools | Description |")
374
+ w("|-------|-------|-------------|")
375
+ for s in group_skills:
376
+ w(f"| `{s.command}` | {tools_cell(s.tools)} | {s.description} |")
377
+ w("")
378
+
379
+ # Individual entries
380
+ for s in group_skills:
381
+ w(f"#### {s.command}")
382
+ w("")
383
+ if s.description:
384
+ w(s.description)
385
+ w("")
386
+ if s.argument_hint:
387
+ w(f"```")
388
+ w(f"{s.command} {s.argument_hint}")
389
+ w(f"```")
390
+ else:
391
+ w(f"```")
392
+ w(f"{s.command}")
393
+ w(f"```")
394
+ w("")
395
+ w(f"**Tools:** {tools_cell(s.tools)} ")
396
+ w(f"**Group:** {s.group} ")
397
+ w(f"**Lines:** {s.line_count}")
398
+ w("")
399
+
400
+ w("---")
401
+ w("")
402
+
403
+ # ── Dev Skills ──
404
+ w(f"## Dev Skills ({len(dev)})")
405
+ w("")
406
+ w("These exist only in the PAN source repository (`.claude/commands/`) and are NOT shipped to end users.")
407
+ w("")
408
+
409
+ dev_by_cat: dict[str, list[Skill]] = {}
410
+ for s in dev:
411
+ cat = dev_category_for(s.filename)
412
+ dev_by_cat.setdefault(cat, []).append(s)
413
+
414
+ for cat_name in DEV_CATEGORIES:
415
+ if cat_name not in dev_by_cat:
416
+ continue
417
+ cat_skills = dev_by_cat[cat_name]
418
+ w(f"### {cat_name}")
419
+ w("")
420
+ w("| Skill | Description |")
421
+ w("|-------|-------------|")
422
+ for s in cat_skills:
423
+ desc = s.description or s.heading or s.filename
424
+ w(f"| `{s.command}` | {desc} |")
425
+ w("")
426
+
427
+ for s in cat_skills:
428
+ w(f"#### {s.command}")
429
+ w("")
430
+ desc = s.description or s.heading or ""
431
+ if desc:
432
+ w(desc)
433
+ w("")
434
+ w(f"**Lines:** {s.line_count}")
435
+ w("")
436
+
437
+ w("---")
438
+ w("")
439
+
440
+ # Handle any dev skills not in a named category
441
+ other_skills = dev_by_cat.get("Other", [])
442
+ if other_skills:
443
+ w("### Other")
444
+ w("")
445
+ w("| Skill | Description |")
446
+ w("|-------|-------------|")
447
+ for s in other_skills:
448
+ desc = s.description or s.heading or s.filename
449
+ w(f"| `{s.command}` | {desc} |")
450
+ w("")
451
+ for s in other_skills:
452
+ w(f"#### {s.command}")
453
+ w("")
454
+ if s.description:
455
+ w(s.description)
456
+ w("")
457
+ w(f"**Lines:** {s.line_count}")
458
+ w("")
459
+ w("---")
460
+ w("")
461
+
462
+ # ── Tool Access Matrix ──
463
+ w("## Tool Access Matrix")
464
+ w("")
465
+ w("Which tools each shipped skill can use:")
466
+ w("")
467
+
468
+ # Header
469
+ tool_short = ["Read", "Write", "Edit", "Bash", "Grep", "Glob",
470
+ "Agent", "Task", "TodoWrite", "AskUser",
471
+ "SlashCmd", "WebSearch", "WebFetch", "mcp"]
472
+ tool_full = ["Read", "Write", "Edit", "Bash", "Grep", "Glob",
473
+ "Agent", "Task", "TodoWrite", "AskUserQuestion",
474
+ "SlashCommand", "WebSearch", "WebFetch"]
475
+ header = "| Skill | " + " | ".join(tool_short) + " |"
476
+ sep = "|-------|" + "|".join([":---:" for _ in tool_short]) + "|"
477
+ w(header)
478
+ w(sep)
479
+
480
+ for s in shipped:
481
+ cells = []
482
+ for short, full in zip(tool_short, tool_full + ["mcp"]):
483
+ if full == "mcp":
484
+ has_mcp = any(t.startswith("mcp__") for t in s.tools)
485
+ cells.append("x" if has_mcp else "")
486
+ elif full in s.tools:
487
+ cells.append("x")
488
+ else:
489
+ cells.append("")
490
+ name = s.command.replace("/pan:", "")
491
+ w(f"| {name} | " + " | ".join(cells) + " |")
492
+
493
+ w("")
494
+
495
+ # ── Quick Reference ──
496
+ w("---")
497
+ w("")
498
+ w("## Quick Reference — Common Workflows")
499
+ w("")
500
+ w("| Scenario | Skill sequence |")
501
+ w("|----------|---------------|")
502
+ w("| Greenfield project | `new-project` > `discuss-phase` > `plan-phase` > `exec-phase` > `verify-phase` |")
503
+ w("| Brownfield project | `map-codebase` > `new-project` > `discuss-phase` > `plan-phase` > `exec-phase` > `verify-phase` |")
504
+ w("| Quick bug fix | `quick` |")
505
+ w("| Substantial ad-hoc work | `quick --full` |")
506
+ w("| Start of day | `progress` > `resume` |")
507
+ w("| End of day | `pause` |")
508
+ w("| New version cycle | `milestone-done` > `milestone-new` |")
509
+ w("| Automated from PRD | `new-project --auto @prd.md` > `plan-phase --auto` > `exec-phase` > `verify-phase` |")
510
+ w("| Continuous improvement | `focus-auto --category <cat>` |")
511
+ w("| Execute micro-prompts | `focus-auto --category prompts` |")
512
+ w("| Feature investigation | `focus-design \"description\"` |")
513
+ w("| Doc hygiene | `focus-drift-walking` or `focus-doc-audit` |")
514
+ w("")
515
+
516
+ return "\n".join(lines)
517
+
518
+
519
+ # ---------------------------------------------------------------------------
520
+ # Main
521
+ # ---------------------------------------------------------------------------
522
+
523
+ def main():
524
+ dry_run = "--dry-run" in sys.argv
525
+ full_only = "--full-only" in sys.argv
526
+ ref_only = "--ref-only" in sys.argv
527
+
528
+ version = get_version()
529
+ shipped = load_shipped_skills()
530
+ dev = load_dev_skills()
531
+
532
+ print(f"PAN Wizard v{version}")
533
+ print(f"Shipped skills: {len(shipped)} (from {SHIPPED_DIR})")
534
+ print(f"Dev skills: {len(dev)} (from {DEV_DIR})")
535
+ print()
536
+
537
+ if not ref_only:
538
+ full_text = generate_full_text(shipped, dev, version)
539
+ full_lines = full_text.count("\n") + 1
540
+ if dry_run:
541
+ print(f"[dry-run] Would write {OUT_FULL_TEXT} ({full_lines} lines)")
542
+ else:
543
+ OUT_FULL_TEXT.write_text(full_text, encoding="utf-8")
544
+ print(f"Wrote {OUT_FULL_TEXT} ({full_lines} lines)")
545
+
546
+ if not full_only:
547
+ reference = generate_reference(shipped, dev, version)
548
+ ref_lines = reference.count("\n") + 1
549
+ if dry_run:
550
+ print(f"[dry-run] Would write {OUT_REFERENCE} ({ref_lines} lines)")
551
+ else:
552
+ OUT_REFERENCE.write_text(reference, encoding="utf-8")
553
+ print(f"Wrote {OUT_REFERENCE} ({ref_lines} lines)")
554
+
555
+ print()
556
+ print("Done.")
557
+
558
+
559
+ if __name__ == "__main__":
560
+ main()