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
package/README.md CHANGED
@@ -659,7 +659,7 @@ PAN is not a replacement for your IDE or AI agent — it's the orchestration lay
659
659
  | `/pan:debug [desc]` | Systematic debugging with persistent state |
660
660
  | `/pan:quick [--full]` | Execute ad-hoc task with PAN guarantees (`--full` adds plan-checking and verification) |
661
661
  | `/pan:health [--repair]` | Validate `.planning/` directory integrity; `--repair` auto-fixes detected issues |
662
- | `/pan:hygiene [--apply] [--trace-age-days N]` | Scan for PAN version drift and stale project artifacts (legacy filenames, .tmp orphans, memory bloat, poisoned cost ledgers, trace debris, fragment planning dirs); `--apply` executes the safe fixes — ledgers are quarantined by rename, never deleted |
662
+ | `/pan:hygiene [--apply] [--trace-age-days N] [--all-tracks]` | Scan for PAN version drift and stale project artifacts (legacy filenames, .tmp orphans, memory bloat, poisoned cost ledgers, trace and report debris, cached-context bloat, fragment planning dirs); `--apply` executes the safe fixes — ledgers are quarantined by rename, never deleted, and settled `state.md` history is archived rather than dropped |
663
663
  | `/pan:links [--strict]` | Validate the doc-code link graph: inline `[[<id>]]` refs, `// @pan:` source anchors, `require-code-mention` contracts (ADR-0027, v3.8.0+) |
664
664
  | `/pan:phase-tests [N]` | Generate tests for a completed phase based on UAT criteria |
665
665
  | `/pan:milestone-cleanup` | Archive accumulated phase directories from completed milestones |
@@ -469,6 +469,13 @@ User interaction (runtimes without a native question tool):
469
469
  }
470
470
 
471
471
  /** Claude command → runtime-neutral SKILL.md (ADR-0028 Phase 1) */
