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 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 once after connecting loads rules + skills.",
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. Loads this room's operating rules + the agentic skills you must use, and confirms you're set up (settings/MCP wired). Call it once right after omni_connect. It returns the skills to read and how to behave; read the linked SKILL.md files before answering.",
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
- understood: { type: "boolean", description: "Set true to confirm you've read the rules and will load the relevant skills." },
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
- bridge.onboarded = true;
319
- const skillBlock = roomSkills.length
320
- ? `\n\n⚡ SKILLS IN THIS ROOM — READ the relevant one(s) NOW (open the link, follow the SKILL.md):\n` +
321
- roomSkills.map((s, i) => ` ${i + 1}. ${s} → ${bridge.url}/api/skills/${s}/skill.md`).join("\n")
322
- : "\n\n(no skills attached to this room yet)";
323
- const ackLine = args.understood
324
- ? `\n\n✅ Acknowledged${Array.isArray(args.skills_to_use) && args.skills_to_use.length ? ` — you'll use: ${args.skills_to_use.join(", ")}` : ""}.`
325
- : `\n\n👉 Confirm by calling omni_onboard({ understood: true, skills_to_use: [...] }) after reading.`;
326
- return text(`OMNI OPERATING RULES (read before you answer):\n` +
327
- ` • You are a live participant; keep YOUR memory + tools. Stay scoped to what the room asks.\n` +
328
- ` • Read the relevant SKILL.md before answering; follow it.\n` +
329
- ` • Run tools/code in YOUR OWN sandbox; omni_stream your work so the room can watch.\n` +
330
- ` • Share results with omni_share_artifact (don't paste walls of text).\n` +
331
- ` • Real-workspace changes go through omni_request_action (the host approves).\n` +
332
- ` • Docs: ${bridge.url}/llms.txt` +
333
- skillBlock +
334
- ackLine +
335
- `\n\n👉 NEXT: omni_wait_for_message() to receive work.`);
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.4",
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.",