pan-wizard 3.15.1 → 3.17.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
@@ -553,7 +553,7 @@ You're never locked in. The system adapts.
553
553
 
554
554
  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
555
 
556
- > For the full comparison across 8 tools and 15+ dimensions, see [COMPARISON.md](docs/COMPARISON.md).
556
+ > For the full comparison across the major AI coding tools and many dimensions, see [COMPARISON.md](docs/COMPARISON.md).
557
557
 
558
558
  ---
559
559
 
@@ -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.17.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"
@@ -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:**