promptgraph-mcp 2.9.61 → 2.10.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 +22 -0
- package/commands/duplicates.js +41 -0
- package/commands/plan.js +45 -0
- package/commands/setup.js +37 -0
- package/duplicates.js +70 -0
- package/index.js +15 -1
- package/indexer.js +4 -4
- package/package.json +2 -1
- package/parser.js +3 -1
- package/planner.js +132 -0
- package/skills/pg-chain.md +94 -0
- package/skills/pg.md +88 -0
package/README.md
CHANGED
|
@@ -77,6 +77,10 @@ pg update # Update promptgraph-mcp to latest version
|
|
|
77
77
|
pg search <query> # Semantic search across your skills
|
|
78
78
|
pg marketplace # Browse community skill bundles (TUI)
|
|
79
79
|
|
|
80
|
+
# Skill graph
|
|
81
|
+
pg plan <skill> # DAG execution plan: order, parallel levels, cycle detection
|
|
82
|
+
pg duplicates # Find near-duplicate / overlapping skills
|
|
83
|
+
|
|
80
84
|
# Install
|
|
81
85
|
pg install <id> # Install a skill by marketplace ID (pg-xxxxxx)
|
|
82
86
|
pg import <github-url> # Import skills directly from a GitHub repo URL
|
|
@@ -128,6 +132,24 @@ Your **skill files** stay wherever you put them (the platform skills dir or any
|
|
|
128
132
|
|
|
129
133
|
---
|
|
130
134
|
|
|
135
|
+
## Skill orchestration — `pg` → `pg-chain`
|
|
136
|
+
|
|
137
|
+
`pg setup` installs two router skills:
|
|
138
|
+
|
|
139
|
+
- **`pg`** — single lookup: search → read → execute one skill for a task.
|
|
140
|
+
- **`pg-chain`** — orchestrator: wraps `pg` in a controlled loop for **multi-step** tasks. The model runs a skill, reassesses, finds the next skill, runs it, and repeats **until the goal is met** — following explicit skill chains (`pg_callees`) and discovering new ones (`pg_search`) as needs emerge.
|
|
141
|
+
|
|
142
|
+
`pg-chain` has hard stop conditions so it can't loop forever: goal met, max 7 skills per chain, a repeat-guard (never re-run the same skill on the same sub-task), and a no-progress guard. It keeps a visible `Goal / Done / Now / Left` ledger so the chain is auditable.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
"Add a feature flag, make sure nothing broke, and write the commit."
|
|
146
|
+
→ feature-flag ✓ → safe-verify ✓ → commit-message ✓ (3 skills chained)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Use `pg` for one-step tasks, `pg-chain` when a request clearly spans several skills.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
131
153
|
## OpenCode — `/pg` slash commands
|
|
132
154
|
|
|
133
155
|
After `pg setup opencode`, two slash commands are available inside OpenCode:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { success, error, info } from '../cli.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
// pg duplicates [--threshold 0.92] [--json] — flag near-duplicate / overlapping skills.
|
|
5
|
+
export default async function handler(args, bin) {
|
|
6
|
+
const ti = args.indexOf('--threshold');
|
|
7
|
+
const threshold = ti !== -1 && args[ti + 1] ? parseFloat(args[ti + 1]) : 0.70;
|
|
8
|
+
const asJson = args.includes('--json');
|
|
9
|
+
|
|
10
|
+
const spin = (await import('../cli.js')).spinner('Comparing skill embeddings...');
|
|
11
|
+
spin.start();
|
|
12
|
+
let pairs;
|
|
13
|
+
try {
|
|
14
|
+
const { findDuplicates } = await import('../duplicates.js');
|
|
15
|
+
pairs = await findDuplicates({ threshold });
|
|
16
|
+
} catch (e) {
|
|
17
|
+
spin.stop();
|
|
18
|
+
error(`Duplicate scan failed: ${e.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
spin.stop();
|
|
22
|
+
|
|
23
|
+
if (asJson) { console.log(JSON.stringify(pairs, null, 2)); process.exit(0); }
|
|
24
|
+
|
|
25
|
+
if (!pairs.length) {
|
|
26
|
+
success(`No near-duplicate skills found above similarity ${threshold}.`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('\n' + chalk.bold(`${pairs.length} near-duplicate pair${pairs.length === 1 ? '' : 's'}`) +
|
|
31
|
+
chalk.gray(` (cosine ≥ ${threshold})\n`));
|
|
32
|
+
for (const p of pairs) {
|
|
33
|
+
const simColor = p.sim >= 0.73 ? chalk.red : p.sim >= 0.71 ? chalk.yellow : chalk.gray;
|
|
34
|
+
const cross = p.sameSource ? '' : chalk.gray(' (different sources)');
|
|
35
|
+
console.log(` ${simColor(p.sim.toFixed(3))} ${chalk.cyan(p.aName)} ${chalk.gray('≈')} ${chalk.cyan(p.bName)}${cross}`);
|
|
36
|
+
console.log(` ${chalk.gray(p.a)}`);
|
|
37
|
+
console.log(` ${chalk.gray(p.b)}`);
|
|
38
|
+
}
|
|
39
|
+
console.log(chalk.gray('\n Review pairs ≥ 0.73 first — likely true duplicates worth merging or removing.\n'));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
package/commands/plan.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { success, error, info } from '../cli.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
// pg plan <skill> — show the DAG execution plan for a goal skill.
|
|
5
|
+
export default async function handler(args, bin) {
|
|
6
|
+
const target = args.slice(1).filter(a => !a.startsWith('-')).join(' ');
|
|
7
|
+
if (!target) { error('Usage: pg plan <skill-id-or-name>'); process.exit(1); }
|
|
8
|
+
const asJson = args.includes('--json');
|
|
9
|
+
|
|
10
|
+
const { buildPlan } = await import('../planner.js');
|
|
11
|
+
const plan = buildPlan(target);
|
|
12
|
+
if (plan?.error) { error(plan.error); process.exit(1); }
|
|
13
|
+
|
|
14
|
+
if (asJson) { console.log(JSON.stringify(plan, null, 2)); process.exit(0); }
|
|
15
|
+
|
|
16
|
+
const name = (id) => plan.nodes[id]?.name || id;
|
|
17
|
+
|
|
18
|
+
console.log('\n' + chalk.bold(`Plan for ${chalk.cyan(plan.root.name)}`) +
|
|
19
|
+
chalk.gray(` (${plan.count} skill${plan.count === 1 ? '' : 's'})`));
|
|
20
|
+
|
|
21
|
+
if (!plan.acyclic) {
|
|
22
|
+
console.log(chalk.red(`\n ✗ Cycle detected — cannot execute as-is:`));
|
|
23
|
+
for (const cyc of plan.cycles) {
|
|
24
|
+
console.log(' ' + chalk.red(cyc.map(name).join(' → ')));
|
|
25
|
+
}
|
|
26
|
+
console.log(chalk.gray(' Break the cycle (a skill ends up depending on itself).'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (plan.unresolved.length) {
|
|
30
|
+
console.log(chalk.yellow(`\n ⚠ Referenced but not indexed (${plan.unresolved.length}):`));
|
|
31
|
+
for (const u of plan.unresolved) console.log(' ' + chalk.yellow(u));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (plan.acyclic && plan.levels.length) {
|
|
35
|
+
console.log(chalk.bold('\n Execution order ') + chalk.gray('(dependencies first; each level can run in parallel):'));
|
|
36
|
+
plan.levels.forEach((batch, i) => {
|
|
37
|
+
const tag = batch.length > 1 ? chalk.gray(` (${batch.length} parallel)`) : '';
|
|
38
|
+
console.log(` ${chalk.gray(`L${i}`)} ${batch.map(id => chalk.white(name(id))).join(chalk.gray(' · '))}${tag}`);
|
|
39
|
+
});
|
|
40
|
+
console.log(chalk.gray('\n Linear order: ') + plan.order.map(name).join(chalk.gray(' → ')));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
package/commands/setup.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { colors, success, error, info, section } from '../cli.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
// Install the bundled router skills (pg, pg-chain) so a fresh install actually
|
|
9
|
+
// has the orchestration capability — not just docs that reference it.
|
|
10
|
+
// Copies into skillsDir (indexed → discoverable via pg_search on every platform)
|
|
11
|
+
// and, on Claude platforms, into ~/.claude/commands (so /pg & /pg-chain slash
|
|
12
|
+
// commands work). Never overwrites a user-edited copy.
|
|
13
|
+
function installRouterSkills(skillsDir, platformId) {
|
|
14
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const srcDir = path.join(pkgRoot, 'skills');
|
|
16
|
+
if (!fs.existsSync(srcDir)) return;
|
|
17
|
+
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.md'));
|
|
18
|
+
|
|
19
|
+
const targets = [path.join(skillsDir, '_promptgraph')];
|
|
20
|
+
if (platformId === 'claude-code' || platformId === 'claude-desktop') {
|
|
21
|
+
targets.push(path.join(os.homedir(), '.claude', 'commands'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let installed = 0;
|
|
25
|
+
for (const dir of targets) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
const dest = path.join(dir, f);
|
|
29
|
+
// Don't clobber a copy the user has customized; refresh only if missing/identical-origin.
|
|
30
|
+
if (fs.existsSync(dest)) continue;
|
|
31
|
+
fs.copyFileSync(path.join(srcDir, f), dest);
|
|
32
|
+
installed++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (installed) success(`Installed router skills (${files.map(f => f.replace('.md', '')).join(', ')})`);
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
export default async function handler(args, bin) {
|
|
5
39
|
const { detectPlatforms, PLATFORMS } = await import('../platform.js');
|
|
@@ -43,6 +77,9 @@ export default async function handler(args, bin) {
|
|
|
43
77
|
success(`Skills directory: ${chalk.white(skillsDir)}`);
|
|
44
78
|
info(chalk.gray(' Marketplace installs and pg import will save here'));
|
|
45
79
|
|
|
80
|
+
// 2b. Install bundled router skills (pg + pg-chain orchestrator)
|
|
81
|
+
try { installRouterSkills(skillsDir, platformId); } catch (e) { info(chalk.gray(` (router skills skipped: ${e.message})`)); }
|
|
82
|
+
|
|
46
83
|
// 3. Reindex
|
|
47
84
|
console.log(chalk.gray('\n Indexing skills...\n'));
|
|
48
85
|
await indexAll();
|
package/duplicates.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getDb, blobToVec } from './db.js';
|
|
2
|
+
import { cosineSimilarity } from './embedder.js';
|
|
3
|
+
import { annSearch, buildAnnIndex } from './ann.js';
|
|
4
|
+
import { getStore } from './src/store/index.js';
|
|
5
|
+
|
|
6
|
+
// ── Near-duplicate / overlap detector ────────────────────────────────────────
|
|
7
|
+
// With ~2000 skills, overlap and redundancy are real. Represent each skill by the
|
|
8
|
+
// mean of its chunk embeddings, use the ANN index to get candidate neighbors
|
|
9
|
+
// (avoids O(n²)), then confirm with exact cosine on the skill-level vectors.
|
|
10
|
+
|
|
11
|
+
function skillMeanVectors(db) {
|
|
12
|
+
const rows = db.prepare('SELECT skill_id, embedding FROM chunks').all();
|
|
13
|
+
const acc = new Map(); // skill_id -> { vec: Float32Array, n }
|
|
14
|
+
for (const r of rows) {
|
|
15
|
+
const v = blobToVec(r.embedding);
|
|
16
|
+
const cur = acc.get(r.skill_id);
|
|
17
|
+
if (!cur) acc.set(r.skill_id, { vec: Float32Array.from(v), n: 1 });
|
|
18
|
+
else { for (let i = 0; i < v.length; i++) cur.vec[i] += v[i]; cur.n++; }
|
|
19
|
+
}
|
|
20
|
+
const out = new Map();
|
|
21
|
+
for (const [id, { vec, n }] of acc) {
|
|
22
|
+
for (let i = 0; i < vec.length; i++) vec[i] /= n;
|
|
23
|
+
out.set(id, vec);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Calibrated to BGE-Small mean-pooled vectors, whose similarity scale is compressed:
|
|
29
|
+
// genuine near-duplicates land around 0.70–0.73, not 0.9+. Default 0.70 surfaces real
|
|
30
|
+
// overlap; 0.73+ is near-identical content.
|
|
31
|
+
export async function findDuplicates({ threshold = 0.70, maxPairs = 200, neighbors = 8 } = {}) {
|
|
32
|
+
const db = getDb();
|
|
33
|
+
// A fresh CLI process has an empty in-memory ANN store — build it from chunks
|
|
34
|
+
// so neighbor lookup works (the MCP server keeps it warm; the CLI must not assume that).
|
|
35
|
+
if (getStore().size === 0) await buildAnnIndex();
|
|
36
|
+
const vecs = skillMeanVectors(db);
|
|
37
|
+
const meta = new Map(
|
|
38
|
+
db.prepare('SELECT id, name, source FROM skills').all().map(s => [s.id, s])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const pairs = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
|
|
44
|
+
for (const [id, vec] of vecs) {
|
|
45
|
+
const hits = await annSearch(vec, neighbors);
|
|
46
|
+
if (!hits) break; // ANN unavailable
|
|
47
|
+
for (const { skill_id: other } of hits) {
|
|
48
|
+
if (other === id) continue;
|
|
49
|
+
const key = id < other ? `${id}|${other}` : `${other}|${id}`;
|
|
50
|
+
if (seen.has(key)) continue;
|
|
51
|
+
const ov = vecs.get(other);
|
|
52
|
+
if (!ov) continue;
|
|
53
|
+
const sim = cosineSimilarity(vec, ov);
|
|
54
|
+
if (sim >= threshold) {
|
|
55
|
+
seen.add(key);
|
|
56
|
+
const a = meta.get(id), b = meta.get(other);
|
|
57
|
+
pairs.push({
|
|
58
|
+
a: id, b: other,
|
|
59
|
+
aName: a?.name || id, bName: b?.name || other,
|
|
60
|
+
aSource: a?.source || '?', bSource: b?.source || '?',
|
|
61
|
+
sameSource: a?.source === b?.source,
|
|
62
|
+
sim: +sim.toFixed(4),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pairs.sort((x, y) => y.sim - x.sim);
|
|
69
|
+
return pairs.slice(0, maxPairs);
|
|
70
|
+
}
|
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const args = process.argv.slice(2);
|
|
|
18
18
|
const rawBin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '');
|
|
19
19
|
const bin = (rawBin && rawBin !== 'index') ? rawBin : 'pg';
|
|
20
20
|
|
|
21
|
-
const KNOWN_COMMANDS = new Set(['reindex', 'update', 'import', 'install', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h', '--version', '-v', 'version', 'bundle', 'status', 'train', 'add-dir']);
|
|
21
|
+
const KNOWN_COMMANDS = new Set(['reindex', 'update', 'import', 'install', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h', '--version', '-v', 'version', 'bundle', 'status', 'train', 'add-dir', 'plan', 'duplicates']);
|
|
22
22
|
|
|
23
23
|
function showHelp() {
|
|
24
24
|
console.log(
|
|
@@ -34,6 +34,8 @@ function showHelp() {
|
|
|
34
34
|
['search <query>', 'Search skills from the terminal'],
|
|
35
35
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
36
36
|
['add-dir <path>', 'Index skills from a local folder (any platform)'],
|
|
37
|
+
['plan <skill>', 'Show the DAG execution plan (order, parallel levels, cycles)'],
|
|
38
|
+
['duplicates', 'Find near-duplicate / overlapping skills'],
|
|
37
39
|
['status', 'Show installed skills, repos, and bundles'],
|
|
38
40
|
['install <name>', 'Install a bundle by name, code, or id'],
|
|
39
41
|
['marketplace', 'Interactive TUI: browse + search + install skills & bundles'],
|
|
@@ -95,6 +97,8 @@ const COMMAND_MAP = {
|
|
|
95
97
|
update: './commands/update.js',
|
|
96
98
|
reindex: './commands/reindex.js',
|
|
97
99
|
'add-dir': './commands/add-dir.js',
|
|
100
|
+
plan: './commands/plan.js',
|
|
101
|
+
duplicates: './commands/duplicates.js',
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
if (COMMAND_MAP[args[0]]) {
|
|
@@ -174,6 +178,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
174
178
|
required: ['name'],
|
|
175
179
|
},
|
|
176
180
|
},
|
|
181
|
+
{
|
|
182
|
+
name: 'pg_plan',
|
|
183
|
+
description: 'Build a DAG execution plan for a goal skill: full transitive dependency graph, topological order (dependencies first), parallelizable levels, cycle detection, and unresolved references. Call before chaining to know the whole plan up front.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: { name: { type: 'string' } },
|
|
187
|
+
required: ['name'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
177
190
|
{
|
|
178
191
|
name: 'pg_rate',
|
|
179
192
|
description: 'Record skill usage outcome. Call after applying a skill: outcome="success" if it helped, "fail" if it did not.',
|
|
@@ -288,6 +301,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
288
301
|
case 'pg_callers': result = getCallers(args.name); break;
|
|
289
302
|
case 'pg_callees': result = getCallees(args.name); break;
|
|
290
303
|
case 'pg_impact': result = getImpact(args.name); break;
|
|
304
|
+
case 'pg_plan': { const { buildPlan } = await import('./planner.js'); result = buildPlan(args.name); break; }
|
|
291
305
|
case 'pg_rate':
|
|
292
306
|
if (args.outcome === 'use') recordUse(args.skill_id);
|
|
293
307
|
else if (args.outcome === 'success') recordSuccess(args.skill_id);
|
package/indexer.js
CHANGED
|
@@ -99,10 +99,10 @@ export async function indexBatch(db, skills, { fast = false, silent = false, onE
|
|
|
99
99
|
for (const skill of skills) {
|
|
100
100
|
const id = skillId(skill.source, skill.name);
|
|
101
101
|
for (const calledName of skill.calls) {
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
const resolved =
|
|
105
|
-
upsertEdge.run(id, resolved
|
|
102
|
+
// Only keep an edge if it resolves to a REAL indexed skill — otherwise
|
|
103
|
+
// `/var`, `/usr`, `/scan` (paths & command words) pollute the call graph.
|
|
104
|
+
const resolved = resolveSameSource.get(calledName, skill.source) || resolveAny.get(calledName);
|
|
105
|
+
if (resolved && resolved.id !== id) upsertEdge.run(id, resolved.id);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
})();
|
package/package.json
CHANGED
package/parser.js
CHANGED
|
@@ -5,7 +5,9 @@ import { hardFilter } from './src/filter/hard-filter.js';
|
|
|
5
5
|
import { classify } from './src/filter/classifier.js';
|
|
6
6
|
import { loadModel } from './src/filter/train.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// /skill-name reference. Trailing (?![a-z0-9-]|\/) forces the full word and rejects
|
|
9
|
+
// path segments like /var/log, /usr/bin (a slash right after the word → not a skill ref).
|
|
10
|
+
const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})(?![a-z0-9-]|\/)/g;
|
|
9
11
|
|
|
10
12
|
// Generic filenames that carry no identity — name comes from the parent folder.
|
|
11
13
|
const GENERIC_SKILL_FILENAMES = new Set([
|
package/planner.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { getDb } from './db.js';
|
|
2
|
+
|
|
3
|
+
// ── DAG execution planner ─────────────────────────────────────────────────────
|
|
4
|
+
// Skills reference other skills (edge A→B means "A calls/uses B"). This builds the
|
|
5
|
+
// full transitive dependency graph for a goal skill and produces a deterministic
|
|
6
|
+
// execution plan BEFORE anything runs: topological order, parallelizable levels,
|
|
7
|
+
// cycle detection (with the actual cycle path), and dangling/unresolved refs.
|
|
8
|
+
|
|
9
|
+
function resolveId(db, nameOrId) {
|
|
10
|
+
const byId = db.prepare('SELECT id FROM skills WHERE id = ?').get(nameOrId);
|
|
11
|
+
if (byId) return { id: byId.id };
|
|
12
|
+
const byName = db.prepare('SELECT id FROM skills WHERE name = ?').all(nameOrId);
|
|
13
|
+
if (byName.length === 1) return { id: byName[0].id };
|
|
14
|
+
if (byName.length > 1) {
|
|
15
|
+
return { error: `Ambiguous name "${nameOrId}" — use a full id: ${byName.map(r => r.id).join(', ')}` };
|
|
16
|
+
}
|
|
17
|
+
return { error: `Skill not found: ${nameOrId}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Find every cycle reachable in the dependency subgraph (DFS with a recursion stack).
|
|
21
|
+
function findCycles(nodeIds, adj) {
|
|
22
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
23
|
+
const color = new Map(nodeIds.map(id => [id, WHITE]));
|
|
24
|
+
const stack = [];
|
|
25
|
+
const cycles = [];
|
|
26
|
+
const seenCycle = new Set();
|
|
27
|
+
|
|
28
|
+
function dfs(u) {
|
|
29
|
+
color.set(u, GRAY);
|
|
30
|
+
stack.push(u);
|
|
31
|
+
for (const v of adj.get(u) || []) {
|
|
32
|
+
if (!color.has(v)) continue; // edge to a node outside the set (unresolved)
|
|
33
|
+
if (color.get(v) === GRAY) {
|
|
34
|
+
// back-edge → cycle from v..u then back to v
|
|
35
|
+
const i = stack.indexOf(v);
|
|
36
|
+
const cycle = stack.slice(i).concat(v);
|
|
37
|
+
const key = [...cycle].sort().join('|');
|
|
38
|
+
if (!seenCycle.has(key)) { seenCycle.add(key); cycles.push(cycle); }
|
|
39
|
+
} else if (color.get(v) === WHITE) {
|
|
40
|
+
dfs(v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
stack.pop();
|
|
44
|
+
color.set(u, BLACK);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const id of nodeIds) if (color.get(id) === WHITE) dfs(id);
|
|
48
|
+
return cycles;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildPlan(nameOrId) {
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const res = resolveId(db, nameOrId);
|
|
54
|
+
if (res.error) return res;
|
|
55
|
+
const rootId = res.id;
|
|
56
|
+
|
|
57
|
+
// 1. Gather the transitive dependency subgraph (root + everything it reaches via callees).
|
|
58
|
+
const nodes = new Map(); // id -> { id, name, present, callees: [] }
|
|
59
|
+
const adj = new Map(); // id -> [calleeIds]
|
|
60
|
+
const unresolved = new Set();
|
|
61
|
+
const stack = [rootId];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
|
|
64
|
+
while (stack.length) {
|
|
65
|
+
const cur = stack.pop();
|
|
66
|
+
if (seen.has(cur)) continue;
|
|
67
|
+
seen.add(cur);
|
|
68
|
+
const skill = db.prepare('SELECT id, name, source FROM skills WHERE id = ?').get(cur);
|
|
69
|
+
const present = !!skill;
|
|
70
|
+
const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(cur).map(r => r.to_skill);
|
|
71
|
+
nodes.set(cur, { id: cur, name: skill?.name || cur, source: skill?.source || null, present, callees });
|
|
72
|
+
adj.set(cur, callees);
|
|
73
|
+
if (!present) { unresolved.add(cur); continue; } // dangling ref — don't expand
|
|
74
|
+
for (const c of callees) if (!seen.has(c)) stack.push(c);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nodeIds = [...nodes.keys()];
|
|
78
|
+
|
|
79
|
+
// 2. Cycle detection (report actual paths).
|
|
80
|
+
const cycles = findCycles(nodeIds, adj);
|
|
81
|
+
const acyclic = cycles.length === 0;
|
|
82
|
+
|
|
83
|
+
// 3. Topological order (Kahn) — dependencies (callees) before dependents.
|
|
84
|
+
// indegree[n] = how many of n's callees are still unscheduled.
|
|
85
|
+
const inSet = new Set(nodeIds);
|
|
86
|
+
const indeg = new Map();
|
|
87
|
+
const dependents = new Map(nodeIds.map(id => [id, []])); // callee -> [callers]
|
|
88
|
+
for (const id of nodeIds) {
|
|
89
|
+
const deps = (adj.get(id) || []).filter(c => inSet.has(c));
|
|
90
|
+
indeg.set(id, deps.length);
|
|
91
|
+
for (const d of deps) dependents.get(d).push(id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const order = [];
|
|
95
|
+
const level = new Map();
|
|
96
|
+
let queue = nodeIds.filter(id => indeg.get(id) === 0);
|
|
97
|
+
for (const id of queue) level.set(id, 0);
|
|
98
|
+
|
|
99
|
+
while (queue.length) {
|
|
100
|
+
const next = [];
|
|
101
|
+
for (const d of queue) {
|
|
102
|
+
order.push(d);
|
|
103
|
+
for (const caller of dependents.get(d)) {
|
|
104
|
+
indeg.set(caller, indeg.get(caller) - 1);
|
|
105
|
+
level.set(caller, Math.max(level.get(caller) || 0, (level.get(d) || 0) + 1));
|
|
106
|
+
if (indeg.get(caller) === 0) next.push(caller);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
queue = next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 4. Parallelizable batches — nodes at the same level have no inter-dependency.
|
|
113
|
+
const levels = [];
|
|
114
|
+
if (acyclic) {
|
|
115
|
+
const maxLevel = Math.max(0, ...[...level.values()]);
|
|
116
|
+
for (let l = 0; l <= maxLevel; l++) {
|
|
117
|
+
const batch = order.filter(id => level.get(id) === l);
|
|
118
|
+
if (batch.length) levels.push(batch);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
root: { id: rootId, name: nodes.get(rootId).name },
|
|
124
|
+
count: nodeIds.length,
|
|
125
|
+
acyclic,
|
|
126
|
+
order, // dependencies first, root last
|
|
127
|
+
levels, // each batch can run in parallel
|
|
128
|
+
cycles, // [[a,b,c,a], …] if any
|
|
129
|
+
unresolved: [...unresolved], // referenced but not indexed
|
|
130
|
+
nodes: Object.fromEntries(nodes),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pg-chain
|
|
3
|
+
description: Skill orchestrator — chains skills in a loop (search → execute → reassess → next) until the whole task is done
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PromptGraph Chain Orchestrator
|
|
7
|
+
|
|
8
|
+
Use this when a request needs **more than one skill** to finish — a multi-step task
|
|
9
|
+
where you execute one skill, then discover you need another, and another, until the
|
|
10
|
+
goal is met. Builds on the `pg` router (single lookup) by wrapping it in a controlled loop.
|
|
11
|
+
|
|
12
|
+
## The loop
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
1. PLAN → restate the goal in one sentence + list the obvious sub-tasks.
|
|
16
|
+
2. SELECT → take the next unfinished sub-task.
|
|
17
|
+
3. FIND → pg_search(<sub-task in English keywords>).
|
|
18
|
+
score ≥ 0.60 → use the skill | score < 0.60 → handle directly (no skill).
|
|
19
|
+
4. CHAIN? → pg_callees(<skill id>) — does this skill explicitly call others?
|
|
20
|
+
If yes, those are the next chain links (follow them before re-searching).
|
|
21
|
+
5. EXECUTE → Read the skill file at `path` (MANDATORY), then run its instructions fully.
|
|
22
|
+
6. REASSESS → Is the GOAL complete?
|
|
23
|
+
• Done → STOP, summarize what was chained.
|
|
24
|
+
• Gap remains → name the next sub-task, go to step 2.
|
|
25
|
+
• Stuck/no skill → handle directly, or report the blocker and STOP.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Stop conditions (hard — never skip)
|
|
29
|
+
|
|
30
|
+
The loop **must** terminate. Stop when ANY of these is true:
|
|
31
|
+
|
|
32
|
+
- **Goal met** — the original request is fully satisfied (state it explicitly).
|
|
33
|
+
- **Max 7 skill executions** in one chain. If you hit 7, stop and report progress + what's left.
|
|
34
|
+
- **No progress** — if a sub-task's skill ran but the goal didn't move closer, do NOT
|
|
35
|
+
re-run the same skill. Mark it tried, pick a different approach or stop.
|
|
36
|
+
- **Repeat guard** — keep a list of executed skill ids. Never execute the same skill id
|
|
37
|
+
twice for the same sub-task. Re-running a skill on identical input is a loop, not progress.
|
|
38
|
+
- **No match + can't proceed** — if `pg_search` < 0.60 and you can't handle it directly, stop and ask the user.
|
|
39
|
+
|
|
40
|
+
## State to track out loud
|
|
41
|
+
|
|
42
|
+
Keep a short visible ledger so the chain is auditable:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Goal: <one sentence>
|
|
46
|
+
Done: [skill-a ✓, skill-b ✓]
|
|
47
|
+
Now: <current sub-task> → <skill being used>
|
|
48
|
+
Left: <remaining sub-tasks, or "none">
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Update it after every EXECUTE step. This is what prevents silent infinite loops.
|
|
52
|
+
|
|
53
|
+
## Two ways skills connect
|
|
54
|
+
|
|
55
|
+
1. **Explicit chain** — a skill's file references another skill (e.g. `/run-tests`).
|
|
56
|
+
`pg_callees` surfaces these. Follow declared chains first — the author intended them.
|
|
57
|
+
2. **Emergent need** — mid-task you realize you need something new. `pg_search` for it.
|
|
58
|
+
This is where the orchestrator earns its keep.
|
|
59
|
+
|
|
60
|
+
Prefer explicit chains (deterministic) over emergent search (discovered) when both apply.
|
|
61
|
+
|
|
62
|
+
## Worked example
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
User: "Add a feature flag, then make sure nothing broke and write the commit."
|
|
66
|
+
|
|
67
|
+
Goal: ship a feature flag safely with a commit.
|
|
68
|
+
Done: []
|
|
69
|
+
Now: add a feature flag → pg_search("add feature flag toggle config")
|
|
70
|
+
→ feature-flag (0.81) → Read → execute. Done: [feature-flag ✓]
|
|
71
|
+
Reassess: code changed but untested.
|
|
72
|
+
Now: verify nothing broke → pg_search("run tests verify no regression")
|
|
73
|
+
→ safe-verify (0.78) → Read → execute. Done: [feature-flag ✓, safe-verify ✓]
|
|
74
|
+
Reassess: tests green, change not committed.
|
|
75
|
+
Now: write commit → pg_search("write git commit message")
|
|
76
|
+
→ commit-message (0.91) → Read → execute. Done: [..., commit-message ✓]
|
|
77
|
+
Reassess: GOAL MET → STOP.
|
|
78
|
+
|
|
79
|
+
Summary: chained feature-flag → safe-verify → commit-message (3 skills).
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## When NOT to chain
|
|
83
|
+
|
|
84
|
+
- Single-skill tasks → just use `pg` (this orchestrator is overhead for one step).
|
|
85
|
+
- Pure-knowledge questions with no skill match → answer directly.
|
|
86
|
+
- If two sub-tasks are independent, you may do them in either order — don't invent
|
|
87
|
+
false dependencies.
|
|
88
|
+
|
|
89
|
+
## Honesty rules
|
|
90
|
+
|
|
91
|
+
- A skill running ≠ the sub-task being done. Verify the **outcome**, then advance.
|
|
92
|
+
- If no skill fits a step, say so and handle it directly — do not force a low-score skill
|
|
93
|
+
just to keep the chain going.
|
|
94
|
+
- Report the final chain (which skills, in what order) so the user can audit it.
|
package/skills/pg.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pg
|
|
3
|
+
description: PromptGraph router — finds and loads the right skill for any task
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PromptGraph Router
|
|
7
|
+
|
|
8
|
+
You have access to a semantic skill index via the `promptgraph` MCP server tools.
|
|
9
|
+
|
|
10
|
+
## Step-by-step for every task
|
|
11
|
+
|
|
12
|
+
1. **Translate** the user's request to English keywords (if needed)
|
|
13
|
+
2. **Call `pg_search`** with those keywords
|
|
14
|
+
3. **Evaluate results:**
|
|
15
|
+
- score ≥ 0.75 → use it, high confidence
|
|
16
|
+
- score 0.60–0.74 → use it, but stay alert if instructions feel off-topic
|
|
17
|
+
- score < 0.60 → skip, handle the task directly
|
|
18
|
+
4. **Read the skill file** at the returned `path` using the Read tool — **MANDATORY. Do NOT skip this step even if the task seems obvious. Saying "using skill X" and then doing the task from memory is a protocol violation.**
|
|
19
|
+
5. **Execute** the skill's instructions fully
|
|
20
|
+
|
|
21
|
+
## Score thresholds
|
|
22
|
+
|
|
23
|
+
| Score | Action |
|
|
24
|
+
|---|---|
|
|
25
|
+
| ≥ 0.75 | Use skill, high confidence |
|
|
26
|
+
| 0.60–0.74 | Use skill, verify relevance |
|
|
27
|
+
| < 0.60 | Handle directly, no skill |
|
|
28
|
+
|
|
29
|
+
## Search query tips
|
|
30
|
+
|
|
31
|
+
- Write in English — the embedding model is English-only
|
|
32
|
+
- Use task-oriented phrases: "refactor without breaking tests", "debug memory leak", "write commit message"
|
|
33
|
+
- If first search returns low scores, try a shorter or more specific query
|
|
34
|
+
|
|
35
|
+
## Available MCP tools
|
|
36
|
+
|
|
37
|
+
| Tool | When to use |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `pg_search` | Find a skill by task description — **always start here** |
|
|
40
|
+
| `pg_context` | Get full details + callers/callees for a known skill id |
|
|
41
|
+
| `pg_callers` | Which skills reference this one (dependency check) |
|
|
42
|
+
| `pg_callees` | Which skills this one calls (before executing a chain) |
|
|
43
|
+
| `pg_impact` | What breaks if a skill changes |
|
|
44
|
+
| `pg_list` | List all indexed skills (use when unsure what's available) |
|
|
45
|
+
| `pg_top_rated` | Best-rated skills by success/fail ratio |
|
|
46
|
+
| `pg_marketplace_browse` | Browse community registry |
|
|
47
|
+
| `pg_marketplace_install` | Install by code (`pg-xxxxxx`), id, or name |
|
|
48
|
+
| `pg_bundle_install` | Install a skill bundle |
|
|
49
|
+
|
|
50
|
+
## Skill sources indexed
|
|
51
|
+
|
|
52
|
+
- `~/.claude/commands/` — local command skills
|
|
53
|
+
- `~/.claude/skills-store/` — personal skills
|
|
54
|
+
- `~/.claude/skills-store/github/alirezarezvani-claude-skills` — 330+ engineering, product, marketing, compliance skills
|
|
55
|
+
- `~/.claude/skills-store/github/trailofbits-skills` — security research and audit skills
|
|
56
|
+
- `~/.claude/skills-store/github/OthmanAdi-planning-with-files` — project planning skills
|
|
57
|
+
- `~/.claude/skills-store/marketplace/` — installed community skills
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
User: "отрефактори этот модуль без багов"
|
|
63
|
+
→ pg_search("safe refactor without breaking tests")
|
|
64
|
+
→ returns safe-refactor (score: 0.82) → Read → execute
|
|
65
|
+
|
|
66
|
+
User: "напиши commit message"
|
|
67
|
+
→ pg_search("write git commit message")
|
|
68
|
+
→ returns commit-message (score: 0.91) → Read → execute
|
|
69
|
+
|
|
70
|
+
User: "аудит безопасности кода"
|
|
71
|
+
→ pg_search("security audit vulnerability scan")
|
|
72
|
+
→ returns audit (score: 0.80) → Read → execute
|
|
73
|
+
|
|
74
|
+
User: "спланируй проект"
|
|
75
|
+
→ pg_search("project planning breakdown tasks")
|
|
76
|
+
→ returns planning skill → Read → execute
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## If no skill matches (score < 0.60)
|
|
80
|
+
|
|
81
|
+
Handle the task directly with your own knowledge. Do not force a low-score skill.
|
|
82
|
+
|
|
83
|
+
## Multi-step tasks → use `pg-chain`
|
|
84
|
+
|
|
85
|
+
This router loads **one** skill. If the task needs several skills in sequence
|
|
86
|
+
(execute one → discover you need another → continue until done), switch to the
|
|
87
|
+
**`pg-chain`** orchestrator skill — it wraps this lookup in a controlled loop with
|
|
88
|
+
hard stop conditions. `pg` = single lookup; `pg-chain` = chained execution.
|