a2acalling 0.6.10 → 0.6.12

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;
@@ -296,17 +299,22 @@ function readWorkspaceContext(baseDir = process.cwd()) {
296
299
  }
297
300
 
298
301
  function printWorkspaceScan(context) {
299
- console.log('Scanning workspace for context...');
300
- console.log(` ${context.found.USER ? '✅' : '⚠️ '} ${context.found.USER ? 'Found USER.md' : 'No USER.md'}${context.found.USER ? ' — identity hints found' : ''}`);
301
- console.log(` ${context.found.SOUL ? '✅' : '⚠️ '} ${context.found.SOUL ? 'Found SOUL.md' : 'No SOUL.md'}${context.found.SOUL ? ' — personality notes available' : ''}`);
302
- console.log(` ${context.found.HEARTBEAT ? '✅' : '⚠️ '} ${context.found.HEARTBEAT ? 'Found HEARTBEAT.md' : 'No HEARTBEAT.md (skipped)'}`);
303
- console.log(` ${context.found.SKILL ? '✅' : '⚠️ '} ${context.found.SKILL ? 'Found SKILL.md' : 'No SKILL.md (skipped)'}`
304
- );
305
- console.log(` ${context.found.CLAUDE ? '✅' : '⚠️ '} ${context.found.CLAUDE ? 'Found CLAUDE.md' : 'No CLAUDE.md (skipped)'}`);
306
- if (context.memoryCount > 0) {
307
- console.log(` ✅ Found ${context.memoryCount} memory file(s)`);
302
+ const fileLabels = {
303
+ USER: 'USER.md — identity hints',
304
+ SOUL: 'SOUL.md — personality notes',
305
+ HEARTBEAT: 'HEARTBEAT.md scheduled tasks',
306
+ SKILL: 'SKILL.md capabilities',
307
+ CLAUDE: 'CLAUDE.md — agent instructions'
308
+ };
309
+ const found = Object.entries(fileLabels)
310
+ .filter(([key]) => context.found[key])
311
+ .map(([, label]) => label);
312
+ if (context.memoryCount > 0) found.push(`${context.memoryCount} memory file(s)`);
313
+
314
+ if (found.length) {
315
+ console.log(`Workspace context: ${found.join(', ')}`);
308
316
  } else {
309
- console.log(' ⚠️ No memory/*.md files');
317
+ console.log('Workspace context: no context files found (disclosure topics will be minimal)');
310
318
  }
311
319
  }
312
320
 
@@ -1205,6 +1213,33 @@ https://github.com/onthegonow/a2a_calling`;
1205
1213
  require('../src/server.js');
1206
1214
  },
1207
1215
 
1216
+ // ==========================================================================
1217
+ // quickstart — full onboarding flow
1218
+ //
1219
+ // OBJECTIVE: Walk an installing agent (or human) through every step needed
1220
+ // to get A2A Calling running, in a single pass. The postinstall hook calls
1221
+ // this automatically, so `npm install -g a2acalling` completes the entire
1222
+ // setup without any follow-up commands.
1223
+ //
1224
+ // Every prompt auto-accepts its default when there is no TTY, so the full
1225
+ // flow completes unattended in non-interactive environments (agents).
1226
+ //
1227
+ // STEPS:
1228
+ // Step 1 — Port selection: scan for an available port and bind it.
1229
+ // Step 2 — Hostname detection: look up the external IP so remote agents
1230
+ // know where to reach this instance.
1231
+ // Step 3 — Server start: launch the A2A server as a detached process,
1232
+ // confirm it's listening, save config, and print a verify hint.
1233
+ // Step 4 — Disclosure prompt: output a full agent-readable prompt that
1234
+ // instructs the agent to scan its own workspace files, extract
1235
+ // tiered disclosure topics, and submit them back via
1236
+ // `a2a quickstart --submit '<json>'`.
1237
+ //
1238
+ // The disclosure prompt does NOT pre-scan files itself — it tells the agent
1239
+ // which files to look for (USER.md, SOUL.md, etc.) and lets the agent read
1240
+ // them with its own tools. This is intentional: the installer runs in a
1241
+ // subprocess where it has no access to the agent's file-reading capabilities.
1242
+ // ==========================================================================
1208
1243
  quickstart: async (args) => {
1209
1244
  const { A2AConfig } = require('../src/lib/config');
1210
1245
  const { isPortListening } = require('../src/lib/port-scanner');
@@ -1214,6 +1249,8 @@ https://github.com/onthegonow/a2a_calling`;
1214
1249
  const config = new A2AConfig();
1215
1250
  const interactive = isInteractiveShell();
1216
1251
 
1252
+ // Handle `quickstart --submit '<json>'` — this is the agent calling back
1253
+ // after it has scanned its workspace and built the disclosure JSON.
1217
1254
  if (await handleDisclosureSubmit(args, 'quickstart')) {
1218
1255
  return;
1219
1256
  }
@@ -1228,10 +1265,9 @@ https://github.com/onthegonow/a2a_calling`;
1228
1265
  return;
1229
1266
  }
