@jenga-ai/agent 1.2.4 → 1.3.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 +1 -0
- package/agents/developer.md +18 -0
- package/agents/scrum-master.md +1 -0
- package/agents/tester.md +18 -0
- package/hooks/on_session_end.sh +27 -0
- package/package.json +18 -17
- package/skills/commit/SKILL.md +11 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/init/SKILL.md +7 -6
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/publish/SKILL.md +66 -0
- package/skills/publish/adapters/npm-ci.md +34 -0
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +29 -0
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +121 -11
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/uncharted/SKILL.md +200 -21
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +57 -0
- package/templates/agent-context.md.tpl +23 -11
- package/templates/copilot-instructions.md.tpl +21 -11
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
package/mcp/router/index.js
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { writeFileSync, unlinkSync, existsSync, readFileSync, mkdirSync } from "fs";
|
|
6
|
-
import { join, dirname } from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
import { buildSkillIndex } from "./skill-index.js";
|
|
9
|
-
import { findBestMatch } from "./matcher.js";
|
|
10
|
-
import { warmUp, embed } from "./embedder.js";
|
|
11
|
-
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const projectRoot = join(__dirname, "..", "..");
|
|
14
|
-
const jengaDir = join(process.cwd(), ".jenga");
|
|
15
|
-
const pidFile = join(jengaDir, "router.pid");
|
|
16
|
-
const sessionFile = join(jengaDir, "session.json");
|
|
17
|
-
const startTime = Date.now();
|
|
18
|
-
|
|
19
|
-
// Load config (optional — defaults used if absent)
|
|
20
|
-
let config = {
|
|
21
|
-
skillsPath: "skills/",
|
|
22
|
-
matchThreshold: 0.75,
|
|
23
|
-
sessionTimeout: 300000,
|
|
24
|
-
agentTarget: "claude",
|
|
25
|
-
};
|
|
26
|
-
const configPath = join(projectRoot, "jenga.cli.json");
|
|
27
|
-
if (existsSync(configPath)) {
|
|
28
|
-
try {
|
|
29
|
-
config = { ...config, ...JSON.parse(readFileSync(configPath, "utf8")) };
|
|
30
|
-
} catch (e) {
|
|
31
|
-
process.stderr.write(`Warning: failed to parse jenga.cli.json: ${e.message}\n`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Build skill index (with semantic embeddings)
|
|
36
|
-
let skillIndex = [];
|
|
37
|
-
if (config.skillsPath) {
|
|
38
|
-
await warmUp();
|
|
39
|
-
skillIndex = await buildSkillIndex(join(projectRoot, config.skillsPath), { embed });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Write PID file
|
|
43
|
-
mkdirSync(jengaDir, { recursive: true });
|
|
44
|
-
writeFileSync(pidFile, String(process.pid), "utf8");
|
|
45
|
-
|
|
46
|
-
// Graceful shutdown
|
|
47
|
-
function shutdown() {
|
|
48
|
-
try { unlinkSync(pidFile); } catch {}
|
|
49
|
-
persistSession({ skill: null, startedAt: null });
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
process.on("SIGTERM", shutdown);
|
|
53
|
-
process.on("SIGINT", shutdown);
|
|
54
|
-
|
|
55
|
-
let activeSession = { skill: null, startedAt: null };
|
|
56
|
-
let sessionTimer = null;
|
|
57
|
-
|
|
58
|
-
function persistSession(state) {
|
|
59
|
-
try {
|
|
60
|
-
writeFileSync(sessionFile, JSON.stringify(state), "utf8");
|
|
61
|
-
} catch (e) {
|
|
62
|
-
process.stderr.write(`[jenga-router] Failed to write .session.json: ${e.message}\n`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const server = new McpServer({ name: "jenga-router", version: "1.0.0" });
|
|
67
|
-
|
|
68
|
-
server.tool("ping", "Health check — returns ok and uptime in seconds.", {}, async () => {
|
|
69
|
-
try {
|
|
70
|
-
return {
|
|
71
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, uptime: Math.floor((Date.now() - startTime) / 1000), skill_count: skillIndex.length }) }],
|
|
72
|
-
};
|
|
73
|
-
} catch (e) {
|
|
74
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
server.tool(
|
|
79
|
-
"route_prompt",
|
|
80
|
-
"Route a prompt through the skill matching pipeline. Returns { action: 'invoke'|'passthrough', transformed: string, skill: string|null, confidence: number }.",
|
|
81
|
-
{ text: z.string().describe("The user prompt to route") },
|
|
82
|
-
async ({ text }) => {
|
|
83
|
-
try {
|
|
84
|
-
// Stage 1: Passthrough rules
|
|
85
|
-
if (text.startsWith("/")) {
|
|
86
|
-
return {
|
|
87
|
-
content: [{
|
|
88
|
-
type: "text",
|
|
89
|
-
text: JSON.stringify({ action: "passthrough", transformed: text, skill: null, confidence: 0 }),
|
|
90
|
-
}],
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Stage 0: Active session — passthrough all prompts while session is active
|
|
95
|
-
if (activeSession.skill) {
|
|
96
|
-
if (sessionTimer) clearTimeout(sessionTimer);
|
|
97
|
-
sessionTimer = setTimeout(() => {
|
|
98
|
-
activeSession = { skill: null, startedAt: null };
|
|
99
|
-
sessionTimer = null;
|
|
100
|
-
persistSession(activeSession);
|
|
101
|
-
process.stderr.write(`[jenga-router] Session auto-expired after ${config.sessionTimeout}ms\n`);
|
|
102
|
-
}, config.sessionTimeout);
|
|
103
|
-
return {
|
|
104
|
-
content: [{ type: "text", text: JSON.stringify({ action: "passthrough", transformed: text, skill: activeSession.skill, confidence: 0 }) }],
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Stage 2: Semantic + keyword matching
|
|
109
|
-
const promptEmbedding = await embed(text);
|
|
110
|
-
const match = findBestMatch(text, skillIndex, config.matchThreshold, promptEmbedding);
|
|
111
|
-
if (match) {
|
|
112
|
-
activeSession = { skill: match.skill.name, startedAt: new Date().toISOString() };
|
|
113
|
-
persistSession(activeSession);
|
|
114
|
-
if (sessionTimer) clearTimeout(sessionTimer);
|
|
115
|
-
sessionTimer = setTimeout(() => {
|
|
116
|
-
activeSession = { skill: null, startedAt: null };
|
|
117
|
-
persistSession(activeSession);
|
|
118
|
-
sessionTimer = null;
|
|
119
|
-
process.stderr.write(`[jenga-router] Session auto-expired after ${config.sessionTimeout}ms\n`);
|
|
120
|
-
}, config.sessionTimeout);
|
|
121
|
-
return {
|
|
122
|
-
content: [{
|
|
123
|
-
type: "text",
|
|
124
|
-
text: JSON.stringify({
|
|
125
|
-
action: "invoke",
|
|
126
|
-
transformed: `/${match.skill.name} ${text}`,
|
|
127
|
-
skill: match.skill.name,
|
|
128
|
-
confidence: match.confidence,
|
|
129
|
-
}),
|
|
130
|
-
}],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Default: passthrough
|
|
135
|
-
return {
|
|
136
|
-
content: [{
|
|
137
|
-
type: "text",
|
|
138
|
-
text: JSON.stringify({ action: "passthrough", transformed: text, skill: null, confidence: 0 }),
|
|
139
|
-
}],
|
|
140
|
-
};
|
|
141
|
-
} catch (e) {
|
|
142
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
server.tool("active_session", "Returns the currently active skill session, if any.", {}, async () => {
|
|
148
|
-
try {
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: JSON.stringify({ skill: activeSession.skill, startedAt: activeSession.startedAt }) }],
|
|
151
|
-
};
|
|
152
|
-
} catch (e) {
|
|
153
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
server.tool(
|
|
158
|
-
"end_session",
|
|
159
|
-
"Clear the active session for the given skill. No-op if skill is not the active session.",
|
|
160
|
-
{ skill: z.string().describe("The skill name to end the session for") },
|
|
161
|
-
async ({ skill }) => {
|
|
162
|
-
try {
|
|
163
|
-
if (activeSession.skill === skill) {
|
|
164
|
-
activeSession = { skill: null, startedAt: null };
|
|
165
|
-
persistSession(activeSession);
|
|
166
|
-
if (sessionTimer) { clearTimeout(sessionTimer); sessionTimer = null; }
|
|
167
|
-
}
|
|
168
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
169
|
-
} catch (e) {
|
|
170
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
server.tool(
|
|
176
|
-
"list_skills",
|
|
177
|
-
"Returns the current skill index — all loaded skills with their name, description, and keywords.",
|
|
178
|
-
{},
|
|
179
|
-
async () => {
|
|
180
|
-
try {
|
|
181
|
-
const skills = skillIndex.map(({ name, description, keywords }) => ({ name, description, keywords }));
|
|
182
|
-
return {
|
|
183
|
-
content: [{ type: "text", text: JSON.stringify({ skills }) }],
|
|
184
|
-
};
|
|
185
|
-
} catch (e) {
|
|
186
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
server.tool("reload_skills", "Re-scan the skills directory and rebuild the in-memory skill index.", {}, async () => {
|
|
192
|
-
try {
|
|
193
|
-
await warmUp();
|
|
194
|
-
skillIndex = await buildSkillIndex(join(projectRoot, config.skillsPath), { embed });
|
|
195
|
-
return {
|
|
196
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, skill_count: skillIndex.length }) }],
|
|
197
|
-
};
|
|
198
|
-
} catch (e) {
|
|
199
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const transport = new StdioServerTransport();
|
|
204
|
-
await server.connect(transport);
|
package/mcp/router/matcher.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scores a prompt against a skill using keyword and name matching.
|
|
3
|
-
* Returns a confidence score between 0 and 1.
|
|
4
|
-
*/
|
|
5
|
-
function scoreSkill(prompt, skill) {
|
|
6
|
-
const text = prompt.toLowerCase();
|
|
7
|
-
let score = 0;
|
|
8
|
-
let matches = 0;
|
|
9
|
-
let total = 0;
|
|
10
|
-
|
|
11
|
-
// Check skill name
|
|
12
|
-
total++;
|
|
13
|
-
if (text.includes(skill.name.toLowerCase())) {
|
|
14
|
-
score += 0.9;
|
|
15
|
-
matches++;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Check keywords
|
|
19
|
-
for (const kw of skill.keywords) {
|
|
20
|
-
total++;
|
|
21
|
-
if (text.includes(kw.toLowerCase())) {
|
|
22
|
-
score += 0.7;
|
|
23
|
-
matches++;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check examples (partial match)
|
|
28
|
-
for (const ex of skill.examples) {
|
|
29
|
-
const exWords = ex.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
30
|
-
const matchedWords = exWords.filter(w => text.includes(w));
|
|
31
|
-
if (matchedWords.length > 0 && exWords.length > 0) {
|
|
32
|
-
total++;
|
|
33
|
-
score += 0.5 * (matchedWords.length / exWords.length);
|
|
34
|
-
matches++;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (total === 0) return 0;
|
|
39
|
-
// Normalize: max possible score per item varies, cap at 1.0
|
|
40
|
-
return Math.min(score / Math.max(total, 1), 1.0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Computes cosine similarity between two Float32Array vectors.
|
|
45
|
-
* Result is clamped to [0, 1].
|
|
46
|
-
*/
|
|
47
|
-
function cosineSimilarity(a, b) {
|
|
48
|
-
let dot = 0, normA = 0, normB = 0;
|
|
49
|
-
for (let i = 0; i < a.length; i++) {
|
|
50
|
-
dot += a[i] * b[i];
|
|
51
|
-
normA += a[i] * a[i];
|
|
52
|
-
normB += b[i] * b[i];
|
|
53
|
-
}
|
|
54
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
55
|
-
if (denom === 0) return 0;
|
|
56
|
-
return Math.max(0, Math.min(1, dot / denom));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Finds the best matching skill for a prompt.
|
|
61
|
-
* Blends keyword score with semantic cosine similarity when embeddings are available.
|
|
62
|
-
* Returns { skill, confidence } or null if no match above threshold.
|
|
63
|
-
*/
|
|
64
|
-
export function findBestMatch(prompt, skillIndex, threshold = 0.75, promptEmbedding = null) {
|
|
65
|
-
let best = null;
|
|
66
|
-
let bestScore = 0;
|
|
67
|
-
|
|
68
|
-
for (const skill of skillIndex) {
|
|
69
|
-
const keywordScore = scoreSkill(prompt, skill);
|
|
70
|
-
let finalScore = keywordScore;
|
|
71
|
-
|
|
72
|
-
if (promptEmbedding !== null && skill.embedding !== null) {
|
|
73
|
-
const semanticScore = cosineSimilarity(promptEmbedding, skill.embedding);
|
|
74
|
-
finalScore = 0.4 * keywordScore + 0.6 * semanticScore;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (finalScore > bestScore) {
|
|
78
|
-
bestScore = finalScore;
|
|
79
|
-
best = skill;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (best && bestScore >= threshold) {
|
|
84
|
-
return { skill: best, confidence: bestScore };
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|