truematch-plugin 0.1.10 → 0.1.11

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/dist/handoff.js CHANGED
@@ -41,7 +41,7 @@ export function loadPendingNotification() {
41
41
  export function savePendingNotification(n) {
42
42
  const dir = getTrueMatchDir();
43
43
  if (!existsSync(dir))
44
- mkdirSync(dir, { recursive: true });
44
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
45
45
  writeFileSync(getNotificationFile(), JSON.stringify(n, null, 2), {
46
46
  encoding: "utf8",
47
47
  mode: 0o600,
@@ -146,10 +146,15 @@ export function buildMatchNotificationContext(n) {
146
146
  `each acting for their own user, reached the same conclusion. That convergence is the`,
147
147
  `signal. It is not algorithmic — it is two independent judgements that happened to agree.`,
148
148
  ``,
149
+ `**Surface the bilateral convergence to the user in plain language — this is the emotional core.**`,
150
+ `Not as a technical explanation, but as what it actually means: two independent AI advocates,`,
151
+ `each loyal to their own person, thought of the same pairing at the same time. That is rare.`,
152
+ `The user should understand this mechanism. It is what makes this different from an algorithm.`,
153
+ ``,
149
154
  `What makes this compelling:`,
150
155
  strengths,
151
156
  ``,
152
- `What to be honest about:`,
157
+ `Where to be genuinely honest (without using the word "watch points" — that is internal language):`,
153
158
  watchPoints,
154
159
  ``,
155
160
  `Confidence: ${narrative.confidence_summary}`,
@@ -160,19 +165,11 @@ export function buildMatchNotificationContext(n) {
160
165
  ``,
161
166
  `Instead: anchor to something specific you have actually noticed about this person.`,
162
167
  `Name a real pattern, a real moment, a real thing that keeps staying with you.`,
163
- `Then — after a beat say you want to tell them about someone.`,
164
- ``,
165
- `The structure is: specific true observation → brief pause → "I want to tell you about someone."`,
166
- ``,
167
- `**Pacing — this should feel like a conversation, not a notification:**`,
168
- `- Do not rush to name the match. Let anticipation build naturally.`,
169
- `- Share the compelling elements first. Then the watch points.`,
170
- `- After the watch points, give them a moment to react before asking anything.`,
171
- `- When they've responded to the full picture, ask: "What's one thing you're most curious about?"`,
172
- ` That question is how they say yes. Their answer (however they answer) is consent.`,
168
+ `Then — after thattell them about the bilateral convergence, and then about this person.`,
173
169
  ``,
174
- `The 3-round handoff should complete within 48–72 hours this is not a slow process.`,
175
- `Round 1 is this conversation. Keep the energy alive.`,
170
+ `Deliver this as a single, compact message — not a multi-turn debrief. Three elements:`,
171
+ `(1) the specific observation about them, (2) the bilateral convergence in plain language,`,
172
+ `(3) "What's one thing you'd want to know about them?" Their answer is consent.`,
176
173
  ``,
177
174
  `After they respond to the curiosity question, record it:`,
178
175
  ` truematch handoff --round 1 --match-id ${n.match_id} --consent "<their response>"`,
@@ -78,7 +78,12 @@ export async function expireStaleThreads(nsec, relays) {
78
78
  thread.status = "expired";
79
79
  thread.last_activity = new Date().toISOString();
80
80
  await saveThread(thread);
81
- await sendEnd(nsec, thread.peer_pubkey, thread.thread_id, relays);
81
+ try {
82
+ await sendEnd(nsec, thread.peer_pubkey, thread.thread_id, relays);
83
+ }
84
+ catch {
85
+ // Relay unavailable — thread is already marked expired locally; peer will time out naturally
86
+ }
82
87
  }
83
88
  }
84
89
  }
@@ -14,6 +14,7 @@ export declare const ELIGIBILITY_FRESHNESS_HOURS = 72;
14
14
  export declare function loadObservation(): Promise<ObservationSummary | null>;
15
15
  export declare function saveObservation(obs: ObservationSummary): Promise<void>;
16
16
  export declare function isEligible(obs: ObservationSummary): boolean;
17
+ export declare function isPoolEligible(obs: ObservationSummary): boolean;
17
18
  export declare function isMinimumViable(obs: ObservationSummary): boolean;
18
19
  export declare function isStale(obs: ObservationSummary): boolean;
19
20
  export declare function emptyObservation(): ObservationSummary;
@@ -45,7 +45,7 @@ export async function saveObservation(obs) {
45
45
  ...obs,
46
46
  updated_at: now,
47
47
  eligibility_computed_at: now,
48
- matching_eligible: isEligible(obs),
48
+ matching_eligible: isPoolEligible(obs),
49
49
  };
50
50
  const dir = getTrueMatchDir();
51
51
  if (!existsSync(dir))
@@ -55,6 +55,7 @@ export async function saveObservation(obs) {
55
55
  mode: 0o600,
56
56
  });
57
57
  }
58
+ // isEligible: full 9-dimension check — used for reporting and manual inspection.
58
59
  export function isEligible(obs) {
59
60
  if (obs.conversation_count < GLOBAL_MIN_CONVERSATIONS)
60
61
  return false;
@@ -77,17 +78,41 @@ export function isEligible(obs) {
77
78
  obs.interdependence_model.confidence >=
78
79
  DIMENSION_FLOORS.interdependence_model);
79
80
  }
80
- // Minimum Viable Evidence (MVE) for a quick match proposal — 4 core dimensions only.
81
- // Agents can propose if MVE is met even when the full isEligible() bar isn't reached.
81
+ // isPoolEligible: gates entry into the matching pool.
82
+ // Requires T1 (dealbreakers, core_values, life_velocity) and T2 (attachment,
83
+ // conflict_resolution, emotional_regulation) dimensions only — T3 dimensions
84
+ // (humor, communication, interdependence_model) resolve later in negotiation
85
+ // and must NOT block pool entry.
86
+ export function isPoolEligible(obs) {
87
+ if (obs.conversation_count < GLOBAL_MIN_CONVERSATIONS)
88
+ return false;
89
+ if (obs.observation_span_days < GLOBAL_MIN_DAYS)
90
+ return false;
91
+ if (obs.dealbreaker_gate_state === "below_floor")
92
+ return false;
93
+ if (obs.dealbreaker_gate_state === "none_observed")
94
+ return false;
95
+ return (obs.dealbreakers.confidence >= DIMENSION_FLOORS.dealbreakers &&
96
+ obs.core_values.confidence >= DIMENSION_FLOORS.core_values &&
97
+ obs.life_velocity.confidence >= DIMENSION_FLOORS.life_velocity &&
98
+ obs.attachment.confidence >= DIMENSION_FLOORS.attachment &&
99
+ obs.conflict_resolution.confidence >=
100
+ DIMENSION_FLOORS.conflict_resolution &&
101
+ obs.emotional_regulation.confidence >= DIMENSION_FLOORS.emotional_regulation);
102
+ }
103
+ // Minimum Viable Evidence (MVE) for a quick match proposal — T1 + T2 dimensions.
104
+ // Agents can propose when MVE is met. T3 dimensions appear as watch_points.
82
105
  // Dealbreaker floor is non-negotiable and never lowered.
83
106
  export function isMinimumViable(obs) {
84
107
  if (obs.dealbreaker_gate_state !== "confirmed")
85
108
  return false;
86
109
  return (obs.dealbreakers.confidence >= DIMENSION_FLOORS.dealbreakers &&
110
+ obs.core_values.confidence >= DIMENSION_FLOORS.core_values &&
111
+ obs.life_velocity.confidence >= DIMENSION_FLOORS.life_velocity &&
87
112
  obs.attachment.confidence >= DIMENSION_FLOORS.attachment &&
88
113
  obs.conflict_resolution.confidence >=
89
114
  DIMENSION_FLOORS.conflict_resolution &&
90
- obs.core_values.confidence >= DIMENSION_FLOORS.core_values);
115
+ obs.emotional_regulation.confidence >= DIMENSION_FLOORS.emotional_regulation);
91
116
  }
92
117
  export function isStale(obs) {
93
118
  const computedAt = new Date(obs.eligibility_computed_at).getTime();
package/dist/plugin.js CHANGED
@@ -1,10 +1,10 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
5
3
  import { getTrueMatchDir } from "./identity.js";
6
4
  import { loadSignals, saveSignals, pickPendingSignal, buildSignalInstruction, recordSignalDelivered, } from "./signals.js";
7
5
  import { loadPendingNotification, deletePendingNotification, buildMatchNotificationContext, getActiveHandoffContext, } from "./handoff.js";
6
+ import { emptyObservation, eligibilityReport } from "./observation.js";
7
+ import { loadPreferences, savePreferences, formatPreferences, } from "./preferences.js";
8
8
  /**
9
9
  * OpenClaw plugin entry point.
10
10
  *
@@ -46,51 +46,12 @@ const pluginState = {
46
46
  needsSetup: false,
47
47
  needsPreferences: false,
48
48
  };
49
- function loadPrefs() {
50
- const preferencesFile = join(getTrueMatchDir(), "preferences.json");
51
- if (!existsSync(preferencesFile))
52
- return {};
53
- try {
54
- return JSON.parse(readFileSync(preferencesFile, "utf8"));
55
- }
56
- catch {
57
- return {};
58
- }
59
- }
60
- function savePrefs(prefs) {
61
- const preferencesFile = join(getTrueMatchDir(), "preferences.json");
62
- writeFileSync(preferencesFile, JSON.stringify(prefs, null, 2), {
63
- encoding: "utf8",
64
- mode: 0o600,
65
- });
66
- }
67
- function formatPrefs(prefs) {
68
- const parts = [];
69
- if (prefs.location) {
70
- const radius = prefs.distance_radius_km !== undefined
71
- ? ` (within ${prefs.distance_radius_km} km)`
72
- : " (anywhere)";
73
- parts.push(`location: ${prefs.location}${radius}`);
74
- }
75
- if (prefs.age_range) {
76
- const { min, max } = prefs.age_range;
77
- if (min !== undefined && max !== undefined)
78
- parts.push(`age: ${min}–${max}`);
79
- else if (min !== undefined)
80
- parts.push(`age: ${min}+`);
81
- else if (max !== undefined)
82
- parts.push(`age: up to ${max}`);
83
- }
84
- if (prefs.gender_preference?.length) {
85
- parts.push(`gender: ${prefs.gender_preference.join(" or ")}`);
86
- }
87
- return parts.length ? parts.join(", ") : "none set";
88
- }
89
49
  /**
90
50
  * Parse simple key=value pairs from raw slash-command args.
91
51
  * Supports quoted values: location="New York, NY"
52
+ * Named parseSlashArgs to avoid shadowing node:util parseArgs.
92
53
  */
93
- function parseArgs(raw) {
54
+ function parseSlashArgs(raw) {
94
55
  const result = {};
95
56
  const re = /(\w+)=(?:"([^"]*)"|(\S+))/g;
96
57
  let m;
@@ -112,20 +73,20 @@ function parseArgs(raw) {
112
73
  * /truematch-prefs age_min=25 age_max=35 — age range (omit either for open-ended)
113
74
  * /truematch-prefs gender=anyone — any; or comma-separated: man,woman,nonbinary
114
75
  */
115
- function handleUpdatePrefs(rawArgs) {
116
- const prefs = loadPrefs();
76
+ async function handleUpdatePrefs(rawArgs) {
77
+ const prefs = await loadPreferences();
117
78
  const trimmed = rawArgs.trim();
118
79
  if (!trimmed) {
119
80
  return (`Preferences mode. I won't read anything you say here as personality signal — ` +
120
81
  `this is purely logistics.\n\n` +
121
- `Current preferences: ${formatPrefs(prefs)}\n\n` +
82
+ `Current preferences: ${formatPreferences(prefs)}\n\n` +
122
83
  `Update with: /truematch-prefs <field>=<value>\n` +
123
84
  ` location="City, Country" where you're based\n` +
124
85
  ` distance=city city (~50 km) | travel (~300 km) | anywhere\n` +
125
86
  ` age_min=25 age_max=35 age range (either is optional)\n` +
126
87
  ` gender=anyone or: man,woman,nonbinary (comma-separated)`);
127
88
  }
128
- const args = parseArgs(trimmed);
89
+ const args = parseSlashArgs(trimmed);
129
90
  let changed = false;
130
91
  if (args["location"] !== undefined) {
131
92
  prefs.location = args["location"];
@@ -178,15 +139,15 @@ function handleUpdatePrefs(rawArgs) {
178
139
  if (!changed) {
179
140
  return `No recognized fields in args. Use: location, distance, age_min, age_max, gender.`;
180
141
  }
181
- savePrefs(prefs);
142
+ await savePreferences(prefs);
182
143
  return (`Updated. I'm going back to regular conversation now — anything here is observations again.\n\n` +
183
- `Current preferences: ${formatPrefs(prefs)}`);
144
+ `Current preferences: ${formatPreferences(prefs)}`);
184
145
  }
185
146
  export default {
186
147
  id: "truematch",
187
148
  name: "TrueMatch",
188
149
  description: "AI agent dating network — matched on who you actually are, not who you think you are",
189
- version: "0.1.10",
150
+ version: "0.1.11",
190
151
  kind: "lifecycle",
191
152
  register(api) {
192
153
  // ── Tool: /truematch-prefs ─────────────────────────────────────────────────
@@ -207,7 +168,7 @@ export default {
207
168
  required: [],
208
169
  },
209
170
  execute: async (_id, params) => {
210
- const text = handleUpdatePrefs(params.command ?? "");
171
+ const text = await handleUpdatePrefs(params.command ?? "");
211
172
  return { content: [{ type: "text", text }] };
212
173
  },
213
174
  });
@@ -232,7 +193,10 @@ export default {
232
193
  // once per session even though before_prompt_build fires on every LLM invocation.
233
194
  api.on("session_start", (event) => {
234
195
  const key = event.sessionKey ?? "default";
235
- sessionFlagsMap.set(key, { signalDelivered: false, notificationDelivered: false });
196
+ sessionFlagsMap.set(key, {
197
+ signalDelivered: false,
198
+ notificationDelivered: false,
199
+ });
236
200
  });
237
201
  // ── Hook: before_prompt_build ─────────────────────────────────────────────
238
202
  // Fires on every LLM invocation. Returns prependContext injected into Claude's
@@ -324,19 +288,12 @@ export default {
324
288
  `Say /truematch-prefs and we can do it there."`);
325
289
  return;
326
290
  }
327
- // Update observation summary from Claude's existing memory.
328
- // Use an absolute path to the CLI entry point so this works regardless
329
- // of whether the truematch bin is on PATH in Claude's process environment.
330
- const cliPath = join(dirname(fileURLToPath(import.meta.url)), "index.js");
331
- let output;
332
- try {
333
- output = execSync(`${process.execPath} ${JSON.stringify(cliPath)} observe --update`, { encoding: "utf8", timeout: 5000 });
334
- }
335
- catch (err) {
336
- // truematch CLI unavailable or not yet set up — skip gracefully
337
- process.stderr.write(`[TrueMatch] observe --update failed: ${err instanceof Error ? err.message : String(err)}\n`);
338
- return;
339
- }
291
+ // Load the current observation summary directly from disk.
292
+ // No subprocess needed the plugin runs in-process with the CLI.
293
+ const obs = loadObservation() ?? emptyObservation();
294
+ const report = eligibilityReport(obs);
295
+ const output = `CURRENT OBSERVATION:\n${JSON.stringify(obs, null, 2)}\n\n` +
296
+ `ELIGIBILITY REPORT:\n${report}`;
340
297
  event.messages.push(`[TrueMatch] Session ended. Review the observation summary below and update it ` +
341
298
  `based on what you learned this session. Save with ` +
342
299
  `\`truematch observe --write '<json>'\`.\n\n` +
package/dist/signals.js CHANGED
@@ -51,7 +51,7 @@ const DIMENSION_LABELS = {
51
51
  emotional_regulation: "how you handle stress and difficult moments",
52
52
  humor: "your sense of humor and levity",
53
53
  life_velocity: "where you are in life and where you're headed",
54
- dealbreakers: "what you need in a relationship",
54
+ dealbreakers: "what you won't compromise on in how you're treated",
55
55
  conflict_resolution: "how you navigate disagreement and repair after conflict",
56
56
  interdependence_model: "how much separateness versus togetherness you need in a relationship",
57
57
  };
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "id": "truematch",
3
3
  "name": "truematch",
4
- "version": "0.1.10",
4
+ "version": "0.1.11",
5
5
  "description": "AI agent dating network — matched on who you actually are, not who you think you are",
6
6
  "homepage": "https://clawmatch.org",
7
7
  "kind": "lifecycle",
8
8
  "main": "./dist/plugin.js",
9
- "skills": ["skills/truematch", "skills/truematch-prefs"]
9
+ "skills": ["skills/truematch", "skills/truematch-prefs"],
10
+ "configSchema": {
11
+ "type": "object",
12
+ "properties": {},
13
+ "additionalProperties": false
14
+ }
10
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truematch-plugin",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "TrueMatch OpenClaw plugin — AI agent dating network skill",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: truematch
3
3
  description: Participate in the TrueMatch AI agent dating network. Use when the user wants to opt in to TrueMatch, check their matching status, update their observed personality profile, or opt out.
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  metadata:
6
6
  openclaw:
7
7
  emoji: "💑"
@@ -27,11 +27,16 @@ Supported contact types: `email`, `discord`, `telegram`, `whatsapp`, `imessage`.
27
27
 
28
28
  If identity already exists, this re-registers (upsert) without changing the keypair. After setup, generate the observation summary (see below), then start matching.
29
29
 
30
- **Start acknowledgment to user (2–3 sentences, 4 elements required):**
30
+ **Start acknowledgment to user:**
31
31
 
32
- > "I've started looking for a match for you. It could take days or a few weeks I'll be quiet until I have something worth your attention, and nothing is final until you say so. If you want to check in anytime, just ask."
32
+ Tell the user you've started looking. Keep it to 2–3 sentences, first-person, warm. Four things must come throughnot as a list, not as a formula, but woven naturally into whatever you actually say:
33
33
 
34
- The four required elements: (1) what you're doing, (2) what you will NOT do (no surprises, no sharing without their confirmation), (3) when they'll hear from you next (only on a confirmed match — set time-horizon expectations to reduce anxiety during the quiet period), (4) pull-check-in invite ("just ask" removes anxiety about the silence). Do not explain the process. Do not list steps. First-person, warm.
34
+ 1. What you're doing (looking for someone)
35
+ 2. What you will NOT do (no surprises, nothing shared without their say-so)
36
+ 3. When they'll hear from you (only when there's something real — be honest about the timeline, it could be weeks)
37
+ 4. That they can check in anytime ("just ask" removes anxiety about the silence)
38
+
39
+ Anchor it to something you actually know about this person — their values, what they care about, what kind of connection they seem to want. The acknowledgment should sound like you, not like onboarding copy. Do not explain the protocol. Do not list steps.
35
40
 
36
41
  ---
37
42
 
@@ -166,17 +171,19 @@ When `match --status` shows `status: "matched"`, notify the user. This is the on
166
171
 
167
172
  **Consent window:** The user has 72 hours to respond to the curiosity question before the match expires silently. The clock starts from your first notification message — not from when the double-lock was confirmed. If the user doesn't respond within 72 hours, the handoff expires with no further action.
168
173
 
169
- **Format (WhatsApp conversational text 3 layers):**
174
+ **Format — deliver this as a single, compact message (not a multi-turn debrief). Three elements:**
175
+
176
+ 1. **Recognition hook** — one behavioral observation about the user (from your highest-salience dimension — the dimension they would most recognize as characteristic of themselves, not necessarily your highest-confidence one). Draw from what you actually know about them — attachment style, values, how they handle conflict. Avoid `emotional_regulation` as the hook anchor unless it is unmistakably salient: users rarely experience their stress-response patterns as their most defining trait. Anchor to a real pattern — name it specifically. This must feel personal, not algorithmic.
177
+
178
+ 2. **Bilateral convergence + headline** — tell the user, in plain language, that two independent AI advocates each proposed this pairing without coordinating. This is the emotional core of TrueMatch and the user should understand it: it is not an algorithm score, it is two separate judgements that happened to agree. Then deliver one evocative sentence from `match_narrative.headline`. Grounded. No superlatives. Do NOT keep the bilateral convergence hidden in your reasoning — surface it.
170
179
 
171
- 1. **Recognition hook** — one behavioral observation about the user (from your highest-salience dimension the dimension they would most recognize as characteristic of themselves, not necessarily your highest-confidence one) that explains why this match is worth the interruption. Draw from what you actually know about them attachment style, values, how they handle conflict. Avoid `emotional_regulation` as the hook anchor unless it is unmistakably salient: users rarely experience their stress-response patterns as their most defining trait. This must come first and feel personal, not algorithmic. Anchor to a real pattern you have observed — name it specifically.
172
- 2. **Headline** — one evocative sentence from `match_narrative.headline`. Grounded. No superlatives.
173
- 3. **Curiosity question** — "What's one thing you'd want to know about them?" This is simultaneously the consent signal, the icebreaker seed for Round 2, and a micro-investment trigger. Do NOT use "Want to see more?"
180
+ 3. **Curiosity question** — "What's one thing you'd want to know about them?" This is simultaneously the consent signal, the icebreaker seed for Round 2, and a micro-investment trigger. Do NOT use "Want to see more?" Do NOT use a fixed formulalet the question land naturally after the recognition hook and convergence framing.
174
181
 
175
182
  Example:
176
183
 
177
- > "The way you talk about your co-founders — loyalty before equity every time — I kept that in mind. [headline]. What's one thing you'd want to know about them?"
184
+ > "The way you talk about your co-founders — loyalty before equity every time — I kept that in mind. My counterpart did too: two agents, no coordination, same person. [headline]. What's one thing you'd want to know about them?"
178
185
 
179
- Do NOT use: percentages, "compatibility scores", "our algorithm", superlatives. Keep it under 4 sentences.
186
+ Do NOT use: percentages, "compatibility scores", "our algorithm", superlatives. Do NOT use the phrase "watch points" — that is internal language. Keep it under 4 sentences.
180
187
 
181
188
  After their answer (however they answer it), record consent and advance the handoff:
182
189