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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/bin/install-lib.cjs +283 -1
  3. package/bin/install.js +127 -0
  4. package/commands/pan/hygiene.md +14 -8
  5. package/commands/pan/milestone-audit.md +10 -4
  6. package/hooks/dist/pan-cost-logger.js +69 -5
  7. package/hooks/dist/pan-stop-guard.js +32 -1
  8. package/hooks/dist/pan-trace-logger.js +35 -2
  9. package/package.json +3 -2
  10. package/pan-wizard-core/bin/lib/bridge.cjs +0 -1
  11. package/pan-wizard-core/bin/lib/bus.cjs +0 -1
  12. package/pan-wizard-core/bin/lib/campaign.cjs +3 -2
  13. package/pan-wizard-core/bin/lib/commands-learnings.cjs +8 -8
  14. package/pan-wizard-core/bin/lib/commands.cjs +15 -14
  15. package/pan-wizard-core/bin/lib/config.cjs +5 -5
  16. package/pan-wizard-core/bin/lib/constants.cjs +27 -0
  17. package/pan-wizard-core/bin/lib/context-budget.cjs +28 -0
  18. package/pan-wizard-core/bin/lib/core.cjs +190 -26
  19. package/pan-wizard-core/bin/lib/cost.cjs +0 -1
  20. package/pan-wizard-core/bin/lib/distill.cjs +3 -3
  21. package/pan-wizard-core/bin/lib/focus.cjs +16 -16
  22. package/pan-wizard-core/bin/lib/hud.cjs +1 -1
  23. package/pan-wizard-core/bin/lib/hygiene.cjs +397 -37
  24. package/pan-wizard-core/bin/lib/init.cjs +90 -13
  25. package/pan-wizard-core/bin/lib/knowledge.cjs +0 -1
  26. package/pan-wizard-core/bin/lib/memory.cjs +1 -1
  27. package/pan-wizard-core/bin/lib/milestone.cjs +3 -3
  28. package/pan-wizard-core/bin/lib/optimize.cjs +3 -3
  29. package/pan-wizard-core/bin/lib/phase.cjs +4 -4
  30. package/pan-wizard-core/bin/lib/planning-root.cjs +327 -0
  31. package/pan-wizard-core/bin/lib/preview.cjs +0 -1
  32. package/pan-wizard-core/bin/lib/review-deep.cjs +0 -1
  33. package/pan-wizard-core/bin/lib/roadmap.cjs +1 -1
  34. package/pan-wizard-core/bin/lib/state-compact.cjs +339 -0
  35. package/pan-wizard-core/bin/lib/state.cjs +0 -1
  36. package/pan-wizard-core/bin/lib/suggest.cjs +141 -0
  37. package/pan-wizard-core/bin/lib/template.cjs +1 -1
  38. package/pan-wizard-core/bin/lib/utils.cjs +39 -11
  39. package/pan-wizard-core/bin/lib/verify-deploy.cjs +113 -2
  40. package/pan-wizard-core/bin/lib/verify.cjs +4 -3
  41. package/pan-wizard-core/bin/lib/whatif.cjs +0 -1
  42. package/pan-wizard-core/bin/pan-tools.cjs +97 -7
  43. package/pan-wizard-core/mcp/native-tools.cjs +159 -0
  44. package/pan-wizard-core/mcp/orchestrator.cjs +179 -0
  45. package/{pan-zcode → pan-wizard-core}/mcp/server.cjs +35 -6
  46. package/{pan-zcode → pan-wizard-core}/mcp/tool-registry.cjs +60 -3
  47. package/pan-wizard-core/workflows/milestone-audit.md +35 -6
  48. package/pan-wizard-core/workflows/verify-phase.md +25 -6
  49. package/pan-zcode/README.md +17 -9
  50. package/pan-zcode/bin/install-zcode.js +4 -1
  51. package/scripts/build-plugin.js +35 -3
  52. package/scripts/deprecate-old-versions.js +225 -0
  53. package/scripts/plugin-path.js +84 -0
  54. package/pan-wizard-core/learnings/internal/.gitkeep +0 -2
  55. package/pan-wizard-core/learnings/internal/experiment-runner.md +0 -81
  56. package/pan-wizard-core/learnings/internal/external-research.md +0 -105
  57. package/pan-wizard-core/learnings/internal/loop-design.md +0 -33
  58. package/pan-wizard-core/learnings/internal/pan-dev-bugs.md +0 -181
  59. package/pan-zcode/mcp/native-tools.cjs +0 -63
  60. package/pan-zcode/mcp/orchestrator.cjs +0 -66
  61. /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-Z MCP bridge server (M1).
4
+ * PAN MCP bridge server.
5
5
  *
6
- * A dependency-free JSON-RPC 2.0 server over stdio implementing the small MCP
7
- * surface ZCode needs: server/discover / initialize / tools/list / tools/call /
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
- /** Default engine location: pan-wizard-core is a sibling of pan-zcode/. */
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, '..', '..', 'pan-wizard-core', 'bin', 'pan-tools.cjs');
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
- const r = runVerb(res.verb, []);
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
- /** Read-only aggregators → MCP resources (no side effects). */
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
- { uri: 'pan://roadmap', name: 'Roadmap', verb: 'roadmap', description: 'The project roadmap: phases, goals, success criteria.' },
35
- { uri: 'pan://phases', name: 'Phases', verb: 'phases', description: 'Phase inventory with per-phase status.' },
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 .planning/phases/*-*/*-summary.md; do
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 `.planning/v{version}-milestone-audit.md` with:
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:** .planning/v{version}-milestone-audit.md
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:** .planning/v{version}-milestone-audit.md
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 .planning/v{version}-milestone-audit.md — see full report
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:** .planning/v{version}-milestone-audit.md
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 command:
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
- TEST_CMD=$(node -e "const p=require('./package.json'); console.log(p.scripts && p.scripts.test || 'echo no test')")
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
- | All tests pass (`TEST_FAIL` = 0) | Record counts, continue to must-haves |
110
- | Tests regress (failures exist) | Set `test_gate_status: failed`, include failure details in verification report |
111
- | No test command found | Record as `test_gate_status: skipped`, continue |
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
  ```
@@ -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-zcode/mcp (this subsystem) a thin, zero-dep bridge — verbs → MCP tools/resources
20
- │ spawn: node pan-tools.cjs <verb> --raw --cwd <root>
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 human-origin
37
- env token that ignores agent-supplied approval; never force/reset/push) + `mcp/orchestrator.cjs`
38
- (the deterministic `next-action` state machine with safety caps + regression circuit-breaker),
39
- exposed as native MCP tools via `mcp/native-tools.cjs`.
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
- const serverPath = path.join(repoRoot, 'pan-zcode', 'mcp', 'server.cjs');
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 });
@@ -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 is gated on one live verification — whether ${CLAUDE_PLUGIN_ROOT}
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). Until then, content references core paths relative to the plugin
17
- * root, which matches the documented plugin working layout.
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 });