prism-mcp-server 20.2.8 → 20.2.9

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
@@ -148,13 +148,16 @@ that would require a host lifecycle hook, launcher, extension, or Prism-owned
148
148
  panel. Context loading itself remains complete even when a host shortens the
149
149
  visible reply.
150
150
 
151
- Free accounts receive the protected 13-skill foundation. It includes
152
- `current-staging-acceptance`, the strict completion extension of
153
- `evidence-first-protocol`: agents must use the exact current staging artifact
154
- and inspect every case and screenshot before reporting acceptance. Paid
155
- accounts receive the current subscribed routing set. Upgrades install newly
156
- entitled packages; downgrades remove only Prism-owned packages while preserving
157
- local skills and locally modified conflicts.
151
+ Free accounts receive only the public hook-free `prism-startup` package; the MCP
152
+ server still supplies a compact, non-proprietary safety and evidence contract.
153
+ Authenticated paid accounts receive the protected behavioral and engineering
154
+ packages plus the current subscribed routing set. The paid
155
+ `evidence-first-protocol` keeps ordinary coding lightweight: one correlated
156
+ reproduction is enough to begin an edit, while strict acceptance starts only
157
+ before a completion claim, push, or release and inspects only the exact artifacts
158
+ used as proof. Upgrades install newly entitled packages; verified downgrades
159
+ remove only Prism-owned packages while preserving local skills and locally
160
+ modified conflicts.
158
161
 
159
162
  When upgrading an older Claude Code installation, `prism connect` removes only
