pan-wizard 3.27.0 → 3.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -48
- package/agents/pan-previewer.md +1 -1
- package/bin/install-lib.cjs +580 -18
- package/bin/install.js +25 -44
- package/commands/pan/army.md +1 -1
- package/commands/pan/cost.md +14 -2
- package/commands/pan/preview.md +2 -2
- package/hooks/dist/pan-check-update.js +4 -0
- package/hooks/dist/pan-cost-logger.js +322 -43
- package/hooks/dist/pan-trace-logger.js +275 -32
- package/package.json +8 -2
- package/pan-wizard-core/bin/lib/commands.cjs +3 -1
- package/pan-wizard-core/bin/lib/constants.cjs +39 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +80 -0
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +511 -0
- package/pan-wizard-core/bin/lib/cost.cjs +174 -18
- package/pan-wizard-core/bin/lib/foreign-planning.cjs +56 -0
- package/pan-wizard-core/bin/lib/git.cjs +5 -1
- package/pan-wizard-core/bin/lib/hud.cjs +5 -3
- package/pan-wizard-core/bin/lib/hygiene.cjs +52 -24
- package/pan-wizard-core/bin/lib/init.cjs +8 -0
- package/pan-wizard-core/bin/lib/memory.cjs +14 -8
- package/pan-wizard-core/bin/lib/optimize.cjs +78 -2
- package/pan-wizard-core/bin/lib/utils.cjs +22 -0
- package/pan-wizard-core/bin/lib/verify.cjs +46 -12
- package/pan-wizard-core/bin/pan-tools.cjs +8 -1
- package/pan-wizard-core/mcp/server.cjs +92 -8
- package/pan-wizard-core/mcp/tool-registry.cjs +50 -3
- package/pan-wizard-core/references/model-profiles.md +2 -2
- package/pan-wizard-core/workflows/health.md +2 -0
- package/pan-zcode/README.md +1 -1
- package/scripts/build-agent-plugin.js +220 -0
- package/scripts/build-plugin.js +48 -3
- package/scripts/coverage-gate.cjs +257 -0
- package/scripts/generate-skills-docs.py +1 -1
- package/scripts/install-git-hooks.js +5 -0
- package/scripts/mutation-probe.cjs +272 -0
- package/scripts/release-check.js +80 -13
- package/scripts/test-quality-lint.cjs +240 -0
- package/scripts/test-surface.cjs +335 -0
package/bin/install-lib.cjs
CHANGED
|
@@ -449,7 +449,7 @@ function convertClaudeCommandToCodexSkill(content, skillName) {
|
|
|
449
449
|
* tree is read by every runtime, so invocation, delegation, and interaction
|
|
450
450
|
* guidance are phrased in terms of "your runtime's native mechanism".
|
|
451
451
|
*/
|
|
452
|
-
function getUnifiedSkillAdapterHeader(skillName) {
|
|
452
|
+
function getUnifiedSkillAdapterHeader(skillName, note) {
|
|
453
453
|
return `<pan_skill_adapter>
|
|
454
454
|
PAN unified skill (Agent Skills standard, shared .agents/skills/ tree):
|
|
455
455
|
- This skill is invoked through your runtime's skill mechanism — slash command (\`/${skillName}\`), mention (\`$${skillName}\`), or skill picker.
|
|
@@ -465,7 +465,7 @@ User interaction (runtimes without a native question tool):
|
|
|
465
465
|
- Ask one question at a time; show numbered options; mark the recommended option with **(recommended)**.
|
|
466
466
|
- Accept numbers ("1"), labels, or free-text descriptions as valid answers.
|
|
467
467
|
- Native interaction tools (e.g. AskUserQuestion blocks), where supported by your runtime, take precedence over this fallback.
|
|
468
|
-
</pan_skill_adapter>`;
|
|
468
|
+
${note ? `\n${note}\n` : ''}</pan_skill_adapter>`;
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
/** Claude command → runtime-neutral SKILL.md (ADR-0028 Phase 1) */
|
|
@@ -476,7 +476,7 @@ User interaction (runtimes without a native question tool):
|
|
|
476
476
|
*/
|
|
477
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
478
|
|
|
479
|
-
function convertClaudeCommandToUnifiedSkill(content, skillName) {
|
|
479
|
+
function convertClaudeCommandToUnifiedSkill(content, skillName, opts = {}) {
|
|
480
480
|
// Normalize command mentions to the readable /pan-<name> form; the adapter
|
|
481
481
|
// header tells each runtime to map that onto its own invocation syntax.
|
|
482
482
|
let converted = convertSlashCommandsToCopilotSkillMentions(content);
|
|
@@ -489,7 +489,10 @@ function convertClaudeCommandToUnifiedSkill(content, skillName) {
|
|
|
489
489
|
}
|
|
490
490
|
description = toSingleLine(description);
|
|
491
491
|
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
492
|
-
|
|
492
|
+
// `opts.adapterNote` (Agent Plugins bundle, ADR-0045) appends a consumer-
|
|
493
|
+
// specific paragraph inside the adapter block; absent, the header is
|
|
494
|
+
// byte-identical to what every install has shipped since ADR-0028.
|
|
495
|
+
const adapter = getUnifiedSkillAdapterHeader(skillName, opts.adapterNote);
|
|
493
496
|
// `compatibility` is the spec's optional field for stating environment
|
|
494
497
|
// requirements, and PAN has real ones: the skill bodies shell out to
|
|
495
498
|
// `pan-tools` (Node) and every workflow reads/writes `.planning/`. Declaring
|
|
@@ -500,6 +503,202 @@ function convertClaudeCommandToUnifiedSkill(content, skillName) {
|
|
|
500
503
|
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()}`;
|
|
501
504
|
}
|
|
502
505
|
|
|
506
|
+
// ─── Unified-skill content rewrites (extracted from bin/install.js, 2026-09) ──
|
|
507
|
+
//
|
|
508
|
+
// The installer's --unified-skills path and the Agent Plugins bundle builder
|
|
509
|
+
// (ADR-0045) need the SAME rewrite of a Claude-flavoured PAN document — the
|
|
510
|
+
// rule from ADR-0028 is one converter, several call sites, never a second copy.
|
|
511
|
+
// Each function below reproduces its installer sequence exactly, in order; the
|
|
512
|
+
// installer now calls these, and `tests/unified-skills-install.test.cjs` pins
|
|
513
|
+
// the output it has always produced.
|
|
514
|
+
//
|
|
515
|
+
// Options (all strings):
|
|
516
|
+
// corePrefix where `pan-wizard-core/` lives for the consumer, with a
|
|
517
|
+
// trailing slash — `./.agents/` (local unified install),
|
|
518
|
+
// `<abs>/.agents/` (global), `{{PAN_PLUGIN_ROOT}}/` (bundle)
|
|
519
|
+
// pathPrefix replacement for a residual `~/.claude/` reference
|
|
520
|
+
// projectDirPrefix replacement for a residual `./.claude/` reference —
|
|
521
|
+
// `./<runtime dir>/` on an install, the root token in a bundle
|
|
522
|
+
// attribution processAttribution() setting: null remove, undefined keep,
|
|
523
|
+
// string replace
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Rewrite a PAN command document's paths for a unified-skills consumer. Does
|
|
527
|
+
* NOT convert it to SKILL.md form — call convertClaudeCommandToUnifiedSkill()
|
|
528
|
+
* on the result, exactly as the installer does.
|
|
529
|
+
*/
|
|
530
|
+
function rewriteUnifiedSkillCommandContent(content, { corePrefix, pathPrefix, projectDirPrefix, attribution }) {
|
|
531
|
+
// Core + agent-definition references → the shared copies (specific, before
|
|
532
|
+
// the generic rewrites); everything else .claude-scoped → the consumer. Agent
|
|
533
|
+
// refs point at the canonical reference copies shipped with the shared core —
|
|
534
|
+
// a runtime's own agents dir may carry a different format (Codex TOML,
|
|
535
|
+
// Copilot .agent.md).
|
|
536
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
537
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
538
|
+
content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
539
|
+
content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
540
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
541
|
+
content = content.replace(/\.\/\.claude\//g, projectDirPrefix);
|
|
542
|
+
// Not every runtime puts a `pan-tools` bin on PATH — invoke via node.
|
|
543
|
+
const panToolsPath = `${corePrefix}pan-wizard-core/bin/pan-tools.cjs`;
|
|
544
|
+
content = content.replace(/\bpan-tools\b(?=\s+[a-z])/g, `node ${panToolsPath}`);
|
|
545
|
+
return processAttribution(content, attribution);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Rewrite a markdown file inside a shared copy of pan-wizard-core (workflows,
|
|
550
|
+
* templates, references, learnings) for a unified-skills consumer.
|
|
551
|
+
*/
|
|
552
|
+
function rewriteSharedCoreMarkdown(content, { corePrefix, pathPrefix, projectDirPrefix, attribution }) {
|
|
553
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
554
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
555
|
+
// Agent-definition refs → the canonical reference copies in the shared core
|
|
556
|
+
// (runtime agents dirs carry runtime-specific formats).
|
|
557
|
+
content = content.replace(/~\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
558
|
+
content = content.replace(/\.\/\.claude\/agents\//g, `${corePrefix}pan-wizard-core/agents/`);
|
|
559
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
560
|
+
content = content.replace(/\.\/\.claude\//g, projectDirPrefix);
|
|
561
|
+
content = processAttribution(content, attribution);
|
|
562
|
+
return convertSlashCommandsToCopilotSkillMentions(content);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Rewrite an agent definition for the canonical reference copy that ships
|
|
567
|
+
* under `<shared core>/agents/` — reading material for agents, not a runtime
|
|
568
|
+
* registration (ADR-0028 agent-ref canonicalization).
|
|
569
|
+
*/
|
|
570
|
+
function rewriteAgentReferenceCopy(content, corePrefix) {
|
|
571
|
+
content = content.replace(/~\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
572
|
+
content = content.replace(/\.\/\.claude\/pan-wizard-core\//g, `${corePrefix}pan-wizard-core/`);
|
|
573
|
+
return convertSlashCommandsToCopilotSkillMentions(content);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Drop internal-scoped topics from a parsed learnings/index.json and recompute
|
|
578
|
+
* its totals exactly (each topic carries its own size fields). Pure: returns
|
|
579
|
+
* the rewritten object, or null when there was nothing internal to drop or
|
|
580
|
+
* the input is not an index. The installer and the bundle builders share it so
|
|
581
|
+
* a shipped index never lists files the package deliberately withholds.
|
|
582
|
+
*/
|
|
583
|
+
function stripInternalLearningsTopics(parsed) {
|
|
584
|
+
if (!parsed || !Array.isArray(parsed.topics)) return null;
|
|
585
|
+
const kept = parsed.topics.filter(t => t && t.scope !== 'internal');
|
|
586
|
+
if (kept.length === parsed.topics.length) return null;
|
|
587
|
+
const out = { ...parsed, topics: kept };
|
|
588
|
+
if (parsed.totals && typeof parsed.totals === 'object') {
|
|
589
|
+
out.totals = {
|
|
590
|
+
...parsed.totals,
|
|
591
|
+
topics: kept.length,
|
|
592
|
+
patterns: kept.reduce((n, t) => n + (Array.isArray(t.patterns) ? t.patterns.length : 0), 0),
|
|
593
|
+
size_bytes: kept.reduce((n, t) => n + (t.size_bytes || 0), 0),
|
|
594
|
+
size_tokens_est: kept.reduce((n, t) => n + (t.size_tokens_est || 0), 0),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ─── Agent Plugins bundle (ADR-0045, 2026-09) ────────────────────────────────
|
|
601
|
+
//
|
|
602
|
+
// A vendor-neutral package: `plugin.json` + `skills/` + `mcp.json` at the root,
|
|
603
|
+
// loaded natively by Copilot CLI / VS Code, Codex, Cursor and Kiro. Every
|
|
604
|
+
// constant here is quoted from the pinned schemas in tests/fixtures/agent-plugins/
|
|
605
|
+
// (read from agent-plugins.org on 2026-09-10) — the manifest schema is CLOSED,
|
|
606
|
+
// so an unlisted key is a fatal plugin rejection, not a warning.
|
|
607
|
+
|
|
608
|
+
const AGENT_PLUGINS_VERSION = '1.0.0';
|
|
609
|
+
const AGENT_PLUGIN_MANIFEST_SCHEMA = `https://agent-plugins.org/schemas/${AGENT_PLUGINS_VERSION}/plugin.schema.json`;
|
|
610
|
+
const AGENT_PLUGIN_MCP_SCHEMA = `https://agent-plugins.org/schemas/${AGENT_PLUGINS_VERSION}/mcp.schema.json`;
|
|
611
|
+
|
|
612
|
+
// Path token inside bundled skill and core markdown. Agent Plugins expands
|
|
613
|
+
// `${PLUGIN_ROOT}` ONLY in mcp.json fields, and Claude's `${CLAUDE_PLUGIN_ROOT}`
|
|
614
|
+
// substitution in content is Claude-specific — so bundle content carries PAN's
|
|
615
|
+
// own token, in the style of `{{PAN_ARGS}}`, and the adapter note defines it.
|
|
616
|
+
const AGENT_PLUGIN_ROOT_TOKEN = '{{PAN_PLUGIN_ROOT}}';
|
|
617
|
+
// A few PAN documents refer to the RUNTIME's own configuration directories —
|
|
618
|
+
// its `settings.json`, its `commands/`, PAN's update-check cache, the local
|
|
619
|
+
// patches dir. The installer maps those to the installing runtime (`~/.codex/`,
|
|
620
|
+
// `./.gemini/`, …); a bundle is built for no runtime in particular, so it
|
|
621
|
+
// carries two more tokens the adapter note defines: the user-level and the
|
|
622
|
+
// project-level runtime directory.
|
|
623
|
+
const AGENT_PLUGIN_RUNTIME_HOME_TOKEN = '{{PAN_RUNTIME_HOME}}';
|
|
624
|
+
const AGENT_PLUGIN_RUNTIME_DIR_TOKEN = '{{PAN_RUNTIME_DIR}}';
|
|
625
|
+
|
|
626
|
+
/** Agent Plugins `plugin.json` — closed schema; mirrors package.json like the Claude manifest. */
|
|
627
|
+
function buildAgentPluginManifest(pkg) {
|
|
628
|
+
return {
|
|
629
|
+
$schema: AGENT_PLUGIN_MANIFEST_SCHEMA,
|
|
630
|
+
name: 'pan-wizard',
|
|
631
|
+
version: pkg.version,
|
|
632
|
+
description: pkg.description || 'Structured, phase-based planning and execution for AI coding agents.',
|
|
633
|
+
author: { name: 'PAN Wizard contributors', url: 'https://github.com/oharms/PanWizard' },
|
|
634
|
+
homepage: 'https://github.com/oharms/PanWizard',
|
|
635
|
+
repository: 'https://github.com/oharms/PanWizard',
|
|
636
|
+
license: pkg.license || 'MIT',
|
|
637
|
+
keywords: ['planning', 'workflow', 'agents', 'phases'],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Agent Plugins `mcp.json` declaring the bundled bridge. `command` must be a
|
|
643
|
+
* single executable token with NO placeholder (spec), so the server is launched
|
|
644
|
+
* as `node` with the `${PLUGIN_ROOT}`-anchored script in `args`, where expansion
|
|
645
|
+
* is defined. No `env`: a plugin serves whatever project the session is in, and
|
|
646
|
+
* `env` may not name PLUGIN_ROOT/PLUGIN_DATA anyway. The default working
|
|
647
|
+
* directory for a stdio server is the PLUGIN ROOT (spec) — which is why the
|
|
648
|
+
* bridge must take the project root per call (ADR-0045 D6, plan item 4g).
|
|
649
|
+
*/
|
|
650
|
+
function buildAgentPluginMcpConfig() {
|
|
651
|
+
return {
|
|
652
|
+
$schema: AGENT_PLUGIN_MCP_SCHEMA,
|
|
653
|
+
mcpServers: {
|
|
654
|
+
pan: {
|
|
655
|
+
type: 'stdio',
|
|
656
|
+
command: 'node',
|
|
657
|
+
args: ['${PLUGIN_ROOT}/pan-wizard-core/mcp/server.cjs'],
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Copilot vendor-directory hooks for an Agent Plugins bundle —
|
|
665
|
+
* `com.github.copilot/hooks/hooks.json` (ADR-0045 D5).
|
|
666
|
+
*
|
|
667
|
+
* Shape from code.visualstudio.com/docs/agent-customization/agent-plugins (read
|
|
668
|
+
* 2026-09-10): the FLAT plugin format — PascalCase lifecycle events, each an
|
|
669
|
+
* array of `{ type: 'command', command }` — with `${CLAUDE_PLUGIN_ROOT}` expanded
|
|
670
|
+
* to the plugin root at runtime and also exported to the hook process. That is
|
|
671
|
+
* VS-Code-verified. Copilot CLI's own hooks how-to describes WORKSPACE hooks
|
|
672
|
+
* (camelCase events, `bash`/`powershell` keys) and does not cover plugins, so a
|
|
673
|
+
* live `copilot plugin install` is the gate before relying on this shape there.
|
|
674
|
+
* The observers-vs-monitor split mirrors the Codex builder: no async flag exists
|
|
675
|
+
* in this format, so nothing is marked.
|
|
676
|
+
*
|
|
677
|
+
* @param {{updateCheckCommand?:string, contextMonitorCommand?:string, costLoggerCommand?:string, traceLoggerCommand?:string}} commands
|
|
678
|
+
*/
|
|
679
|
+
function buildCopilotPluginHooksConfig(commands) {
|
|
680
|
+
const { updateCheckCommand, contextMonitorCommand, costLoggerCommand, traceLoggerCommand } = commands || {};
|
|
681
|
+
const hooks = {};
|
|
682
|
+
if (updateCheckCommand) hooks.SessionStart = [{ type: 'command', command: updateCheckCommand }];
|
|
683
|
+
if (contextMonitorCommand) hooks.PostToolUse = [{ type: 'command', command: contextMonitorCommand }];
|
|
684
|
+
const subagentStop = [];
|
|
685
|
+
if (costLoggerCommand) subagentStop.push({ type: 'command', command: costLoggerCommand });
|
|
686
|
+
if (traceLoggerCommand) subagentStop.push({ type: 'command', command: traceLoggerCommand });
|
|
687
|
+
if (subagentStop.length > 0) hooks.SubagentStop = subagentStop;
|
|
688
|
+
return { hooks };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Copilot's reverse-domain extension namespace — the top-level directory its plugin components live in. */
|
|
692
|
+
const COPILOT_PLUGIN_NAMESPACE = 'com.github.copilot';
|
|
693
|
+
|
|
694
|
+
/** The adapter paragraph appended to every bundled skill (ADR-0045 D3). */
|
|
695
|
+
function agentPluginSkillAdapterNote() {
|
|
696
|
+
return `Plugin bundle (Agent Plugins format):
|
|
697
|
+
- \`${AGENT_PLUGIN_ROOT_TOKEN}\` in this skill is the directory that holds this plugin's \`plugin.json\` — two levels above this SKILL.md. Your runtime reports this skill's file location when it loads it; derive the root from that path and substitute it wherever \`${AGENT_PLUGIN_ROOT_TOKEN}\` appears before running a command.
|
|
698
|
+
- Prefer the \`pan\` MCP server's tools when your runtime has connected this plugin's \`mcp.json\`, and pass the project's absolute path as each tool's \`cwd\` argument — the server is started in the plugin's directory, which is never the project. Otherwise run \`node ${AGENT_PLUGIN_ROOT_TOKEN}/pan-wizard-core/bin/pan-tools.cjs <verb>\` from the project root.
|
|
699
|
+
- \`${AGENT_PLUGIN_RUNTIME_HOME_TOKEN}\` is your runtime's user-level configuration directory (for example \`~/.claude\`, \`~/.codex\`, \`~/.gemini\`, \`~/.config/opencode\`, \`~/.copilot\`) and \`${AGENT_PLUGIN_RUNTIME_DIR_TOKEN}\` its project-level directory (\`.claude\`, \`.codex\`, \`.gemini\`, \`.opencode\`, \`.github\`). Substitute the one that applies to the runtime you are.`;
|
|
700
|
+
}
|
|
701
|
+
|
|
503
702
|
/** Generate Copilot CLI skill adapter header */
|
|
504
703
|
function getCopilotSkillAdapterHeader(skillName) {
|
|
505
704
|
const invocation = `/pan-${skillName.replace(/^pan-/, '')}`;
|
|
@@ -540,7 +739,18 @@ function convertClaudeCommandToCopilotSkill(content, skillName) {
|
|
|
540
739
|
}
|
|
541
740
|
|
|
542
741
|
/** Claude agent → Copilot .agent.md */
|
|
543
|
-
|
|
742
|
+
/**
|
|
743
|
+
* @param {string} content - Claude agent markdown
|
|
744
|
+
* @param {object} [opts]
|
|
745
|
+
* @param {Record<string,string[]>} [opts.modelLists] - Copilot CLI (>= 1.0.83) accepts a
|
|
746
|
+
* `model:` LIST tried in order plus `model-policy`. When a PAN agent pins `model:
|
|
747
|
+
* <alias>` and this map has an entry for the alias, the Copilot agent gets that list
|
|
748
|
+
* and `model-policy: prefer` (degrade gracefully; `required` would refuse to run).
|
|
749
|
+
* NOT wired into the installer yet: the Copilot model ids must be verified on a live
|
|
750
|
+
* CLI first (ADR-0028's rule; harness/scenarios/live-gate-copilot.json carries the
|
|
751
|
+
* probe). Reality check RC15 / plan item R13, 2026-09-10.
|
|
752
|
+
*/
|
|
753
|
+
function convertClaudeToCopilotAgent(content, opts = {}) {
|
|
544
754
|
const converted = convertClaudeToCopilotMarkdown(content);
|
|
545
755
|
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
546
756
|
let name = '';
|
|
@@ -589,7 +799,17 @@ function convertClaudeToCopilotAgent(content) {
|
|
|
589
799
|
const toolsYaml = copilotTools.length > 0
|
|
590
800
|
? `\ntools:\n${copilotTools.map(t => ` - ${yamlQuote(t)}`).join('\n')}`
|
|
591
801
|
: '';
|
|
592
|
-
|
|
802
|
+
// R13: optional model fallback list for agents that pin a model alias.
|
|
803
|
+
let modelYaml = '';
|
|
804
|
+
const lists = opts && opts.modelLists;
|
|
805
|
+
if (lists && frontmatter) {
|
|
806
|
+
const pinned = extractFrontmatterField(frontmatter, 'model');
|
|
807
|
+
const list = pinned && Array.isArray(lists[pinned]) ? lists[pinned].filter(Boolean) : null;
|
|
808
|
+
if (list && list.length) {
|
|
809
|
+
modelYaml = `\nmodel:\n${list.map(m => ` - ${yamlQuote(m)}`).join('\n')}\nmodel-policy: prefer`;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(description)}${toolsYaml}${modelYaml}\n---\n${body}`;
|
|
593
813
|
}
|
|
594
814
|
|
|
595
815
|
// ─── Attribution Processing ─────────────────────────────────────────────────
|
|
@@ -928,7 +1148,7 @@ const MCP_REGISTRATION = Object.freeze({
|
|
|
928
1148
|
}),
|
|
929
1149
|
opencode: Object.freeze({
|
|
930
1150
|
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.',
|
|
1151
|
+
why: 'opencode.ai/docs/mcp-servers: opencode.json `mcp` block, type "local", command as one array, env block named `environment`. PAN already writes this file. LOCATION: the docs page (opencode.ai/docs/config) lists only a repo-root opencode.json; the .opencode/opencode.json PAN writes for local installs is read by the loader SOURCE — packages/opencode/src/config/config.ts, the branch for directories ending in .opencode reads opencode.json and opencode.jsonc (read 2026-09-10). Live but undocumented: re-check the loader on OpenCode upgrades (harness/scenarios/live-gate-opencode.json asks the CLI).',
|
|
932
1152
|
}),
|
|
933
1153
|
codex: Object.freeze({
|
|
934
1154
|
// Config-dir-relative like the others (resolves to `.codex/config.toml`).
|
|
@@ -1041,21 +1261,46 @@ function mergeCodexHooksConfig(existing, commands) {
|
|
|
1041
1261
|
const config = (existing && typeof existing === 'object') ? existing : {};
|
|
1042
1262
|
if (!config.hooks || typeof config.hooks !== 'object') config.hooks = {};
|
|
1043
1263
|
|
|
1264
|
+
// The fourth column is Codex's `async` flag (command handlers, Codex CLI
|
|
1265
|
+
// 0.148+, changelog 2026-08-17): an async handler runs off the agent's critical
|
|
1266
|
+
// path and CANNOT block, approve, deny, or inject — its output is deferred to
|
|
1267
|
+
// the next turn. So it is right for pure observers and wrong for anything the
|
|
1268
|
+
// model must read now:
|
|
1269
|
+
// - cost-logger / trace-logger append ledger rows and print nothing → async.
|
|
1270
|
+
// - check-update spawns a detached child and prints nothing → async.
|
|
1271
|
+
// - context-monitor returns `additionalContext` the model must see THIS
|
|
1272
|
+
// turn → stays synchronous.
|
|
1273
|
+
// Codex-only: Claude Code and Copilot hook schemas were not checked for an
|
|
1274
|
+
// equivalent flag (plan item 2 gate) — do not copy this column into their
|
|
1275
|
+
// builders without reading their docs first.
|
|
1044
1276
|
const wanted = [
|
|
1045
|
-
['SessionStart', updateCheckCommand, 'pan-check-update'],
|
|
1046
|
-
['PostToolUse', contextMonitorCommand, 'pan-context-monitor'],
|
|
1047
|
-
['SubagentStop', costLoggerCommand, 'pan-cost-logger'],
|
|
1048
|
-
['SubagentStop', traceLoggerCommand, 'pan-trace-logger'],
|
|
1277
|
+
['SessionStart', updateCheckCommand, 'pan-check-update', true],
|
|
1278
|
+
['PostToolUse', contextMonitorCommand, 'pan-context-monitor', false],
|
|
1279
|
+
['SubagentStop', costLoggerCommand, 'pan-cost-logger', true],
|
|
1280
|
+
['SubagentStop', traceLoggerCommand, 'pan-trace-logger', true],
|
|
1049
1281
|
];
|
|
1050
1282
|
|
|
1051
|
-
for (const [event, command, marker] of wanted) {
|
|
1283
|
+
for (const [event, command, marker, async] of wanted) {
|
|
1052
1284
|
if (!command) continue;
|
|
1053
1285
|
if (!Array.isArray(config.hooks[event])) config.hooks[event] = [];
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1286
|
+
let existingHandler = null;
|
|
1287
|
+
for (const group of config.hooks[event]) {
|
|
1288
|
+
if (!Array.isArray(group.hooks)) continue;
|
|
1289
|
+
existingHandler = group.hooks.find(h => h && h.command && h.command.includes(marker)) || null;
|
|
1290
|
+
if (existingHandler) break;
|
|
1291
|
+
}
|
|
1292
|
+
if (existingHandler) {
|
|
1293
|
+
// Upgrade path: a hooks.json written before the async column keeps its
|
|
1294
|
+
// handler (and any command edits) but must pick up the flag — and lose it
|
|
1295
|
+
// if the column ever says synchronous. Otherwise an install upgraded from
|
|
1296
|
+
// 3.27 would run the observers on the critical path forever.
|
|
1297
|
+
if (async) existingHandler.async = true;
|
|
1298
|
+
else delete existingHandler.async;
|
|
1299
|
+
continue;
|
|
1058
1300
|
}
|
|
1301
|
+
const handler = { type: 'command', command };
|
|
1302
|
+
if (async) handler.async = true;
|
|
1303
|
+
config.hooks[event].push({ hooks: [handler] });
|
|
1059
1304
|
}
|
|
1060
1305
|
return config;
|
|
1061
1306
|
}
|
|
@@ -1475,7 +1720,7 @@ function buildPluginHooksConfig() {
|
|
|
1475
1720
|
* rather than hardcoded so this file stays the single source of that string.
|
|
1476
1721
|
* @returns {string} markdown for `commands/pan-plugin-selftest.md` in the plugin
|
|
1477
1722
|
*/
|
|
1478
|
-
function buildPluginSelfTestCommand(placeholder = '${CLAUDE_PLUGIN_ROOT}') {
|
|
1723
|
+
function buildPluginSelfTestCommand(placeholder = '${CLAUDE_PLUGIN_ROOT}', pluginName = 'pan-wizard') {
|
|
1479
1724
|
// Sentinels the agent quotes between. Deliberately ugly so they cannot occur
|
|
1480
1725
|
// naturally in surrounding prose or be mistaken for instructions.
|
|
1481
1726
|
const OPEN = 'PAN_PROBE_BEGIN>>>';
|
|
@@ -1486,7 +1731,7 @@ description: Diagnose whether the plugin-root placeholder expands in plugin comm
|
|
|
1486
1731
|
|
|
1487
1732
|
# PAN plugin self-test
|
|
1488
1733
|
|
|
1489
|
-
Answer
|
|
1734
|
+
Answer four questions and print the verdict table. **Do not fix anything.** This
|
|
1490
1735
|
command is a measurement; a "fail" here is the result, not a problem to repair.
|
|
1491
1736
|
|
|
1492
1737
|
## Probe 1 — textual substitution in markdown (the question that matters)
|
|
@@ -1516,6 +1761,18 @@ Run this and record whether it prints JSON or errors, as \`probe3\`:
|
|
|
1516
1761
|
node "${placeholder}/pan-wizard-core/bin/pan-tools.cjs" --help
|
|
1517
1762
|
\`\`\`
|
|
1518
1763
|
|
|
1764
|
+
## Probe 4 — do this plugin's agents and workflows load under the scoped name
|
|
1765
|
+
|
|
1766
|
+
The plugin ships its agents under \`${pluginName}:<agent>\` and its native workflow
|
|
1767
|
+
scripts spawn them by that scoped name. Measure, do not assume:
|
|
1768
|
+
|
|
1769
|
+
- From the list of agent types available to you in this session (the Agent tool's
|
|
1770
|
+
own list — do not run a shell), record as \`probe4a\` how many names begin with
|
|
1771
|
+
\`${pluginName}:pan-\`, followed by the first three such names verbatim. If none,
|
|
1772
|
+
record any names that begin with \`pan-\` instead and say so.
|
|
1773
|
+
- Record as \`probe4b\` whether a slash command named \`/${pluginName}:pan-review-pipeline\`
|
|
1774
|
+
is available to you. If you cannot tell, write "unknown" — that is a valid answer.
|
|
1775
|
+
|
|
1519
1776
|
## Verdict
|
|
1520
1777
|
|
|
1521
1778
|
Print this table, filled in:
|
|
@@ -1525,6 +1782,8 @@ Print this table, filled in:
|
|
|
1525
1782
|
| 1 — markdown substitution | \`probe1\` verbatim |
|
|
1526
1783
|
| 2 — env var | \`probe2\` or "(empty)" |
|
|
1527
1784
|
| 3 — engine through placeholder | ok / failed, with the error's first line |
|
|
1785
|
+
| 4a — scoped agent names | count and first three names, or the bare names seen |
|
|
1786
|
+
| 4b — scoped workflow command | available / not available / unknown |
|
|
1528
1787
|
|
|
1529
1788
|
Then state which case holds:
|
|
1530
1789
|
|
|
@@ -1544,6 +1803,13 @@ Then state which case holds:
|
|
|
1544
1803
|
address the plugin root at all. PAN would need content that resolves paths at
|
|
1545
1804
|
runtime instead, and marketplace publishing stays gated.
|
|
1546
1805
|
|
|
1806
|
+
Probe 4 does not change the case letter — it measures a separate premise: the
|
|
1807
|
+
plugin's \`workflows/\` scripts were written to spawn \`${pluginName}:pan-…\` because
|
|
1808
|
+
plugin agents are documented to load under the scoped name. If \`probe4a\` reports
|
|
1809
|
+
bare \`pan-…\` names and none scoped, that premise is false on this build and the
|
|
1810
|
+
workflow scripts inside the plugin would not resolve their agents. Report it as a
|
|
1811
|
+
separate line, exactly like \`AGENT_SCOPE: scoped\` or \`AGENT_SCOPE: bare\`.
|
|
1812
|
+
|
|
1547
1813
|
Finish with the case letter on its own line, exactly like \`VERDICT: case A\`,
|
|
1548
1814
|
so the result is greppable out of the transcript.
|
|
1549
1815
|
`;
|
|
@@ -1576,6 +1842,27 @@ function buildPluginMcpConfig() {
|
|
|
1576
1842
|
};
|
|
1577
1843
|
}
|
|
1578
1844
|
|
|
1845
|
+
/**
|
|
1846
|
+
* Rewrite the `agentType` values in a native workflow script for a PLUGIN copy.
|
|
1847
|
+
*
|
|
1848
|
+
* Plugin agents load under a scoped name: `agents/pan-reviewer.md` inside a
|
|
1849
|
+
* plugin named `pan-wizard` is `pan-wizard:pan-reviewer`
|
|
1850
|
+
* (code.claude.com/docs/en/plugins-reference, read 2026-09-10). The scripts
|
|
1851
|
+
* `buildNativeWorkflowScripts()` emits are written for a loose-file install,
|
|
1852
|
+
* where the bare name resolves, so the plugin builder runs them through this
|
|
1853
|
+
* before writing `workflows/`. Idempotent: an already-scoped name (contains
|
|
1854
|
+
* ':') is left alone, and nothing outside `agentType: '…'` is touched.
|
|
1855
|
+
*
|
|
1856
|
+
* @param {string} content - emitted script source
|
|
1857
|
+
* @param {string} pluginName - the manifest `name`
|
|
1858
|
+
* @returns {string}
|
|
1859
|
+
*/
|
|
1860
|
+
function namespaceWorkflowAgentTypes(content, pluginName) {
|
|
1861
|
+
if (typeof content !== 'string' || !pluginName) return content;
|
|
1862
|
+
return content.replace(/agentType:(\s*)'([^':]+)'/g,
|
|
1863
|
+
(_m, ws, name) => `agentType:${ws}'${pluginName}:${name}'`);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1579
1866
|
// ─── Native Claude Code workflows (2026-06) ─────────────────────────────────
|
|
1580
1867
|
//
|
|
1581
1868
|
// Claude Code discovers deterministic orchestration scripts in
|
|
@@ -1599,6 +1886,7 @@ function buildNativeWorkflowScripts() {
|
|
|
1599
1886
|
{ title: 'Merge', detail: 'meta-reviewer dedupes, disputes, and issues the verdict' },
|
|
1600
1887
|
],
|
|
1601
1888
|
}
|
|
1889
|
+
// twin: commands/pan/review-deep.md
|
|
1602
1890
|
|
|
1603
1891
|
const target = (typeof args === 'string' && args.trim())
|
|
1604
1892
|
? args.trim()
|
|
@@ -1664,6 +1952,7 @@ return merged
|
|
|
1664
1952
|
{ title: 'Synthesize', detail: 'merge area maps into one codebase overview' },
|
|
1665
1953
|
],
|
|
1666
1954
|
}
|
|
1955
|
+
// twin: pan-wizard-core/workflows/map-codebase.md
|
|
1667
1956
|
|
|
1668
1957
|
phase('Scan')
|
|
1669
1958
|
const AREAS = {
|
|
@@ -1700,11 +1989,238 @@ const synthesis = await agent(
|
|
|
1700
1989
|
{ label: 'synthesize', phase: 'Synthesize' })
|
|
1701
1990
|
|
|
1702
1991
|
return { areas_mapped: maps.filter(Boolean).length, synthesis }
|
|
1992
|
+
`;
|
|
1993
|
+
|
|
1994
|
+
// ── §3.2 ports (2026-09, plan item 5a). Selection rule: a protocol becomes a
|
|
1995
|
+
// script when its control flow is knowable BEFORE the run — a fan-out whose
|
|
1996
|
+
// width the engine reports, waves that are genuinely barriers. It stays
|
|
1997
|
+
// markdown when the next step depends on reading the last result. exec-phase's
|
|
1998
|
+
// wave dispatch and diagnose-issues' per-gap fan-out qualify; verify-phase and
|
|
1999
|
+
// milestone-gaps (single-agent judgment) do not, whatever the plan first guessed.
|
|
2000
|
+
// A script also cannot pause for a human (only agent permission prompts pause a
|
|
2001
|
+
// run), so the wave script REFUSES phases with checkpoint plans instead of
|
|
2002
|
+
// pretending. Each script names its markdown twin; the drift test pins the pair.
|
|
2003
|
+
//
|
|
2004
|
+
// Paths: the engine is invoked by AGENTS (scripts have no shell), so prompts
|
|
2005
|
+
// describe where pan-tools lives rather than hard-coding one install layout.
|
|
2006
|
+
const execWaves = `export const meta = {
|
|
2007
|
+
name: 'pan-exec-waves',
|
|
2008
|
+
description: 'PAN phase execution: wave-grouped executor fan-out for a checkpoint-free phase, then verification',
|
|
2009
|
+
whenToUse: 'Deterministic version of the /pan-exec-phase wave dispatch. Pass the phase number as args. Refuses a phase that contains checkpoint plans (a workflow cannot pause for a human) — run /pan-exec-phase for those.',
|
|
2010
|
+
phases: [
|
|
2011
|
+
{ title: 'Index', detail: 'plan inventory with wave grouping, from the PAN engine' },
|
|
2012
|
+
{ title: 'Execute', detail: 'one executor per plan; waves in order, plans within a wave in parallel' },
|
|
2013
|
+
{ title: 'Verify', detail: 'the phase verifier over the completed plans' },
|
|
2014
|
+
],
|
|
2015
|
+
}
|
|
2016
|
+
// twin: pan-wizard-core/workflows/exec-phase.md
|
|
2017
|
+
|
|
2018
|
+
const PAN_TOOLS = 'PAN engine (pan-tools): node <PAN core>/bin/pan-tools.cjs — the PAN core is .claude/pan-wizard-core in a project install, ~/.claude/pan-wizard-core in a global install, or pan-wizard-core under the plugin root when PAN runs as a plugin.'
|
|
2019
|
+
const CORE_DOCS = 'PAN core documents (same core directory): workflows/execute-plan.md, templates/summary.md, references/checkpoints.md, references/tdd.md.'
|
|
2020
|
+
|
|
2021
|
+
const phaseArg = (typeof args === 'string' && args.trim())
|
|
2022
|
+
? args.trim()
|
|
2023
|
+
: (args && typeof args === 'object' && args.phase != null ? String(args.phase) : '')
|
|
2024
|
+
if (!phaseArg) return { error: 'pass the phase number as args, e.g. /pan-exec-waves 3' }
|
|
2025
|
+
|
|
2026
|
+
phase('Index')
|
|
2027
|
+
const INDEX = {
|
|
2028
|
+
type: 'object',
|
|
2029
|
+
properties: {
|
|
2030
|
+
phase_found: { type: 'boolean' },
|
|
2031
|
+
phase_number: { type: 'string' },
|
|
2032
|
+
phase_name: { type: 'string' },
|
|
2033
|
+
phase_dir: { type: 'string' },
|
|
2034
|
+
parallelization: { type: 'boolean' },
|
|
2035
|
+
has_checkpoints: { type: 'boolean' },
|
|
2036
|
+
plans: {
|
|
2037
|
+
type: 'array',
|
|
2038
|
+
items: {
|
|
2039
|
+
type: 'object',
|
|
2040
|
+
properties: {
|
|
2041
|
+
id: { type: 'string' },
|
|
2042
|
+
file: { type: 'string' },
|
|
2043
|
+
wave: { type: 'integer' },
|
|
2044
|
+
autonomous: { type: 'boolean' },
|
|
2045
|
+
has_summary: { type: 'boolean' },
|
|
2046
|
+
objective: { type: 'string' },
|
|
2047
|
+
},
|
|
2048
|
+
required: ['id', 'wave', 'autonomous', 'has_summary'],
|
|
2049
|
+
},
|
|
2050
|
+
},
|
|
2051
|
+
},
|
|
2052
|
+
required: ['phase_found', 'has_checkpoints', 'plans'],
|
|
2053
|
+
}
|
|
2054
|
+
const index = await agent(
|
|
2055
|
+
'Index phase ' + phaseArg + ' for execution using the ' + PAN_TOOLS + ' Run two verbs and merge their JSON: (1) init execute-phase ' + phaseArg + ' — take phase_found, phase_number, phase_name, phase_dir, parallelization; (2) phase-plan-index ' + phaseArg + ' — take has_checkpoints and plans[] (id, wave, autonomous, has_summary, objective; include each plan file path as file). Do not execute anything; return only the merged index.',
|
|
2056
|
+
{ label: 'index', phase: 'Index', schema: INDEX })
|
|
2057
|
+
if (!index || !index.phase_found) return { error: 'phase ' + phaseArg + ' not found' }
|
|
2058
|
+
if (index.has_checkpoints) {
|
|
2059
|
+
return { error: 'phase ' + phaseArg + ' contains checkpoint plans (autonomous: false). A workflow cannot pause for a human — run /pan-exec-phase ' + phaseArg + ' instead.', plans: index.plans.map(p => p.id) }
|
|
2060
|
+
}
|
|
2061
|
+
const pending = index.plans.filter(p => !p.has_summary)
|
|
2062
|
+
if (pending.length === 0) return { phase: phaseArg, done: true, message: 'every plan already has a summary — nothing to execute' }
|
|
2063
|
+
const waveNumbers = [...new Set(pending.map(p => p.wave))].sort((a, b) => a - b)
|
|
2064
|
+
const parallelWithinWave = index.parallelization !== false
|
|
2065
|
+
log(pending.length + ' plans across ' + waveNumbers.length + ' wave(s)' + (parallelWithinWave ? '' : ', sequential within waves'))
|
|
2066
|
+
|
|
2067
|
+
phase('Execute')
|
|
2068
|
+
const EXEC_RESULT = {
|
|
2069
|
+
type: 'object',
|
|
2070
|
+
properties: {
|
|
2071
|
+
plan_id: { type: 'string' },
|
|
2072
|
+
status: { type: 'string', enum: ['complete', 'failed', 'checkpoint'] },
|
|
2073
|
+
summary_path: { type: 'string' },
|
|
2074
|
+
commits: { type: 'integer' },
|
|
2075
|
+
self_check: { type: 'string', enum: ['passed', 'failed', 'unknown'] },
|
|
2076
|
+
notes: { type: 'string' },
|
|
2077
|
+
},
|
|
2078
|
+
required: ['plan_id', 'status', 'self_check'],
|
|
2079
|
+
}
|
|
2080
|
+
const executorPrompt = (p) =>
|
|
2081
|
+
'Execute plan ' + p.id + ' of phase ' + (index.phase_number || phaseArg) + (index.phase_name ? '-' + index.phase_name : '') + '. Commit each task atomically. Create summary.md. Update state.md and roadmap.md (via roadmap update-plan-progress).\\n\\n'
|
|
2082
|
+
+ 'Read first, in this order: ' + CORE_DOCS + '\\n\\n'
|
|
2083
|
+
+ 'Then read: ' + (p.file || (index.phase_dir + '/' + p.id)) + ' (the plan), .planning/state.md, .planning/config.json (if present), ./CLAUDE.md (if present — follow its conventions), .agents/skills/ (if present — follow relevant skills), and every .planning/memory/*.md (apply every rule without exception).\\n\\n'
|
|
2084
|
+
+ 'Report plan_id, status (complete | failed | checkpoint), summary_path, the number of commits you made, and self_check (passed if your summary carries no "Self-Check: FAILED" marker).'
|
|
2085
|
+
const executed = []
|
|
2086
|
+
let halted = null
|
|
2087
|
+
for (const w of waveNumbers) {
|
|
2088
|
+
const wavePlans = pending.filter(p => p.wave === w)
|
|
2089
|
+
log('wave ' + w + ': ' + wavePlans.map(p => p.id).join(', '))
|
|
2090
|
+
let results
|
|
2091
|
+
if (parallelWithinWave) {
|
|
2092
|
+
results = await parallel(wavePlans.map(p => () =>
|
|
2093
|
+
agent(executorPrompt(p), { agentType: 'pan-executor', label: 'exec:' + p.id, phase: 'Execute', schema: EXEC_RESULT })))
|
|
2094
|
+
} else {
|
|
2095
|
+
results = []
|
|
2096
|
+
for (const p of wavePlans) {
|
|
2097
|
+
results.push(await agent(executorPrompt(p), { agentType: 'pan-executor', label: 'exec:' + p.id, phase: 'Execute', schema: EXEC_RESULT }))
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const settled = results.filter(Boolean)
|
|
2101
|
+
executed.push(...settled)
|
|
2102
|
+
const bad = settled.filter(r => r.status !== 'complete' || r.self_check === 'failed')
|
|
2103
|
+
const dropped = wavePlans.length - settled.length
|
|
2104
|
+
if (bad.length > 0 || dropped > 0) {
|
|
2105
|
+
// Mirror exec-phase's failure handler without the question it asks: stop
|
|
2106
|
+
// before the next wave and return what happened, so a human decides.
|
|
2107
|
+
halted = { wave: w, failed: bad.map(r => r.plan_id), unanswered: dropped }
|
|
2108
|
+
break
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (halted) {
|
|
2112
|
+
return { phase: phaseArg, halted, executed, next: 'Inspect the failed plan(s), then re-run /pan-exec-waves ' + phaseArg + ' (completed plans are skipped) or fall back to /pan-exec-phase ' + phaseArg }
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
phase('Verify')
|
|
2116
|
+
const VERIFY = {
|
|
2117
|
+
type: 'object',
|
|
2118
|
+
properties: {
|
|
2119
|
+
status: { type: 'string', enum: ['passed', 'gaps_found', 'human_needed', 'failed'] },
|
|
2120
|
+
verification_path: { type: 'string' },
|
|
2121
|
+
gaps: { type: 'array', items: { type: 'string' } },
|
|
2122
|
+
summary: { type: 'string' },
|
|
2123
|
+
},
|
|
2124
|
+
required: ['status', 'summary'],
|
|
2125
|
+
}
|
|
2126
|
+
const verdict = await agent(
|
|
2127
|
+
'Verify phase ' + phaseArg + ' following the PAN verify-phase protocol (PAN core: workflows/verify-phase.md — ' + PAN_TOOLS + '). Check the phase goals against what the plans delivered, write the verification file the protocol prescribes, and report status (passed | gaps_found | human_needed | failed), the verification file path, any gaps, and a summary. Do not mark the phase complete or advance state — that decision stays with the user.',
|
|
2128
|
+
{ agentType: 'pan-verifier', label: 'verify', phase: 'Verify', schema: VERIFY })
|
|
2129
|
+
|
|
2130
|
+
return { phase: phaseArg, waves_run: waveNumbers.length, plans_complete: executed.length, verification: verdict, next: 'Review the verification, then continue with /pan-exec-phase ' + phaseArg + ' (transition) or /pan-plan-phase for the next phase' }
|
|
2131
|
+
`;
|
|
2132
|
+
|
|
2133
|
+
const diagnoseIssues = `export const meta = {
|
|
2134
|
+
name: 'pan-diagnose-issues',
|
|
2135
|
+
description: 'PAN UAT diagnosis: one debugger per failed UAT truth, in parallel, then root causes written back',
|
|
2136
|
+
whenToUse: 'Deterministic version of /pan-diagnose-issues. Pass the phase number as args. Investigates only — fixes come from /pan-plan-phase --gaps.',
|
|
2137
|
+
phases: [
|
|
2138
|
+
{ title: 'Gaps', detail: 'read the phase UAT file and list the failed truths' },
|
|
2139
|
+
{ title: 'Diagnose', detail: 'one pan-debugger per gap, in parallel, root cause only' },
|
|
2140
|
+
{ title: 'Record', detail: 'write root causes and artifacts back into the UAT gaps' },
|
|
2141
|
+
],
|
|
2142
|
+
}
|
|
2143
|
+
// twin: pan-wizard-core/workflows/diagnose-issues.md
|
|
2144
|
+
|
|
2145
|
+
const PAN_TOOLS = 'PAN engine (pan-tools): node <PAN core>/bin/pan-tools.cjs — the PAN core is .claude/pan-wizard-core in a project install, ~/.claude/pan-wizard-core in a global install, or pan-wizard-core under the plugin root when PAN runs as a plugin.'
|
|
2146
|
+
|
|
2147
|
+
const phaseArg = (typeof args === 'string' && args.trim())
|
|
2148
|
+
? args.trim()
|
|
2149
|
+
: (args && typeof args === 'object' && args.phase != null ? String(args.phase) : '')
|
|
2150
|
+
if (!phaseArg) return { error: 'pass the phase number as args, e.g. /pan-diagnose-issues 3' }
|
|
2151
|
+
|
|
2152
|
+
phase('Gaps')
|
|
2153
|
+
const GAPS = {
|
|
2154
|
+
type: 'object',
|
|
2155
|
+
properties: {
|
|
2156
|
+
phase_dir: { type: 'string' },
|
|
2157
|
+
uat_path: { type: 'string' },
|
|
2158
|
+
gaps: {
|
|
2159
|
+
type: 'array',
|
|
2160
|
+
items: {
|
|
2161
|
+
type: 'object',
|
|
2162
|
+
properties: {
|
|
2163
|
+
test_num: { type: 'integer' },
|
|
2164
|
+
truth: { type: 'string' },
|
|
2165
|
+
severity: { type: 'string' },
|
|
2166
|
+
reason: { type: 'string' },
|
|
2167
|
+
expected: { type: 'string' },
|
|
2168
|
+
},
|
|
2169
|
+
required: ['test_num', 'truth', 'severity'],
|
|
2170
|
+
},
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
required: ['uat_path', 'gaps'],
|
|
2174
|
+
}
|
|
2175
|
+
const found = await agent(
|
|
2176
|
+
'Locate phase ' + phaseArg + ' with the ' + PAN_TOOLS + ' (find-phase ' + phaseArg + ' gives the phase directory) and read its UAT file ({phase_dir}/{phase}-uat.md). List every gap in the Gaps section whose status is failed: test number, the truth that failed, severity, the reason the user reported, and the expected behaviour from the matching test. Do not investigate anything; return the list.',
|
|
2177
|
+
{ label: 'gaps', phase: 'Gaps', schema: GAPS })
|
|
2178
|
+
if (!found || !found.uat_path) return { error: 'no UAT file found for phase ' + phaseArg }
|
|
2179
|
+
const gaps = (found.gaps || []).filter(Boolean)
|
|
2180
|
+
if (gaps.length === 0) return { phase: phaseArg, uat_path: found.uat_path, gaps: 0, message: 'no failed truths to diagnose' }
|
|
2181
|
+
log(gaps.length + ' gap(s) to diagnose')
|
|
2182
|
+
|
|
2183
|
+
phase('Diagnose')
|
|
2184
|
+
const DIAGNOSIS = {
|
|
2185
|
+
type: 'object',
|
|
2186
|
+
properties: {
|
|
2187
|
+
issue_id: { type: 'string' },
|
|
2188
|
+
status: { type: 'string', enum: ['root_cause_found', 'inconclusive'] },
|
|
2189
|
+
root_cause: { type: 'string' },
|
|
2190
|
+
evidence: { type: 'array', items: { type: 'string' } },
|
|
2191
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
2192
|
+
suggested_fix: { type: 'string' },
|
|
2193
|
+
debug_path: { type: 'string' },
|
|
2194
|
+
remaining_possibilities: { type: 'array', items: { type: 'string' } },
|
|
2195
|
+
},
|
|
2196
|
+
required: ['issue_id', 'status'],
|
|
2197
|
+
}
|
|
2198
|
+
const diagnoses = await parallel(gaps.map(g => () => agent(
|
|
2199
|
+
'Debug issue UAT-' + g.test_num + ' for phase ' + phaseArg + ' — root cause ONLY, do not fix (fixes come from /pan-plan-phase --gaps).\\n\\n'
|
|
2200
|
+
+ 'Symptoms (pre-filled from UAT, treat as given): expected: ' + (g.expected || g.truth) + '. actual: ' + (g.reason || 'not recorded') + '. reproduction: test ' + g.test_num + ' in ' + found.uat_path + '. severity: ' + g.severity + '.\\n\\n'
|
|
2201
|
+
+ 'Follow the PAN debugger protocol: create the debug session file under .planning/debug/ named from the issue, investigate autonomously (read code, form hypotheses, test them), and report issue_id, status (root_cause_found | inconclusive), root_cause with evidence, files involved, a suggested fix direction, and the debug session path. If inconclusive, list the remaining possibilities. Also read ' + found.uat_path + ' and .planning/state.md for context.',
|
|
2202
|
+
{ agentType: 'pan-debugger', label: 'debug:UAT-' + g.test_num, phase: 'Diagnose', schema: DIAGNOSIS })))
|
|
2203
|
+
const results = diagnoses.filter(Boolean)
|
|
2204
|
+
log(results.filter(r => r.status === 'root_cause_found').length + ' root cause(s) found, ' + results.filter(r => r.status === 'inconclusive').length + ' inconclusive')
|
|
2205
|
+
|
|
2206
|
+
phase('Record')
|
|
2207
|
+
const RECORDED = {
|
|
2208
|
+
type: 'object',
|
|
2209
|
+
properties: { uat_path: { type: 'string' }, gaps_updated: { type: 'integer' } },
|
|
2210
|
+
required: ['uat_path', 'gaps_updated'],
|
|
2211
|
+
}
|
|
2212
|
+
const recorded = await agent(
|
|
2213
|
+
'Update the Gaps section of ' + found.uat_path + ' with these diagnoses, following the PAN diagnose-issues protocol: for each gap add root_cause, artifacts (the debug session path), the files involved, and the suggested fix direction; mark inconclusive ones as needing manual review with their remaining possibilities. Edit in place — do not rewrite unrelated content. Report the path and how many gaps you updated.\\n\\nDiagnoses:\\n' + JSON.stringify(results, null, 2),
|
|
2214
|
+
{ label: 'record', phase: 'Record', schema: RECORDED })
|
|
2215
|
+
|
|
2216
|
+
return { phase: phaseArg, uat_path: found.uat_path, gaps: gaps.length, root_causes_found: results.filter(r => r.status === 'root_cause_found').length, inconclusive: results.filter(r => r.status === 'inconclusive').length, recorded, next: 'Run /pan-plan-phase ' + phaseArg + ' --gaps to plan the fixes' }
|
|
1703
2217
|
`;
|
|
1704
2218
|
|
|
1705
2219
|
return [
|
|
1706
2220
|
{ name: 'pan-review-pipeline.js', content: reviewPipeline },
|
|
1707
2221
|
{ name: 'pan-map-codebase.js', content: mapCodebase },
|
|
2222
|
+
{ name: 'pan-exec-waves.js', content: execWaves },
|
|
2223
|
+
{ name: 'pan-diagnose-issues.js', content: diagnoseIssues },
|
|
1708
2224
|
];
|
|
1709
2225
|
}
|
|
1710
2226
|
|
|
@@ -1792,6 +2308,22 @@ module.exports = {
|
|
|
1792
2308
|
buildCodexMcpSnippet,
|
|
1793
2309
|
removeCodexPanHooks,
|
|
1794
2310
|
buildNativeWorkflowScripts,
|
|
2311
|
+
namespaceWorkflowAgentTypes,
|
|
2312
|
+
rewriteUnifiedSkillCommandContent,
|
|
2313
|
+
rewriteSharedCoreMarkdown,
|
|
2314
|
+
rewriteAgentReferenceCopy,
|
|
2315
|
+
stripInternalLearningsTopics,
|
|
2316
|
+
AGENT_PLUGINS_VERSION,
|
|
2317
|
+
AGENT_PLUGIN_MANIFEST_SCHEMA,
|
|
2318
|
+
AGENT_PLUGIN_MCP_SCHEMA,
|
|
2319
|
+
AGENT_PLUGIN_ROOT_TOKEN,
|
|
2320
|
+
AGENT_PLUGIN_RUNTIME_HOME_TOKEN,
|
|
2321
|
+
AGENT_PLUGIN_RUNTIME_DIR_TOKEN,
|
|
2322
|
+
buildAgentPluginManifest,
|
|
2323
|
+
buildAgentPluginMcpConfig,
|
|
2324
|
+
agentPluginSkillAdapterNote,
|
|
2325
|
+
buildCopilotPluginHooksConfig,
|
|
2326
|
+
COPILOT_PLUGIN_NAMESPACE,
|
|
1795
2327
|
buildPluginManifest,
|
|
1796
2328
|
buildPluginHooksConfig,
|
|
1797
2329
|
buildPluginMcpConfig,
|
|
@@ -1807,3 +2339,33 @@ module.exports = {
|
|
|
1807
2339
|
PAN_AGENTS_BEGIN,
|
|
1808
2340
|
PAN_AGENTS_END,
|
|
1809
2341
|
};
|
|
2342
|
+
|
|
2343
|
+
/**
|
|
2344
|
+
* Content digest of a directory tree: sha256 over the sorted list of
|
|
2345
|
+
* `<relative posix path>:<sha256 of bytes>` lines. Order-independent, content-
|
|
2346
|
+
* sensitive, ignores mtimes. Used by release-check Gate 8 to refuse a stale
|
|
2347
|
+
* dist/pan-agent-plugin — the Codex and Copilot marketplaces install from that path
|
|
2348
|
+
* with no rebuild-on-resolve, so a stale bundle would ship silently (reality check
|
|
2349
|
+
* RC12 / plan item R10, 2026-09-10; two fresh builds were measured byte-identical).
|
|
2350
|
+
* Pure apart from reading the tree; throws if `dir` is not a directory.
|
|
2351
|
+
*/
|
|
2352
|
+
function dirDigest(dir) {
|
|
2353
|
+
// Local requires: install-lib keeps no module-level filesystem imports (its top
|
|
2354
|
+
// level is pure); this helper is the one export that reads a tree.
|
|
2355
|
+
const fs = require('fs');
|
|
2356
|
+
const path = require('path');
|
|
2357
|
+
const crypto = require('crypto');
|
|
2358
|
+
const lines = [];
|
|
2359
|
+
const walk = (abs, rel) => {
|
|
2360
|
+
const entries = fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
2361
|
+
for (const e of entries) {
|
|
2362
|
+
const childAbs = path.join(abs, e.name);
|
|
2363
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2364
|
+
if (e.isDirectory()) walk(childAbs, childRel);
|
|
2365
|
+
else lines.push(`${childRel}:${crypto.createHash('sha256').update(fs.readFileSync(childAbs)).digest('hex')}`);
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
walk(dir, '');
|
|
2369
|
+
return crypto.createHash('sha256').update(lines.join('\n')).digest('hex');
|
|
2370
|
+
}
|
|
2371
|
+
module.exports.dirDigest = dirDigest;
|