pan-wizard 3.8.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +80 -9
  2. package/agents/pan-conductor.md +15 -3
  3. package/agents/pan-counterfactual.md +1 -2
  4. package/agents/pan-debugger.md +1 -2
  5. package/agents/pan-distiller.md +1 -2
  6. package/agents/pan-document_code.md +1 -0
  7. package/agents/pan-executor.md +1 -0
  8. package/agents/pan-experiment-runner.md +1 -2
  9. package/agents/pan-hardener.md +1 -2
  10. package/agents/pan-integration-checker.md +1 -2
  11. package/agents/pan-knowledge.md +1 -2
  12. package/agents/pan-meta-reviewer.md +1 -2
  13. package/agents/pan-optimizer.md +1 -0
  14. package/agents/pan-phase-researcher.md +1 -0
  15. package/agents/pan-plan-checker.md +1 -2
  16. package/agents/pan-planner.md +1 -0
  17. package/agents/pan-previewer.md +1 -2
  18. package/agents/pan-project-researcher.md +6 -0
  19. package/agents/pan-release.md +58 -0
  20. package/agents/pan-research-synthesizer.md +7 -0
  21. package/agents/pan-reviewer.md +2 -3
  22. package/agents/pan-roadmapper.md +1 -0
  23. package/agents/pan-verifier.md +1 -2
  24. package/assets/pan-avatar.png +0 -0
  25. package/assets/pan-developer.png +0 -0
  26. package/assets/pan-docs-header.png +0 -0
  27. package/assets/pan-hero.png +0 -0
  28. package/assets/pan-logo-2000-transparent.svg +11 -30
  29. package/assets/pan-logo-2000.svg +12 -43
  30. package/assets/pan-logo-lockup.svg +11 -0
  31. package/assets/pan-mark.svg +7 -0
  32. package/assets/pan-orchestration.png +0 -0
  33. package/assets/pan-readme-hero.png +0 -0
  34. package/assets/terminal.svg +39 -119
  35. package/bin/install-lib.cjs +661 -46
  36. package/bin/install.js +722 -116
  37. package/commands/pan/army.md +169 -0
  38. package/commands/pan/dashboard.md +25 -0
  39. package/commands/pan/experiment.md +2 -0
  40. package/commands/pan/focus-auto.md +32 -4
  41. package/commands/pan/hud.md +91 -0
  42. package/commands/pan/profile.md +2 -0
  43. package/hooks/dist/pan-cost-logger.js +22 -7
  44. package/package.json +5 -4
  45. package/pan-wizard-core/bin/lib/campaign.cjs +198 -0
  46. package/pan-wizard-core/bin/lib/commands-learnings.cjs +544 -0
  47. package/pan-wizard-core/bin/lib/commands.cjs +12 -523
  48. package/pan-wizard-core/bin/lib/constants.cjs +8 -0
  49. package/pan-wizard-core/bin/lib/core.cjs +80 -0
  50. package/pan-wizard-core/bin/lib/cost.cjs +62 -8
  51. package/pan-wizard-core/bin/lib/focus.cjs +13 -1
  52. package/pan-wizard-core/bin/lib/git.cjs +6 -1
  53. package/pan-wizard-core/bin/lib/hud.cjs +887 -0
  54. package/pan-wizard-core/bin/lib/lock.cjs +108 -0
  55. package/pan-wizard-core/bin/lib/milestone.cjs +3 -2
  56. package/pan-wizard-core/bin/lib/phase-remove.cjs +392 -0
  57. package/pan-wizard-core/bin/lib/phase.cjs +4 -369
  58. package/pan-wizard-core/bin/lib/runner.cjs +5 -0
  59. package/pan-wizard-core/bin/lib/squads.cjs +152 -0
  60. package/pan-wizard-core/bin/lib/state.cjs +10 -1
  61. package/pan-wizard-core/bin/lib/verify-deploy.cjs +181 -0
  62. package/pan-wizard-core/bin/lib/verify-drift.cjs +255 -0
  63. package/pan-wizard-core/bin/lib/verify-preflight.cjs +261 -0
  64. package/pan-wizard-core/bin/lib/verify-retro.cjs +177 -0
  65. package/pan-wizard-core/bin/lib/verify.cjs +10 -797
  66. package/pan-wizard-core/bin/lib/worktree.cjs +123 -0
  67. package/pan-wizard-core/bin/pan-tools.cjs +78 -0
  68. package/pan-wizard-core/learnings/universal/autonomous-loop.md +56 -0
  69. package/pan-wizard-core/workflows/plan-phase.md +11 -0
  70. package/scripts/build-plugin.js +105 -0
  71. package/scripts/install-git-hooks.js +64 -0
  72. package/scripts/release-check.js +13 -2
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Worktree — branch-per-agent isolation for the bot army (ADR-0033).
3
+ *
4
+ * The Build squad parallelizes by giving each builder its own git worktree
5
+ * on its own `army/<task>` branch, so concurrent agents never touch the same
6
+ * working tree or the same file. Generalizes the worktree primitive proven in
7
+ * whatif.cjs; the army campaign command drives it. Zero deps, synchronous,
8
+ * cross-platform (delegates to execGit).
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const { execGit, isGitRepo, toPosix, generateSlugInternal, output, error } = require('./core.cjs');
15
+
16
+ const ARMY_BRANCH_PREFIX = 'army/';
17
+
18
+ /**
19
+ * Create an isolated worktree + branch for one army task.
20
+ * @param {string} cwd - main project root
21
+ * @param {string} task - free-text task name (slugified for branch/path)
22
+ * @param {Object} [opts] - { base: ref (default 'HEAD'), worktree_root }
23
+ * @returns {{worktree_path, branch, base}|{error}}
24
+ */
25
+ function createTaskWorktree(cwd, task, opts) {
26
+ if (!task || !String(task).trim()) return { error: 'task name required' };
27
+ if (!isGitRepo(cwd)) return { error: 'Not a git repo — branch-per-agent requires git worktree support' };
28
+
29
+ const slug = generateSlugInternal(String(task)).slice(0, 40);
30
+ const branch = `${ARMY_BRANCH_PREFIX}${slug}`;
31
+ const worktreeRoot = opts?.worktree_root
32
+ || path.join(path.dirname(path.resolve(cwd)), `pan-army-${slug}`);
33
+ const base = opts?.base || 'HEAD';
34
+
35
+ const result = execGit(cwd, ['worktree', 'add', '-b', branch, worktreeRoot, base]);
36
+ if (result.exitCode !== 0) {
37
+ return { error: `git worktree add failed: ${result.stderr}` };
38
+ }
39
+ return { worktree_path: toPosix(worktreeRoot), branch, base };
40
+ }
41
+
42
+ /**
43
+ * Remove an army worktree + its branch. Best-effort; warnings surfaced.
44
+ * @returns {{removed: true, warnings: string[]}|{error}}
45
+ */
46
+ function removeTaskWorktree(cwd, worktreePath, branch, opts) {
47
+ if (!isGitRepo(cwd)) return { error: 'Not a git repo' };
48
+ const warnings = [];
49
+ const rmArgs = ['worktree', 'remove'];
50
+ if (opts?.force === true) rmArgs.push('--force');
51
+ rmArgs.push(worktreePath);
52
+ const rm = execGit(cwd, rmArgs);
53
+ if (rm.exitCode !== 0) warnings.push(`worktree remove: ${rm.stderr.trim()}`);
54
+
55
+ if (branch) {
56
+ // Only delete branches we created (army/ prefix), and only if not checked out.
57
+ if (branch.startsWith(ARMY_BRANCH_PREFIX)) {
58
+ const del = execGit(cwd, ['branch', '-D', branch]);
59
+ if (del.exitCode !== 0) warnings.push(`branch -D ${branch}: ${del.stderr.trim()}`);
60
+ } else {
61
+ warnings.push(`refused to delete non-army branch ${branch}`);
62
+ }
63
+ }
64
+ return { removed: true, warnings };
65
+ }
66
+
67
+ /**
68
+ * List the army worktrees currently registered (army/ branches only).
69
+ * Parses `git worktree list --porcelain`.
70
+ * @returns {Array<{worktree, branch}>}
71
+ */
72
+ function listArmyWorktrees(cwd) {
73
+ if (!isGitRepo(cwd)) return [];
74
+ const r = execGit(cwd, ['worktree', 'list', '--porcelain']);
75
+ if (r.exitCode !== 0) return [];
76
+ const out = [];
77
+ let current = {};
78
+ for (const line of r.stdout.split(/\r?\n/)) {
79
+ if (line.startsWith('worktree ')) {
80
+ current = { worktree: toPosix(line.slice('worktree '.length).trim()) };
81
+ } else if (line.startsWith('branch ')) {
82
+ const ref = line.slice('branch '.length).trim().replace('refs/heads/', '');
83
+ current.branch = ref;
84
+ if (ref.startsWith(ARMY_BRANCH_PREFIX)) out.push({ ...current });
85
+ } else if (line === '') {
86
+ current = {};
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ // ─── CLI ─────────────────────────────────────────────────────────────────────
93
+
94
+ function cmdWorktreeList(cwd, raw) {
95
+ const trees = listArmyWorktrees(cwd);
96
+ const human = trees.length
97
+ ? trees.map(t => `${t.branch} → ${t.worktree}`).join('\n')
98
+ : 'No army worktrees';
99
+ output({ worktrees: trees, count: trees.length }, raw, human);
100
+ }
101
+
102
+ function cmdWorktreeCreate(cwd, task, raw, opts) {
103
+ const r = createTaskWorktree(cwd, task, opts);
104
+ if (r.error) return error(r.error);
105
+ output(r, raw, `${r.branch} → ${r.worktree_path}`);
106
+ }
107
+
108
+ function cmdWorktreeRemove(cwd, worktreePath, branch, raw, opts) {
109
+ if (!worktreePath) return error('worktree path required');
110
+ const r = removeTaskWorktree(cwd, worktreePath, branch, opts);
111
+ if (r.error) return error(r.error);
112
+ output(r, raw, r.warnings.length ? r.warnings.join('\n') : 'removed');
113
+ }
114
+
115
+ module.exports = {
116
+ ARMY_BRANCH_PREFIX,
117
+ createTaskWorktree,
118
+ removeTaskWorktree,
119
+ listArmyWorktrees,
120
+ cmdWorktreeList,
121
+ cmdWorktreeCreate,
122
+ cmdWorktreeRemove,
123
+ };
@@ -147,6 +147,7 @@
147
147
  * Pre-Flight & Dashboard:
148
148
  * preflight [phase|batch] Validate execution prerequisites
149
149
  * dashboard Aggregated project status overview
150
+ * hud [--out f] [--open] [--stdout] Single-page HTML army + project dashboard
150
151
  *
151
152
  * Session Learnings:
152
153
  * learnings extract Extract patterns from session data
@@ -856,6 +857,16 @@ async function main() {
856
857
  break;
857
858
  }
858
859
 
860
+ case 'hud': {
861
+ const hud = require('./lib/hud.cjs');
862
+ hud.cmdHud(cwd, {
863
+ out: getArgValue(args, '--out'),
864
+ open: args.includes('--open'),
865
+ stdout: args.includes('--stdout'),
866
+ }, raw);
867
+ break;
868
+ }
869
+
859
870
  case 'learnings': {
860
871
  const subcommand = args[1];
861
872
  if (subcommand === 'extract') {
@@ -1039,6 +1050,73 @@ async function main() {
1039
1050
  break;
1040
1051
  }
1041
1052
 
1053
+ case 'models': {
1054
+ const subcommand = args[1];
1055
+ if (subcommand === 'check' || !subcommand) {
1056
+ cost.cmdModelsCheck(raw);
1057
+ } else {
1058
+ error('Unknown models subcommand. Available: check');
1059
+ }
1060
+ break;
1061
+ }
1062
+
1063
+ case 'squad': {
1064
+ const squads = require('./lib/squads.cjs');
1065
+ const subcommand = args[1];
1066
+ if (subcommand === 'list' || !subcommand) {
1067
+ squads.cmdSquadList(raw);
1068
+ } else if (subcommand === 'show') {
1069
+ squads.cmdSquadShow(args[2], raw);
1070
+ } else {
1071
+ error('Unknown squad subcommand. Available: list, show');
1072
+ }
1073
+ break;
1074
+ }
1075
+
1076
+ case 'worktree': {
1077
+ const worktree = require('./lib/worktree.cjs');
1078
+ const subcommand = args[1];
1079
+ if (subcommand === 'list' || !subcommand) {
1080
+ worktree.cmdWorktreeList(cwd, raw);
1081
+ } else if (subcommand === 'create') {
1082
+ worktree.cmdWorktreeCreate(cwd, args[2], raw, { base: getArgValue(args, '--base') });
1083
+ } else if (subcommand === 'remove') {
1084
+ worktree.cmdWorktreeRemove(cwd, args[2], getArgValue(args, '--branch'), raw, { force: args.includes('--force') });
1085
+ } else {
1086
+ error('Unknown worktree subcommand. Available: list, create, remove');
1087
+ }
1088
+ break;
1089
+ }
1090
+
1091
+ case 'campaign': {
1092
+ const campaign = require('./lib/campaign.cjs');
1093
+ const subcommand = args[1];
1094
+ if (subcommand === 'schedule') {
1095
+ const budget = getArgValue(args, '--daily-budget');
1096
+ campaign.cmdCampaignSchedule(cwd, {
1097
+ goal: getArgValue(args, '--goal'),
1098
+ source: getArgValue(args, '--source'),
1099
+ cadence: getArgValue(args, '--cadence', 'daily'),
1100
+ daily_budget: budget != null ? Number(budget) : undefined,
1101
+ enabled: args.includes('--disable') ? false : undefined,
1102
+ paused: args.includes('--pause') ? true : (args.includes('--resume') ? false : undefined),
1103
+ }, raw);
1104
+ } else if (subcommand === 'status' || !subcommand) {
1105
+ campaign.cmdCampaignStatus(cwd, raw);
1106
+ } else if (subcommand === 'due') {
1107
+ campaign.cmdCampaignDue(cwd, raw);
1108
+ } else if (subcommand === 'record-run') {
1109
+ const r = campaign.recordRun(cwd, {
1110
+ items_landed: Number(getArgValue(args, '--items', 0)),
1111
+ points_used: Number(getArgValue(args, '--points', 0)),
1112
+ });
1113
+ if (r.error) { error(r.error); } else { output(r, raw, `recorded · next ${r.next_due}`); }
1114
+ } else {
1115
+ error('Unknown campaign subcommand. Available: schedule, status, due, record-run');
1116
+ }
1117
+ break;
1118
+ }
1119
+
1042
1120
  case 'bus': {
1043
1121
  const subcommand = args[1];
1044
1122
  if (subcommand === 'publish') {
@@ -0,0 +1,56 @@
1
+ ---
2
+ topic: autonomous-loop
3
+ last_updated: 2026-06-12T00:00:00.000Z
4
+ patterns:
5
+ - id: P-310
6
+ summary: Autonomous build loops should fan out research and verify in parallel but keep implement/build a single serial step, then seal with one clean build at loop end
7
+ promoted_at: 2026-06-12T00:00:00.000Z
8
+ source_experiments: [montyhall-focus-loop]
9
+ ---
10
+
11
+ # Autonomous Loop (AI-derived)
12
+
13
+ > Hand-promoted from a downstream `/focus-loop` campaign command (MontyHall compiler project) and generalized in ADR-0031. Patterns are **advisory** — orchestrators weight them against current context.
14
+
15
+ ## P-310 — Parallel-research → single-serial-build → parallel-verify, then clean-seal
16
+
17
+ **Evidence:** A backlog-driven autonomous loop that landed many items per run converged on this shape (the "cc#67/cc#68" discipline): research and verification are read-only and parallelize cheaply via the Workflow tool, but the implement/build step mutates shared state and must be a *single* serial actor. Per-item incremental commits twice produced cross-item orphans — a symbol defined only in an uncommitted file, or a combined state that didn't build clean — which only a from-scratch build at loop end caught.
18
+
19
+ **Rule:** For any pick → build → verify → commit → repeat loop:
20
+
21
+ 1. **Fan out research in parallel** (read-only agents): map the substrate, scope the honest-partial boundary, probe for support. Mutate nothing.
22
+ 2. **Implement/build is exactly ONE serial actor** — never inside a `parallel()`. If the project's build trees corrupt under concurrency, enforce at-most-one-builder across the whole loop (a per-project opt-in, not a universal law).
23
+ 3. **Fan out verify in parallel** (read-only) over the already-built tree: correctness / security / honesty lenses.
24
+ 4. **Commit-quality gates:** a *staging-miss guard* (no implementer-touched file left unstaged — never `git add -A`, never a hand-picked subset) and an *orphan audit* (HEAD references no symbol defined only in an uncommitted file).
25
+ 5. **Clean-build seal once at loop end:** per-item builds are incremental for speed; a single from-scratch build + full verification after the last item catches cross-item orphans the incremental builds hid.
26
+ 6. **Rank the backlog from the CURRENT document** (value/effort), never a frozen ID list, so the order never goes stale; honest-partial aggressively and strike only what landed.
27
+
28
+ **Applies in:** `/pan:focus-auto` (`--source backlog`, `--parallel-research`/`--parallel-verify`/`--clean-seal`; ADR-0031), any Workflow-orchestrated build campaign, hierarchical exec (`pan-conductor`).
29
+
30
+ ## P-330 — Scale agents into a coordinated army with squads, worktree isolation, and a human-gated ship
31
+
32
+ **Evidence:** The bot-army model (ADR-0032/0033) showed the durable shape for running a whole-project goal across many agents: a delegation-only coordinator (never codes) fans work to role-scoped *squads* (architecture/build/quality/release), the build squad parallelizes by giving each agent its own branch + git worktree (so concurrent builders never touch the same file), quality is adversarial and read-only, and the path to a protected branch is a human-approved gate, not a bot merge.
33
+
34
+ **Rule:** When scaling beyond a single agent:
35
+
36
+ 1. **Coordinator delegates, never codes** — its tools are delegation-only; it plans, decomposes, and routes to squads, then aggregates tight summaries.
37
+ 2. **Group agents into role-scoped squads with least-privilege tools** — design read-only, build read/write, quality read-only/adversarial, release always-ask. Resolve the roster from data, not hardcoded prompt lists.
38
+ 3. **Parallelize by isolation, not by hope** — one branch + worktree per concurrent builder; never two agents in one tree. Serialize builds only where the build tree corrupts under concurrency (a per-project opt-in).
39
+ 4. **The mutating boundary is human-gated** — merging to a protected branch is `always-ask`; recovery is revert / previous tag, never force-push or history rewrite.
40
+ 5. **The harness scales with the army, not after it** — depth caps, spawn/budget ceilings, and an abort kill-switch checked before every spawn are mandatory; a longer loop must not relax a single cap. Power and safety are the same investment.
41
+
42
+ **Applies in:** `/pan:army` (ADR-0033), `pan-conductor` campaign mode, `squads.cjs` / `worktree.cjs`, any multi-agent delivery.
43
+
44
+ ## P-340 — Schedule autonomy as a due-check the host fires, not a daemon you embed
45
+
46
+ **Evidence:** Making the army run over days (ADR-0034) surfaced the right boundary for a prompt-driven tool that lives inside a host session: it cannot wake itself while the session is closed, and pretending to (an embedded scheduler/daemon spawning agents in the background) is both a false promise and a security surface. The durable design split ownership — PAN owns the schedule *descriptor* and the *decision whether a run is due*; the host scheduler owns *firing* it.
47
+
48
+ **Rule:** For "run X automatically over time" in a session-bound agent:
49
+
50
+ 1. **Persist a schedule descriptor, not a timer** — cadence, next-due, per-day budget, enabled/paused — in project state.
51
+ 2. **Expose a cheap, side-effect-free `due` check** the external trigger polls; make the *reason* explicit (`not_yet` / `paused` / `budget_exhausted_today` / `due`) so skips are auditable.
52
+ 3. **Let the host fire it** — cron / routines / a session loop / a next-open nudge — rather than embedding a background daemon.
53
+ 4. **Resume from persisted state**, advancing next-due and accruing per-day spend on each run; cap the day, not just the run.
54
+ 5. **Never let a schedule lower an irreversible-action gate** — scheduled or not, the human approves the merge. Autonomy extends up to the irreversible step, never through it.
55
+
56
+ **Applies in:** `campaign.cjs` + `/pan:army --schedule` (ADR-0034), any cron/`/loop`-driven PAN automation, the self-improvement loop on a cadence.
@@ -511,6 +511,17 @@ Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
511
511
 
512
512
  ## 13. Present Final Status
513
513
 
514
+ **Sync state.md first** — without this, state.md says "Ready to plan" until the
515
+ first plan's summary lands, and `/pan:progress`/`preflight` read a stale picture:
516
+
517
+ ```bash
518
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state update "Status" "Ready to execute"
519
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state update "Current Plan" "1"
520
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state update "Total Plans in Phase" "${PLAN_COUNT}"
521
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state update "Last Activity" "$(node ~/.claude/pan-wizard-core/bin/pan-tools.cjs current-timestamp date --raw)"
522
+ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs state update "Last Activity Description" "Phase ${PHASE_NUMBER} planned — ${PLAN_COUNT} plans created"
523
+ ```
524
+
514
525
  Route to `<offer_next>` OR `auto_advance` depending on flags/config.
515
526
 
516
527
  **Circular optimization — log plan creation:**
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Build the PAN Wizard Claude Code plugin (ecosystem review item: plugin
3
+ * distribution, first slice). Emits a self-contained plugin directory at
4
+ * dist/pan-wizard-plugin/ following the verified plugin layout:
5
+ *
6
+ * .claude-plugin/plugin.json manifest (metadata; components auto-discover)
7
+ * commands/pan/*.md command markdown (Claude flavor)
8
+ * agents/pan-*.md agent definitions
9
+ * hooks/hooks.json PAN hooks with ${CLAUDE_PLUGIN_ROOT} paths
10
+ * hooks/pan-*.js hook scripts
11
+ * pan-wizard-core/ dispatcher + modules + workflows + templates
12
+ *
13
+ * Distribution status: built ALONGSIDE the loose-file installer. Marketplace
14
+ * publishing is gated on one live verification — whether ${CLAUDE_PLUGIN_ROOT}
15
+ * expands inside command markdown content (documented for hook/MCP configs
16
+ * only). Until then, content references core paths relative to the plugin
17
+ * root, which matches the documented plugin working layout.
18
+ *
19
+ * Usage: node scripts/build-plugin.js (or npm run build:plugin)
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ const ROOT = path.join(__dirname, '..');
28
+ const OUT = path.join(ROOT, 'dist', 'pan-wizard-plugin');
29
+ const pkg = require(path.join(ROOT, 'package.json'));
30
+ const lib = require(path.join(ROOT, 'bin', 'install-lib.cjs'));
31
+
32
+ // Plugin-relative prefix used inside markdown content. Hook/MCP configs get
33
+ // the documented ${CLAUDE_PLUGIN_ROOT} form via buildPluginHooksConfig().
34
+ const CONTENT_PREFIX = '${CLAUDE_PLUGIN_ROOT}/';
35
+
36
+ function rewriteContent(content) {
37
+ return content
38
+ .replace(/~\/\.claude\/pan-wizard-core\//g, `${CONTENT_PREFIX}pan-wizard-core/`)
39
+ .replace(/\.\/\.claude\/pan-wizard-core\//g, `${CONTENT_PREFIX}pan-wizard-core/`)
40
+ .replace(/~\/\.claude\/agents\//g, `${CONTENT_PREFIX}agents/`)
41
+ .replace(/\.\/\.claude\/agents\//g, `${CONTENT_PREFIX}agents/`)
42
+ .replace(/~\/\.claude\//g, CONTENT_PREFIX)
43
+ .replace(/\.\/\.claude\//g, CONTENT_PREFIX);
44
+ }
45
+
46
+ function copyTree(srcDir, destDir, transformMd) {
47
+ fs.mkdirSync(destDir, { recursive: true });
48
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
49
+ const srcPath = path.join(srcDir, entry.name);
50
+ const destPath = path.join(destDir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ copyTree(srcPath, destPath, transformMd);
53
+ } else if (transformMd && entry.name.endsWith('.md')) {
54
+ fs.writeFileSync(destPath, transformMd(fs.readFileSync(srcPath, 'utf8')));
55
+ } else {
56
+ fs.copyFileSync(srcPath, destPath);
57
+ }
58
+ }
59
+ }
60
+
61
+ function main() {
62
+ // Clean output
63
+ fs.rmSync(OUT, { recursive: true, force: true });
64
+ fs.mkdirSync(path.join(OUT, '.claude-plugin'), { recursive: true });
65
+
66
+ // 1. Manifest
67
+ fs.writeFileSync(
68
+ path.join(OUT, '.claude-plugin', 'plugin.json'),
69
+ JSON.stringify(lib.buildPluginManifest(pkg), null, 2) + '\n'
70
+ );
71
+
72
+ // 2. Commands (Claude flavor, plugin-root-relative paths)
73
+ copyTree(path.join(ROOT, 'commands', 'pan'), path.join(OUT, 'commands', 'pan'), rewriteContent);
74
+
75
+ // 3. Agents
76
+ copyTree(path.join(ROOT, 'agents'), path.join(OUT, 'agents'), rewriteContent);
77
+
78
+ // 4. Hooks: config + scripts
79
+ fs.mkdirSync(path.join(OUT, 'hooks'), { recursive: true });
80
+ fs.writeFileSync(
81
+ path.join(OUT, 'hooks', 'hooks.json'),
82
+ JSON.stringify(lib.buildPluginHooksConfig(), null, 2) + '\n'
83
+ );
84
+ const hooksDist = path.join(ROOT, 'hooks', 'dist');
85
+ if (fs.existsSync(hooksDist)) {
86
+ for (const f of fs.readdirSync(hooksDist).filter(n => n.endsWith('.js'))) {
87
+ fs.copyFileSync(path.join(hooksDist, f), path.join(OUT, 'hooks', f));
88
+ }
89
+ }
90
+
91
+ // 5. Core (strip source-only internal learnings, same policy as the installer)
92
+ copyTree(path.join(ROOT, 'pan-wizard-core'), path.join(OUT, 'pan-wizard-core'), rewriteContent);
93
+ fs.rmSync(path.join(OUT, 'pan-wizard-core', 'learnings', 'internal'), { recursive: true, force: true });
94
+ fs.writeFileSync(path.join(OUT, 'pan-wizard-core', 'VERSION'), pkg.version);
95
+
96
+ // Sanity report
97
+ const count = (p) => { try { return fs.readdirSync(p).length; } catch { return 0; } };
98
+ console.log('PAN plugin built at', path.relative(ROOT, OUT));
99
+ console.log(' commands/pan:', count(path.join(OUT, 'commands', 'pan')));
100
+ console.log(' agents:', count(path.join(OUT, 'agents')));
101
+ console.log(' hooks:', count(path.join(OUT, 'hooks')));
102
+ console.log(' version:', pkg.version);
103
+ }
104
+
105
+ main();
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install-git-hooks.js
4
+ *
5
+ * Points this repo's git at `scripts/git-hooks/` instead of the per-clone
6
+ * `.git/hooks/` directory. Run automatically by the `prepare` npm script,
7
+ * which fires on `npm install` inside the source repo.
8
+ *
9
+ * Why this exists:
10
+ * `.git/hooks/` is per-clone (never committed). Without this, every fresh
11
+ * clone of PAN Wizard would have to manually `cp scripts/git-hooks/pre-commit
12
+ * .git/hooks/` to get the gitleaks pre-commit scan. With `core.hooksPath`
13
+ * set to the tracked `scripts/git-hooks/` directory, the hook is active
14
+ * the moment you finish `npm install`.
15
+ *
16
+ * No-op when not in a git working tree (e.g., the package is being installed
17
+ * as a dependency in someone else's `node_modules/`, where there's no `.git`).
18
+ *
19
+ * Safe to re-run.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const { execFileSync } = require('child_process');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const REPO_ROOT = path.resolve(__dirname, '..');
29
+ const HOOKS_DIR = 'scripts/git-hooks';
30
+
31
+ // 1. Are we in a git working tree? If not, this is a downstream install —
32
+ // do nothing.
33
+ const gitDir = path.join(REPO_ROOT, '.git');
34
+ if (!fs.existsSync(gitDir)) {
35
+ // Silent no-op for downstream consumers. Their `node_modules/pan-wizard/`
36
+ // doesn't have its own .git directory.
37
+ process.exit(0);
38
+ }
39
+
40
+ // 2. Set core.hooksPath. Idempotent — overwrites any previous value.
41
+ try {
42
+ execFileSync('git', ['config', '--local', 'core.hooksPath', HOOKS_DIR], {
43
+ cwd: REPO_ROOT,
44
+ stdio: 'inherit',
45
+ });
46
+ } catch (err) {
47
+ // If `git` isn't on PATH or the config write fails, warn but don't fail
48
+ // the install. The user can run this manually later.
49
+ console.warn(`[install-git-hooks] could not set core.hooksPath: ${err.message}`);
50
+ process.exit(0);
51
+ }
52
+
53
+ // 3. Confirm the hook file is executable on Unix. On Windows the bit doesn't
54
+ // matter — Git Bash treats `.sh` and shebanged scripts as executable.
55
+ const hookFile = path.join(REPO_ROOT, HOOKS_DIR, 'pre-commit');
56
+ if (process.platform !== 'win32') {
57
+ try {
58
+ fs.chmodSync(hookFile, 0o755);
59
+ } catch {
60
+ // Best effort.
61
+ }
62
+ }
63
+
64
+ console.error(`[install-git-hooks] core.hooksPath → ${HOOKS_DIR} (gitleaks pre-commit active)`);
@@ -100,6 +100,17 @@ process.stderr.write('\n[release-check] Gate 4/6: doc-lint counts docs/\n');
100
100
  }
101
101
  }
102
102
 
103
+
104
+ // npm runs lifecycle scripts (prepare) before pack; any of their stdout noise
105
+ // lands ahead of the --json payload. npm pretty-prints the JSON array starting
106
+ // on its own line — parse from there.
107
+ function parseNpmJson(stdout) {
108
+ try { return JSON.parse(stdout); } catch { /* fall through to extraction */ }
109
+ const m = stdout.search(/^[[{]s*$/m);
110
+ if (m === -1) throw new Error('no JSON payload found in npm output');
111
+ return JSON.parse(stdout.slice(m));
112
+ }
113
+
103
114
  // Gate 5: npm pack dry-run
104
115
  process.stderr.write('\n[release-check] Gate 5/6: npm pack --dry-run\n');
105
116
  {
@@ -111,7 +122,7 @@ process.stderr.write('\n[release-check] Gate 5/6: npm pack --dry-run\n');
111
122
  }
112
123
  // Parse the JSON output to check size
113
124
  try {
114
- const parsed = JSON.parse(r.stdout);
125
+ const parsed = parseNpmJson(r.stdout);
115
126
  const entry = Array.isArray(parsed) ? parsed[0] : parsed;
116
127
  const size = entry.size || 0;
117
128
  const fileCount = entry.files ? entry.files.length : 0;
@@ -139,7 +150,7 @@ if (SKIP_SMOKE) {
139
150
  logGate('smoke install (pack)', false, `exit ${pack.status}`);
140
151
  process.exit(1);
141
152
  }
142
- const packJson = JSON.parse(pack.stdout);
153
+ const packJson = parseNpmJson(pack.stdout);
143
154
  const tarball = path.join(tmpDir, packJson[0].filename);
144
155
  if (!fs.existsSync(tarball)) {
145
156
  logGate('smoke install (pack)', false, `tarball not found at ${tarball}`);