pan-wizard 3.34.0 → 3.35.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 (76) hide show
  1. package/README.md +6 -6
  2. package/agents/pan-conductor.md +2 -2
  3. package/agents/pan-debugger.md +3 -1
  4. package/agents/pan-design-checker.md +1 -0
  5. package/agents/pan-executor.md +18 -1
  6. package/agents/pan-experiment-runner.md +1 -1
  7. package/agents/pan-hardener.md +1 -1
  8. package/agents/pan-meta-reviewer.md +1 -1
  9. package/agents/pan-phase-researcher.md +2 -2
  10. package/agents/pan-plan-checker.md +9 -8
  11. package/agents/pan-planner.md +3 -1
  12. package/agents/pan-project-researcher.md +1 -1
  13. package/agents/pan-research-synthesizer.md +1 -1
  14. package/agents/pan-reviewer.md +3 -2
  15. package/agents/pan-verifier.md +1 -1
  16. package/bin/install-lib.cjs +119 -16
  17. package/bin/install.js +34 -62
  18. package/commands/pan/cost.md +0 -4
  19. package/commands/pan/dashboard.md +0 -3
  20. package/commands/pan/design-phase.md +2 -2
  21. package/commands/pan/hud.md +1 -5
  22. package/commands/pan/knowledge.md +0 -1
  23. package/commands/pan/mcp-bridge.md +1 -3
  24. package/commands/pan/optimize.md +2 -0
  25. package/commands/pan/phase-tests.md +1 -1
  26. package/commands/pan/preview.md +1 -2
  27. package/commands/pan/report.md +1 -5
  28. package/commands/pan/research-phase.md +4 -4
  29. package/commands/pan/review-deep.md +0 -2
  30. package/commands/pan/what-if.md +1 -2
  31. package/hooks/dist/pan-context-monitor.js +12 -3
  32. package/hooks/dist/pan-guard.js +244 -0
  33. package/hooks/dist/pan-statusline.js +5 -2
  34. package/package.json +1 -1
  35. package/pan-wizard-core/bin/lib/commands.cjs +24 -2
  36. package/pan-wizard-core/bin/lib/config.cjs +5 -0
  37. package/pan-wizard-core/bin/lib/findings.cjs +126 -19
  38. package/pan-wizard-core/bin/lib/hygiene.cjs +11 -5
  39. package/pan-wizard-core/bin/lib/init.cjs +6 -0
  40. package/pan-wizard-core/bin/lib/optimize.cjs +13 -1
  41. package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
  42. package/pan-wizard-core/bin/lib/phase.cjs +24 -0
  43. package/pan-wizard-core/bin/lib/preview.cjs +32 -31
  44. package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
  45. package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
  46. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
  47. package/pan-wizard-core/bin/lib/verify-scope.cjs +6 -5
  48. package/pan-wizard-core/bin/lib/whatif.cjs +48 -6
  49. package/pan-wizard-core/bin/pan-tools.cjs +13 -4
  50. package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
  51. package/pan-wizard-core/mcp/native-tools.cjs +3 -3
  52. package/pan-wizard-core/opencode/pan-wizard.js +36 -8
  53. package/pan-wizard-core/references/model-profiles.md +1 -1
  54. package/pan-wizard-core/references/planning-config.md +3 -3
  55. package/pan-wizard-core/references/verdict-contract.md +6 -1
  56. package/pan-wizard-core/templates/config.json +2 -6
  57. package/pan-wizard-core/templates/debug-subagent-prompt.md +2 -1
  58. package/pan-wizard-core/templates/phase-prompt.md +2 -0
  59. package/pan-wizard-core/templates/planner-subagent-prompt.md +2 -0
  60. package/pan-wizard-core/templates/preview-report.md +1 -1
  61. package/pan-wizard-core/templates/state.md +4 -4
  62. package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
  63. package/pan-wizard-core/workflows/discuss-phase.md +2 -1
  64. package/pan-wizard-core/workflows/exec-phase.md +51 -40
  65. package/pan-wizard-core/workflows/milestone-gaps.md +2 -2
  66. package/pan-wizard-core/workflows/new-project.md +8 -16
  67. package/pan-wizard-core/workflows/plan-phase.md +22 -8
  68. package/pan-wizard-core/workflows/progress.md +1 -1
  69. package/pan-wizard-core/workflows/quick.md +2 -2
  70. package/pan-wizard-core/workflows/todo-check.md +3 -2
  71. package/pan-wizard-core/workflows/transition.md +4 -0
  72. package/pan-wizard-core/workflows/verify-phase.md +6 -6
  73. package/scripts/build-hooks.js +1 -0
  74. package/scripts/generate-skills-docs.py +2 -2
  75. package/scripts/prompt-lint.cjs +167 -0
  76. package/scripts/test-surface.cjs +1 -0
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Prompt-contract lint (program L2): the contract a shipped prompt has with the
5
+ * engine and the host, checked in code. Applied to commands/pan, agents and
6
+ * pan-wizard-core/workflows by tests/prompt-lint.test.cjs, with
7
+ * tests/fixtures/prompt-lint-allowlist.json (every entry names its reason, usually
8
+ * the plan item that fixes it).
9
+ *
10
+ * unknown-verb a `pan-tools <verb> [<sub>]` call naming a verb or a subcommand
11
+ * the engine does not have (tests/fixtures/surface.json)
12
+ * tool-not-granted an agent told to use a tool its frontmatter `tools:` does not grant
13
+ * pasted-source an @-include of engine source (`*.cjs` / `*.js`) into a prompt
14
+ * oversized-question an AskUserQuestion block offering more than four options (the
15
+ * host's limit: two to four)
16
+ * untyped-spawn a Task( spawn without `model=`, or a general-purpose spawn told
17
+ * to read a role file instead of naming the agent type
18
+ * dev-skill a shipped prompt sending the user to a dev-only skill of this
19
+ * repository (.claude/commands/*.md), which no install carries
20
+ *
21
+ * node scripts/prompt-lint.cjs # report the current tree, exit 1 on a finding
22
+ */
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ const ROOT = path.join(__dirname, '..');
27
+ const TOOLS = ['Write', 'Edit', 'WebSearch', 'WebFetch', 'Task'];
28
+
29
+ function surface(root = ROOT) {
30
+ const s = JSON.parse(fs.readFileSync(path.join(root, 'tests', 'fixtures', 'surface.json'), 'utf8'));
31
+ const subs = new Map();
32
+ for (const entry of s.subcommands) {
33
+ const [verb, sub] = entry.split(' ');
34
+ if (!subs.has(verb)) subs.set(verb, new Set());
35
+ subs.get(verb).add(sub);
36
+ }
37
+ return { verbs: new Set(s.verbs), subs };
38
+ }
39
+
40
+ function devSkills(root = ROOT) {
41
+ try {
42
+ return fs.readdirSync(path.join(root, '.claude', 'commands')).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
43
+ } catch { return []; }
44
+ }
45
+
46
+ const lineOf = (text, index) => text.slice(0, index).split('\n').length;
47
+
48
+ function frontmatterTools(text) {
49
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
50
+ if (!fm) return null;
51
+ const m = fm[1].match(/^tools:[ \t]*(.+)$/m);
52
+ return m ? new Set(m[1].split(',').map((t) => t.trim().replace(/\*$/, '')).filter(Boolean)) : null;
53
+ }
54
+
55
+ /**
56
+ * Lint one file's text.
57
+ * @param {string} rel - repository-relative path (decides which rules apply)
58
+ * @param {string} raw - file content
59
+ * @param {{verbs: Set, subs: Map, dev: string[]}} ctx
60
+ * @returns {Array<{rule, file, line, text}>}
61
+ */
62
+ function lintText(rel, raw, ctx) {
63
+ const text = raw.replace(/\r\n/g, '\n');
64
+ const out = [];
65
+ const hit = (rule, index, snippet) => out.push({ rule, file: rel, line: lineOf(text, index), text: String(snippet).trim().slice(0, 140) });
66
+
67
+ // unknown-verb
68
+ // An invocation, not prose: the script path (`pan-tools.cjs <verb>`) or a backticked `pan-tools <verb>`.
69
+ const callRe = /(?:pan-tools\.cjs"?|`pan-tools)[ \t]+([a-z][a-z0-9-]*)(?:[ \t]+([a-z][a-z0-9-]*))?/g;
70
+ let m;
71
+ while ((m = callRe.exec(text))) {
72
+ const [, verb, sub] = m;
73
+ if (!ctx.verbs.has(verb)) { hit('unknown-verb', m.index, m[0]); continue; }
74
+ const known = ctx.subs.get(verb);
75
+ if (known && sub && !known.has(sub) && !ctx.verbs.has(sub)) hit('unknown-verb', m.index, m[0]);
76
+ }
77
+
78
+ // pasted-source
79
+ const incRe = /^@\S+\.(?:cjs|mjs|js)\s*$/gm;
80
+ while ((m = incRe.exec(text))) hit('pasted-source', m.index, m[0]);
81
+
82
+ // tool-not-granted (agents only)
83
+ if (rel.startsWith('agents/')) {
84
+ const granted = frontmatterTools(text);
85
+ if (granted) {
86
+ for (const tool of TOOLS) {
87
+ if (granted.has(tool)) continue;
88
+ const re = new RegExp(`\\b(?:use|using|with|via|call|run)\\s+(?:the\\s+)?\`?${tool}\`?(?:\\s+tool)?\\b|\\bthe\\s+\`?${tool}\`?\\s+tool\\b`, 'g');
89
+ let mm;
90
+ while ((mm = re.exec(text))) {
91
+ // "Never use Edit or Write tools" is the rule being kept, not broken.
92
+ const before = text.slice(Math.max(0, mm.index - 24), mm.index).toLowerCase();
93
+ if (/\b(never|not|don't|do not|no)\s*$/.test(before)) continue;
94
+ hit('tool-not-granted', mm.index, `${tool} (granted: ${[...granted].join(', ')}): ${mm[0]}`);
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // oversized-question
102
+ const askRe = /AskUserQuestion/g;
103
+ while ((m = askRe.exec(text))) {
104
+ const window = text.slice(m.index, m.index + 6000);
105
+ const optRe = /options\s*:\s*\[/g;
106
+ let o;
107
+ while ((o = optRe.exec(window))) {
108
+ let depth = 0;
109
+ let end = -1;
110
+ for (let i = o.index + o[0].length - 1; i < window.length; i++) {
111
+ if (window[i] === '[') depth++;
112
+ else if (window[i] === ']') { depth--; if (depth === 0) { end = i; break; } }
113
+ }
114
+ if (end < 0) break;
115
+ const labels = (window.slice(o.index, end).match(/\blabel\s*:/g) || []).length;
116
+ if (labels > 4) hit('oversized-question', m.index + o.index, `${labels} options in one question`);
117
+ optRe.lastIndex = end;
118
+ }
119
+ askRe.lastIndex = m.index + 1;
120
+ }
121
+
122
+ // untyped-spawn (workflows and commands)
123
+ if (!rel.startsWith('agents/')) {
124
+ const lines = text.split('\n');
125
+ lines.forEach((l, i) => {
126
+ if (!/^\s*Task\(/.test(l)) return;
127
+ let j = i;
128
+ while (j < lines.length - 1 && !/^\s*\)\s*$/.test(lines[j]) && !/description="[^"]*"\s*\)\s*$/.test(lines[j])) j++;
129
+ const block = lines.slice(i, j + 1).join('\n');
130
+ const at = lines.slice(0, i).join('\n').length + 1;
131
+ if (!/\bmodel\s*=/.test(block)) hit('untyped-spawn', at, 'a Task( spawn without model=');
132
+ else if (/subagent_type="general-purpose"/.test(block) && /agents\/pan-[a-z-]+\.md for your role/.test(block)) hit('untyped-spawn', at, 'general-purpose spawn told to read a role file');
133
+ });
134
+ }
135
+
136
+ // dev-skill
137
+ if (ctx.dev.length) {
138
+ // Every regex metacharacter escaped, backslash included (inline: CodeQL does not follow a helper).
139
+ const names = ctx.dev.map((d) => d.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&'));
140
+ const devRe = new RegExp(`(^|[\\s\`'"(])/(${names.join('|')})(?=[\\s\`'".,;:)]|$)`, 'gm');
141
+ while ((m = devRe.exec(text))) hit('dev-skill', m.index + m[1].length, m[0]);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /** The shipped prompt files, repository-relative. */
147
+ function shippedPrompts(root = ROOT) {
148
+ const list = [];
149
+ for (const dir of ['commands/pan', 'agents', 'pan-wizard-core/workflows']) {
150
+ for (const f of fs.readdirSync(path.join(root, dir)).filter((x) => x.endsWith('.md')).sort()) list.push(`${dir}/${f}`);
151
+ }
152
+ return list;
153
+ }
154
+
155
+ function lintTree(root = ROOT) {
156
+ const ctx = { ...surface(root), dev: devSkills(root) };
157
+ return shippedPrompts(root).flatMap((rel) => lintText(rel, fs.readFileSync(path.join(root, rel), 'utf8'), ctx));
158
+ }
159
+
160
+ if (require.main === module) {
161
+ const findings = lintTree();
162
+ for (const f of findings) process.stdout.write(`${f.rule} ${f.file}:${f.line} — ${f.text}\n`);
163
+ process.stdout.write(`${findings.length} finding(s)\n`);
164
+ process.exitCode = findings.length ? 1 : 0;
165
+ }
166
+
167
+ module.exports = { lintText, lintTree, shippedPrompts, surface, devSkills, frontmatterTools };
@@ -65,6 +65,7 @@ const EXTRA_HOOK_ROWS = Object.freeze([
65
65
  ]);
66
66
  const EVENT_HOOKS = Object.freeze({
67
67
  sessionStart: ['pan-check-update.js'],
68
+ preToolUse: ['pan-guard.js'],
68
69
  postToolUse: ['pan-context-monitor.js'],
69
70
  subagentStop: ['pan-cost-logger.js', 'pan-trace-logger.js'],
70
71
  stop: ['pan-stop-guard.js'],