a2acalling 0.6.4 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  "a2acalling": "scripts/install-openclaw.js"
9
9
  },
10
10
  "scripts": {
11
+ "postinstall": "node scripts/postinstall.js",
11
12
  "start": "node src/server.js",
12
13
  "test": "node test/run.js"
13
14
  },
@@ -153,7 +153,7 @@ function ensureConfigAndManifest(inviteHost, port, options = {}) {
153
153
 
154
154
  try {
155
155
  const { A2AConfig } = require('../src/lib/config');
156
- const { loadManifest, saveManifest, generateDefaultManifest, readContextFiles } = require('../src/lib/disclosure');
156
+ const { loadManifest, saveManifest, generateDefaultManifest } = require('../src/lib/disclosure');
157
157
 
158
158
  const config = new A2AConfig();
159
159
  const defaults = config.getDefaults() || {};
@@ -167,8 +167,7 @@ function ensureConfigAndManifest(inviteHost, port, options = {}) {
167
167
 
168
168
  const manifest = loadManifest();
169
169
  if (!manifest || Object.keys(manifest).length === 0) {
170
- const contextFiles = readContextFiles(process.cwd());
171
- const generated = generateDefaultManifest(contextFiles);
170
+ const generated = generateDefaultManifest();
172
171
  saveManifest(generated);
173
172
  const manifestFile = path.join(configDir, 'a2a-disclosure.json');
174
173
  log(`Generated default disclosure manifest: ${manifestFile}`);
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Only show the banner for global installs; skip in CI, dev, and Docker builds.
4
+ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
5
+ if (process.env.npm_config_global !== 'true') process.exit(0);
6
+
7
+ console.log(`
8
+ ╔══════════════════════════════════════════════════════════╗
9
+ ║ 🤝 A2A Calling installed successfully! ║
10
+ ╠══════════════════════════════════════════════════════════╣
11
+ ║ ║
12
+ ║ Next step: ║
13
+ ║ a2a quickstart --hostname YOUR_DOMAIN:PORT ║
14
+ ║ ║
15
+ ║ Example: ║
16
+ ║ a2a quickstart --hostname myserver.com:3001 ║
17
+ ║ ║
18
+ ║ This will: ║
19
+ ║ • Configure your agent's disclosure topics ║
20
+ ║ • Set up permission tiers (public/friends/family) ║
21
+ ║ • Start the A2A server ║
22
+ ║ • Generate your first invite to share ║
23
+ ║ ║
24
+ ╚══════════════════════════════════════════════════════════╝
25
+ `);
@@ -17,6 +17,62 @@ const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
17
17
 
18
18
  const TIER_HIERARCHY = ['public', 'friends', 'family'];
19
19
  const logger = createLogger({ component: 'a2a.disclosure' });
20
+ const SKIP_FILES = new Set(['heartbeat', 'skill', 'claude']);
21
+
22
+ function normalizeTopic(raw) {
23
+ return String(raw || '').trim();
24
+ }
25
+
26
+ function dedupeByTopic(items) {
27
+ const seen = new Set();
28
+ const out = [];
29
+ for (const item of items) {
30
+ const topic = normalizeTopic(item && item.topic);
31
+ if (!topic || seen.has(topic.toLowerCase())) continue;
32
+ seen.add(topic.toLowerCase());
33
+ out.push({
34
+ topic,
35
+ detail: normalizeTopic(item && item.detail)
36
+ });
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function parseTopicLine(rawLine) {
42
+ const line = normalizeTopic(rawLine);
43
+ if (!line) return null;
44
+
45
+ const splitPoint = line.search(/\s+[-–—:]\s+/);
46
+ if (splitPoint > 10) {
47
+ const topic = normalizeTopic(line.slice(0, splitPoint));
48
+ const detail = normalizeTopic(line.slice(splitPoint + 3));
49
+ return { topic, detail };
50
+ }
51
+
52
+ return { topic: line, detail: '' };
53
+ }
54
+
55
+ function isValidTopic(line) {
56
+ if (!line || line.length < 5) return false;
57
+ if (line.includes('`')) return false;
58
+ if (line.includes('http')) return false;
59
+ if (line.includes('**:')) return false;
60
+ if (line.startsWith('//')) return false;
61
+ if (line.includes('()')) return false;
62
+ if (/\d{4}-\d{2}-\d{2}/.test(line)) return false;
63
+ if (line.toLowerCase().includes('todo')) return false;
64
+ if (line.toLowerCase().includes('fixme')) return false;
65
+ return true;
66
+ }
67
+
68
+ function truncateAtWordBoundary(text, max = 60) {
69
+ const normalized = normalizeTopic(text);
70
+ if (normalized.length <= max) return normalized;
71
+
72
+ const truncated = normalized.slice(0, max);
73
+ const lastSpace = truncated.lastIndexOf(' ');
74
+ return (lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated) + '...';
75
+ }
20
76
 
21
77
  /**
22
78
  * Load manifest from disk. Returns {} if not found.
@@ -122,196 +178,278 @@ function formatTopicsForPrompt(tierTopics) {
122
178
  }
123
179
 
124
180
  /**
125
- * Generate a default manifest by reading USER.md, HEARTBEAT.md, SOUL.md
126
- * from the owner's workspace. Falls back to a minimal starter if files don't exist.
181
+ * Generate a minimal starter manifest. This provides safe defaults when
182
+ * no agent-driven extraction has been performed yet.
183
+ *
184
+ * For proper topic extraction, use buildExtractionPrompt() to instruct
185
+ * an agent, then validate the result with validateDisclosureSubmission().
127
186
  */
128
187
  function generateDefaultManifest(contextFiles = {}) {
129
188
  const now = new Date().toISOString();
189
+ const source = {};
190
+ const raw = contextFiles || {};
191
+ Object.keys(raw).forEach((key) => {
192
+ if (!SKIP_FILES.has(key.toLowerCase())) {
193
+ source[key] = raw[key];
194
+ }
195
+ });
196
+
197
+ const userContent = String(source.user || '');
198
+ const soulContent = String(source.soul || '');
199
+ function extractFromSource(content, sectionNames) {
200
+ const sectionPattern = new RegExp(
201
+ `##\\s*(?:${sectionNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[^\\n]*\\n([\\s\\S]*?)(?=\\n##|$)`,
202
+ 'i'
203
+ );
204
+ const match = String(content || '').match(sectionPattern);
205
+ if (!match) {
206
+ return [];
207
+ }
130
208
 
131
- const manifest = {
209
+ return String(match[1] || '')
210
+ .split('\n')
211
+ .map(line => normalizeTopic(line))
212
+ .filter(line => line.startsWith('-') || line.startsWith('*'))
213
+ .map(line => normalizeTopic(line.replace(/^[\s\-\*]+/, '')))
214
+ .map(parseTopicLine)
215
+ .filter(topic => topic && isValidTopic(topic.topic))
216
+ .map(topic => ({
217
+ topic: truncateAtWordBoundary(topic.topic, 60),
218
+ detail: truncateAtWordBoundary(topic.detail || '', 120)
219
+ }));
220
+ }
221
+
222
+ const candidateTopics = dedupeByTopic([
223
+ ...extractFromSource(userContent, ['Goals', 'Interests', 'Projects', 'Current']),
224
+ ...extractFromSource(soulContent, ['Goals', 'Interests', 'Projects', 'Current', 'Values', 'Personal'])
225
+ ]);
226
+
227
+ if (candidateTopics.length === 0) {
228
+ return {
229
+ version: 1,
230
+ generated_at: now,
231
+ updated_at: now,
232
+ topics: {
233
+ 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' }]
237
+ },
238
+ friends: { lead_with: [], discuss_freely: [], deflect: [] },
239
+ family: { lead_with: [], discuss_freely: [], deflect: [] }
240
+ },
241
+ never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
242
+ personality_notes: 'Direct and technical. Prefers depth over breadth.'
243
+ };
244
+ }
245
+
246
+ const publicLead = [];
247
+ const publicDiscuss = [];
248
+ const publicDeflect = [];
249
+ const friendsLead = [];
250
+ const friendsDiscuss = [];
251
+ const familyDiscuss = [];
252
+
253
+ candidateTopics.forEach((entry, index) => {
254
+ const topic = truncateAtWordBoundary(entry.topic || '', 60);
255
+ const detail = truncateAtWordBoundary(entry.detail || 'Open discussion topic.', 120);
256
+ if (!topic) return;
257
+
258
+ const node = { topic, detail };
259
+ if (index < 2) {
260
+ publicLead.push(node);
261
+ return;
262
+ }
263
+ if (index < 6) {
264
+ publicDiscuss.push(node);
265
+ return;
266
+ }
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
+ }
278
+ });
279
+
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.' });
288
+ }
289
+
290
+ return {
132
291
  version: 1,
133
292
  generated_at: now,
134
293
  updated_at: now,
135
294
  topics: {
136
- public: { lead_with: [], discuss_freely: [], deflect: [] },
137
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
138
- family: { lead_with: [], discuss_freely: [], deflect: [] }
295
+ public: {
296
+ lead_with: publicLead,
297
+ discuss_freely: publicDiscuss,
298
+ deflect: publicDeflect
299
+ },
300
+ friends: {
301
+ lead_with: friendsLead,
302
+ discuss_freely: friendsDiscuss,
303
+ deflect: []
304
+ },
305
+ family: {
306
+ lead_with: [],
307
+ discuss_freely: familyDiscuss,
308
+ deflect: []
309
+ }
139
310
  },
140
311
  never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
141
- personality_notes: 'Direct and technical. Prefers depth over breadth.'
312
+ personality_notes: 'Direct and practical. Open to collaboration with clear boundaries.'
142
313
  };
314
+ }
143
315
 
144
- const userContent = contextFiles.user || '';
145
- const heartbeatContent = contextFiles.heartbeat || '';
146
- const soulContent = contextFiles.soul || '';
316
+ /**
317
+ * Check if a string contains technical content that shouldn't appear in
318
+ * disclosure topics (code snippets, URLs, markdown formatting, camelCase identifiers).
319
+ */
320
+ function isTechnicalContent(line) {
321
+ return /`/.test(line) ||
322
+ /https?:\/\//.test(line) ||
323
+ /\*\*:/.test(line) ||
324
+ /:\*\*/.test(line) ||
325
+ /\b[a-z]{3,}[A-Z][a-z]{3,}/.test(line);
326
+ }
147
327
 
148
- const hasContent = userContent || heartbeatContent || soulContent ||
149
- contextFiles.memory || contextFiles.claude;
328
+ /**
329
+ * Validate an agent-submitted disclosure submission against the expected schema.
330
+ * Returns { valid: boolean, manifest: object|null, errors: string[] }.
331
+ */
332
+ function validateDisclosureSubmission(data) {
333
+ const errors = [];
150
334
 
151
- if (!hasContent) {
152
- // Minimal starter manifest
153
- manifest.topics.public.lead_with.push(
154
- { topic: 'What I do', detail: 'Brief professional description' }
155
- );
156
- manifest.topics.public.discuss_freely.push(
157
- { topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }
158
- );
159
- manifest.topics.public.deflect.push(
160
- { topic: 'Personal details', detail: 'Redirect to direct owner contact' }
161
- );
162
- return manifest;
335
+ // Must be a non-null object
336
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
337
+ return { valid: false, manifest: null, errors: ['Submission must be a non-null object'] };
163
338
  }
164
339
 
165
- // Extract from USER.md
166
- if (userContent) {
167
- // Goals/seeking
168
- const goalsMatch = userContent.match(/##\s*(?:Goals|Current|Seeking|Working On)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
169
- if (goalsMatch) {
170
- const goals = goalsMatch[1]
171
- .split('\n')
172
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
173
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
174
- .filter(Boolean);
175
-
176
- goals.forEach((goal, i) => {
177
- if (i < 2) {
178
- manifest.topics.public.lead_with.push({ topic: goal.slice(0, 60), detail: goal });
179
- } else {
180
- manifest.topics.public.discuss_freely.push({ topic: goal.slice(0, 60), detail: goal });
181
- }
182
- });
183
- }
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');
343
+ return { valid: false, manifest: null, errors };
344
+ }
184
345
 
185
- // Interests/projects
186
- const interestsMatch = userContent.match(/##\s*(?:Interests|Projects|Skills)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
187
- if (interestsMatch) {
188
- const interests = interestsMatch[1]
189
- .split('\n')
190
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
191
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
192
- .filter(Boolean);
193
-
194
- interests.forEach(interest => {
195
- manifest.topics.public.discuss_freely.push({ topic: interest.slice(0, 60), detail: interest });
196
- });
346
+ // Require all three tiers
347
+ 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`);
197
350
  }
351
+ }
352
+ if (errors.length > 0) {
353
+ return { valid: false, manifest: null, errors };
354
+ }
198
355
 
199
- // Private/personal sections go to friends/family
200
- const privateMatch = userContent.match(/##\s*(?:Private|Personal|Family)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
201
- if (privateMatch) {
202
- const privateItems = privateMatch[1]
203
- .split('\n')
204
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
205
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
206
- .filter(Boolean);
207
-
208
- privateItems.forEach(item => {
209
- manifest.topics.family.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
210
- });
211
-
212
- // Deflect these for public
213
- manifest.topics.public.deflect.push(
214
- { topic: 'Personal life', detail: 'Redirect — suggest owners connect directly' }
215
- );
216
- }
356
+ // Reject extra tiers beyond the known hierarchy
357
+ const extraTiers = Object.keys(data.topics).filter(t => !TIER_HIERARCHY.includes(t));
358
+ if (extraTiers.length > 0) {
359
+ errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
217
360
  }
218
361
 
219
- // Extract from HEARTBEAT.md (recent activity/status)
220
- if (heartbeatContent) {
221
- const recentLines = heartbeatContent
222
- .split('\n')
223
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
224
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
225
- .filter(Boolean)
226
- .slice(0, 5);
227
-
228
- recentLines.forEach((line, i) => {
229
- if (i < 2) {
230
- manifest.topics.public.lead_with.push({ topic: line.slice(0, 60), detail: line });
231
- } else {
232
- manifest.topics.friends.discuss_freely.push({ topic: line.slice(0, 60), detail: line });
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 };
365
+ 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;
233
371
  }
234
- });
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;
380
+ }
381
+ if (item.topic.trim().length === 0) {
382
+ errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
383
+ continue;
384
+ }
385
+ if (item.topic.length > 160) {
386
+ errors.push(`topics.${tier}.${cat}[${i}]: topic exceeds 160 character limit (got ${item.topic.length})`);
387
+ }
388
+ if (item.detail.length > 500) {
389
+ errors.push(`topics.${tier}.${cat}[${i}]: detail exceeds 500 character limit (got ${item.detail.length})`);
390
+ }
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`);
393
+ }
394
+ }
395
+ }
235
396
  }
236
397
 
237
- // Extract from SOUL.md (personality, values)
238
- if (soulContent) {
239
- // Look for personality cues
240
- const personalityLines = soulContent
241
- .split('\n')
242
- .filter(l => l.trim() && !l.startsWith('#'))
243
- .slice(0, 3)
244
- .join(' ')
245
- .trim();
246
-
247
- if (personalityLines) {
248
- manifest.personality_notes = personalityLines.slice(0, 300);
398
+ // Validate never_disclose (optional, defaults to sensible list)
399
+ if (data.never_disclose !== undefined) {
400
+ if (!Array.isArray(data.never_disclose)) {
401
+ errors.push('"never_disclose" must be an array of strings');
402
+ } else {
403
+ if (data.never_disclose.length > 20) {
404
+ errors.push('never_disclose has too many items — max 20');
405
+ }
406
+ for (let i = 0; i < data.never_disclose.length; i++) {
407
+ if (typeof data.never_disclose[i] !== 'string') {
408
+ errors.push(`never_disclose[${i}] must be a string`);
409
+ } else if (data.never_disclose[i].length > 200) {
410
+ errors.push(`never_disclose[${i}] exceeds 200 chars`);
411
+ }
412
+ }
249
413
  }
414
+ }
250
415
 
251
- // Values become friends-tier topics
252
- const valuesMatch = soulContent.match(/##\s*(?:Values|Beliefs|Principles)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
253
- if (valuesMatch) {
254
- const values = valuesMatch[1]
255
- .split('\n')
256
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
257
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
258
- .filter(Boolean);
259
-
260
- values.forEach(value => {
261
- manifest.topics.friends.discuss_freely.push({ topic: value.slice(0, 60), detail: value });
262
- });
416
+ // Validate personality_notes (optional)
417
+ if (data.personality_notes !== undefined) {
418
+ if (typeof data.personality_notes !== 'string') {
419
+ errors.push('"personality_notes" must be a string');
420
+ } else if (data.personality_notes.length > 500) {
421
+ errors.push('"personality_notes" exceeds 500 chars');
263
422
  }
264
423
  }
265
424
 
266
- // Extract topic keywords from memory files
267
- if (contextFiles.memory) {
268
- const memoryLines = contextFiles.memory
269
- .split('\n')
270
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
271
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
272
- .filter(l => l.length > 5 && l.length < 120)
273
- .slice(0, 4);
274
-
275
- memoryLines.forEach(item => {
276
- manifest.topics.friends.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
277
- });
425
+ if (errors.length > 0) {
426
+ return { valid: false, manifest: null, errors };
278
427
  }
279
428
 
280
- // Extract project context from CLAUDE.md
281
- if (contextFiles.claude) {
282
- const claudeMatch = contextFiles.claude.match(/##\s*(?:What|Quick|About|Context)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
283
- if (claudeMatch) {
284
- const contextLines = claudeMatch[1]
285
- .split('\n')
286
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*') || (l.trim().length > 10 && !l.startsWith('#')))
287
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
288
- .filter(Boolean)
289
- .slice(0, 3);
290
-
291
- contextLines.forEach(item => {
292
- manifest.topics.public.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
293
- });
429
+ // Rebuild topics from only validated keys to prevent extra properties passing through
430
+ const cleanTopics = {};
431
+ 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 => ({
435
+ topic: item.topic,
436
+ detail: item.detail
437
+ }));
294
438
  }
295
439
  }
296
440
 
297
- // Ensure at least something in each public category
298
- if (manifest.topics.public.lead_with.length === 0) {
299
- manifest.topics.public.lead_with.push(
300
- { topic: 'Current focus', detail: 'Primary work and interests' }
301
- );
302
- }
303
- if (manifest.topics.public.discuss_freely.length === 0) {
304
- manifest.topics.public.discuss_freely.push(
305
- { topic: 'General interests', detail: 'Non-sensitive topics' }
306
- );
307
- }
308
- if (manifest.topics.public.deflect.length === 0) {
309
- manifest.topics.public.deflect.push(
310
- { topic: 'Private matters', detail: 'Redirect to direct owner contact' }
311
- );
312
- }
441
+ // Build valid manifest
442
+ const now = new Date().toISOString();
443
+ const manifest = {
444
+ version: 1,
445
+ generated_at: now,
446
+ updated_at: now,
447
+ topics: cleanTopics,
448
+ never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
449
+ personality_notes: data.personality_notes || ''
450
+ };
313
451
 
314
- return manifest;
452
+ return { valid: true, manifest, errors: [] };
315
453
  }
316
454
 
317
455
  /**
@@ -355,6 +493,72 @@ function readContextFiles(workspaceDir) {
355
493
  return result;
356
494
  }
357
495
 
496
+ /**
497
+ * Generate the extraction prompt that instructs an agent on exactly what
498
+ * structured disclosure data to return.
499
+ *
500
+ * @param {Object} [availableFiles] - Map of filename to truthy if present
501
+ * @returns {string} The instruction prompt for the agent
502
+ */
503
+ function buildExtractionPrompt(availableFiles = {}) {
504
+ const fileList = Object.entries(availableFiles)
505
+ .filter(([, present]) => present)
506
+ .map(([name]) => ` - ${name}`)
507
+ .join('\n') || ' (no workspace files detected)';
508
+
509
+ 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```';
510
+
511
+ return `## A2A Disclosure Extraction
512
+
513
+ You are helping the owner set up their A2A disclosure profile — the topics and information their agent is willing to discuss with other agents at different trust levels.
514
+
515
+ ### Available workspace files
516
+ ${fileList}
517
+
518
+ Read the available files above and extract disclosure topics. Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
519
+
520
+ ### What to extract
521
+
522
+ For each trust tier, identify topics the owner would want to discuss:
523
+
524
+ - **public** — safe for anyone: professional role, public interests, general project descriptions
525
+ - **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
526
+ - **family** — inner circle only: personal interests, private projects, sensitive plans
527
+
528
+ For each tier, categorize topics as:
529
+ - **lead_with** — proactively bring up (max 3 per tier)
530
+ - **discuss_freely** — happy to discuss if asked (max 8 per tier)
531
+ - **deflect** — redirect or decline (max 3 per tier)
532
+
533
+ Also identify:
534
+ - **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
535
+ - **personality_notes** — a 1-2 sentence description of the owner's communication style
536
+
537
+ ### What NOT to extract
538
+
539
+ Do NOT include as topics:
540
+ - Code snippets, CLI commands, or technical documentation
541
+ - URLs or file paths
542
+ - Agent instructions or operational tasks (e.g., "post 50 comments/day")
543
+ - Markdown formatting artifacts (bold markers, backticks)
544
+ - Anything from HEARTBEAT.md (these are agent tasks, not disclosure topics)
545
+
546
+ ### Required JSON format
547
+
548
+ Return ONLY valid JSON in this exact structure:
549
+
550
+ ${jsonBlock}
551
+
552
+ ### Rules
553
+
554
+ 1. Each "topic" string must be a short, human-readable label (max 160 chars)
555
+ 2. Each "detail" string explains the topic more fully (max 500 chars)
556
+ 3. Topics should be things a person would discuss, not technical artifacts
557
+ 4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
558
+ 5. Present this to the owner for review before submitting
559
+ 6. The owner may edit, remove, or add topics before final submission`;
560
+ }
561
+
358
562
  module.exports = {
359
563
  loadManifest,
360
564
  saveManifest,
@@ -362,5 +566,8 @@ module.exports = {
362
566
  formatTopicsForPrompt,
363
567
  generateDefaultManifest,
364
568
  readContextFiles,
569
+ validateDisclosureSubmission,
570
+ isTechnicalContent,
571
+ buildExtractionPrompt,
365
572
  MANIFEST_FILE
366
573
  };