1230
1267
 
1231
- const context = readWorkspaceContext(process.env.A2A_WORKSPACE || process.cwd());
1232
- const availableFiles = getDisclosurePromptFiles(context);
1233
-
1234
- // If server is already running and awaiting disclosure, skip to Step 2
1268
+ // Resume point: if the server is already running and we're waiting for the
1269
+ // agent to submit disclosure topics, skip straight to the disclosure prompt.
1270
+ // This happens when the agent re-runs quickstart after a previous partial run.
1235
1271
  let currentStep = 'not_started';
1236
1272
  try {
1237
1273
  const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
@@ -1244,42 +1280,46 @@ https://github.com/onthegonow/a2a_calling`;
1244
1280
  if (currentStep === 'awaiting_disclosure' && !args.flags.force) {
1245
1281
  console.log('\nStep 1 already complete. Server is running.\n');
1246
1282
  console.log('Step 2 of 4: Configure disclosure topics\n');
1247
- console.log(buildExtractionPrompt(availableFiles));
1283
+ console.log(buildExtractionPrompt());
1248
1284
  console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
1249
1285
  console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
1250
1286
  return;
1251
1287
  }
1252
1288
 
1253
1289
  printStepHeader('🤝 A2A Calling — First-Time Setup');
1254
- printWorkspaceScan(context);
1255
1290
 
1291
+ // Interactive: ask for confirmation. Non-interactive: auto-accepts (Y).
1256
1292
  const continueSetup = await promptYesNo('Continue with setup? [Y/n] ');
1257
1293
  if (!continueSetup) {
1258
1294
  console.log('\nSetup cancelled.\n');
1259
1295
  return;
1260
1296
  }
1261
1297
 
1298
+ // ── Step 1: Port selection ───────────────────────────────────────────
1299
+ // Scan ports 80, 3001-3020 and pick the first available one.
1300
+ // Only show the selected port — agents don't need to see every candidate.
1301
+ // Interactive users can override with a custom port; non-interactive
1302
+ // auto-accepts the recommendation.
1262
1303
  printSection('Port Configuration');
1263
1304
  const preferredPort = parsePort(args.flags.port || args.flags.p, null);
1264
1305
  const candidates = await inspectPorts(preferredPort);
1265
1306
  const availableCandidates = candidates.filter(c => c.available);
1266
1307
  const recommendedPort = availableCandidates.length ? availableCandidates[0].port : null;
1267
1308
 
1268
- summarizePortResults(candidates).forEach(line => {
1269
- console.log(` ${line}`);
1270
- });
1271
-
1272
1309
  if (!recommendedPort) {
1273
1310
  console.error(' Could not find a bindable port in the scan range.');
1274
1311
  console.error(' Re-run with --port <number> after freeing one of these ports.\n');
1312
+ if (interactive) {
1313
+ console.log(' Ports scanned:');
1314
+ summarizePortResults(candidates).forEach(line => console.log(` ${line}`));
1315
+ }
1275
1316
  process.exit(1);
1276
1317
  }
1277
1318
 
1278
- console.log(`\n Recommended: ${recommendedPort}`);
1319
+ console.log(` Selected port: ${recommendedPort}`);
1279
1320
  let serverPort = recommendedPort;
1280
1321
  const portChoice = await promptText(`Use port ${recommendedPort}? [Y/n/custom]: `, 'y');
1281
1322
  if (!interactive) {
1282
- // explicit default for non-interactive mode
1283
1323
  serverPort = recommendedPort;
1284
1324
  } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
1285
1325
  if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
@@ -1318,6 +1358,10 @@ https://github.com/onthegonow/a2a_calling`;
1318
1358
  }
