pan-wizard 3.26.0 → 3.28.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 +48 -48
- package/agents/pan-previewer.md +1 -1
- package/bin/install-lib.cjs +580 -18
- package/bin/install.js +25 -44
- package/commands/pan/army.md +1 -1
- package/commands/pan/hygiene.md +14 -8
- package/commands/pan/milestone-audit.md +10 -4
- package/commands/pan/preview.md +2 -2
- package/hooks/dist/pan-cost-logger.js +69 -5
- package/hooks/dist/pan-stop-guard.js +32 -1
- package/hooks/dist/pan-trace-logger.js +35 -2
- package/package.json +5 -2
- package/pan-wizard-core/bin/lib/bridge.cjs +0 -1
- package/pan-wizard-core/bin/lib/bus.cjs +0 -1
- package/pan-wizard-core/bin/lib/campaign.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +8 -8
- package/pan-wizard-core/bin/lib/commands.cjs +15 -14
- package/pan-wizard-core/bin/lib/config.cjs +5 -5
- package/pan-wizard-core/bin/lib/constants.cjs +49 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +98 -0
- package/pan-wizard-core/bin/lib/core.cjs +190 -26
- package/pan-wizard-core/bin/lib/cost.cjs +113 -11
- package/pan-wizard-core/bin/lib/distill.cjs +3 -3
- package/pan-wizard-core/bin/lib/focus.cjs +16 -16
- package/pan-wizard-core/bin/lib/foreign-planning.cjs +56 -0
- package/pan-wizard-core/bin/lib/hud.cjs +1 -1
- package/pan-wizard-core/bin/lib/hygiene.cjs +428 -37
- package/pan-wizard-core/bin/lib/init.cjs +98 -13
- package/pan-wizard-core/bin/lib/knowledge.cjs +0 -1
- package/pan-wizard-core/bin/lib/memory.cjs +1 -1
- package/pan-wizard-core/bin/lib/milestone.cjs +3 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +3 -3
- package/pan-wizard-core/bin/lib/phase.cjs +4 -4
- package/pan-wizard-core/bin/lib/planning-root.cjs +327 -0
- package/pan-wizard-core/bin/lib/preview.cjs +0 -1
- package/pan-wizard-core/bin/lib/review-deep.cjs +0 -1
- package/pan-wizard-core/bin/lib/roadmap.cjs +1 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +339 -0
- package/pan-wizard-core/bin/lib/state.cjs +0 -1
- package/pan-wizard-core/bin/lib/template.cjs +1 -1
- package/pan-wizard-core/bin/lib/utils.cjs +39 -11
- package/pan-wizard-core/bin/lib/verify.cjs +26 -5
- package/pan-wizard-core/bin/lib/whatif.cjs +0 -1
- package/pan-wizard-core/bin/pan-tools.cjs +58 -4
- package/pan-wizard-core/mcp/server.cjs +92 -8
- package/pan-wizard-core/mcp/tool-registry.cjs +50 -3
- package/pan-wizard-core/references/model-profiles.md +2 -2
- package/pan-wizard-core/workflows/health.md +1 -0
- package/pan-wizard-core/workflows/milestone-audit.md +35 -6
- package/pan-zcode/README.md +1 -1
- package/scripts/build-agent-plugin.js +220 -0
- package/scripts/build-plugin.js +48 -3
- package/scripts/generate-skills-docs.py +1 -1
- package/scripts/release-check.js +58 -12
|
@@ -50,7 +50,36 @@ const META_SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
|
50
50
|
// claim to speak a version we don't. Newest first (the `server/discover` order).
|
|
51
51
|
const SUPPORTED_VERSIONS_LIST = [MODERN_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
52
52
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(SUPPORTED_VERSIONS_LIST);
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The version the server reports in `initialize` / `server/discover`. Read from the
|
|
55
|
+
* package.json two levels up: the repository root in the source tree, the runtime
|
|
56
|
+
* directory in an install (the installer writes package.json beside pan-wizard-core/).
|
|
57
|
+
* The plugin bundles carry no package.json there, so fall back to the plugin manifest
|
|
58
|
+
* and finally to a marker that is visibly not a release. Never throws: a missing
|
|
59
|
+
* file must not stop the server from answering. Reality check R9: this was a literal
|
|
60
|
+
* '0.1.0' while the package shipped 3.x.
|
|
61
|
+
*/
|
|
62
|
+
function readPackageVersion(baseDir = path.join(__dirname, '..', '..')) {
|
|
63
|
+
// Order: the repo/runtime package.json when it carries a version; the install
|
|
64
|
+
// manifest every runtime writes (the runtime directory's package.json is a bare
|
|
65
|
+
// `{"type":"commonjs"}` marker with no version — measured on a fresh install,
|
|
66
|
+
// 2026-09-10, where the first version of this reader answered 0.0.0-unknown); the
|
|
67
|
+
// Claude plugin manifest; an Agent Plugins manifest.
|
|
68
|
+
const candidates = [
|
|
69
|
+
path.join(baseDir, 'package.json'),
|
|
70
|
+
path.join(baseDir, 'pan-file-manifest.json'),
|
|
71
|
+
path.join(baseDir, '.claude-plugin', 'plugin.json'),
|
|
72
|
+
path.join(baseDir, 'plugin.json'),
|
|
73
|
+
];
|
|
74
|
+
for (const file of candidates) {
|
|
75
|
+
try {
|
|
76
|
+
const v = JSON.parse(fs.readFileSync(file, 'utf8')).version;
|
|
77
|
+
if (typeof v === 'string' && v.trim()) return v.trim();
|
|
78
|
+
} catch { /* try the next candidate */ }
|
|
79
|
+
}
|
|
80
|
+
return '0.0.0-unknown';
|
|
81
|
+
}
|
|
82
|
+
const SERVER_INFO = { name: 'pan-mcp', version: readPackageVersion() };
|
|
54
83
|
|
|
55
84
|
/**
|
|
56
85
|
* Default engine location: `bin/` is a sibling of this `mcp/` directory inside
|
|
@@ -75,6 +104,25 @@ function defaultPanToolsPath() {
|
|
|
75
104
|
return path.join(__dirname, '..', 'bin', 'pan-tools.cjs');
|
|
76
105
|
}
|
|
77
106
|
|
|
107
|
+
/**
|
|
108
|
+
* A verdict payload: a JSON object with no error-family key. The family is `error`
|
|
109
|
+
* and any key ending in `_error` — the same definition core.cjs's reportsFailure()
|
|
110
|
+
* uses for the CLI exit code, mirrored here because the server stays engine-agnostic
|
|
111
|
+
* (it never requires the engine's modules; it spawns them). Plural collections such
|
|
112
|
+
* as `errors[]` are verdict DETAIL, not a failure signal. Returns the parsed object,
|
|
113
|
+
* or null when the text is not such a payload.
|
|
114
|
+
*/
|
|
115
|
+
function parseVerdict(text) {
|
|
116
|
+
if (typeof text !== 'string' || !text.trim()) return null;
|
|
117
|
+
let parsed;
|
|
118
|
+
try { parsed = JSON.parse(text); } catch { return null; }
|
|
119
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
120
|
+
for (const k of Object.keys(parsed)) {
|
|
121
|
+
if (k === 'error' || k.endsWith('_error')) { if (parsed[k]) return null; }
|
|
122
|
+
}
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
|
|
78
126
|
/** Real spawn: shell-less execFile of `node <argv...>`. */
|
|
79
127
|
function defaultSpawn(nodeArgs) {
|
|
80
128
|
try {
|
|
@@ -154,7 +202,7 @@ function createServer(opts = {}) {
|
|
|
154
202
|
const gitImpl = opts.gitImpl || makeDefaultGit(cwd);
|
|
155
203
|
const env = opts.env || process.env;
|
|
156
204
|
|
|
157
|
-
function runVerb(verb, extraArgs) {
|
|
205
|
+
function runVerb(verb, extraArgs, verbCwd = cwd) {
|
|
158
206
|
// Defense in depth: the verb always comes from the registry, but re-check the
|
|
159
207
|
// forbidden pattern here so no future caller can smuggle a force/reset op past it.
|
|
160
208
|
if (reg.FORBIDDEN_VERB.test(verb)) {
|
|
@@ -163,22 +211,46 @@ function createServer(opts = {}) {
|
|
|
163
211
|
// No --raw: pan-tools' default output is structured JSON (which is what the MCP
|
|
164
212
|
// client wants); --raw would instead emit a bare human scalar. Large results
|
|
165
213
|
// arrive via the @file: overflow protocol, resolved here.
|
|
166
|
-
const r = spawn([panToolsPath, verb, ...extraArgs, '--cwd',
|
|
214
|
+
const r = spawn([panToolsPath, verb, ...extraArgs, '--cwd', verbCwd]);
|
|
167
215
|
if (r && r.ok) r.stdout = resolveOverflow(r.stdout);
|
|
168
216
|
return r;
|
|
169
217
|
}
|
|
170
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Per-call project root (ADR-0045 D6). Every TOOL accepts an optional `cwd`;
|
|
221
|
+
* it must be an absolute path to an existing directory. Returns
|
|
222
|
+
* { cwd, input } with the field removed from the input handed to the tool, or
|
|
223
|
+
* { error } shaped for a -32602 — a bad root is a bad REQUEST, and nothing is
|
|
224
|
+
* spawned. Resources never come through here: their argv is static.
|
|
225
|
+
*/
|
|
226
|
+
function resolveCallCwd(input) {
|
|
227
|
+
const src = input || {};
|
|
228
|
+
if (src.cwd === undefined) return { cwd, input: src };
|
|
229
|
+
let candidate;
|
|
230
|
+
try { candidate = reg.validateProjectCwd(src.cwd); }
|
|
231
|
+
catch (e) { return { error: { code: -32602, message: String((e && e.message) || e) } }; }
|
|
232
|
+
let isDir = false;
|
|
233
|
+
try { isDir = fs.statSync(candidate).isDirectory(); } catch { /* absent → not a directory */ }
|
|
234
|
+
if (!isDir) return { error: { code: -32602, message: `Invalid "cwd": not an existing directory: ${candidate}` } };
|
|
235
|
+
const { cwd: _omit, ...rest } = src;
|
|
236
|
+
return { cwd: path.resolve(candidate), input: rest };
|
|
237
|
+
}
|
|
238
|
+
|
|
171
239
|
// Returns { error:{code,message} } for JSON-RPC protocol errors (unknown tool /
|
|
172
240
|
// invalid arguments — a bad *request*), or { result:{content,isError} } where
|
|
173
241
|
// isError:true signals a genuine tool *execution* failure (the verb ran and failed).
|
|
174
242
|
function callTool(name, input) {
|
|
175
243
|
const tool = reg.byToolName[name];
|
|
176
244
|
if (!tool) return { error: { code: -32602, message: `Unknown tool: ${name}` } };
|
|
245
|
+
const call = resolveCallCwd(input);
|
|
246
|
+
if (call.error) return { error: call.error };
|
|
177
247
|
// Native, in-process tools (orchestrator / merge gate) run a handler; a thrown
|
|
178
248
|
// Error means bad params (-32602), matching the spawn-tool validation path.
|
|
249
|
+
// The git executor follows the per-call root unless a test injected one.
|
|
179
250
|
if (typeof tool.handler === 'function') {
|
|
180
251
|
try {
|
|
181
|
-
const
|
|
252
|
+
const gitForCall = opts.gitImpl ? gitImpl : (call.cwd === cwd ? gitImpl : makeDefaultGit(call.cwd));
|
|
253
|
+
const out = tool.handler({ cwd: call.cwd, input: call.input, env, gitImpl: gitForCall });
|
|
182
254
|
const text = (out && out.text != null) ? out.text : JSON.stringify(out && out.json);
|
|
183
255
|
return { result: { content: [{ type: 'text', text }], isError: !!(out && out.isError) } };
|
|
184
256
|
} catch (e) {
|
|
@@ -186,9 +258,9 @@ function createServer(opts = {}) {
|
|
|
186
258
|
}
|
|
187
259
|
}
|
|
188
260
|
let extra;
|
|
189
|
-
try { extra = tool.args ? tool.args(input
|
|
261
|
+
try { extra = tool.args ? tool.args(call.input) : []; }
|
|
190
262
|
catch (e) { return { error: { code: -32602, message: String((e && e.message) || e) } }; }
|
|
191
|
-
const r = runVerb(tool.verb, extra);
|
|
263
|
+
const r = runVerb(tool.verb, extra, call.cwd);
|
|
192
264
|
return { result: { content: [{ type: 'text', text: r.ok ? r.stdout : (r.stderr || 'error') }], isError: !r.ok } };
|
|
193
265
|
}
|
|
194
266
|
|
|
@@ -206,7 +278,19 @@ function createServer(opts = {}) {
|
|
|
206
278
|
// non-array into the spawn.
|
|
207
279
|
const tail = Array.isArray(res.args) ? res.args : [];
|
|
208
280
|
const r = runVerb(res.verb, tail);
|
|
209
|
-
if (!r.ok)
|
|
281
|
+
if (!r.ok) {
|
|
282
|
+
// A VERDICT is data, not a failed read. `validate health` (pan://health) exits
|
|
283
|
+
// non-zero when its verdict is `broken` — CLI-REFERENCE: verdict commands set
|
|
284
|
+
// their exit code explicitly, for shell gating — while still printing the full
|
|
285
|
+
// JSON report. Over MCP the report IS the resource, so accept stdout when it is
|
|
286
|
+
// a JSON object carrying no error-family key. Anything else (no JSON, or an
|
|
287
|
+
// `error`/`*_error` key) is a genuine read failure → JSON-RPC error.
|
|
288
|
+
const text = resolveOverflow(r.stdout);
|
|
289
|
+
if (parseVerdict(text) !== null) {
|
|
290
|
+
return { result: { contents: [{ uri, mimeType: 'application/json', text }] } };
|
|
291
|
+
}
|
|
292
|
+
return { error: { code: -32603, message: r.stderr || 'resource read failed' } };
|
|
293
|
+
}
|
|
210
294
|
return { result: { contents: [{ uri, mimeType: 'application/json', text: r.stdout }] } };
|
|
211
295
|
}
|
|
212
296
|
|
|
@@ -315,6 +399,6 @@ function main() {
|
|
|
315
399
|
if (require.main === module) main();
|
|
316
400
|
|
|
317
401
|
module.exports = {
|
|
318
|
-
createServer, defaultPanToolsPath, defaultSpawn, SERVER_INFO, toMcpTool, toMcpResource,
|
|
402
|
+
createServer, defaultPanToolsPath, defaultSpawn, parseVerdict, readPackageVersion, SERVER_INFO, toMcpTool, toMcpResource,
|
|
319
403
|
PROTOCOL_VERSION, MODERN_PROTOCOL_VERSION, SUPPORTED_VERSIONS_LIST, META_PROTOCOL_VERSION_KEY,
|
|
320
404
|
};
|
|
@@ -39,7 +39,9 @@ const QUERY_RE = /^[\w .,:/&()-]{1,120}$/; // find-phase query fragment
|
|
|
39
39
|
*
|
|
40
40
|
* THE RULE FOR ADDING ONE — a resource must be readable on ANY project, including
|
|
41
41
|
* a bare directory with no `.planning/`. If "no data yet" is reported as an error
|
|
42
|
-
* (
|
|
42
|
+
* (an error-family key in the JSON it prints, or no JSON at all — a non-zero exit BY
|
|
43
|
+
* ITSELF is a verdict signal for shell gating, and the reader accepts the JSON as data;
|
|
44
|
+
* see server.cjs readResource), it is a TOOL, not a resource: a client
|
|
43
45
|
* that lists resources and reads them should not collect failures for a young
|
|
44
46
|
* project. `preview` is the worked example — `preview phases` exits non-zero
|
|
45
47
|
* without a roadmap, so it is exposed as a tool below rather than as a resource.
|
|
@@ -62,7 +64,7 @@ const RESOURCES = [
|
|
|
62
64
|
description: 'Phase inventory: the phase directories present, with a count.' },
|
|
63
65
|
{ uri: 'pan://progress', name: 'Progress', verb: 'progress', description: 'Requirement and plan completion progress.' },
|
|
64
66
|
{ uri: 'pan://health', name: 'Project health', verb: 'validate', args: ['health'],
|
|
65
|
-
description: 'Health check over .planning/: issue codes with severities. Reports an unhealthy project as DATA
|
|
67
|
+
description: 'Health check over .planning/: issue codes with severities. Reports an unhealthy project as DATA: the JSON verdict is the resource even when the CLI exits non-zero for shell gating, so it is readable on a broken or empty project.' },
|
|
66
68
|
{ uri: 'pan://links', name: 'Doc-code links', verb: 'links', args: ['validate'],
|
|
67
69
|
description: 'Doc↔code link graph verdict: forward links, backlink contracts, and anchor targets, with finding codes.' },
|
|
68
70
|
{ uri: 'pan://cost', name: 'Token cost', verb: 'cost', args: ['report'],
|
|
@@ -138,7 +140,51 @@ const FORBIDDEN_VERB = /(^|-)(push|reset|rebase|force)($|-)/;
|
|
|
138
140
|
// tools into one advertised list. Required after SPAWN_TOOLS/FORBIDDEN_VERB so the
|
|
139
141
|
// native module (which imports nothing back from here) composes cleanly — no cycle.
|
|
140
142
|
const { NATIVE_TOOLS } = require('./native-tools.cjs');
|
|
141
|
-
|
|
143
|
+
|
|
144
|
+
// ─── Per-call project root (ADR-0045 D6, 2026-09) ───────────────────────────
|
|
145
|
+
//
|
|
146
|
+
// The server resolves its project as `opts.cwd || PAN_PROJECT_ROOT || process.cwd()`.
|
|
147
|
+
// Under Claude Code the process cwd IS the project. Under an Agent Plugins client
|
|
148
|
+
// the spec makes the PLUGIN ROOT the default working directory of a stdio server,
|
|
149
|
+
// so every verb would read `.planning/` from inside the plugin cache and report an
|
|
150
|
+
// empty project — cleanly, which is the worst kind of failure. So every TOOL takes
|
|
151
|
+
// an optional `cwd`: the absolute path of the project to operate on, honoured for
|
|
152
|
+
// that call only. Applied here, centrally, so a tool added later cannot miss it.
|
|
153
|
+
//
|
|
154
|
+
// Resources deliberately do NOT get it: their argv is a static array and the
|
|
155
|
+
// safety argument of ADR-0041 is that no client input reaches it.
|
|
156
|
+
const PROJECT_CWD_PROPERTY = Object.freeze({
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Absolute path of the PAN project to operate on. Optional: defaults to the directory the server was started in. Pass it when the server was launched from a plugin directory (Agent Plugins clients do this by default), or to address another project.',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const PROJECT_CWD_MAX = 1024;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Shape-validate a per-call project root: a non-empty absolute path with no NUL,
|
|
165
|
+
* within a sane length. Existence is the SERVER's check (it has fs); this module
|
|
166
|
+
* stays pure. Throws a message fit for a -32602 on failure.
|
|
167
|
+
*/
|
|
168
|
+
function validateProjectCwd(value) {
|
|
169
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > PROJECT_CWD_MAX) {
|
|
170
|
+
throw new Error(`Invalid "cwd": must be a non-empty string of at most ${PROJECT_CWD_MAX} chars`);
|
|
171
|
+
}
|
|
172
|
+
if (value.includes('\0')) throw new Error('Invalid "cwd": contains a NUL byte');
|
|
173
|
+
// Absolute on either platform family: `/…`, `C:\…`, `C:/…`, or a UNC `\\host\share`.
|
|
174
|
+
if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value)) throw new Error('Invalid "cwd": must be an absolute path');
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Return a copy of a tool descriptor whose inputSchema also accepts `cwd`. Never mutates the source. */
|
|
179
|
+
function withProjectCwd(tool) {
|
|
180
|
+
const schema = tool.inputSchema || { type: 'object', additionalProperties: false, properties: {} };
|
|
181
|
+
return {
|
|
182
|
+
...tool,
|
|
183
|
+
inputSchema: { ...schema, properties: { ...(schema.properties || {}), cwd: PROJECT_CWD_PROPERTY } },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const TOOLS = [...SPAWN_TOOLS, ...NATIVE_TOOLS].map(withProjectCwd);
|
|
142
188
|
|
|
143
189
|
const byToolName = Object.create(null);
|
|
144
190
|
for (const t of TOOLS) byToolName[t.name] = t;
|
|
@@ -148,4 +194,5 @@ for (const r of RESOURCES) byResourceUri[r.uri] = r;
|
|
|
148
194
|
module.exports = {
|
|
149
195
|
TOOLS, SPAWN_TOOLS, NATIVE_TOOLS, RESOURCES, byToolName, byResourceUri, FORBIDDEN_VERB,
|
|
150
196
|
AGENT_RE, PHASE_RE, QUERY_RE, str,
|
|
197
|
+
PROJECT_CWD_PROPERTY, validateProjectCwd, withProjectCwd,
|
|
151
198
|
};
|
|
@@ -11,8 +11,8 @@ PAN uses three abstract tiers instead of hardcoded model names:
|
|
|
11
11
|
| Tier | Purpose | Anthropic | OpenAI | Google |
|
|
12
12
|
|------|---------|-----------|--------|--------|
|
|
13
13
|
| `reasoning` | Architecture, planning, complex decisions | inherit (your session's top-tier model) | inherit | inherit |
|
|
14
|
-
| `mid` | Execution, research, verification | Sonnet | mid |
|
|
15
|
-
| `fast` | Read-only extraction, budget tasks | Haiku | fast |
|
|
14
|
+
| `mid` | Execution, research, verification | Sonnet | mid | gemini-2.5-flash |
|
|
15
|
+
| `fast` | Read-only extraction, budget tasks | Haiku | fast | gemini-2.5-flash-lite |
|
|
16
16
|
|
|
17
17
|
**Why `inherit` for reasoning?** Host runtimes map "opus" to a specific model version. PAN returns `inherit` for reasoning-tier agents, so they use whatever top-tier model the user has configured. This avoids version conflicts and silent fallbacks.
|
|
18
18
|
|
|
@@ -152,6 +152,7 @@ Report final status.
|
|
|
152
152
|
| E003 | error | roadmap.md not found | No |
|
|
153
153
|
| E004 | error | state.md not found | Yes |
|
|
154
154
|
| E005 | error | config.json parse error | Yes |
|
|
155
|
+
| E006 | error | `.planning/` belongs to another tool (gsd-core markers found); PAN stops before E002-E005 and `--repair` writes nothing | No |
|
|
155
156
|
| W001 | warning | project.md missing required section | No |
|
|
156
157
|
| W002 | warning | state.md references invalid phase | Yes |
|
|
157
158
|
| W003 | warning | config.json not found | Yes |
|
|
@@ -16,6 +16,35 @@ INIT=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs init milestone-op)
|
|
|
16
16
|
|
|
17
17
|
Extract from init JSON: `milestone_version`, `milestone_name`, `phase_count`, `completed_phases`, `commit_docs`.
|
|
18
18
|
|
|
19
|
+
**Also extract `planning_root` and `track`, and use `$PLANNING_ROOT` for every planning path in this workflow** — the project may hold several planning trees, and auditing the wrong one produces a confident, plausible, wrong report:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
PLANNING_ROOT=$(printf '%s' "$INIT" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).planning_root))")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 0a. Which tree am I auditing?
|
|
26
|
+
|
|
27
|
+
**Always state the resolved `planning_root` at the top of the audit report.** If `planning_root_exists` is `false`, STOP — that is a mistyped `--track`, not an empty milestone.
|
|
28
|
+
|
|
29
|
+
To audit a specific tree, pass `--track <name>`. To see every tree's milestone state before choosing:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs init milestone-op --all-tracks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That returns `{track_count, ambiguous_tracks, tracks[]}`, one entry per planning tree, each with its own `planning_root`, `milestone_version`, `milestone_name`, and `milestone_basis`.
|
|
36
|
+
|
|
37
|
+
### 0b. Refuse to audit an unresolvable milestone
|
|
38
|
+
|
|
39
|
+
The init payload carries how the milestone was decided:
|
|
40
|
+
|
|
41
|
+
- `milestone_basis` — `marked-current` (a `(current)` / 🚧 marker), `first-unshipped`, `last-shipped`, or `default`
|
|
42
|
+
- `milestone_ambiguous` — **`true` means the roadmap marks more than one milestone current**
|
|
43
|
+
|
|
44
|
+
**If `milestone_ambiguous` is `true`, STOP and report the planning-state error.** Do not audit. Two milestones marked current is a roadmap defect the owner must resolve; picking one silently is how an audit ends up describing a milestone that does not exist.
|
|
45
|
+
|
|
46
|
+
If `milestone_basis` is `default`, there is no milestone heading in the roadmap at all — say so rather than auditing `v1.0 milestone`.
|
|
47
|
+
|
|
19
48
|
Resolve integration checker model:
|
|
20
49
|
```bash
|
|
21
50
|
CHECKER_MODEL=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs resolve-model pan-integration-checker --raw)
|
|
@@ -103,7 +132,7 @@ For each phase's verification.md, extract the expanded requirements table:
|
|
|
103
132
|
|
|
104
133
|
For each phase's summary.md, extract `requirements-completed` from YAML frontmatter:
|
|
105
134
|
```bash
|
|
106
|
-
for summary in
|
|
135
|
+
for summary in "$PLANNING_ROOT"/phases/*-*/*-summary.md; do
|
|
107
136
|
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs summary-extract "$summary" --fields requirements_completed | jq -r '.requirements_completed'
|
|
108
137
|
done
|
|
109
138
|
```
|
|
@@ -129,7 +158,7 @@ For each REQ-ID, determine status using all three sources:
|
|
|
129
158
|
|
|
130
159
|
## 6. Aggregate into v{version}-milestone-audit.md
|
|
131
160
|
|
|
132
|
-
Create
|
|
161
|
+
Create `{planning_root}/v{version}-milestone-audit.md` with:
|
|
133
162
|
|
|
134
163
|
```yaml
|
|
135
164
|
---
|
|
@@ -186,7 +215,7 @@ Output this markdown directly (not as a code block). Route based on status:
|
|
|
186
215
|
## ✓ Milestone {version} — Audit Passed
|
|
187
216
|
|
|
188
217
|
**Score:** {N}/{M} requirements satisfied
|
|
189
|
-
**Report:**
|
|
218
|
+
**Report:** {planning_root}/v{version}-milestone-audit.md
|
|
190
219
|
|
|
191
220
|
All requirements covered. Cross-phase integration verified. E2E flows complete.
|
|
192
221
|
|
|
@@ -209,7 +238,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete.
|
|
|
209
238
|
## ⚠ Milestone {version} — Gaps Found
|
|
210
239
|
|
|
211
240
|
**Score:** {N}/{M} requirements satisfied
|
|
212
|
-
**Report:**
|
|
241
|
+
**Report:** {planning_root}/v{version}-milestone-audit.md
|
|
213
242
|
|
|
214
243
|
### Unsatisfied Requirements
|
|
215
244
|
|
|
@@ -240,7 +269,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete.
|
|
|
240
269
|
───────────────────────────────────────────────────────────────
|
|
241
270
|
|
|
242
271
|
**Also available:**
|
|
243
|
-
- cat
|
|
272
|
+
- cat {planning_root}/v{version}-milestone-audit.md — see full report
|
|
244
273
|
- /pan:milestone-done {version} — proceed anyway (accept tech debt)
|
|
245
274
|
|
|
246
275
|
───────────────────────────────────────────────────────────────
|
|
@@ -252,7 +281,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete.
|
|
|
252
281
|
## ⚡ Milestone {version} — Tech Debt Review
|
|
253
282
|
|
|
254
283
|
**Score:** {N}/{M} requirements satisfied
|
|
255
|
-
**Report:**
|
|
284
|
+
**Report:** {planning_root}/v{version}-milestone-audit.md
|
|
256
285
|
|
|
257
286
|
All requirements met. No critical blockers. Accumulated tech debt needs review.
|
|
258
287
|
|
package/pan-zcode/README.md
CHANGED
|
@@ -13,7 +13,7 @@ ZCode through the one interface it speaks: **MCP**.
|
|
|
13
13
|
|
|
14
14
|
## How it fits
|
|
15
15
|
|
|
16
|
-
```
|
|
16
|
+
```text
|
|
17
17
|
ZCode harness (GLM-5.2) primary Agent drives everything; ported subagents fan out
|
|
18
18
|
│ MCP · local stdio
|
|
19
19
|
pan-wizard-core/mcp (SHARED) a thin, zero-dep bridge — verbs → MCP tools/resources
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the PAN Wizard **Agent Plugins 1.0** bundle (ADR-0045) — the vendor-
|
|
3
|
+
* neutral package that Copilot CLI / VS Code, Codex, Cursor and Kiro load
|
|
4
|
+
* natively. Emits a self-contained directory at dist/pan-agent-plugin/:
|
|
5
|
+
*
|
|
6
|
+
* plugin.json closed-schema manifest ($schema + name + metadata)
|
|
7
|
+
* skills/pan-<name>/SKILL.md every PAN command as an Agent Skill, from the
|
|
8
|
+
* ONE unified-skills compiler (ADR-0028)
|
|
9
|
+
* mcp.json the bundled bridge, launched as
|
|
10
|
+
* `node ${PLUGIN_ROOT}/pan-wizard-core/mcp/server.cjs`
|
|
11
|
+
* pan-wizard-core/ dispatcher + modules + workflows + templates +
|
|
12
|
+
* references + learnings (internal stripped) +
|
|
13
|
+
* canonical agent reference copies under agents/
|
|
14
|
+
*
|
|
15
|
+
* hooks/pan-*.js PAN's hook scripts (pure Node), shared by every vendor
|
|
16
|
+
* hooks/hooks.json Codex: default plugin hooks location; matcher-group
|
|
17
|
+
* shape with `${PLUGIN_ROOT}` paths and `async` observers
|
|
18
|
+
* (developers.openai.com/plugins/build/plugins, 2026-09-10)
|
|
19
|
+
* com.github.copilot/ Copilot's reverse-domain namespace (ADR-0045 D5):
|
|
20
|
+
* agents/pan-*.agent.md agents in Copilot's format
|
|
21
|
+
* hooks/hooks.json flat PascalCase format, `${CLAUDE_PLUGIN_ROOT}` paths
|
|
22
|
+
* (VS-Code-verified; Copilot CLI live install is the gate)
|
|
23
|
+
*
|
|
24
|
+
* NOT emitted: Codex agents (no plugin agent component is documented) and any
|
|
25
|
+
* Antigravity variant (its manifest schema is closed and different — a separate
|
|
26
|
+
* layout, deferred until its file shapes are read from a primary source).
|
|
27
|
+
*
|
|
28
|
+
* Paths inside skill and core markdown use PAN's `{{PAN_PLUGIN_ROOT}}` token,
|
|
29
|
+
* defined for the model by the adapter note in every skill; `${PLUGIN_ROOT}`
|
|
30
|
+
* (the client-expanded variable) appears only in mcp.json, the one place the
|
|
31
|
+
* spec expands it.
|
|
32
|
+
*
|
|
33
|
+
* Usage: node scripts/build-agent-plugin.js (or npm run build:agent-plugin)
|
|
34
|
+
* PAN_AGENT_PLUGIN_OUT=<dir> overrides the output directory (tests build into
|
|
35
|
+
* private temp dirs so parallel test files never race on dist/).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
|
|
43
|
+
const ROOT = path.join(__dirname, '..');
|
|
44
|
+
const OUT = process.env.PAN_AGENT_PLUGIN_OUT
|
|
45
|
+
? path.resolve(process.env.PAN_AGENT_PLUGIN_OUT)
|
|
46
|
+
: path.join(ROOT, 'dist', 'pan-agent-plugin');
|
|
47
|
+
const pkg = require(path.join(ROOT, 'package.json'));
|
|
48
|
+
const lib = require(path.join(ROOT, 'bin', 'install-lib.cjs'));
|
|
49
|
+
|
|
50
|
+
const TOKEN_PREFIX = `${lib.AGENT_PLUGIN_ROOT_TOKEN}/`;
|
|
51
|
+
const REWRITE = {
|
|
52
|
+
// Core and agent references → inside the bundle.
|
|
53
|
+
corePrefix: TOKEN_PREFIX,
|
|
54
|
+
// Residual `~/.claude/…` → the consuming runtime's USER config dir; residual
|
|
55
|
+
// `./.claude/…` → its PROJECT dir. Neither is known at build time, so both are
|
|
56
|
+
// tokens the adapter note defines (install-lib AGENT_PLUGIN_RUNTIME_*_TOKEN).
|
|
57
|
+
pathPrefix: `${lib.AGENT_PLUGIN_RUNTIME_HOME_TOKEN}/`,
|
|
58
|
+
projectDirPrefix: `${lib.AGENT_PLUGIN_RUNTIME_DIR_TOKEN}/`,
|
|
59
|
+
attribution: undefined, // keep the documents' default attribution — no runtime to consult
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Refuse to wipe a directory that is not a previous bundle build (same rule as
|
|
64
|
+
* build-plugin.js). Empty or absent directories, and our own previous output —
|
|
65
|
+
* recognised by a manifest carrying the Agent Plugins schema — are fair game.
|
|
66
|
+
*/
|
|
67
|
+
function assertSafeToReplace(dir) {
|
|
68
|
+
if (!fs.existsSync(dir)) return;
|
|
69
|
+
if (fs.readdirSync(dir).length === 0) return;
|
|
70
|
+
try {
|
|
71
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf8'));
|
|
72
|
+
if (manifest && manifest.$schema === lib.AGENT_PLUGIN_MANIFEST_SCHEMA) return;
|
|
73
|
+
} catch { /* fall through to refusal */ }
|
|
74
|
+
throw new Error(`build-agent-plugin: refusing to replace ${dir} — it is non-empty and does not look like a previous bundle build (no Agent Plugins plugin.json)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** commands/pan/**.md → skills/pan-<name>/SKILL.md, mirroring the installer's recursion. */
|
|
78
|
+
function emitSkills(srcDir, skillsDir, prefix) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
(function recurse(currentSrc, currentPrefix) {
|
|
81
|
+
for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
|
|
82
|
+
const srcPath = path.join(currentSrc, entry.name);
|
|
83
|
+
if (entry.isDirectory()) { recurse(srcPath, `${currentPrefix}-${entry.name}`); continue; }
|
|
84
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
85
|
+
const skillName = `${currentPrefix}-${entry.name.replace(/\.md$/, '')}`;
|
|
86
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
87
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
88
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
89
|
+
content = lib.rewriteUnifiedSkillCommandContent(content, REWRITE);
|
|
90
|
+
content = lib.convertClaudeCommandToUnifiedSkill(content, skillName, { adapterNote: lib.agentPluginSkillAdapterNote() });
|
|
91
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
})(srcDir, prefix);
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** pan-wizard-core → bundle, markdown rewritten, everything else verbatim. */
|
|
99
|
+
function emitCore(srcDir, destDir) {
|
|
100
|
+
(function recurse(currentSrc, currentDest) {
|
|
101
|
+
fs.mkdirSync(currentDest, { recursive: true });
|
|
102
|
+
for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
|
|
103
|
+
const srcPath = path.join(currentSrc, entry.name);
|
|
104
|
+
const destPath = path.join(currentDest, entry.name);
|
|
105
|
+
if (entry.isDirectory()) recurse(srcPath, destPath);
|
|
106
|
+
else if (entry.name.endsWith('.md')) fs.writeFileSync(destPath, lib.rewriteSharedCoreMarkdown(fs.readFileSync(srcPath, 'utf8'), REWRITE));
|
|
107
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
108
|
+
}
|
|
109
|
+
})(srcDir, destDir);
|
|
110
|
+
|
|
111
|
+
// learnings/internal is source-only — strip the files AND the index entries,
|
|
112
|
+
// exactly as the installer and the Claude plugin builder do.
|
|
113
|
+
fs.rmSync(path.join(destDir, 'learnings', 'internal'), { recursive: true, force: true });
|
|
114
|
+
const indexPath = path.join(destDir, 'learnings', 'index.json');
|
|
115
|
+
try {
|
|
116
|
+
const stripped = lib.stripInternalLearningsTopics(JSON.parse(fs.readFileSync(indexPath, 'utf8')));
|
|
117
|
+
if (stripped) fs.writeFileSync(indexPath, JSON.stringify(stripped, null, 2) + '\n');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err.code !== 'ENOENT') throw err;
|
|
120
|
+
}
|
|
121
|
+
fs.writeFileSync(path.join(destDir, 'VERSION'), pkg.version);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Canonical agent reference copies under <core>/agents/ (ADR-0028). */
|
|
125
|
+
function emitAgentReferenceCopies(agentsSrc, agentsRefDir) {
|
|
126
|
+
fs.mkdirSync(agentsRefDir, { recursive: true });
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (const f of fs.readdirSync(agentsSrc).filter(n => n.endsWith('.md'))) {
|
|
129
|
+
fs.writeFileSync(path.join(agentsRefDir, f), lib.rewriteAgentReferenceCopy(fs.readFileSync(path.join(agentsSrc, f), 'utf8'), TOKEN_PREFIX));
|
|
130
|
+
count++;
|
|
131
|
+
}
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Hook scripts: the built copies from hooks/dist when present, else the pure-Node sources. */
|
|
136
|
+
function emitHookScripts(destDir) {
|
|
137
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
138
|
+
const dist = path.join(ROOT, 'hooks', 'dist');
|
|
139
|
+
const src = fs.existsSync(dist) ? dist : path.join(ROOT, 'hooks');
|
|
140
|
+
const names = fs.readdirSync(src).filter(n => /^pan-[a-z-]+\.js$/.test(n)).sort();
|
|
141
|
+
for (const n of names) fs.copyFileSync(path.join(src, n), path.join(destDir, n));
|
|
142
|
+
return names;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The four hook commands, anchored at the plugin root through whichever variable the consumer expands. */
|
|
146
|
+
function hookCommands(rootVar) {
|
|
147
|
+
const cmd = (script) => `node ${rootVar}/hooks/${script}`;
|
|
148
|
+
return {
|
|
149
|
+
updateCheckCommand: cmd('pan-check-update.js'),
|
|
150
|
+
contextMonitorCommand: cmd('pan-context-monitor.js'),
|
|
151
|
+
costLoggerCommand: cmd('pan-cost-logger.js'),
|
|
152
|
+
traceLoggerCommand: cmd('pan-trace-logger.js'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Copilot vendor directory: agents in Copilot's `.agent.md` format + plugin hooks. */
|
|
157
|
+
function emitCopilotNamespace(agentsSrc, nsDir) {
|
|
158
|
+
const agentsDest = path.join(nsDir, 'agents');
|
|
159
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
160
|
+
let agents = 0;
|
|
161
|
+
for (const f of fs.readdirSync(agentsSrc).filter(n => n.endsWith('.md'))) {
|
|
162
|
+
let content = fs.readFileSync(path.join(agentsSrc, f), 'utf8');
|
|
163
|
+
// Core references → the bundle token; mentions → /pan-<name>; then the same
|
|
164
|
+
// two steps the installer applies to a Copilot agent (thinking frontmatter
|
|
165
|
+
// strip, Copilot frontmatter/tool-name conversion).
|
|
166
|
+
content = lib.rewriteAgentReferenceCopy(content, TOKEN_PREFIX);
|
|
167
|
+
content = lib.stripThinkingFrontmatter(content, 'copilot');
|
|
168
|
+
content = lib.convertClaudeToCopilotAgent(content);
|
|
169
|
+
fs.writeFileSync(path.join(agentsDest, f.replace(/\.md$/, '.agent.md')), content);
|
|
170
|
+
agents++;
|
|
171
|
+
}
|
|
172
|
+
fs.mkdirSync(path.join(nsDir, 'hooks'), { recursive: true });
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(nsDir, 'hooks', 'hooks.json'),
|
|
175
|
+
JSON.stringify(lib.buildCopilotPluginHooksConfig(hookCommands('${CLAUDE_PLUGIN_ROOT}')), null, 2) + '\n'
|
|
176
|
+
);
|
|
177
|
+
return agents;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function main() {
|
|
181
|
+
assertSafeToReplace(OUT);
|
|
182
|
+
fs.rmSync(OUT, { recursive: true, force: true });
|
|
183
|
+
fs.mkdirSync(OUT, { recursive: true });
|
|
184
|
+
|
|
185
|
+
// 1. Manifest (closed schema — nothing beyond the ten permitted keys)
|
|
186
|
+
fs.writeFileSync(path.join(OUT, 'plugin.json'), JSON.stringify(lib.buildAgentPluginManifest(pkg), null, 2) + '\n');
|
|
187
|
+
|
|
188
|
+
// 2. Skills
|
|
189
|
+
const skills = emitSkills(path.join(ROOT, 'commands', 'pan'), path.join(OUT, 'skills'), 'pan');
|
|
190
|
+
|
|
191
|
+
// 3. Core (+ 4. canonical agent copies inside it)
|
|
192
|
+
const coreDest = path.join(OUT, 'pan-wizard-core');
|
|
193
|
+
emitCore(path.join(ROOT, 'pan-wizard-core'), coreDest);
|
|
194
|
+
const agents = emitAgentReferenceCopies(path.join(ROOT, 'agents'), path.join(coreDest, 'agents'));
|
|
195
|
+
|
|
196
|
+
// 5. MCP declaration
|
|
197
|
+
fs.writeFileSync(path.join(OUT, 'mcp.json'), JSON.stringify(lib.buildAgentPluginMcpConfig(), null, 2) + '\n');
|
|
198
|
+
|
|
199
|
+
// 6. Hooks: scripts once, at the root; a Codex hooks.json at the documented
|
|
200
|
+
// default location (`hooks/hooks.json`, `${PLUGIN_ROOT}` expanded in commands,
|
|
201
|
+
// observers async — the same builder the installer uses for .codex/hooks.json).
|
|
202
|
+
const hooksDir = path.join(OUT, 'hooks');
|
|
203
|
+
const hookScripts = emitHookScripts(hooksDir);
|
|
204
|
+
fs.writeFileSync(
|
|
205
|
+
path.join(hooksDir, 'hooks.json'),
|
|
206
|
+
JSON.stringify(lib.mergeCodexHooksConfig(null, hookCommands('${PLUGIN_ROOT}')), null, 2) + '\n'
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 7. Copilot vendor namespace
|
|
210
|
+
const copilotAgents = emitCopilotNamespace(path.join(ROOT, 'agents'), path.join(OUT, lib.COPILOT_PLUGIN_NAMESPACE));
|
|
211
|
+
|
|
212
|
+
console.log('PAN Agent Plugins bundle built at', path.relative(ROOT, OUT) || OUT);
|
|
213
|
+
console.log(' skills:', skills);
|
|
214
|
+
console.log(' agent reference copies:', agents);
|
|
215
|
+
console.log(' hook scripts:', hookScripts.length);
|
|
216
|
+
console.log(` ${lib.COPILOT_PLUGIN_NAMESPACE}/agents:`, copilotAgents);
|
|
217
|
+
console.log(' version:', pkg.version);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main();
|