a2acalling 0.6.10 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -200,11 +200,14 @@ function parseArgs(argv) {
200
200
  }
201
201
 
202
202
  async function promptYesNo(question) {
203
- const defaultValue = question.includes('[Y/n]') || question.includes('[y/N]') || question.includes('[Y/N]')
204
- ? question.includes('[Y/n]') || question.includes('[y/n]')
203
+ const q = String(question || '');
204
+ // Support both bracket and paren styles: [Y/n], (y/N), etc.
205
+ // Convention: uppercase letter is the default when user presses Enter.
206
+ const defaultValue = q.includes('y/N')
207
+ ? false
208
+ : q.includes('Y/n')
205
209
  ? true
206
- : false
207
- : true;
210
+ : true;
208
211
 
209
212
  if (!isInteractiveShell()) {
210
213
  return defaultValue;
@@ -1368,6 +1371,16 @@ https://github.com/onthegonow/a2a_calling`;
1368
1371
  console.log(' Configuration summary:');
1369
1372
  console.log(` Port: ${serverPort}`);
1370
1373
  console.log(` Public host: ${publicHost}`);
1374
+
1375
+ if (!interactive) {
1376
+ console.log('\n Non-interactive mode detected (no TTY).');
1377
+ console.log(' Not starting the server automatically.\n');
1378
+ console.log(' Next steps:');
1379
+ console.log(' 1. Re-run in a terminal: a2a quickstart');
1380
+ console.log(` 2. Or start manually: a2a server --port ${serverPort}\n`);
1381
+ return;
1382
+ }
1383
+
1371
1384
  const startServer = await promptYesNo('Start the A2A server now? [Y/n] ');
1372
1385
  if (!startServer) {
1373
1386
  console.log('\nServer not started. Run with:\n a2a server --port <port> --hostname <host>\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -5,32 +5,73 @@ if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
5
5
  if (process.env.DOCKER) process.exit(0);
6
6
  if (process.env.npm_config_global !== 'true') process.exit(0);
7
7
 
8
+ const fs = require('fs');
9
+ const path = require('path');
8
10
  const { spawnSync } = require('child_process');
9
- const isInteractive = Boolean(process.stdout && process.stderr && process.stdin &&
10
- process.stdout.isTTY && process.stderr.isTTY && process.stdin.isTTY);
11
11
 
12
- function warnSuppressedOutput() {
13
- if (!isInteractive) {
14
- console.warn('\n⚠️ Output may be suppressed. Run \'a2a quickstart\' manually if you don\'t see prompts.');
12
+ function openDevTty() {
13
+ if (process.env.A2A_POSTINSTALL_DISABLE_TTY === '1') return null;
14
+ if (process.platform === 'win32') return null;
15
+
16
+ try {
17
+ // npm may pipe lifecycle stdio even when the user ran npm in a terminal.
18
+ // /dev/tty lets us talk to the actual interactive terminal when present.
19
+ const fdIn = fs.openSync('/dev/tty', 'r');
20
+ const fdOut = fs.openSync('/dev/tty', 'w');
21
+ return { fdIn, fdOut };
22
+ } catch (err) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ const initCwd = process.env.INIT_CWD || process.env.HOME || process.cwd();
28
+ const tty = openDevTty();
29
+
30
+ if (!tty) {
31
+ // Do NOT auto-start a background server in non-interactive installs.
32
+ console.warn('\n⚠️ A2A Calling installed.');
33
+ console.warn(' Setup requires an interactive terminal.');
34
+ console.warn(' Next: a2a quickstart\n');
35
+ process.exit(0);
36
+ }
37
+
38
+ function writeTty(message) {
39
+ try {
40
+ fs.writeSync(tty.fdOut, String(message));
41
+ } catch (err) {
42
+ // ignore
15
43
  }
16
44
  }
17
45
 
18
- warnSuppressedOutput();
46
+ const cliPath = path.join(__dirname, '..', 'bin', 'cli.js');
19
47
 
20
- // Launch quickstart directly stdio: 'inherit' forces foreground output
21
- // even when npm v10+ suppresses postinstall stdout by default.
22
- const result = spawnSync('a2a', ['quickstart'], {
23
- stdio: 'inherit',
24
- shell: true,
25
- cwd: process.env.HOME || process.cwd()
48
+ // Launch quickstart attached to /dev/tty so prompts and output are visible
49
+ // even when npm suppresses postinstall output.
50
+ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
51
+ stdio: [tty.fdIn, tty.fdOut, tty.fdOut],
52
+ cwd: initCwd,
53
+ env: {
54
+ ...process.env,
55
+ A2A_WORKSPACE: process.env.A2A_WORKSPACE || initCwd
56
+ }
26
57
  });
27
58
 
28
- if (result.error || result.status === 127) {
29
- // spawn error or shell couldn't find the a2a binary
30
- const reason = result.error ? result.error.message : 'a2a not found in PATH';
31
- console.error('Could not auto-launch onboarding:', reason);
32
- console.log('\nRun manually: a2a quickstart');
59
+ if (result.error) {
60
+ writeTty('\nCould not auto-launch onboarding.\n');
61
+ writeTty(`Reason: ${result.error.message}\n`);
62
+ writeTty('\nRun manually: a2a quickstart\n');
63
+ try {
64
+ fs.closeSync(tty.fdIn);
65
+ fs.closeSync(tty.fdOut);
66
+ } catch (err) {}
33
67
  process.exit(0); // don't fail the install
34
68
  }
35
69
 
70
+ try {
71
+ fs.closeSync(tty.fdIn);
72
+ fs.closeSync(tty.fdOut);
73
+ } catch (err) {
74
+ // Best-effort cleanup.
75
+ }
76
+
36
77
  process.exit(result.status || 0);
@@ -1,986 +0,0 @@
1
- # Agent-Driven Disclosure Extraction — Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Replace the naive file-parser in `generateDefaultManifest()` with an agent-driven extraction flow where the onboarder instructs the agent what structured data to return, the agent extracts and presents to the human for confirmation, and the onboarder validates the submission before storing.
6
-
7
- **Architecture:** The onboarding system provides explicit instructions (a prompt) telling the agent the exact JSON schema it needs to return. The agent reads workspace files itself, structures the data, gets human confirmation, and submits. Our code validates the submission against the schema and either stores it or returns errors. `generateDefaultManifest()` is stripped to a minimal fallback (no file parsing). Two new functions are added: `buildExtractionPrompt()` (instructions for the agent) and `validateDisclosureSubmission()` (validates agent output). The CLI `onboard` command gains `--submit` mode.
8
-
9
- **Tech Stack:** Node.js, existing test framework (`test/run.js`), existing config validation (`config.js:validateTierPatch`)
10
-
11
- ---
12
-
13
- ## New flow
14
-
15
- ```
16
- 1. a2a onboard → prints extraction prompt for agent
17
- 2. Agent reads workspace files → extracts topics/goals semantically
18
- 3. Agent presents to human → human confirms or amends
19
- 4. a2a onboard --submit '{...}' → validates structured output
20
- 5. Valid? → save to manifest + config tiers, print success
21
- Invalid → print errors, agent retries
22
- ```
23
-
24
- ## JSON schema the agent must return
25
-
26
- ```json
27
- {
28
- "topics": {
29
- "public": {
30
- "lead_with": [
31
- { "topic": "OpenClaw development", "detail": "Building agent-to-agent communication tools" }
32
- ],
33
- "discuss_freely": [
34
- { "topic": "A2A federation protocol", "detail": "Design and implementation of federation" }
35
- ],
36
- "deflect": [
37
- { "topic": "Personal finances", "detail": "Redirect to direct owner contact" }
38
- ]
39
- },
40
- "friends": {
41
- "lead_with": [],
42
- "discuss_freely": [],
43
- "deflect": []
44
- },
45
- "family": {
46
- "lead_with": [],
47
- "discuss_freely": [],
48
- "deflect": []
49
- }
50
- },
51
- "never_disclose": ["API keys", "Other users' data"],
52
- "personality_notes": "Direct and technical. Prefers depth over breadth."
53
- }
54
- ```
55
-
56
- ---
57
-
58
- ### Task 1: Add `validateDisclosureSubmission()` to disclosure.js
59
-
60
- **Files:**
61
- - Test: `test/unit/disclosure.test.js`
62
- - Modify: `src/lib/disclosure.js`
63
-
64
- This is the validator that checks agent-submitted JSON against the expected schema.
65
-
66
- **Step 1: Write failing tests**
67
-
68
- Add to `test/unit/disclosure.test.js` inside the module.exports function, before the closing `};`:
69
-
70
- ```javascript
71
- // ── Disclosure Submission Validation ──────────────────────────
72
-
73
- test('validateDisclosureSubmission accepts valid submission', () => {
74
- const disc = freshDisclosure();
75
- const result = disc.validateDisclosureSubmission({
76
- topics: {
77
- public: {
78
- lead_with: [{ topic: 'AI development', detail: 'Building AI tools' }],
79
- discuss_freely: [{ topic: 'Open source', detail: 'Contributing to OSS' }],
80
- deflect: [{ topic: 'Personal life', detail: 'Redirect to owner' }]
81
- },
82
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
83
- family: { lead_with: [], discuss_freely: [], deflect: [] }
84
- },
85
- never_disclose: ['API keys'],
86
- personality_notes: 'Friendly and technical'
87
- });
88
- assert.ok(result.valid);
89
- assert.equal(result.errors.length, 0);
90
- assert.ok(result.manifest);
91
- assert.equal(result.manifest.version, 1);
92
- tmp.cleanup();
93
- });
94
-
95
- test('validateDisclosureSubmission rejects non-object input', () => {
96
- const disc = freshDisclosure();
97
- const result = disc.validateDisclosureSubmission('not an object');
98
- assert.equal(result.valid, false);
99
- assert.ok(result.errors.length > 0);
100
- assert.ok(result.errors[0].includes('object'));
101
- tmp.cleanup();
102
- });
103
-
104
- test('validateDisclosureSubmission rejects missing topics', () => {
105
- const disc = freshDisclosure();
106
- const result = disc.validateDisclosureSubmission({
107
- never_disclose: ['secrets'],
108
- personality_notes: 'Nice'
109
- });
110
- assert.equal(result.valid, false);
111
- assert.ok(result.errors.some(e => e.includes('topics')));
112
- tmp.cleanup();
113
- });
114
-
115
- test('validateDisclosureSubmission rejects missing tier', () => {
116
- const disc = freshDisclosure();
117
- const result = disc.validateDisclosureSubmission({
118
- topics: {
119
- public: { lead_with: [], discuss_freely: [], deflect: [] }
120
- // missing friends and family
121
- },
122
- never_disclose: [],
123
- personality_notes: ''
124
- });
125
- assert.equal(result.valid, false);
126
- assert.ok(result.errors.some(e => e.includes('friends')));
127
- tmp.cleanup();
128
- });
129
-
130
- test('validateDisclosureSubmission rejects invalid topic shape', () => {
131
- const disc = freshDisclosure();
132
- const result = disc.validateDisclosureSubmission({
133
- topics: {
134
- public: {
135
- lead_with: ['just a string'], // wrong: should be {topic, detail}
136
- discuss_freely: [],
137
- deflect: []
138
- },
139
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
140
- family: { lead_with: [], discuss_freely: [], deflect: [] }
141
- },
142
- never_disclose: [],
143
- personality_notes: ''
144
- });
145
- assert.equal(result.valid, false);
146
- assert.ok(result.errors.some(e => e.includes('topic')));
147
- tmp.cleanup();
148
- });
149
-
150
- test('validateDisclosureSubmission rejects topics with technical content', () => {
151
- const disc = freshDisclosure();
152
- const result = disc.validateDisclosureSubmission({
153
- topics: {
154
- public: {
155
- lead_with: [
156
- { topic: 'Run `npm test` to verify', detail: 'Code command' },
157
- { topic: 'https://github.com/example', detail: 'Raw URL' }
158
- ],
159
- discuss_freely: [],
160
- deflect: []
161
- },
162
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
163
- family: { lead_with: [], discuss_freely: [], deflect: [] }
164
- },
165
- never_disclose: [],
166
- personality_notes: ''
167
- });
168
- assert.equal(result.valid, false);
169
- assert.ok(result.errors.some(e => e.includes('technical')));
170
- tmp.cleanup();
171
- });
172
-
173
- test('validateDisclosureSubmission enforces max topic length', () => {
174
- const disc = freshDisclosure();
175
- const longTopic = 'A'.repeat(200);
176
- const result = disc.validateDisclosureSubmission({
177
- topics: {
178
- public: {
179
- lead_with: [{ topic: longTopic, detail: 'Too long topic' }],
180
- discuss_freely: [],
181
- deflect: []
182
- },
183
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
184
- family: { lead_with: [], discuss_freely: [], deflect: [] }
185
- },
186
- never_disclose: [],
187
- personality_notes: ''
188
- });
189
- assert.equal(result.valid, false);
190
- assert.ok(result.errors.some(e => e.includes('160')));
191
- tmp.cleanup();
192
- });
193
- ```
194
-
195
- **Step 2: Run tests to verify they fail**
196
-
197
- Run: `npm test 2>&1 | grep -E 'validateDisclosureSubmission|passing|failing'`
198
- Expected: 7 new FAILs — `validateDisclosureSubmission is not a function`
199
-
200
- **Step 3: Implement `validateDisclosureSubmission()` in disclosure.js**
201
-
202
- Add before the `module.exports` block:
203
-
204
- ```javascript
205
- /**
206
- * Validate structured disclosure data submitted by an agent.
207
- * Returns { valid: boolean, manifest: object|null, errors: string[] }.
208
- */
209
- function validateDisclosureSubmission(data) {
210
- const errors = [];
211
-
212
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
213
- return { valid: false, manifest: null, errors: ['Submission must be a JSON object'] };
214
- }
215
-
216
- // Require topics object
217
- if (!data.topics || typeof data.topics !== 'object' || Array.isArray(data.topics)) {
218
- errors.push('Missing or invalid "topics" — must be an object with public, friends, family keys');
219
- return { valid: false, manifest: null, errors };
220
- }
221
-
222
- // Require all three tiers
223
- for (const tier of TIER_HIERARCHY) {
224
- if (!data.topics[tier] || typeof data.topics[tier] !== 'object') {
225
- errors.push(`Missing tier "${tier}" in topics`);
226
- }
227
- }
228
- if (errors.length > 0) return { valid: false, manifest: null, errors };
229
-
230
- // Validate each tier's topic arrays
231
- const TOPIC_CATEGORIES = ['lead_with', 'discuss_freely', 'deflect'];
232
- for (const tier of TIER_HIERARCHY) {
233
- const tierData = data.topics[tier];
234
- for (const cat of TOPIC_CATEGORIES) {
235
- if (!Array.isArray(tierData[cat])) {
236
- errors.push(`topics.${tier}.${cat} must be an array`);
237
- continue;
238
- }
239
- for (let i = 0; i < tierData[cat].length; i++) {
240
- const item = tierData[cat][i];
241
- if (!item || typeof item !== 'object' || typeof item.topic !== 'string' || typeof item.detail !== 'string') {
242
- errors.push(`topics.${tier}.${cat}[${i}] must have "topic" and "detail" strings`);
243
- continue;
244
- }
245
- if (item.topic.length > 160) {
246
- errors.push(`topics.${tier}.${cat}[${i}].topic exceeds 160 chars`);
247
- }
248
- if (item.detail.length > 500) {
249
- errors.push(`topics.${tier}.${cat}[${i}].detail exceeds 500 chars`);
250
- }
251
- if (isTechnicalContent(item.topic)) {
252
- errors.push(`topics.${tier}.${cat}[${i}].topic contains technical content (code, URLs, or formatting) — topics should be human-readable`);
253
- }
254
- }
255
- }
256
- }
257
-
258
- // Validate never_disclose (optional, defaults to [])
259
- if (data.never_disclose !== undefined) {
260
- if (!Array.isArray(data.never_disclose)) {
261
- errors.push('"never_disclose" must be an array of strings');
262
- } else {
263
- for (let i = 0; i < data.never_disclose.length; i++) {
264
- if (typeof data.never_disclose[i] !== 'string') {
265
- errors.push(`never_disclose[${i}] must be a string`);
266
- }
267
- }
268
- }
269
- }
270
-
271
- // Validate personality_notes (optional, defaults to '')
272
- if (data.personality_notes !== undefined && typeof data.personality_notes !== 'string') {
273
- errors.push('"personality_notes" must be a string');
274
- }
275
-
276
- if (errors.length > 0) return { valid: false, manifest: null, errors };
277
-
278
- // Build valid manifest
279
- const now = new Date().toISOString();
280
- const manifest = {
281
- version: 1,
282
- generated_at: now,
283
- updated_at: now,
284
- topics: data.topics,
285
- never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
286
- personality_notes: data.personality_notes || 'Direct and technical. Prefers depth over breadth.'
287
- };
288
-
289
- return { valid: true, manifest, errors: [] };
290
- }
291
- ```
292
-
293
- **Step 4: Export it**
294
-
295
- In `module.exports`, add `validateDisclosureSubmission`.
296
-
297
- **Step 5: Run tests to verify they pass**
298
-
299
- Run: `npm test`
300
- Expected: All 7 new tests PASS, all existing tests still pass.
301
-
302
- **Step 6: Commit**
303
-
304
- ```bash
305
- git add src/lib/disclosure.js test/unit/disclosure.test.js
306
- git commit -m "feat(disclosure): add validateDisclosureSubmission for agent-driven extraction"
307
- ```
308
-
309
- ---
310
-
311
- ### Task 2: Add `buildExtractionPrompt()` to disclosure.js
312
-
313
- **Files:**
314
- - Test: `test/unit/disclosure.test.js`
315
- - Modify: `src/lib/disclosure.js`
316
-
317
- This function generates the explicit instructions the agent receives.
318
-
319
- **Step 1: Write failing tests**
320
-
321
- ```javascript
322
- // ── Extraction Prompt Generation ──────────────────────────────
323
-
324
- test('buildExtractionPrompt returns string with JSON schema', () => {
325
- const disc = freshDisclosure();
326
- const prompt = disc.buildExtractionPrompt();
327
- assert.equal(typeof prompt, 'string');
328
- assert.includes(prompt, 'lead_with');
329
- assert.includes(prompt, 'discuss_freely');
330
- assert.includes(prompt, 'deflect');
331
- assert.includes(prompt, 'never_disclose');
332
- assert.includes(prompt, 'personality_notes');
333
- assert.includes(prompt, 'public');
334
- assert.includes(prompt, 'friends');
335
- assert.includes(prompt, 'family');
336
- tmp.cleanup();
337
- });
338
-
339
- test('buildExtractionPrompt lists available context files', () => {
340
- const disc = freshDisclosure();
341
- const prompt = disc.buildExtractionPrompt({ user: true, soul: true, heartbeat: false });
342
- assert.includes(prompt, 'USER.md');
343
- assert.includes(prompt, 'SOUL.md');
344
- tmp.cleanup();
345
- });
346
-
347
- test('buildExtractionPrompt includes guidance on what NOT to extract', () => {
348
- const disc = freshDisclosure();
349
- const prompt = disc.buildExtractionPrompt();
350
- // Should warn against extracting code, URLs, instructions
351
- assert.includes(prompt, 'URL');
352
- assert.includes(prompt, 'code');
353
- tmp.cleanup();
354
- });
355
- ```
356
-
357
- **Step 2: Run tests to verify they fail**
358
-
359
- Run: `npm test`
360
- Expected: 3 new FAILs
361
-
362
- **Step 3: Implement `buildExtractionPrompt()` in disclosure.js**
363
-
364
- ```javascript
365
- /**
366
- * Generate the extraction prompt that instructs an agent on exactly what
367
- * structured disclosure data to return. The agent reads workspace files,
368
- * determines topics semantically, and returns the JSON schema below.
369
- *
370
- * @param {Object} [availableFiles] - Map of filename → truthy if present
371
- * @returns {string} The instruction prompt for the agent
372
- */
373
- function buildExtractionPrompt(availableFiles = {}) {
374
- const fileList = Object.entries(availableFiles)
375
- .filter(([, present]) => present)
376
- .map(([name]) => ` - ${name}`)
377
- .join('\n') || ' (no workspace files detected)';
378
-
379
- return `## A2A Disclosure Extraction
380
-
381
- 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.
382
-
383
- ### Available workspace files
384
- ${fileList}
385
-
386
- 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.
387
-
388
- ### What to extract
389
-
390
- For each trust tier, identify topics the owner would want to discuss:
391
-
392
- - **public** — safe for anyone: professional role, public interests, general project descriptions
393
- - **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
394
- - **family** — inner circle only: personal interests, private projects, sensitive plans
395
-
396
- For each tier, categorize topics as:
397
- - **lead_with** — proactively bring up (max 3 per tier)
398
- - **discuss_freely** — happy to discuss if asked (max 8 per tier)
399
- - **deflect** — redirect or decline (max 3 per tier)
400
-
401
- Also identify:
402
- - **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
403
- - **personality_notes** — a 1-2 sentence description of the owner's communication style
404
-
405
- ### What NOT to extract
406
-
407
- Do NOT include as topics:
408
- - Code snippets, CLI commands, or technical documentation
409
- - URLs or file paths
410
- - Agent instructions or operational tasks (e.g., "post 50 comments/day")
411
- - Markdown formatting artifacts (bold markers, backticks)
412
- - Anything from HEARTBEAT.md (these are agent tasks, not disclosure topics)
413
-
414
- ### Required JSON format
415
-
416
- Return ONLY valid JSON in this exact structure:
417
-
418
- \`\`\`json
419
- {
420
- "topics": {
421
- "public": {
422
- "lead_with": [
423
- { "topic": "Short label (max 60 chars)", "detail": "Longer description of the topic" }
424
- ],
425
- "discuss_freely": [],
426
- "deflect": []
427
- },
428
- "friends": {
429
- "lead_with": [],
430
- "discuss_freely": [],
431
- "deflect": []
432
- },
433
- "family": {
434
- "lead_with": [],
435
- "discuss_freely": [],
436
- "deflect": []
437
- }
438
- },
439
- "never_disclose": ["API keys", "Credentials", "Financial figures"],
440
- "personality_notes": "Brief description of communication style"
441
- }
442
- \`\`\`
443
-
444
- ### Rules
445
-
446
- 1. Each "topic" string must be a short, human-readable label (max 160 chars)
447
- 2. Each "detail" string explains the topic more fully (max 500 chars)
448
- 3. Topics should be things a person would discuss, not technical artifacts
449
- 4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
450
- 5. Present this to the owner for review before submitting
451
- 6. The owner may edit, remove, or add topics before final submission`;
452
- }
453
- ```
454
-
455
- **Step 4: Export it**
456
-
457
- Add `buildExtractionPrompt` to module.exports.
458
-
459
- **Step 5: Run tests**
460
-
461
- Run: `npm test`
462
- Expected: All 3 new tests PASS.
463
-
464
- **Step 6: Commit**
465
-
466
- ```bash
467
- git add src/lib/disclosure.js test/unit/disclosure.test.js
468
- git commit -m "feat(disclosure): add buildExtractionPrompt for agent-driven extraction"
469
- ```
470
-
471
- ---
472
-
473
- ### Task 3: Strip file parsing from `generateDefaultManifest()`
474
-
475
- **Files:**
476
- - Test: `test/unit/disclosure.test.js`
477
- - Modify: `src/lib/disclosure.js`
478
-
479
- Remove all the context-file parsing from `generateDefaultManifest()`. It becomes a minimal-starter-only function. The agent-driven flow replaces file parsing.
480
-
481
- **Step 1: Update existing tests**
482
-
483
- Several tests call `generateDefaultManifest(contextFiles)` and expect parsed topics. These need to change:
484
-
485
- - `generateDefaultManifest extracts goals from USER.md content` → remove (agent does this now)
486
- - `generateDefaultManifest extracts personality from SOUL.md` → remove (agent does this now)
487
- - `generateDefaultManifest uses memory content to add topics` → remove
488
- - `generateDefaultManifest uses CLAUDE.md context` → remove
489
- - The 3 new bug-fix tests (HEARTBEAT, code filtering, truncation) → remove (no longer relevant)
490
- - `generateDefaultManifest ignores HEARTBEAT.md entirely` → remove
491
- - `generateDefaultManifest ignores SKILL.md...` → remove
492
-
493
- Keep:
494
- - `generateDefaultManifest with no context returns starter` → update to verify it returns same starter even WITH context
495
-
496
- Add new test:
497
-
498
- ```javascript
499
- test('generateDefaultManifest returns minimal starter regardless of context', () => {
500
- const disc = freshDisclosure();
501
- const manifest = disc.generateDefaultManifest({
502
- user: '## Goals\n- Build AI tools\n',
503
- soul: 'Refined and precise.\n## Values\n- Craftsmanship\n',
504
- memory: '- Working on distributed systems\n',
505
- claude: '## Quick Context\n- A2A enables agent-to-agent communication\n'
506
- });
507
-
508
- // Should return only the minimal starter, no parsed content
509
- assert.equal(manifest.topics.public.lead_with.length, 1);
510
- assert.equal(manifest.topics.public.lead_with[0].topic, 'What I do');
511
- assert.equal(manifest.topics.friends.lead_with.length, 0);
512
- assert.equal(manifest.topics.friends.discuss_freely.length, 0);
513
- tmp.cleanup();
514
- });
515
- ```
516
-
517
- **Step 2: Run tests to verify the new test fails (old behavior parses files)**
518
-
519
- Run: `npm test`
520
- Expected: New test FAILS because `generateDefaultManifest` still parses files.
521
-
522
- **Step 3: Strip file parsing from `generateDefaultManifest()`**
523
-
524
- Replace the entire function body with:
525
-
526
- ```javascript
527
- function generateDefaultManifest() {
528
- const now = new Date().toISOString();
529
-
530
- return {
531
- version: 1,
532
- generated_at: now,
533
- updated_at: now,
534
- topics: {
535
- public: {
536
- lead_with: [{ topic: 'What I do', detail: 'Brief professional description' }],
537
- discuss_freely: [{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }],
538
- deflect: [{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }]
539
- },
540
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
541
- family: { lead_with: [], discuss_freely: [], deflect: [] }
542
- },
543
- never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
544
- personality_notes: 'Direct and technical. Prefers depth over breadth.'
545
- };
546
- }
547
- ```
548
-
549
- Remove `isTechnicalContent()` and `truncateAtWord()` — they are no longer needed in the manifest generator. **Keep `isTechnicalContent()`** — it's still used by `validateDisclosureSubmission()`.
550
-
551
- Remove `truncateAtWord()` — no longer used.
552
-
553
- **Step 4: Remove tests that tested the old parsing behavior**
554
-
555
- Delete these tests:
556
- - `generateDefaultManifest extracts goals from USER.md content`
557
- - `generateDefaultManifest extracts personality from SOUL.md`
558
- - `generateDefaultManifest uses memory content to add topics`
559
- - `generateDefaultManifest uses CLAUDE.md context`
560
- - `generateDefaultManifest ignores SKILL.md bullet lists for disclosure topics`
561
- - `generateDefaultManifest ignores HEARTBEAT.md entirely`
562
- - `generateDefaultManifest filters out code/technical content from topics`
563
- - `generateDefaultManifest truncates at word boundaries`
564
-
565
- **Step 5: Run tests**
566
-
567
- Run: `npm test`
568
- Expected: All tests PASS. Fewer total tests (removed old parsing tests).
569
-
570
- **Step 6: Commit**
571
-
572
- ```bash
573
- git add src/lib/disclosure.js test/unit/disclosure.test.js
574
- git commit -m "refactor(disclosure): strip file parsing from generateDefaultManifest
575
-
576
- generateDefaultManifest now returns only the minimal starter manifest.
577
- Topic extraction is handled by agent-driven flow via
578
- buildExtractionPrompt + validateDisclosureSubmission."
579
- ```
580
-
581
- ---
582
-
583
- ### Task 4: Update CLI `onboard` command for agent-driven flow
584
-
585
- **Files:**
586
- - Modify: `bin/cli.js` (the `onboard` command, ~lines 1499-1538)
587
- - Test: `test/integration/onboarding.test.js`
588
-
589
- **Step 1: Write failing test**
590
-
591
- Add to `test/integration/onboarding.test.js`:
592
-
593
- ```javascript
594
- test('onboard --submit validates and saves agent disclosure submission', () => {
595
- tmp = helpers.tmpConfigDir('onboard-submit');
596
- const fs = require('fs');
597
- const path = require('path');
598
- const { execFileSync } = require('child_process');
599
-
600
- const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
601
- const env = { ...process.env, A2A_CONFIG_DIR: tmp.dir };
602
-
603
- const submission = JSON.stringify({
604
- topics: {
605
- public: {
606
- lead_with: [{ topic: 'AI development', detail: 'Building AI-powered tools' }],
607
- discuss_freely: [{ topic: 'Open source', detail: 'Contributing to OSS projects' }],
608
- deflect: [{ topic: 'Personal finances', detail: 'Redirect to owner' }]
609
- },
610
- friends: {
611
- lead_with: [{ topic: 'Current projects', detail: 'Deep work on A2A protocol' }],
612
- discuss_freely: [],
613
- deflect: []
614
- },
615
- family: { lead_with: [], discuss_freely: [], deflect: [] }
616
- },
617
- never_disclose: ['API keys', 'Passwords'],
618
- personality_notes: 'Technical and direct'
619
- });
620
-
621
- const result = execFileSync(process.execPath, [cliPath, 'onboard', '--submit', submission], {
622
- env,
623
- encoding: 'utf8'
624
- });
625
-
626
- assert.includes(result, 'Disclosure manifest saved');
627
-
628
- // Verify manifest was saved correctly
629
- delete require.cache[require.resolve('../../src/lib/disclosure')];
630
- const disc = require('../../src/lib/disclosure');
631
- const manifest = disc.loadManifest();
632
- assert.equal(manifest.version, 1);
633
- assert.equal(manifest.topics.public.lead_with[0].topic, 'AI development');
634
- assert.equal(manifest.topics.friends.lead_with[0].topic, 'Current projects');
635
-
636
- tmp.cleanup();
637
- });
638
-
639
- test('onboard --submit rejects invalid submission with errors', () => {
640
- tmp = helpers.tmpConfigDir('onboard-submit-fail');
641
- const { execFileSync } = require('child_process');
642
- const path = require('path');
643
-
644
- const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
645
- const env = { ...process.env, A2A_CONFIG_DIR: tmp.dir };
646
-
647
- const badSubmission = JSON.stringify({ not: 'valid' });
648
-
649
- let threw = false;
650
- try {
651
- execFileSync(process.execPath, [cliPath, 'onboard', '--submit', badSubmission], {
652
- env,
653
- encoding: 'utf8',
654
- stdio: ['pipe', 'pipe', 'pipe']
655
- });
656
- } catch (err) {
657
- threw = true;
658
- const stderr = err.stderr || '';
659
- const stdout = err.stdout || '';
660
- const output = stderr + stdout;
661
- assert.ok(output.includes('topics') || output.includes('Validation'), 'Should mention validation error');
662
- }
663
- assert.ok(threw, 'Should exit with non-zero code on invalid submission');
664
-
665
- tmp.cleanup();
666
- });
667
-
668
- test('onboard without --submit prints extraction prompt', () => {
669
- tmp = helpers.tmpConfigDir('onboard-prompt');
670
- const fs = require('fs');
671
- const path = require('path');
672
- const { execFileSync } = require('child_process');
673
-
674
- const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
675
- const workspaceDir = path.join(tmp.dir, 'ws');
676
- fs.mkdirSync(workspaceDir, { recursive: true });
677
- fs.writeFileSync(path.join(workspaceDir, 'USER.md'), '## Goals\n- Build cool tools\n');
678
-
679
- const env = { ...process.env, A2A_CONFIG_DIR: tmp.dir, A2A_WORKSPACE: workspaceDir };
680
-
681
- const result = execFileSync(process.execPath, [cliPath, 'onboard'], {
682
- env,
683
- encoding: 'utf8'
684
- });
685
-
686
- assert.includes(result, 'lead_with');
687
- assert.includes(result, 'discuss_freely');
688
- assert.includes(result, 'USER.md');
689
-
690
- tmp.cleanup();
691
- });
692
- ```
693
-
694
- **Step 2: Run tests to verify they fail**
695
-
696
- Run: `npm test`
697
- Expected: 3 new FAILs
698
-
699
- **Step 3: Rewrite the `onboard` CLI command**
700
-
701
- Replace the `onboard` handler in `bin/cli.js` (~lines 1499-1538):
702
-
703
- ```javascript
704
- onboard: (args) => {
705
- const { A2AConfig } = require('../src/lib/config');
706
- const {
707
- readContextFiles,
708
- buildExtractionPrompt,
709
- validateDisclosureSubmission,
710
- saveManifest,
711
- MANIFEST_FILE
712
- } = require('../src/lib/disclosure');
713
- const config = new A2AConfig();
714
-
715
- // ── Submit mode: agent sends structured JSON ──────────────
716
- const submitRaw = args.flags.submit;
717
- if (submitRaw) {
718
- let parsed;
719
- try {
720
- parsed = JSON.parse(String(submitRaw));
721
- } catch (e) {
722
- console.error('\n\u274c Invalid JSON in --submit flag.');
723
- console.error(` Parse error: ${e.message}\n`);
724
- process.exit(1);
725
- }
726
-
727
- const result = validateDisclosureSubmission(parsed);
728
- if (!result.valid) {
729
- console.error('\n\u274c Disclosure submission validation failed:\n');
730
- result.errors.forEach(err => console.error(` \u2022 ${err}`));
731
- console.error(`\nFix the errors above and resubmit with: a2a onboard --submit '<json>'\n`);
732
- process.exit(1);
733
- }
734
-
735
- saveManifest(result.manifest);
736
-
737
- const agentName = args.flags.name || config.getAgent().name || process.env.A2A_AGENT_NAME || '';
738
- const hostname = args.flags.hostname || config.getAgent().hostname || process.env.A2A_HOSTNAME || '';
739
- if (agentName) config.setAgent({ name: agentName });
740
- if (hostname) config.setAgent({ hostname });
741
-
742
- console.log('\n\u2705 Disclosure manifest saved.');
743
- console.log(` Manifest: ${MANIFEST_FILE}`);
744
- console.log(' Next: a2a quickstart\n');
745
- return;
746
- }
747
-
748
- // ── Prompt mode: print extraction instructions for agent ──
749
- if (config.isOnboarded() && !args.flags.force) {
750
- console.log('\u2705 Onboarding already complete. Use --force to regenerate.');
751
- return;
752
- }
753
-
754
- const workspaceDir = process.env.A2A_WORKSPACE || process.cwd();
755
- const contextFiles = readContextFiles(workspaceDir);
756
-
757
- const availableFiles = {
758
- 'USER.md': Boolean(contextFiles.user),
759
- 'SOUL.md': Boolean(contextFiles.soul),
760
- 'HEARTBEAT.md': Boolean(contextFiles.heartbeat),
761
- 'SKILL.md': Boolean(contextFiles.skill),
762
- 'CLAUDE.md': Boolean(contextFiles.claude),
763
- 'memory/*.md': Boolean(contextFiles.memory)
764
- };
765
-
766
- console.log(buildExtractionPrompt(availableFiles));
767
- console.log('\n---');
768
- console.log('After the owner confirms, submit with:');
769
- console.log(" a2a onboard --submit '<json>'\n");
770
- },
771
- ```
772
-
773
- **Step 4: Run tests**
774
-
775
- Run: `npm test`
776
- Expected: All 3 new tests PASS, all existing tests still pass.
777
-
778
- **Step 5: Commit**
779
-
780
- ```bash
781
- git add bin/cli.js test/integration/onboarding.test.js
782
- git commit -m "feat(cli): rewrite onboard command for agent-driven disclosure extraction
783
-
784
- onboard now has two modes:
785
- - Default: prints extraction prompt with JSON schema for agent
786
- - --submit: validates agent's structured JSON and saves manifest"
787
- ```
788
-
789
- ---
790
-
791
- ### Task 5: Update CLI `quickstart` Step 1 to stop auto-parsing
792
-
793
- **Files:**
794
- - Modify: `bin/cli.js` (~lines 1105-1127, quickstart Step 1)
795
- - Test: `test/integration/onboarding.test.js`
796
-
797
- **Step 1: Write failing test**
798
-
799
- ```javascript
800
- test('quickstart uses existing manifest without regenerating from files', () => {
801
- tmp = helpers.tmpConfigDir('quickstart-no-regen');
802
- const fs = require('fs');
803
- const path = require('path');
804
- const http = require('http');
805
- const { execFileSync } = require('child_process');
806
-
807
- // Pre-save a manifest via --submit
808
- const cliPath = path.join(__dirname, '..', '..', 'bin', 'cli.js');
809
- const env = { ...process.env, A2A_CONFIG_DIR: tmp.dir };
810
-
811
- const submission = JSON.stringify({
812
- topics: {
813
- public: {
814
- lead_with: [{ topic: 'Agent-submitted topic', detail: 'This was submitted by agent' }],
815
- discuss_freely: [],
816
- deflect: []
817
- },
818
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
819
- family: { lead_with: [], discuss_freely: [], deflect: [] }
820
- },
821
- never_disclose: [],
822
- personality_notes: 'Agent-set personality'
823
- });
824
-
825
- execFileSync(process.execPath, [cliPath, 'onboard', '--submit', submission], { env, encoding: 'utf8' });
826
-
827
- // Now write workspace files that would produce different topics if parsed
828
- const workspaceDir = path.join(tmp.dir, 'ws');
829
- fs.mkdirSync(workspaceDir, { recursive: true });
830
- fs.writeFileSync(path.join(workspaceDir, 'USER.md'), '## Goals\n- Completely different goal\n');
831
- env.A2A_WORKSPACE = workspaceDir;
832
-
833
- // Start a minimal server for quickstart ping
834
- const server = http.createServer((req, res) => {
835
- if (req.method === 'GET' && req.url === '/api/a2a/ping') {
836
- res.writeHead(200, { 'Content-Type': 'application/json' });
837
- res.end(JSON.stringify({ pong: true }));
838
- return;
839
- }
840
- res.statusCode = 404;
841
- res.end();
842
- });
843
-
844
- const done = new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
845
- return done.then(() => {
846
- const backendPort = String(server.address().port);
847
- try {
848
- execFileSync(process.execPath, [cliPath, 'quickstart', '--port', backendPort], {
849
- env,
850
- stdio: 'ignore'
851
- });
852
-
853
- // Verify the agent-submitted topic is preserved (not overwritten)
854
- delete require.cache[require.resolve('../../src/lib/disclosure')];
855
- const disc = require('../../src/lib/disclosure');
856
- const manifest = disc.loadManifest();
857
- assert.equal(manifest.topics.public.lead_with[0].topic, 'Agent-submitted topic');
858
- } finally {
859
- server.close();
860
- tmp.cleanup();
861
- }
862
- });
863
- });
864
- ```
865
-
866
- **Step 2: Run tests to verify it fails**
867
-
868
- Expected: FAIL because quickstart Step 1 still calls `generateDefaultManifest(contextFiles)` which overwrites.
869
-
870
- Wait — actually quickstart only generates if manifest is empty: `if (!manifest || Object.keys(manifest).length === 0)`. Since we pre-saved via `--submit`, the manifest exists and quickstart should preserve it. Let me check if the `--force` flag path is the issue...
871
-
872
- Actually this test might already pass since quickstart only regenerates on `--force` or empty manifest. Let me adjust — the key change needed is to make `--force` / `--regen-manifest` use the minimal starter instead of file parsing.
873
-
874
- **Step 3: Update quickstart Step 1**
875
-
876
- Replace lines ~1105-1127 in the quickstart handler:
877
-
878
- ```javascript
879
- // ── Step 1: Background bootstrap (config + manifest) ─────────
880
- let contextFiles = {};
881
- let manifest = {};
882
- try {
883
- contextFiles = disc.readContextFiles(workspaceDir);
884
- const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
885
- if (forceManifest) {
886
- // Force-regen uses minimal starter; agent-driven extraction is the
887
- // proper way to populate topics (via `a2a onboard --submit`).
888
- const generated = disc.generateDefaultManifest();
889
- disc.saveManifest(generated);
890
- manifest = generated;
891
- } else {
892
- manifest = disc.loadManifest();
893
- if (!manifest || Object.keys(manifest).length === 0) {
894
- const generated = disc.generateDefaultManifest();
895
- disc.saveManifest(generated);
896
- manifest = generated;
897
- }
898
- }
899
- } catch (err) {
900
- // Non-fatal: onboarding can proceed even if manifest fails.
901
- contextFiles = {};
902
- manifest = {};
903
- }
904
- ```
905
-
906
- The change is small: `disc.generateDefaultManifest(contextFiles)` → `disc.generateDefaultManifest()` (no context files). Since the function now ignores them anyway, this is just for clarity.
907
-
908
- **Step 4: Run tests**
909
-
910
- Run: `npm test`
911
- Expected: All tests PASS.
912
-
913
- **Step 5: Commit**
914
-
915
- ```bash
916
- git add bin/cli.js test/integration/onboarding.test.js
917
- git commit -m "fix(quickstart): stop auto-parsing workspace files into manifest
918
-
919
- quickstart Step 1 now uses minimal starter on force-regen instead of
920
- parsing workspace files. Proper topic extraction happens via
921
- agent-driven 'a2a onboard --submit'."
922
- ```
923
-
924
- ---
925
-
926
- ### Task 6: Update integration tests that call `generateDefaultManifest`
927
-
928
- **Files:**
929
- - Modify: `test/integration/bramble-calls-bappybot.test.js`
930
- - Modify: `test/integration/golda-calls-bappybot.test.js`
931
- - Modify: `test/integration/nyx-calls-bappybot.test.js`
932
- - Modify: `scripts/install-openclaw.js`
933
-
934
- These all call `generateDefaultManifest()` with no args as a fallback. Since the function signature hasn't changed (it just ignores args now), these should already work. Verify and clean up any references to context file args.
935
-
936
- **Step 1: Verify integration tests pass without changes**
937
-
938
- Run: `npm test`
939
- Expected: All pass. The integration tests call `disc.generateDefaultManifest()` with no args, which still returns the minimal starter.
940
-
941
- **Step 2: Clean up `scripts/install-openclaw.js` if it passes context files**
942
-
943
- Check if it passes `contextFiles` — if so, remove the arg since it's now ignored.
944
-
945
- **Step 3: Commit (if any changes needed)**
946
-
947
- ```bash
948
- git add scripts/install-openclaw.js
949
- git commit -m "chore: remove unused context file args from generateDefaultManifest calls"
950
- ```
951
-
952
- ---
953
-
954
- ### Task 7: Final verification and cleanup
955
-
956
- **Step 1: Run full test suite**
957
-
958
- Run: `npm test`
959
- Expected: All tests pass, no regressions.
960
-
961
- **Step 2: Verify the new flow end-to-end manually**
962
-
963
- ```bash
964
- # Generate extraction prompt
965
- A2A_WORKSPACE=. node bin/cli.js onboard
966
-
967
- # Submit valid disclosure
968
- node bin/cli.js onboard --submit '{"topics":{"public":{"lead_with":[{"topic":"A2A federation","detail":"Agent-to-agent communication protocol"}],"discuss_freely":[],"deflect":[]},"friends":{"lead_with":[],"discuss_freely":[],"deflect":[]},"family":{"lead_with":[],"discuss_freely":[],"deflect":[]}},"never_disclose":["API keys"],"personality_notes":"Technical"}'
969
-
970
- # Verify it rejects bad input
971
- node bin/cli.js onboard --submit '{"bad":"data"}'
972
- ```
973
-
974
- **Step 3: Commit any final fixes**
975
-
976
- ---
977
-
978
- ## Summary of changes
979
-
980
- | File | Change |
981
- |------|--------|
982
- | `src/lib/disclosure.js` | Add `buildExtractionPrompt()`, `validateDisclosureSubmission()`. Strip file parsing from `generateDefaultManifest()`. Keep `isTechnicalContent()` for validation. Remove `truncateAtWord()`. |
983
- | `bin/cli.js` | Rewrite `onboard` command (prompt mode + `--submit` mode). Update quickstart Step 1 to not pass context files. |
984
- | `test/unit/disclosure.test.js` | Remove old parsing tests. Add validation + prompt generation tests. |
985
- | `test/integration/onboarding.test.js` | Add `--submit` and prompt mode tests. |
986
- | `scripts/install-openclaw.js` | Remove unused context file arg (if present). |