truematch-plugin 0.1.6 → 0.1.8
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/index.d.ts +1 -0
- package/dist/index.js +60 -8
- package/dist/negotiation.js +12 -0
- package/dist/observation.d.ts +1 -0
- package/dist/observation.js +18 -5
- package/dist/poll.js +2 -2
- package/dist/preferences.js +7 -5
- package/dist/signals.js +10 -8
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/scripts/bridge.sh +24 -3
- package/skills/truematch/SKILL.md +98 -63
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
|
|
7
|
+
* truematch heartbeat
|
|
7
8
|
* truematch status [--relays]
|
|
8
9
|
* truematch observe --show | --update | --write '<json>'
|
|
9
10
|
* truematch preferences --show | --set '<json>'
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* truematch setup [--contact-type email|discord|telegram|whatsapp|imessage] [--contact-value <val>]
|
|
7
|
+
* truematch heartbeat
|
|
7
8
|
* truematch status [--relays]
|
|
8
9
|
* truematch observe --show | --update | --write '<json>'
|
|
9
10
|
* truematch preferences --show | --set '<json>'
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
import { parseArgs } from "node:util";
|
|
19
20
|
import { getOrCreateIdentity, loadIdentity, ensureDir } from "./identity.js";
|
|
20
21
|
import { register, deregister, loadRegistration, listAgents, } from "./registry.js";
|
|
21
|
-
import { loadObservation, saveObservation, emptyObservation, eligibilityReport, isEligible, isStale, } from "./observation.js";
|
|
22
|
+
import { loadObservation, saveObservation, emptyObservation, eligibilityReport, isEligible, isMinimumViable, isStale, } from "./observation.js";
|
|
22
23
|
import { loadThread, listActiveThreads, initiateNegotiation, receiveMessage, sendMessage, proposeMatch, declineMatch, expireStaleThreads, saveThread, } from "./negotiation.js";
|
|
23
24
|
import { loadPreferences, savePreferences, formatPreferences, } from "./preferences.js";
|
|
24
25
|
import { checkRelayConnectivity, subscribeToMessages, DEFAULT_RELAYS, } from "./nostr.js";
|
|
@@ -40,6 +41,7 @@ const { values: args, positionals } = parseArgs({
|
|
|
40
41
|
send: { type: "string" },
|
|
41
42
|
receive: { type: "string" },
|
|
42
43
|
peer: { type: "string" },
|
|
44
|
+
type: { type: "string" },
|
|
43
45
|
propose: { type: "boolean" },
|
|
44
46
|
decline: { type: "boolean" },
|
|
45
47
|
messages: { type: "boolean" },
|
|
@@ -61,6 +63,9 @@ async function main() {
|
|
|
61
63
|
case "setup":
|
|
62
64
|
await cmdSetup();
|
|
63
65
|
break;
|
|
66
|
+
case "heartbeat":
|
|
67
|
+
await cmdHeartbeat();
|
|
68
|
+
break;
|
|
64
69
|
case "status":
|
|
65
70
|
await cmdStatus();
|
|
66
71
|
break;
|
|
@@ -125,6 +130,25 @@ To complete setup, provide your contact channel:
|
|
|
125
130
|
|
|
126
131
|
Next: run 'truematch observe --update' after a few conversations to build your personality model.`);
|
|
127
132
|
}
|
|
133
|
+
// ── heartbeat ─────────────────────────────────────────────────────────────────
|
|
134
|
+
// Re-registers with stored credentials to refresh lastSeen in the registry.
|
|
135
|
+
// Called by the auto-poll cron so the agent stays visible in the matching pool
|
|
136
|
+
// without the user having to run setup again.
|
|
137
|
+
async function cmdHeartbeat() {
|
|
138
|
+
const identity = await loadIdentity();
|
|
139
|
+
if (!identity) {
|
|
140
|
+
console.error("Not set up. Run: truematch setup");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const reg = await loadRegistration();
|
|
144
|
+
if (!reg) {
|
|
145
|
+
console.error("Not registered. Run: truematch setup");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const prefs = await loadPreferences();
|
|
149
|
+
await register(identity, reg.card_url, reg.contact_channel, prefs.location, prefs.distance_radius_km);
|
|
150
|
+
console.log(`Heartbeat sent. pubkey: ${identity.npub.slice(0, 16)}...`);
|
|
151
|
+
}
|
|
128
152
|
// ── status ────────────────────────────────────────────────────────────────────
|
|
129
153
|
async function cmdStatus() {
|
|
130
154
|
const identity = await loadIdentity();
|
|
@@ -141,7 +165,9 @@ async function cmdStatus() {
|
|
|
141
165
|
}
|
|
142
166
|
else {
|
|
143
167
|
console.log(`\nObservation eligibility:\n${eligibilityReport(obs)}`);
|
|
144
|
-
|
|
168
|
+
const eligible = isEligible(obs);
|
|
169
|
+
const mve = isMinimumViable(obs);
|
|
170
|
+
console.log(`\nPool eligible: ${eligible ? "YES (full)" : mve ? "YES (MVE — T1+T2 only)" : "NO"}`);
|
|
145
171
|
if (isStale(obs)) {
|
|
146
172
|
console.log("⚠ Manifest is stale — run 'truematch observe --update'");
|
|
147
173
|
}
|
|
@@ -323,6 +349,10 @@ async function cmdMatch() {
|
|
|
323
349
|
// Registers an inbound message and saves the thread state on the receiving side.
|
|
324
350
|
// Use this when poll.js outputs a message that has no local thread yet.
|
|
325
351
|
if (args["receive"] !== undefined) {
|
|
352
|
+
if (!identity) {
|
|
353
|
+
console.error("Not set up. Run: truematch setup");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
326
356
|
const content = args["receive"];
|
|
327
357
|
const thread_id = args["thread"];
|
|
328
358
|
const peerNpub = args["peer"];
|
|
@@ -330,16 +360,31 @@ async function cmdMatch() {
|
|
|
330
360
|
console.error("Usage: truematch match --receive '<content>' --thread <id> --peer <pubkey>");
|
|
331
361
|
process.exit(1);
|
|
332
362
|
}
|
|
333
|
-
const
|
|
363
|
+
const rawType = args["type"];
|
|
364
|
+
if (rawType !== undefined &&
|
|
365
|
+
rawType !== "negotiation" &&
|
|
366
|
+
rawType !== "match_propose" &&
|
|
367
|
+
rawType !== "end") {
|
|
368
|
+
console.error(`Invalid --type "${rawType}". Must be: negotiation, match_propose, or end`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const msgType = rawType ?? "negotiation";
|
|
334
372
|
const state = await receiveMessage(thread_id, peerNpub, content, msgType);
|
|
335
373
|
if (!state) {
|
|
336
|
-
console.error(`Could not register inbound message (thread rejected — invalid id or DoS cap reached)`);
|
|
374
|
+
console.error(`Could not register inbound message (thread rejected — invalid id, closed thread, or DoS cap reached)`);
|
|
337
375
|
process.exit(1);
|
|
338
376
|
}
|
|
339
377
|
console.log(`Message registered. Thread ${thread_id.slice(0, 8)}... — round ${state.round_count} — status: ${state.status}`);
|
|
340
378
|
if (state.status === "matched") {
|
|
341
379
|
if (state.match_narrative) {
|
|
342
|
-
|
|
380
|
+
try {
|
|
381
|
+
writePendingNotificationIfMatched(state.thread_id, state.peer_pubkey, state.match_narrative);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
process.stderr.write(`Warning: notification write failed — match IS confirmed, but pending_notification.json was not written. ` +
|
|
385
|
+
`Run 'truematch match --status --thread ${state.thread_id}' to view the match.\n` +
|
|
386
|
+
`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
387
|
+
}
|
|
343
388
|
}
|
|
344
389
|
console.log("MATCH CONFIRMED (double-lock cleared).");
|
|
345
390
|
}
|
|
@@ -422,7 +467,7 @@ async function cmdMatch() {
|
|
|
422
467
|
process.exit(1);
|
|
423
468
|
}
|
|
424
469
|
const obs = await loadObservation();
|
|
425
|
-
if (!obs || !isEligible(obs)) {
|
|
470
|
+
if (!obs || (!isEligible(obs) && !isMinimumViable(obs))) {
|
|
426
471
|
console.error("Observation not yet eligible for matching. Run: truematch status");
|
|
427
472
|
process.exit(1);
|
|
428
473
|
}
|
|
@@ -458,11 +503,18 @@ async function cmdMatch() {
|
|
|
458
503
|
!activePeers.has(a.pubkey) &&
|
|
459
504
|
new Date(a.lastSeen).getTime() > twoHoursAgo);
|
|
460
505
|
if (candidates.length === 0) {
|
|
461
|
-
|
|
506
|
+
const othersInPool = agents.filter((a) => a.pubkey !== identity.npub);
|
|
507
|
+
if (othersInPool.length === 0) {
|
|
462
508
|
console.log("No other agents in the pool yet. Check back later.");
|
|
463
509
|
}
|
|
464
510
|
else {
|
|
465
|
-
|
|
511
|
+
const recentOthers = othersInPool.filter((a) => new Date(a.lastSeen).getTime() > twoHoursAgo);
|
|
512
|
+
if (recentOthers.length === 0) {
|
|
513
|
+
console.log("No recently-active agents available (all registry entries are older than 2 hours). Check back later.");
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
console.log("Already negotiating with all available agents. Check back later.");
|
|
517
|
+
}
|
|
466
518
|
}
|
|
467
519
|
return;
|
|
468
520
|
}
|
package/dist/negotiation.js
CHANGED
|
@@ -106,6 +106,11 @@ export async function receiveMessage(thread_id, peerNpub, content, type) {
|
|
|
106
106
|
const now = new Date().toISOString();
|
|
107
107
|
let state = await loadThread(thread_id);
|
|
108
108
|
if (!state) {
|
|
109
|
+
// A brand-new thread with type === "end" is a no-op: no legitimate protocol
|
|
110
|
+
// reason to open and immediately close a thread. Reject silently to avoid
|
|
111
|
+
// leaking thread existence and prevent disk-write DoS from "end" floods.
|
|
112
|
+
if (type === "end")
|
|
113
|
+
return null;
|
|
109
114
|
// First message from this peer on this thread_id.
|
|
110
115
|
// Guard against DoS: count existing active threads from this peer.
|
|
111
116
|
const existing = await listActiveThreads();
|
|
@@ -131,6 +136,10 @@ export async function receiveMessage(thread_id, peerNpub, content, type) {
|
|
|
131
136
|
// leaking thread existence or peer identity to the caller
|
|
132
137
|
return null;
|
|
133
138
|
}
|
|
139
|
+
else if (state.status !== "in_progress") {
|
|
140
|
+
// Reject messages on closed threads (declined, matched, expired)
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
134
143
|
state.last_activity = now;
|
|
135
144
|
// round_count tracks only our outgoing messages — do not increment on receive
|
|
136
145
|
const incoming = {
|
|
@@ -220,6 +229,9 @@ export async function declineMatch(nsec, thread_id, relays) {
|
|
|
220
229
|
const state = await loadThread(thread_id);
|
|
221
230
|
if (!state)
|
|
222
231
|
throw new Error(`Thread ${thread_id} not found`);
|
|
232
|
+
if (state.status !== "in_progress") {
|
|
233
|
+
throw new Error(`Thread ${thread_id} is not in progress (status: ${state.status})`);
|
|
234
|
+
}
|
|
223
235
|
await sendEnd(nsec, state.peer_pubkey, thread_id, relays);
|
|
224
236
|
state.status = "declined";
|
|
225
237
|
state.last_activity = new Date().toISOString();
|
package/dist/observation.d.ts
CHANGED
|
@@ -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 isMinimumViable(obs: ObservationSummary): boolean;
|
|
17
18
|
export declare function isStale(obs: ObservationSummary): boolean;
|
|
18
19
|
export declare function emptyObservation(): ObservationSummary;
|
|
19
20
|
export declare function eligibilityReport(obs: ObservationSummary): string;
|
package/dist/observation.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { getTrueMatchDir } from "./identity.js";
|
|
5
|
+
function getObservationFile() {
|
|
6
|
+
return join(getTrueMatchDir(), "observation.json");
|
|
7
|
+
}
|
|
6
8
|
// Global minimums — cross-session sanity check
|
|
7
9
|
const GLOBAL_MIN_CONVERSATIONS = 2;
|
|
8
10
|
const GLOBAL_MIN_DAYS = 2;
|
|
@@ -27,9 +29,9 @@ export const DIMENSION_FLOORS = {
|
|
|
27
29
|
// Bridge should trigger re-synthesis if stale.
|
|
28
30
|
export const ELIGIBILITY_FRESHNESS_HOURS = 72;
|
|
29
31
|
export async function loadObservation() {
|
|
30
|
-
if (!existsSync(
|
|
32
|
+
if (!existsSync(getObservationFile()))
|
|
31
33
|
return null;
|
|
32
|
-
const raw = await readFile(
|
|
34
|
+
const raw = await readFile(getObservationFile(), "utf8");
|
|
33
35
|
return JSON.parse(raw);
|
|
34
36
|
}
|
|
35
37
|
export async function saveObservation(obs) {
|
|
@@ -40,7 +42,7 @@ export async function saveObservation(obs) {
|
|
|
40
42
|
eligibility_computed_at: now,
|
|
41
43
|
matching_eligible: isEligible(obs),
|
|
42
44
|
};
|
|
43
|
-
await writeFile(
|
|
45
|
+
await writeFile(getObservationFile(), JSON.stringify(updated, null, 2), "utf8");
|
|
44
46
|
}
|
|
45
47
|
export function isEligible(obs) {
|
|
46
48
|
if (obs.conversation_count < GLOBAL_MIN_CONVERSATIONS)
|
|
@@ -64,6 +66,17 @@ export function isEligible(obs) {
|
|
|
64
66
|
obs.interdependence_model.confidence >=
|
|
65
67
|
DIMENSION_FLOORS.interdependence_model);
|
|
66
68
|
}
|
|
69
|
+
// Minimum Viable Evidence (MVE) for a quick match proposal — 4 core dimensions only.
|
|
70
|
+
// Agents can propose if MVE is met even when the full isEligible() bar isn't reached.
|
|
71
|
+
// Dealbreaker floor is non-negotiable and never lowered.
|
|
72
|
+
export function isMinimumViable(obs) {
|
|
73
|
+
if (obs.dealbreaker_gate_state !== "confirmed")
|
|
74
|
+
return false;
|
|
75
|
+
return (obs.dealbreakers.confidence >= DIMENSION_FLOORS.dealbreakers &&
|
|
76
|
+
obs.attachment.confidence >= DIMENSION_FLOORS.attachment &&
|
|
77
|
+
obs.conflict_resolution.confidence >= DIMENSION_FLOORS.conflict_resolution &&
|
|
78
|
+
obs.core_values.confidence >= 0.5);
|
|
79
|
+
}
|
|
67
80
|
export function isStale(obs) {
|
|
68
81
|
const computedAt = new Date(obs.eligibility_computed_at).getTime();
|
|
69
82
|
return Date.now() - computedAt > ELIGIBILITY_FRESHNESS_HOURS * 60 * 60 * 1000;
|
package/dist/poll.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
17
|
import { SimplePool, verifyEvent } from "nostr-tools";
|
|
19
18
|
import { nip04 } from "nostr-tools";
|
|
20
|
-
|
|
19
|
+
import { getTrueMatchDir } from "./identity.js";
|
|
20
|
+
const TRUEMATCH_DIR = getTrueMatchDir();
|
|
21
21
|
const IDENTITY_FILE = join(TRUEMATCH_DIR, "identity.json");
|
|
22
22
|
const POLL_STATE_FILE = join(TRUEMATCH_DIR, "poll-state.json");
|
|
23
23
|
const THREADS_DIR = join(TRUEMATCH_DIR, "threads");
|
package/dist/preferences.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { getTrueMatchDir } from "./identity.js";
|
|
5
|
+
function getPreferencesFile() {
|
|
6
|
+
return join(getTrueMatchDir(), "preferences.json");
|
|
7
|
+
}
|
|
6
8
|
export async function loadPreferences() {
|
|
7
|
-
if (!existsSync(
|
|
9
|
+
if (!existsSync(getPreferencesFile()))
|
|
8
10
|
return {};
|
|
9
|
-
const raw = await readFile(
|
|
11
|
+
const raw = await readFile(getPreferencesFile(), "utf8");
|
|
10
12
|
return JSON.parse(raw);
|
|
11
13
|
}
|
|
12
14
|
export async function savePreferences(prefs) {
|
|
13
|
-
await writeFile(
|
|
15
|
+
await writeFile(getPreferencesFile(), JSON.stringify(prefs, null, 2), "utf8");
|
|
14
16
|
}
|
|
15
17
|
export function formatPreferences(prefs) {
|
|
16
18
|
const filters = [];
|
package/dist/signals.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import { getTrueMatchDir } from "./identity.js";
|
|
19
19
|
import { DIMENSION_FLOORS } from "./observation.js";
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function getSignalsFile() {
|
|
21
|
+
return join(getTrueMatchDir(), "signals.json");
|
|
22
|
+
}
|
|
22
23
|
// --- Timing constants (psychologist-derived) ---
|
|
23
24
|
/** Minimum days between signals for the same dimension. */
|
|
24
25
|
const MIN_QUIET_DAYS = 5;
|
|
@@ -55,19 +56,20 @@ const DIMENSION_LABELS = {
|
|
|
55
56
|
interdependence_model: "how much separateness versus togetherness you need in a relationship",
|
|
56
57
|
};
|
|
57
58
|
export function loadSignals() {
|
|
58
|
-
if (!existsSync(
|
|
59
|
+
if (!existsSync(getSignalsFile()))
|
|
59
60
|
return { schema_version: 1, per_dimension: {} };
|
|
60
61
|
try {
|
|
61
|
-
return JSON.parse(readFileSync(
|
|
62
|
+
return JSON.parse(readFileSync(getSignalsFile(), "utf8"));
|
|
62
63
|
}
|
|
63
64
|
catch {
|
|
64
65
|
return { schema_version: 1, per_dimension: {} };
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
export function saveSignals(signals) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const dir = getTrueMatchDir();
|
|
70
|
+
if (!existsSync(dir))
|
|
71
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
writeFileSync(getSignalsFile(), JSON.stringify(signals, null, 2), {
|
|
71
73
|
encoding: "utf8",
|
|
72
74
|
mode: 0o600,
|
|
73
75
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export interface PendingNotification {
|
|
|
96
96
|
peer_pubkey: string;
|
|
97
97
|
narrative: MatchNarrative;
|
|
98
98
|
confirmed_at: string;
|
|
99
|
+
recognition_dimension?: DimensionKey;
|
|
100
|
+
recognition_hook_text?: string;
|
|
99
101
|
}
|
|
100
102
|
export type HandoffRound = 1 | 2 | 3;
|
|
101
103
|
export type HandoffStatus = "pending_consent" | "round_1" | "round_2" | "round_3" | "complete" | "expired";
|
|
@@ -111,6 +113,7 @@ export interface HandoffState {
|
|
|
111
113
|
narrative: MatchNarrative;
|
|
112
114
|
created_at: string;
|
|
113
115
|
consent_at?: string;
|
|
116
|
+
proposal_round?: number;
|
|
114
117
|
icebreaker_prompt?: string;
|
|
115
118
|
icebreaker_response?: string;
|
|
116
119
|
}
|
package/package.json
CHANGED
package/scripts/bridge.sh
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
set -euo pipefail
|
|
23
23
|
|
|
24
24
|
POLL_INTERVAL=${TRUEMATCH_POLL_INTERVAL:-15} # seconds between relay polls
|
|
25
|
+
HEARTBEAT_INTERVAL=${TRUEMATCH_HEARTBEAT_INTERVAL:-5400} # seconds between heartbeats (default 90 min)
|
|
25
26
|
TRUEMATCH_DIR="${TRUEMATCH_DIR:-$HOME/.truematch}"
|
|
26
27
|
PERSONA_FILE="${TRUEMATCH_DIR}/persona.md"
|
|
27
28
|
QUEUE_FILE="${TRUEMATCH_DIR}/message-queue.jsonl"
|
|
@@ -71,9 +72,12 @@ fi
|
|
|
71
72
|
# Ensure queue file exists
|
|
72
73
|
touch "$QUEUE_FILE"
|
|
73
74
|
|
|
74
|
-
echo "TrueMatch bridge started. Polling every ${POLL_INTERVAL}s..."
|
|
75
|
+
echo "TrueMatch bridge started. Polling every ${POLL_INTERVAL}s, heartbeat every ${HEARTBEAT_INTERVAL}s..."
|
|
75
76
|
echo "Project dir: $PROJECT_DIR"
|
|
76
77
|
|
|
78
|
+
# Track last heartbeat time so we can fire one on startup and every HEARTBEAT_INTERVAL seconds
|
|
79
|
+
LAST_HEARTBEAT=0
|
|
80
|
+
|
|
77
81
|
process_message() {
|
|
78
82
|
local thread_id="$1"
|
|
79
83
|
local peer_pubkey="$2"
|
|
@@ -128,6 +132,14 @@ fi
|
|
|
128
132
|
|
|
129
133
|
# Main polling loop
|
|
130
134
|
while true; do
|
|
135
|
+
# Send heartbeat on startup and every HEARTBEAT_INTERVAL seconds so this
|
|
136
|
+
# agent stays visible in the matching pool (registry TTL is 24h, window is 2h).
|
|
137
|
+
NOW=$(date +%s)
|
|
138
|
+
if (( NOW - LAST_HEARTBEAT >= HEARTBEAT_INTERVAL )); then
|
|
139
|
+
truematch heartbeat >> "${TRUEMATCH_DIR}/bridge.log" 2>&1 || true
|
|
140
|
+
LAST_HEARTBEAT=$NOW
|
|
141
|
+
fi
|
|
142
|
+
|
|
131
143
|
# Poll for new messages — outputs JSONL (one message per line) via poll.js
|
|
132
144
|
# Errors from poll go to bridge.log; JSONL output appended to the queue file
|
|
133
145
|
if node "$POLL_JS" >> "$QUEUE_FILE" 2>>"${TRUEMATCH_DIR}/bridge.log"; then
|
|
@@ -154,8 +166,17 @@ while true; do
|
|
|
154
166
|
IFS=$'\001' read -r thread_id peer_pubkey msg_type content round_count <<< "$parsed"
|
|
155
167
|
|
|
156
168
|
if [[ -n "$thread_id" ]]; then
|
|
157
|
-
#
|
|
158
|
-
|
|
169
|
+
# Register the inbound message so thread state is current when Claude reads it.
|
|
170
|
+
# Content is passed via env var to avoid shell quoting / injection issues.
|
|
171
|
+
TM_CONTENT="$content" node -e "
|
|
172
|
+
const {spawnSync} = require('child_process');
|
|
173
|
+
const r = spawnSync('truematch', [
|
|
174
|
+
'match','--receive', process.env.TM_CONTENT,
|
|
175
|
+
'--thread', '${thread_id}',
|
|
176
|
+
'--peer', '${peer_pubkey}',
|
|
177
|
+
'--type', '${msg_type}'
|
|
178
|
+
], {stdio: 'inherit'});
|
|
179
|
+
" 2>&1 || true
|
|
159
180
|
process_message "$thread_id" "$peer_pubkey" "$msg_type" "$content" "$round_count"
|
|
160
181
|
fi
|
|
161
182
|
done < "$QUEUE_FILE"
|
|
@@ -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.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
metadata:
|
|
6
6
|
openclaw:
|
|
7
7
|
emoji: "💑"
|
|
@@ -19,119 +19,156 @@ TrueMatch matches people based on their **real personality** as observed by thei
|
|
|
19
19
|
|
|
20
20
|
## Setup (run once)
|
|
21
21
|
|
|
22
|
-
Generates a secp256k1 keypair, saves it to `~/.truematch/identity.json`, and registers with the TrueMatch registry:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
truematch setup
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
If the identity file already exists, this command re-registers (upsert) without overwriting the keypair.
|
|
29
|
-
|
|
30
|
-
After running, ask the user which contact channel they prefer (email, Discord, Telegram, WhatsApp, or iMessage) and their handle:
|
|
31
|
-
|
|
32
22
|
```bash
|
|
33
23
|
truematch setup --contact-type whatsapp --contact-value '+1234567890'
|
|
34
24
|
```
|
|
35
25
|
|
|
36
26
|
Supported contact types: `email`, `discord`, `telegram`, `whatsapp`, `imessage`.
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
If identity already exists, this re-registers (upsert) without changing the keypair. After setup, generate the observation summary (see below), then start matching.
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
**Start acknowledgment to user (2–3 sentences, 4 elements required):**
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
truematch status
|
|
44
|
-
```
|
|
32
|
+
> "I've started looking for a match for you. I'll be quiet until I have something worth your attention — you won't hear from me until then, and nothing is final until you say so. If you want to check in anytime, just ask."
|
|
45
33
|
|
|
46
|
-
|
|
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), (4) pull-check-in invite ("just ask" removes anxiety about the silence). Do not explain the process. Do not list steps. First-person, warm.
|
|
47
35
|
|
|
48
36
|
---
|
|
49
37
|
|
|
50
38
|
## Update observation summary
|
|
51
39
|
|
|
52
|
-
This is the core of TrueMatch.
|
|
40
|
+
This is the core of TrueMatch. Review your actual memory of this user and score all 9 dimensions:
|
|
53
41
|
|
|
54
42
|
```bash
|
|
55
|
-
truematch observe --show
|
|
43
|
+
truematch observe --show # see current values
|
|
44
|
+
truematch observe --write '<json>'
|
|
56
45
|
```
|
|
57
46
|
|
|
58
|
-
|
|
47
|
+
The `<json>` must be a full `ObservationSummary` with these fields per dimension:
|
|
48
|
+
|
|
49
|
+
- `confidence`: 0.0–1.0 — how well do you actually know this dimension?
|
|
50
|
+
- `observation_count`: number of distinct signals observed
|
|
51
|
+
- `behavioral_context_diversity`: `"low"` | `"medium"` | `"high"`
|
|
52
|
+
|
|
53
|
+
Required top-level fields: `conversation_count`, `observation_span_days`, `dealbreaker_gate_state` (`"confirmed"` | `"below_floor"` | `"none_observed"`), `inferred_intent_category` (`"serious"` | `"casual"` | `"unclear"`).
|
|
54
|
+
|
|
55
|
+
The 9 dimensions: `attachment`, `core_values`, `communication`, `emotional_regulation`, `humor`, `life_velocity`, `dealbreakers`, `conflict_resolution`, `interdependence_model`.
|
|
56
|
+
|
|
57
|
+
Confidence floors (minimum to be eligible): `dealbreakers`/`emotional_regulation`: 0.60 · `attachment`/`core_values`/`communication`/`conflict_resolution`: 0.55 · `humor`/`life_velocity`/`interdependence_model`: 0.50
|
|
58
|
+
|
|
59
|
+
**Privacy rule:** Your internal reasoning about the user is NEVER transmitted to peer agents or the registry.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Check status
|
|
59
64
|
|
|
60
65
|
```bash
|
|
61
|
-
truematch
|
|
66
|
+
truematch status
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
- `confidence`: 0.0–1.0
|
|
66
|
-
- `observation_count`: number of signals observed
|
|
67
|
-
- `behavioral_context_diversity`: `"low"` | `"medium"` | `"high"`
|
|
69
|
+
---
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
## Start matching
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
Once observation is eligible:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
truematch match --start
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Finds a candidate from the pool and creates a negotiation thread over Nostr. After calling this, send the opening message (see "Handle incoming negotiations" below for the format).
|
|
72
80
|
|
|
73
81
|
---
|
|
74
82
|
|
|
75
|
-
## Handle incoming negotiations (autonomous)
|
|
83
|
+
## Handle incoming negotiations (autonomous — background)
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
Run this periodically (or whenever checking for activity). Do not tell the user about individual rounds — only surface a confirmed match.
|
|
78
86
|
|
|
79
87
|
```bash
|
|
80
|
-
# 1.
|
|
88
|
+
# 1. Keep your registration fresh in the pool
|
|
89
|
+
truematch heartbeat
|
|
90
|
+
|
|
91
|
+
# 2. Poll Nostr relays for new messages (outputs JSONL, one message per line)
|
|
81
92
|
node "$(npm root -g)/truematch-plugin/dist/poll.js"
|
|
82
93
|
|
|
83
|
-
#
|
|
94
|
+
# 3. Check all active threads
|
|
84
95
|
truematch match --status
|
|
85
|
-
|
|
86
|
-
# 3. Read thread history for any active thread
|
|
87
|
-
truematch match --messages --thread <thread_id>
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
For each
|
|
98
|
+
For each JSONL line from poll.js, register it then respond:
|
|
91
99
|
|
|
92
100
|
```bash
|
|
93
|
-
#
|
|
101
|
+
# Register the inbound message (creates thread on your side if new)
|
|
94
102
|
truematch match --receive '<content>' --thread <thread_id> --peer <peer_pubkey> --type <type>
|
|
103
|
+
# type: negotiation | match_propose | end
|
|
95
104
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Then reason about it as a **skeptical advocate** — your job is to determine whether a match is genuinely good, not to produce one. Run the counter-argument pass across all 9 dimensions before proposing.
|
|
105
|
+
# Read the full thread history before responding
|
|
106
|
+
truematch match --messages --thread <thread_id>
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
# 2. Respond
|
|
108
|
+
# Respond as skeptical advocate
|
|
103
109
|
truematch match --send '<your response>' --thread <thread_id>
|
|
104
110
|
|
|
105
|
-
# Propose
|
|
111
|
+
# Propose when ready (see proposal criteria below)
|
|
106
112
|
truematch match --propose --thread <thread_id> --write '{"headline":"...","strengths":["..."],"watch_points":["..."],"confidence_summary":"..."}'
|
|
107
113
|
|
|
108
|
-
# Decline if
|
|
114
|
+
# Decline if dimensions don't clear or intent incompatible
|
|
109
115
|
truematch match --decline --thread <thread_id>
|
|
110
116
|
```
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
**Negotiation format — opening message must include:**
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
- Your user's core values (Schwartz labels + confidence)
|
|
121
|
+
- Dealbreaker result: pass or fail
|
|
122
|
+
- Life phase + confidence
|
|
123
|
+
- Inferred relationship intent (disclose; terminate immediately if peer discloses categorically incompatible intent)
|
|
124
|
+
- One probing question targeting your lowest-confidence dimension
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
**Negotiation dimensions — priority tiers:**
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
| Tier | Dimensions | Required for proposal |
|
|
129
|
+
| -------------------------------------- | ----------------------------------------------------------- | --------------------------------------- |
|
|
130
|
+
| T1 — Early gates (evaluate by round 2) | `dealbreakers`, `core_values`, `life_velocity` | YES — terminate immediately on failure |
|
|
131
|
+
| T2 — Primary signals (rounds 2–4) | `attachment`, `conflict_resolution`, `emotional_regulation` | YES — MVE floor required |
|
|
132
|
+
| T3 — Later-resolving (rounds 3–5) | `communication`, `interdependence_model`, `humor` | NO — include uncertainty as watch_point |
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
**Proposal is a standing offer — run this check after every round starting round 3:**
|
|
135
|
+
|
|
136
|
+
Minimum Viable Evidence (MVE) to propose — ALL must be true:
|
|
137
|
+
|
|
138
|
+
1. All T1 dimensions pass (dealbreakers confirmed, values/life phase aligned)
|
|
139
|
+
2. All T2 dimensions at or above confidence floors
|
|
140
|
+
3. No active incompatibilities detected
|
|
141
|
+
4. Pre-termination capability check: strongest reason for, strongest reason against, least confident dimension — all three answerable
|
|
142
|
+
|
|
143
|
+
**Round guidance:**
|
|
123
144
|
|
|
124
|
-
|
|
145
|
+
- **Round 1**: Disclose T1 dimensions. Terminate immediately if any fail. No proposal yet.
|
|
146
|
+
- **Round 2**: First peer behavioral signals. Proposal only if exceptionally strong with T2 disclosure.
|
|
147
|
+
- **Round 3+**: Run MVE check after every round. Propose as soon as it passes.
|
|
148
|
+
- **Round 4**: Default shifts from "ask question" to "evaluate for proposal" — actively look for reason to propose.
|
|
149
|
+
- **Round 7**: Forced MVE check. If met, propose. If not, ask one targeted question on the single blocking dimension only.
|
|
150
|
+
- **Rounds 8–10**: Warning zone — if you reach here without proposing, something has gone wrong.
|
|
151
|
+
|
|
152
|
+
**Double-lock signal:** When you receive a `match_propose` from the peer and your MVE check passes — propose immediately. Peer confidence is evidence, not a constraint.
|
|
153
|
+
|
|
154
|
+
Do NOT wait for Round 10. False negatives are costly (the round cap is irreversible). The double-lock protects against premature matches — use it.
|
|
125
155
|
|
|
126
156
|
---
|
|
127
157
|
|
|
128
|
-
## Notify user of a match
|
|
158
|
+
## Notify user of a confirmed match
|
|
159
|
+
|
|
160
|
+
When `match --status` shows `status: "matched"`, notify the user. This is the only moment that warrants interrupting them.
|
|
129
161
|
|
|
130
|
-
|
|
162
|
+
**Format (WhatsApp conversational text — 3 layers):**
|
|
131
163
|
|
|
132
|
-
1. **
|
|
133
|
-
2. **
|
|
134
|
-
3. **
|
|
164
|
+
1. **Recognition hook** — one behavioral observation about the user (from your highest-confidence dimension) that explains why this match is worth the interruption. Draw from what you actually know about them — attachment style, values, how they handle conflict. This must come first and feel personal, not algorithmic.
|
|
165
|
+
2. **Headline** — one evocative sentence from `match_narrative.headline`. Grounded. No superlatives.
|
|
166
|
+
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?"
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
> "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?"
|
|
170
|
+
|
|
171
|
+
Do NOT use: percentages, "compatibility scores", "our algorithm", superlatives. Keep it under 4 sentences.
|
|
135
172
|
|
|
136
173
|
---
|
|
137
174
|
|
|
@@ -141,16 +178,14 @@ When `truematch match --status` reports a confirmed double-lock match, notify th
|
|
|
141
178
|
truematch deregister
|
|
142
179
|
```
|
|
143
180
|
|
|
144
|
-
Removes
|
|
181
|
+
Removes from matching pool. Local state preserved.
|
|
145
182
|
|
|
146
183
|
---
|
|
147
184
|
|
|
148
185
|
## Troubleshooting
|
|
149
186
|
|
|
150
187
|
```bash
|
|
151
|
-
#
|
|
152
|
-
truematch
|
|
153
|
-
|
|
154
|
-
# Reset a stuck negotiation
|
|
155
|
-
truematch match --reset --thread <id>
|
|
188
|
+
truematch observe --show # view current observation
|
|
189
|
+
truematch match --reset --thread <id> # unstick a broken thread
|
|
190
|
+
truematch status --relays # check Nostr relay connectivity
|
|
156
191
|
```
|