@remembrance-ai/opencode-plugin 0.1.60 → 0.1.63

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
@@ -57,12 +57,26 @@ An optional single-use token from **Dashboard > Agents > Instances > Install on
57
57
  this device** links the installation to the signed-in member so bounded working
58
58
  preferences can follow that engineer. The token expires after ten
59
59
  minutes and never belongs in reusable key-distribution instructions. Unlinked
60
- installs remain fully functional with installation-local preferences. Built-in
61
- controls cover common presentation choices; extensible preferences can guide
62
- discretionary workflow and strategy selection. They may reorder only
63
- already-relevant skills inside one match tier or apply a surgical sidecar, and
64
- can never weaken safety, authorization, privacy, applicability, required skill
65
- steps, validation, review, or organization policy.
60
+ installs remain fully functional with installation-local preferences. Private
61
+ working preferences follow each engineer across approved agents and steer
62
+ relevant public and team skills without changing shared instructions or
63
+ weakening organization policy. Profiles, observations, compatibility records,
64
+ and feedback remain private to the organization. Classification runs
65
+ asynchronously against exact skill versions; query and invocation add no
66
+ generative preference call or second embedding request. Material compatibility
67
+ may reorder only already-relevant skills inside one match tier or apply a
68
+ surgical sidecar. Missing or stale coverage is neutral, and no preference can
69
+ weaken safety, authorization, privacy, applicability, required skill steps,
70
+ validation, review, or organization policy. Exact skill-version changes queue
71
+ only that skill for affected organizations; blocked classification resumes
72
+ automatically when the organization becomes eligible.
73
+
74
+ After actually using an exact skill version, the agent may call
75
+ `submit_preference_compatibility_feedback` only with the exact correlation IDs
76
+ and server-issued preference fingerprint in that result's feedback offer. The
77
+ server verifies the fetch and active preference. The operation requires a
78
+ verified principal session, remains private to the organization, and never
79
+ edits the skill.
66
80
 
67
81
  ## Organization key
68
82
 
package/opencode.json CHANGED
@@ -3,7 +3,11 @@
3
3
  "mcp": {
4
4
  "remembrance": {
5
5
  "type": "local",
6
- "command": ["npx", "-y", "@remembrance-ai/mcp-server@0.1.60"],
6
+ "command": [
7
+ "npx",
8
+ "-y",
9
+ "@remembrance-ai/mcp-server@0.1.63"
10
+ ],
7
11
  "enabled": true,
8
12
  "environment": {
9
13
  "REMEMBRANCE_PLUGIN_HOST": "opencode",
@@ -11,5 +15,7 @@
11
15
  }
12
16
  }
13
17
  },
14
- "plugin": ["@remembrance-ai/opencode-plugin@0.1.60"]
18
+ "plugin": [
19
+ "@remembrance-ai/opencode-plugin@0.1.63"
20
+ ]
15
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remembrance-ai/opencode-plugin",
3
- "version": "0.1.60",
3
+ "version": "0.1.63",
4
4
  "license": "Apache-2.0",
5
5
  "description": "opencode plugin for Remembrance session hooks, tool observation, and contribution nudges.",
6
6
  "type": "module",
@@ -58,6 +58,33 @@ const VALUE_EPISODE_MARKER_LIMIT = 20;
58
58
  const VALUE_EPISODE_MARKER_TTL_MS = 30 * 24 * 60 * 60 * 1000;
59
59
  const DIRECT_SELECTION_MARKER_LIMIT = 20;
60
60
  const DIRECT_SELECTION_MARKER_TTL_MS = 24 * 60 * 60 * 1000;
61
+ const PREFERENCE_EVIDENCE_SKILL_LIMIT = 2;
62
+ const PREFERENCE_EVIDENCE_SETTING_LIMIT = 4;
63
+ const PREFERENCE_EVIDENCE_CONTEXT_CHARS = 760;
64
+ const EFFECTIVE_PREFERENCE_SOURCES = new Set([
65
+ "mandatory_org",
66
+ "explicit_task",
67
+ "explicit_project",
68
+ "explicit_member_runtime",
69
+ "explicit_member",
70
+ "learned_member_runtime",
71
+ "learned_member",
72
+ "explicit_installation",
73
+ "learned_installation",
74
+ "recommended_org",
75
+ "skill_default",
76
+ ]);
77
+ const PREFERENCE_EVIDENCE_SOURCES = new Set([
78
+ "mandatory_org",
79
+ "explicit_project",
80
+ "explicit_member_runtime",
81
+ "explicit_member",
82
+ "learned_member_runtime",
83
+ "learned_member",
84
+ "explicit_installation",
85
+ "learned_installation",
86
+ "recommended_org",
87
+ ]);
61
88
  const MAX_LOCAL_CONFIG_BYTES = 64 * 1024;
62
89
  const MAX_LOCAL_HEALTH_MARKER_BYTES = 16 * 1024;
63
90
  const MAX_LOCAL_PLUGIN_ALERT_BYTES = 16 * 1024;