1319
1359
  }
1320
1360
 
1361
+ // ── Step 2: Hostname detection ───────────────────────────────────────
1362
+ // Look up the machine's external IP so invite URLs point to a routable
1363
+ // address. Non-interactive: auto-uses the detected IP. Interactive: lets
1364
+ // the user choose IP, domain, or skip.
1321
1365
  printSection('Hostname Configuration');
1322
1366
  const ipResult = await getExternalIp();
1323
1367
  const externalIp = ipResult.ip || null;
@@ -1364,17 +1408,23 @@ https://github.com/onthegonow/a2a_calling`;
1364
1408
  console.log(` External IP lookup failed: ${ipResult.error}`);
1365
1409
  }
1366
1410
 
1411
+ // ── Step 3: Server start ─────────────────────────────────────────────
1412
+ // Launch the A2A Express server as a detached background process, wait
1413
+ // for it to bind, then save the config with the server PID and port.
1414
+ // Non-interactive: auto-starts. Interactive: asks for confirmation.
1415
+ // Also prints a one-line networking hint (reverse proxy or firewall)
1416
+ // and a curl command the agent can use to verify external reachability.
1367
1417
  printSection('Starting Server');
1368
1418
  console.log(' Configuration summary:');
1369
1419
  console.log(` Port: ${serverPort}`);
1370
1420
  console.log(` Public host: ${publicHost}`);
1421
+
1371
1422
  const startServer = await promptYesNo('Start the A2A server now? [Y/n] ');
1372
1423
  if (!startServer) {
1373
1424
  console.log('\nServer not started. Run with:\n a2a server --port <port> --hostname <host>\n');
1374
1425
  return;
1375
1426
  }
1376
1427
 
1377
- // Start server
1378
1428
  const isAlreadyListening = await isPortListening(serverPort, '127.0.0.1', { timeoutMs: 250 });
1379
1429
  let serverPid = null;
1380
1430
  if (!isAlreadyListening.listening) {
@@ -1414,28 +1464,32 @@ https://github.com/onthegonow/a2a_calling`;
1414
1464
  }
1415
1465
  console.log(' ✅ A2A server is running');
1416
1466
 
1417
- if (serverPort !== 80 && externalIp) {
1418
- console.log('\n External access required because port 80 is not in use.');
1419
- console.log(' Option A (recommended): Set up a reverse proxy (HTTP or HTTPS).');
1420
- console.log(` Configure your web server to forward /api/a2a/* to localhost:${serverPort}.`);
1421
- console.log(' If you serve HTTPS on port 443, proxy from there instead.');
1422
- console.log(' A reverse proxy avoids firewall changes entirely.');
1423
- console.log(`\n Option B: Open port ${serverPort} in your firewall (e.g. ufw allow ${serverPort}).`);
1424
- console.log(' Most users prefer not to modify firewall settings.\n');
1425
- }
1426
1467
  if (externalIp) {
1427
1468
  const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1428
- console.log(' External ping check:\n curl -s ' + verifyUrl);
1469
+ if (serverPort !== 80) {
1470
+ console.log(`\n Note: Port ${serverPort} must be reachable externally.`);
1471
+ console.log(` Reverse-proxy /api/a2a/* to localhost:${serverPort}, or open the port in your firewall.`);
1472
+ }
1473
+ console.log(` Verify: curl -s ${verifyUrl}`);
1429
1474
  }
1430
1475
 
1431
- // Save server config and advance onboarding
1476
+ // Save server config and advance onboarding state to awaiting_disclosure.
1432
1477
  config.setAgent({ hostname: publicHost });
1433
1478
  config.setOnboarding({ step: 'awaiting_disclosure' });
1434
1479
 
