a2acalling 0.6.18 → 0.6.20

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/bin/cli.js CHANGED
@@ -335,35 +335,27 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
335
335
 
336
336
  // Sync tier config from manifest
337
337
  const manifest = result.manifest;
338
- function flattenTopics(sections) {
339
- const out = [];
340
- for (const section of sections) {
341
- for (const item of section) {
342
- const t = String(item && item.topic || '').trim();
343
- if (t && !out.includes(t)) out.push(t);
344
- }
345
- }
346
- return out;
338
+
339
+ // Helper to extract topic names
340
+ function getTierTopics(tierData) {
341
+ if (!tierData || !Array.isArray(tierData.topics)) return [];
342
+ return tierData.topics.map(t => String(t && t.topic || '').trim()).filter(Boolean);
347
343
  }
348
344
 
345
+ // Get tiers data (support both new 'tiers' key and legacy 'topics' key)
346
+ const tiersData = manifest.tiers || manifest.topics || {};
347
+
349
348
  try {
350
349
  config.setTier('public', {
351
- topics: flattenTopics([manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect]),
350
+ topics: getTierTopics(tiersData.public),
352
351
  disclosure: 'public'
353
352
  });
354
353
  config.setTier('friends', {
355
- topics: flattenTopics([
356
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
357
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect
358
- ]),
354
+ topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends)],
359
355
  disclosure: 'minimal'
360
356
  });
361
357
  config.setTier('family', {
362
- topics: flattenTopics([
363
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
364
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect,
365
- manifest.topics.family.lead_with, manifest.topics.family.discuss_freely, manifest.topics.family.deflect
366
- ]),
358
+ topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends), ...getTierTopics(tiersData.family)],
367
359
  disclosure: 'minimal'
368
360
  });
