pan-wizard 3.25.0 → 3.27.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 +1 -1
- package/bin/install-lib.cjs +283 -1
- package/bin/install.js +127 -0
- package/commands/pan/hygiene.md +14 -8
- package/commands/pan/milestone-audit.md +10 -4
- 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 +3 -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 +27 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +28 -0
- package/pan-wizard-core/bin/lib/core.cjs +190 -26
- package/pan-wizard-core/bin/lib/cost.cjs +0 -1
- 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/hud.cjs +1 -1
- package/pan-wizard-core/bin/lib/hygiene.cjs +397 -37
- package/pan-wizard-core/bin/lib/init.cjs +90 -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/suggest.cjs +141 -0
- 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-deploy.cjs +113 -2
- package/pan-wizard-core/bin/lib/verify.cjs +4 -3
- package/pan-wizard-core/bin/lib/whatif.cjs +0 -1
- package/pan-wizard-core/bin/pan-tools.cjs +97 -7
- package/pan-wizard-core/mcp/native-tools.cjs +159 -0
- package/pan-wizard-core/mcp/orchestrator.cjs +179 -0
- package/{pan-zcode → pan-wizard-core}/mcp/server.cjs +35 -6
- package/{pan-zcode → pan-wizard-core}/mcp/tool-registry.cjs +60 -3
- package/pan-wizard-core/workflows/milestone-audit.md +35 -6
- package/pan-wizard-core/workflows/verify-phase.md +25 -6
- package/pan-zcode/README.md +17 -9
- package/pan-zcode/bin/install-zcode.js +4 -1
- package/scripts/build-plugin.js +35 -3
- package/scripts/deprecate-old-versions.js +225 -0
- package/scripts/plugin-path.js +84 -0
- package/pan-wizard-core/learnings/internal/.gitkeep +0 -2
- package/pan-wizard-core/learnings/internal/experiment-runner.md +0 -81
- package/pan-wizard-core/learnings/internal/external-research.md +0 -105
- package/pan-wizard-core/learnings/internal/loop-design.md +0 -33
- package/pan-wizard-core/learnings/internal/pan-dev-bugs.md +0 -181
- package/pan-zcode/mcp/native-tools.cjs +0 -63
- package/pan-zcode/mcp/orchestrator.cjs +0 -66
- /package/{pan-zcode → pan-wizard-core}/mcp/merge-gate.cjs +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The deterministic orchestrator ("next-action" state machine).
|
|
5
|
+
*
|
|
6
|
+
* A host with no workflow engine cannot machine-intercept a subagent spawn, so
|
|
7
|
+
* PAN's sequencing + safety harness (waves, regression circuit-breaker,
|
|
8
|
+
* spawn/budget caps, the human merge gate) cannot live in agent prose or a
|
|
9
|
+
* pre-spawn hook. The fix: keep the state machine here and expose ONE
|
|
10
|
+
* `next-action` tool the primary Agent polls before each step. Enforcement then
|
|
11
|
+
* happens at the (gateable) MCP-tool-call boundary, not at the (un-gateable)
|
|
12
|
+
* spawn event.
|
|
13
|
+
*
|
|
14
|
+
* `nextAction` is a PURE function of a snapshot, so its decisions are
|
|
15
|
+
* reproducible and unit-testable.
|
|
16
|
+
*
|
|
17
|
+
* ─── THE VOCABULARY, and why it is what it is ──────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* An external audit (2026-08-15) found this machine keyed on statuses that
|
|
20
|
+
* NOTHING IN PAN EMITS. It expected `executed` and `verified`; the only producer
|
|
21
|
+
* of a phase status is `classifyPhaseStatus()` (bin/lib/utils.cjs), which emits
|
|
22
|
+
*
|
|
23
|
+
* complete · partial · planned · researched · discussed · empty
|
|
24
|
+
*
|
|
25
|
+
* The consequences were severe and all three were reproduced before this rewrite:
|
|
26
|
+
*
|
|
27
|
+
* 1. `verify` and `request_merge` were UNREACHABLE. A phase ran
|
|
28
|
+
* planned → execute → complete and the machine moved to the next one, so
|
|
29
|
+
* **the human merge gate — the safety centrepiece — was skipped entirely.**
|
|
30
|
+
* 2. `partial` had no entry and fell through to `plan`, re-planning a
|
|
31
|
+
* half-executed phase forever (a live-lock).
|
|
32
|
+
* 3. `pan://progress` reports Title Case (`"Planned"`), so a caller following
|
|
33
|
+
* this file's own instruction to assemble the snapshot from the MCP
|
|
34
|
+
* resources got `undefined` → `plan` on every cycle, forever.
|
|
35
|
+
*
|
|
36
|
+
* The suite was green throughout because its fixtures spoke `executed`/`verified`
|
|
37
|
+
* — a vocabulary no producer emits. The fixture was MORE capable than reality and
|
|
38
|
+
* therefore produced false confidence. Fixtures here must use the real vocabulary.
|
|
39
|
+
*
|
|
40
|
+
* ─── The model now ─────────────────────────────────────────────────────────
|
|
41
|
+
*
|
|
42
|
+
* Disk status answers "how far did the artifacts get?". It cannot answer "was
|
|
43
|
+
* this verified?" or "was it merged?" — those are RUN facts, known to the caller
|
|
44
|
+
* that just performed them, and no file on disk records them. So a phase carries
|
|
45
|
+
* its disk status plus two optional booleans:
|
|
46
|
+
*
|
|
47
|
+
* { number, status, verified?: boolean, merged?: boolean }
|
|
48
|
+
*
|
|
49
|
+
* which makes the full ladder reachable from the vocabulary PAN actually emits:
|
|
50
|
+
*
|
|
51
|
+
* planned → execute
|
|
52
|
+
* partial → execute (RESUME, never re-plan)
|
|
53
|
+
* empty | discussed | researched → plan
|
|
54
|
+
* complete & !verified → verify
|
|
55
|
+
* complete & verified & !merged → request_merge
|
|
56
|
+
* complete & verified & merged → (skip; this phase is finished)
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
// Budget is advisory by default (enforceBudget:false) — it never stops the loop
|
|
60
|
+
// unless the caller opts in. maxCycles remains a hard safety stop.
|
|
61
|
+
const DEFAULT_CAPS = { maxCycles: 25, budget: Infinity, enforceBudget: false };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Disk status → next action, for statuses that precede execution.
|
|
65
|
+
* Keys are the REAL `classifyPhaseStatus()` vocabulary. `complete` is absent on
|
|
66
|
+
* purpose: what follows completion depends on the run facts, not on disk.
|
|
67
|
+
*
|
|
68
|
+
* The two legacy keys are retained as aliases so a caller written against the
|
|
69
|
+
* old contract keeps working, but nothing in PAN produces them.
|
|
70
|
+
*/
|
|
71
|
+
const PHASE_NEXT = {
|
|
72
|
+
// real disk vocabulary
|
|
73
|
+
empty: 'plan',
|
|
74
|
+
discussed: 'plan',
|
|
75
|
+
researched: 'plan',
|
|
76
|
+
planned: 'execute',
|
|
77
|
+
partial: 'execute',
|
|
78
|
+
// legacy aliases — no producer emits these; kept for callers written against
|
|
79
|
+
// the pre-2026-08 contract. Do NOT use them in new fixtures.
|
|
80
|
+
none: 'plan',
|
|
81
|
+
executed: 'verify',
|
|
82
|
+
verified: 'request_merge',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Statuses meaning "the artifacts are all there". */
|
|
86
|
+
const COMPLETE_STATUS = 'complete';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalise a status to the lowercase vocabulary the table keys on.
|
|
90
|
+
*
|
|
91
|
+
* `pan://progress` emits Title Case (`"Planned"`) while `roadmap analyze` emits
|
|
92
|
+
* lowercase (`"planned"`); this file's docs point callers at the resources, so
|
|
93
|
+
* the Title Case form is the one a compliant caller will actually send. Folding
|
|
94
|
+
* case here is what makes the documented assembly path work at all.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeStatus(status) {
|
|
97
|
+
return typeof status === 'string' ? status.trim().toLowerCase() : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True when a phase still needs work of any kind. */
|
|
101
|
+
function isPhaseOpen(phase) {
|
|
102
|
+
if (!phase || typeof phase !== 'object') return false;
|
|
103
|
+
const status = normalizeStatus(phase.status);
|
|
104
|
+
if (status !== COMPLETE_STATUS) return true;
|
|
105
|
+
// Complete on disk, but the run facts decide whether it is actually finished.
|
|
106
|
+
return !(phase.verified === true && phase.merged === true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Decide the next deterministic action.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} state snapshot:
|
|
113
|
+
* { phases:[{number, status, verified?, merged?}], cycles?, points_used?,
|
|
114
|
+
* tests_before?, tests_after?, awaiting_approval?, aborted? }
|
|
115
|
+
* `phases` is REQUIRED. A snapshot without a usable one is reported as
|
|
116
|
+
* `no_phases` / `done:false` — never as success (see below).
|
|
117
|
+
* @param {Object} [caps] { maxCycles, budget, enforceBudget }
|
|
118
|
+
* @returns {{action:string, args?:Object, reason:string, done:boolean}}
|
|
119
|
+
* action ∈ plan | execute | verify | request_merge | await_approval | stop
|
|
120
|
+
*/
|
|
121
|
+
function nextAction(state, caps) {
|
|
122
|
+
const c = Object.assign({}, DEFAULT_CAPS, caps || {});
|
|
123
|
+
state = state || {};
|
|
124
|
+
|
|
125
|
+
// Hard stops first — safety caps and the circuit-breaker outrank all progress.
|
|
126
|
+
if (state.aborted) return { action: 'stop', reason: 'aborted', done: true };
|
|
127
|
+
if (
|
|
128
|
+
typeof state.tests_before === 'number' &&
|
|
129
|
+
typeof state.tests_after === 'number' &&
|
|
130
|
+
state.tests_after < state.tests_before
|
|
131
|
+
) {
|
|
132
|
+
return { action: 'stop', reason: 'regression', done: true };
|
|
133
|
+
}
|
|
134
|
+
if ((state.cycles || 0) >= c.maxCycles) return { action: 'stop', reason: 'max_cycles', done: true };
|
|
135
|
+
if (c.enforceBudget && (state.points_used || 0) >= c.budget) return { action: 'stop', reason: 'budget_cap', done: true };
|
|
136
|
+
|
|
137
|
+
// The human merge gate is a barrier: while a merge awaits approval, do nothing else.
|
|
138
|
+
if (state.awaiting_approval) return { action: 'await_approval', reason: 'human_gate', done: false };
|
|
139
|
+
|
|
140
|
+
// A MISSING OR MISSHAPEN `phases` IS NOT SUCCESS.
|
|
141
|
+
//
|
|
142
|
+
// This previously collapsed to `[]`, and `[].find(...)` is indistinguishable
|
|
143
|
+
// from "every phase is complete" — so `{}`, a wrong key, or a non-array all
|
|
144
|
+
// returned all_complete/done:true. An orchestrator wrong in the "keep working"
|
|
145
|
+
// direction wastes tokens and gets noticed; one wrong in the `done:true`
|
|
146
|
+
// direction silently stops the loop and reports success. `no_phases` carries
|
|
147
|
+
// `done:false` precisely so a caller cannot mistake "I could not read this"
|
|
148
|
+
// for "the work is finished".
|
|
149
|
+
const hasPhases = Array.isArray(state.phases)
|
|
150
|
+
&& state.phases.some((p) => p && typeof p === 'object' && (p.status !== undefined || p.number !== undefined));
|
|
151
|
+
if (!hasPhases) {
|
|
152
|
+
return { action: 'stop', reason: 'no_phases', done: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Advance the first phase that is not finished.
|
|
156
|
+
const phase = state.phases.find(isPhaseOpen);
|
|
157
|
+
if (!phase) return { action: 'stop', reason: 'all_complete', done: true };
|
|
158
|
+
|
|
159
|
+
const status = normalizeStatus(phase.status);
|
|
160
|
+
const args = { phase: phase.number };
|
|
161
|
+
|
|
162
|
+
// Complete on disk: the run facts decide. This is the branch that makes the
|
|
163
|
+
// merge gate reachable — without it a completed phase was simply skipped.
|
|
164
|
+
if (status === COMPLETE_STATUS) {
|
|
165
|
+
if (phase.verified !== true) return { action: 'verify', args, reason: 'phase_complete_unverified', done: false };
|
|
166
|
+
return { action: 'request_merge', args, reason: 'phase_verified_unmerged', done: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const action = PHASE_NEXT[status];
|
|
170
|
+
if (!action) {
|
|
171
|
+
// An unknown status is a caller/producer mismatch, not a licence to re-plan.
|
|
172
|
+
// Say so, rather than silently defaulting the way the old `|| 'plan'` did —
|
|
173
|
+
// that default is what turned Title Case into an infinite plan loop.
|
|
174
|
+
return { action: 'plan', args, reason: `phase_unknown_status_${status || 'missing'}`, done: false };
|
|
175
|
+
}
|
|
176
|
+
return { action, args, reason: `phase_${status}`, done: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { nextAction, DEFAULT_CAPS, PHASE_NEXT, normalizeStatus, isPhaseOpen };
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* PAN
|
|
4
|
+
* PAN MCP bridge server.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Canonical home: `pan-wizard-core/mcp/`, so it ships with the engine to every
|
|
7
|
+
* install and every runtime. It was originally written for the PAN-Z/ZCode
|
|
8
|
+
* preview (`pan-zcode/`), which remains a CONSUMER rather than the owner — the
|
|
9
|
+
* protocol layer is harness-neutral and must not be forked per consumer.
|
|
10
|
+
*
|
|
11
|
+
* A dependency-free JSON-RPC 2.0 server over stdio implementing a small MCP
|
|
12
|
+
* surface: server/discover / initialize / tools/list / tools/call /
|
|
8
13
|
* resources/list / resources/read / ping. It is a DUAL-ERA server (see the MCP
|
|
9
14
|
* 2026-07-28 versioning spec): legacy clients open with the `initialize`
|
|
10
15
|
* handshake; modern clients (2026-07-28+) declare their protocol version in each
|
|
@@ -47,9 +52,27 @@ const SUPPORTED_VERSIONS_LIST = [MODERN_PROTOCOL_VERSION, '2025-06-18', '2025-03
|
|
|
47
52
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set(SUPPORTED_VERSIONS_LIST);
|
|
48
53
|
const SERVER_INFO = { name: 'pan-mcp', version: '0.1.0' };
|
|
49
54
|
|
|
50
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Default engine location: `bin/` is a sibling of this `mcp/` directory inside
|
|
57
|
+
* pan-wizard-core. That holds in the source repo AND in every install, because
|
|
58
|
+
* the installer copies pan-wizard-core wholesale, so the two stay siblings
|
|
59
|
+
* wherever the tree lands. Callers can still override via `opts.panToolsPath`
|
|
60
|
+
* (an out-of-tree engine, a test fixture, a pinned version) or PAN_TOOLS_PATH.
|
|
61
|
+
*
|
|
62
|
+
* This replaced `join(__dirname, '..', '..', 'pan-wizard-core', 'bin', …)`,
|
|
63
|
+
* carried over from when the module lived in `pan-zcode/mcp/`. Do NOT record
|
|
64
|
+
* that as a bug the relocation fixed — from this directory the two forms
|
|
65
|
+
* resolve to the identical path (the grandparent of `mcp/` contains
|
|
66
|
+
* `pan-wizard-core/` in both the source tree and an install). The old form is
|
|
67
|
+
* merely over-specified: it requires the grandparent to hold a directory
|
|
68
|
+
* *named* `pan-wizard-core`, so it breaks if the core is vendored or renamed,
|
|
69
|
+
* while the sibling form only requires the layout it actually depends on.
|
|
70
|
+
* Covered by the "engine path resolution" suite in tests/pan-zcode-mcp.test.cjs,
|
|
71
|
+
* which exists because every other test injects a spawn or passes an explicit
|
|
72
|
+
* path — so this function had zero coverage when the module moved.
|
|
73
|
+
*/
|
|
51
74
|
function defaultPanToolsPath() {
|
|
52
|
-
return path.join(__dirname, '..', '
|
|
75
|
+
return path.join(__dirname, '..', 'bin', 'pan-tools.cjs');
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
/** Real spawn: shell-less execFile of `node <argv...>`. */
|
|
@@ -176,7 +199,13 @@ function createServer(opts = {}) {
|
|
|
176
199
|
function readResource(uri) {
|
|
177
200
|
const res = reg.byResourceUri[uri];
|
|
178
201
|
if (!res) return { unknown: true };
|
|
179
|
-
|
|
202
|
+
// A resource's argv tail is a STATIC array on its descriptor (for verbs whose
|
|
203
|
+
// read is a subcommand, e.g. `validate health`). It never derives from the
|
|
204
|
+
// request: resources take no client parameters, so there is no input path into
|
|
205
|
+
// this argv. Guard the type anyway — a descriptor typo must not spread a
|
|
206
|
+
// non-array into the spawn.
|
|
207
|
+
const tail = Array.isArray(res.args) ? res.args : [];
|
|
208
|
+
const r = runVerb(res.verb, tail);
|
|
180
209
|
if (!r.ok) return { error: { code: -32603, message: r.stderr || 'resource read failed' } };
|
|
181
210
|
return { result: { contents: [{ uri, mimeType: 'application/json', text: r.stdout }] } };
|
|
182
211
|
}
|
|
@@ -28,12 +28,45 @@ const AGENT_RE = /^[a-z][a-z0-9_-]{1,60}$/; // agent type, e.g. pan-planner
|
|
|
28
28
|
const PHASE_RE = /^[0-9]{1,3}$/; // phase number, e.g. 03
|
|
29
29
|
const QUERY_RE = /^[\w .,:/&()-]{1,120}$/; // find-phase query fragment
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Read-only aggregators → MCP resources (no side effects).
|
|
33
|
+
*
|
|
34
|
+
* Optional `args` is a STATIC argv tail for verbs whose read lives in a
|
|
35
|
+
* subcommand (`validate health`, `links validate`, `cost report`). It is
|
|
36
|
+
* deliberately a fixed array and never a function of client input — a resource
|
|
37
|
+
* takes no parameters, and that is precisely what makes the surface safe: there
|
|
38
|
+
* is no path from an LLM tool-call to these argv elements.
|
|
39
|
+
*
|
|
40
|
+
* THE RULE FOR ADDING ONE — a resource must be readable on ANY project, including
|
|
41
|
+
* a bare directory with no `.planning/`. If "no data yet" is reported as an error
|
|
42
|
+
* (non-zero exit / an error-family key), it is a TOOL, not a resource: a client
|
|
43
|
+
* that lists resources and reads them should not collect failures for a young
|
|
44
|
+
* project. `preview` is the worked example — `preview phases` exits non-zero
|
|
45
|
+
* without a roadmap, so it is exposed as a tool below rather than as a resource.
|
|
46
|
+
* Check before adding: run the verb in an empty dir and read `$?`.
|
|
47
|
+
*/
|
|
32
48
|
const RESOURCES = [
|
|
33
49
|
{ uri: 'pan://state', name: 'Project state', verb: 'state', description: 'Current PAN project state snapshot derived from .planning/.' },
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
// NOTE: there is deliberately no `pan://roadmap`. One existed and was DEAD from
|
|
51
|
+
// M1 until 2026-08 — its descriptor named the bare verb `roadmap`, which requires
|
|
52
|
+
// a subcommand, so every read returned "Unknown roadmap subcommand". Nothing
|
|
53
|
+
// caught it because the protocol tests inject a fake spawn, so no test had ever
|
|
54
|
+
// run a resource against the real engine. It is now `pan_roadmap_analyze` in
|
|
55
|
+
// TOOLS: `roadmap analyze` exits non-zero on a project with no roadmap.md, which
|
|
56
|
+
// fails the resource rule above. Removing the URI breaks no consumer — no
|
|
57
|
+
// consumer can have depended on a read that always errored.
|
|
58
|
+
// `phases` alone is not a verb — the subcommand is `list`. This descriptor named
|
|
59
|
+
// the bare verb and was DEAD from M1 alongside pan://roadmap, for the same reason
|
|
60
|
+
// and found by the same test. Returns {directories, count}.
|
|
61
|
+
{ uri: 'pan://phases', name: 'Phases', verb: 'phases', args: ['list'],
|
|
62
|
+
description: 'Phase inventory: the phase directories present, with a count.' },
|
|
36
63
|
{ uri: 'pan://progress', name: 'Progress', verb: 'progress', description: 'Requirement and plan completion progress.' },
|
|
64
|
+
{ 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 (exit 0), so it is readable even on a broken or empty one.' },
|
|
66
|
+
{ uri: 'pan://links', name: 'Doc-code links', verb: 'links', args: ['validate'],
|
|
67
|
+
description: 'Doc↔code link graph verdict: forward links, backlink contracts, and anchor targets, with finding codes.' },
|
|
68
|
+
{ uri: 'pan://cost', name: 'Token cost', verb: 'cost', args: ['report'],
|
|
69
|
+
description: 'Aggregated token spend from the .planning/metrics ledger. Reads as zeros on a project with no recorded calls.' },
|
|
37
70
|
];
|
|
38
71
|
|
|
39
72
|
/** Actionable pan-tools verbs → MCP tools (each spawns `node pan-tools.cjs <verb>`). */
|
|
@@ -58,6 +91,30 @@ const SPAWN_TOOLS = [
|
|
|
58
91
|
},
|
|
59
92
|
args: (i) => [str('query', i && i.query, QUERY_RE, 120)],
|
|
60
93
|
},
|
|
94
|
+
{
|
|
95
|
+
name: 'pan_roadmap_analyze', title: 'Analyze the roadmap', verb: 'roadmap',
|
|
96
|
+
description: 'The roadmap read: milestones, phases, goals and success criteria with completion analysis. A tool rather than a resource because it reports a project with no roadmap.md as an error.',
|
|
97
|
+
readOnly: true, destructive: false,
|
|
98
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
99
|
+
args: () => ['analyze'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'pan_preview_phases', title: 'Preview all phases (dependency graph)', verb: 'preview',
|
|
103
|
+
description: 'Phase dependency graph: a mermaid DAG, the parallel-executable batches, and any hidden dependencies. Errors on a project with no roadmap, which is why this is a tool rather than a resource.',
|
|
104
|
+
readOnly: true, destructive: false,
|
|
105
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
106
|
+
args: () => ['phases'],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'pan_preview_phase', title: 'Preview one phase (blast radius)', verb: 'preview',
|
|
110
|
+
description: 'Blast radius for a single phase: what it touches and what depends on it, before any work starts.',
|
|
111
|
+
readOnly: true, destructive: false,
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object', additionalProperties: false, required: ['phase'],
|
|
114
|
+
properties: { phase: { type: 'string', description: 'Phase number, e.g. 03' } },
|
|
115
|
+
},
|
|
116
|
+
args: (i) => ['phase', str('phase', i && i.phase, PHASE_RE, 3)],
|
|
117
|
+
},
|
|
61
118
|
{
|
|
62
119
|
name: 'pan_report_phase', title: 'Generate a phase HTML report', verb: 'report',
|
|
63
120
|
description: 'Render the self-contained HTML report for one phase (a build deliverable). Writes only its own file; non-destructive and idempotent.',
|
|
@@ -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
|
|
|
@@ -88,11 +88,18 @@ This step provides awareness; the hard gate is enforced by exec-phase.
|
|
|
88
88
|
|
|
89
89
|
This step catches test regressions that goal-backward analysis cannot detect.
|
|
90
90
|
|
|
91
|
-
1. Detect test
|
|
91
|
+
1. Detect whether a test script exists. **This decides `skipped` vs `failed` later**, so
|
|
92
|
+
it must be answered BEFORE running anything — `npm test` on a project with no `test`
|
|
93
|
+
script exits non-zero with "Missing script", which is indistinguishable from a crash
|
|
94
|
+
once you are only looking at the exit code.
|
|
95
|
+
|
|
92
96
|
```bash
|
|
93
|
-
|
|
97
|
+
HAS_TEST=$(node -e "try{const p=require('./package.json');process.stdout.write(p.scripts&&p.scripts.test?'yes':'no')}catch(e){process.stdout.write('no')}")
|
|
94
98
|
```
|
|
95
99
|
|
|
100
|
+
**If `HAS_TEST` is `no`:** record `test_gate_status: skipped` and go to step 4. Do not
|
|
101
|
+
run the suite, and do not report a crash — there is nothing to run.
|
|
102
|
+
|
|
96
103
|
2. Run test suite and capture results:
|
|
97
104
|
```bash
|
|
98
105
|
TEST_OUTPUT=$(npm test 2>&1)
|
|
@@ -102,13 +109,25 @@ TEST_PASS=$(echo "$TEST_OUTPUT" | grep -E "^ℹ pass" | awk '{print $NF}')
|
|
|
102
109
|
TEST_FAIL=$(echo "$TEST_OUTPUT" | grep -E "^ℹ fail" | awk '{print $NF}')
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
3. Evaluate results
|
|
112
|
+
3. Evaluate results.
|
|
113
|
+
|
|
114
|
+
**Check `TEST_EXIT` FIRST, before any count.** A suite that did not run emits no
|
|
115
|
+
`ℹ fail` line at all, so `TEST_FAIL` comes back EMPTY — not `0`. Reading an empty
|
|
116
|
+
value as "no failures" scores a crashed suite as a pass, which is the worst
|
|
117
|
+
direction this gate can fail in: a phase ships green on a project whose tests never
|
|
118
|
+
executed. Empty is not zero. Judge the exit code, then the counts.
|
|
106
119
|
|
|
107
120
|
| Condition | Action |
|
|
108
121
|
|-----------|--------|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
122
|
+
| `TEST_EXIT` = 0 **and** `TEST_FAIL` = 0 (a real number) | Record counts, continue to must-haves |
|
|
123
|
+
| `TEST_EXIT` ≠ 0 **and** failures were reported | Set `test_gate_status: failed`, include failure details |
|
|
124
|
+
| `TEST_EXIT` ≠ 0 **and** `TEST_FAIL` is empty/absent — the suite CRASHED or could not run (syntax error, missing module, bad import, no runner) | Set `test_gate_status: failed`, and record the reason as `suite did not run`. **Never `skipped`, never `passed`.** A suite that cannot execute is stronger evidence of a broken phase than one that runs and fails |
|
|
125
|
+
| `HAS_TEST` = `no` (handled in step 1) | Record as `test_gate_status: skipped`, continue. This is the ONLY legitimate route to `skipped` |
|
|
126
|
+
|
|
127
|
+
**The distinction that matters:** *no test command exists* is a project that never had
|
|
128
|
+
tests — a known, acceptable gap. *A test command exists and did not run* is a broken
|
|
129
|
+
project. They must never share a verdict; the first is `skipped`, the second is
|
|
130
|
+
`failed`.
|
|
112
131
|
|
|
113
132
|
4. Store test gate results for inclusion in verification.md:
|
|
114
133
|
```
|
package/pan-zcode/README.md
CHANGED
|
@@ -16,27 +16,35 @@ ZCode through the one interface it speaks: **MCP**.
|
|
|
16
16
|
```
|
|
17
17
|
ZCode harness (GLM-5.2) primary Agent drives everything; ported subagents fan out
|
|
18
18
|
│ MCP · local stdio
|
|
19
|
-
pan-
|
|
20
|
-
│ spawn: node pan-tools.cjs <verb> --
|
|
19
|
+
pan-wizard-core/mcp (SHARED) a thin, zero-dep bridge — verbs → MCP tools/resources
|
|
20
|
+
│ spawn: node pan-tools.cjs <verb> --cwd <root>
|
|
21
21
|
pan-wizard-core (reused as-is) the deterministic engine; .planning/ stays the state store
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
**The bridge is no longer part of this subsystem.** It was written here, but it lives at
|
|
25
|
+
`pan-wizard-core/mcp/` so it ships with the engine to every install and every runtime — PAN-Z
|
|
26
|
+
is now a **consumer** of it, alongside the main installer. `install-zcode.js` emits an MCP
|
|
27
|
+
registration pointing at that shared path. **Never fork a copy back under `pan-zcode/`**: one
|
|
28
|
+
protocol layer, many consumers, or the two drift the way the per-runtime command trees did
|
|
29
|
+
before ADR-0028.
|
|
30
|
+
|
|
24
31
|
**Scope boundary (by design):** the bridge exposes `pan-tools` verbs as MCP tools/resources and nothing more. It intentionally does **not** carry rich agent *session state* — diffs, streaming, live thread lifecycle — because MCP can't faithfully represent it (the reason OpenAI built the Codex harness as a native Rust core rather than over MCP). Keep the bridge to tool/resource exposure; the CLI's JSON contract is the tool contract. See `KNOWN-BETA-RISKS.md`.
|
|
25
32
|
|
|
26
33
|
## Status — M1–M5 built (M0 is the human verify spike)
|
|
27
34
|
|
|
28
|
-
- **M1 — bridge core.** `mcp/tool-registry.cjs` (pure verb→tool/resource map, with a hard
|
|
29
|
-
guardrail against exposing a force/reset/rebase/push verb) + `mcp/server.cjs` (a
|
|
35
|
+
- **M1 — bridge core.** `pan-wizard-core/mcp/tool-registry.cjs` (pure verb→tool/resource map, with a hard
|
|
36
|
+
guardrail against exposing a force/reset/rebase/push verb) + `pan-wizard-core/mcp/server.cjs` (a
|
|
30
37
|
**zero-dependency** JSON-RPC 2.0 stdio MCP server; reads → resources, actions → tools with
|
|
31
38
|
accurate hints; shell-less `execFile` spawn; `@file:` overflow protocol; strict per-arg
|
|
32
39
|
validation). **Dual-era** per the MCP 2026-07-28 stateless spec (ADR-0041): legacy clients
|
|
33
40
|
use the `initialize` handshake; modern clients declare their protocol version in each
|
|
34
41
|
request's `_meta`, probe `server/discover`, and get `UnsupportedProtocolVersionError`
|
|
35
42
|
(`-32022`) on a version mismatch.
|
|
36
|
-
- **M2 — determinism grafts.** `mcp/merge-gate.cjs` (two-step, model-proof merge: a
|
|
37
|
-
env token that ignores agent-supplied approval; never force/reset/push) +
|
|
38
|
-
(the deterministic `next-action` state machine with safety
|
|
39
|
-
exposed as native MCP tools via
|
|
43
|
+
- **M2 — determinism grafts.** `pan-wizard-core/mcp/merge-gate.cjs` (two-step, model-proof merge: a
|
|
44
|
+
human-origin env token that ignores agent-supplied approval; never force/reset/push) +
|
|
45
|
+
`pan-wizard-core/mcp/orchestrator.cjs` (the deterministic `next-action` state machine with safety
|
|
46
|
+
caps + regression circuit-breaker), exposed as native MCP tools via
|
|
47
|
+
`pan-wizard-core/mcp/native-tools.cjs`.
|
|
40
48
|
- **M3 — content port.** `lib/convert-agent.cjs` — Claude agents → ZCode subagents (reusing the
|
|
41
49
|
installer's frontmatter helpers): drops `Task` (no nesting), maps PAN tiers → `inherit`,
|
|
42
50
|
preserves the body; plus a command → skill wrapper.
|
|
@@ -59,7 +67,7 @@ A third M0 checkpoint (added 2026-08): **which protocol era does the real ZCode
|
|
|
59
67
|
The bridge is now dual-era (ADR-0041), so it answers both a legacy `initialize` handshake and a
|
|
60
68
|
modern `server/discover` probe. Confirm on a real install which path ZCode takes and that the
|
|
61
69
|
version it declares is in our supported list; if ZCode ever declares a revision newer than
|
|
62
|
-
`2026-07-28`, add it to `SUPPORTED_VERSIONS_LIST` in `mcp/server.cjs` once its method shapes are
|
|
70
|
+
`2026-07-28`, add it to `SUPPORTED_VERSIONS_LIST` in `pan-wizard-core/mcp/server.cjs` once its method shapes are
|
|
63
71
|
implemented.
|
|
64
72
|
|
|
65
73
|
## Zero dependencies
|
|
@@ -98,7 +98,10 @@ function buildBundle(o) {
|
|
|
98
98
|
assertNotInSourceRepo(destDir, repoRoot);
|
|
99
99
|
|
|
100
100
|
const agentsSrc = path.join(repoRoot, 'agents');
|
|
101
|
-
|
|
101
|
+
// The MCP protocol layer lives in pan-wizard-core/mcp/ (it ships with the
|
|
102
|
+
// engine to every install and every runtime). PAN-Z is a CONSUMER of it, not
|
|
103
|
+
// its owner — never fork a copy back under pan-zcode/, or the two drift.
|
|
104
|
+
const serverPath = path.join(repoRoot, 'pan-wizard-core', 'mcp', 'server.cjs');
|
|
102
105
|
const panToolsPath = path.join(repoRoot, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
|
|
103
106
|
|
|
104
107
|
fs.mkdirSync(destDir, { recursive: true });
|
package/scripts/build-plugin.js
CHANGED
|
@@ -11,10 +11,23 @@
|
|
|
11
11
|
* pan-wizard-core/ dispatcher + modules + workflows + templates
|
|
12
12
|
*
|
|
13
13
|
* Distribution status: built ALONGSIDE the loose-file installer. Marketplace
|
|
14
|
-
* publishing
|
|
14
|
+
* publishing WAS gated on one live verification — whether ${CLAUDE_PLUGIN_ROOT}
|
|
15
15
|
* expands inside command markdown content (documented for hook/MCP configs
|
|
16
|
-
* only).
|
|
17
|
-
*
|
|
16
|
+
* only).
|
|
17
|
+
*
|
|
18
|
+
* ANSWERED 2026-08-14, Claude Code 2.1.233 on Windows, by installing this plugin
|
|
19
|
+
* from the `command`-source marketplace in `marketplace/` and running
|
|
20
|
+
* `/pan-plugin-selftest`: **it does expand.** The command body reached the model
|
|
21
|
+
* with a real absolute path — no placeholder text survived — and invoking
|
|
22
|
+
* pan-tools through that path worked. So the CONTENT_PREFIX rewrite below is
|
|
23
|
+
* correct as it stands, and the gate is lifted.
|
|
24
|
+
*
|
|
25
|
+
* One measurement from the same run that constrains how far to take this: the
|
|
26
|
+
* `CLAUDE_PLUGIN_ROOT` environment variable is NOT exported into the Bash tool's
|
|
27
|
+
* environment (it read as empty). Textual substitution and shell expansion are
|
|
28
|
+
* therefore NOT interchangeable — generated content must keep using the
|
|
29
|
+
* substituted form, because `$CLAUDE_PLUGIN_ROOT` evaluated by a shell at runtime
|
|
30
|
+
* expands to nothing. Re-measure before relying on the shell form anywhere.
|
|
18
31
|
*
|
|
19
32
|
* Usage: node scripts/build-plugin.js (or npm run build:plugin)
|
|
20
33
|
*/
|
|
@@ -88,6 +101,25 @@ function main() {
|
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
// 2b. Plugin-only self-test command. NOT copied from commands/pan/ — it is
|
|
105
|
+
// generated here so the shipped command set stays unchanged and no ordinary
|
|
106
|
+
// install gains a diagnostic. It answers the one question gating publication:
|
|
107
|
+
// whether CONTENT_PREFIX expands inside command markdown. The placeholder must
|
|
108
|
+
// reach the plugin UNEXPANDED or the probe measures nothing, so this write
|
|
109
|
+
// deliberately bypasses rewriteContent().
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(OUT, 'commands', 'pan-plugin-selftest.md'),
|
|
112
|
+
lib.buildPluginSelfTestCommand(CONTENT_PREFIX.replace(/\/$/, ''))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// 4b. MCP registration. The server itself rides along inside pan-wizard-core
|
|
116
|
+
// (step 5 copies it wholesale), but shipping it is not the same as declaring
|
|
117
|
+
// it — without this file the plugin carried the bridge and never registered it.
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
path.join(OUT, '.mcp.json'),
|
|
120
|
+
JSON.stringify(lib.buildPluginMcpConfig(), null, 2) + '\n'
|
|
121
|
+
);
|
|
122
|
+
|
|
91
123
|
// 5. Core (strip source-only internal learnings, same policy as the installer)
|
|
92
124
|
copyTree(path.join(ROOT, 'pan-wizard-core'), path.join(OUT, 'pan-wizard-core'), rewriteContent);
|
|
93
125
|
fs.rmSync(path.join(OUT, 'pan-wizard-core', 'learnings', 'internal'), { recursive: true, force: true });
|