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
@@ -147,7 +147,13 @@ function cmdDocLintSchemaCheck(cwd, schemaPath, opts = {}) {
147
147
  const COUNT_PATTERNS = [
148
148
  // "52 commands", "21 agents", "30 modules", "2667 tests", etc.
149
149
  // Word boundaries + allowed plurals; case-insensitive matching.
150
- { re: /(?<!\.)\b(\d+)\s+(commands?|agents?|modules?|workflows?|templates?|references?|specs?|adrs?|test\s+files?|test\s+suites?)\b/gi,
150
+ // Bare "N tests" / "N hooks" added (previously only "(N tests)" and
151
+ // "test files/suites" matched, so "3115 tests" slipped). Adjective-separated
152
+ // ("slash commands") and hyphen-compound ("sub-agents") variants are left
153
+ // uncaught on purpose — broadening to them also matched years ("2026
154
+ // multi-agent") and narrative, breaking the docs-clean invariant; the
155
+ // CLAUDE.md-table self-audit test is the stronger backstop for the counts.
156
+ { re: /(?<!\.)\b(\d+)\s+(commands?|agents?|modules?|workflows?|templates?|references?|specs?|adrs?|hooks?|test\s+files?|test\s+suites?|tests?)\b/gi,
151
157
  label: 'noun-phrase count' },
152
158
  // "27th module", "21st agent", "52nd command" — drift-prone ordinals
153
159
  { re: /(?<!\.)\b(\d+)(th|st|nd|rd)\s+(module|reference|agent|command|template|hook|workflow|spec|adr)\b/gi,
@@ -277,10 +283,89 @@ function cmdDocLintCounts(cwd, dir, opts = {}) {
277
283
  process.exit(violations.length > 0 ? 1 : 0);
278
284
  }
279
285
 
286
+ // ─── Aspirational-flag checker (ADR-0036 review — closes the "documented CLI
287
+ // flag that doesn't exist in the parser" gap). Heuristic: a `--flag` that
288
+ // appears in a doc line referencing the PAN CLI but never appears as a literal
289
+ // anywhere in the source is very likely fake or stale. Scoped to PAN-CLI lines
290
+ // to avoid flagging unrelated tool flags (git/npm/node). ─────────────────────
291
+ function collectSourceFlags(cwd, roots) {
292
+ const flags = new Set();
293
+ const walk = (dir) => {
294
+ let entries;
295
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
296
+ for (const e of entries) {
297
+ const fp = path.join(dir, e.name);
298
+ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(fp); }
299
+ else if (/\.(cjs|js|mjs)$/.test(e.name)) {
300
+ let c = ''; try { c = fs.readFileSync(fp, 'utf-8'); } catch { continue; }
301
+ for (const m of c.matchAll(/--[a-z][a-z0-9-]+/g)) flags.add(m[0]);
302
+ }
303
+ }
304
+ };
305
+ for (const r of roots) walk(path.join(cwd, r));
306
+ return flags;
307
+ }
308
+
309
+ function scanDocFlags(cwd, opts = {}) {
310
+ const sourceFlags = collectSourceFlags(cwd, opts.sourceRoots || ['pan-wizard-core/bin', 'bin']);
311
+ const docDirs = opts.docDirs || ['docs'];
312
+ const collected = [];
313
+ for (const d of docDirs) {
314
+ const abs = path.join(cwd, d);
315
+ if (!fs.existsSync(abs)) continue;
316
+ for (const f of walkMarkdownFiles(abs)) {
317
+ if (f.readError) continue;
318
+ collected.push({ rel: path.join(d, f.relativePath).replace(/\\/g, '/'), content: f.content });
319
+ }
320
+ }
321
+ for (const rel of (opts.files || [])) {
322
+ try { collected.push({ rel: rel.replace(/\\/g, '/'), content: fs.readFileSync(path.join(cwd, rel), 'utf-8') }); } catch { /* skip */ }
323
+ }
324
+ // Scope to `pan-tools <cmd>` lines ONLY — that is the surface whose flags are
325
+ // parsed in bin/ source. Slash-command flags (`/pan:exec-phase --gaps-only`)
326
+ // are a different surface: they are parsed by the command/workflow markdown
327
+ // prompts, so they legitimately never appear in bin/ source and must not be
328
+ // flagged here.
329
+ const CLI_CTX = /\bpan-tools\b/;
330
+ const violations = [];
331
+ for (const file of collected) {
332
+ // Skip frozen/aspirational docs (feature specs, ADRs, experiments, archive)
333
+ // — by design they describe proposed/future flags, same allowlist as counts.
334
+ if (isCountAllowed(file.rel)) continue;
335
+ const lines = file.content.split(/\r?\n/);
336
+ for (let i = 0; i < lines.length; i++) {
337
+ if (!CLI_CTX.test(lines[i])) continue;
338
+ // Lookbehind excludes mid-token double-dashes — notably markdown anchors
339
+ // like `#army--project-dashboard` — so only real ` --flag` tokens match.
340
+ for (const m of lines[i].matchAll(/(?<![\w#-])(--[a-z][a-z0-9-]+)/g)) {
341
+ if (!sourceFlags.has(m[1])) violations.push({ file: file.rel, line: i + 1, flag: m[1] });
342
+ }
343
+ }
344
+ }
345
+ return { source_flags: sourceFlags.size, doc_files: collected.length, violation_count: violations.length, violations };
346
+ }
347
+
348
+ function cmdDocLintFlags(cwd, opts = {}, raw) {
349
+ const r = scanDocFlags(cwd, opts);
350
+ if (raw) {
351
+ if (r.violation_count === 0) {
352
+ process.stdout.write(`OK — ${r.doc_files} docs scanned against ${r.source_flags} source flags, no aspirational CLI flags\n`);
353
+ } else {
354
+ for (const v of r.violations) process.stdout.write(`${v.file}:${v.line} — ${v.flag} (documented for the PAN CLI but not found in source)\n`);
355
+ process.stdout.write(`\n${r.violation_count} aspirational flag(s)\n`);
356
+ }
357
+ } else {
358
+ output(r, false);
359
+ }
360
+ process.exit(r.violation_count > 0 ? 1 : 0);
361
+ }
362
+
280
363
  module.exports = {
281
364
  cmdDocLint,
282
365
  cmdDocLintSchemaCheck,
283
366
  cmdDocLintCounts,
367
+ scanDocFlags,
368
+ cmdDocLintFlags,
284
369
  isCountAllowed,
285
370
  COUNT_PATTERNS,
286
371
  DEFAULT_SCHEMA_PATH,
@@ -8,7 +8,7 @@
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
- const { output, error, safeReadFile, loadConfig, scanPendingTodos, scanSourceTodos, toPosix, isGitRepo, execGit } = require('./core.cjs');
11
+ const { output, error, safeReadFile, loadConfig, scanPendingTodos, scanSourceTodos, toPosix, isGitRepo, execGit, escapeRegex } = require('./core.cjs');
12
12
  const {
13
13
  PLANNING_DIR, PHASES_DIR, ROADMAP_FILE, PATTERNS_FILE, EFFORT_POINTS, PRIORITY_LEVELS, EFFORT_SIZES,
14
14
  FOCUS_MODES, FOCUS_TIERS, FOCUS_DIR,
@@ -457,7 +457,7 @@ function checkDocStaleness(cwd, opts) {
457
457
  function checkOldCommandNames(content, file, stale) {
458
458
  for (const [oldName, newName] of Object.entries(COMMAND_RENAME_MAP)) {
459
459
  // Match /pan:old-name or pan:old-name (command references)
460
- const pattern = new RegExp(`pan:${oldName.replace(/-/g, '\\-')}\\b`);
460
+ const pattern = new RegExp(`pan:${escapeRegex(oldName)}\\b`);
461
461
  if (pattern.test(content)) {
462
462
  stale.push({ file, entity: 'renamed_command', old: oldName, new: newName });
463
463
  }
@@ -717,6 +717,45 @@ function focusAutoStop(cwd, raw) {
717
717
  }, raw);
718
718
  }
719
719
 
720
+ // Anti-fake (ADR-0036 review): the regression breaker compares tests_after vs
721
+ // tests_before, both historically agent-supplied CLI args that focus-auto never
722
+ // re-ran — so a fabricated "tests green" passed unchecked. When verification is
723
+ // enabled (config.focus.verify_tests), re-run the node:test suite and use the
724
+ // REAL pass count; otherwise keep the supplied value but mark the cycle
725
+ // tests_verified:false so the loop's trust is visible to the HUD/audit/human.
726
+ //
727
+ // Invocation is a fixed execFile('node', ['--test']) — NO shell and NO
728
+ // configurable command string, so there is no command-injection surface (same
729
+ // safe pattern verify.cjs uses). If no node:test suite is present, fall back to
730
+ // the supplied value rather than mis-firing the regression breaker.
731
+ function verifyTestCount(cwd, config, fallback) {
732
+ const focusCfg = (config && config.focus) || {};
733
+ if (!focusCfg.verify_tests) return { tests_after: fallback, verified: false };
734
+ const { execFileSync } = require('child_process');
735
+ const num = (out, re) => { const m = String(out || '').match(re); return m ? Number(m[1]) : null; };
736
+ const interpret = (out, exitCode) => {
737
+ // node --test summary lines use either the TAP marker ("# tests N") or the
738
+ // spec reporter's info marker ("ℹ tests N"), depending on Node version /
739
+ // reporter — match both so the count is captured regardless.
740
+ const total = num(out, /[#ℹ]\s*tests\s+(\d+)/);
741
+ if (!total) return { tests_after: fallback, verified: false }; // no node:test suite here
742
+ const passed = num(out, /[#ℹ]\s*pass\s+(\d+)/);
743
+ return { tests_after: passed !== null ? passed : (exitCode === 0 ? fallback : 0), verified: true, exit_code: exitCode };
744
+ };
745
+ // Strip any NODE_TEST_* context so the child runs as a standalone runner with
746
+ // predictable spec/TAP output — otherwise, when focus-auto is itself invoked
747
+ // from within a `node --test` process, the child inherits the nested-test
748
+ // context and emits a serialized format we can't parse.
749
+ const env = { ...process.env };
750
+ for (const k of Object.keys(env)) { if (k.startsWith('NODE_TEST')) delete env[k]; }
751
+ try {
752
+ const out = execFileSync('node', ['--test'], { cwd, timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', env });
753
+ return interpret(out, 0);
754
+ } catch (err) {
755
+ return interpret(err && err.stdout, (err && err.status) || 1);
756
+ }
757
+ }
758
+
720
759
  function focusAutoUpdate(cwd, raw, getVal) {
721
760
  const run = readAutoRun(cwd);
722
761
  if (!run) return error('No auto-run in progress. Cannot update.');
@@ -735,6 +774,12 @@ function focusAutoUpdate(cwd, raw, getVal) {
735
774
  timestamp: new Date().toISOString(),
736
775
  };
737
776
 
777
+ // Anti-fake: re-run the suite when verification is enabled; else record that
778
+ // this count was self-reported (tests_verified:false) so the trust is visible.
779
+ const tv = verifyTestCount(cwd, loadConfig(cwd), cycle.tests_after);
780
+ cycle.tests_after = tv.tests_after;
781
+ cycle.tests_verified = tv.verified;
782
+
738
783
  if (!run.cycles) run.cycles = [];
739
784
  run.cycles.push(cycle);
740
785
 
@@ -1016,6 +1061,7 @@ module.exports = {
1016
1061
  writeAutoRun,
1017
1062
  cmdFocusAuto,
1018
1063
  determineStopReason,
1064
+ verifyTestCount,
1019
1065
  // Opus 4.7
1020
1066
  determineContinuation,
1021
1067
  classifyStageDependencies,