472
+ /**
473
+ * `compatibility` value for emitted unified skills — the spec's optional field
474
+ * for environment requirements (max 500 chars). Kept short and factual: these
475
+ * are the two things a host cannot infer and that every PAN skill depends on.
476
+ */
477
+ const SKILL_COMPATIBILITY = 'Requires Node.js (skills invoke the bundled pan-tools CLI) and a project with a .planning/ directory, created by /pan-new-project or /pan-map-codebase.';
478
+
472
479
  function convertClaudeCommandToUnifiedSkill(content, skillName) {
473
480
  // Normalize command mentions to the readable /pan-<name> form; the adapter
474
481
  // header tells each runtime to map that onto its own invocation syntax.
@@ -483,7 +490,14 @@ function convertClaudeCommandToUnifiedSkill(content, skillName) {
483
490
  description = toSingleLine(description);
484
491
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
485
492
  const adapter = getUnifiedSkillAdapterHeader(skillName);
486
- return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
493
+ // `compatibility` is the spec's optional field for stating environment
494
+ // requirements, and PAN has real ones: the skill bodies shell out to
495
+ // `pan-tools` (Node) and every workflow reads/writes `.planning/`. Declaring
496
+ // them beats the alternative, which is a host discovering it mid-run.
497
+ // Deliberately NOT emitting `allowed-tools`: it is marked experimental in the
498
+ // spec, and ADR-0028's frontmatter rule is that anything unverified stays out
499
+ // until a live per-runtime check confirms no parser rejects it.
500
+ return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\ncompatibility: ${yamlQuote(SKILL_COMPATIBILITY)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
487
501
  }
488
502
 
489
503
  /** Generate Copilot CLI skill adapter header */
@@ -869,6 +883,146 @@ const HOOK_EVENT_MAP = Object.freeze({
869
883
  opencode: null,
870
884
  });
871
885
 
886
+ // ─── MCP server registration (2026-08) ──────────────────────────────────────
887
+
888
+ /**
889
+ * Cross-runtime MCP registration map. Every path and shape below was verified
890
+ * against primary docs on 2026-08-12 — do NOT edit from memory, and re-verify
891
+ * before trusting: this table's ancestor (`HOOK_EVENT_MAP`) exists because PAN
892
+ * shipped two DEAD config paths that had been written from secondary sources.
893
+ *
894
+ * `register: false` means PAN deliberately does not write the file and prints a
895
+ * copy-pasteable snippet instead. That is a risk decision, not an omission:
896
+ * - **codex** — MCP lives in `config.toml`, which PAN does not touch anywhere
897
+ * else and cannot merge non-destructively without a TOML parser (PAN is
898
+ * zero-dep and only ever *generates* TOML). Hand-merging a user's config
899
+ * risks their settings for no gain over a printed snippet.
900
+ * - **claude global** — user scope is `~/.claude.json`, a large file keyed by
901
+ * every project path the user has opened. PAN writes the project-scoped
902
+ * `.mcp.json` instead, which is the documented shareable surface.
903
+ *
904
+ * Shape notes that differ per runtime and are easy to get wrong:
905
+ * - claude/copilot/gemini use `mcpServers`; **opencode uses `mcp`**.
906
+ * - claude/copilot/gemini take `command` + `args[]`; **opencode takes a single
907
+ * `command` ARRAY** ([cmd, ...args]) and names its env block `environment`.
908
+ * - copilot/opencode want `type: "local"`; claude/gemini infer stdio from
909
+ * `command` and are not given a `type` here.
910
+ */
911
+ const MCP_REGISTRATION = Object.freeze({
912
+ claude: Object.freeze({
913
+ register: true, key: 'mcpServers', localPath: '.mcp.json', globalPath: null,
914
+ why: 'Project-scoped .mcp.json at the repo root (code.claude.com/docs/en/mcp). Needs one interactive approval; workspace trust gates it.',
915
+ }),
916
+ copilot: Object.freeze({
917
+ // PATHS HERE ARE RELATIVE TO THE RUNTIME'S CONFIG DIR, not the repo root.
918
+ // Copilot's config dir already *is* `.github/`, so this is `mcp.json` — it
919
+ // resolves to `.github/mcp.json` on disk. Writing `.github/mcp.json` here
920
+ // produced `.github/.github/mcp.json`, a file Copilot never reads; the
921
+ // doubling is pinned by a test because it installs and verifies "cleanly".
922
+ register: true, key: 'mcpServers', localPath: 'mcp.json', globalPath: 'mcp-config.json',
923
+ why: 'docs.github.com add-mcp-servers: project-level .github/mcp.json (also .mcp.json up-tree), user-level ~/.copilot/mcp-config.json.',
924
+ }),
925
+ gemini: Object.freeze({
926
+ register: true, key: 'mcpServers', localPath: 'settings.json', globalPath: 'settings.json',
927
+ why: 'Gemini reads mcpServers from .gemini/settings.json (workspace) or ~/.gemini/settings.json (user) — the same file PAN already writes hooks into.',
928
+ }),
929
+ opencode: Object.freeze({
930
+ register: true, key: 'mcp', localPath: 'opencode.json', globalPath: 'opencode.json',
931
+ why: 'opencode.ai/docs: opencode.json `mcp` block, type "local", command as one array, env block named `environment`. PAN already writes this file.',
932
+ }),
933
+ codex: Object.freeze({
934
+ // Config-dir-relative like the others (resolves to `.codex/config.toml`).
935
+ // These are documentation-only while register is false — but they follow the
936
+ // convention anyway, so that flipping register:true later cannot inherit the
937
+ // path-doubling bug Copilot's row shipped with.
938
+ register: false, key: 'mcp_servers', localPath: 'config.toml', globalPath: 'config.toml',
939
+ why: 'TOML-only surface ([mcp_servers.NAME] in config.toml, verified learn.chatgpt.com). PAN has no TOML merge and is zero-dep; a printed snippet is safer than hand-editing a user config.',
940
+ }),
941
+ });
942
+
943
+ /**
944
+ * Build one PAN MCP server entry in the shape a given runtime expects.
945
+ *
946
+ * @param {string} runtime - claude | copilot | gemini | opencode | codex
947
+ * @param {string} serverPath - absolute path to pan-wizard-core/mcp/server.cjs
948
+ * @param {string} panToolsPath - absolute path to pan-wizard-core/bin/pan-tools.cjs
949
+ * @param {string} [projectRoot] - value for PAN_PROJECT_ROOT; omitted when falsy
950
+ * @returns {object} the entry (NOT wrapped in its container key)
951
+ */
952
+ function buildMcpServerEntry(runtime, serverPath, panToolsPath, projectRoot) {
953
+ const env = { PAN_TOOLS_PATH: panToolsPath };
954
+ if (projectRoot) env.PAN_PROJECT_ROOT = projectRoot;
955
+
956
+ if (runtime === 'opencode') {
957
+ // Single command ARRAY + `environment` — opencode's shape is the outlier.
958
+ return { type: 'local', command: ['node', serverPath], enabled: true, environment: env };
959
+ }
960
+ const entry = { command: 'node', args: [serverPath], env };
961
+ // Copilot's documented example carries an explicit local type; Claude and
962
+ // Gemini infer stdio from `command`, and Claude's `type` vocabulary does not
963
+ // include "local", so it must NOT be added there.
964
+ if (runtime === 'copilot') entry.type = 'local';
965
+ return entry;
966
+ }
967
+
968
+ /**
969
+ * Merge PAN's MCP server into an existing config object, non-destructively.
970
+ * Foreign servers are preserved; the PAN entry is replaced wholesale so a
971
+ * reinstall is idempotent and a path change takes effect.
972
+ *
973
+ * @param {object|null} existing - parsed config, or null when absent/unusable
974
+ * @param {string} runtime
975
+ * @param {object} entry - from buildMcpServerEntry
976
+ * @param {string} [serverName='pan'] - registration key
977
+ * @returns {object} merged config to serialize
978
+ */
979
+ function mergeMcpRegistration(existing, runtime, entry, serverName = 'pan') {
980
+ const spec = MCP_REGISTRATION[runtime];
981
+ if (!spec) throw new Error(`mergeMcpRegistration: unknown runtime "${runtime}"`);
982
+ const config = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
983
+ const key = spec.key;
984
+ if (!config[key] || typeof config[key] !== 'object' || Array.isArray(config[key])) config[key] = {};
985
+ config[key][serverName] = entry;
986
+ return config;
987
+ }
988
+
989
+ /**
990
+ * Remove PAN's MCP server from a config object, preserving foreign entries.
991
+ * Empties the container key when PAN was its only member, so uninstall does not
992
+ * leave `{"mcpServers":{}}` behind.
993
+ *
994
+ * @returns {{config: object, removed: boolean}}
995
+ */
996
+ function stripMcpRegistration(existing, runtime, serverName = 'pan') {
997
+ const spec = MCP_REGISTRATION[runtime];
998
+ if (!spec) throw new Error(`stripMcpRegistration: unknown runtime "${runtime}"`);
999
+ const config = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
1000
+ const bag = config[spec.key];
1001
+ if (!bag || typeof bag !== 'object' || !(serverName in bag)) return { config, removed: false };
1002
+ delete bag[serverName];
1003
+ if (Object.keys(bag).length === 0) delete config[spec.key];
1004
+ return { config, removed: true };
1005
+ }
1006
+
1007
+ /**
1008
+ * The copy-pasteable TOML a user adds by hand for Codex (register: false).
1009
+ * Emitting a snippet rather than merging is the deliberate choice recorded in
1010
+ * MCP_REGISTRATION — keep this in the shape verified at learn.chatgpt.com
1011
+ * (`[mcp_servers.NAME]` with a nested `[mcp_servers.NAME.env]` table).
1012
+ */
1013
+ function buildCodexMcpSnippet(serverPath, panToolsPath, projectRoot, serverName = 'pan') {
1014
+ const lines = [
1015
+ `[mcp_servers.${serverName}]`,
1016
+ 'command = "node"',
1017
+ `args = [${JSON.stringify(serverPath)}]`,
1018
+ '',
1019
+ `[mcp_servers.${serverName}.env]`,
1020
+ `PAN_TOOLS_PATH = ${JSON.stringify(panToolsPath)}`,
1021
+ ];
1022
+ if (projectRoot) lines.push(`PAN_PROJECT_ROOT = ${JSON.stringify(projectRoot)}`);
1023
+ return lines.join('\n');
1024
+ }
1025
+
872
1026
  /**
873
1027
  * Merge PAN hook registrations into a `.codex/hooks.json` config.
874
1028
  *
@@ -1301,6 +1455,127 @@ function buildPluginHooksConfig() {
1301
1455
  };
1302
1456
  }
1303
1457
 
1458
+ /**
1459
+ * Build the plugin-only self-test command that answers PAN's one gated question:
1460
+ * does `${CLAUDE_PLUGIN_ROOT}` expand inside plugin COMMAND MARKDOWN? It is
1461
+ * documented as substituted in hook and MCP configs; content is unverified, and
1462
+ * that is what has kept `dist/pan-wizard-plugin/` from being published.
1463
+ *
1464
+ * Emitted ONLY into the plugin build, never into `commands/pan/`, so the shipped
1465
+ * command set is unchanged and no install gains a diagnostic.
1466
+ *
1467
+ * THE PROBE MUST SEPARATE TWO THINGS that a naive test conflates. If the body
1468
+ * simply ran `node "${CLAUDE_PLUGIN_ROOT}/…"` and it worked, that proves nothing
1469
+ * about markdown: the shell would expand `${CLAUDE_PLUGIN_ROOT}` on its own if the
1470
+ * variable happens to be exported into the tool environment. So probe 1 asks for
1471
+ * the RAW CHARACTERS with no shell involved, probe 2 checks the environment
1472
+ * separately, and the verdict table maps the pair onto what PAN may rely on.
1473
+ *
1474
+ * @param {string} placeholder - the literal PAN rewrites content to, injected
1475
+ * rather than hardcoded so this file stays the single source of that string.
1476
+ * @returns {string} markdown for `commands/pan-plugin-selftest.md` in the plugin
1477
+ */
1478
+ function buildPluginSelfTestCommand(placeholder = '${CLAUDE_PLUGIN_ROOT}') {
1479
+ // Sentinels the agent quotes between. Deliberately ugly so they cannot occur
1480
+ // naturally in surrounding prose or be mistaken for instructions.
1481
+ const OPEN = 'PAN_PROBE_BEGIN>>>';
1482
+ const CLOSE = '<<<PAN_PROBE_END';
1483
+ return `---
1484
+ description: Diagnose whether the plugin-root placeholder expands in plugin command markdown
1485
+ ---
1486
+
1487
+ # PAN plugin self-test
1488
+
1489
+ Answer three questions and print the verdict table. **Do not fix anything.** This
1490
+ command is a measurement; a "fail" here is the result, not a problem to repair.
1491
+
1492
+ ## Probe 1 — textual substitution in markdown (the question that matters)
1493
+
1494
+ Between the sentinels below sits one token. Report **the exact characters you see
1495
+ there, verbatim**. Do not run a shell. Do not resolve, expand, guess at, or tidy
1496
+ the value — if it looks like a placeholder, say so and quote it literally; if it
1497
+ looks like an absolute path, quote that path.
1498
+
1499
+ ${OPEN}${placeholder}${CLOSE}
1500
+
1501
+ Record it as \`probe1\`.
1502
+
1503
+ ## Probe 2 — the environment variable, measured separately
1504
+
1505
+ Run exactly this and record stdout as \`probe2\` (empty output is a valid, expected result):
1506
+
1507
+ \`\`\`bash
1508
+ node -e "process.stdout.write(process.env.CLAUDE_PLUGIN_ROOT || '')"
1509
+ \`\`\`
1510
+
1511
+ ## Probe 3 — does the engine actually resolve through the placeholder path
1512
+
1513
+ Run this and record whether it prints JSON or errors, as \`probe3\`:
1514
+
1515
+ \`\`\`bash
1516
+ node "${placeholder}/pan-wizard-core/bin/pan-tools.cjs" --help
1517
+ \`\`\`
1518
+
1519
+ ## Verdict
1520
+
1521
+ Print this table, filled in:
1522
+
1523
+ | probe | result |
1524
+ |---|---|
1525
+ | 1 — markdown substitution | \`probe1\` verbatim |
1526
+ | 2 — env var | \`probe2\` or "(empty)" |
1527
+ | 3 — engine through placeholder | ok / failed, with the error's first line |
1528
+
1529
+ Then state which case holds:
1530
+
1531
+ - **case A — markdown IS substituted** (probe 1 returned an absolute path). Plugin
1532
+ content may reference the plugin root directly, and PAN's existing content
1533
+ rewrite is correct as it stands. This unblocks marketplace publishing.
1534
+ - **case B — markdown is NOT substituted, but the env var is set** (probe 1
1535
+ returned the literal token, probe 2 non-empty). Content must not rely on textual
1536
+ substitution; a *shell* command inside content would still work, because the
1537
+ shell expands the variable. Anything read as a path by something other than a
1538
+ shell — an \`@\` file import, for instance — would break.
1539
+ **Note:** on the one environment measured so far (Claude Code 2.1.233, Windows)
1540
+ probe 2 came back EMPTY, so this case did not occur and its shell-expansion
1541
+ premise is unverified. If you land here, confirm the variable really is visible
1542
+ to the Bash tool before relying on it — otherwise you are actually in case C.
1543
+ - **case C — neither** (probe 1 literal, probe 2 empty). Plugin content cannot
1544
+ address the plugin root at all. PAN would need content that resolves paths at
1545
+ runtime instead, and marketplace publishing stays gated.
1546
+
1547
+ Finish with the case letter on its own line, exactly like \`VERDICT: case A\`,
1548
+ so the result is greppable out of the transcript.
1549
+ `;
1550
+ }
1551
+
1552
+ /**
1553
+ * Build the plugin's MCP registration (`.mcp.json` at the plugin root).
1554
+ *
1555
+ * Plugins may declare MCP servers in a plugin-root `.mcp.json`, and unlike
1556
+ * command markdown — where `${CLAUDE_PLUGIN_ROOT}` expansion is unverified and
1557
+ * is why marketplace publishing is still gated — hook and MCP *configs* are the
1558
+ * documented place the variable is substituted. So the same form
1559
+ * `buildPluginHooksConfig()` relies on is correct here.
1560
+ *
1561
+ * No `env` block: a plugin serves whatever project the session is in, so pinning
1562
+ * PAN_PROJECT_ROOT would be wrong, and the server resolves its engine from its
1563
+ * own location (see `defaultPanToolsPath`) with `cwd` falling back to the
1564
+ * process cwd. Nothing to configure per install.
1565
+ *
1566
+ * @returns {Object} a `.mcp.json` object for the plugin root
1567
+ */
1568
+ function buildPluginMcpConfig() {
1569
+ return {
1570
+ mcpServers: {
1571
+ pan: {
1572
+ command: 'node',
1573
+ args: ['${CLAUDE_PLUGIN_ROOT}/pan-wizard-core/mcp/server.cjs'],
1574
+ },
1575
+ },
1576
+ };
1577
+ }
1578
+
1304
1579
  // ─── Native Claude Code workflows (2026-06) ─────────────────────────────────
1305
1580
  //
1306
1581
  // Claude Code discovers deterministic orchestration scripts in
@@ -1510,10 +1785,17 @@ module.exports = {
1510
1785
  buildCopilotHooksConfig,
1511
1786
  HOOK_EVENT_MAP,
1512
1787
  mergeCodexHooksConfig,
1788
+ MCP_REGISTRATION,
1789
+ buildMcpServerEntry,
1790
+ mergeMcpRegistration,
1791
+ stripMcpRegistration,
1792
+ buildCodexMcpSnippet,
1513
1793
  removeCodexPanHooks,
1514
1794
  buildNativeWorkflowScripts,
1515
1795
  buildPluginManifest,
1516
1796
  buildPluginHooksConfig,
1797
+ buildPluginMcpConfig,
1798
+ buildPluginSelfTestCommand,
1517
1799
  // Install verification (v3.7.10)
1518
1800
  verifyInstall,
1519
1801
  // AGENTS.md universal rules layer (ADR-0028 Phase 3)
package/bin/install.js CHANGED
@@ -1611,6 +1611,39 @@ function uninstall(isGlobal, runtime = 'claude') {
1611
1611
  }
1612
1612
  }
1613
1613
 
1614
+ // 7b. Strip the MCP registration. Foreign servers and every other key in the
1615
+ // file are preserved; a file PAN created and that holds nothing else is
1616
+ // removed rather than left as `{}`. An unparseable config is left ALONE —
1617
+ // never rewrite JSON we could not read (the readSettings null-vs-{} contract).
1618
+ {
1619
+ const mcpPath = mcpConfigPathFor(runtime, isGlobal, targetDir);
1620
+ if (mcpPath) {
1621
+ const existing = readSettings(mcpPath);
1622
+ if (existing === null) {
1623
+ try {
1624
+ if (fs.existsSync(mcpPath)) {
1625
+ console.log(` ${yellow}✗${reset} ${displayPath(mcpPath)} is not valid JSON — left untouched (remove the "pan" server by hand)`);
1626
+ }
1627
+ } catch { /* stat failed — nothing to report */ }
1628
+ } else {
1629
+ try {
1630
+ const { config, removed } = lib.stripMcpRegistration(existing, runtime);
1631
+ if (removed) {
1632
+ if (Object.keys(config).length === 0) {
1633
+ fs.unlinkSync(mcpPath);
1634
+ console.log(` ${green}✓${reset} Removed ${displayPath(mcpPath)} (only the PAN server remained)`);
1635
+ } else {
1636
+ fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n');
1637
+ console.log(` ${green}✓${reset} Removed PAN MCP server from ${displayPath(mcpPath)}`);
1638
+ }
1639
+ }
1640
+ } catch (e) {
1641
+ console.error(` ${yellow}✗${reset} Failed to strip MCP registration from ${displayPath(mcpPath)}: ${e.message}`);
1642
+ }
1643
+ }
1644
+ }
1645
+ }
1646
+
1614
1647
  // 8. Clean up empty PAN directories
1615
1648
  const dirsToClean = [
1616
1649
  path.join(targetDir, 'agents'),
@@ -2017,6 +2050,96 @@ function reportLocalPatches(configDir, runtime = 'claude') {
2017
2050
  return meta.files || [];
2018
2051
  }
2019
2052
 
2053
+ /**
2054
+ * Resolve where a runtime's MCP registration file lives, per MCP_REGISTRATION.
2055
+ *
2056
+ * Returns null when this runtime/scope combination is deliberately not written
2057
+ * (see the table's `register` flag and the claude-global note). `targetDir` is
2058
+ * the runtime's config dir (.claude/, .github/, .gemini/, .opencode/ …).
2059
+ *
2060
+ * The important asymmetry: Claude's project surface is `.mcp.json` at the REPO
2061
+ * ROOT, not inside `.claude/`. Every other runtime keeps its MCP config inside
2062
+ * its own config dir, so only Claude escapes targetDir.
2063
+ */
2064
+ function mcpConfigPathFor(runtime, isGlobal, targetDir) {
2065
+ const spec = lib.MCP_REGISTRATION[runtime];
2066
+ if (!spec || !spec.register) return null;
2067
+ if (runtime === 'claude') {
2068
+ // No global path by design — ~/.claude.json is a per-project-keyed user file.
2069
+ return isGlobal ? null : path.join(process.cwd(), spec.localPath);
2070
+ }
2071
+ const rel = isGlobal ? spec.globalPath : spec.localPath;
2072
+ if (!rel) return null;
2073
+ return path.join(targetDir, rel);
2074
+ }
2075
+
2076
+ /**
2077
+ * Register PAN's MCP bridge for one runtime, non-destructively.
2078
+ *
2079
+ * Reuses readSettings() so an existing-but-unusable config is distinguished from
2080
+ * an absent one (null vs {}) — the distinction that exists because an earlier
2081
+ * installer destroyed unparseable settings files. An unusable file is left
2082
+ * ALONE and reported; PAN never rewrites JSON it could not parse.
2083
+ *
2084
+ * Codex gets a printed TOML snippet rather than a write (MCP_REGISTRATION.codex
2085
+ * records why). Gemini/OpenCode share a settings file PAN already writes, so the
2086
+ * merge preserves everything else in it.
2087
+ */
2088
+ function registerMcpServer(runtime, isGlobal, targetDir) {
2089
+ const spec = lib.MCP_REGISTRATION[runtime];
2090
+ if (!spec) return;
2091
+
2092
+ // Absolute paths into the core copy this install just wrote.
2093
+ const coreDir = path.join(targetDir, 'pan-wizard-core');
2094
+ const serverPath = path.join(coreDir, 'mcp', 'server.cjs');
2095
+ const enginePath = path.join(coreDir, 'bin', 'pan-tools.cjs');
2096
+ // A global install serves many projects, so it must NOT pin PAN_PROJECT_ROOT;
2097
+ // the server falls back to process.cwd() per project. Local installs pin it.
2098
+ const projectRoot = isGlobal ? null : process.cwd();
2099
+
2100
+ if (!fs.existsSync(serverPath)) {
2101
+ pushInstallWarning('mcpRegister', serverPath, new Error('MCP server missing; skipped registration'));
2102
+ return;
2103
+ }
2104
+
2105
+ if (!spec.register) {
2106
+ const snippet = lib.buildCodexMcpSnippet(serverPath, enginePath, projectRoot);
2107
+ const where = isGlobal ? '~/.codex/config.toml' : '.codex/config.toml';
2108
+ console.log(` ${cyan}i${reset} MCP: add PAN to ${where} by hand (TOML is not safely mergeable):`);
2109
+ console.log(snippet.split('\n').map((l) => ` ${dim}${l}${reset}`).join('\n'));
2110
+ return;
2111
+ }
2112
+
2113
+ const configPath = mcpConfigPathFor(runtime, isGlobal, targetDir);
2114
+ if (!configPath) {
2115
+ if (runtime === 'claude' && isGlobal) {
2116
+ console.log(` ${cyan}i${reset} MCP: run ${cyan}claude mcp add pan --scope user -- node "${serverPath}"${reset} to register globally`);
2117
+ }
2118
+ return;
2119
+ }
2120
+
2121
+ const existing = readSettings(configPath);
2122
+ if (existing === null) {
2123
+ // Exists but unparseable — do not touch it.
2124
+ pushInstallWarning('mcpRegister', configPath, new Error('config exists but is not valid JSON; left untouched'));
2125
+ console.log(` ${yellow}✗${reset} MCP: ${displayPath(configPath)} is not valid JSON — left untouched, register PAN by hand`);
2126
+ return;
2127
+ }
2128
+
2129
+ try {
2130
+ const entry = lib.buildMcpServerEntry(runtime, serverPath, enginePath, projectRoot);
2131
+ const merged = lib.mergeMcpRegistration(existing, runtime, entry);
2132
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
2133
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n');
2134
+ console.log(` ${green}✓${reset} Registered MCP server (${displayPath(configPath)})`);
2135
+ if (runtime === 'claude') {
2136
+ console.log(` ${dim}Claude prompts once to approve a project-scoped server.${reset}`);
2137
+ }
2138
+ } catch (e) {
2139
+ pushInstallWarning('mcpRegister', configPath, e);
2140
+ }
2141
+ }
2142
+
2020
2143
  function install(isGlobal, runtime = 'claude') {
2021
2144
  const isOpencode = runtime === 'opencode';
2022
2145
  const isGemini = runtime === 'gemini';
@@ -2286,6 +2409,10 @@ function install(isGlobal, runtime = 'claude') {
2286
2409
  failures.push('pan-wizard-core');
2287
2410
  }
2288
2411
 
2412
+ // Register the MCP bridge now that pan-wizard-core (which contains mcp/) has
2413
+ // landed — the registration points at files inside it, so it must come after.
2414
+ registerMcpServer(runtime, isGlobal, targetDir);
2415
+
2289
2416
  // Copy agents to agents directory
2290
2417
  try {
2291
2418
  const agentsSrc = path.join(src, 'agents');
@@ -2,7 +2,7 @@
2
2
  name: pan:hygiene
3
3
  group: System
4
4
  description: Scan the project for PAN version drift and stale artifacts (legacy filenames, memory bloat, poisoned ledgers, trace debris) and apply safe cleanups
5
- argument-hint: "[--apply] [--trace-age-days N]"
5
+ argument-hint: "[--apply] [--trace-age-days N] [--all-tracks] [--track <name>]"
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Bash
@@ -17,12 +17,18 @@ Keep a PAN-managed project aligned with the latest PAN version and free of accum
17
17
  ## 1. Scan
18
18
 
19
19
  ```bash
20
- SCAN=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene scan)
20
+ SCAN=$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene scan --all-tracks)
21
21
  ```
22
22
 
23
- Parse JSON: `findings[]` (`check`, `severity`, `path`, `detail`, `fixable`), `installs[]`, `latest_version`, `summary`.
23
+ Pass `--all-tracks` by default: a project may hold several planning trees (`.planning/` plus `.planning/tracks/<name>/`), and scanning only the root tree reports "clean" for debris sitting in a sibling track. Use `--track <name>` to scan one specific tree instead.
24
24
 
25
- Display the findings grouped by severity (critical → warn → info). If `summary.total` is 0: report "Project is clean and aligned" and stop.
25
+ Parse JSON: `findings[]` (`check`, `severity`, `path`, `detail`, `fixable`, `track`), `installs[]`, `latest_version`, `roots_scanned[]`, `summary` (including `summary.by_track`).
26
+
27
+ Display the findings grouped by severity (critical → warn → info), labelling each with its `track` when more than one tree was scanned.
28
+
29
+ **Always state which trees were scanned** — read `roots_scanned[]` and name them. If `summary.total` is 0, report "Project is clean and aligned" *together with* the list of trees that were read. A clean verdict without its scope is what let stale debris hide in an unscanned track; never report one without the other.
30
+
31
+ If any root has `planning_root_exists: false`, say so plainly — that is a mistyped `--track`, not a clean tree.
26
32
 
27
33
  ## 2. Version drift (manual remediation)
28
34
 
@@ -41,7 +47,7 @@ Hygiene never runs the installer itself.
41
47
  **Without `--apply` in $ARGUMENTS:** run the dry-run and present what WOULD change:
42
48
 
43
49
  ```bash
44
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean
50
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --all-tracks
45
51
  ```
46
52
 
47
53
  Then ask the user (AskUserQuestion, header "Apply fixes", options: "Apply safe fixes" / "Skip") unless running headless — in auto/headless contexts, report the dry-run only and stop.
@@ -49,14 +55,14 @@ Then ask the user (AskUserQuestion, header "Apply fixes", options: "Apply safe f
49
55
  **With `--apply` (or after user confirmation):**
50
56
 
51
57
  ```bash
52
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --apply
58
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs hygiene clean --all-tracks --apply
53
59
  ```
54
60
 
55
- Safe fixes are: lowercase renames of legacy planning filenames, deletion of aged .tmp orphans, memory-log compaction, poisoned-ledger quarantine (rename in place — never deleted), and pruning of trace sessions past retention (newest 5 always kept). Pass through `--trace-age-days N` if provided.
61
+ Safe fixes are: lowercase renames of legacy planning filenames, deletion of aged .tmp orphans, memory-log compaction, poisoned-ledger quarantine (rename in place — never deleted), pruning of trace sessions **and optimization reports** past retention (newest 5 of each always kept), and **state.md compaction** (`compact-state`) — settled history is archived to `state-history.md` so it stops being re-read into every agent call. Nothing is deleted by any of these: the ledger is renamed, and state history is written to the archive before state.md is rewritten. Each fix is applied inside its own tree's scope, so a track's debris is cleaned in that track. Pass through `--trace-age-days N` and any `--track <name>` if provided.
56
62
 
57
63
  ## 4. Report
58
64
 
59
- Summarize: fixes executed / failed / left manual, plus the installer command if version drift remains. Recommend re-running `/pan:hygiene` after the installer to confirm alignment.
65
+ Summarize: the trees scanned, fixes executed / failed / left manual (attributed per track), plus the installer command if version drift remains. If a `cache-context` finding appeared, state the per-call token cost it represents — that block is re-read on **every** agent call, so it is the project's largest recurring expense. Recommend re-running `/pan:hygiene` after the installer to confirm alignment.
60
66
 
61
67
  </process>
62
68
 
@@ -2,7 +2,7 @@
2
2
  name: pan:milestone-audit
3
3
  group: Milestone
4
4
  description: Audit milestone completion against original intent before archiving
5
- argument-hint: "[version]"
5
+ argument-hint: "[version] [--track <name>] [--all-tracks]"
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Glob
@@ -26,9 +26,11 @@ Version: $ARGUMENTS (optional — defaults to current milestone)
26
26
 
27
27
  Core planning files are resolved in-workflow (`init milestone-op`) and loaded only as needed.
28
28
 
29
- **Completed Work:**
30
- Glob: .planning/phases/*/*-summary.md
31
- Glob: .planning/phases/*/*-verification.md
29
+ **Planning tree:** the project may hold several. `init milestone-op` reports the `planning_root` it resolved; use that root for every glob below rather than assuming `.planning/`. Pass `--track <name>` to audit a specific tree, or run `init milestone-op --all-tracks` first to see every tree's milestone state.
30
+
31
+ **Completed Work:** (relative to the resolved `planning_root`)
32
+ Glob: {planning_root}/phases/*/*-summary.md
33
+ Glob: {planning_root}/phases/*/*-verification.md
32
34
  </context>
33
35
 
34
36
  <citation_requirement>
@@ -57,4 +59,8 @@ Do not trust summary files at face value. If a verification.md says "all tests p
57
59
  <process>
58
60
  Execute the audit-milestone workflow from @~/.claude/pan-wizard-core/workflows/milestone-audit.md end-to-end.
59
61
  Preserve all workflow gates (scope determination, verification reading, integration check, requirements coverage, routing).
62
+
63
+ Two gates are non-negotiable because they guard against auditing the wrong thing:
64
+ - **State the resolved `planning_root` in the report.** An audit that does not name the tree it read cannot be checked.
65
+ - **Stop if `milestone_ambiguous` is true.** More than one milestone marked current is a roadmap defect for the owner to fix; auditing one of them silently is how a report ends up describing a milestone that does not exist.
60
66
  </process>