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.
- package/README.md +8 -8
- package/agents/pan-conductor.md +189 -0
- package/agents/pan-counterfactual.md +112 -0
- package/agents/pan-debugger.md +15 -1
- package/agents/pan-document_code.md +21 -0
- package/agents/pan-executor.md +16 -0
- package/agents/pan-hardener.md +113 -0
- package/agents/pan-integration-checker.md +2 -0
- package/agents/pan-knowledge.md +81 -0
- package/agents/pan-meta-reviewer.md +91 -0
- package/agents/pan-plan-checker.md +2 -0
- package/agents/pan-previewer.md +98 -0
- package/agents/pan-project-researcher.md +4 -4
- package/agents/pan-reviewer.md +2 -0
- package/agents/pan-verifier.md +2 -0
- package/bin/install-lib.cjs +197 -0
- package/bin/install.js +1999 -1959
- package/commands/pan/cost.md +132 -0
- package/commands/pan/exec-phase.md +15 -0
- package/commands/pan/focus-auto.md +18 -0
- package/commands/pan/focus-exec.md +10 -1
- package/commands/pan/knowledge.md +129 -0
- package/commands/pan/map-codebase.md +15 -0
- package/commands/pan/mcp-bridge.md +145 -0
- package/commands/pan/plan-phase.md +11 -0
- package/commands/pan/preview.md +114 -0
- package/commands/pan/profile.md +37 -0
- package/commands/pan/review-deep.md +128 -0
- package/commands/pan/verify-phase.md +11 -0
- package/commands/pan/what-if.md +146 -0
- package/hooks/dist/pan-cost-logger.js +102 -0
- package/hooks/dist/pan-statusline.js +154 -108
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/bridge.cjs +269 -0
- package/pan-wizard-core/bin/lib/bus.cjs +251 -0
- package/pan-wizard-core/bin/lib/codebase.cjs +118 -0
- package/pan-wizard-core/bin/lib/constants.cjs +39 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +27 -0
- package/pan-wizard-core/bin/lib/core.cjs +91 -6
- package/pan-wizard-core/bin/lib/cost.cjs +359 -0
- package/pan-wizard-core/bin/lib/focus.cjs +100 -2
- package/pan-wizard-core/bin/lib/init.cjs +5 -5
- package/pan-wizard-core/bin/lib/knowledge.cjs +331 -0
- package/pan-wizard-core/bin/lib/memory.cjs +252 -0
- package/pan-wizard-core/bin/lib/phase.cjs +40 -13
- package/pan-wizard-core/bin/lib/preview.cjs +480 -0
- package/pan-wizard-core/bin/lib/review-deep.cjs +280 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +4 -4
- package/pan-wizard-core/bin/lib/state.cjs +2 -2
- package/pan-wizard-core/bin/lib/verify.cjs +34 -1
- package/pan-wizard-core/bin/lib/whatif.cjs +289 -0
- package/pan-wizard-core/bin/pan-tools.cjs +239 -4
- package/pan-wizard-core/templates/playbook.md +53 -0
- package/pan-wizard-core/templates/preview-report.md +93 -0
- package/pan-wizard-core/templates/roadmap.md +24 -24
- package/pan-wizard-core/templates/state.md +12 -9
- package/pan-wizard-core/workflows/plan-phase.md +1 -1
- 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
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
@@ -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
|
+
};
|