160
163
  the exact Prism-owned startup, skill-sync, handoff, and drift hook actions from
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { configureClaudeAgentPolicy, configureClaudeNativeStartup, configureCode
12
12
  import { runBrowserCli } from './browserCli.js';
13
13
  import { filterPrismMemoryContext } from './utils/memoryQuality.js';
14
14
  import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
15
+ import { verifyBehaviorHandler } from './tools/behavioralVerifierHandler.js';
15
16
  const program = new Command();
16
17
  /** Build the stable `prism load --json` envelope from depth-specific context. */
17
18
  export function buildLoadJsonOutput(project, data, level, metadata) {
@@ -111,6 +112,50 @@ program
111
112
  .command('bootstrap')
112
113
  .description('Print the canonical dashboard-configured first-turn Prism greeting')
113
114
  .action(runBootstrapCommand);
115
+ /**
116
+ * One-shot fallback for hosts whose long-lived MCP transport has closed.
117
+ * This calls the canonical handler, including its authenticated portal path
118
+ * and fail-closed offline scenario; it never asks the host to invent one.
119
+ */
120
+ export async function runVerifyBehaviorCommand(options) {
121
+ try {
122
+ const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
123
+ process.env.SYNALUX_BASE_URL?.trim() ||
124
+ (await getSetting('PRISM_SYNALUX_BASE_URL', '')).trim() ||
125
+ (await getSetting('SYNALUX_BASE_URL', '')).trim() ||
126
+ 'https://synalux.ai';
127
+ const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim() ||
128
+ (await getSetting('PRISM_SYNALUX_API_KEY', '')).trim();
129
+ process.env.PRISM_SYNALUX_BASE_URL = baseUrl.replace(/\/+$/, '');
130
+ if (apiKey)
131
+ process.env.PRISM_SYNALUX_API_KEY = apiKey;
132
+ const result = await verifyBehaviorHandler({
133
+ file_path: options.file,
134
+ change_summary: options.summary,
135
+ ...(options.project ? { project: options.project } : {}),
136
+ ...(options.workspaceId ? { workspace_id: options.workspaceId } : {}),
137
+ });
138
+ const output = result.content
139
+ ?.map((part) => part?.text)
140
+ .filter(Boolean)
141
+ .join('\n') || '';
142
+ if (!output)
143
+ throw new Error('verify_behavior returned no scenario');
144
+ console.log(output);
145
+ }
146
+ catch (err) {
147
+ console.error(`Behavioral verification failed: ${err instanceof Error ? err.message : String(err)}`);
148
+ process.exitCode = 1;
149
+ }
150
+ }
151
+ program
152
+ .command('verify-behavior')
153
+ .description('Get the behavioral edit scenario when an MCP transport is unavailable')
154
+ .requiredOption('--file <path>', 'Path of the file about to be edited')
155
+ .requiredOption('--summary <text>', 'Brief description of the intended change')
156
+ .option('--project <project>', 'Project identifier for workspace-scoped scenarios')
157
+ .option('--workspace-id <id>', 'Workspace ID for custom scenarios')
158
+ .action(runVerifyBehaviorCommand);
114
159
  // Parsed by the direct dispatch at the bottom so all Python CLI flags pass
115
160
  // through unchanged. Registering it here keeps the command visible in help.
116
161
  program
package/dist/connect.js CHANGED
@@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as w
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7
+ import { EVIDENCE_WORKFLOW_POLICY_LINES } from "./evidenceWorkflowPolicy.js";
7
8
  import { LOCAL_FIRST_POLICY_ID, LOCAL_FIRST_POLICY_LINES } from "./localFirstPolicy.js";
8
9
  export const CONNECT_HOSTS = [
9
10
  "claude-code",
@@ -46,6 +47,8 @@ const CODEX_STARTUP_BODY = [
46
47
  "block is managed by `prism connect`; do not edit it manually.",
47
48
  "",
48
49
  ...LOCAL_FIRST_POLICY_LINES,
50
+ "",
51
+ ...EVIDENCE_WORKFLOW_POLICY_LINES,
49
52
  ];
50
53
  const CONNECT_STORAGE_BACKENDS = ["auto", "local", "synalux", "supabase"];
51
54
  const LEGACY_CLAUDE_PROJECT_PRISM_ENTRY = {
@@ -542,6 +545,8 @@ function serializeClaudeStartupBlock(newline) {
542
545
  "managed by `prism connect`; do not edit it manually.",
543
546
  "",
544
547
  ...LOCAL_FIRST_POLICY_LINES,
548
+ "",
549
+ ...EVIDENCE_WORKFLOW_POLICY_LINES,
545
550
  CLAUDE_STARTUP_MANAGED_END,
546
551
  "",
547
552
  ].join(newline);
@@ -617,6 +622,8 @@ function serializeGeminiStartupBlock(newline) {
617
622
  "session_detect_drift calls. This block is managed by `prism connect`; do not edit it manually.",
618
623
  "",
619
624
  ...LOCAL_FIRST_POLICY_LINES,
625
+ "",
626
+ ...EVIDENCE_WORKFLOW_POLICY_LINES,
620
627
  GEMINI_STARTUP_MANAGED_END,
621
628
  "",
622
629
  ].join(newline);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Minimum evidence contract shared by every MCP host.
3
+ *
4
+ * Native-skill hosts also receive the full evidence-first protocol. Keep this
5
+ * compact copy in MCP initialize instructions so hosts without a filesystem
6
+ * skill surface, including Claude Desktop, still follow the same workflow.
7
+ */
8
+ export const EVIDENCE_WORKFLOW_POLICY_LINES = [
9
+ "## Prism evidence workflow",
10
+ "During diagnosis and editing, one trustworthy correlated reproduction is enough; do not block coding on",
11
+ "inspecting unrelated diagnostic screenshots, trace frames, or abandoned attempts.",
12
+ "Before a completion claim, push, or release, exercise the corrected path with fresh evidence from the current",
13
+ "build and bind stateful proof to the exact run and entity. Inspect every artifact used to support the claim.",
14
+ "When a screenshot is requested or used as proof, the active agent must open it and compare its visible state",
15
+ "with the issue's expected and forbidden states. Complete that review yourself; do not ask the user to verify it.",
16
+ "A screenshot is an observation, not absolute truth. Reject stale, wrong-run, wrong-entity, or visibly failing",
17
+ "evidence even when its metadata says passed.",
18
+ ];
19
+ export const EVIDENCE_WORKFLOW_POLICY_TEXT = EVIDENCE_WORKFLOW_POLICY_LINES.join(" ");
@@ -28,8 +28,16 @@ export function monitorMcpTransport(server, options) {
28
28
  options.onFailure(reason, error);
29
29
  };
30
30
  const handleClose = () => {
31
- previousOnClose?.();
32
- fail("MCP_TRANSPORT_CLOSED");
31
+ let closeError;
32
+ try {
33
+ previousOnClose?.();
34
+ }
35
+ catch (error) {
36
+ closeError = error instanceof Error ? error : new Error(String(error));
37
+ }
38
+ finally {
39
+ fail("MCP_TRANSPORT_CLOSED", closeError);
40
+ }
33
41
  };
34
42
  server.onclose = handleClose;
35
43
  const check = async () => {
package/dist/server.js CHANGED
@@ -85,6 +85,7 @@ import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
85
85
  import { recordInvocation } from "./utils/analytics.js";
86
86
  import { BOUNDARIES_TEXT } from "./boundaries/boundaries.js";
87
87
  import { triggerSkillManifestSync } from "./skillManifestSync.js";
88
+ import { EVIDENCE_WORKFLOW_POLICY_TEXT } from "./evidenceWorkflowPolicy.js";
88
89
  import { LOCAL_FIRST_POLICY_TEXT } from "./localFirstPolicy.js";
89
90
  // ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
90
91
  import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL, CODE_MODE_TRANSFORM_TOOL, BRAVE_ANSWERS_TOOL, RESEARCH_PAPER_ANALYSIS_TOOL, webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, braveLocalSearchCodeModeHandler, codeModeTransformHandler, braveAnswersHandler, researchPaperAnalysisHandler, } from "./tools/index.js";
@@ -328,6 +329,7 @@ export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI A
328
329
  `Reuse the conversation_id returned by session_bootstrap in structuredContent for those saves and for ` +
329
330
  `session_detect_drift, the 60-minute goal-alignment drift check. Do not add the id to the visible greeting.\n\n` +
330
331
  `${LOCAL_FIRST_POLICY_TEXT}\n\n` +
332
+ `${EVIDENCE_WORKFLOW_POLICY_TEXT}\n\n` +
331
333
  `Architecture: session_save_ledger and session_save_handoff require context loaded by session_bootstrap ` +
332
334
  `or session_load_context when a conversation_id is supplied. ${BOUNDARIES_TEXT} ` +
333
335
  `All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`;
@@ -4,7 +4,7 @@ import { access, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realp
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { applyManagedSkillManifest, getSetting, refreshConfigStorageCache, } from "./storage/configStorage.js";
7
- import { REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
7
+ import { FREE_NATIVE_SKILL_NAMES, REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
8
8
  import { getSynaluxJwt, invalidateSynaluxJwt } from "./utils/synaluxJwt.js";
9
9
  const OWNER = "prism-skill-sync-v1";
10
10
  const MARKER = ".prism-managed.json";
@@ -181,17 +181,20 @@ export function validateSkillManifest(payload) {
181
181
  }
182
182
  }
183
183
  }
184
- const requiredNames = new Set(REQUIRED_NATIVE_SKILL_NAMES);
185
- for (const required of REQUIRED_NATIVE_SKILL_NAMES) {
184
+ const requiredForTier = value.tier === "free"
185
+ ? FREE_NATIVE_SKILL_NAMES
186
+ : REQUIRED_NATIVE_SKILL_NAMES;
187
+ const requiredNames = new Set(requiredForTier);
188
+ for (const required of requiredForTier) {
186
189
  const requiredSkill = skills.find((skill) => skill.name === required);
187
190
  if (!requiredSkill)
188
- throw new Error(`manifest is missing required protected skill: ${required}`);
191
+ throw new Error(`manifest is missing required native skill: ${required}`);
189
192
  if (!requiredSkill.metadata.protected || !requiredSkill.metadata.categories.includes("universal")) {
190
- throw new Error(`required skill is not protected universal: ${required}`);
193
+ throw new Error(`required native skill is not protected universal: ${required}`);
191
194
  }
192
195
  }
193
196
  if (value.tier === "free" && (skills.length !== requiredNames.size || skills.some((skill) => !requiredNames.has(skill.name)))) {
194
- throw new Error("free manifest must contain exactly the protected skill floor");
197
+ throw new Error("free manifest must contain exactly the public startup package");
195
198
  }
196
199
  const normalized = {
197
200
  schema_version: 1,
@@ -20,6 +20,7 @@
20
20
  * Methods migrated to portal:
21
21
  * - saveLedger → POST /api/v1/prism/memory action=save_ledger
22
22
  * - saveHandoff → POST /api/v1/prism/memory action=save_handoff
23
+ * - saveHistorySnapshot → POST /api/v1/prism/memory action=save_history_snapshot
23
24
  * - loadContext → POST /api/v1/prism/memory action=load_context
24
25
  * - searchKnowledge → POST /api/v1/prism/memory action=search
25
26
  * - softDeleteLedger → POST /api/v1/prism/memory action=forget_memory (Phase 3 Tier A)
@@ -203,7 +204,46 @@ export class SynaluxStorage extends SupabaseStorage {
203
204
  role: handoff.role,
204
205
  expected_version: expectedVersion ?? undefined,
205
206
  });
206
- return (result.handoff ?? result);
207
+ const candidate = Object.prototype.hasOwnProperty.call(result, "result")
208
+ ? result.result
209
+ : Object.prototype.hasOwnProperty.call(result, "handoff")
210
+ ? result.handoff
211
+ : result;
212
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
213
+ throw new Error("[SynaluxStorage] Invalid save_handoff response: missing result");
214
+ }
215
+ const value = candidate;
216
+ if (value.status === "conflict" && Number.isSafeInteger(value.current_version)) {
217
+ return {
218
+ status: "conflict",
219
+ current_version: value.current_version,
220
+ };
221
+ }
222
+ if ((value.status === "created" || value.status === "updated")
223
+ && Number.isSafeInteger(value.version)) {
224
+ return {
225
+ status: value.status,
226
+ version: value.version,
227
+ };
228
+ }
229
+ // Older portal RPC wrappers returned only the new version. Preserve that
230
+ // rolling-upgrade contract without accepting an unversioned success.
231
+ if (value.status === undefined && Number.isSafeInteger(value.version)) {
232
+ return {
233
+ status: "updated",
234
+ version: value.version,
235
+ };
236
+ }
237
+ throw new Error("[SynaluxStorage] Invalid save_handoff response: malformed OCC result");
238
+ }
239
+ async saveHistorySnapshot(handoff, branch = "main") {
240
+ await this.portalPost("/api/v1/prism/memory", {
241
+ action: "save_history_snapshot",
242
+ project: handoff.project,
243
+ version: handoff.version,
244
+ snapshot: handoff,
245
+ branch,
246
+ });
207
247
  }
208
248
  // ─── Context ─────────────────────────────────────────────────
209
249
  async loadContext(project, level, userId, role) {
@@ -8,7 +8,7 @@
8
8
  * FAIL-CLOSED: if the portal is unreachable, returns a generic
9
9
  * verification challenge rather than skipping verification.
10
10
  */
11
- import { PRISM_SYNALUX_BASE_URL, SYNALUX_CONFIGURED } from "../config.js";
11
+ import { PRISM_SYNALUX_BASE_URL } from "../config.js";
12
12
  import { getSynaluxJwt } from "../utils/synaluxJwt.js";
13
13
  const FALLBACK_SCENARIO = [
14
14
  "⚠️ BEHAVIORAL VERIFICATION (OFFLINE MODE)",
@@ -33,7 +33,13 @@ export async function verifyBehaviorHandler(args) {
33
33
  return { content: [{ type: "text", text: await buildScenarioText(args) }] };
34
34
  }
35
35
  async function buildScenarioText(args) {
36
- if (!SYNALUX_CONFIGURED || !PRISM_SYNALUX_BASE_URL) {
36
+ const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
37
+ process.env.SYNALUX_BASE_URL?.trim() ||
38
+ PRISM_SYNALUX_BASE_URL;
39
+ // OAuth/JWT-backed installs intentionally do not copy a long-lived API key
40
+ // into host configuration. A valid portal URL is enough to attempt the
41
+ // short-lived JWT flow; getSynaluxJwt() remains the fail-closed auth gate.
42
+ if (!baseUrl) {
37
43
  return FALLBACK_SCENARIO;
38
44
  }
39
45
  const jwt = await getSynaluxJwt();
@@ -42,7 +48,7 @@ async function buildScenarioText(args) {
42
48
  return FALLBACK_SCENARIO;
43
49
  }
44
50
  try {
45
- const url = `${PRISM_SYNALUX_BASE_URL}/api/v1/prism/verify-behavior`;
51
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/prism/verify-behavior`;
46
52
  const res = await fetch(url, {
47
53
  method: "POST",
48
54
  headers: {
@@ -60,7 +66,11 @@ async function buildScenarioText(args) {
60
66
  console.error(`[verify-behavior] ⚠️ portal returned ${res.status} — fail-closed. URL: ${url}`);
61
67
  return FALLBACK_SCENARIO;
62
68
  }
63
- const data = (await res.json());
69
+ const data = await res.json();
70
+ if (!isVerifyBehaviorResult(data)) {
71
+ console.error("[verify-behavior] ⚠️ portal returned a malformed response — fail-closed");
72
+ return FALLBACK_SCENARIO;
73
+ }
64
74
  return formatResult(data);
65
75
  }
66
76
  catch (err) {
@@ -68,6 +78,12 @@ async function buildScenarioText(args) {
68
78
  return FALLBACK_SCENARIO;
69
79
  }
70
80
  }
81
+ function isVerifyBehaviorResult(value) {
82
+ return typeof value === "object" &&
83
+ value !== null &&
84
+ !Array.isArray(value) &&
85
+ typeof value.requires_verification === "boolean";
86
+ }
71
87
  function formatResult(data) {
72
88
  if (!data.requires_verification) {
73
89
  return JSON.stringify({ requires_verification: false, reason: data.reason || "non-behavioral file" });
@@ -142,29 +142,42 @@ function parseNativeSkillNames(value) {
142
142
  }
143
143
  }
144
144
  async function resolveNativeSkillManifestSnapshot(syncResult) {
145
- const { REQUIRED_NATIVE_SKILL_NAMES } = await import("./skillRouting.js");
145
+ const { FREE_NATIVE_SKILL_NAMES, REQUIRED_NATIVE_SKILL_NAMES } = await import("./skillRouting.js");
146
146
  const [storedNamesValue, storedTierValue] = await Promise.all([
147
147
  getSetting("skill_manifest:names", "[]"),
148
148
  getSetting("skill_manifest:tier", ""),
149
149
  ]);
150
- const partialNames = syncResult.status === "partial" && Array.isArray(syncResult.entitledNames)
151
- ? syncResult.entitledNames.filter((name) => typeof name === "string" && NATIVE_SKILL_NAME.test(name))
150
+ const partialNamesInput = syncResult.status === "partial" && Array.isArray(syncResult.entitledNames)
151
+ ? syncResult.entitledNames
152
+ : null;
153
+ const partialProvided = partialNamesInput !== null;
154
+ const partialNames = partialNamesInput
155
+ ? partialNamesInput.filter((name) => typeof name === "string" && NATIVE_SKILL_NAME.test(name))
152
156
  : [];
153
157
  const storedNames = parseNativeSkillNames(storedNamesValue);
154
- const names = partialNames.length > 0
155
- ? [...new Set(partialNames)]
156
- : storedNames.length > 0
157
- ? storedNames
158
- : [...REQUIRED_NATIVE_SKILL_NAMES];
159
- const source = partialNames.length > 0
160
- ? "validated-partial"
161
- : storedNames.length > 0
162
- ? "committed"
163
- : "protected-fallback";
164
158
  const candidateTier = syncResult.status === "partial" ? syncResult.tier : storedTierValue || syncResult.tier;
165
159
  const tier = typeof candidateTier === "string" && SYNALUX_TIERS.has(candidateTier)
166
160
  ? candidateTier
167
161
  : "free";
162
+ const tierFallbackNames = tier === "free"
163
+ ? FREE_NATIVE_SKILL_NAMES
164
+ : REQUIRED_NATIVE_SKILL_NAMES;
165
+ const freeNames = new Set(FREE_NATIVE_SKILL_NAMES);
166
+ const namesForTier = (names) => tier === "free"
167
+ ? names.filter((name) => freeNames.has(name))
168
+ : names;
169
+ const entitledPartialNames = namesForTier([...new Set(partialNames)]);
170
+ const entitledStoredNames = namesForTier(storedNames);
171
+ const names = partialProvided
172
+ ? entitledPartialNames.length > 0 ? entitledPartialNames : [...tierFallbackNames]
173
+ : entitledStoredNames.length > 0
174
+ ? entitledStoredNames
175
+ : [...tierFallbackNames];
176
+ const source = partialProvided
177
+ ? "validated-partial"
178
+ : entitledStoredNames.length > 0
179
+ ? "committed"
180
+ : "tier-fallback";
168
181
  return {
169
182
  names,
170
183
  tier,
@@ -216,10 +229,10 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
216
229
  `> - 🧠 **Context depth:** ${depth}\n` +
217
230
  `> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · native materialization incomplete${conflictSuffix}`;
218
231
  }
219
- if (snapshot.source === "protected-fallback") {
232
+ if (snapshot.source === "tier-fallback") {
220
233
  return `> **Prism System Ready**\n>\n` +
221
234
  `> - 🪪 **Subscription tier:** ${snapshot.tier}\n` +
222
- `> - 🛡️ **Protected fallback names:** ${formatBoundedSkillNames(coreSkills, "fallback")}\n` +
235
+ `> - 🛡️ **Fallback skill names:** ${formatBoundedSkillNames(snapshot.names, "fallback")}\n` +
223
236
  `> - 🧠 **Context depth:** ${depth}\n` +
224
237
  `> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · no committed manifest${conflictSuffix}`;
225
238
  }
@@ -341,38 +354,46 @@ export async function sessionSaveLedgerHandler(args) {
341
354
  role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
342
355
  });
343
356
  // ─── Fire-and-forget embedding generation ───
357
+ let embeddingQueued = false;
344
358
  if (result) {
345
359
  const embeddingText = [summary, ...(decisions || [])].join("\n");
346
360
  const savedEntry = Array.isArray(result) ? result[0] : result;
347
361
  const entryId = savedEntry?.id;
348
362
  if (entryId) {
349
- getLLMProvider().generateEmbedding(embeddingText)
350
- .then(async (embedding) => {
351
- // Build atomic patch — float32 + TurboQuant in ONE DB update
352
- const patchData = {
353
- embedding: JSON.stringify(embedding),
354
- };
355
- // TurboQuant: compress alongside float32 (non-fatal)
356
- try {
357
- const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
358
- const compressor = getDefaultCompressor();
359
- const compressed = compressor.compress(embedding);
360
- const buf = serialize(compressed);
361
- patchData.embedding_compressed = buf.toString("base64");
362
- patchData.embedding_format = `turbo${compressor.bits}`;
363
- patchData.embedding_turbo_radius = compressed.radius;
364
- debugLog(`[session_save_ledger] TurboQuant compressed: ${buf.length} bytes (${(3072 / buf.length).toFixed(1)}× ratio)`);
365
- }
366
- catch (turboErr) {
367
- console.error(`[session_save_ledger] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
368
- }
369
- // Single atomic DB update for all embedding data
370
- await storage.patchLedger(entryId, patchData);
371
- debugLog(`[session_save_ledger] Embedding saved for entry ${entryId}`);
372
- })
373
- .catch((err) => {
374
- console.error(`[session_save_ledger] Embedding generation failed (non-fatal): ${err.message}`);
375
- });
363
+ try {
364
+ const embeddingPromise = getLLMProvider().generateEmbedding(embeddingText);
365
+ embeddingQueued = true;
366
+ embeddingPromise
367
+ .then(async (embedding) => {
368
+ // Build atomic patch — float32 + TurboQuant in ONE DB update
369
+ const patchData = {
370
+ embedding: JSON.stringify(embedding),
371
+ };
372
+ // TurboQuant: compress alongside float32 (non-fatal)
373
+ try {
374
+ const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
375
+ const compressor = getDefaultCompressor();
376
+ const compressed = compressor.compress(embedding);
377
+ const buf = serialize(compressed);
378
+ patchData.embedding_compressed = buf.toString("base64");
379
+ patchData.embedding_format = `turbo${compressor.bits}`;
380
+ patchData.embedding_turbo_radius = compressed.radius;
381
+ debugLog(`[session_save_ledger] TurboQuant compressed: ${buf.length} bytes (${(3072 / buf.length).toFixed(1)}× ratio)`);
382
+ }
383
+ catch (turboErr) {
384
+ console.error(`[session_save_ledger] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
385
+ }
386
+ // Single atomic DB update for all embedding data
387
+ await storage.patchLedger(entryId, patchData);
388
+ debugLog(`[session_save_ledger] Embedding saved for entry ${entryId}`);
389
+ })
390
+ .catch((err) => {
391
+ console.error(`[session_save_ledger] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
392
+ });
393
+ }
394
+ catch (err) {
395
+ console.error(`[session_save_ledger] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
396
+ }
376
397
  }
377
398
  }
378
399
  // ─── v6.0 Phase 3: Fire-and-forget auto-linking ────────────
@@ -439,7 +460,9 @@ export async function sessionSaveLedgerHandler(args) {
439
460
  (todos?.length ? `TODOs: ${todos.length} items\n` : "") +
440
461
  (files_changed?.length ? `Files changed: ${files_changed.length}\n` : "") +
441
462
  (decisions?.length ? `Decisions: ${decisions.length}\n` : "") +
442
- `📊 Embedding generation queued for semantic search.` +
463
+ (embeddingQueued
464
+ ? `📊 Embedding generation queued for semantic search.`
465
+ : `📊 Primary history saved; optional semantic indexing was not queued.`) +
443
466
  metricsBlock +
444
467
  resolverNote,
445
468
  }],
@@ -599,9 +622,11 @@ export async function sessionSaveHandoffHandler(args, server) {
599
622
  }
600
623
  // ─── Success: handoff created or updated ───
601
624
  const newVersion = data.version;
602
- // ─── TIME MACHINE: Auto-snapshot for time travel (fire-and-forget) ───
625
+ // ─── TIME MACHINE: Auto-snapshot for time travel ───
603
626
  // Every successful save creates a snapshot so the user can revert later.
604
- // We don't await this should never block the success response.
627
+ // Await the attempt so short-lived CLI/MCP transports cannot exit before the
628
+ // snapshot reaches storage. Failure stays non-fatal because the primary
629
+ // handoff has already been durably saved.
605
630
  if (data.status === "created" || data.status === "updated") {
606
631
  const snapshotEntry = {
607
632
  project,
@@ -614,9 +639,15 @@ export async function sessionSaveHandoffHandler(args, server) {
614
639
  active_branch: active_branch ?? null,
615
640
  version: newVersion,
616
641
  };
617
- storage.saveHistorySnapshot(snapshotEntry).catch(err => console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`));
642
+ try {
643
+ await storage.saveHistorySnapshot(snapshotEntry);
644
+ }
645
+ catch (err) {
646
+ console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
647
+ }
618
648
  }
619
649
  // ─── Fire-and-forget embedding generation (enables semantic search on handoffs) ───
650
+ let embeddingQueued = false;
620
651
  if (data.status === "created" || data.status === "updated") {
621
652
  const embeddingText = [
622
653
  last_summary || "",
@@ -624,30 +655,37 @@ export async function sessionSaveHandoffHandler(args, server) {
624
655
  ...(open_todos || []),
625
656
  ].filter(Boolean).join("\n");
626
657
  if (embeddingText.trim()) {
627
- getLLMProvider().generateEmbedding(embeddingText)
628
- .then(async (embedding) => {
629
- const patchData = {
630
- embedding: JSON.stringify(embedding),
631
- };
632
- try {
633
- const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
634
- const compressor = getDefaultCompressor();
635
- const compressed = compressor.compress(embedding);
636
- const buf = serialize(compressed);
637
- patchData.embedding_compressed = buf.toString("base64");
638
- patchData.embedding_format = `turbo${compressor.bits}`;
639
- patchData.embedding_turbo_radius = compressed.radius;
640
- debugLog(`[session_save_handoff] TurboQuant compressed: ${buf.length} bytes`);
641
- }
642
- catch (turboErr) {
643
- console.error(`[session_save_handoff] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
644
- }
645
- await storage.patchHandoff(project, PRISM_USER_ID, patchData);
646
- debugLog(`[session_save_handoff] Embedding saved for project "${project}"`);
647
- })
648
- .catch((err) => {
649
- console.error(`[session_save_handoff] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
650
- });
658
+ try {
659
+ const embeddingPromise = getLLMProvider().generateEmbedding(embeddingText);
660
+ embeddingQueued = true;
661
+ embeddingPromise
662
+ .then(async (embedding) => {
663
+ const patchData = {
664
+ embedding: JSON.stringify(embedding),
665
+ };
666
+ try {
667
+ const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
668
+ const compressor = getDefaultCompressor();
669
+ const compressed = compressor.compress(embedding);
670
+ const buf = serialize(compressed);
671
+ patchData.embedding_compressed = buf.toString("base64");
672
+ patchData.embedding_format = `turbo${compressor.bits}`;
673
+ patchData.embedding_turbo_radius = compressed.radius;
674
+ debugLog(`[session_save_handoff] TurboQuant compressed: ${buf.length} bytes`);
675
+ }
676
+ catch (turboErr) {
677
+ console.error(`[session_save_handoff] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
678
+ }
679
+ await storage.patchHandoff(project, PRISM_USER_ID, patchData);
680
+ debugLog(`[session_save_handoff] Embedding saved for project "${project}"`);
681
+ })
682
+ .catch((err) => {
683
+ console.error(`[session_save_handoff] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
684
+ });
685
+ }
686
+ catch (err) {
687
+ console.error(`[session_save_handoff] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
688
+ }
651
689
  }
652
690
  }
653
691
  // ─── Trigger resource subscription notification ───
@@ -776,7 +814,9 @@ export async function sessionSaveHandoffHandler(args, server) {
776
814
  (last_summary ? `Last summary: ${last_summary}\n` : "") +
777
815
  (open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
778
816
  (active_branch ? `Active branch: ${active_branch}\n` : "") +
779
- `📊 Embedding generation queued for semantic search.\n` +
817
+ (embeddingQueued
818
+ ? `📊 Embedding generation queued for semantic search.\n`
819
+ : `📊 Primary history saved; optional semantic indexing was not queued.\n`) +
780
820
  metricsBlock +
781
821
  `\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
782
822
  `to maintain concurrency control.`);
@@ -1961,16 +2001,21 @@ export async function sessionSaveExperienceHandler(args) {
1961
2001
  const savedEntry = Array.isArray(result) ? result[0] : result;
1962
2002
  const entryId = savedEntry?.id;
1963
2003
  if (entryId) {
1964
- getLLMProvider().generateEmbedding(embeddingText)
1965
- .then(async (embedding) => {
1966
- await storage.patchLedger(entryId, {
1967
- embedding: JSON.stringify(embedding),
2004
+ try {
2005
+ getLLMProvider().generateEmbedding(embeddingText)
2006
+ .then(async (embedding) => {
2007
+ await storage.patchLedger(entryId, {
2008
+ embedding: JSON.stringify(embedding),
2009
+ });
2010
+ debugLog(`[session_save_experience] Embedding saved for entry ${entryId}`);
2011
+ })
2012
+ .catch((err) => {
2013
+ console.error(`[session_save_experience] Embedding failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
1968
2014
  });
1969
- debugLog(`[session_save_experience] Embedding saved for entry ${entryId}`);
1970
- })
1971
- .catch((err) => {
1972
- console.error(`[session_save_experience] Embedding failed (non-fatal): ${err.message}`);
1973
- });
2015
+ }
2016
+ catch (err) {
2017
+ console.error(`[session_save_experience] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2018
+ }
1974
2019
  }
1975
2020
  }
1976
2021
  return {
@@ -1839,7 +1839,8 @@ export const VERIFY_BEHAVIOR_TOOL = {
1839
1839
  description: "Call BEFORE editing behavioral source files (API routes, ordering logic, billing, auth, migrations). " +
1840
1840
  "Returns a domain-specific scenario you must answer to demonstrate understanding of the end-user impact. " +
1841
1841
  "Example: editing a KDS route returns 'A cook has a 3-item ticket. One item is voided. What should the cook see?' " +
1842
- "Answer the scenario concretely before proceeding with the edit.",
1842
+ "Answer the scenario concretely before proceeding with the edit. If the MCP transport is unavailable, use the " +
1843
+ "packaged `prism verify-behavior` CLI fallback; never fabricate a replacement scenario.",
1843
1844
  inputSchema: {
1844
1845
  type: "object",
1845
1846
  properties: {
@@ -22,11 +22,10 @@ export const REQUIRED_PROTECTED_SKILL_NAMES = [
22
22
  'pre-commit-protocol',
23
23
  'pre-push-audit',
24
24
  'implementation-integrity-audit',
25
- 'current-staging-acceptance',
26
25
  'local-inference-first',
27
26
  ];
28
27
  /**
29
- * Native skills that every subscription tier receives through `prism connect`.
28
+ * Native skills that paid subscription tiers receive through `prism connect`.
30
29
  *
31
30
  * `prism-startup` is deliberately not part of OFFLINE_FALLBACK: it tells the
32
31
  * host to call session_load_context, so injecting it back into that tool's
@@ -37,6 +36,8 @@ export const REQUIRED_NATIVE_SKILL_NAMES = [
37
36
  ...REQUIRED_PROTECTED_SKILL_NAMES,
38
37
  'prism-startup',
39
38
  ];
39
+ /** Public hook-free bootstrap package available without a paid entitlement. */
40
+ export const FREE_NATIVE_SKILL_NAMES = ['prism-startup'];
40
41
  export const OFFLINE_FALLBACK = {
41
42
  version: 1,
42
43
  universal: REQUIRED_PROTECTED_SKILL_NAMES.map((name, priority) => ({ name, priority, protected: true })),
@@ -41,7 +41,9 @@ const UNFENCED_PYTHON_START_RE = /^\s*(?:from\s+\S+\s+import|import\s+\S+|(?:asy
41
41
  const TRAILING_PROSE_LINE_RE = /^(?!(?:async\s+def|def|class|from|import|return|raise|yield|assert|del|global|nonlocal|if|elif|else|for|while|try|except|finally|with|match|case)\b)(?![A-Za-z_]\w*\s*=)[A-Za-z][A-Za-z'’-]*:?(?:\s+(?:[A-Za-z][A-Za-z'’+/-]*|`[^`\r\n]+`|https?:\/\/\S+|\d[\w.,%()+\-]*|[+*=<>-])){2,}[.!?]$/;
42
42
  const TRAILING_URL_LINE_RE = /^https?:\/\/\S+$/i;
43
43
  const PYTHON_INVALID_DEF_RE = /(?:^|\n)\s*(?:async\s+)?def\s+[A-Za-z_]\w*\s*:/m;
44
- const PYTHON_AST_SCRIPT = "import ast,sys; tree=ast.parse(sys.stdin.read()); compile(tree, '<prism-coding-gate>', 'exec')";
44
+ const PYTHON_READY_SENTINEL = "PRISM_PYTHON_READY";
45
+ const PYTHON_AST_SCRIPT = `import ast,sys; print('${PYTHON_READY_SENTINEL}', flush=True); ` +
46
+ "tree=ast.parse(sys.stdin.read()); compile(tree, '<prism-coding-gate>', 'exec')";
45
47
  const PYTHON_COMMANDS = ["python3", "python"];
46
48
  const PYTHON_CHILDREN_KEYS_UNPACK_RE = /\bfor\s+[A-Za-z_]\w*\s*,\s*child(?:_node)?\s+in\s+(?:sorted\(\s*)?[A-Za-z_][\w.]*\.children\.keys\(\)\s*\)?\s*:/;
47
49
  function extractUnfencedPythonCode(output) {
@@ -279,6 +281,12 @@ function pythonSyntaxFailure(code) {
279
281
  if (parsed.error && parsed.error.code === "ENOENT") {
280
282
  continue;
281
283
  }
284
+ const parserStarted = parsed.stdout
285
+ ?.split(/\r?\n/)
286
+ .includes(PYTHON_READY_SENTINEL) === true;
287
+ if (!parserStarted) {
288
+ continue;
289
+ }
282
290
  return parsed.status === 0 ? undefined : "python_syntax_error";
283
291
  }
284
292
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.2.8",
3
+ "version": "20.2.9",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
6
6
  "module": "index.ts",
@@ -22,7 +22,7 @@
22
22
  "prebuild": "npm run clean",
23
23
  "build": "tsc && npm run chmod-bins",
24
24
  "chmod-bins": "node -e \"['dist/cli.js','dist/server.js','dist/utils/universalImporter.js'].forEach(f => { try { require('fs').chmodSync(f, 0o755); } catch (e) { console.warn('chmod skipped', f, e.message); } })\"",
25
- "prepublishOnly": "npm run build",
25
+ "prepublishOnly": "node scripts/check-publish-clean.mjs && npm run build",
26
26
  "lint:dashboard": "node scripts/lint-dashboard-es5.cjs",
27
27
  "start": "node dist/server.js",
28
28
  "test": "vitest run",