369
361
  } catch (err) {
@@ -382,10 +374,7 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
382
374
  const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
383
375
  if (args.flags.name) config.setAgent({ name: agentName });
384
376
 
385
- const publicTopics = flattenTopics([
386
- manifest.topics.public.lead_with,
387
- manifest.topics.public.discuss_freely
388
- ]);
377
+ const publicTopics = getTierTopics(tiersData.public);
389
378
 
390
379
  const { token } = store.create({
391
380
  name: agentName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.18",
3
+ "version": "0.6.20",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -32,7 +32,37 @@ function dedupeByTopic(items) {
32
32
  seen.add(topic.toLowerCase());
33
33
  out.push({
34
34
  topic,
35
- detail: normalizeTopic(item && item.detail)
35
+ description: normalizeTopic(item && (item.description || item.detail))
36
+ });
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function dedupeByObjective(items) {
42
+ const seen = new Set();
43
+ const out = [];
44
+ for (const item of items) {
45
+ const objective = normalizeTopic(item && item.objective);
46
+ if (!objective || seen.has(objective.toLowerCase())) continue;
47
+ seen.add(objective.toLowerCase());
48
+ out.push({
49
+ objective,
50
+ description: normalizeTopic(item && item.description)
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function dedupeDoNotDiscuss(items) {
57
+ const seen = new Set();
58
+ const out = [];
59
+ for (const item of items) {
60
+ const topic = normalizeTopic(item && item.topic);
61
+ if (!topic || seen.has(topic.toLowerCase())) continue;
62
+ seen.add(topic.toLowerCase());
63
+ out.push({
64
+ topic,
65
+ reason: normalizeTopic(item && item.reason)
36
66
  });
37
67
  }
38
68
  return out;
@@ -118,11 +148,11 @@ function saveManifest(manifest) {
118
148
  * Get topics for a given tier, merged down the hierarchy.
119
149
  * family gets everything, friends gets friends+public, public gets public only.
120
150
  *
121
- * Returns { lead_with, discuss_freely, deflect, never_disclose }
151
+ * Returns { topics, objectives, do_not_discuss, never_disclose }
122
152
  */
123
153
  function getTopicsForTier(tier) {
124
154
  const manifest = loadManifest();
125
- const topics = manifest.topics || {};
155
+ const tiers = manifest.tiers || manifest.topics || {};
126
156
 
127
157
  const tierIndex = TIER_HIERARCHY.indexOf(tier);
128
158
  if (tierIndex === -1) {
@@ -134,26 +164,27 @@ function getTopicsForTier(tier) {
134
164
  const tiersToMerge = TIER_HIERARCHY.slice(0, tierIndex + 1);
135
165
 
136
166
  const merged = {
137
- lead_with: [],
138
- discuss_freely: [],
139
- deflect: [],
167
+ topics: [],
168
+ objectives: [],
169
+ do_not_discuss: [],
140
170
  never_disclose: manifest.never_disclose || []
141
171
  };
142
172
 
143
173
  for (const t of tiersToMerge) {
144
- const tierTopics = topics[t] || {};
145
- if (tierTopics.lead_with) merged.lead_with.push(...tierTopics.lead_with);
146
- if (tierTopics.discuss_freely) merged.discuss_freely.push(...tierTopics.discuss_freely);
147
- if (tierTopics.deflect) merged.deflect.push(...tierTopics.deflect);
174
+ const tierData = tiers[t] || {};
175
+ if (tierData.topics) merged.topics.push(...tierData.topics);
176
+ if (tierData.objectives) merged.objectives.push(...tierData.objectives);
177
+ if (tierData.do_not_discuss) merged.do_not_discuss.push(...tierData.do_not_discuss);
148
178
  }
149
179
 
150
- // Deflect items: remove any that already appear in lead_with or discuss_freely
151
- // (higher tiers promote topics from deflect to discuss/lead)
152
- const promoted = new Set([
153
- ...merged.lead_with.map(t => t.topic),
154
- ...merged.discuss_freely.map(t => t.topic)
155
- ]);
156
- merged.deflect = merged.deflect.filter(t => !promoted.has(t.topic));
180
+ // Remove do_not_discuss items that appear in topics (higher tiers promote them)
181
+ const promoted = new Set(merged.topics.map(t => (t.topic || '').toLowerCase()));
182
+ merged.do_not_discuss = merged.do_not_discuss.filter(t => !promoted.has((t.topic || '').toLowerCase()));
183
+
184
+ // Dedupe
185
+ merged.topics = dedupeByTopic(merged.topics);
186
+ merged.objectives = dedupeByObjective(merged.objectives);
187
+ merged.do_not_discuss = dedupeDoNotDiscuss(merged.do_not_discuss);
157
188
 
158
189
  return merged;
159
190
  }
@@ -162,15 +193,25 @@ function getTopicsForTier(tier) {
162
193
  * Format topic lists into readable bullet points for prompt injection.
163
194
  */
164
195
  function formatTopicsForPrompt(tierTopics) {
165
- const formatList = (items) => {
196
+ const formatTopicList = (items) => {
166
197
  if (!items || items.length === 0) return ' (none specified)';
167
- return items.map(item => ` - ${item.topic}: ${item.detail}`).join('\n');
198
+ return items.map(item => ` - ${item.topic}: ${item.description || item.detail || ''}`).join('\n');
199
+ };
200
+
201
+ const formatObjectiveList = (items) => {
202
+ if (!items || items.length === 0) return ' (none specified)';
203
+ return items.map(item => ` - ${item.objective}: ${item.description || ''}`).join('\n');
204
+ };
205
+
206
+ const formatDoNotDiscuss = (items) => {
207
+ if (!items || items.length === 0) return ' (none specified)';
208
+ return items.map(item => ` - ${item.topic}: ${item.reason || ''}`).join('\n');
168
209
  };
169
210
 
170
211
  return {
171
- leadWithTopics: formatList(tierTopics.lead_with),
172
- discussFreelyTopics: formatList(tierTopics.discuss_freely),
173
- deflectTopics: formatList(tierTopics.deflect),
212
+ topics: formatTopicList(tierTopics.topics),
213
+ objectives: formatObjectiveList(tierTopics.objectives),
214
+ doNotDiscuss: formatDoNotDiscuss(tierTopics.do_not_discuss),
174
215
  neverDisclose: tierTopics.never_disclose?.length
175
216
  ? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
176
217
  : ' (none specified)'
@@ -226,86 +267,71 @@ function generateDefaultManifest(contextFiles = {}) {
226
267
 
227
268
  if (candidateTopics.length === 0) {
228
269
  return {
229
- version: 1,
270
+ version: 2,
230
271
  generated_at: now,
231
272
  updated_at: now,
232
- topics: {
273
+ tiers: {
233
274
  public: {
234
- lead_with: [{ topic: 'What I do', detail: 'Brief professional description' }],
235
- discuss_freely: [{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }],
236
- deflect: [{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }]
275
+ topics: [{ topic: 'What I do', description: 'Brief professional description' }],
276
+ objectives: [{ objective: 'Networking', description: 'Connect with others in the field' }],
277
+ do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
237
278
  },
238
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
239
- family: { lead_with: [], discuss_freely: [], deflect: [] }
279
+ friends: { topics: [], objectives: [], do_not_discuss: [] },
280
+ family: { topics: [], objectives: [], do_not_discuss: [] }
240
281
  },
241
282
  never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
242
283
  personality_notes: 'Direct and technical. Prefers depth over breadth.'
243
284
  };
244
285
  }
245
286
 
246
- const publicLead = [];
247
- const publicDiscuss = [];
248
- const publicDeflect = [];
249
- const friendsLead = [];
250
- const friendsDiscuss = [];
251
- const familyDiscuss = [];
287
+ const publicTopics = [];
288
+ const publicObjectives = [];
289
+ const friendsTopics = [];
290
+ const friendsObjectives = [];
291
+ const familyTopics = [];
252
292
 
253
293
  candidateTopics.forEach((entry, index) => {
254
- const topic = truncateAtWordBoundary(entry.topic || '', 60);
255
- const detail = truncateAtWordBoundary(entry.detail || 'Open discussion topic.', 120);
294
+ const topic = truncateAtWordBoundary(entry.topic || '', 160);
295
+ const description = truncateAtWordBoundary(entry.description || entry.detail || 'Open discussion topic.', 500);
256
296
  if (!topic) return;
257
297
 
258
- const node = { topic, detail };
259
- if (index < 2) {
260
- publicLead.push(node);
298
+ const node = { topic, description };
299
+ if (index < 5) {
300
+ publicTopics.push(node);
261
301
  return;
262
302
  }
263
- if (index < 6) {
264
- publicDiscuss.push(node);
303
+ if (index < 10) {
304
+ friendsTopics.push(node);
265
305
  return;
266
306
  }
267
- if (index < 8) {
268
- friendsLead.push(node);
269
- return;
270
- }
271
- if (index < 12) {
272
- friendsDiscuss.push(node);
273
- return;
274
- }
275
- if (index < 14) {
276
- familyDiscuss.push(node);
277
- }
307
+ familyTopics.push(node);
278
308
  });
279
309
 
280
- if (publicLead.length === 0) {
281
- publicLead.push({ topic: 'Open source', detail: 'General product and engineering topics.' });
282
- }
283
- if (publicDiscuss.length === 0) {
284
- publicDiscuss.push({ topic: 'Collaboration', detail: 'Ways to collaborate and support each other.' });
285
- }
286
- if (publicDeflect.length === 0) {
287
- publicDeflect.push({ topic: 'Personal details', detail: 'Redirect to direct owner contact.' });
310
+ if (publicTopics.length === 0) {
311
+ publicTopics.push({ topic: 'Open source', description: 'General product and engineering topics.' });
288
312
  }
289
313
 
290
314
  return {
291
- version: 1,
315
+ version: 2,
292
316
  generated_at: now,
293
317
  updated_at: now,
294
- topics: {
318
+ tiers: {
295
319
  public: {
296
- lead_with: publicLead,
297
- discuss_freely: publicDiscuss,
298
- deflect: publicDeflect
320
+ topics: publicTopics,
321
+ objectives: publicObjectives.length > 0 ? publicObjectives : [
322
+ { objective: 'Grow network', description: 'Connect with others working on similar problems' }
323
+ ],
324
+ do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
299
325
  },
300
326
  friends: {
301
- lead_with: friendsLead,
302
- discuss_freely: friendsDiscuss,
303
- deflect: []
327
+ topics: friendsTopics,
328
+ objectives: friendsObjectives,
329
+ do_not_discuss: []
304
330
  },
305
331
  family: {
306
- lead_with: [],
307
- discuss_freely: familyDiscuss,
308
- deflect: []
332
+ topics: familyTopics,
333
+ objectives: [],
334
+ do_not_discuss: []
309
335
  }
310
336
  },
311
337
  never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
@@ -337,16 +363,17 @@ function validateDisclosureSubmission(data) {
337
363
  return { valid: false, manifest: null, errors: ['Submission must be a non-null object'] };
338
364
  }
339
365
 
340
- // Require topics object
341
- if (!data.topics || typeof data.topics !== 'object' || Array.isArray(data.topics)) {
342
- errors.push('Submission must include a "topics" object');
366
+ // Support both new format (tiers) and legacy format (topics)
367
+ const tiersData = data.tiers || data.topics;
368
+ if (!tiersData || typeof tiersData !== 'object' || Array.isArray(tiersData)) {
369
+ errors.push('Submission must include a "tiers" object (or legacy "topics" object)');
343
370
  return { valid: false, manifest: null, errors };
344
371
  }
345
372
 
346
373
  // Require all three tiers
347
374
  for (const tier of TIER_HIERARCHY) {
348
- if (!data.topics[tier] || typeof data.topics[tier] !== 'object') {
349
- errors.push(`Missing required tier: "${tier}" in topics`);
375
+ if (!tiersData[tier] || typeof tiersData[tier] !== 'object') {
376
+ errors.push(`Missing required tier: "${tier}"`);
350
377
  }
351
378
  }
352
379
  if (errors.length > 0) {
@@ -354,42 +381,78 @@ function validateDisclosureSubmission(data) {
354
381
  }
355
382
 
356
383
  // Reject extra tiers beyond the known hierarchy
357
- const extraTiers = Object.keys(data.topics).filter(t => !TIER_HIERARCHY.includes(t));
384
+ const extraTiers = Object.keys(tiersData).filter(t => !TIER_HIERARCHY.includes(t));
358
385
  if (extraTiers.length > 0) {
359
386
  errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
360
387
  }
361
388
 
362
- // Validate each tier's structure
363
- const requiredLists = ['lead_with', 'discuss_freely', 'deflect'];
364
- const LIST_LIMITS = { lead_with: 10, discuss_freely: 20, deflect: 10 };
389
+ const LIST_LIMITS = { topics: 15, objectives: 8, do_not_discuss: 10 };
390
+
365
391
  for (const tier of TIER_HIERARCHY) {
366
- const tierData = data.topics[tier];
367
- for (const cat of requiredLists) {
368
- if (!Array.isArray(tierData[cat])) {
369
- errors.push(`topics.${tier}.${cat} must be an array`);
370
- continue;
371
- }
372
- if (tierData[cat].length > LIST_LIMITS[cat]) {
373
- errors.push(`topics.${tier}.${cat} has ${tierData[cat].length} items max ${LIST_LIMITS[cat]}`);
374
- }
375
- for (let i = 0; i < tierData[cat].length; i++) {
376
- const item = tierData[cat][i];
377
- if (!item || typeof item !== 'object' || typeof item.topic !== 'string' || typeof item.detail !== 'string') {
378
- errors.push(`topics.${tier}.${cat}[${i}]: each topic item must have "topic" (string) and "detail" (string)`);
379
- continue;
392
+ const tierData = tiersData[tier];
393
+
394
+ // Validate topics array
395
+ if (tierData.topics !== undefined) {
396
+ if (!Array.isArray(tierData.topics)) {
397
+ errors.push(`tiers.${tier}.topics must be an array`);
398
+ } else {
399
+ if (tierData.topics.length > LIST_LIMITS.topics) {
400
+ errors.push(`tiers.${tier}.topics has ${tierData.topics.length} items — max ${LIST_LIMITS.topics}`);
380
401
  }
381
- if (item.topic.trim().length === 0) {
382
- errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
383
- continue;
402
+ for (let i = 0; i < tierData.topics.length; i++) {
403
+ const item = tierData.topics[i];
404
+ if (!item || typeof item !== 'object' || typeof item.topic !== 'string') {
405
+ errors.push(`tiers.${tier}.topics[${i}]: must have "topic" (string) and "description" (string)`);
406
+ continue;
407
+ }
408
+ if (item.topic.trim().length === 0) {
409
+ errors.push(`tiers.${tier}.topics[${i}].topic must not be empty`);
410
+ }
411
+ if (item.topic.length > 160) {
412
+ errors.push(`tiers.${tier}.topics[${i}]: topic exceeds 160 chars`);
413
+ }
414
+ const desc = item.description || '';
415
+ if (desc.length > 500) {
416
+ errors.push(`tiers.${tier}.topics[${i}]: description exceeds 500 chars`);
417
+ }
384
418
  }
385
- if (item.topic.length > 160) {
386
- errors.push(`topics.${tier}.${cat}[${i}]: topic exceeds 160 character limit (got ${item.topic.length})`);
419
+ }
420
+ }
421
+
422
+ // Validate objectives array
423
+ if (tierData.objectives !== undefined) {
424
+ if (!Array.isArray(tierData.objectives)) {
425
+ errors.push(`tiers.${tier}.objectives must be an array`);
426
+ } else {
427
+ if (tierData.objectives.length > LIST_LIMITS.objectives) {
428
+ errors.push(`tiers.${tier}.objectives has ${tierData.objectives.length} items — max ${LIST_LIMITS.objectives}`);
387
429
  }
388
- if (item.detail.length > 500) {
389
- errors.push(`topics.${tier}.${cat}[${i}]: detail exceeds 500 character limit (got ${item.detail.length})`);
430
+ for (let i = 0; i < tierData.objectives.length; i++) {
431
+ const item = tierData.objectives[i];
432
+ if (!item || typeof item !== 'object' || typeof item.objective !== 'string') {
433
+ errors.push(`tiers.${tier}.objectives[${i}]: must have "objective" (string) and "description" (string)`);
434
+ continue;
435
+ }
436
+ if (item.objective.trim().length === 0) {
437
+ errors.push(`tiers.${tier}.objectives[${i}].objective must not be empty`);
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ // Validate do_not_discuss array
444
+ if (tierData.do_not_discuss !== undefined) {
445
+ if (!Array.isArray(tierData.do_not_discuss)) {
446
+ errors.push(`tiers.${tier}.do_not_discuss must be an array`);
447
+ } else {
448
+ if (tierData.do_not_discuss.length > LIST_LIMITS.do_not_discuss) {
449
+ errors.push(`tiers.${tier}.do_not_discuss has ${tierData.do_not_discuss.length} items — max ${LIST_LIMITS.do_not_discuss}`);
390
450
  }
391
- if (isTechnicalContent(item.topic) || isTechnicalContent(item.detail)) {
392
- errors.push(`topics.${tier}.${cat}[${i}]: contains technical content (code, URLs, or markdown formatting) — use plain language`);
451
+ for (let i = 0; i < tierData.do_not_discuss.length; i++) {
452
+ const item = tierData.do_not_discuss[i];
453
+ if (!item || typeof item !== 'object' || typeof item.topic !== 'string') {
454
+ errors.push(`tiers.${tier}.do_not_discuss[${i}]: must have "topic" (string) and "reason" (string)`);
455
+ }
393
456
  }
394
457
  }
395
458
  }
@@ -426,25 +489,33 @@ function validateDisclosureSubmission(data) {
426
489
  return { valid: false, manifest: null, errors };
427
490
  }
428
491
 
429
- // Rebuild topics from only validated keys to prevent extra properties passing through
430
- const cleanTopics = {};
492
+ // Rebuild clean structure
493
+ const now = new Date().toISOString();
494
+
495
+ // Build clean manifest with new format only
496
+ const cleanTiers = {};
431
497
  for (const tier of TIER_HIERARCHY) {
432
- cleanTopics[tier] = {};
433
- for (const cat of ['lead_with', 'discuss_freely', 'deflect']) {
434
- cleanTopics[tier][cat] = (data.topics[tier][cat] || []).map(item => ({
498
+ cleanTiers[tier] = {
499
+ topics: (tiersData[tier].topics || []).map(item => ({
435
500
  topic: item.topic,
436
- detail: item.detail
437
- }));
438
- }
501
+ description: item.description || ''
502
+ })),
503
+ objectives: (tiersData[tier].objectives || []).map(item => ({
504
+ objective: item.objective,
505
+ description: item.description || ''
506
+ })),
507
+ do_not_discuss: (tiersData[tier].do_not_discuss || []).map(item => ({
508
+ topic: item.topic,
509
+ reason: item.reason || ''
510
+ }))
511
+ };
439
512
  }
440
513
 
441
- // Build valid manifest
442
- const now = new Date().toISOString();
443
514
  const manifest = {
444
- version: 1,
515
+ version: 2,
445
516
  generated_at: now,
446
517
  updated_at: now,
447
- topics: cleanTopics,
518
+ tiers: cleanTiers,
448
519
  never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
449
520
  personality_notes: data.personality_notes || ''
450
521
  };
@@ -520,7 +591,35 @@ function buildExtractionPrompt(availableFiles) {
520
591
  Look for these files in your workspace directory and read the ones that exist. Extract disclosure topics from USER.md and SOUL.md primarily.`;
521
592
  }
522
593
 
523
- const jsonBlock = '```json\n{\n "topics": {\n "public": {\n "lead_with": [\n { "topic": "Short label (max 160 chars)", "detail": "Longer description of the topic" }\n ],\n "discuss_freely": [],\n "deflect": []\n },\n "friends": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n },\n "family": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n }\n },\n "never_disclose": ["API keys", "Credentials", "Financial figures"],\n "personality_notes": "Brief description of communication style"\n}\n```';
594
+ const jsonBlock = `\`\`\`json
595
+ {
596
+ "tiers": {
597
+ "public": {
598
+ "topics": [
599
+ { "topic": "Short label (max 160 chars)", "description": "Longer description of the topic" }
600
+ ],
601
+ "objectives": [
602
+ { "objective": "What you want to achieve", "description": "Longer description of this goal" }
603
+ ],
604
+ "do_not_discuss": [
605
+ { "topic": "Topic to avoid", "reason": "Why this should be redirected" }
606
+ ]
607
+ },
608
+ "friends": {
609
+ "topics": [],
610
+ "objectives": [],
611
+ "do_not_discuss": []
612
+ },
613
+ "family": {
614
+ "topics": [],
615
+ "objectives": [],
616
+ "do_not_discuss": []
617
+ }
618
+ },
619
+ "never_disclose": ["API keys", "Credentials", "Financial figures"],
620
+ "personality_notes": "Brief description of communication style"
621
+ }
622
+ \`\`\``;
524
623
 
525
624
  return `## A2A Disclosure Extraction
526
625
 
@@ -530,21 +629,35 @@ ${fileSection}
530
629
 
531
630
  Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
532
631
 
533
- ### What to extract
632
+ ### Tier Inheritance
633
+
634
+ - **public** — base tier, anyone can see these
635
+ - **friends** — inherits all PUBLIC topics/objectives, plus additional friend-only items
636
+ - **family** — inherits all FRIENDS and PUBLIC items, plus additional family-only items
637
+
638
+ Family callers see everything. Friends see friends + public. Public callers see only public.
639
+
640
+ ### What to extract for each tier
534
641
 
535
- For each trust tier, identify topics the owner would want to discuss:
642
+ **topics** Things the owner is interested in or working on:
643
+ - Professional role and expertise
644
+ - Current projects and interests
645
+ - Hobbies and activities
646
+ - Max 8 topics per tier
536
647
 
537
- - **public** — safe for anyone: professional role, public interests, general project descriptions
538
- - **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
539
- - **family** — inner circle only: personal interests, private projects, sensitive plans
648
+ **objectives** — What the owner wants to achieve in conversations:
649
+ - Networking goals
650
+ - Collaboration interests
651
+ - Opportunities they're seeking
652
+ - Max 4 objectives per tier
540
653
 
541
- For each tier, categorize topics as:
542
- - **lead_with** proactively bring up (max 3 per tier)
543
- - **discuss_freely** — happy to discuss if asked (max 8 per tier)
544
- - **deflect** — redirect or decline (max 3 per tier)
654
+ **do_not_discuss** Topics to redirect or decline (can be empty):
655
+ - Personal matters (for public tier)
656
+ - Sensitive subjects
657
+ - Max 3 per tier
545
658
 
546
659
  Also identify:
547
- - **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
660
+ - **never_disclose** — information that should NEVER be shared regardless of tier (API keys, credentials, financial data, etc.)
548
661
  - **personality_notes** — a 1-2 sentence description of the owner's communication style
549
662
 
550
663
  ### What NOT to extract
@@ -565,11 +678,11 @@ ${jsonBlock}
565
678
  ### Rules
566
679
 
567
680
  1. Each "topic" string must be a short, human-readable label (max 160 chars)
568
- 2. Each "detail" string explains the topic more fully (max 500 chars)
681
+ 2. Each "description" string explains the topic more fully (max 500 chars)
569
682
  3. Topics should be things a person would discuss, not technical artifacts
570
- 4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
683
+ 4. Higher tiers inherit lower-tier items automatically — only add NEW items at each tier
571
684
  5. Present this to the owner for review before submitting
572
- 6. The owner may edit, remove, or add topics before final submission`;
685
+ 6. The owner may edit, remove, or add items before final submission`;
573
686
  }
574
687
 
575
688
  module.exports = {
@@ -49,9 +49,9 @@ function buildConnectionPrompt(options) {
49
49
  } = options;
50
50
 
51
51
  const {
52
- leadWithTopics = ' (none specified)',
53
- discussFreelyTopics = ' (none specified)',
54
- deflectTopics = ' (none specified)',
52
+ topics = ' (none specified)',
53
+ objectives = ' (none specified)',
54
+ doNotDiscuss = ' (none specified)',
55
55
  neverDisclose = ' (none specified)'
56
56
  } = tierTopics || {};
57
57
 
@@ -62,17 +62,16 @@ Your job is NOT to answer questions and hang up. Your job is to have a real conv
62
62
 
63
63
  == WHAT YOU BRING TO THE TABLE ==
64
64
 
65
- ${ownerName} is currently focused on:
66
- ${leadWithTopics}
65
+ ${ownerName}'s topics of interest:
66
+ ${topics}
67
67
 
68
- Other areas of active interest:
69
- ${discussFreelyTopics}
68
+ == OBJECTIVES FOR THIS CALL ==
70
69
 
71
- == STRATEGIC GOALS FOR THIS CALL ==
72
-
73
- ${tierGoals && tierGoals.length > 0
74
- ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}\n\nPursue these goals naturally during conversation. Surface opportunities that align with them.`
75
- : `No specific goals configured for this tier. Focus on general discovery and relationship building.`}
70
+ ${objectives !== ' (none specified)'
71
+ ? `What ${ownerName} wants to achieve:\n${objectives}`
72
+ : (tierGoals && tierGoals.length > 0
73
+ ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}`
74
+ : `No specific objectives configured. Focus on general discovery and relationship building.`)}
76
75
 
77
76
  == WHAT THEY SHARED WITH YOU ==
78
77
 
@@ -123,16 +122,13 @@ PACING RULES:
123
122
 
124
123
  Access level for this call: ${accessTier}
125
124
 
126
- LEAD WITH (proactively share):
127
- ${leadWithTopics}
128
-
129
- DISCUSS FREELY (share when relevant):
130
- ${discussFreelyTopics}
125
+ TOPICS (discuss openly):
126
+ ${topics}
131
127
 
132
- DEFLECT (acknowledge but redirect):
133
- ${deflectTopics}
128
+ DO NOT DISCUSS (redirect):
129
+ ${doNotDiscuss}
134
130
 
135
- When deflecting, don't say "I can't discuss that." Instead redirect naturally, acknowledge without detail, or suggest the owners connect directly.
131
+ When redirecting, don't say "I can't discuss that." Instead redirect naturally, acknowledge without detail, or suggest the owners connect directly.
136
132
 
137
133
  NEVER disclose:
138
134
  ${neverDisclose}
@@ -178,9 +174,9 @@ function buildAdaptiveConnectionPrompt(options) {
178
174
  } = options;
179
175
 
180
176
  const {
181
- leadWithTopics = ' (none specified)',
182
- discussFreelyTopics = ' (none specified)',
183
- deflectTopics = ' (none specified)',
177
+ topics = ' (none specified)',
178
+ objectives = ' (none specified)',
179
+ doNotDiscuss = ' (none specified)',
184
180
  neverDisclose = ' (none specified)'
185
181
  } = tierTopics || {};
186
182
 
@@ -217,17 +213,16 @@ ${openQuestions}
217
213
 
218
214
  == WHAT YOU BRING TO THE TABLE ==
219
215
 
220
- ${ownerName} is currently focused on:
221
- ${leadWithTopics}
216
+ ${ownerName}'s topics of interest:
217
+ ${topics}
222
218
 
223
- Other areas of active interest:
224
- ${discussFreelyTopics}
219
+ == OBJECTIVES FOR THIS CALL ==
225
220
 
226
- == STRATEGIC GOALS FOR THIS CALL ==
227
-
228
- ${tierGoals && tierGoals.length > 0
229
- ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}\n\nPursue these goals naturally during conversation. Surface opportunities that align with them.`
230
- : `No specific goals configured for this tier. Focus on general discovery and relationship building.`}
221
+ ${objectives !== ' (none specified)'
222
+ ? `What ${ownerName} wants to achieve:\n${objectives}`
223
+ : (tierGoals && tierGoals.length > 0
224
+ ? `At the ${accessTier} access level, ${ownerName}'s objectives are:\n${formatList(tierGoals)}`
225
+ : `No specific objectives configured. Focus on general discovery and relationship building.`)}
231
226
 
232
227
  == WHAT THEY SHARED WITH YOU ==
233
228
 
@@ -265,16 +260,13 @@ Pacing:
265
260
 
266
261
  Access level for this call: ${accessTier}
267
262
 
268
- LEAD WITH (proactively share):
269
- ${leadWithTopics}
270
-
271
- DISCUSS FREELY (share when relevant):
272
- ${discussFreelyTopics}
263
+ TOPICS (discuss openly):
264
+ ${topics}
273
265
 
274
- DEFLECT (acknowledge but redirect):
275
- ${deflectTopics}
266
+ DO NOT DISCUSS (redirect):
267
+ ${doNotDiscuss}
276
268
 
277
- When deflecting, do not mention policy mechanics. Redirect naturally and suggest direct owner follow-up when needed.
269
+ When redirecting, do not mention policy mechanics. Redirect naturally and suggest direct owner follow-up when needed.
278
270
 
279
271
  NEVER disclose:
280
272
  ${neverDisclose}
@@ -1032,9 +1032,9 @@ function createDashboardApiRouter(options = {}) {
1032
1032
  disclosure: configTier.disclosure || 'minimal',
1033
1033
  examples: sanitizeStringArray(configTier.examples || [], 20, 120),
1034
1034
  manifest: {
1035
- lead_with: manifestTier.lead_with || [],
1036
- discuss_freely: manifestTier.discuss_freely || [],
1037
- deflect: manifestTier.deflect || []
1035
+ topics: manifestTier.topics || [],
1036
+ objectives: manifestTier.objectives || [],
1037
+ do_not_discuss: manifestTier.do_not_discuss || []
1038
1038
  }
1039
1039
  };
1040
1040
  });
@@ -1082,11 +1082,11 @@ function createDashboardApiRouter(options = {}) {
1082
1082
 
1083
1083
  if (body.manifest) {
1084
1084
  const manifest = loadManifest();
1085
- manifest.topics = manifest.topics || {};
1086
- manifest.topics[tierId] = {
1087
- lead_with: parseTopicObjects(body.manifest.lead_with),
1088
- discuss_freely: parseTopicObjects(body.manifest.discuss_freely),
1089
- deflect: parseTopicObjects(body.manifest.deflect)
1085
+ manifest.tiers = manifest.tiers || {};
1086
+ manifest.tiers[tierId] = {
1087
+ topics: parseTopicObjects(body.manifest.topics),
1088
+ objectives: parseTopicObjects(body.manifest.objectives),
1089
+ do_not_discuss: parseTopicObjects(body.manifest.do_not_discuss)
1090
1090
  };
1091
1091
  saveManifest(manifest);
1092
1092
  }
package/src/server.js CHANGED
@@ -184,24 +184,34 @@ function extractSignalPhrases(text, pattern, maxItems = 3) {
184
184
 
185
185
  function collectTopicKeywords(tierTopics) {
186
186
  const keywords = new Set();
187
- const sourceLists = ['lead_with', 'discuss_freely'];
187
+
188
+ const topicsList = tierTopics?.topics || [];
189
+ const objectivesList = tierTopics?.objectives || [];
190
+
191
+ for (const item of topicsList) {
192
+ for (const part of [item?.topic, item?.description, item?.detail]) {
193
+ if (!part) continue;
194
+ const terms = String(part)
195
+ .toLowerCase()
196
+ .split(/[^a-z0-9]+/)
197
+ .filter(term => term.length >= 4);
198
+ for (const term of terms.slice(0, 6)) {
199
+ keywords.add(term);
200
+ if (keywords.size >= 48) return Array.from(keywords);
201
+ }
202
+ }
203
+ }
188
204
 
189
- for (const listName of sourceLists) {
190
- for (const item of tierTopics?.[listName] || []) {
191
- for (const part of [item?.topic, item?.detail]) {
192
- if (!part) {
193
- continue;
194
- }
195
- const terms = String(part)
196
- .toLowerCase()
197
- .split(/[^a-z0-9]+/)
198
- .filter(term => term.length >= 4);
199
- for (const term of terms.slice(0, 6)) {
200
- keywords.add(term);
201
- if (keywords.size >= 48) {
202
- return Array.from(keywords);
203
- }
204
- }
205
+ for (const item of objectivesList) {
206
+ for (const part of [item?.objective, item?.description]) {
207
+ if (!part) continue;
208
+ const terms = String(part)
209
+ .toLowerCase()
210
+ .split(/[^a-z0-9]+/)
211
+ .filter(term => term.length >= 4);
212
+ for (const term of terms.slice(0, 6)) {
213
+ keywords.add(term);
214
+ if (keywords.size >= 48) return Array.from(keywords);
205
215
  }
206
216
  }
207
217
  }