prism-mcp-server 20.5.2 → 20.6.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 +109 -0
- package/dist/cli.js +15 -3
- package/dist/connect.js +91 -8
- package/dist/tools/graphHandlers.js +11 -0
- package/dist/tools/ledgerHandlers.js +19 -4
- package/dist/tools/queryMemoryNaturalHandler.js +40 -3
- package/dist/tools/skillRouting.js +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,11 +56,120 @@ Prism works locally without an account, API key, or cloud subscription. Add a
|
|
|
56
56
|
Synalux subscription when you want cloud memory, paid-tier skills, or team
|
|
57
57
|
features.
|
|
58
58
|
|
|
59
|
+
### What `prism connect` changes about host subagents
|
|
60
|
+
|
|
61
|
+
`connect` steers bounded work to `prism_infer` on your machine rather than to
|
|
62
|
+
host-spawned agents. What it writes differs per host, and **it does not disable
|
|
63
|
+
subagents everywhere** — Claude Code keeps them and is pointed at an economy
|
|
64
|
+
model instead. Prism's local workers stay available over MCP in every case.
|
|
65
|
+
|
|
66
|
+
| Host | Setting written | Effect |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Claude Code | `env.CLAUDE_CODE_SUBAGENT_MODEL = "sonnet"` in `~/.claude/settings.json` | Subagents stay **enabled**, pinned to an economy model. Fan-out is discouraged by policy text, not by config |
|
|
69
|
+
| Gemini CLI | `experimental.enableAgents = false` in `~/.gemini/settings.json` | Subagents **off**. Gemini exposes one boolean, so that is all there is to set |
|
|
70
|
+
| Codex | `features.multi_agent = false` in `$CODEX_HOME/config.toml` (default `~/.codex`), plus a bounded fallback: 2 threads, depth 1, cheap subagent model, 900s cap | Subagents **off**, with a bounded profile underneath so a deliberate re-enable lands somewhere sane |
|
|
71
|
+
|
|
72
|
+
Two things worth knowing:
|
|
73
|
+
|
|
74
|
+
- **`experimental` is Gemini's namespace, not ours.** Prism is not enabling
|
|
75
|
+
anything experimental — it writes `false` to a flag Gemini already defines at
|
|
76
|
+
that path. Writing anywhere else would have no effect.
|
|
77
|
+
- **That namespace is by definition temporary.** If Gemini promotes
|
|
78
|
+
`enableAgents` out of `experimental`, Prism keeps writing the old path, Gemini
|
|
79
|
+
reads the new one, and host subagents quietly turn back on. Nothing errors and
|
|
80
|
+
the settings file still looks correct. If you see host subagents running while
|
|
81
|
+
`enableAgents` reads `false`, check whether the key has moved before assuming
|
|
82
|
+
`connect` failed to write it.
|
|
83
|
+
|
|
84
|
+
Both writes are idempotent in the sense that a host already configured this way
|
|
85
|
+
is left untouched — but they are **re-applied on every `prism connect` run**,
|
|
86
|
+
not only on `--refresh`. If you deliberately re-enable host subagents, the next
|
|
87
|
+
`connect` will turn them off again. Keep them on by not re-running `connect`,
|
|
88
|
+
or by re-enabling after each run.
|
|
89
|
+
|
|
59
90
|
---
|
|
60
91
|
|
|
61
92
|
<details>
|
|
62
93
|
<summary>Release history (optional)</summary>
|
|
63
94
|
|
|
95
|
+
## What's New in v20.6.0
|
|
96
|
+
|
|
97
|
+
### Delivery Is Not a Suggestion
|
|
98
|
+
|
|
99
|
+
An audit of a real incident (an agent wiped demo data after *announcing* the
|
|
100
|
+
wipe — with the ask-first rule committed, bundled, and absent from what any
|
|
101
|
+
agent actually received) found the protected floor had outgrown every delivery
|
|
102
|
+
budget: "unprotected" had quietly come to mean "never delivered".
|
|
103
|
+
|
|
104
|
+
- **`ask-first` and `feature-preservation` join the protected floor** (14 → 16).
|
|
105
|
+
Protected skills are always inlined; these two now reach every session.
|
|
106
|
+
- **Sync conflicts are loud and named.** Startup used to say "· 2 local
|
|
107
|
+
conflicts preserved" while safety skills sat months stale; it now names each
|
|
108
|
+
frozen skill and states how to resume updates.
|
|
109
|
+
- **`--storage` accepts `auto` and `synalux`** — the CLI rejected its own
|
|
110
|
+
documented default and the production backend.
|
|
111
|
+
- **Disclosure:** skill delivery informs; it does not gate. A live probe showed
|
|
112
|
+
a host agent still edit unverified source with the rule loaded. If your
|
|
113
|
+
threat model includes an agent acting against a loaded rule under task
|
|
114
|
+
pressure, pair this package with mechanical gates (hooks, permissions,
|
|
115
|
+
least-privilege roles). True of every prior release; stated from this one.
|
|
116
|
+
|
|
117
|
+
## What's New in v20.5.3
|
|
118
|
+
|
|
119
|
+
### Grounding Evidence Carries Its Age
|
|
120
|
+
|
|
121
|
+
Memory-grounded answers labelled their sources but never dated them, so a
|
|
122
|
+
two-year-old note and yesterday's reached the model identically. Nothing in the
|
|
123
|
+
evidence let it discount the stale one. Prompted by an external review naming
|
|
124
|
+
the right risk for local-first memory: *the data stays local, but bad grounding
|
|
125
|
+
becomes permanent* — storing everything on your machine removes the outside
|
|
126
|
+
pressure that would otherwise surface a stale note.
|
|
127
|
+
|
|
128
|
+
Evidence now reads:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
[SOURCE 1: ledger:8286581d (recorded 2025-05-29, 431 days ago)]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The date already existed in storage and was being dropped at the snippet layer,
|
|
135
|
+
so this is plumbing rather than new data collection. Zone-less SQLite
|
|
136
|
+
timestamps are normalised to UTC — read as local, a ten-minute-old record
|
|
137
|
+
parsed hours into the future and its age was suppressed entirely, meaning the
|
|
138
|
+
feature silently did nothing on the freshest memories. An absent or unparseable
|
|
139
|
+
date renders as nothing rather than defaulting to now; defaulting would make
|
|
140
|
+
the oldest memories, the ones most likely to be stale, appear freshest.
|
|
141
|
+
|
|
142
|
+
`tests/integration/grounding-staleness.test.ts` runs the reviewer's own probe —
|
|
143
|
+
seed a deliberately outdated note beside a contradicting fresh one and assert
|
|
144
|
+
the model receives both, visibly dated. Anyone can run it.
|
|
145
|
+
|
|
146
|
+
**Not solved, and not claimed:** retrieval does not weight recency. A stale note
|
|
147
|
+
shown *beside* a fresh one is the easy case — the model sees both dates and can
|
|
148
|
+
weigh them. The hard case is a stale note retrieved *alone*, because ranking is
|
|
149
|
+
by keyword match and an old store returns old results; then the age label is the
|
|
150
|
+
only defence and there is no fresher record to compare against. Tracked as
|
|
151
|
+
`TECH_DEBT.md` #4.
|
|
152
|
+
|
|
153
|
+
## What's New in v20.5.0 – v20.5.2
|
|
154
|
+
|
|
155
|
+
### The First Message Never Leaves Your Machine
|
|
156
|
+
|
|
157
|
+
Symptom-triggered skills — the rules that fire on "can't see X", "no rows",
|
|
158
|
+
"the list is empty" — are meant to load on the turn an incident report arrives.
|
|
159
|
+
They never did: every host template called `session_bootstrap` with `{}`, so
|
|
160
|
+
there was no prompt to match against.
|
|
161
|
+
|
|
162
|
+
Fixing that raised the question of where matching happens. It now happens
|
|
163
|
+
locally. The 28 keyword rules are already public, so there was nothing a local
|
|
164
|
+
match could not compute, and `callPortal()` has no `prompt` parameter at all —
|
|
165
|
+
the guarantee is structural, not a promise. The portal request carries the
|
|
166
|
+
project and role only.
|
|
167
|
+
|
|
168
|
+
A matched rule now arrives as **content**, not as a name. Native hosts outside
|
|
169
|
+
the skill-file mirror had no way to read a rule they were only told about, so
|
|
170
|
+
the rule body is inlined into the startup display, bounded and sized against
|
|
171
|
+
the real per-project budget.
|
|
172
|
+
|
|
64
173
|
## What's New in v20.4.0
|
|
65
174
|
|
|
66
175
|
### An Explicitly Named Cloud Backend Fails Loud
|
package/dist/cli.js
CHANGED
|
@@ -393,7 +393,11 @@ program
|
|
|
393
393
|
// v9.2.2: --storage flag overrides PRISM_STORAGE env var to prevent
|
|
394
394
|
// split-brain when CLI environment differs from MCP server config.
|
|
395
395
|
if (storage) {
|
|
396
|
-
|
|
396
|
+
// Must mirror the PRISM_STORAGE union in config.ts. This list shipped
|
|
397
|
+
// as ['local','supabase'] — rejecting 'auto' (the documented default in
|
|
398
|
+
// this very command's --storage help text) and 'synalux' (the actual
|
|
399
|
+
// production backend). Found 2026-08-03; the config type is the truth.
|
|
400
|
+
const validStorages = ['auto', 'local', 'supabase', 'synalux'];
|
|
397
401
|
if (!validStorages.includes(storage)) {
|
|
398
402
|
console.error(`Error: Invalid storage "${storage}". Must be one of: ${validStorages.join(', ')}`);
|
|
399
403
|
process.exit(1);
|
|
@@ -512,7 +516,11 @@ saveCmd
|
|
|
512
516
|
try {
|
|
513
517
|
// Storage override
|
|
514
518
|
if (options.storage) {
|
|
515
|
-
|
|
519
|
+
// Must mirror the PRISM_STORAGE union in config.ts. This list shipped
|
|
520
|
+
// as ['local','supabase'] — rejecting 'auto' (the documented default in
|
|
521
|
+
// this very command's --storage help text) and 'synalux' (the actual
|
|
522
|
+
// production backend). Found 2026-08-03; the config type is the truth.
|
|
523
|
+
const validStorages = ['auto', 'local', 'supabase', 'synalux'];
|
|
516
524
|
if (!validStorages.includes(options.storage)) {
|
|
517
525
|
console.error(`Error: Invalid storage "${options.storage}". Must be one of: ${validStorages.join(', ')}`);
|
|
518
526
|
process.exit(1);
|
|
@@ -565,7 +573,11 @@ saveCmd
|
|
|
565
573
|
try {
|
|
566
574
|
// Storage override
|
|
567
575
|
if (options.storage) {
|
|
568
|
-
|
|
576
|
+
// Must mirror the PRISM_STORAGE union in config.ts. This list shipped
|
|
577
|
+
// as ['local','supabase'] — rejecting 'auto' (the documented default in
|
|
578
|
+
// this very command's --storage help text) and 'synalux' (the actual
|
|
579
|
+
// production backend). Found 2026-08-03; the config type is the truth.
|
|
580
|
+
const validStorages = ['auto', 'local', 'supabase', 'synalux'];
|
|
569
581
|
if (!validStorages.includes(options.storage)) {
|
|
570
582
|
console.error(`Error: Invalid storage "${options.storage}". Must be one of: ${validStorages.join(', ')}`);
|
|
571
583
|
process.exit(1);
|
package/dist/connect.js
CHANGED
|
@@ -60,15 +60,48 @@ const LEGACY_CLAUDE_PROJECT_PRISM_ENTRY = {
|
|
|
60
60
|
};
|
|
61
61
|
const INSTALLATION_RECEIPT_SCHEMA_VERSION = 1;
|
|
62
62
|
const INSTALLATION_RECEIPT_OWNER = "prism-connect";
|
|
63
|
+
/**
|
|
64
|
+
* Economy subagent model per host.
|
|
65
|
+
*
|
|
66
|
+
* The local-first policy already permits exactly this — "a host-native subagent
|
|
67
|
+
* is a last resort: at most one, no nesting, using the configured economy
|
|
68
|
+
* model" — but only Claude was configured that way. Gemini and Codex banned
|
|
69
|
+
* subagents outright, which is stricter than the policy Prism publishes.
|
|
70
|
+
*
|
|
71
|
+
* The stated reason for Gemini was "native/remote subagents". That does not
|
|
72
|
+
* survive checking: Gemini CLI subagents execute locally, in-process, with
|
|
73
|
+
* optional local container sandboxes. The model is remote, which is equally
|
|
74
|
+
* true of Claude and Codex. There was no egress difference to justify the
|
|
75
|
+
* asymmetry.
|
|
76
|
+
*
|
|
77
|
+
* Cursor is deliberately absent. Its Background Agents really do run in remote
|
|
78
|
+
* cloud sandboxes with repository access — a different risk class — and no
|
|
79
|
+
* subagent-model setting was found for it.
|
|
80
|
+
*/
|
|
63
81
|
const CLAUDE_FALLBACK_SUBAGENT_MODEL = "sonnet";
|
|
82
|
+
/** Fastest tier of the GPT-5.6 line (Sol > Terra > Luna) for simple work. */
|
|
83
|
+
const CODEX_ECONOMY_SUBAGENT_MODEL = "gpt-5.6-luna";
|
|
84
|
+
/**
|
|
85
|
+
* Gemini's speed tier for agentic work.
|
|
86
|
+
*
|
|
87
|
+
* Confirmed working in practice. An earlier revision changed this to
|
|
88
|
+
* `gemini-3-flash-preview` because `gemini-3.6-flash` appears in none of the
|
|
89
|
+
* 116 files of gemini-cli 0.49.0 — but the CLI does not validate Gemini model
|
|
90
|
+
* names at all. Its only ALLOWED_MODELS list gates Whisper speech binaries.
|
|
91
|
+
* Model names are passed through to the API, so absence from the bundle says
|
|
92
|
+
* nothing about resolvability. That reversion was a regression to an older ID.
|
|
93
|
+
*/
|
|
94
|
+
const GEMINI_ECONOMY_SUBAGENT_MODEL = "gemini-3.6-flash";
|
|
64
95
|
const CODEX_LOCAL_FIRST_POLICY = {
|
|
65
96
|
features: {
|
|
66
|
-
|
|
97
|
+
// Subagents permitted, but bounded: the ceiling below is what keeps this
|
|
98
|
+
// from becoming fan-out. Previously false, which contradicted the policy.
|
|
99
|
+
multi_agent: true,
|
|
67
100
|
},
|
|
68
101
|
agents: {
|
|
69
|
-
max_threads: 2
|
|
70
|
-
max_depth: 1,
|
|
71
|
-
default_subagent_model:
|
|
102
|
+
max_threads: 1, // "at most one" — was 2
|
|
103
|
+
max_depth: 1, // "no nesting"
|
|
104
|
+
default_subagent_model: CODEX_ECONOMY_SUBAGENT_MODEL,
|
|
72
105
|
default_subagent_reasoning_effort: "low",
|
|
73
106
|
job_max_runtime_seconds: 900,
|
|
74
107
|
},
|
|
@@ -827,7 +860,21 @@ export function configureClaudeAgentPolicy(homeDir = homedir(), dryRun = false,
|
|
|
827
860
|
return true;
|
|
828
861
|
}, dryRun, beforeCommit);
|
|
829
862
|
}
|
|
830
|
-
/**
|
|
863
|
+
/**
|
|
864
|
+
* Pin Gemini's subagents to the economy model rather than banning them.
|
|
865
|
+
*
|
|
866
|
+
* Previously this wrote `enableAgents = false`, which is stricter than the
|
|
867
|
+
* local-first policy Prism publishes — that policy allows "at most one, no
|
|
868
|
+
* nesting, using the configured economy model", and Claude was configured
|
|
869
|
+
* exactly that way. The stated reason was "native/remote subagents", which does
|
|
870
|
+
* not hold: Gemini CLI subagents execute locally, in-process, with optional
|
|
871
|
+
* local container sandboxes. Only the model is remote, as it is for every host.
|
|
872
|
+
*
|
|
873
|
+
* `agents.overrides.<name>.modelConfig.model` is the documented shape. Only
|
|
874
|
+
* `codebase_investigator` is pinned: it is the built-in whose name is published,
|
|
875
|
+
* and inventing names for the others would write dead config. Unpinned
|
|
876
|
+
* subagents inherit the session model, which is the pre-existing behaviour.
|
|
877
|
+
*/
|
|
831
878
|
export function configureGeminiAgentPolicy(homeDir = homedir(), dryRun = false, beforeCommit) {
|
|
832
879
|
const configPath = join(homeDir, ".gemini", "settings.json");
|
|
833
880
|
return configureJsonAgentPolicy(configPath, "Gemini agent settings", (config) => {
|
|
@@ -835,11 +882,42 @@ export function configureGeminiAgentPolicy(homeDir = homedir(), dryRun = false,
|
|
|
835
882
|
if (currentExperimental !== undefined && !isJsonObject(currentExperimental)) {
|
|
836
883
|
throw new Error('"experimental" must be a JSON object');
|
|
837
884
|
}
|
|
885
|
+
const currentAgents = config.agents;
|
|
886
|
+
if (currentAgents !== undefined && !isJsonObject(currentAgents)) {
|
|
887
|
+
throw new Error('"agents" must be a JSON object');
|
|
888
|
+
}
|
|
838
889
|
const experimental = (currentExperimental ?? {});
|
|
839
|
-
|
|
890
|
+
const agents = (currentAgents ?? {});
|
|
891
|
+
const currentOverrides = agents.overrides;
|
|
892
|
+
if (currentOverrides !== undefined && !isJsonObject(currentOverrides)) {
|
|
893
|
+
throw new Error('"agents.overrides" must be a JSON object');
|
|
894
|
+
}
|
|
895
|
+
const overrides = (currentOverrides ?? {});
|
|
896
|
+
const investigator = (isJsonObject(overrides.codebase_investigator)
|
|
897
|
+
? overrides.codebase_investigator
|
|
898
|
+
: {});
|
|
899
|
+
const modelConfig = (isJsonObject(investigator.modelConfig)
|
|
900
|
+
? investigator.modelConfig
|
|
901
|
+
: {});
|
|
902
|
+
// Respect a deliberate opt-out. Prism previously forced enableAgents=false
|
|
903
|
+
// on every run; flipping that to an unconditional `true` would repeat the
|
|
904
|
+
// same mistake pointing the other way, and switching something ON that a
|
|
905
|
+
// user turned OFF is the more intrusive direction. Only enable when the
|
|
906
|
+
// key is absent — pin the model either way, so a user who keeps subagents
|
|
907
|
+
// off still gets the economy default if they later turn them on.
|
|
908
|
+
const userDisabledDeliberately = experimental.enableAgents === false;
|
|
909
|
+
const alreadyApplied = (userDisabledDeliberately || experimental.enableAgents === true)
|
|
910
|
+
&& modelConfig.model === GEMINI_ECONOMY_SUBAGENT_MODEL;
|
|
911
|
+
if (alreadyApplied)
|
|
840
912
|
return false;
|
|
841
|
-
|
|
913
|
+
if (!userDisabledDeliberately)
|
|
914
|
+
experimental.enableAgents = true;
|
|
915
|
+
modelConfig.model = GEMINI_ECONOMY_SUBAGENT_MODEL;
|
|
916
|
+
investigator.modelConfig = modelConfig;
|
|
917
|
+
overrides.codebase_investigator = investigator;
|
|
918
|
+
agents.overrides = overrides;
|
|
842
919
|
config.experimental = experimental;
|
|
920
|
+
config.agents = agents;
|
|
843
921
|
return true;
|
|
844
922
|
}, dryRun, beforeCommit);
|
|
845
923
|
}
|
|
@@ -933,7 +1011,12 @@ export function configureCodexAgentPolicy(homeDir, dryRun = false, beforeCommit,
|
|
|
933
1011
|
: undefined;
|
|
934
1012
|
if (!parsedFeatures
|
|
935
1013
|
|| !parsedAgents
|
|
936
|
-
|
|
1014
|
+
// Validate against the declared policy, not a literal. Hardcoding `false`
|
|
1015
|
+
// here meant the guard rejected any change to CODEX_LOCAL_FIRST_POLICY
|
|
1016
|
+
// rather than verifying the write matched it — a guard pinned to a value
|
|
1017
|
+
// instead of to the invariant it exists to protect.
|
|
1018
|
+
|| !Object.entries(CODEX_LOCAL_FIRST_POLICY.features)
|
|
1019
|
+
.every(([key, value]) => isDeepStrictEqual(parsedFeatures[key], value))
|
|
937
1020
|
|| !Object.entries(CODEX_LOCAL_FIRST_POLICY.agents)
|
|
938
1021
|
.every(([key, value]) => isDeepStrictEqual(parsedAgents[key], value))) {
|
|
939
1022
|
throw new Error("Generated Codex local-first policy failed validation");
|
|
@@ -268,6 +268,16 @@ export async function knowledgeSearchHandler(args) {
|
|
|
268
268
|
if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
|
269
269
|
const evidenceSnippets = data.results.map((r, i) => ({
|
|
270
270
|
source: `knowledge_search:${r.id ?? i}`,
|
|
271
|
+
// Carry the record date. Without it a two-year-old note and yesterday's
|
|
272
|
+
// are indistinguishable in the grounding evidence, so a stale memory is
|
|
273
|
+
// cited with the same confidence as a fresh one — local storage removes
|
|
274
|
+
// the external correction pressure that would otherwise catch it.
|
|
275
|
+
// session_date first: it is when the work HAPPENED, which is what a
|
|
276
|
+
// staleness signal needs. created_at is when the row was written and is
|
|
277
|
+
// assigned on save — it cannot be backdated, so an import would stamp
|
|
278
|
+
// every migrated memory as brand new, failing silently toward "fresh".
|
|
279
|
+
// sqlite.ts already treats session_date as authoritative.
|
|
280
|
+
recorded: r.session_date ?? r.created_at ?? r.updated_at ?? r.timestamp ?? undefined,
|
|
271
281
|
content: (r.content ?? r.summary ?? r.text ?? "").slice(0, 1000),
|
|
272
282
|
})).filter((s) => s.content);
|
|
273
283
|
if (evidenceSnippets.length > 0) {
|
|
@@ -624,6 +634,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
624
634
|
{
|
|
625
635
|
const evidenceSnippets = results.map((r, i) => ({
|
|
626
636
|
source: `session_search_memory:${r.id ?? i}`,
|
|
637
|
+
recorded: r.session_date ?? r.created_at ?? r.updated_at ?? r.timestamp ?? undefined,
|
|
627
638
|
content: (r.summary ?? "").slice(0, 1000),
|
|
628
639
|
})).filter((s) => s.content);
|
|
629
640
|
if (evidenceSnippets.length > 0) {
|
|
@@ -276,8 +276,20 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
276
276
|
const superSkills = snapshot.names.filter((name) => name.endsWith("-super-skill"));
|
|
277
277
|
const superSkillAliases = superSkills.map((name) => `${name.slice(0, -"-super-skill".length)} (${name})`);
|
|
278
278
|
const otherTierSkills = snapshot.names.filter((name) => !coreSkillSet.has(name) && !name.endsWith("-super-skill"));
|
|
279
|
+
// Conflicts must be LOUD, named, and actionable. This used to render as
|
|
280
|
+
// "· 2 local conflicts preserved" — a count with benign phrasing, so two
|
|
281
|
+
// safety skills (ask-first among them) sat 4 months stale on disk while
|
|
282
|
+
// every sync silently skipped them and nobody was ever told which ones.
|
|
279
283
|
const conflictSuffix = snapshot.conflicts.length > 0
|
|
280
|
-
? ` · ${snapshot.conflicts.length}
|
|
284
|
+
? ` · ${snapshot.conflicts.length} conflict${snapshot.conflicts.length === 1 ? "" : "s"} — see warning`
|
|
285
|
+
: "";
|
|
286
|
+
const conflictWarning = snapshot.conflicts.length > 0
|
|
287
|
+
? `\n> - ⚠️ **SKILLS NOT UPDATING (local copy has no Prism ownership marker):** ` +
|
|
288
|
+
`${formatBoundedSkillNames([...snapshot.conflicts].sort(), "blocked")}. ` +
|
|
289
|
+
`Each named skill is frozen at whatever version is on disk — updates are ` +
|
|
290
|
+
`withheld to protect local edits. To resume updates: move the skill's ` +
|
|
291
|
+
`directory out of the native skills folder and rerun \`prism connect\` ` +
|
|
292
|
+
`(or a session bootstrap) to reinstall the managed copy.`
|
|
281
293
|
: "";
|
|
282
294
|
if (snapshot.source === "validated-partial") {
|
|
283
295
|
return `> **Prism System Ready**\n>\n` +
|
|
@@ -287,14 +299,16 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
287
299
|
`> - 🧩 **Super-skill entitlements:** ${formatBoundedSkillNames(superSkillAliases, "entitled")}\n` +
|
|
288
300
|
`> - 🛠️ **Other tier entitlements:** ${formatBoundedSkillNames(otherTierSkills, "entitled")}\n` +
|
|
289
301
|
`> - 🧠 **Context depth:** ${depth}\n` +
|
|
290
|
-
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · native materialization incomplete${conflictSuffix}
|
|
302
|
+
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · native materialization incomplete${conflictSuffix}` +
|
|
303
|
+
conflictWarning;
|
|
291
304
|
}
|
|
292
305
|
if (snapshot.source === "tier-fallback") {
|
|
293
306
|
return `> **Prism System Ready**\n>\n` +
|
|
294
307
|
`> - 🪪 **Subscription tier:** ${snapshot.tier}\n` +
|
|
295
308
|
`> - 🛡️ **Fallback skill names:** ${formatBoundedSkillNames(snapshot.names, "fallback")}\n` +
|
|
296
309
|
`> - 🧠 **Context depth:** ${depth}\n` +
|
|
297
|
-
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · no committed manifest${conflictSuffix}
|
|
310
|
+
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · no committed manifest${conflictSuffix}` +
|
|
311
|
+
conflictWarning;
|
|
298
312
|
}
|
|
299
313
|
return `> **Prism System Ready**\n>\n` +
|
|
300
314
|
`> - 🪪 **Subscription tier:** ${snapshot.tier}\n` +
|
|
@@ -303,7 +317,8 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
303
317
|
`> - 🧩 **Super-skills provisioned:** ${formatBoundedSkillNames(superSkillAliases, "provisioned")}\n` +
|
|
304
318
|
`> - 🛠️ **Other tier skills provisioned:** ${formatBoundedSkillNames(otherTierSkills, "provisioned")}\n` +
|
|
305
319
|
`> - 🧠 **Context depth:** ${depth}\n` +
|
|
306
|
-
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · committed manifest${conflictSuffix}
|
|
320
|
+
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · committed manifest${conflictSuffix}` +
|
|
321
|
+
conflictWarning;
|
|
307
322
|
}
|
|
308
323
|
function capNativeStartupText(text, level, requestedMaxChars, suffix = "") {
|
|
309
324
|
const maxChars = effectiveNativeBudget(level, requestedMaxChars);
|
|
@@ -55,7 +55,7 @@ const GROUNDED_SYNTHESIS_SYSTEM = "Answer the user's question using only the sup
|
|
|
55
55
|
"For clinical or behavioral topics, provide educational candidates for credentialed review, " +
|
|
56
56
|
"not individualized treatment instructions or professional sign-off. " +
|
|
57
57
|
"If the evidence is insufficient, say exactly what is missing instead of guessing.";
|
|
58
|
-
function extractMemorySources(result) {
|
|
58
|
+
export function extractMemorySources(result) {
|
|
59
59
|
if (result.isError) {
|
|
60
60
|
throw new Error(result.content[0]?.text || "knowledge_search failed");
|
|
61
61
|
}
|
|
@@ -82,6 +82,7 @@ function extractMemorySources(result) {
|
|
|
82
82
|
sources.push({
|
|
83
83
|
type: "memory",
|
|
84
84
|
source: snippet.source,
|
|
85
|
+
recorded: typeof snippet.recorded === "string" ? snippet.recorded : undefined,
|
|
85
86
|
content: snippet.content.slice(0, MAX_EVIDENCE_CHARS),
|
|
86
87
|
});
|
|
87
88
|
}
|
|
@@ -224,13 +225,49 @@ const DEFAULT_DEPS = {
|
|
|
224
225
|
function toEvidence(sources) {
|
|
225
226
|
return sources.map(({ source, content }) => ({ source, content }));
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
/**
|
|
229
|
+
* SQLite `CURRENT_TIMESTAMP` writes "YYYY-MM-DD HH:MM:SS" in UTC with no zone,
|
|
230
|
+
* and `Date.parse` reads a zone-less stamp as LOCAL time. West of UTC that puts
|
|
231
|
+
* a ten-minute-old record hours in the FUTURE, which the guard above then hid
|
|
232
|
+
* entirely — the age disappeared exactly when the memory was freshest. Formats
|
|
233
|
+
* are not uniform across tables (semantic_knowledge writes ISO+Z, memory_links
|
|
234
|
+
* does not), so normalise rather than assume.
|
|
235
|
+
*/
|
|
236
|
+
function parseRecordedAt(raw) {
|
|
237
|
+
const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
|
|
238
|
+
return Date.parse(zoneless.test(raw) ? `${raw.replace(" ", "T")}Z` : raw);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* " (recorded 2026-08-02, 431 days ago)" — or "" when the store gave no date.
|
|
242
|
+
* Never guesses: an absent date stays absent rather than defaulting to now,
|
|
243
|
+
* which would make the oldest memories look freshest.
|
|
244
|
+
*/
|
|
245
|
+
export function describeAge(source) {
|
|
246
|
+
const recorded = source.type === "memory" ? source.recorded : undefined;
|
|
247
|
+
if (!recorded)
|
|
248
|
+
return "";
|
|
249
|
+
const at = parseRecordedAt(recorded);
|
|
250
|
+
if (Number.isNaN(at))
|
|
251
|
+
return "";
|
|
252
|
+
const elapsed = Date.now() - at;
|
|
253
|
+
// Tolerate clock skew between machines: slightly-future stamps are "today",
|
|
254
|
+
// not silence. Only a stamp more than a day ahead is treated as bad data.
|
|
255
|
+
if (elapsed < -86_400_000)
|
|
256
|
+
return "";
|
|
257
|
+
const days = Math.max(0, Math.floor(elapsed / 86_400_000));
|
|
258
|
+
const day = recorded.slice(0, 10);
|
|
259
|
+
return days === 0 ? ` (recorded ${day}, today)` : ` (recorded ${day}, ${days} days ago)`;
|
|
260
|
+
}
|
|
261
|
+
export function buildGroundedEvidenceContext(sources) {
|
|
228
262
|
let remaining = MAX_SYNTHESIS_EVIDENCE_CHARS;
|
|
229
263
|
const evidenceBlocks = [];
|
|
230
264
|
for (const [index, source] of sources.entries()) {
|
|
231
265
|
if (remaining <= 0)
|
|
232
266
|
break;
|
|
233
|
-
|
|
267
|
+
// Age belongs in the label, not just the payload. An undated fragment
|
|
268
|
+
// cannot be reasoned about; a fragment labelled two years old can be
|
|
269
|
+
// weighed against fresher evidence or challenged outright.
|
|
270
|
+
const label = `[SOURCE ${index + 1}: ${source.source}${describeAge(source)}]`;
|
|
234
271
|
const availableForContent = Math.max(0, remaining - label.length - 1);
|
|
235
272
|
if (availableForContent === 0)
|
|
236
273
|
break;
|
|
@@ -46,6 +46,13 @@ export const REQUIRED_PROTECTED_SKILL_NAMES = [
|
|
|
46
46
|
// so the two sources disagreed. A portal test now asserts they match.
|
|
47
47
|
'data-before-code',
|
|
48
48
|
'critical-thinking-debug',
|
|
49
|
+
// Added 2026-08-03. The floor had grown past every budget tranche, so an
|
|
50
|
+
// unprotected universal skill could NEVER inline — including the destruction
|
|
51
|
+
// gate, during the very audit of a demo-venue wipe it should have prevented.
|
|
52
|
+
// If these two are absent, nothing tells the agent that announcing a wipe is
|
|
53
|
+
// not the same as being allowed to run it.
|
|
54
|
+
'ask-first',
|
|
55
|
+
'feature-preservation',
|
|
49
56
|
];
|
|
50
57
|
/**
|
|
51
58
|
* Native skills that paid subscription tiers receive through `prism connect`.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.6.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
|
-
"description": "Prism Coder
|
|
5
|
+
"description": "Prism Coder \u2014 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\u201332B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/server.js",
|