pan-wizard 3.13.1 → 3.15.0

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 (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -5
  3. package/commands/pan/audit-deployment.md +384 -384
  4. package/commands/pan/focus-auto.md +683 -683
  5. package/commands/pan/focus-doc-audit.md +530 -530
  6. package/commands/pan/focus-drift-walking.md +525 -525
  7. package/commands/pan/git.md +1 -1
  8. package/commands/pan/hud.md +3 -2
  9. package/commands/pan/report.md +70 -0
  10. package/hooks/dist/pan-check-update.js +62 -62
  11. package/hooks/dist/pan-context-monitor.js +134 -122
  12. package/hooks/dist/pan-statusline.js +7 -1
  13. package/package.json +5 -5
  14. package/pan-wizard-core/bin/lib/config.cjs +14 -1
  15. package/pan-wizard-core/bin/lib/core.cjs +6 -2
  16. package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
  17. package/pan-wizard-core/bin/lib/focus.cjs +48 -2
  18. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
  19. package/pan-wizard-core/bin/lib/hud.cjs +202 -17
  20. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -2
  21. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  22. package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
  23. package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
  24. package/pan-wizard-core/bin/lib/phase.cjs +4 -4
  25. package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
  26. package/pan-wizard-core/bin/lib/utils.cjs +171 -171
  27. package/pan-wizard-core/bin/lib/verify.cjs +172 -61
  28. package/pan-wizard-core/bin/pan-tools.cjs +1499 -1463
  29. package/pan-wizard-core/references/checkpoints.md +776 -776
  30. package/pan-wizard-core/references/continuation-format.md +249 -249
  31. package/pan-wizard-core/references/questioning.md +145 -145
  32. package/pan-wizard-core/references/tdd.md +263 -263
  33. package/pan-wizard-core/references/ui-brand.md +160 -160
  34. package/pan-wizard-core/templates/config.json +38 -38
  35. package/pan-wizard-core/workflows/exec-phase.md +14 -0
  36. package/scripts/build-hooks.js +51 -51
  37. package/scripts/git-hooks/pre-commit +0 -0
  38. package/scripts/release-check.js +53 -47
  39. package/scripts/run-tests.cjs +44 -0
@@ -133,7 +133,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
133
133
  const baseExists = dirs.some(dir => dir.startsWith(normalized + '-') || dir === normalized);
134
134
 
135
135
  // Find existing decimal phases for this base
136
- const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
136
+ const decimalPattern = new RegExp(`^${escapeRegex(normalized)}\\.(\\d+)`);
137
137
  const existingDecimals = [];
138
138
 
139
139
  for (const dir of dirs) {
@@ -444,7 +444,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
444
444
  // Normalize input then strip leading zeros for flexible matching
445
445
  const normalizedAfter = normalizePhaseName(afterPhase);
446
446
  const unpadded = normalizedAfter.replace(/^0+/, '');
447
- const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
447
+ const afterPhaseEscaped = escapeRegex(unpadded);
448
448
  const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
449
449
  if (!targetPattern.test(content)) {
450
450
  error(`Phase ${afterPhase} not found in roadmap.md`);
@@ -458,7 +458,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
458
458
  try {
459
459
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
460
460
  const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name);
461
- const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
461
+ const decimalPattern = new RegExp(`^${escapeRegex(normalizedBase)}\\.(\\d+)`);
462
462
  for (const dir of dirs) {
463
463
  const decMatch = dir.match(decimalPattern);
464
464
  if (decMatch) existingDecimals.push(parseInt(decMatch[1], 10));
@@ -484,7 +484,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
484
484
  const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /pan:plan-phase ${decimalPhase} to break down)\n`;
485
485
 
486
486
  // Insert after the target phase section
487
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
487
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i'); // afterPhaseEscaped already run through escapeRegex above
488
488
  const headerMatch = content.match(headerPattern);
489
489
  if (!headerMatch) {
490
490
  error(`Could not find Phase ${afterPhase} header`);
@@ -198,7 +198,9 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
198
198
  lines.push('|----------|--------|----------|-------------|------|');
199
199
  for (const f of payload.findings) {
200
200
  const loc = f.file ? `\`${f.file}${f.line ? `:${f.line}` : ''}\`` : '—';
201
- const desc = f.description.replace(/\|/g, '\\|');
201
+ // Neutralize markdown-table-breaking chars: escape pipes and flatten any
202
+ // newlines so a finding description can't corrupt the table structure.
203
+ const desc = String(f.description).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ');
202
204
  lines.push(`| ${f.severity} | ${f.source} | ${f.category} | ${desc} | ${loc} |`);
203
205
  }
204
206
  lines.push('');
@@ -1,171 +1,171 @@
1
- /**
2
- * Utils — Shared utility functions used across multiple modules
3
- *
4
- * Functions here were previously duplicated in core.cjs, commands.cjs,
5
- * state.cjs, and phase.cjs. Now centralized for single-source-of-truth.
6
- */
7
-
8
- const fs = require('fs');
9
- const os = require('os');
10
- const path = require('path');
11
- const {
12
- PLANNING_DIR,
13
- PHASES_DIR,
14
- MILESTONES_DIR,
15
- isPlanFile,
16
- isSummaryFile,
17
- PHASE_DIR_RE,
18
- } = require('./constants.cjs');
19
-
20
- // ─── File utilities ──────────────────────────────────────────────────────────
21
-
22
- /**
23
- * Read and parse a JSON file, returning null on any failure.
24
- * @param {string} filePath - Absolute path to the JSON file
25
- * @returns {Object|null} Parsed JSON object, or null if unreadable/unparseable
26
- */
27
- function readJsonFile(filePath) {
28
- try {
29
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- /**
36
- * Remove surrounding quotes (single or double) from a string.
37
- * @param {string} str - Input string possibly wrapped in quotes
38
- * @returns {string} String with leading/trailing quotes removed
39
- */
40
- function removeQuotes(str) {
41
- return str.replace(/^["']|["']$/g, '');
42
- }
43
-
44
- // ─── Phase directory utilities ───────────────────────────────────────────────
45
-
46
- /**
47
- * Build the absolute path to the .planning directory.
48
- * @param {string} cwd - Project root directory
49
- * @returns {string} Absolute path to .planning/
50
- */
51
- function planningPath(cwd) {
52
- return path.join(cwd, PLANNING_DIR);
53
- }
54
-
55
- /**
56
- * Build the absolute path to the phases directory.
57
- * @param {string} cwd - Project root directory
58
- * @returns {string} Absolute path to .planning/phases/
59
- */
60
- function phasesPath(cwd) {
61
- return path.join(cwd, PLANNING_DIR, PHASES_DIR);
62
- }
63
-
64
- /**
65
- * Build the absolute path to the milestones directory.
66
- * @param {string} cwd - Project root directory
67
- * @returns {string} Absolute path to .planning/milestones/
68
- */
69
- function milestonesPath(cwd) {
70
- return path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
71
- }
72
-
73
- /**
74
- * Read phase directories from the phases folder, sorted by phase number.
75
- * @param {string} cwd - Project root directory
76
- * @returns {string[]} Sorted array of directory names, or empty array on failure
77
- */
78
- function listPhaseDirs(cwd) {
79
- const { comparePhaseNum } = require('./core.cjs');
80
- try {
81
- const entries = fs.readdirSync(phasesPath(cwd), { withFileTypes: true });
82
- return entries
83
- .filter(e => e.isDirectory())
84
- .map(e => e.name)
85
- .sort((a, b) => comparePhaseNum(a, b));
86
- } catch {
87
- return [];
88
- }
89
- }
90
-
91
- /**
92
- * Filter an array of filenames to only plan files, sorted.
93
- * @param {string[]} files - Array of filenames
94
- * @returns {string[]} Sorted plan filenames
95
- */
96
- function filterPlanFiles(files) {
97
- return files.filter(isPlanFile).sort();
98
- }
99
-
100
- /**
101
- * Filter an array of filenames to only summary files, sorted.
102
- * @param {string[]} files - Array of filenames
103
- * @returns {string[]} Sorted summary filenames
104
- */
105
- function filterSummaryFiles(files) {
106
- return files.filter(isSummaryFile).sort();
107
- }
108
-
109
- /**
110
- * Extract the phase number and name from a phase directory name.
111
- * e.g. "01-setup-auth" → { number: "01", name: "setup-auth" }
112
- * @param {string} dirName - Phase directory name
113
- * @returns {{ number: string, name: string|null }} Parsed phase info
114
- */
115
- function parsePhaseDir(dirName) {
116
- const match = dirName.match(PHASE_DIR_RE);
117
- if (!match) return { number: dirName, name: null };
118
- return {
119
- number: match[1],
120
- name: match[2] || null,
121
- };
122
- }
123
-
124
- /**
125
- * Classify phase status from file counts. Returns a granular status string.
126
- * @param {number} planCount - Number of plan files
127
- * @param {number} summaryCount - Number of summary files
128
- * @param {{hasContext?: boolean, hasResearch?: boolean}} [flags] - Extra file flags
129
- * @returns {string} One of: 'complete', 'partial', 'planned', 'researched', 'discussed', 'empty'
130
- */
131
- function classifyPhaseStatus(planCount, summaryCount, flags = {}) {
132
- if (summaryCount >= planCount && planCount > 0) return 'complete';
133
- if (summaryCount > 0) return 'partial';
134
- if (planCount > 0) return 'planned';
135
- if (flags.hasResearch) return 'researched';
136
- if (flags.hasContext) return 'discussed';
137
- return 'empty';
138
- }
139
-
140
- /**
141
- * Check if a file is accessible (exists and is readable).
142
- * @param {string} filePath - Absolute path to check
143
- * @returns {boolean}
144
- */
145
- function fileAccessible(filePath) {
146
- try { fs.accessSync(filePath, fs.constants.R_OK); return true; } catch { return false; }
147
- }
148
-
149
- /**
150
- * Detect whether Brave Search API key is available (env var or key file).
151
- * @returns {boolean}
152
- */
153
- function hasBraveSearchKey() {
154
- if (process.env.BRAVE_API_KEY) return true;
155
- return fileAccessible(path.join(os.homedir(), '.pan-wizard', 'brave_api_key'));
156
- }
157
-
158
- module.exports = {
159
- readJsonFile,
160
- removeQuotes,
161
- planningPath,
162
- phasesPath,
163
- milestonesPath,
164
- listPhaseDirs,
165
- filterPlanFiles,
166
- filterSummaryFiles,
167
- parsePhaseDir,
168
- classifyPhaseStatus,
169
- fileAccessible,
170
- hasBraveSearchKey,
171
- };
1
+ /**
2
+ * Utils — Shared utility functions used across multiple modules
3
+ *
4
+ * Functions here were previously duplicated in core.cjs, commands.cjs,
5
+ * state.cjs, and phase.cjs. Now centralized for single-source-of-truth.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const {
12
+ PLANNING_DIR,
13
+ PHASES_DIR,
14
+ MILESTONES_DIR,
15
+ isPlanFile,
16
+ isSummaryFile,
17
+ PHASE_DIR_RE,
18
+ } = require('./constants.cjs');
19
+
20
+ // ─── File utilities ──────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Read and parse a JSON file, returning null on any failure.
24
+ * @param {string} filePath - Absolute path to the JSON file
25
+ * @returns {Object|null} Parsed JSON object, or null if unreadable/unparseable
26
+ */
27
+ function readJsonFile(filePath) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Remove surrounding quotes (single or double) from a string.
37
+ * @param {string} str - Input string possibly wrapped in quotes
38
+ * @returns {string} String with leading/trailing quotes removed
39
+ */
40
+ function removeQuotes(str) {
41
+ return str.replace(/^["']|["']$/g, '');
42
+ }
43
+
44
+ // ─── Phase directory utilities ───────────────────────────────────────────────
45
+
46
+ /**
47
+ * Build the absolute path to the .planning directory.
48
+ * @param {string} cwd - Project root directory
49
+ * @returns {string} Absolute path to .planning/
50
+ */
51
+ function planningPath(cwd) {
52
+ return path.join(cwd, PLANNING_DIR);
53
+ }
54
+
55
+ /**
56
+ * Build the absolute path to the phases directory.
57
+ * @param {string} cwd - Project root directory
58
+ * @returns {string} Absolute path to .planning/phases/
59
+ */
60
+ function phasesPath(cwd) {
61
+ return path.join(cwd, PLANNING_DIR, PHASES_DIR);
62
+ }
63
+
64
+ /**
65
+ * Build the absolute path to the milestones directory.
66
+ * @param {string} cwd - Project root directory
67
+ * @returns {string} Absolute path to .planning/milestones/
68
+ */
69
+ function milestonesPath(cwd) {
70
+ return path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
71
+ }
72
+
73
+ /**
74
+ * Read phase directories from the phases folder, sorted by phase number.
75
+ * @param {string} cwd - Project root directory
76
+ * @returns {string[]} Sorted array of directory names, or empty array on failure
77
+ */
78
+ function listPhaseDirs(cwd) {
79
+ const { comparePhaseNum } = require('./core.cjs');
80
+ try {
81
+ const entries = fs.readdirSync(phasesPath(cwd), { withFileTypes: true });
82
+ return entries
83
+ .filter(e => e.isDirectory())
84
+ .map(e => e.name)
85
+ .sort((a, b) => comparePhaseNum(a, b));
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Filter an array of filenames to only plan files, sorted.
93
+ * @param {string[]} files - Array of filenames
94
+ * @returns {string[]} Sorted plan filenames
95
+ */
96
+ function filterPlanFiles(files) {
97
+ return files.filter(isPlanFile).sort();
98
+ }
99
+
100
+ /**
101
+ * Filter an array of filenames to only summary files, sorted.
102
+ * @param {string[]} files - Array of filenames
103
+ * @returns {string[]} Sorted summary filenames
104
+ */
105
+ function filterSummaryFiles(files) {
106
+ return files.filter(isSummaryFile).sort();
107
+ }
108
+
109
+ /**
110
+ * Extract the phase number and name from a phase directory name.
111
+ * e.g. "01-setup-auth" → { number: "01", name: "setup-auth" }
112
+ * @param {string} dirName - Phase directory name
113
+ * @returns {{ number: string, name: string|null }} Parsed phase info
114
+ */
115
+ function parsePhaseDir(dirName) {
116
+ const match = dirName.match(PHASE_DIR_RE);
117
+ if (!match) return { number: dirName, name: null };
118
+ return {
119
+ number: match[1],
120
+ name: match[2] || null,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Classify phase status from file counts. Returns a granular status string.
126
+ * @param {number} planCount - Number of plan files
127
+ * @param {number} summaryCount - Number of summary files
128
+ * @param {{hasContext?: boolean, hasResearch?: boolean}} [flags] - Extra file flags
129
+ * @returns {string} One of: 'complete', 'partial', 'planned', 'researched', 'discussed', 'empty'
130
+ */
131
+ function classifyPhaseStatus(planCount, summaryCount, flags = {}) {
132
+ if (summaryCount >= planCount && planCount > 0) return 'complete';
133
+ if (summaryCount > 0) return 'partial';
134
+ if (planCount > 0) return 'planned';
135
+ if (flags.hasResearch) return 'researched';
136
+ if (flags.hasContext) return 'discussed';
137
+ return 'empty';
138
+ }
139
+
140
+ /**
141
+ * Check if a file is accessible (exists and is readable).
142
+ * @param {string} filePath - Absolute path to check
143
+ * @returns {boolean}
144
+ */
145
+ function fileAccessible(filePath) {
146
+ try { fs.accessSync(filePath, fs.constants.R_OK); return true; } catch { return false; }
147
+ }
148
+
149
+ /**
150
+ * Detect whether Brave Search API key is available (env var or key file).
151
+ * @returns {boolean}
152
+ */
153
+ function hasBraveSearchKey() {
154
+ if (process.env.BRAVE_API_KEY) return true;
155
+ return fileAccessible(path.join(os.homedir(), '.pan-wizard', 'brave_api_key'));
156
+ }
157
+
158
+ module.exports = {
159
+ readJsonFile,
160
+ removeQuotes,
161
+ planningPath,
162
+ phasesPath,
163
+ milestonesPath,
164
+ listPhaseDirs,
165
+ filterPlanFiles,
166
+ filterSummaryFiles,
167
+ parsePhaseDir,
168
+ classifyPhaseStatus,
169
+ fileAccessible,
170
+ hasBraveSearchKey,
171
+ };