pan-wizard 2.9.1 → 3.4.1

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 (58) hide show
  1. package/README.md +8 -8
  2. package/agents/pan-conductor.md +189 -0
  3. package/agents/pan-counterfactual.md +112 -0
  4. package/agents/pan-debugger.md +15 -1
  5. package/agents/pan-document_code.md +21 -0
  6. package/agents/pan-executor.md +16 -0
  7. package/agents/pan-hardener.md +113 -0
  8. package/agents/pan-integration-checker.md +2 -0
  9. package/agents/pan-knowledge.md +81 -0
  10. package/agents/pan-meta-reviewer.md +91 -0
  11. package/agents/pan-plan-checker.md +2 -0
  12. package/agents/pan-previewer.md +98 -0
  13. package/agents/pan-project-researcher.md +4 -4
  14. package/agents/pan-reviewer.md +2 -0
  15. package/agents/pan-verifier.md +2 -0
  16. package/bin/install-lib.cjs +197 -0
  17. package/bin/install.js +1999 -1959
  18. package/commands/pan/cost.md +132 -0
  19. package/commands/pan/exec-phase.md +15 -0
  20. package/commands/pan/focus-auto.md +18 -0
  21. package/commands/pan/focus-exec.md +10 -1
  22. package/commands/pan/knowledge.md +129 -0
  23. package/commands/pan/map-codebase.md +15 -0
  24. package/commands/pan/mcp-bridge.md +145 -0
  25. package/commands/pan/plan-phase.md +11 -0
  26. package/commands/pan/preview.md +114 -0
  27. package/commands/pan/profile.md +37 -0
  28. package/commands/pan/review-deep.md +128 -0
  29. package/commands/pan/verify-phase.md +11 -0
  30. package/commands/pan/what-if.md +146 -0
  31. package/hooks/dist/pan-cost-logger.js +102 -0
  32. package/hooks/dist/pan-statusline.js +154 -108
  33. package/package.json +1 -1
  34. package/pan-wizard-core/bin/lib/bridge.cjs +269 -0
  35. package/pan-wizard-core/bin/lib/bus.cjs +251 -0
  36. package/pan-wizard-core/bin/lib/codebase.cjs +118 -0
  37. package/pan-wizard-core/bin/lib/constants.cjs +39 -0
  38. package/pan-wizard-core/bin/lib/context-budget.cjs +27 -0
  39. package/pan-wizard-core/bin/lib/core.cjs +91 -6
  40. package/pan-wizard-core/bin/lib/cost.cjs +359 -0
  41. package/pan-wizard-core/bin/lib/focus.cjs +100 -2
  42. package/pan-wizard-core/bin/lib/init.cjs +5 -5
  43. package/pan-wizard-core/bin/lib/knowledge.cjs +331 -0
  44. package/pan-wizard-core/bin/lib/memory.cjs +252 -0
  45. package/pan-wizard-core/bin/lib/phase.cjs +40 -13
  46. package/pan-wizard-core/bin/lib/preview.cjs +480 -0
  47. package/pan-wizard-core/bin/lib/review-deep.cjs +280 -0
  48. package/pan-wizard-core/bin/lib/roadmap.cjs +4 -4
  49. package/pan-wizard-core/bin/lib/state.cjs +2 -2
  50. package/pan-wizard-core/bin/lib/verify.cjs +34 -1
  51. package/pan-wizard-core/bin/lib/whatif.cjs +289 -0
  52. package/pan-wizard-core/bin/pan-tools.cjs +239 -4
  53. package/pan-wizard-core/templates/playbook.md +53 -0
  54. package/pan-wizard-core/templates/preview-report.md +93 -0
  55. package/pan-wizard-core/templates/roadmap.md +24 -24
  56. package/pan-wizard-core/templates/state.md +12 -9
  57. package/pan-wizard-core/workflows/plan-phase.md +1 -1
  58. package/scripts/build-hooks.js +2 -1
