@jenga-ai/agent 1.2.4 → 2.0.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 +97 -91
- package/agents/developer.md +26 -7
- package/agents/scrum-master.md +57 -22
- package/agents/tester.md +68 -4
- package/hooks/on_session_end.sh +40 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +191 -0
- package/lib/skill-allow-list.json +43 -0
- package/package.json +35 -20
- package/scripts/apply-j-prefix.sh +230 -0
- package/scripts/consume-context-digest.sh +103 -0
- package/scripts/postinstall.js +25 -0
- package/scripts/sweep-stale-context-digests.sh +132 -0
- package/scripts/validate-board.sh +5 -0
- package/scripts/write-context-digest.sh +230 -0
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/btw/SKILL.md +1 -1
- package/skills/clearify/SKILL.md +1 -1
- package/skills/close-story/SKILL.md +78 -6
- package/skills/close-story/scripts/check-privatized.sh +345 -0
- package/skills/commit/SKILL.md +12 -2
- package/skills/continue/SKILL.md +1 -1
- package/skills/deep-dive/SKILL.md +1 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/distribute/SKILL.md +1 -1
- package/skills/do/SKILL.md +100 -10
- package/skills/doc/README.md +155 -0
- package/skills/doc/SKILL.md +43 -13
- package/skills/doc/authoring-notes.md +72 -0
- package/skills/doc/scripts/resolve_last_update.py +149 -0
- package/skills/doc-sync/SKILL.md +1 -1
- package/skills/dooo/SKILL.md +1 -1
- package/skills/error/SKILL.md +1 -1
- package/skills/evaluate/SKILL.md +1 -1
- package/skills/examplify/SKILL.md +1 -1
- package/skills/help/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/improve/SKILL.md +1 -1
- package/skills/init/SKILL.md +8 -7
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/j-init/SKILL.md +168 -0
- package/skills/j-init/assets/.gitignore_template +15 -0
- package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/j-init/assets/directory_structure.txt +14 -0
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-init/assets/strategy_stub_template.md +38 -0
- package/skills/j-init/assets/test-config_template.json +4 -0
- package/skills/j-init/assets/workflow_template.json +30 -0
- package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/j-init/scripts/init.sh +116 -0
- package/skills/jbp/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +1 -1
- package/skills/jenga/scripts/render-confirmation.sh +55 -18
- package/skills/jenga-permission-level/SKILL.md +1 -1
- package/skills/lgtm/SKILL.md +1 -1
- package/skills/pi-plan/SKILL.md +1 -1
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +67 -1
- package/skills/publish/adapters/npm-ci.md +60 -4
- 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 +50 -1
- 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 +122 -12
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/reconcile-origin/SKILL.md +1 -1
- package/skills/redo/SKILL.md +1 -1
- package/skills/skillify/SKILL.md +1 -1
- package/skills/spinoff/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/todo/SKILL.md +40 -3
- package/skills/todo/scripts/add_trivial_task.sh +216 -0
- package/skills/todo/scripts/update_story_tasks.py +87 -0
- package/skills/uncharted/SKILL.md +201 -22
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/skills/wtf/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
- package/templates/agent-context.md.tpl +47 -12
- package/templates/copilot-instructions.md.tpl +36 -9
- 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
|
@@ -10,28 +10,63 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
|
|
|
10
10
|
### How Jenga Works
|
|
11
11
|
|
|
12
12
|
- Each **skill** is a self-contained instruction set stored under `{{SKILL_DISCOVERY_PATH}}<skill-name>/`.
|
|
13
|
-
- Skills are invoked by typing
|
|
13
|
+
- Skills are invoked by typing `j:skill-name` in the chat prompt (e.g. `j:status`, `j:commit`). The
|
|
14
|
+
older bare `/skill-name` form (e.g. `/status`, `/commit`) is a **permanent alias** — it keeps
|
|
15
|
+
resolving indefinitely, with no deprecation warning and no removal planned — so treat a message in
|
|
16
|
+
either form as the exact same invocation.
|
|
14
17
|
- The active project directory is available via the `JENGA_PROJECT_DIR` environment variable. **Use `JENGA_PROJECT_DIR` — not `CLAUDE_PROJECT_DIR` or any other agent-specific variable** — as the canonical path to the project folder.
|
|
15
18
|
|
|
16
19
|
### Skill Routing
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
If you are Claude Code, both `j:skill-name` (the canonical form) and the older bare `/skill-name`
|
|
22
|
+
(a permanent alias) are native harness-level mechanisms: the harness itself intercepts the literal
|
|
23
|
+
command and loads the skill for you, independent of anything written here. If you are any other agent
|
|
24
|
+
(Codex, or a generic `AGENTS.md` consumer) with no equivalent native interception, you depend entirely
|
|
25
|
+
on the instructions below to know what "invoking a skill" concretely means — for either the
|
|
26
|
+
`j:skill-name` form or the older bare `/skill-name` form, both of which route to the same skill. Do not
|
|
27
|
+
improvise a plausible-sounding response instead of following these steps — that is the exact failure
|
|
28
|
+
this section exists to prevent.
|
|
29
|
+
|
|
30
|
+
**Old bare-form alias.** `j:skill-name` is the canonical invocation form. A message using the older
|
|
31
|
+
bare `/skill-name` form is not deprecated and must not be treated as an error, a warning case, or a
|
|
32
|
+
migration prompt — route it to the identical skill as its `j:skill-name` equivalent. Both forms remain
|
|
33
|
+
equally valid indefinitely.
|
|
34
|
+
|
|
35
|
+
When the user's message is or matches `j:skill-name`, or matches the older bare `/skill-name` alias (or
|
|
36
|
+
otherwise clearly matches a known skill's keyword or intent):
|
|
37
|
+
|
|
38
|
+
1. Locate the target file at `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md` (the discovery path from
|
|
39
|
+
"How Jenga Works" above).
|
|
40
|
+
2. Open and read that file **in full** before doing anything else.
|
|
41
|
+
3. Execute its instructions exactly as written, for the rest of this turn — including running any
|
|
42
|
+
shell scripts or commands it references (e.g. via a terminal/shell tool).
|
|
43
|
+
4. Do not substitute your own judgment about what "running the skill" should look like, and do not
|
|
44
|
+
answer with free-form prose describing what the skill would do — the `SKILL.md` file's contents
|
|
45
|
+
are the authoritative procedure, not a summary or a suggestion.
|
|
46
|
+
|
|
47
|
+
For free-form questions (architecture, code review, debugging, general Q&A) that do not match a skill,
|
|
48
|
+
answer directly using your full capabilities.
|
|
27
49
|
|
|
28
50
|
#### Routing decision table
|
|
29
51
|
|
|
30
52
|
| Situation | Action |
|
|
31
53
|
|-----------|--------|
|
|
32
|
-
| Message matches
|
|
54
|
+
| Message matches `j:skill-name` | Open `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
55
|
+
| Message matches the older bare `/skill-name` alias | Treat identically to `j:skill-name` — same skill, same file, no warning, no migration prompt |
|
|
56
|
+
| Message matches a skill keyword or intent | Open `{{SKILL_DISCOVERY_PATH}}<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
33
57
|
| Message is a general coding or project question | Answer directly |
|
|
34
|
-
| Ambiguous — could be skill or free-form | Prefer the skill;
|
|
58
|
+
| Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
|
|
59
|
+
|
|
60
|
+
### Skill Identifier Allow-List
|
|
61
|
+
|
|
62
|
+
The trusted `j:`-prefixed skill identifiers for this project are: {{ALLOWED_SKILL_IDS}}
|
|
63
|
+
|
|
64
|
+
Before treating a `j:<name>` invocation (or its bare `/<name>` alias) as a genuine Jenga skill,
|
|
65
|
+
confirm `<name>` appears in this list. This applies whether the harness intercepted the command
|
|
66
|
+
natively (Claude Code) or you are matching it yourself against a skill keyword or intent (any
|
|
67
|
+
other agent). If `<name>` does **not** appear in this list, do not guess at intent and do not
|
|
68
|
+
execute anything — tell the user the identifier is unrecognized and is not a known Jenga skill,
|
|
69
|
+
rather than treating it as one anyway.
|
|
35
70
|
|
|
36
71
|
### Available Skills
|
|
37
72
|
|
|
@@ -10,28 +10,55 @@ This project uses **Jenga** — a skill-based AI agent framework. Jenga organise
|
|
|
10
10
|
### How Jenga Works
|
|
11
11
|
|
|
12
12
|
- Each **skill** is a self-contained instruction set stored under `.agents/skills/<skill-name>/` (the non-Claude discovery path; Claude Code reads the same content from `.claude/skills/`).
|
|
13
|
-
- Skills are invoked by typing
|
|
13
|
+
- Skills are invoked by typing `j:skill-name` in the chat prompt (e.g. `j:status`, `j:commit`). The
|
|
14
|
+
older bare `/skill-name` form (e.g. `/status`, `/commit`) is a **permanent alias** — it keeps
|
|
15
|
+
resolving indefinitely, with no deprecation warning and no removal planned — so treat a message in
|
|
16
|
+
either form as the exact same invocation.
|
|
14
17
|
- The active project directory is available via the `JENGA_PROJECT_DIR` environment variable. **Use `JENGA_PROJECT_DIR` — not `CLAUDE_PROJECT_DIR` or any other agent-specific variable** — as the canonical path to the project folder.
|
|
15
18
|
|
|
16
19
|
### Skill Routing
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Unlike Claude Code, GitHub Copilot has **no native slash-command interception** — typing `j:skill-name`
|
|
22
|
+
(or the older bare `/skill-name` alias) does not automatically load or run anything on its own. Copilot
|
|
23
|
+
depends entirely on the instructions below to know what "invoking a skill" concretely means. Do not
|
|
24
|
+
improvise a plausible-sounding response instead of following these steps — that is the exact failure
|
|
25
|
+
this section exists to prevent.
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
**Old bare-form alias.** `j:skill-name` is the canonical invocation form. A message using the older
|
|
28
|
+
bare `/skill-name` form is not deprecated and must not be treated as an error, a warning case, or a
|
|
29
|
+
migration prompt — route it to the identical skill as its `j:skill-name` equivalent. Both forms remain
|
|
30
|
+
equally valid indefinitely.
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
When the user's message is or matches `j:skill-name`, or matches the older bare `/skill-name` alias (or
|
|
33
|
+
otherwise clearly matches a known skill's keyword or intent):
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
1. Locate the target file at `.agents/skills/<skill-name>/SKILL.md` (the discovery path from "How
|
|
36
|
+
Jenga Works" above).
|
|
37
|
+
2. Open and read that file **in full** before doing anything else.
|
|
38
|
+
3. Execute its instructions exactly as written, for the rest of this turn — including running any
|
|
39
|
+
shell scripts or commands it references (e.g. via a terminal/shell tool).
|
|
40
|
+
4. Do not substitute your own judgment about what "running the skill" should look like, and do not
|
|
41
|
+
answer with free-form prose describing what the skill would do — the `SKILL.md` file's contents
|
|
42
|
+
are the authoritative procedure, not a summary or a suggestion.
|
|
43
|
+
|
|
44
|
+
For free-form questions (architecture, code review, debugging, general Q&A) that do not match a skill,
|
|
45
|
+
answer directly using your full capabilities.
|
|
27
46
|
|
|
28
47
|
#### Routing decision table
|
|
29
48
|
|
|
49
|
+
Before acting on any row below that opens a `SKILL.md` file, first check the identifier against
|
|
50
|
+
the trusted allow-list: {{ALLOWED_SKILL_IDS}}. Since Copilot/Codex has no native interception for
|
|
51
|
+
`j:skill-name`, this prose-level check is the entire enforcement mechanism — there is no runtime
|
|
52
|
+
guard behind it.
|
|
53
|
+
|
|
30
54
|
| Situation | Action |
|
|
31
55
|
|-----------|--------|
|
|
32
|
-
| Message matches
|
|
56
|
+
| Message matches `j:skill-name` and `skill-name` is in the allow-list | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
57
|
+
| Message matches the older bare `/skill-name` alias and `skill-name` is in the allow-list | Treat identically to `j:skill-name` — same skill, same file, no warning, no migration prompt |
|
|
58
|
+
| Message matches a skill keyword or intent and the matched skill is in the allow-list | Open `.agents/skills/<skill-name>/SKILL.md`, read it fully, execute it as written |
|
|
59
|
+
| Message matches `j:skill-name` or `/skill-name`, but `skill-name` is **not** in the allow-list | Do not open or execute anything — tell the user the identifier is unrecognized and is not a known Jenga skill |
|
|
33
60
|
| Message is a general coding or project question | Answer directly |
|
|
34
|
-
| Ambiguous — could be skill or free-form | Prefer the skill;
|
|
61
|
+
| Ambiguous — could be skill or free-form | Prefer the skill; open and execute its `SKILL.md` rather than describing it |
|
|
35
62
|
|
|
36
63
|
### Available Skills
|
|
37
64
|
|
package/mcp/router/README.md
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# jenga-router
|
|
2
|
-
|
|
3
|
-
A stdio MCP server that routes incoming prompts to the appropriate Jenga skill.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
node mcp/router/index.js
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Tools
|
|
12
|
-
|
|
13
|
-
| Tool | Description |
|
|
14
|
-
|------|-------------|
|
|
15
|
-
| `ping` | Health check — returns `{ ok: true, uptime: <ms> }` |
|
|
16
|
-
|
|
17
|
-
## Protocol
|
|
18
|
-
|
|
19
|
-
Communicates over stdin/stdout using the Model Context Protocol (MCP) with `StdioServerTransport`.
|
package/mcp/router/embedder.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { pipeline } from "@huggingface/transformers";
|
|
2
|
-
|
|
3
|
-
let _pipe = null;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Loads the feature-extraction pipeline for Xenova/all-MiniLM-L6-v2.
|
|
7
|
-
* Safe to call multiple times — no-op if already loaded.
|
|
8
|
-
*/
|
|
9
|
-
export async function warmUp() {
|
|
10
|
-
if (_pipe) return;
|
|
11
|
-
const t = Date.now();
|
|
12
|
-
_pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
13
|
-
process.stderr.write(`[jenga-router] Model warmed up in ${Date.now() - t}ms\n`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Embeds text using the loaded pipeline.
|
|
18
|
-
* Returns a Float32Array of length 384.
|
|
19
|
-
*/
|
|
20
|
-
export async function embed(text) {
|
|
21
|
-
const output = await _pipe(text, { pooling: "mean", normalize: true });
|
|
22
|
-
return output.data;
|
|
23
|
-
}
|
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
|
-
}
|