pan-wizard 3.15.1 → 3.18.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 CHANGED
@@ -201,6 +201,29 @@ npm run test:all # All tests (unit + scenario)
201
201
 
202
202
  </details>
203
203
 
204
+ <details>
205
+ <summary><strong>Experimental: ZCode support (preview) — ZCode is beta</strong></summary>
206
+
207
+ **PAN-Z** is an experimental, separate subsystem that brings the PAN workflow to
208
+ [ZCode](https://zcode.z.ai), z.ai's GLM coding-agent harness. ZCode has no
209
+ slash-commands or hooks to host PAN directly, so PAN-Z instead exposes PAN's engine to
210
+ ZCode over **MCP**: PAN's agents become ZCode subagents, and the deterministic engine —
211
+ including a model-proof human merge gate — is reached as MCP tools.
212
+
213
+ > **ZCode is beta**, and its on-disk formats change frequently. PAN-Z is a **preview**:
214
+ > two facts (whether a subagent can call MCP tools, and whether local MCP calls are
215
+ > metered) can only be confirmed on a live ZCode install. See
216
+ > [`pan-zcode/README.md`](pan-zcode/README.md) and
217
+ > [`pan-zcode/KNOWN-BETA-RISKS.md`](pan-zcode/KNOWN-BETA-RISKS.md).
218
+
219
+ ```bash
220
+ # From an installed pan-wizard package, build the ZCode bundle into a target dir
221
+ node "$(npm root -g)/pan-wizard/pan-zcode/bin/install-zcode.js" --target ./zcode-bundle
222
+ # then finish setup inside ZCode per the generated INSTALL-ZCODE.md
223
+ ```
224
+
225
+ </details>
226
+
204
227
  ### Recommended: Skip Permissions Mode
205
228
 
206
229
  PAN is designed for frictionless automation. Run Claude Code with:
@@ -553,7 +576,7 @@ You're never locked in. The system adapts.
553
576
 
554
577
  PAN is not a replacement for your IDE or AI agent — it's the orchestration layer that handles everything *around* them: breaking work into context-safe chunks, researching before planning, verifying after execution, and maintaining state across sessions.
555
578
 
556
- > For the full comparison across 8 tools and 15+ dimensions, see [COMPARISON.md](docs/COMPARISON.md).
579
+ > For the full comparison across the major AI coding tools and many dimensions, see [COMPARISON.md](docs/COMPARISON.md).
557
580
 
558
581
  ---
559
582
 
@@ -125,9 +125,13 @@ Spawn the Quality squad on the built tree (parallel, read-only). Merge findings
125
125
  ### Phase 5 — Integrate (Release, human-gated)
126
126
  Spawn `pan-release`. It prepares the squash-merge, runs the configured `verification`, and surfaces an **always-ask** approval request. A human approves the merge to the protected branch; release then tags and records the rollback target. `--push` pushes the approved result.
127
127
 
128
+ **Phase report (opt-in build deliverable):** when `workflow.phase_reports.enabled` is `true`, generate the mission's self-contained per-phase HTML report **in the built tree, before staging the squash-merge** — `pan-tools report phase <N>` — so the report rides along in the merge as a phase deliverable. **Never run `report index` inside a squad worktree:** the timeline index is a single shared file that aggregates *all* phases, so a worktree would see only its own phase and concurrent squads would race on it. The index is a single-writer, post-merge concern (Phase 6). Never opens a browser.
129
+
128
130
  ### Phase 6 — Learn (Dreaming)
129
131
  Squad summaries return to Mission Control. Run `/pan:retro --write-memory` (and `/pan:learn` if traces exist) so recurring patterns persist into agent memory for the next mission. Strike the landed item; update loop-state. For a scheduled campaign, also `pan-tools campaign record-run --items <n> --points <p>` so the next-due time and the day's spend advance.
130
132
 
133
+ **Rebuild the timeline index (single writer).** When `workflow.phase_reports.enabled` and `workflow.phase_reports.index` are `true`, Mission Control — and *only* Mission Control, on the integration branch after the merge has landed — rebuilds the project index once against the now-merged set of phases: `pan-tools report index`, then commit it (the commit honors `commit_docs`). Doing this post-merge from the single conductor is what keeps `report-index.html` consistent while builds run in parallel worktrees.
134
+
131
135
  ---
132
136
 
133
137
  ## Scheduled, self-resuming campaigns (ADR-0034)
@@ -156,7 +156,7 @@ For each target document, extract every verifiable claim.
156
156
  | Category | Pattern | Example |
157
157
  |----------|---------|---------|
158
158
  | **Version claim** | `v1.2.3`, `version 1.2`, `Version: X` | "Currently at v3.1.0" |
159
- | **Count claim** | Number + noun | "42 test suites", "15 API endpoints" |
159
+ | **Count claim** | Number + noun | "42 database tables", "15 API endpoints" |
160
160
  | **File path claim** | Path-like strings | "`src/core/handler.ts`" |
161
161
  | **Command claim** | Code block with shell prefix or instruction | "`npm run build`" |
162
162
  | **Feature claim** | "supports X", "includes Y", "provides Z" | "Supports hot reloading" |
@@ -2,7 +2,7 @@
2
2
  name: pan:report
3
3
  group: Observability
4
4
  description: Generate a self-contained HTML report for one phase, or a project-level timeline index linking every phase report
5
- argument-hint: "phase <N> | index | all [--out <file>] [--open] [--stdout]"
5
+ argument-hint: "phase <N> | index [--bundle] | all [--out <file>] [--open] [--stdout]"
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Bash
@@ -24,7 +24,7 @@ Reports are honest by construction: the `verify reconcile` verdict is shown besi
24
24
 
25
25
  ```
26
26
  pan-tools report phase <N> [--out <file>] [--open] [--stdout]
27
- pan-tools report index [--out <file>] [--open] [--stdout]
27
+ pan-tools report index [--out <file>] [--open] [--stdout] [--bundle]
28
28
  pan-tools report all [--open]
29
29
  ```
30
30
 
@@ -37,6 +37,7 @@ pan-tools report all [--open]
37
37
  - `--out <file>` — write to a custom path instead of the default (relative paths resolve against the project root).
38
38
  - `--open` — best-effort: launch the written file in the default browser (cross-platform; silently no-ops if no opener is available, and never opens when nothing was written).
39
39
  - `--stdout` — print the HTML to stdout instead of writing a file (for `phase`/`index`).
40
+ - `--bundle` (on `index`) — emit one self-contained `.planning/report-bundle.html`: the timeline plus every phase report inlined under in-page anchors, with no links to sibling files. This is the form to email or attach — a single file that stays whole when moved off-disk.
40
41
 
41
42
  **Behaviour worth knowing:**
42
43
  - **Deterministic writes.** Re-running with unchanged phase data rewrites nothing (the only volatile value, the generated-at timestamp, is ignored when comparing) — so reports produce no git churn.
@@ -61,7 +62,7 @@ pan-tools report all [--open]
61
62
 
62
63
  **Review a phase:** run `pan-tools report phase <N> --open` to see one phase's objective, what changed, and its verification verdict at a glance.
63
64
 
64
- **Share the project:** `pan-tools report index` builds the timeline entry point — send `.planning/report-index.html` and the linked phase files, or open the index and click through.
65
+ **Share the project:** `pan-tools report index` builds the timeline entry point — send `.planning/report-index.html` and the linked phase files, or open the index and click through. To hand someone a single file instead, `pan-tools report index --bundle` inlines every phase into one `.planning/report-bundle.html` that survives being emailed or moved.
65
66
 
66
67
  **Refresh everything:** `pan-tools report all` after a milestone regenerates every report; unchanged ones are skipped, so only what actually moved is rewritten.
67
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "3.15.1",
3
+ "version": "3.18.0",
4
4
  "description": "Command a bot army for your codebase: an Opus Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
5
5
  "bin": {
6
6
  "pan-wizard": "bin/install.js"
@@ -12,7 +12,8 @@
12
12
  "agents",
13
13
  "hooks/dist",
14
14
  "scripts",
15
- "assets"
15
+ "assets",
16
+ "pan-zcode"
16
17
  ],
17
18
  "keywords": [
18
19
  "claude",
@@ -53,6 +53,8 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
53
53
  verifier: true,
54
54
  nyquist_validation: false,
55
55
  phase_record_compact: false,
56
+ // M2: opt-in per-phase HTML reports at the verify→complete gate.
57
+ phase_reports: { enabled: false, open: false, theme: 'auto', index: true },
56
58
  },
57
59
  parallelization: true,
58
60
  brave_search: hasBraveSearch,
@@ -809,11 +809,48 @@ function focusAutoUpdate(cwd, raw, getVal) {
809
809
  }, raw);
