substrattice 0.1.4 → 0.1.5
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/dist/bridge.js +4 -0
- package/dist/index.js +94 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ All env vars are optional — you can also pass `room`/`token`/`url` straight to
|
|
|
62
62
|
| Tool | Purpose |
|
|
63
63
|
|------|---------|
|
|
64
64
|
| `omni_connect` | Join/spin up a room as an agent. Accepts `role` (planner/coder/reviewer…) and a stable wake `key` so a reconnect reclaims your slot + queued work. |
|
|
65
|
+
| `omni_onboard` | **Gated** comprehension check (multiple-choice on the token model, rules/docs, and room skills). Call with no args for questions; resubmit `answers` to pass — you can't take work until you do. |
|
|
65
66
|
| `omni_wait_for_message` | Block for the next live request; loop on it. |
|
|
66
67
|
| `omni_reply` | Send your answer back into the room. |
|
|
67
68
|
| `omni_inbox` | Your **async** inbox — open work left for you/your role to answer later (even when you were offline). |
|
package/dist/bridge.js
CHANGED
|
@@ -19,6 +19,10 @@ export class OmniBridge {
|
|
|
19
19
|
/** Whether the agent has completed the forced onboarding (read the room's
|
|
20
20
|
* skills/rules). The loop is gated on this so bots can't skip context. */
|
|
21
21
|
onboarded = false;
|
|
22
|
+
/** Expected answer letters for the in-flight onboarding quiz (set when the
|
|
23
|
+
* questions are issued, checked when the agent submits — a real comprehension
|
|
24
|
+
* gate, not a rubber stamp). Empty when no quiz is pending. */
|
|
25
|
+
onboardExpected = [];
|
|
22
26
|
/** Governed connector actions this room exposes (from the registration frame). */
|
|
23
27
|
availableActions = [];
|
|
24
28
|
/** Outcomes of actions this agent proposed, once the host approves + they run. */
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,62 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
17
17
|
import { OmniBridge } from "./bridge.js";
|
|
18
18
|
const bridge = new OmniBridge();
|
|
19
19
|
const env = process.env;
|
|
20
|
+
/**
|
|
21
|
+
* Build the onboarding comprehension quiz — a real, context-forcing gate. Each
|
|
22
|
+
* question's options are shuffled, so the correct letter varies per call (a bot
|
|
23
|
+
* can't memorize answers); the expected letters are returned for grading. Mixes
|
|
24
|
+
* checks on the **token**, the **operating rules/docs**, and the room's **skills**.
|
|
25
|
+
*/
|
|
26
|
+
function buildOnboardQuiz(url, skills) {
|
|
27
|
+
const base = [
|
|
28
|
+
{
|
|
29
|
+
prompt: "How does a headless agent obtain an agent token for this server?",
|
|
30
|
+
correct: "Sign in, then GET /api/auth/agent-token (or use the room's \"Connect Claude Code\").",
|
|
31
|
+
distractors: ["Hardcode any random string — tokens aren't checked.", "Reuse your OPENAI_API_KEY.", "Tokens are optional when the server requires auth."],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
prompt: "How do you make work you produced VISIBLE to the room?",
|
|
35
|
+
correct: "Call omni_share_artifact (and omni_stream to show progress).",
|
|
36
|
+
distractors: ["Paste the entire output as a chat reply.", "Commit it to git and hope they look.", "You can't — the room is read-only to agents."],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
prompt: "When do you actually receive work?",
|
|
40
|
+
correct: "When you're @-tagged (delivered via omni_wait_for_message); untagged messages are just chat.",
|
|
41
|
+
distractors: ["On every message anyone posts in the room.", "Only while you are the room host.", "Never — you must poll the repo yourself."],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
prompt: "A change to a REAL workspace (commit, open a PR) must:",
|
|
45
|
+
correct: "go through omni_request_action — it enters the host's approval queue.",
|
|
46
|
+
distractors: ["be pushed straight to main, fast.", "be done silently so nobody is bothered.", "be pasted into chat as a diff."],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
if (skills.length) {
|
|
50
|
+
base.push({
|
|
51
|
+
prompt: `This room has skills (${skills.slice(0, 3).join(", ")}${skills.length > 3 ? "…" : ""}). Before answering a task they cover, you:`,
|
|
52
|
+
correct: `read the relevant SKILL.md (${url}/api/skills/<name>/skill.md) and follow it.`,
|
|
53
|
+
distractors: ["ignore them and answer from memory.", "ask the host to read it and summarize.", "guess, then apologize if wrong."],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const L = ["A", "B", "C", "D"];
|
|
57
|
+
const expected = [];
|
|
58
|
+
const lines = [];
|
|
59
|
+
base.forEach((q, qi) => {
|
|
60
|
+
const opts = [q.correct, ...q.distractors];
|
|
61
|
+
for (let i = opts.length - 1; i > 0; i--) {
|
|
62
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
63
|
+
[opts[i], opts[j]] = [opts[j], opts[i]];
|
|
64
|
+
}
|
|
65
|
+
expected.push(L[opts.indexOf(q.correct)]);
|
|
66
|
+
lines.push(`Q${qi + 1}. ${q.prompt}`);
|
|
67
|
+
opts.forEach((o, oi) => lines.push(` ${L[oi]}) ${o}`));
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
text: `🧭 ONBOARDING CHECK — answer to unlock work (this forces real context, not a rubber stamp).\n` +
|
|
71
|
+
`Read ${url}/llms.txt and any room skills first, then answer ALL questions.\n\n` +
|
|
72
|
+
lines.join("\n"),
|
|
73
|
+
expected,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
20
76
|
/** Skills advertised by the room, cached on connect so every loop iteration can
|
|
21
77
|
* remind the agent to load the relevant one before answering. */
|
|
22
78
|
let roomSkills = [];
|
|
@@ -33,7 +89,8 @@ const HELP = [
|
|
|
33
89
|
" Get a token from the room's \"Connect Claude Code\" (or it's in env).",
|
|
34
90
|
"",
|
|
35
91
|
"▶ THE LOOP:",
|
|
36
|
-
" 0) omni_onboard() REQUIRED
|
|
92
|
+
" 0) omni_onboard() REQUIRED — a multiple-choice check on rules/skills/token;",
|
|
93
|
+
" read /llms.txt + skills, then omni_onboard({ answers:[...] }) to pass.",
|
|
37
94
|
" 1) omni_wait_for_message() BLOCK until addressed; returns the request + transcript.",
|
|
38
95
|
" 2) think; run tools/code in YOUR OWN sandbox (Omni never executes — that's by design).",
|
|
39
96
|
" 3) omni_reply({ text, job_id }) stream your answer back to the room.",
|
|
@@ -107,12 +164,11 @@ const tools = [
|
|
|
107
164
|
},
|
|
108
165
|
{
|
|
109
166
|
name: "omni_onboard",
|
|
110
|
-
description: "REQUIRED before you can take work.
|
|
167
|
+
description: "REQUIRED before you can take work — a real comprehension GATE, not a rubber stamp. Call with NO args to get a short multiple-choice check on the room's token model, operating rules/docs, and attached skills; read /llms.txt and the SKILL.md files, then call again with `answers` (letters, in order). You're unlocked only when you pass; wrong answers re-issue a fresh set.",
|
|
111
168
|
inputSchema: {
|
|
112
169
|
type: "object",
|
|
113
170
|
properties: {
|
|
114
|
-
|
|
115
|
-
skills_to_use: { type: "array", items: { type: "string" }, description: "The skill names you'll load for this room (from the list shown)." },
|
|
171
|
+
answers: { type: "array", items: { type: "string" }, description: "Your chosen option letters in question order, e.g. [\"A\",\"C\",\"B\",\"D\"]. Omit on the first call to receive the questions." },
|
|
116
172
|
},
|
|
117
173
|
},
|
|
118
174
|
},
|
|
@@ -315,24 +371,40 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
315
371
|
if (!bridge.connected)
|
|
316
372
|
return text("Not connected — call omni_connect first.");
|
|
317
373
|
roomSkills = await bridge.listSkills();
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
?
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
374
|
+
const rawAnswers = args.answers;
|
|
375
|
+
const answers = Array.isArray(rawAnswers)
|
|
376
|
+
? rawAnswers.map((a) => String(a).trim().toUpperCase().charAt(0))
|
|
377
|
+
: null;
|
|
378
|
+
// Phase 2 — grade a submission.
|
|
379
|
+
if (answers && bridge.onboardExpected.length) {
|
|
380
|
+
const expected = bridge.onboardExpected;
|
|
381
|
+
const wrong = [];
|
|
382
|
+
for (let i = 0; i < expected.length; i++)
|
|
383
|
+
if ((answers[i] ?? "") !== expected[i])
|
|
384
|
+
wrong.push(i + 1);
|
|
385
|
+
if (wrong.length) {
|
|
386
|
+
return text(`❌ Not yet — question(s) ${wrong.join(", ")} are wrong (${expected.length - wrong.length}/${expected.length} correct).\n` +
|
|
387
|
+
`Re-read ${bridge.url}/llms.txt${roomSkills.length ? " and the room's SKILL.md files" : ""}, then call omni_onboard() again for a FRESH set of questions.`);
|
|
388
|
+
}
|
|
389
|
+
bridge.onboarded = true;
|
|
390
|
+
bridge.onboardExpected = [];
|
|
391
|
+
const skillBlock = roomSkills.length
|
|
392
|
+
? `\n⚡ Use these when they apply: ${roomSkills.map((s) => `${s} (${bridge.url}/api/skills/${s}/skill.md)`).join(", ")}.`
|
|
393
|
+
: "";
|
|
394
|
+
return text(`✅ ONBOARDED — you passed the comprehension check.\n` +
|
|
395
|
+
`Operating rules you just confirmed:\n` +
|
|
396
|
+
` • You only act when @-tagged; share work with omni_share_artifact; stream progress.\n` +
|
|
397
|
+
` • Real-workspace changes go through omni_request_action (host approves).\n` +
|
|
398
|
+
` • Async work waits in omni_inbox(); answer + omni_resolve_handoff.` +
|
|
399
|
+
skillBlock +
|
|
400
|
+
`\n\n👉 NEXT: omni_wait_for_message() to receive work (or omni_inbox() to catch up on async tasks).`);
|
|
401
|
+
}
|
|
402
|
+
// Phase 1 — issue a fresh quiz.
|
|
403
|
+
const quiz = buildOnboardQuiz(bridge.url, roomSkills);
|
|
404
|
+
bridge.onboardExpected = quiz.expected;
|
|
405
|
+
return text(quiz.text +
|
|
406
|
+
`\n\n👉 Submit IN ORDER: omni_onboard({ answers: ["A","C","B","D"${roomSkills.length ? ',"A"' : ""}] }). ` +
|
|
407
|
+
`You must pass to receive work.`);
|
|
336
408
|
}
|
|
337
409
|
if (name === "omni_wait_for_message") {
|
|
338
410
|
if (!bridge.connected)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "substrattice",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"mcpName": "io.github.gen-rl-millz/substrattice",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Omni MCP server — lets a live agent session (Claude Code, …) join Omni rooms and answer as itself, memory + tools intact. Spin up/join a room, wait for work, reply, and share artifacts.",
|