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.
- package/README.md +6 -6
- package/agents/pan-conductor.md +2 -2
- package/agents/pan-debugger.md +3 -1
- package/agents/pan-design-checker.md +1 -0
- package/agents/pan-executor.md +18 -1
- package/agents/pan-experiment-runner.md +1 -1
- package/agents/pan-hardener.md +1 -1
- package/agents/pan-meta-reviewer.md +1 -1
- package/agents/pan-phase-researcher.md +2 -2
- package/agents/pan-plan-checker.md +9 -8
- package/agents/pan-planner.md +3 -1
- package/agents/pan-project-researcher.md +1 -1
- package/agents/pan-research-synthesizer.md +1 -1
- package/agents/pan-reviewer.md +3 -2
- package/agents/pan-verifier.md +1 -1
- package/bin/install-lib.cjs +119 -16
- package/bin/install.js +34 -62
- package/commands/pan/cost.md +0 -4
- package/commands/pan/dashboard.md +0 -3
- package/commands/pan/design-phase.md +2 -2
- package/commands/pan/hud.md +1 -5
- package/commands/pan/knowledge.md +0 -1
- package/commands/pan/mcp-bridge.md +1 -3
- package/commands/pan/optimize.md +2 -0
- package/commands/pan/phase-tests.md +1 -1
- package/commands/pan/preview.md +1 -2
- package/commands/pan/report.md +1 -5
- package/commands/pan/research-phase.md +4 -4
- package/commands/pan/review-deep.md +0 -2
- package/commands/pan/what-if.md +1 -2
- package/hooks/dist/pan-context-monitor.js +12 -3
- package/hooks/dist/pan-guard.js +244 -0
- package/hooks/dist/pan-statusline.js +5 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/commands.cjs +24 -2
- package/pan-wizard-core/bin/lib/config.cjs +5 -0
- package/pan-wizard-core/bin/lib/findings.cjs +126 -19
- package/pan-wizard-core/bin/lib/hygiene.cjs +11 -5
- package/pan-wizard-core/bin/lib/init.cjs +6 -0
- package/pan-wizard-core/bin/lib/optimize.cjs +13 -1
- package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
- package/pan-wizard-core/bin/lib/phase.cjs +24 -0
- package/pan-wizard-core/bin/lib/preview.cjs +32 -31
- package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
- package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
- package/pan-wizard-core/bin/lib/verify-scope.cjs +6 -5
- package/pan-wizard-core/bin/lib/whatif.cjs +48 -6
- package/pan-wizard-core/bin/pan-tools.cjs +13 -4
- package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
- package/pan-wizard-core/mcp/native-tools.cjs +3 -3
- package/pan-wizard-core/opencode/pan-wizard.js +36 -8
- package/pan-wizard-core/references/model-profiles.md +1 -1
- package/pan-wizard-core/references/planning-config.md +3 -3
- package/pan-wizard-core/references/verdict-contract.md +6 -1
- package/pan-wizard-core/templates/config.json +2 -6
- package/pan-wizard-core/templates/debug-subagent-prompt.md +2 -1
- package/pan-wizard-core/templates/phase-prompt.md +2 -0
- package/pan-wizard-core/templates/planner-subagent-prompt.md +2 -0
- package/pan-wizard-core/templates/preview-report.md +1 -1
- package/pan-wizard-core/templates/state.md +4 -4
- package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
- package/pan-wizard-core/workflows/discuss-phase.md +2 -1
- package/pan-wizard-core/workflows/exec-phase.md +51 -40
- package/pan-wizard-core/workflows/milestone-gaps.md +2 -2
- package/pan-wizard-core/workflows/new-project.md +8 -16
- package/pan-wizard-core/workflows/plan-phase.md +22 -8
- package/pan-wizard-core/workflows/progress.md +1 -1
- package/pan-wizard-core/workflows/quick.md +2 -2
- package/pan-wizard-core/workflows/todo-check.md +3 -2
- package/pan-wizard-core/workflows/transition.md +4 -0
- package/pan-wizard-core/workflows/verify-phase.md +6 -6
- package/scripts/build-hooks.js +1 -0
- package/scripts/generate-skills-docs.py +2 -2
- package/scripts/prompt-lint.cjs +167 -0
- 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 };
|
package/scripts/test-surface.cjs
CHANGED
|
@@ -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'],
|