810
810
  }
811
811
 
812
+ /**
813
+ * Whether opt-in HTML phase reports are enabled for this project.
814
+ * Reads the raw config.json (loadConfig doesn't surface the workflow block);
815
+ * absent / malformed config → disabled.
816
+ */
817
+ function phaseReportsEnabled(cwd) {
818
+ try {
819
+ const raw = JSON.parse(fs.readFileSync(path.join(planningPath(cwd), 'config.json'), 'utf-8'));
820
+ return !!(raw.workflow && raw.workflow.phase_reports && raw.workflow.phase_reports.enabled);
821
+ } catch {
822
+ return false;
823
+ }
824
+ }
825
+
826
+ /**
827
+ * Regenerate every phase report + index as part of a focus-auto checkpoint,
828
+ * when the feature is enabled. Best-effort and side-effect-light: never opens a
829
+ * browser, and silently no-ops for phase-less / focus-auto-only projects. The
830
+ * fresh HTML lands in .planning/ so the checkpoint's `git add` sweeps it in.
831
+ */
832
+ function maybeRenderPhaseReports(cwd) {
833
+ if (!phaseReportsEnabled(cwd)) return;
834
+ try {
835
+ const { renderAllToDisk } = require('./phase-report.cjs');
836
+ renderAllToDisk(cwd, {});
837
+ } catch {
838
+ // Report generation must never block the checkpoint commit.
839
+ }
840
+ }
841
+
812
842
  function focusAutoCheckpointCommit(cwd, cycle, run) {
813
843
  if (!isGitRepo(cwd)) return null;
814
844
  const config = loadConfig(cwd);
815
845
  const autoCommit = config.focus && config.focus.auto_commit !== undefined ? config.focus.auto_commit : true;
816
846
  if (!autoCommit) return null;
847
+ // commit_docs is the global switch for committing planning artifacts; focus-auto
848
+ // is a planning-doc committer, so honor it. commit_docs=false → hands the .planning
849
+ // commit (and any report regeneration) back to the user.
850
+ if (config.commit_docs === false) return null;
851
+ // Enabled projects: refresh the HTML reports before staging so the committed
852
+ // .planning/ snapshot reflects this cycle.
853
+ maybeRenderPhaseReports(cwd);
817
854
  const status = execGit(cwd, ['status', '--porcelain', PLANNING_DIR + '/']);
818
855
  if (status.exitCode !== 0 || !status.stdout) return null;
819
856
  execGit(cwd, ['add', PLANNING_DIR + '/']);
@@ -44,6 +44,7 @@ const cost = require('./cost.cjs');
44
44
 
45
45
  const REPORT_SUFFIX = '-report.html';
46
46
  const INDEX_FILE = 'report-index.html';
47
+ const BUNDLE_FILE = 'report-bundle.html';
47
48
 
48
49
  // ─── small helpers ─────────────────────────────────────────────────────────────
49
50
 
@@ -548,7 +549,8 @@ function indexHero(d) {
548
549
  </section>`;
549
550
  }
550
551
 
551
- function indexTimeline(d) {
552
+ function indexTimeline(d, hrefFn) {
553
+ const href = hrefFn || ((p) => p.href);
552
554
  const rows = d.phases.map((p, i) => {
553
555
  const color = STATUS_DOT[p.status] || '#C9C0AE';
554
556
  const isLast = i === d.phases.length - 1;
@@ -560,7 +562,7 @@ function indexTimeline(d) {
560
562
  return `
561
563
  <div class="trow">
562
564
  <div class="rail"><span class="rd" style="background:${color}"></span>${isLast ? '' : `<span class="rl" style="background:${railColor}"></span>`}</div>
563
- <a class="tcard" href="${esc(p.href)}">
565
+ <a class="tcard" href="${esc(href(p))}">
564
566
  <div>
565
567
  <div class="tt">${esc(p.number)}${p.name ? ' · ' + esc(p.name) : ''}</div>
566
568
  <div class="tsub"><span class="mini">${mini}</span><span>${p.plansTotal ? p.plansDone + ' / ' + p.plansTotal + ' plans' : 'no plans'}</span>${p.has_report ? '' : '<span>· report pending</span>'}</div>
@@ -577,6 +579,33 @@ function renderIndexHtml(d) {
577
579
  return docShell(`PanWizard · ${d.project} — timeline`, body, d.generated_at);
578
580
  }
579
581
 
582
+ /**
583
+ * Bundle: the timeline index plus EVERY phase report inlined into one
584
+ * self-contained document, cross-linked by in-page #phase-NN anchors. Because
585
+ * navigation is anchors (not sibling files), the links never dangle when the
586
+ * single file is emailed or copied on its own. (indexData, phaseDatas) are pure.
587
+ */
588
+ function renderBundleHtml(indexData, phaseDatas) {
589
+ const anchorHref = (p) => '#phase-' + encodeURIComponent(String(p.number));
590
+ const sections = (phaseDatas || []).filter(Boolean).map(d => `
591
+ <section id="phase-${esc(d.phase.number)}" style="margin-top:26px;border-top:1px solid var(--border);padding-top:8px">
592
+ <div class="crumbs"><a href="#top">↑ back to timeline</a></div>
593
+ ${reportHero(d)}
594
+ ${reportMetrics(d)}
595
+ <div class="grid"><div class="gcol">${reportObjective(d)}</div><div class="gcol">${reportVerification(d)}</div></div>
596
+ ${reportChanges(d)}
597
+ ${reportGaps(d)}
598
+ </section>`).join('\n');
599
+ const body = [
600
+ '<span id="top"></span>',
601
+ indexTopBar(indexData),
602
+ indexHero(indexData),
603
+ indexTimeline(indexData, anchorHref),
604
+ sections,
605
+ ].filter(Boolean).join('\n');
606
+ return docShell(`PanWizard · ${indexData.project} — full report`, body, indexData.generated_at);
607
+ }
608
+
580
609
  // ─── side effects (the only impure layer) ────────────────────────────────────────
581
610
 
582
611
  /**
@@ -667,6 +696,19 @@ function cmdReport(cwd, opts = {}, raw) {
667
696
  if (action === 'index') {
668
697
  const data = collectIndexData(cwd, { now });
669
698
  if (!data) return error('No phases found — nothing to index (phase-less / focus-auto project). Use `pan-tools hud` instead.');
699
+ if (opts.bundle) {
700
+ const phaseDatas = data.phases.map(p => collectPhaseData(cwd, p.number, { now })).filter(Boolean);
701
+ const html = renderBundleHtml(data, phaseDatas);
702
+ if (opts.stdout) { process.stdout.write(html); return; }
703
+ const outPath = opts.out ? path.resolve(cwd, opts.out) : path.join(planningPath(cwd), BUNDLE_FILE);
704
+ const res = writeIfChanged(outPath, html);
705
+ const opened = opts.open && res.written ? openInBrowser(outPath) : false;
706
+ return output(
707
+ { action: 'bundle', phases: phaseDatas.length, path: toPosix(outPath), bytes: Buffer.byteLength(html), written: res.written, opened },
708
+ raw,
709
+ `bundle ${res.written ? 'written' : 'unchanged'}: ${toPosix(outPath)}`,
710
+ );
711
+ }
670
712
  const html = renderIndexHtml(data);
671
713
  if (opts.stdout) { process.stdout.write(html); return; }
672
714
  const outPath = opts.out ? path.resolve(cwd, opts.out) : path.join(planningPath(cwd), INDEX_FILE);
@@ -680,42 +722,62 @@ function cmdReport(cwd, opts = {}, raw) {
680
722
  }
681
723
 
682
724
  if (action === 'all') {
683
- const dirs = listPhaseDirs(cwd);
684
- if (!dirs.length) return error('No phases found — nothing to report.');
685
- const reports = [];
686
- for (const dirName of dirs) {
687
- const { number } = parsePhaseDir(dirName);
688
- const data = collectPhaseData(cwd, number, { now });
689
- if (!data) continue;
690
- const outPath = path.join(data.phase.dir, `${data.phase.number}${REPORT_SUFFIX}`);
691
- const res = writeIfChanged(outPath, renderPhaseHtml(data));
692
- reports.push({ phase: data.phase.number, path: toPosix(outPath), written: res.written });
693
- }
694
- const idx = collectIndexData(cwd, { now });
695
- let index = { path: null, written: false };
696
- if (idx) {
697
- const outPath = path.join(planningPath(cwd), INDEX_FILE);
698
- const res = writeIfChanged(outPath, renderIndexHtml(idx));
699
- index = { path: toPosix(outPath), written: res.written };
700
- }
725
+ const res = renderAllToDisk(cwd, { now });
726
+ if (!res) return error('No phases found — nothing to report.');
701
727
  return output(
702
- { action, reports, index },
728
+ { action, reports: res.reports, index: res.index },
703
729
  raw,
704
- `generated ${reports.length} phase report(s)${index.path ? ' + index' : ''}`,
730
+ `generated ${res.reports.length} phase report(s)${res.index.path ? ' + index' : ''}`,
705
731
  );
706
732
  }
707
733
 
708
734
  return error('Unknown report action. Available: phase <N>, index, all');
709
735
  }
710
736
 
737
+ /**
738
+ * Regenerate every phase report + the timeline index straight to disk.
739
+ * Programmatic sibling of `cmdReport('all')` with NO stdout side effects and
740
+ * no browser opening — used by focus-auto / army in-tree checkpoints, which
741
+ * emit their own JSON and must not have report output interleaved into it.
742
+ * @param {string} cwd
743
+ * @param {{now?:Date}} opts
744
+ * @returns {{reports:Array<{phase:string,path:string,written:boolean}>, index:{path:string|null,written:boolean}}|null}
745
+ * null when the project has no phases (phase-less / focus-auto-only).
746
+ */
747
+ function renderAllToDisk(cwd, opts = {}) {
748
+ const now = opts.now;
749
+ const dirs = listPhaseDirs(cwd);
750
+ if (!dirs.length) return null;
751
+ const reports = [];
752
+ for (const dirName of dirs) {
753
+ const { number } = parsePhaseDir(dirName);
754
+ const data = collectPhaseData(cwd, number, { now });
755
+ if (!data) continue;
756
+ const outPath = path.join(data.phase.dir, `${data.phase.number}${REPORT_SUFFIX}`);
757
+ const res = writeIfChanged(outPath, renderPhaseHtml(data));
758
+ reports.push({ phase: data.phase.number, path: toPosix(outPath), written: res.written });
759
+ }
760
+ const idx = collectIndexData(cwd, { now });
761
+ let index = { path: null, written: false };
762
+ if (idx) {
763
+ const outPath = path.join(planningPath(cwd), INDEX_FILE);
764
+ const res = writeIfChanged(outPath, renderIndexHtml(idx));
765
+ index = { path: toPosix(outPath), written: res.written };
766
+ }
767
+ return { reports, index };
768
+ }
769
+
711
770
  module.exports = {
712
771
  REPORT_SUFFIX,
713
772
  INDEX_FILE,
773
+ BUNDLE_FILE,
714
774
  collectPhaseData,
715
775
  collectIndexData,
716
776
  renderPhaseHtml,
717
777
  renderIndexHtml,
778
+ renderBundleHtml,
718
779
  cmdReport,
780
+ renderAllToDisk,
719
781
  // exported for focused unit tests
720
782
  resolvePhase,
721
783
  extractPhaseGoal,
@@ -890,6 +890,7 @@ async function main() {
890
890
  out: getArgValue(args, '--out'),
891
891
  open: args.includes('--open'),
892
892
  stdout: args.includes('--stdout'),
893
+ bundle: args.includes('--bundle'),
893
894
  }, raw);
894
895
  } else if (subcommand === 'all') {
895
896
  phaseReport.cmdReport(cwd, { action: 'all', open: args.includes('--open') }, raw);
@@ -7,7 +7,8 @@
7
7
  "verifier": true,
8
8
  "auto_advance": false,
9
9
  "nyquist_validation": false,
10
- "phase_record_compact": false
10
+ "phase_record_compact": false,
11
+ "phase_reports": { "enabled": false, "open": false, "theme": "auto", "index": true }
11
12
  },
12
13
  "planning": {
13
14
  "commit_docs": true,
@@ -602,8 +602,24 @@ The CLI handles:
602
602
 
603
603
  Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`.
604
604
 
605
+ **Phase reports (opt-in build deliverable):** when `workflow.phase_reports.enabled` is `true`, generate the self-contained per-phase HTML report — and, when `workflow.phase_reports.index` is `true`, the project timeline index — at this verify→complete gate so they ship with the phase and ride the commit below. Disabled by default; phase-less projects are skipped automatically by `report`. Never opens a browser here (that's reserved for a manual `pan-tools report --open`).
606
+
607
+ ```bash
608
+ REPORT_FILES=""
609
+ if [ "$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs config-get workflow.phase_reports.enabled 2>/dev/null || echo false)" = "true" ]; then
610
+ if node ~/.claude/pan-wizard-core/bin/pan-tools.cjs report phase "${PHASE_NUMBER}" >/dev/null 2>&1; then
611
+ REPORT_FILES="{phase_dir}/*-report.html"
612
+ if [ "$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs config-get workflow.phase_reports.index 2>/dev/null || echo true)" = "true" ]; then
613
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs report index >/dev/null 2>&1 && REPORT_FILES="$REPORT_FILES .planning/report-index.html"
614
+ fi
615
+ fi
616
+ fi
617
+ ```
618
+
619
+ The report files are appended to the completion commit's `--files` list (`$REPORT_FILES`, empty when disabled), so `cmdCommit` still honors `commit_docs` + gitignore + safety checks with no new commit logic:
620
+
605
621
  ```bash
606
- node ~/.claude/pan-wizard-core/bin/pan-tools.cjs commit "docs(phase-{X}): complete phase execution" --files .planning/roadmap.md .planning/state.md .planning/requirements.md {phase_dir}/*-verification.md
622
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs commit "docs(phase-{X}): complete phase execution" --files .planning/roadmap.md .planning/state.md .planning/requirements.md {phase_dir}/*-verification.md $REPORT_FILES
607
623
  ```
608
624
 
609
625
  **Circular optimization — finalize trace session:**
@@ -0,0 +1,36 @@
1
+ # PAN-Z — Known Beta Risks & M0 Open Questions
2
+
3
+ ZCode is a fast-moving, closed Beta: three subsystems PAN-Z touches (subagent
4
+ frontmatter, plugin/skill layout, MCP config path) changed within a single ~12-day
5
+ window on ZCode's changelog. This ledger tracks what the design deliberately does **not**
6
+ hard-depend on, and the go/no-go facts that can only be settled on a real ZCode install
7
+ (the **M0 verify spike**). Re-check these before trusting any on-disk format.
8
+
9
+ ## M0 — go/no-go (settle empirically on a real ZCode)
10
+
11
+ | # | Question | If NO → fallback (already in the design) |
12
+ |---|----------|------------------------------------------|
13
+ | 1 | Can a **subagent call MCP tools**? | Primary Agent makes every `pan-mcp` call and feeds results into subagent prompts. Determinism unaffected. |
14
+ | 2 | Are **local stdio MCP calls metered** against the MCP-Calls quota? | Read verbs are MCP *resources*, not tools; batch/cache; never make a hosted z.ai MCP load-bearing. |
15
+ | 3 | Exact **subagent frontmatter schema** / plugin layout / MCP config path? | Drive ZCode's own Import / Settings; treat `pan-zcode/lib/convert-agent.cjs` output as a labelled fallback, not a contract. |
16
+ | 4 | Any **file-based / headless** way to register an MCP server, or GUI-only? | `install-zcode.js` emits a bundle + `INSTALL-ZCODE.md`; the human finishes in ZCode's UI. |
17
+ | 5 | Any **lifecycle hook** that can hard-gate a spawn? | Enforce caps at the MCP-tool-call boundary (the orchestrator), not pre-spawn. |
18
+
19
+ ## Standing constraints (confirmed by the review)
20
+
21
+ - **The merge gate is not self-sufficient.** Under ZCode Full Access, a raw Bash
22
+ `git push` bypasses `pan_confirm_merge`. Non-bypassability rests on **server-side
23
+ branch protection**; the MCP gate is the second, in-process lock. `INSTALL-ZCODE.md`
24
+ mandates branch protection and "never Full Access during install."
25
+ - **No subagent nesting.** The army flattens to one delegation layer (PAN already caps
26
+ nesting at 2, so this is tolerable). `Task` is dropped from ported subagents.
27
+ - **User-global subagents only.** No repo-scoped rosters or per-project model profiles.
28
+ - **Genuinely lost:** scheduled self-resuming multi-day campaigns (no headless/daemon),
29
+ background execution, committable permissions, and custom slash-commands.
30
+
31
+ ## Format-drift policy
32
+
33
+ Do **not** rely on a passive "write the file and hope" strategy. All three churning
34
+ formats are GUI-owned implementation details: prefer ZCode's authoring surfaces, keep the
35
+ converter output clearly labelled best-effort, and re-verify against this ledger on every
36
+ ZCode version bump.
@@ -0,0 +1,58 @@
1
+ # PAN-Z — a ZCode-native PAN subsystem
2
+
3
+ PAN-Z gives [ZCode](https://zcode.z.ai) (z.ai's GLM-5.2 coding harness) the PAN Wizard
4
+ workflow — the multi-phase lifecycle, the bot-army with a human merge gate, deterministic
5
+ state tracking — **without** cloning PAN's slash-commands or hooks (ZCode has neither).
6
+
7
+ Instead of porting the command surface, PAN-Z **reuses PAN's engine in place** and reaches
8
+ ZCode through the one interface it speaks: **MCP**.
9
+
10
+ > Design + verified feasibility: [`docs/specs/pan_zcode_mcp_bridge_featureai.md`](../docs/specs/pan_zcode_mcp_bridge_featureai.md).
11
+ > The architecture was confirmed by a 38-agent adversarial review; four optimistic
12
+ > assumptions were refuted and their fixes folded into the design.
13
+
14
+ ## How it fits
15
+
16
+ ```
17
+ ZCode harness (GLM-5.2) primary Agent drives everything; ported subagents fan out
18
+ │ MCP · local stdio
19
+ pan-zcode/mcp (this subsystem) a thin, zero-dep bridge — verbs → MCP tools/resources
20
+ │ spawn: node pan-tools.cjs <verb> --raw --cwd <root>
21
+ pan-wizard-core (reused as-is) the deterministic engine; .planning/ stays the state store
22
+ ```
23
+
24
+ ## Status — M1–M5 built (M0 is the human verify spike)
25
+
26
+ - **M1 — bridge core.** `mcp/tool-registry.cjs` (pure verb→tool/resource map, with a hard
27
+ guardrail against exposing a force/reset/rebase/push verb) + `mcp/server.cjs` (a
28
+ **zero-dependency** JSON-RPC 2.0 stdio MCP server; reads → resources, actions → tools with
29
+ accurate hints; shell-less `execFile` spawn; `@file:` overflow protocol; protocol-version
30
+ negotiation; strict per-arg validation).
31
+ - **M2 — determinism grafts.** `mcp/merge-gate.cjs` (two-step, model-proof merge: a human-origin
32
+ env token that ignores agent-supplied approval; never force/reset/push) + `mcp/orchestrator.cjs`
33
+ (the deterministic `next-action` state machine with safety caps + regression circuit-breaker),
34
+ exposed as native MCP tools via `mcp/native-tools.cjs`.
35
+ - **M3 — content port.** `lib/convert-agent.cjs` — Claude agents → ZCode subagents (reusing the
36
+ installer's frontmatter helpers): drops `Task` (no nesting), maps PAN tiers → `inherit`,
37
+ preserves the body; plus a command → skill wrapper.
38
+ - **M4 — bundle + install.** `bin/install-zcode.js` — assembles `agents/` + `pan-mcp.json` +
39
+ manifest + `INSTALL-ZCODE.md` into a `--target` dir; refuses to write inside the source repo;
40
+ drives ZCode's own Import to finish.
41
+ - **M5 — hardening + docs.** Full test matrix + [`KNOWN-BETA-RISKS.md`](KNOWN-BETA-RISKS.md)
42
+ (the beta-churn ledger + the M0 go/no-go questions).
43
+
44
+ Tests: `tests/pan-zcode-mcp.test.cjs`, `tests/pan-zcode-orchestration.test.cjs`,
45
+ `tests/pan-zcode-install.test.cjs`.
46
+
47
+ ## Still pending — M0 (needs a real ZCode install)
48
+
49
+ Two go/no-go facts can only be settled empirically: **can a subagent call MCP tools?** and **are
50
+ local stdio MCP calls metered?** Both have folded-in fallbacks (see `KNOWN-BETA-RISKS.md`), so the
51
+ design holds either way — but confirm them before relying on the richer paths.
52
+
53
+ ## Zero dependencies
54
+
55
+ Like the rest of PAN, this subsystem ships **no runtime dependencies**. The MCP protocol is
56
+ implemented directly rather than via an SDK. If protocol drift ever makes that costly, the
57
+ escape hatch is to vendor an MCP SDK **inside this package only**, leaving `pan-wizard-core`
58
+ untouched.
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * PAN-Z M4 — assemble the ZCode bundle and emit a pull-based install.
6
+ *
7
+ * Produces, into an explicit --target directory:
8
+ * agents/*.md PAN agents converted to ZCode subagents (best-effort)
9
+ * pan-mcp.json the MCP server registration (command/args/env)
10
+ * pan-zcode-manifest.json what was generated
11
+ * INSTALL-ZCODE.md how to finish setup inside ZCode
12
+ *
13
+ * It NEVER writes to a real ~/.zcode on its own and NEVER writes inside the PAN
14
+ * source repo — ZCode's on-disk formats churn weekly, so the human finishes setup by
15
+ * driving ZCode's own "Import from Claude Code" / Settings surfaces using this bundle.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { convertAgentsDir } = require('../lib/convert-agent.cjs');
21
+
22
+ /** The MCP registration ZCode reads (Claude-compatible: command/args/env). */
23
+ function mcpServerConfig(panToolsPath, serverPath, projectRoot) {
24
+ return {
25
+ mcpServers: {
26
+ 'pan-mcp': {
27
+ command: 'node',
28
+ args: [serverPath],
29
+ env: { PAN_TOOLS_PATH: panToolsPath, PAN_PROJECT_ROOT: projectRoot || '.' },
30
+ },
31
+ },
32
+ };
33
+ }
34
+
35
+ const INSTRUCTIONS = `# Install PAN-Z into ZCode
36
+
37
+ PAN-Z bridges PAN's engine to ZCode over MCP. Finish setup inside ZCode:
38
+
39
+ 1. **Register the MCP server.** Add the contents of \`pan-mcp.json\` to ZCode's MCP
40
+ configuration (Settings → MCP), or point ZCode's MCP config at this file. This
41
+ exposes the \`pan_*\` tools (state, plan, verify, report, next-action, merge gate).
42
+
43
+ 2. **Import the subagents.** The \`agents/\` folder holds PAN agents in ZCode subagent
44
+ form. Prefer ZCode's **Import from Claude Code** / Settings → Subagents to register
45
+ them, since ZCode owns the on-disk format (it is a fast-moving Beta). These files are
46
+ a fallback, not a contract.
47
+
48
+ 3. **Protect your branches.** The merge gate (\`pan_confirm_merge\`) is one lock; the
49
+ real, non-bypassable one is **server-side branch protection** on your remote. Enable
50
+ required reviews so a raw push under Full Access cannot merge. Approve a staged merge
51
+ by setting \`PAN_MERGE_APPROVAL=<request-id>\` in the MCP server's environment.
52
+
53
+ 4. **Never run the install/import step in ZCode Full Access mode.**
54
+
55
+ Not yet resolved (the M0 verify spike, on your real ZCode): whether a subagent can call
56
+ MCP tools, and whether local stdio MCP calls are metered. Until confirmed, the primary
57
+ Agent makes every pan-mcp call.
58
+ `;
59
+
60
+ /** Real path of the nearest EXISTING ancestor of p, with the not-yet-created tail re-attached. */
61
+ function realpathNearest(p) {
62
+ const tail = [];
63
+ let cur = path.resolve(p);
64
+ for (;;) {
65
+ try {
66
+ const real = fs.realpathSync(cur);
67
+ return tail.length ? path.join(real, ...tail) : real;
68
+ } catch {
69
+ const parent = path.dirname(cur);
70
+ if (parent === cur) return path.resolve(p); // hit the root; nothing existed
71
+ tail.unshift(path.basename(cur));
72
+ cur = parent;
73
+ }
74
+ }
75
+ }
76
+
77
+ function assertNotInSourceRepo(destDir, repoRoot) {
78
+ // Resolve symlinks/junctions (a --target under a junction into the repo would else
79
+ // slip past) and case-fold on case-insensitive filesystems (NTFS/APFS) so a
80
+ // case-variant path can't bypass the guard.
81
+ const ci = process.platform === 'win32' || process.platform === 'darwin';
82
+ const norm = (p) => { const r = realpathNearest(p); return ci ? r.toLowerCase() : r; };
83
+ const d = norm(destDir);
84
+ const r = norm(repoRoot);
85
+ if (d === r || d.startsWith(r + path.sep)) {
86
+ throw new Error('Refusing to write the PAN-Z bundle inside the PAN source repo. Choose a --target outside it.');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Build the bundle.
92
+ * @param {{repoRoot:string, destDir:string, projectRoot?:string}} o
93
+ * @returns {{agents:number, destDir:string, mcp:Object, files:string[]}}
94
+ */
95
+ function buildBundle(o) {
96
+ const repoRoot = path.resolve(o.repoRoot);
97
+ const destDir = path.resolve(o.destDir);
98
+ assertNotInSourceRepo(destDir, repoRoot);
99
+
100
+ const agentsSrc = path.join(repoRoot, 'agents');
101
+ const serverPath = path.join(repoRoot, 'pan-zcode', 'mcp', 'server.cjs');
102
+ const panToolsPath = path.join(repoRoot, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
103
+
104
+ fs.mkdirSync(destDir, { recursive: true });
105
+ const agents = fs.existsSync(agentsSrc) ? convertAgentsDir(agentsSrc, path.join(destDir, 'agents')) : [];
106
+
107
+ const mcp = mcpServerConfig(panToolsPath, serverPath, o.projectRoot);
108
+ fs.writeFileSync(path.join(destDir, 'pan-mcp.json'), JSON.stringify(mcp, null, 2), 'utf8');
109
+
110
+ const manifest = {
111
+ subsystem: 'pan-zcode',
112
+ agents: agents.map((a) => a.name),
113
+ mcp_config: 'pan-mcp.json',
114
+ server: serverPath,
115
+ pan_tools: panToolsPath,
116
+ };
117
+ fs.writeFileSync(path.join(destDir, 'pan-zcode-manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
118
+ fs.writeFileSync(path.join(destDir, 'INSTALL-ZCODE.md'), INSTRUCTIONS, 'utf8');
119
+
120
+ return { agents: agents.length, destDir, mcp, files: ['agents/', 'pan-mcp.json', 'pan-zcode-manifest.json', 'INSTALL-ZCODE.md'] };
121
+ }
122
+
123
+ function getArg(args, name) {
124
+ const i = args.indexOf(name);
125
+ return i !== -1 && i + 1 < args.length ? args[i + 1] : null;
126
+ }
127
+
128
+ function main() {
129
+ const args = process.argv.slice(2);
130
+ const target = getArg(args, '--target');
131
+ if (!target) {
132
+ console.error('Usage: install-zcode.js --target <dir> [--project-root <dir>]');
133
+ console.error('Writes the PAN-Z ZCode bundle to <dir> (outside the PAN source repo).');
134
+ process.exit(2);
135
+ }
136
+ const repoRoot = path.resolve(__dirname, '..', '..');
137
+ try {
138
+ const res = buildBundle({ repoRoot, destDir: target, projectRoot: getArg(args, '--project-root') });
139
+ console.log(`pan-zcode bundle written to ${res.destDir} (${res.agents} subagents + MCP config).`);
140
+ console.log('Next: open INSTALL-ZCODE.md and finish setup inside ZCode.');
141
+ } catch (e) {
142
+ console.error(`install-zcode: ${e.message}`);
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ if (require.main === module) main();
148
+
149
+ module.exports = { buildBundle, mcpServerConfig, assertNotInSourceRepo };