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
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 |
|
package/bin/install-lib.cjs
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/commands/pan/hygiene.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
**
|
|
30
|
-
|
|
31
|
-
|
|
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>
|