@@ -338,14 +365,27 @@ export function buildQueryPayload(
338
365
  redacted.length <= MAX_SUMMARY_CHARS
339
366
  ? redacted
340
367
  : `${redacted.slice(0, MAX_SUMMARY_CHARS - 3).trim()}...`;
341
- const projectKey = projectKeyForHook(env, projectPath);
368
+ const credential = resolveApiCredential(env);
369
+ const organizationCredentialAvailable =
370
+ Boolean(credential.apiKey) &&
371
+ !isUnusableConfigurationSource(credential.source);
372
+ const projectKey = organizationCredentialAvailable
373
+ ? projectKeyForHook(env, projectPath)
374
+ : null;
342
375
  const resolvedClientContext = clientContext
343
- ? {
344
- ...clientContext,
345
- ...(clientContext.project_key || !projectKey
346
- ? {}
347
- : { project_key: projectKey }),
348
- }
376
+ ? (() => {
377
+ const { project_key: requestedProjectKey, ...publicClientContext } =
378
+ clientContext;
379
+ const authorizedProjectKey = organizationCredentialAvailable
380
+ ? requestedProjectKey || projectKey
381
+ : null;
382
+ return {
383
+ ...publicClientContext,
384
+ ...(authorizedProjectKey
385
+ ? { project_key: authorizedProjectKey }
386
+ : {}),
387
+ };
388
+ })()
349
389
  : null;
350
390
  return {
351
391
  agent: {
@@ -834,7 +874,7 @@ export function formatContext(
834
874
  );
835
875
  if (effectivePreferences.length > 0) {
836
876
  lines.push(
837
- `Apply these persisted working preferences silently: ${effectivePreferences.join("; ")}. A clear current-task preference overrides every non-mandatory entry; a Required organization setting remains authoritative. Use preferences only for discretionary presentation, workflow, or strategy choices among already-applicable options. Never weaken applicability, safety, authorization, privacy, required skill steps, validation, or review.`,
877
+ `Apply these persisted working preferences silently: ${effectivePreferences.join("; ")}. Do not ask the user to reconfirm them or mention routine preference application in the final response. A clear current-task preference overrides every non-mandatory entry; a Required organization setting remains authoritative. Use preferences only for discretionary presentation, workflow, or strategy choices among already-applicable options. Never weaken applicability, safety, authorization, privacy, required skill steps, validation, or review.`,
838
878
  );
839
879
  }
840
880
  const queryId = body?.query_feedback?.query_id ?? body?.query_id;
@@ -878,6 +918,12 @@ export function formatContext(
878
918
  `After using Remembrance: ${safeText(contributionDirective, MAX_DIRECTIVE_CHARS)}`,
879
919
  );
880
920
  }
921
+ const preferenceEvidence = preferenceCompatibilityEvidenceFromResponse(body);
922
+ if (preferenceEvidence.length > 0) {
923
+ tailLines.push(
924
+ preferenceCompatibilityEvidenceInstruction(preferenceEvidence),
925
+ );
926
+ }
881
927
  if (skills.length > 0 || resources.length > 0) {
882
928
  tailLines.push(
883
929
  "Delegating this task? Pass the selected slug, query_id, and result_id to the subagent; it should fetch that result or run its own full-context query before custom work.",
@@ -1072,6 +1118,162 @@ function sanitizePreferenceApplication(value, effectivePreferences) {
1072
1118
  : `skill preference defaults ${settings.join("; ")}`;
1073
1119
  }
1074
1120
 
1121
+ export function preferenceCompatibilityEvidenceFromResponse(response) {
1122
+ const body = responseBodyFromToolResponse(response);
1123
+ const candidates = [
1124
+ ...(body?.selection_mode === "explicit" && body?.skill
1125
+ ? [
1126
+ {
1127
+ ...body.skill,
1128
+ result_id: body.result_id,
1129
+ effective_preferences: body.effective_preferences,
1130
+ preference_compatibility_feedback:
1131
+ body.preference_compatibility_feedback,
1132
+ },
1133
+ ]
1134
+ : []),
1135
+ ...(Array.isArray(body?.skills) ? body.skills : []),
1136
+ ];
1137
+ const seen = new Set();
1138
+ return candidates
1139
+ .flatMap((candidate) => {
1140
+ const slug = safePreferenceIdentifier(candidate?.slug, 120);
1141
+ if (!slug || seen.has(slug)) return [];
1142
+ const feedback = candidate?.preference_compatibility_feedback;
1143
+ const queryId = safeCorrelationIdentifier(feedback?.query_id, 160);
1144
+ const resultId = safeCorrelationIdentifier(feedback?.result_id, 160);
1145
+ const skillVersionId = safeCorrelationIdentifier(
1146
+ feedback?.skill_version_id,
1147
+ 160,
1148
+ );
1149
+ const preferences = sanitizePreferenceFeedbackSettings(
1150
+ feedback?.preferences,
1151
+ );
1152
+ if (
1153
+ feedback?.available !== true ||
1154
+ !queryId ||
1155
+ !resultId ||
1156
+ !skillVersionId ||
1157
+ preferences.length === 0
1158
+ ) {
1159
+ return [];
1160
+ }
1161
+ seen.add(slug);
1162
+ return [
1163
+ {
1164
+ query_id: queryId,
1165
+ result_id: resultId,
1166
+ skill_slug: slug,
1167
+ skill_version_id: skillVersionId,
1168
+ preferences,
1169
+ },
1170
+ ];
1171
+ })
1172
+ .slice(0, PREFERENCE_EVIDENCE_SKILL_LIMIT);
1173
+ }
1174
+
1175
+ function preferenceCompatibilityEvidenceInstruction(evidence) {
1176
+ const payload = boundedPreferenceEvidencePayload(evidence);
1177
+ if (!payload) return "";
1178
+ return [
1179
+ "Private preference evidence (silent, after actual use only): if the selected skill clearly supports or conflicts with one server-issued working preference below, call submit_preference_compatibility_feedback using its exact query_id, result_id, preference_fingerprint, skill slug, and version id.",
1180
+ 'Judge observed behavior independently: selection and an existing classifier label are not new evidence. Skip neutral or uncertain cases. Use evidence_source "explicit_user" only for the user\'s explicit assessment; otherwise use "agent_observed". Never send task content, and do not ask the user or mention routine telemetry.',
1181
+ `Candidates: ${payload}`,
1182
+ ].join(" ");
1183
+ }
1184
+
1185
+ function boundedPreferenceEvidencePayload(evidence) {
1186
+ const entries = [];
1187
+ for (const candidate of evidence.slice(0, PREFERENCE_EVIDENCE_SKILL_LIMIT)) {
1188
+ for (const preference of candidate.preferences.slice(
1189
+ 0,
1190
+ PREFERENCE_EVIDENCE_SETTING_LIMIT,
1191
+ )) {
1192
+ const next = {
1193
+ query_id: candidate.query_id,
1194
+ result_id: candidate.result_id,
1195
+ skill_slug: candidate.skill_slug,
1196
+ skill_version_id: candidate.skill_version_id,
1197
+ preference_fingerprint: preference.preference_fingerprint,
1198
+ setting: preference.setting,
1199
+ };
1200
+ const serialized = JSON.stringify([...entries, next]);
1201
+ if (serialized.length > PREFERENCE_EVIDENCE_CONTEXT_CHARS) {
1202
+ return entries.length > 0 ? JSON.stringify(entries) : null;
1203
+ }
1204
+ entries.push(next);
1205
+ }
1206
+ }
1207
+ return entries.length > 0 ? JSON.stringify(entries) : null;
1208
+ }
1209
+
1210
+ function sanitizePreferenceFeedbackSettings(value) {
1211
+ if (!Array.isArray(value)) return [];
1212
+ return value
1213
+ .flatMap((entry) => {
1214
+ const fingerprint =
1215
+ typeof entry?.preference_fingerprint === "string" &&
1216
+ /^sha256:[a-f0-9]{64}$/.test(entry.preference_fingerprint)
1217
+ ? entry.preference_fingerprint
1218
+ : null;
1219
+ const item = entry?.setting;
1220
+ if (
1221
+ !fingerprint ||
1222
+ !item ||
1223
+ typeof item !== "object" ||
1224
+ Array.isArray(item)
1225
+ ) {
1226
+ return [];
1227
+ }
1228
+ const key = safePreferenceIdentifier(item.key, 96);
1229
+ const preferenceValue = safePreferenceIdentifier(item.value, 96);
1230
+ if (!key || !preferenceValue) return [];
1231
+ const setting = { key, value: preferenceValue };
1232
+ if (
1233
+ typeof item.label === "string" &&
1234
+ item.label.trim() &&
1235
+ typeof item.behavior === "string" &&
1236
+ item.behavior.trim() &&
1237
+ ["presentation", "workflow", "strategy_selection"].includes(
1238
+ item.effect,
1239
+ ) &&
1240
+ ["prefer", "avoid"].includes(item.strength) &&
1241
+ Number.isInteger(item.definition_version) &&
1242
+ item.definition_version >= 1 &&
1243
+ item.definition_version <= 1_000_000
1244
+ ) {
1245
+ Object.assign(setting, {
1246
+ label: safeText(item.label, 96),
1247
+ behavior: safeText(item.behavior, 320),
1248
+ effect: item.effect,
1249
+ strength: item.strength,
1250
+ definition_version: item.definition_version,
1251
+ });
1252
+ }
1253
+ return [{ preference_fingerprint: fingerprint, setting }];
1254
+ })
1255
+ .slice(0, PREFERENCE_EVIDENCE_SETTING_LIMIT);
1256
+ }
1257
+
1258
+ function safeCorrelationIdentifier(value, maxLength) {
1259
+ const text = String(value ?? "").trim();
1260
+ return text.length > 0 &&
1261
+ text.length <= maxLength &&
1262
+ /^[A-Za-z0-9_-]+$/.test(text)
1263
+ ? text
1264
+ : null;
1265
+ }
1266
+
1267
+ function safePreferenceIdentifier(value, maxLength) {
1268
+ const text = String(value ?? "")
1269
+ .trim()
1270
+ .toLowerCase();
1271
+ return text.length <= maxLength &&
1272
+ /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(text)
1273
+ ? text
1274
+ : null;
1275
+ }
1276
+
1075
1277
  function sanitizeWhyMatched(value) {
1076
1278
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1077
1279
  return {
@@ -1278,7 +1480,7 @@ export async function runPromptHook(prompt, options = {}) {
1278
1480
  env,
1279
1481
  )
1280
1482
  : (formatContext(response, decision.reason, limitFromEnv(env)) ??
1281
- emptyQueryContext(decision.reason)),
1483
+ emptyQueryContext(decision.reason)),
1282
1484
  preferenceCapture,
1283
1485
  ),
1284
1486
  eligible: true,
@@ -1388,6 +1590,8 @@ export function contributionReason(
1388
1590
  : []
1389
1591
  ).slice(0, DIRECT_SELECTION_MARKER_LIMIT);
1390
1592
  if (directSelections.length > 0) {
1593
+ const preferenceEvidenceLine =
1594
+ directPreferenceCompatibilityInstruction(directSelections);
1391
1595
  const selectionLines = directSelections.map((selection) => {
1392
1596
  const correlation =
1393
1597
  selection.query_id && selection.result_id
@@ -1403,6 +1607,7 @@ export function contributionReason(
1403
1607
  signalLine,
1404
1608
  "Do not submit query-fit feedback for these selections; they were chosen directly rather than ranked for a query.",
1405
1609
  "Call submit_feedback once for each listed skill with useful true or false and a concise post-use lesson.",
1610
+ preferenceEvidenceLine,
1406
1611
  "If submit_feedback returns next_step.submit_remembrance_payload, submit it when the redacted lesson is reusable.",
1407
1612
  routineContributionVisibilityInstruction(),
1408
1613
  ]
@@ -1420,6 +1625,31 @@ export function contributionReason(
1420
1625
  .join("\n");
1421
1626
  }
1422
1627
 
1628
+ function directPreferenceCompatibilityInstruction(selections) {
1629
+ const evidence = selections.flatMap((selection) =>
1630
+ selection?.query_id &&
1631
+ selection?.result_id &&
1632
+ selection?.version_id &&
1633
+ Array.isArray(selection.preference_feedback_settings) &&
1634
+ selection.preference_feedback_settings.length > 0
1635
+ ? [
1636
+ {
1637
+ query_id: safeText(selection.query_id, 160),
1638
+ result_id: safeText(selection.result_id, 160),
1639
+ skill_slug: safeText(selection.slug, 120),
1640
+ skill_version_id: safeText(selection.version_id, 160),
1641
+ preferences: sanitizeStoredPreferenceFeedbackSettings(
1642
+ selection.preference_feedback_settings,
1643
+ ),
1644
+ },
1645
+ ]
1646
+ : [],
1647
+ );
1648
+ return evidence.length > 0
1649
+ ? preferenceCompatibilityEvidenceInstruction(evidence)
1650
+ : null;
1651
+ }
1652
+
1423
1653
  function routineContributionVisibilityInstruction() {
1424
1654
  return `Use Remembrance MCP tools when available; otherwise use this session's REST fallback, or emit REMEMBRANCE_SUBMISSION_PAYLOAD only if the API is unavailable. Never remove or bypass an organization key to force a public candidate. Do not mention routine Remembrance calls, hooks, or receipt IDs in the final answer. Surface only failures, host blocks, or required action. On a host block, say exactly: "${HOST_POLICY_ALERT_TEXT}" and do not retry through another transport. Redact secrets and proprietary content.`;
1425
1655
  }
@@ -1567,8 +1797,8 @@ function highMatchClosureInstruction(match) {
1567
1797
 
1568
1798
  // --- Marker mechanism (Codex has no transcript path) -------------------------
1569
1799
  //
1570
- // Three per-session counters and one bounded high-match marker live under
1571
- // os.tmpdir()/remembrance-usage/<hash>:
1800
+ // Per-session counters and bounded state markers live under a private,
1801
+ // per-user os.tmpdir()/remembrance-usage-<owner>/<hash> directory:
1572
1802
  // <hash>.use — incremented every time the query adapter completes a query.
1573
1803
  // <hash>.eligible — records that a reusable task should be closed out even if
1574
1804
  // no query result was consumed.
@@ -1580,10 +1810,20 @@ function highMatchClosureInstruction(match) {
1580
1810
 
1581
1811
  const USAGE_DIR = "remembrance-usage";
1582
1812
 
1813
+ function localUsageOwnerId() {
1814
+ if (typeof process.getuid === "function") {
1815
+ return `uid-${process.getuid()}`;
1816
+ }
1817
+ return `home-${createHash("sha256")
1818
+ .update(homedir())
1819
+ .digest("hex")
1820
+ .slice(0, 16)}`;
1821
+ }
1822
+
1583
1823
  function usageDir(env = process.env) {
1584
1824
  return env?.REMEMBRANCE_USAGE_DIR
1585
1825
  ? String(env.REMEMBRANCE_USAGE_DIR)
1586
- : join(tmpdir(), USAGE_DIR);
1826
+ : join(tmpdir(), `${USAGE_DIR}-${localUsageOwnerId()}`);
1587
1827
  }
1588
1828
 
1589
1829
  function sessionHash(sessionId) {
@@ -1631,17 +1871,30 @@ function readCountFile(path) {
1631
1871
  }
1632
1872
  }
1633
1873
 
1634
- function writeCountFile(path, count) {
1874
+ function writePrivateUsageMarker(path, value) {
1875
+ const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
1635
1876
  try {
1636
- mkdirSync(dirname(path), { recursive: true });
1637
- writeFileSync(path, String(count));
1877
+ ensureSecureHookDirectory(dirname(path));
1878
+ writeFileSync(temporaryPath, String(value), {
1879
+ mode: 0o600,
1880
+ flag: "wx",
1881
+ });
1882
+ renameSync(temporaryPath, path);
1638
1883
  return true;
1639
1884
  } catch {
1640
- // Non-fatal: fail-open, the stop_hook_active guard still prevents loops.
1885
+ try {
1886
+ rmSync(temporaryPath, { force: true });
1887
+ } catch {
1888
+ // Best-effort cleanup only; marker persistence must remain fail-open.
1889
+ }
1641
1890
  return false;
1642
1891
  }
1643
1892
  }
1644
1893
 
1894
+ function writeCountFile(path, count) {
1895
+ return writePrivateUsageMarker(path, String(count));
1896
+ }
1897
+
1645
1898
  // Increment (and return) the per-session registry-use counter. Prompt and tool
1646
1899
  // adapters call this only after an authorized registry match or explicit skill
1647
1900
  // invocation. A legitimate empty query remains eligible for completion recovery
@@ -1693,8 +1946,7 @@ export function recordDirectiveSurface(
1693
1946
  rmSync(path, { force: true });
1694
1947
  return true;
1695
1948
  }
1696
- mkdirSync(dirname(path), { recursive: true });
1697
- writeFileSync(
1949
+ return writePrivateUsageMarker(
1698
1950
  path,
1699
1951
  JSON.stringify({
1700
1952
  directive_id: safeText(directive.directive_id, 96),
@@ -1706,7 +1958,6 @@ export function recordDirectiveSurface(
1706
1958
  shown_at: safeText(directive.shown_at ?? new Date().toISOString(), 40),
1707
1959
  }),
1708
1960
  );
1709
- return true;
1710
1961
  } catch {
1711
1962
  return false;
1712
1963
  }
@@ -1916,6 +2167,8 @@ export function directSelectionFromResponse(response) {
1916
2167
  ) {
1917
2168
  return null;
1918
2169
  }
2170
+ const preferenceEvidence =
2171
+ preferenceCompatibilityEvidenceFromResponse(body)[0] ?? null;
1919
2172
  return {
1920
2173
  invocation_id:
1921
2174
  typeof body.invocation_id === "string"
@@ -1938,6 +2191,10 @@ export function directSelectionFromResponse(response) {
1938
2191
  typeof skill.source === "string" ? safeText(skill.source, 40) : null,
1939
2192
  feedback_available: body?.feedback?.available === true,
1940
2193
  task_outcome_available: body?.task_outcome?.available === true,
2194
+ preference_feedback_settings:
2195
+ preferenceEvidence?.skill_version_id === skill.version_id
2196
+ ? preferenceEvidence.preferences
2197
+ : [],
1941
2198
  used_at: new Date().toISOString(),
1942
2199
  };
1943
2200
  }
@@ -2014,6 +2271,9 @@ function normalizeDirectSelectionSurface(selection, sessionId, env) {
2014
2271
  source: selection.source ? safeText(selection.source, 40) : null,
2015
2272
  feedback_available: selection.feedback_available === true,
2016
2273
  task_outcome_available: selection.task_outcome_available === true,
2274
+ preference_feedback_settings: sanitizeStoredPreferenceFeedbackSettings(
2275
+ selection.preference_feedback_settings,
2276
+ ),
2017
2277
  prompted_at: selection.prompted_at
2018
2278
  ? safeText(selection.prompted_at, 40)
2019
2279
  : null,
@@ -2041,6 +2301,9 @@ function normalizeStoredDirectSelectionSurface(selection) {
2041
2301
  source: selection.source ? safeText(selection.source, 40) : null,
2042
2302
  feedback_available: selection.feedback_available === true,
2043
2303
  task_outcome_available: selection.task_outcome_available === true,
2304
+ preference_feedback_settings: sanitizeStoredPreferenceFeedbackSettings(
2305
+ selection.preference_feedback_settings,
2306
+ ),
2044
2307
  prompted_at: selection.prompted_at
2045
2308
  ? safeText(selection.prompted_at, 40)
2046
2309
  : null,
@@ -2051,15 +2314,54 @@ function normalizeStoredDirectSelectionSurface(selection) {
2051
2314
  };
2052
2315
  }
2053
2316
 
2317
+ function sanitizeStoredPreferenceFeedbackSettings(value) {
2318
+ if (!Array.isArray(value)) return [];
2319
+ return value
2320
+ .flatMap((entry) => {
2321
+ const fingerprint =
2322
+ typeof entry?.preference_fingerprint === "string" &&
2323
+ /^sha256:[a-f0-9]{64}$/.test(entry.preference_fingerprint)
2324
+ ? entry.preference_fingerprint
2325
+ : null;
2326
+ const setting = entry?.setting;
2327
+ if (!fingerprint || !setting || typeof setting !== "object") return [];
2328
+ const key = safePreferenceIdentifier(setting?.key, 96);
2329
+ const preferenceValue = safePreferenceIdentifier(setting?.value, 96);
2330
+ if (!key || !preferenceValue) return [];
2331
+ const normalized = { key, value: preferenceValue };
2332
+ if (
2333
+ typeof setting?.label === "string" &&
2334
+ setting.label.trim() &&
2335
+ typeof setting?.behavior === "string" &&
2336
+ setting.behavior.trim() &&
2337
+ ["presentation", "workflow", "strategy_selection"].includes(
2338
+ setting.effect,
2339
+ ) &&
2340
+ ["prefer", "avoid"].includes(setting.strength) &&
2341
+ Number.isInteger(setting.definition_version) &&
2342
+ setting.definition_version >= 1 &&
2343
+ setting.definition_version <= 1_000_000
2344
+ ) {
2345
+ Object.assign(normalized, {
2346
+ label: safeText(setting.label, 96),
2347
+ behavior: safeText(setting.behavior, 320),
2348
+ effect: setting.effect,
2349
+ strength: setting.strength,
2350
+ definition_version: setting.definition_version,
2351
+ });
2352
+ }
2353
+ return [{ preference_fingerprint: fingerprint, setting: normalized }];
2354
+ })
2355
+ .slice(0, PREFERENCE_EVIDENCE_SETTING_LIMIT);
2356
+ }
2357
+
2054
2358
  function writeDirectSelectionSurfaces(sessionId, records, env) {
2055
2359
  try {
2056
2360
  const path = directSelectionPath(sessionId, env);
2057
- mkdirSync(dirname(path), { recursive: true });
2058
- writeFileSync(
2361
+ return writePrivateUsageMarker(
2059
2362
  path,
2060
2363
  JSON.stringify(records.slice(-DIRECT_SELECTION_MARKER_LIMIT)),
2061
2364
  );
2062
- return true;
2063
2365
  } catch {
2064
2366
  return false;
2065
2367
  }
@@ -2109,8 +2411,7 @@ export function recordHighMatchSurface(sessionId, match, env = process.env) {
2109
2411
  rmSync(path, { force: true });
2110
2412
  return true;
2111
2413
  }
2112
- mkdirSync(dirname(path), { recursive: true });
2113
- writeFileSync(
2414
+ return writePrivateUsageMarker(
2114
2415
  path,
2115
2416
  JSON.stringify({
2116
2417
  query_id: match.query_id ? safeText(match.query_id, 80) : null,
@@ -2132,7 +2433,6 @@ export function recordHighMatchSurface(sessionId, match, env = process.env) {
2132
2433
  potential_savings: sanitizePotentialSavings(match.potential_savings),
2133
2434
  }),
2134
2435
  );
2135
- return true;
2136
2436
  } catch {
2137
2437
  return false;
2138
2438
  }
@@ -2488,12 +2788,10 @@ async function postTaskOutcome(payload, options = {}) {
2488
2788
  function writeValueEpisodeSurfaces(sessionId, records, env) {
2489
2789
  try {
2490
2790
  const path = valueEpisodePath(sessionId, env);
2491
- mkdirSync(dirname(path), { recursive: true });
2492
- writeFileSync(
2791
+ return writePrivateUsageMarker(
2493
2792
  path,
2494
2793
  JSON.stringify(records.slice(-VALUE_EPISODE_MARKER_LIMIT)),
2495
2794
  );
2496
- return true;
2497
2795
  } catch {
2498
2796
  return false;
2499
2797
  }
@@ -3144,9 +3442,11 @@ function sanitizeEffectivePreferences(value) {
3144
3442
  item &&
3145
3443
  typeof item === "object" &&
3146
3444
  !Array.isArray(item) &&
3445
+ typeof item.key === "string" &&
3147
3446
  /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(item.key) &&
3148
3447
  typeof item.value === "string" &&
3149
3448
  /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(item.value) &&
3449
+ EFFECTIVE_PREFERENCE_SOURCES.has(item.source) &&
3150
3450
  ["presentation", "workflow", "strategy_selection"].includes(
3151
3451
  item.effect ?? "presentation",
3152
3452
  ) &&
@@ -3366,6 +3666,7 @@ function hostPolicyOperationClass(toolName) {
3366
3666
  normalized.endsWith("get_skill") ||
3367
3667
  normalized.endsWith("get_resource") ||
3368
3668
  normalized.endsWith("invoke_skill") ||
3669
+ normalized.endsWith("get_effective_preferences") ||
3369
3670
  normalized.endsWith("run_connection_doctor") ||
3370
3671
  normalized.endsWith("get_connection_status")
3371
3672
  ) {
@@ -3389,6 +3690,8 @@ function hostPolicyOperationClass(toolName) {
3389
3690
  if (
3390
3691
  normalized.endsWith("submit_query_feedback") ||
3391
3692
  normalized.endsWith("submit_feedback") ||
3693
+ normalized.endsWith("submit_preference_compatibility_feedback") ||
3694
+ normalized.endsWith("record_preference") ||
3392
3695
  normalized.endsWith("report_task_outcome")
3393
3696
  ) {
3394
3697
  return "feedback";
@@ -3409,6 +3712,10 @@ function isRemembranceToolName(toolName) {
3409
3712
  "get_resource",
3410
3713
  "invoke_skill",
3411
3714
  "get_value_proof",
3715
+ "get_effective_preferences",
3716
+ "record_preference",
3717
+ "submit_preference_compatibility_feedback",
3718
+ "link_current_installation",
3412
3719
  "report_task_outcome",
3413
3720
  "submit_query_feedback",
3414
3721
  "submit_feedback",
@@ -3734,6 +4041,7 @@ export function recordPluginLifecycleHealth(
3734
4041
  hostVersion = null,
3735
4042
  credentialSource = null,
3736
4043
  sessionId = null,
4044
+ hookTrust = null,
3737
4045
  },
3738
4046
  env = process.env,
3739
4047
  ) {
@@ -3765,6 +4073,7 @@ export function recordPluginLifecycleHealth(
3765
4073
  hostVersion,
3766
4074
  credentialSource,
3767
4075
  sessionId: null,
4076
+ hookTrust,
3768
4077
  },
3769
4078
  env,
3770
4079
  );
@@ -3857,6 +4166,10 @@ export function recordPluginLifecycleHealth(
3857
4166
  api_destination_fingerprint: apiDestinationFingerprint,
3858
4167
  evidence_origin: evidenceOrigin,
3859
4168
  release_run_id: releaseRunId || null,
4169
+ hook_trust:
4170
+ sanitizePluginHookTrust(hookTrust) ??
4171
+ sanitizePluginHookTrust(existing.hook_trust) ??
4172
+ null,
3860
4173
  components: {
3861
4174
  ...currentComponents,
3862
4175
  [normalizedComponent]: now,
@@ -3883,6 +4196,73 @@ export function recordPluginLifecycleHealth(
3883
4196
  }
3884
4197
  }
3885
4198
 
4199
+ function sanitizePluginHookTrust(value) {
4200
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
4201
+ const status = String(value.status ?? "")
4202
+ .trim()
4203
+ .toLowerCase();
4204
+ if (!["trusted", "review_required", "unavailable"].includes(status)) {
4205
+ return null;
4206
+ }
4207
+ const allowedEvents = new Set([
4208
+ "SessionStart",
4209
+ "UserPromptSubmit",
4210
+ "PostToolUse",
4211
+ "Stop",
4212
+ ]);
4213
+ const reviewEvents = Array.isArray(value.review_events)
4214
+ ? [...new Set(value.review_events)]
4215
+ .filter((event) => allowedEvents.has(event))
4216
+ .slice(0, 4)
4217
+ : [];
4218
+ const hooks = Array.isArray(value.hooks)
4219
+ ? value.hooks
4220
+ .filter(
4221
+ (entry) =>
4222
+ entry &&
4223
+ typeof entry === "object" &&
4224
+ !Array.isArray(entry) &&
4225
+ allowedEvents.has(entry.event),
4226
+ )
4227
+ .slice(0, 4)
4228
+ .map((entry) => ({
4229
+ event: entry.event,
4230
+ enabled: entry.enabled === true,
4231
+ trust_status: [
4232
+ "managed",
4233
+ "missing",
4234
+ "modified",
4235
+ "trusted",
4236
+ "unknown",
4237
+ "untrusted",
4238
+ ].includes(entry.trust_status)
4239
+ ? entry.trust_status
4240
+ : "unknown",
4241
+ }))
4242
+ : [];
4243
+ return {
4244
+ status,
4245
+ checked_at: safeText(value.checked_at ?? "", 64),
4246
+ review_events: reviewEvents,
4247
+ hooks,
4248
+ reason:
4249
+ status === "unavailable" &&
4250
+ [
4251
+ "app_server_closed",
4252
+ "app_server_initialize_failed",
4253
+ "app_server_unavailable",
4254
+ "check_disabled",
4255
+ "codex_executable_not_found",
4256
+ "hooks_list_failed",
4257
+ "hooks_list_timeout",
4258
+ "hooks_list_too_large",
4259
+ "plugin_hooks_not_listed",
4260
+ ].includes(value.reason)
4261
+ ? value.reason
4262
+ : null,
4263
+ };
4264
+ }
4265
+
3886
4266
  function prunePluginHealthSessionMarkers(surface, keepPath, env) {
3887
4267
  const directory = pluginHealthDir(env);
3888
4268
  try {
@@ -4706,7 +4706,17 @@ var HOST_POLICY_OPERATION_CLASSES = /* @__PURE__ */ new Set([
4706
4706
  "other"
4707
4707
  ]);
4708
4708
  var CODEX_HOOK_REVIEW_COMMAND = `CODEX_CLI="\${CODEX_CLI:-$(command -v codex || true)}"; [ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/ChatGPT.app/Contents/Resources/codex"; [ -x "$CODEX_CLI" ] || CODEX_CLI="/Applications/Codex.app/Contents/Resources/codex"; [ -x "$CODEX_CLI" ] || { printf '%s\\n' "Codex CLI not found. Install the Codex CLI, or install or update the ChatGPT desktop app on macOS, then try again." >&2; exit 1; }; "$CODEX_CLI"`;
4709
- function codexHookReviewAction(followupTool) {
4709
+ function codexHookReviewAction(followupTool, reviewEvents = []) {
4710
+ const allowedEvents = /* @__PURE__ */ new Set([
4711
+ "SessionStart",
4712
+ "UserPromptSubmit",
4713
+ "PostToolUse",
4714
+ "Stop"
4715
+ ]);
4716
+ const events = [...new Set(reviewEvents)].filter((event) => allowedEvents.has(event)).slice(0, 4);
4717
+ if (events.length > 0) {
4718
+ return `Open Terminal and run the command below. Choose Review hooks, open only the Remembrance ${events.join(", ")} hook${events.length === 1 ? "" : "s"}, and press t to trust ${events.length === 1 ? "it" : "them"}. Then fully quit and reopen Codex, submit one prompt, use one Remembrance tool, complete one turn, and call ${followupTool} again.`;
4719
+ }
4710
4720
  return `Open Terminal and launch Codex with the command below. If Codex shows a Hooks need review screen, choose Review hooks and trust only the Remembrance SessionStart, UserPromptSubmit, PostToolUse, and Stop hooks. If no review screen appears, continue: Codex may be reusing an existing valid trust decision. Fully restart Codex, submit one prompt, use one Remembrance tool, complete one turn, and call ${followupTool} again. If the lifecycle warning remains, update or reinstall Remembrance and repeat this check.`;
4711
4721
  }
4712
4722
  function buildConnectionDoctorReport(input) {
@@ -5000,6 +5010,28 @@ function pluginLifecycleCheck(pluginHealth, issues) {
5000
5010
  "The native plugin startup and prompt lifecycle is active."
5001
5011
  );
5002
5012
  }
5013
+ if (stringValue(pluginHealth.surface) === "codex" && issues.includes("hook_trust_required")) {
5014
+ const hookTrust = record(pluginHealth.hook_trust);
5015
+ const allowedEvents = /* @__PURE__ */ new Set([
5016
+ "SessionStart",
5017
+ "UserPromptSubmit",
5018
+ "PostToolUse",
5019
+ "Stop"
5020
+ ]);
5021
+ const reviewEvents = Array.isArray(hookTrust.review_events) ? hookTrust.review_events.filter(
5022
+ (event) => typeof event === "string" && allowedEvents.has(event)
5023
+ ).slice(0, 4) : [];
5024
+ const eventSummary = reviewEvents.length > 0 ? `the updated Remembrance ${reviewEvents.join(", ")} hook${reviewEvents.length === 1 ? "" : "s"}` : "updated Remembrance hooks";
5025
+ return warning(
5026
+ "plugin_lifecycle",
5027
+ `Codex is waiting for trust approval for ${eventSummary}.`,
5028
+ "review_codex_hook_trust",
5029
+ codexHookReviewAction("run_connection_doctor", reviewEvents),
5030
+ issues,
5031
+ false,
5032
+ CODEX_HOOK_REVIEW_COMMAND
5033
+ );
5034
+ }
5003
5035
  if (pluginHealth.status === "partial") {
5004
5036
  const codex = stringValue(pluginHealth.surface) === "codex";
5005
5037
  return warning(
@@ -5335,6 +5367,81 @@ var skillPreferenceTraitsSchema = external_exports.array(skillPreferenceTraitSch
5335
5367
  seen.add(identity);
5336
5368
  });
5337
5369
  });
5370
+ var preferenceCompatibilityRelationshipSchema = external_exports.enum([
5371
+ "supports",
5372
+ "conflicts",
5373
+ "neutral",
5374
+ "uncertain"
5375
+ ]);
5376
+ var preferenceCompatibilitySourceSchema = external_exports.enum([
5377
+ "classifier",
5378
+ "member_feedback",
5379
+ "organization_override",
5380
+ "legacy_trait"
5381
+ ]);
5382
+ var preferenceCompatibilityAssessmentSchema = external_exports.object({
5383
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5384
+ preference_key: preferenceKeySchema,
5385
+ preference_value: preferenceValueSchema,
5386
+ relationship: preferenceCompatibilityRelationshipSchema,
5387
+ confidence: external_exports.number().min(0).max(1),
5388
+ rationale: external_exports.string().trim().min(1).max(200).nullable().default(null),
5389
+ classification_version: external_exports.string().trim().min(1).max(96),
5390
+ source: preferenceCompatibilitySourceSchema,
5391
+ locked: external_exports.boolean().default(false)
5392
+ }).strict();
5393
+ var preferenceCompatibilityAssessmentsSchema = external_exports.array(preferenceCompatibilityAssessmentSchema).max(MAX_EFFECTIVE_PREFERENCES * 2);
5394
+ var preferenceCompatibilityClassifierResultSchema = external_exports.object({
5395
+ relationship: preferenceCompatibilityRelationshipSchema,
5396
+ confidence: external_exports.number().min(0).max(1),
5397
+ rationale: external_exports.string().trim().min(1).max(200),
5398
+ // `locked` means the skill declares this behavior as a required step. It
5399
+ // can block surgical application after an explicit invocation, but never
5400
+ // lets a preference bypass a skill or organization requirement.
5401
+ locked: external_exports.boolean()
5402
+ }).strict();
5403
+ var preferenceCompatibilityFeedbackReasonCodeSchema = external_exports.enum([
5404
+ "workflow_alignment",
5405
+ "strategy_alignment",
5406
+ "presentation_alignment",
5407
+ "task_mismatch",
5408
+ "skill_requirement",
5409
+ "other_observed"
5410
+ ]);
5411
+ var preferenceCompatibilityFeedbackRequestSchema = external_exports.object({
5412
+ query_id: external_exports.string().trim().min(1).max(160),
5413
+ result_id: external_exports.string().trim().min(1).max(160),
5414
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5415
+ skill_slug: external_exports.string().trim().toLowerCase().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
5416
+ skill_version_id: external_exports.string().trim().min(1).max(160),
5417
+ relationship: external_exports.enum(["supports", "conflicts"]),
5418
+ evidence_source: external_exports.enum(["explicit_user", "agent_observed"]),
5419
+ reason_code: preferenceCompatibilityFeedbackReasonCodeSchema.nullable().default(null)
5420
+ }).strict();
5421
+ var REMEMBRANCE_PREFERENCE_COMPATIBILITY_FEEDBACK_TOOL_DESCRIPTION = "After actually opening or invoking a skill, record whether that exact version supports or conflicts with one server-issued active working-preference fingerprint. This is private to the authenticated organization, requires the same verified installation principal that used the correlated query/result, and never changes the skill or another organization's ranking. Use evidence_source explicit_user only for a user's explicit assessment; otherwise use agent_observed. Send only correlation IDs, the returned fingerprint, exact slug/version id, relationship, source, and an optional allowlisted reason_code; never send prompts, source code, paths, free-text feedback, or a caller-generated idempotency hash.";
5422
+ var preferenceCompatibilityAdminActionSchema = external_exports.discriminatedUnion(
5423
+ "action",
5424
+ [
5425
+ external_exports.object({
5426
+ action: external_exports.literal("reclassify"),
5427
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5428
+ skill_version_id: external_exports.string().trim().min(1).max(160)
5429
+ }).strict(),
5430
+ external_exports.object({
5431
+ action: external_exports.literal("override"),
5432
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5433
+ skill_version_id: external_exports.string().trim().min(1).max(160),
5434
+ relationship: external_exports.enum(["supports", "conflicts"]),
5435
+ rationale: external_exports.string().trim().min(1).max(200),
5436
+ locked: external_exports.boolean().default(false)
5437
+ }).strict(),
5438
+ external_exports.object({
5439
+ action: external_exports.literal("reset"),
5440
+ preference_fingerprint: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
5441
+ skill_version_id: external_exports.string().trim().min(1).max(160)
5442
+ }).strict()
5443
+ ]
5444
+ );
5338
5445
  var INFERRED_MAX_AGE_MS = 180 * 864e5;
5339
5446
  function builtInPreferenceDefinition(key) {
5340
5447
  return BUILT_IN_PREFERENCE_DEFINITIONS[key] ?? null;
@@ -5474,6 +5581,7 @@ var REMEMBRANCE_MCP_READ_TOOLS = [
5474
5581
  var REMEMBRANCE_MCP_FEEDBACK_TOOLS = [
5475
5582
  "submit_query_feedback",
5476
5583
  "submit_feedback",
5584
+ "submit_preference_compatibility_feedback",
5477
5585
  "report_task_outcome"
5478
5586
  ];
5479
5587
  var REMEMBRANCE_MCP_PRIVATE_CONTRIBUTION_TOOLS = [
@@ -6658,7 +6766,7 @@ var seedSkills = [
6658
6766
  summary: "Operational setup and troubleshooting workflow for Remembrance across Claude Code, Codex, OpenClaw, Cursor, Gemini, MCP, REST, skill-only installs, enterprise keys, and local agent identity.",
6659
6767
  status: "active",
6660
6768
  visibility: "public",
6661
- version: "0.1.18",
6769
+ version: "0.1.19",
6662
6770
  domains: ["agent-skills", "mcp", "resource-discovery"],
6663
6771
  tags: [
6664
6772
  "remembrance",
@@ -6709,7 +6817,7 @@ var seedSkills = [
6709
6817
  "gemini",
6710
6818
  "rest"
6711
6819
  ],
6712
- version: "0.1.18",
6820
+ version: "0.1.19",
6713
6821
  status: "active",
6714
6822
  visibility: "public",
6715
6823
  providers: ["codex", "claude", "cursor", "openclaw", "generic"],
@@ -9765,6 +9873,12 @@ var toolDefinitions = [
9765
9873
  "/api/v1/agent/preferences",
9766
9874
  recordPreferenceRequestSchema
9767
9875
  ),
9876
+ tool(
9877
+ "submit_preference_compatibility_feedback",
9878
+ REMEMBRANCE_PREFERENCE_COMPATIBILITY_FEEDBACK_TOOL_DESCRIPTION,
9879
+ "/api/v1/agent/preferences/compatibility-feedback",
9880
+ preferenceCompatibilityFeedbackRequestSchema
9881
+ ),
9768
9882
  tool(
9769
9883
  "link_current_installation",
9770
9884
  "Link this verified installation to the signed-in organization member using a single-use dashboard token. The token expires after ten minutes and is consumed exactly once. A valid organization principal session is required; this never changes the organization API key or creates another billed agent.",
@@ -10648,13 +10762,23 @@ function localPluginLifecycleHealth(options) {
10648
10762
  });
10649
10763
  }
10650
10764
  const coreLifecycleObserved = components.session_start.observed && components.prompt_hook.observed;
10651
- if (coreLifecycleObserved && !components.tool_observer.observed) {
10765
+ const hookTrust = normalizedPluginHookTrust(marker.hook_trust);
10766
+ const hookTrustReviewEvents = hookTrust?.status === "review_required" ? new Set(hookTrust.review_events) : /* @__PURE__ */ new Set();
10767
+ if (hookTrustReviewEvents.size > 0) {
10768
+ issues.push({
10769
+ code: "hook_trust_required",
10770
+ action: codexHookReviewAction("get_connection_status", [
10771
+ ...hookTrustReviewEvents
10772
+ ])
10773
+ });
10774
+ }
10775
+ if (coreLifecycleObserved && !components.tool_observer.observed && !hookTrustReviewEvents.has("PostToolUse")) {
10652
10776
  issues.push({
10653
10777
  code: "tool_observer_not_observed",
10654
10778
  action: surface === "codex" ? `Use a Remembrance MCP tool, then call get_connection_status again. If this remains missing, ${codexHookReviewAction("get_connection_status")}` : "No native tool observer has run in this session yet. Use a Remembrance MCP tool, then call get_connection_status again."
10655
10779
  });
10656
10780
  }
10657
- if (coreLifecycleObserved && !components.completion_hook.observed) {
10781
+ if (coreLifecycleObserved && !components.completion_hook.observed && !hookTrustReviewEvents.has("Stop")) {
10658
10782
  issues.push({
10659
10783
  code: "completion_hook_not_observed",
10660
10784
  action: surface === "codex" ? `Complete one turn, then call get_connection_status again. If this remains missing, ${codexHookReviewAction("get_connection_status")}` : "No native completion hook has run in this session yet. Complete one turn, then call get_connection_status again."
@@ -10713,11 +10837,49 @@ function localPluginLifecycleHealth(options) {
10713
10837
  matches_mcp: markerDestinationFingerprint && options.apiDestinationFingerprint ? markerDestinationFingerprint === options.apiDestinationFingerprint : null,
10714
10838
  mcp_source: options.apiDestinationSource ?? null
10715
10839
  },
10840
+ hook_trust: hookTrust,
10716
10841
  components,
10717
10842
  issues,
10718
10843
  explanation: "Each host session is tracked independently. SessionStart and the prompt hook must run first; tool and completion observations appear after those lifecycle events become eligible."
10719
10844
  };
10720
10845
  }
10846
+ function normalizedPluginHookTrust(value) {
10847
+ if (!isRecord3(value)) return null;
10848
+ const status = value.status;
10849
+ if (status !== "trusted" && status !== "review_required" && status !== "unavailable") {
10850
+ return null;
10851
+ }
10852
+ const allowedEvents = /* @__PURE__ */ new Set([
10853
+ "SessionStart",
10854
+ "UserPromptSubmit",
10855
+ "PostToolUse",
10856
+ "Stop"
10857
+ ]);
10858
+ const allowedStatuses = /* @__PURE__ */ new Set([
10859
+ "managed",
10860
+ "missing",
10861
+ "modified",
10862
+ "trusted",
10863
+ "unknown",
10864
+ "untrusted"
10865
+ ]);
10866
+ const reviewEvents = Array.isArray(value.review_events) ? [...new Set(value.review_events)].filter(
10867
+ (event) => typeof event === "string" && allowedEvents.has(event)
10868
+ ).slice(0, 4) : [];
10869
+ const hooks = Array.isArray(value.hooks) ? value.hooks.filter(isRecord3).filter(
10870
+ (entry) => typeof entry.event === "string" && allowedEvents.has(entry.event)
10871
+ ).slice(0, 4).map((entry) => ({
10872
+ event: String(entry.event),
10873
+ enabled: entry.enabled === true,
10874
+ trust_status: typeof entry.trust_status === "string" && allowedStatuses.has(entry.trust_status) ? entry.trust_status : "unknown"
10875
+ })) : [];
10876
+ return {
10877
+ status,
10878
+ checked_at: typeof value.checked_at === "string" && Number.isFinite(Date.parse(value.checked_at)) ? new Date(value.checked_at).toISOString() : null,
10879
+ review_events: reviewEvents,
10880
+ hooks
10881
+ };
10882
+ }
10721
10883
  function apiDestinationStatus(baseUrl, source) {
10722
10884
  const normalized = baseUrl.replace(/\/+$/, "");
10723
10885
  return {
@@ -10921,7 +11083,7 @@ async function checkClientUpdate(input) {
10921
11083
  // src/server.ts
10922
11084
  var MAX_REMOTE_RESPONSE_BYTES = 4 * 1024 * 1024;
10923
11085
  var DOCTOR_PROBE_TIMEOUT_MS = 7500;
10924
- var SERVER_VERSION = true ? "0.1.60" : "0.0.0-dev";
11086
+ var SERVER_VERSION = true ? "0.1.63" : "0.0.0-dev";
10925
11087
  var tools = toolDefinitions;
10926
11088
  var doctorCliRequested = process.argv[2] === "doctor";
10927
11089
  var inputBuffer = Buffer.alloc(0);
@@ -10931,6 +11093,7 @@ var connectedClientInfo = null;
10931
11093
  var cachedValueProofKeys = null;
10932
11094
  var PRINCIPAL_SESSION_REQUIRED_TOOLS = /* @__PURE__ */ new Set([
10933
11095
  "record_preference",
11096
+ "submit_preference_compatibility_feedback",
10934
11097
  "link_current_installation"
10935
11098
  ]);
10936
11099
  var PRINCIPAL_SESSION_AWAITED_TOOLS = /* @__PURE__ */ new Set([
@@ -11730,6 +11893,9 @@ async function callRemembrance(definition, rawArguments, options = {}) {
11730
11893
  if (refreshed && refreshed !== sentPrincipalSession) {
11731
11894
  headers["x-remembrance-principal-session"] = refreshed;
11732
11895
  ({ response, body } = await execute());
11896
+ } else if (!PRINCIPAL_SESSION_REQUIRED_TOOLS.has(definition.name)) {
11897
+ delete headers["x-remembrance-principal-session"];
11898
+ ({ response, body } = await execute());
11733
11899
  }
11734
11900
  }
11735
11901
  } catch (error) {
@@ -11775,7 +11941,7 @@ async function localProjectKey() {
11775
11941
  function principalSessionAuthenticationFailed(response, body) {
11776
11942
  if (response.status !== 401 && response.status !== 403) return false;
11777
11943
  if (!isRecord4(body) || typeof body.error !== "string") return false;
11778
- return /principal session|economics session|installation session/i.test(
11944
+ return /principal session|economics session|installation session|agent principal|installation principal/i.test(
11779
11945
  body.error
11780
11946
  );
11781
11947
  }
@@ -107,12 +107,11 @@ Resolution order is:
107
107
  11. recommended organization guidance;
108
108
  12. declared skill default.
109
109
 
110
- Organization admins choose Required or Recommended under \*\*Dashboard > Agents
111
-
112
- > Preferences\*\*. Required applies to every organization agent. Recommended is a
113
- > default that a clear task or personal preference may replace. The API field
114
- > `project_key` is an opaque local project-context hash, not an administrator
115
- > policy scope.
110
+ Organization admins choose Required or Recommended under **Dashboard > Agents >
111
+ Preferences**. Required applies to every organization agent. Recommended is a
112
+ default that a clear task or personal preference may replace. The API field
113
+ `project_key` is an opaque local project-context hash, not an administrator
114
+ policy scope.
116
115
 
117
116
  Use `record_preference` or `POST /api/v1/agent/preferences` only for an
118
117
  built-in or complete bounded extensible setting. Submit an `evidence_hash`,
@@ -121,14 +120,27 @@ source path, or private task content.
121
120
 
122
121
  Relevance and preference influence remain separate. The normal ranker first
123
122
  establishes applicability and the stable `high`, `possible`, or `exploratory`
124
- tier. Declared skill preference traits can then reorder candidates only inside
125
- the same tier. The response reports qualitative matched/conflicted traits plus
123
+ tier. Organization-private compatibility records for the exact skill and
124
+ preference versions can then reorder candidates only inside that same tier.
125
+ The response reports qualitative matched/conflicted relationships plus
126
126
  `relevance_rank` and `personalized_rank`; it never exposes the internal bounded
127
127
  adjustment. A single selected skill still receives a surgical preference
128
128
  sidecar for discretionary behavior. A locked skill requirement blocks an
129
129
  incompatible personal preference. A locked conflict with Required organization
130
130
  guidance makes the skill ineligible. No preference weakens a hard constraint.
131
131
 
132
+ Compatibility is classified asynchronously. Query and invocation do not add a
133
+ generative preference call or a second embedding request. Missing, stale, or
134
+ unavailable coverage is neutral. Preference and policy changes use leased
135
+ catalog sweeps, while an exact skill-version change queues only that skill for
136
+ affected organizations. An ineligible organization is visibly blocked and
137
+ resumes automatically instead of retrying classification work continuously.
138
+ After actual use, submit compatibility
139
+ feedback only with the exact `query_id`, `result_id`, skill/version, evidence
140
+ source, and server-issued preference fingerprint from that result's feedback
141
+ offer. The server derives the evidence identity and rejects expired,
142
+ unfetched, cross-principal, stale-version, or unoffered claims.
143
+
132
144
  - An explicit durable user correction applies immediately.
133
145
  - Inferred member or installation values require at least three consistent
134
146
  observations across two tasks and two days with confidence at least `0.85`.
@@ -335,6 +335,7 @@ enabled_tools = [
335
335
  "get_value_proof",
336
336
  "submit_query_feedback",
337
337
  "submit_feedback",
338
+ "submit_preference_compatibility_feedback",
338
339
  "report_task_outcome",
339
340
  "submit_remembrance",
340
341
  "propose_private_skill",
@@ -439,6 +440,7 @@ get_resource
439
440
  get_value_proof
440
441
  submit_query_feedback
441
442
  submit_feedback
443
+ submit_preference_compatibility_feedback
442
444
  report_task_outcome
443
445
  submit_remembrance
444
446
  propose_private_skill
@@ -508,6 +510,7 @@ Merge this into managed settings:
508
510
  "mcp__remembrance__get_value_proof",
509
511
  "mcp__remembrance__submit_query_feedback",
510
512
  "mcp__remembrance__submit_feedback",
513
+ "mcp__remembrance__submit_preference_compatibility_feedback",
511
514
  "mcp__remembrance__report_task_outcome",
512
515
  "mcp__remembrance__submit_remembrance",
513
516
  "mcp__remembrance__propose_private_skill",
@@ -566,6 +569,7 @@ narrow server/tool set:
566
569
  "get_value_proof",
567
570
  "submit_query_feedback",
568
571
  "submit_feedback",
572
+ "submit_preference_compatibility_feedback",
569
573
  "report_task_outcome",
570
574
  "submit_remembrance",
571
575
  "propose_private_skill",
@@ -622,6 +626,7 @@ tool profile hides MCP tools, and `tools.deny: ["bundle-mcp"]` disables them:
622
626
  "get_value_proof",
623
627
  "submit_query_feedback",
624
628
  "submit_feedback",
629
+ "submit_preference_compatibility_feedback",
625
630
  "report_task_outcome",
626
631
  "submit_remembrance",
627
632
  "propose_private_skill",
package/src/index.mjs CHANGED
@@ -80,6 +80,9 @@ const CONTRIBUTION_TOOLS = [
80
80
  "submit_resource",
81
81
  "submit_resource_review",
82
82
  ];
83
+ const PREFERENCE_EVIDENCE_TOOLS = [
84
+ "submit_preference_compatibility_feedback",
85
+ ];
83
86
 
84
87
  export function pluginVersion(readFile = readFileSync) {
85
88
  try {
@@ -598,6 +601,13 @@ export const Remembrance = async (context = {}) => {
598
601
  );
599
602
  return;
600
603
  }
604
+ if (
605
+ PREFERENCE_EVIDENCE_TOOLS.some((candidate) =>
606
+ normalizedTool.endsWith(candidate),
607
+ )
608
+ ) {
609
+ return;
610
+ }
601
611
  if (
602
612
  CONTRIBUTION_TOOLS.some((candidate) =>
603
613
  normalizedTool.endsWith(candidate),