1435
- // Step 2 of 4: Configure disclosure topics
1480
+ // ── Step 4: Disclosure prompt ────────────────────────────────────────
1481
+ // Output a full agent-readable prompt that tells the agent to:
1482
+ // 1. Scan its own workspace for USER.md, SOUL.md, etc.
1483
+ // 2. Extract tiered disclosure topics from those files
1484
+ // 3. Build the required JSON structure
1485
+ // 4. Submit it back via `a2a quickstart --submit '<json>'`
1486
+ //
1487
+ // IMPORTANT: We do NOT pre-scan files here. The installer runs in a
1488
+ // subprocess and has no access to the agent's file-reading tools. The
1489
+ // prompt lists which files to look for and the agent reads them itself.
1436
1490
  printSection('Disclosure Topic Extraction');
1437
1491
  console.log('Step 2 of 4: Configure disclosure topics\n');
1438
- console.log(buildExtractionPrompt(availableFiles));
1492
+ console.log(buildExtractionPrompt());
1439
1493
  console.log('\n Read your workspace files, extract topics, and present to your owner for review.');
1440
1494
  console.log(" Then submit with: a2a quickstart --submit '<json>'\n");
1441
1495
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,35 +1,57 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Only run for global installs; skip in CI, dev, and Docker builds.
3
+ // ============================================================================
4
+ // npm postinstall hook — automatic onboarding launcher
5
+ //
6
+ // OBJECTIVE: When a user (or agent) runs `npm install -g a2acalling`, this
7
+ // script fires automatically and launches the full quickstart onboarding flow.
8
+ // The agent should never need a second command — install IS onboarding.
9
+ //
10
+ // KEY DESIGN DECISIONS:
11
+ //
12
+ // 1. No TTY gate. Agents are the primary users of this package and they
13
+ // run in non-interactive (piped) environments. Every prompt in the
14
+ // quickstart flow auto-accepts sensible defaults when there is no TTY,
15
+ // so the full onboarding completes unattended: port selection, hostname
16
+ // detection, server start, and disclosure prompt output.
17
+ //
18
+ // 2. stdio: 'inherit'. The onboarding output goes straight to whatever
19
+ // called npm — if that's a terminal the user sees it; if it's an agent
20
+ // the agent reads it. No /dev/tty tricks needed.
21
+ //
22
+ // 3. Never fail the install. If quickstart can't launch (e.g. missing node
23
+ // binary edge case), we print a hint and exit 0. A broken postinstall
24
+ // would prevent the package from installing at all.
25
+ //
26
+ // 4. Skip non-global installs, CI, and Docker builds. Local `npm install`
27
+ // in a project shouldn't trigger onboarding. CI and Docker are build
28
+ // environments, not runtime hosts.
29
+ // ============================================================================
30
+
4
31
  if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) process.exit(0);
5
32
  if (process.env.DOCKER) process.exit(0);
6
33
  if (process.env.npm_config_global !== 'true') process.exit(0);
7
34
 
35
+ const path = require('path');
8
36
  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
37
 
12
- function warnSuppressedOutput() {
13
- if (!isInteractive) {
14
- console.warn('\n⚠️ Output may be suppressed. Run \'a2a quickstart\' manually if you don\'t see prompts.');
15
- }
16
- }
38
+ const initCwd = process.env.INIT_CWD || process.env.HOME || process.cwd();
39
+ const cliPath = path.join(__dirname, '..', 'bin', 'cli.js');
17
40
 
18
- warnSuppressedOutput();
19
-
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'], {
41
+ // Launch quickstart — prompts auto-accept defaults when there's no TTY.
42
+ const result = spawnSync(process.execPath, [cliPath, 'quickstart'], {
23
43
  stdio: 'inherit',
24
- shell: true,
25
- cwd: process.env.HOME || process.cwd()
44
+ cwd: initCwd,
45
+ env: {
46
+ ...process.env,
47
+ A2A_WORKSPACE: process.env.A2A_WORKSPACE || initCwd
48
+ }
26
49
  });
27
50
 
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');
51
+ if (result.error) {
52
+ console.error('\nCould not auto-launch onboarding.');
53
+ console.error(`Reason: ${result.error.message}`);
54
+ console.error('\nRun manually: a2a quickstart\n');
33
55
  process.exit(0); // don't fail the install
34
56
  }
35
57
 
@@ -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). |