@@ -1,108 +1,154 @@
1
- #!/usr/bin/env node
2
- // Claude Code Statusline - PAN Edition
3
- // Shows: model | current task | directory | context usage
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const os = require('os');
8
-
9
- // Read JSON from stdin
10
- let input = '';
11
- process.stdin.setEncoding('utf8');
12
- process.stdin.on('data', chunk => input += chunk);
13
- process.stdin.on('end', () => {
14
- try {
15
- const data = JSON.parse(input);
16
- const model = data.model?.display_name || 'Claude';
17
- const dir = data.workspace?.current_dir || process.cwd();
18
- const session = data.session_id || '';
19
- const remaining = data.context_window?.remaining_percentage;
20
-
21
- // Context window display (shows USED percentage scaled to 80% limit)
22
- // Claude Code enforces an 80% context limit, so we scale to show 100% at that point
23
- let ctx = '';
24
- if (remaining != null) {
25
- const rem = Math.round(remaining);
26
- const rawUsed = Math.max(0, Math.min(100, 100 - rem));
27
- // Scale: 80% real usage = 100% displayed
28
- const used = Math.min(100, Math.round((rawUsed / 80) * 100));
29
-
30
- // Write context metrics to bridge file for the context-monitor PostToolUse hook.
31
- // The monitor reads this file to inject agent-facing warnings when context is low.
32
- if (session) {
33
- try {
34
- const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
35
- const bridgeData = JSON.stringify({
36
- session_id: session,
37
- remaining_percentage: remaining,
38
- used_pct: used,
39
- timestamp: Math.floor(Date.now() / 1000)
40
- });
41
- fs.writeFileSync(bridgePath, bridgeData);
42
- } catch (e) {
43
- // Silent fail -- bridge is best-effort, don't break statusline
44
- }
45
- }
46
-
47
- // Build progress bar (10 segments)
48
- const filled = Math.floor(used / 10);
49
- const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
50
-
51
- // Color based on scaled usage (thresholds adjusted for new scale)
52
- if (used < 63) { // ~50% real
53
- ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
54
- } else if (used < 81) { // ~65% real
55
- ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
56
- } else if (used < 95) { // ~76% real
57
- ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
58
- } else {
59
- ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
60
- }
61
- }
62
-
63
- // Current task from todos
64
- let task = '';
65
- const homeDir = os.homedir();
66
- const todosDir = path.join(homeDir, '.claude', 'todos');
67
- if (session && fs.existsSync(todosDir)) {
68
- try {
69
- const files = fs.readdirSync(todosDir)
70
- .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
71
- .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
72
- .sort((a, b) => b.mtime - a.mtime);
73
-
74
- if (files.length > 0) {
75
- try {
76
- const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
77
- const inProgress = todos.find(t => t.status === 'in_progress');
78
- if (inProgress) task = inProgress.activeForm || '';
79
- } catch (e) {}
80
- }
81
- } catch (e) {
82
- // Silently fail on file system errors - don't break statusline
83
- }
84
- }
85
-
86
- // PAN update available?
87
- let panUpdate = '';
88
- const cacheFile = path.join(homeDir, '.claude', 'cache', 'pan-update-check.json');
89
- if (fs.existsSync(cacheFile)) {
90
- try {
91
- const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
92
- if (cache.update_available) {
93
- panUpdate = '\x1b[33m⬆ /pan:update\x1b[0m ';
94
- }
95
- } catch (e) {}
96
- }
97
-
98
- // Output
99
- const dirname = path.basename(dir);
100
- if (task) {
101
- process.stdout.write(`${panUpdate}\x1b[2m${model}\x1b[0m \x1b[1m${task}\x1b[0m \x1b[2m${dirname}\x1b[0m${ctx}`);
102
- } else {
103
- process.stdout.write(`${panUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
104
- }
105
- } catch (e) {
106
- // Silent fail - don't break statusline on parse errors
107
- }
108
- });
1
+ #!/usr/bin/env node
2
+ // Claude Code Statusline - PAN Edition
3
+ // Shows: model | current task | directory | context | cache | thinking
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ /**
10
+ * Build the statusline output string from the JSON payload Claude Code pipes
11
+ * to stdin. Pure function — no stdin, no stdout, no process exit. Safe to
12
+ * call from tests.
13
+ *
14
+ * @param {Object} data - Parsed stdin JSON from Claude Code.
15
+ * @param {Object} [deps] - Optional dep injection for testing.
16
+ * {fs, path, homeDir, tmpDir} — defaults to real modules + OS paths.
17
+ * @returns {string} The statusline content.
18
+ */
19
+ function buildStatuslineOutput(data, deps) {
20
+ const d = deps || {};
21
+ const fsMod = d.fs || fs;
22
+ const pathMod = d.path || path;
23
+ const homeDir = d.homeDir || os.homedir();
24
+ const tmpDir = d.tmpDir || os.tmpdir();
25
+
26
+ if (!data || typeof data !== 'object') return '';
27
+
28
+ const model = data.model?.display_name || 'Claude';
29
+ const dir = data.workspace?.current_dir || process.cwd();
30
+ const session = data.session_id || '';
31
+ const remaining = data.context_window?.remaining_percentage;
32
+
33
+ // Context window bar — shows USED percentage scaled so 80% real = 100% shown.
34
+ let ctx = '';
35
+ if (remaining != null) {
36
+ const rem = Math.round(remaining);
37
+ const rawUsed = Math.max(0, Math.min(100, 100 - rem));
38
+ const used = Math.min(100, Math.round((rawUsed / 80) * 100));
39
+
40
+ if (session && d.skipBridge !== true) {
41
+ try {
42
+ const bridgePath = pathMod.join(tmpDir, `claude-ctx-${session}.json`);
43
+ fsMod.writeFileSync(bridgePath, JSON.stringify({
44
+ session_id: session,
45
+ remaining_percentage: remaining,
46
+ used_pct: used,
47
+ timestamp: Math.floor(Date.now() / 1000),
48
+ }));
49
+ } catch { /* bridge is best-effort */ }
50
+ }
51
+
52
+ const filled = Math.floor(used / 10);
53
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
54
+
55
+ if (used < 63) {
56
+ ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
57
+ } else if (used < 81) {
58
+ ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
59
+ } else if (used < 95) {
60
+ ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
61
+ } else {
62
+ ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
63
+ }
64
+ }
65
+
66
+ // E-8: Opus 4.7 indicators. Read from stdin data first (if present),
67
+ // else merge from optional bridge file `claude-pan-<session>.json` that
68
+ // an agent or external process can write.
69
+ let panExtras = null;
70
+ if (session) {
71
+ try {
72
+ const extrasPath = pathMod.join(tmpDir, `claude-pan-${session}.json`);
73
+ const extrasRaw = fsMod.readFileSync(extrasPath, 'utf8');
74
+ panExtras = JSON.parse(extrasRaw);
75
+ } catch { /* no extras is fine */ }
76
+ }
77
+
78
+ const thinkingActive = (data.thinking && data.thinking.active === true)
79
+ || (panExtras && panExtras.thinking_active === true);
80
+ const cacheHitRate = (data.cache && typeof data.cache.hit_rate_pct === 'number')
81
+ ? data.cache.hit_rate_pct
82
+ : (panExtras && typeof panExtras.cache_hit_rate_pct === 'number'
83
+ ? panExtras.cache_hit_rate_pct
84
+ : null);
85
+
86
+ let thinkingBadge = '';
87
+ if (thinkingActive) thinkingBadge = ' \x1b[35m🧠\x1b[0m';
88
+
89
+ let cacheBadge = '';
90
+ if (cacheHitRate != null) {
91
+ const pct = Math.max(0, Math.min(100, Math.round(cacheHitRate)));
92
+ // Color: green ≥70%, yellow 30-70%, dim <30% (warmup).
93
+ const color = pct >= 70 ? '\x1b[32m' : pct >= 30 ? '\x1b[33m' : '\x1b[2m';
94
+ cacheBadge = ` ${color}⚡${pct}%\x1b[0m`;
95
+ }
96
+
97
+ // Current task from todos
98
+ let task = '';
99
+ const todosDir = pathMod.join(homeDir, '.claude', 'todos');
100
+ let todosExists = false;
101
+ try { todosExists = fsMod.statSync(todosDir).isDirectory(); } catch { /* missing */ }
102
+ if (session && todosExists) {
103
+ try {
104
+ const files = fsMod.readdirSync(todosDir)
105
+ .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
106
+ .map(f => {
107
+ try { return { name: f, mtime: fsMod.statSync(pathMod.join(todosDir, f)).mtime }; }
108
+ catch { return null; }
109
+ })
110
+ .filter(Boolean)
111
+ .sort((a, b) => b.mtime - a.mtime);
112
+
113
+ if (files.length > 0) {
114
+ try {
115
+ const todos = JSON.parse(fsMod.readFileSync(pathMod.join(todosDir, files[0].name), 'utf8'));
116
+ const inProgress = todos.find(t => t.status === 'in_progress');
117
+ if (inProgress) task = inProgress.activeForm || '';
118
+ } catch { /* malformed todos file — skip */ }
119
+ }
120
+ } catch { /* fs errors — silent */ }
121
+ }
122
+
123
+ // PAN update available?
124
+ let panUpdate = '';
125
+ const cacheFile = pathMod.join(homeDir, '.claude', 'cache', 'pan-update-check.json');
126
+ try {
127
+ const cache = JSON.parse(fsMod.readFileSync(cacheFile, 'utf8'));
128
+ if (cache.update_available) panUpdate = '\x1b[33m⬆ /pan:update\x1b[0m │ ';
129
+ } catch { /* no update cache — silent */ }
130
+
131
+ const dirname = pathMod.basename(dir);
132
+ const head = `${panUpdate}\x1b[2m${model}\x1b[0m`;
133
+ const taskSegment = task ? ` │ \x1b[1m${task}\x1b[0m` : '';
134
+ const dirSegment = ` │ \x1b[2m${dirname}\x1b[0m`;
135
+ return `${head}${taskSegment}${dirSegment}${ctx}${cacheBadge}${thinkingBadge}`;
136
+ }
137
+
138
+ // ─── Stdin driver ───────────────────────────────────────────────────────────
139
+
140
+ if (require.main === module) {
141
+ let input = '';
142
+ process.stdin.setEncoding('utf8');
143
+ process.stdin.on('data', chunk => input += chunk);
144
+ process.stdin.on('end', () => {
145
+ try {
146
+ const data = JSON.parse(input);
147
+ process.stdout.write(buildStatuslineOutput(data));
148
+ } catch {
149
+ // Silent fail — don't break statusline on parse errors.
150
+ }
151
+ });
152
+ }
153
+
154
+ module.exports = { buildStatuslineOutput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "2.9.1",
3
+ "version": "3.4.1",
4
4
  "description": "A lightweight workflow automation and context engineering system for Claude Code, OpenCode, Gemini CLI, Codex, and Copilot CLI.",
5
5
  "bin": {
6
6
  "pan-wizard": "bin/install.js"
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Bridge — MCP tool discovery + per-phase recommendation (Spec B v2 Y-5, v3.3).
3
+ *
4
+ * Discovery-only scope. We discover what MCP tools are reachable from the
5
+ * host runtime and what their shapes look like, then recommend which tools
6
+ * a given phase plan might use. We do NOT auto-inject tools into plans or
7
+ * auto-invoke them — those belong to a future wave once MCP schemas stabilize.
8
+ *
9
+ * Data lives at `.planning/bridge/available-tools.json`:
10
+ * {
11
+ * cached_at: "2026-04-18T...",
12
+ * runtime: "claude",
13
+ * servers: [
14
+ * {
15
+ * name: "linear",
16
+ * version: "1.2.3",
17
+ * tools: [
18
+ * { name: "linear.updateTicket", description: "...", schema: {...} },
19
+ * ...
20
+ * ]
21
+ * },
22
+ * ...
23
+ * ]
24
+ * }
25
+ *
26
+ * Populating this file is the host runtime's responsibility (Claude Code's
27
+ * MCP list API, etc.). This module reads the cache and reasons over it.
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { output, error, safeReadFile, toPosix, findPhaseInternal } = require('./core.cjs');
33
+ const { PLANNING_DIR } = require('./constants.cjs');
34
+ const { planningPath } = require('./utils.cjs');
35
+
36
+ const BRIDGE_DIR = 'bridge';
37
+ const TOOLS_FILE = 'available-tools.json';
38
+
39
+ function bridgeDir(cwd) {
40
+ return path.join(planningPath(cwd), BRIDGE_DIR);
41
+ }
42
+
43
+ function toolsCacheFile(cwd) {
44
+ return path.join(bridgeDir(cwd), TOOLS_FILE);
45
+ }
46
+
47
+ /**
48
+ * Load the cached tool list. Returns an empty catalog if the cache is
49
+ * missing or malformed.
50
+ *
51
+ * @param {string} cwd - Project root
52
+ * @returns {{cached_at: string|null, runtime: string|null, servers: Array, source: 'cache'|'empty'}}
53
+ */
54
+ function loadToolCache(cwd) {
55
+ const raw = safeReadFile(toolsCacheFile(cwd));
56
+ if (!raw) return { cached_at: null, runtime: null, servers: [], source: 'empty' };
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ return {
60
+ cached_at: parsed.cached_at || null,
61
+ runtime: parsed.runtime || null,
62
+ servers: Array.isArray(parsed.servers) ? parsed.servers : [],
63
+ source: 'cache',
64
+ };
65
+ } catch {
66
+ return { cached_at: null, runtime: null, servers: [], source: 'empty' };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Write the tool cache. Used by the command when the host runtime provides
72
+ * a fresh tool list (or by tests for fixture setup).
73
+ *
74
+ * @param {string} cwd - Project root
75
+ * @param {{runtime, servers}} data
76
+ * @returns {{written: true, file: string}|{error: string}}
77
+ */
78
+ function writeToolCache(cwd, data) {
79
+ if (!data || typeof data !== 'object') return { error: 'data required' };
80
+ try {
81
+ fs.mkdirSync(bridgeDir(cwd), { recursive: true });
82
+ } catch (e) {
83
+ return { error: `Failed to create bridge dir: ${e.message}` };
84
+ }
85
+ const payload = {
86
+ cached_at: new Date().toISOString(),
87
+ runtime: data.runtime || null,
88
+ servers: Array.isArray(data.servers) ? data.servers : [],
89
+ };
90
+ const file = toolsCacheFile(cwd);
91
+ try {
92
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2), 'utf-8');
93
+ } catch (e) {
94
+ return { error: `Failed to write ${file}: ${e.message}` };
95
+ }
96
+ return { written: true, file: toPosix(path.relative(cwd, file)) };
97
+ }
98
+
99
+ /**
100
+ * Flatten server → tool hierarchy into a single list for easier reasoning.
101
+ *
102
+ * @param {Array} servers - From loadToolCache().servers
103
+ * @returns {Array<{server, name, description, schema}>}
104
+ */
105
+ function flattenTools(servers) {
106
+ const tools = [];
107
+ for (const server of servers || []) {
108
+ if (!server || !Array.isArray(server.tools)) continue;
109
+ for (const tool of server.tools) {
110
+ if (!tool || !tool.name) continue;
111
+ tools.push({
112
+ server: server.name,
113
+ name: tool.name,
114
+ description: tool.description || '',
115
+ schema: tool.schema || null,
116
+ });
117
+ }
118
+ }
119
+ return tools;
120
+ }
121
+
122
+ /**
123
+ * List all available MCP tools, flattened.
124
+ *
125
+ * @param {string} cwd - Project root
126
+ * @returns {Object}
127
+ */
128
+ function listTools(cwd) {
129
+ const cache = loadToolCache(cwd);
130
+ const tools = flattenTools(cache.servers);
131
+ return {
132
+ cached_at: cache.cached_at,
133
+ runtime: cache.runtime,
134
+ server_count: cache.servers.length,
135
+ tool_count: tools.length,
136
+ tools,
137
+ source: cache.source,
138
+ };
139
+ }
140
+
141
+ // ─── Recommendation scoring ────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Score a tool's relevance to a phase by matching plan keywords against
145
+ * tool name + description. Naive frequency-based scoring (no embeddings).
146
+ *
147
+ * @param {string} phaseText - Combined plan text
148
+ * @param {{server, name, description}} tool
149
+ * @returns {{score: number, hits: Array<string>}}
150
+ */
151
+ function scoreToolForPhase(phaseText, tool) {
152
+ if (!phaseText || !tool) return { score: 0, hits: [] };
153
+ const body = phaseText.toLowerCase();
154
+ const haystack = `${tool.server || ''} ${tool.name || ''} ${tool.description || ''}`.toLowerCase();
155
+
156
+ // Extract keywords from the tool's identity: split on non-word chars, dedupe, keep ≥3 chars.
157
+ const keywords = [...new Set(haystack.split(/\W+/).filter(w => w.length >= 3))];
158
+
159
+ let score = 0;
160
+ const hits = [];
161
+ for (const kw of keywords) {
162
+ const re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
163
+ const count = (body.match(re) || []).length;
164
+ if (count > 0) {
165
+ score += count;
166
+ hits.push(kw);
167
+ }
168
+ }
169
+ return { score, hits };
170
+ }
171
+
172
+ /**
173
+ * Recommend MCP tools for a specific phase based on plan text keyword match.
174
+ *
175
+ * @param {string} cwd - Project root
176
+ * @param {string|number} phaseNum - Phase identifier
177
+ * @param {Object} [opts] - {max_recommendations, min_score}
178
+ * @returns {Object}
179
+ */
180
+ function recommendForPhase(cwd, phaseNum, opts) {
181
+ const cache = loadToolCache(cwd);
182
+ const tools = flattenTools(cache.servers);
183
+ if (tools.length === 0) {
184
+ return {
185
+ phase: String(phaseNum),
186
+ runtime: cache.runtime,
187
+ recommendations: [],
188
+ reason: 'no MCP tools cached — run `pan-tools bridge cache` or ensure host runtime populates .planning/bridge/available-tools.json',
189
+ };
190
+ }
191
+
192
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
193
+ if (!phaseInfo || !phaseInfo.found) {
194
+ return {
195
+ phase: String(phaseNum),
196
+ error: `Phase ${phaseNum} not found in .planning/phases/`,
197
+ };
198
+ }
199
+
200
+ const phaseDir = path.join(cwd, phaseInfo.directory);
201
+ const planTexts = (phaseInfo.plans || [])
202
+ .map(f => safeReadFile(path.join(phaseDir, f)) || '')
203
+ .join('\n');
204
+
205
+ const max = Math.max(1, Math.min(50, Number(opts?.max_recommendations) || 10));
206
+ const minScore = Math.max(0, Number(opts?.min_score) || 1);
207
+
208
+ const scored = tools
209
+ .map(tool => ({
210
+ ...tool,
211
+ ...scoreToolForPhase(planTexts, tool),
212
+ }))
213
+ .filter(t => t.score >= minScore)
214
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
215
+ .slice(0, max);
216
+
217
+ return {
218
+ phase: String(phaseNum),
219
+ phase_name: phaseInfo.name || null,
220
+ runtime: cache.runtime,
221
+ total_candidates: tools.length,
222
+ recommendations: scored.map(t => ({
223
+ server: t.server,
224
+ name: t.name,
225
+ description: t.description,
226
+ score: t.score,
227
+ hits: t.hits,
228
+ })),
229
+ };
230
+ }
231
+
232
+ // ─── CLI wrappers ───────────────────────────────────────────────────────────
233
+
234
+ function cmdBridgeList(cwd, raw) {
235
+ output(listTools(cwd), raw);
236
+ }
237
+
238
+ function cmdBridgeRecommend(cwd, phaseNum, opts, raw) {
239
+ if (!phaseNum) error('Usage: bridge recommend <phase> [--max N] [--min-score N]');
240
+ output(recommendForPhase(cwd, phaseNum, opts), raw);
241
+ }
242
+
243
+ function cmdBridgeCache(cwd, serversJson, runtime, raw) {
244
+ // For scripted cache writes. Normally the host runtime writes the file,
245
+ // but this CLI path lets users seed it for testing or from external scripts.
246
+ if (!serversJson) {
247
+ // No payload — just echo the current cache path/state.
248
+ output(listTools(cwd), raw);
249
+ return;
250
+ }
251
+ let servers;
252
+ try { servers = JSON.parse(serversJson); }
253
+ catch (e) { error(`Invalid --servers JSON: ${e.message}`); }
254
+ output(writeToolCache(cwd, { runtime, servers }), raw);
255
+ }
256
+
257
+ module.exports = {
258
+ loadToolCache,
259
+ writeToolCache,
260
+ flattenTools,
261
+ listTools,
262
+ scoreToolForPhase,
263
+ recommendForPhase,
264
+ cmdBridgeList,
265
+ cmdBridgeRecommend,
266
+ cmdBridgeCache,
267
+ BRIDGE_DIR,
268
+ TOOLS_FILE,
269
+ };