@dp-pcs/ogp 0.4.2 → 0.4.3

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.
Files changed (61) hide show
  1. package/README.md +45 -15
  2. package/dist/cli/federation.d.ts +14 -0
  3. package/dist/cli/federation.d.ts.map +1 -1
  4. package/dist/cli/federation.js +165 -17
  5. package/dist/cli/federation.js.map +1 -1
  6. package/dist/cli/project.d.ts +4 -3
  7. package/dist/cli/project.d.ts.map +1 -1
  8. package/dist/cli/project.js +34 -24
  9. package/dist/cli/project.js.map +1 -1
  10. package/dist/cli/setup.d.ts +4 -0
  11. package/dist/cli/setup.d.ts.map +1 -1
  12. package/dist/cli/setup.js +57 -4
  13. package/dist/cli/setup.js.map +1 -1
  14. package/dist/cli.js +11 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/daemon/agent-comms.js +1 -1
  17. package/dist/daemon/agent-comms.js.map +1 -1
  18. package/dist/daemon/heartbeat.d.ts +22 -0
  19. package/dist/daemon/heartbeat.d.ts.map +1 -0
  20. package/dist/daemon/heartbeat.js +119 -0
  21. package/dist/daemon/heartbeat.js.map +1 -0
  22. package/dist/daemon/intent-registry.d.ts.map +1 -1
  23. package/dist/daemon/intent-registry.js +12 -6
  24. package/dist/daemon/intent-registry.js.map +1 -1
  25. package/dist/daemon/keypair.d.ts +1 -0
  26. package/dist/daemon/keypair.d.ts.map +1 -1
  27. package/dist/daemon/keypair.js +119 -7
  28. package/dist/daemon/keypair.js.map +1 -1
  29. package/dist/daemon/message-handler.js +23 -16
  30. package/dist/daemon/message-handler.js.map +1 -1
  31. package/dist/daemon/notify.d.ts.map +1 -1
  32. package/dist/daemon/notify.js +47 -0
  33. package/dist/daemon/notify.js.map +1 -1
  34. package/dist/daemon/peers.d.ts +13 -0
  35. package/dist/daemon/peers.d.ts.map +1 -1
  36. package/dist/daemon/peers.js +77 -6
  37. package/dist/daemon/peers.js.map +1 -1
  38. package/dist/daemon/projects.d.ts +9 -6
  39. package/dist/daemon/projects.d.ts.map +1 -1
  40. package/dist/daemon/projects.js +23 -16
  41. package/dist/daemon/projects.js.map +1 -1
  42. package/dist/daemon/server.d.ts +1 -0
  43. package/dist/daemon/server.d.ts.map +1 -1
  44. package/dist/daemon/server.js +54 -44
  45. package/dist/daemon/server.js.map +1 -1
  46. package/dist/shared/help.js +2 -1
  47. package/dist/shared/help.js.map +1 -1
  48. package/docs/CLI-REFERENCE.md +1 -0
  49. package/docs/GETTING-STARTED.md +12 -1
  50. package/docs/cloudflare-named-tunnel-setup.md +126 -0
  51. package/docs/federation-flow.md +6 -6
  52. package/docs/project-intent-testing.md +97 -0
  53. package/docs/quickstart.md +12 -4
  54. package/docs/scopes.md +13 -13
  55. package/package.json +4 -4
  56. package/scripts/install-skills.js +19 -1
  57. package/scripts/test-project-intents.mjs +614 -0
  58. package/skills/ogp/SKILL.md +1 -1
  59. package/skills/ogp-agent-comms/SKILL.md +1 -1
  60. package/skills/ogp-expose/SKILL.md +103 -8
  61. package/skills/ogp-project/SKILL.md +47 -33
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, cpSync, readdirSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, cpSync, readdirSync, rmSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { fileURLToPath } from 'node:url';
@@ -55,6 +55,13 @@ const availableSkills = readdirSync(skillsSrc, { withFileTypes: true })
55
55
  .filter(d => d.isDirectory() && existsSync(join(skillsSrc, d.name, 'SKILL.md')))
56
56
  .map(d => d.name);
57
57
 
58
+ function getSkillVersion(skill) {
59
+ const skillFile = join(skillsSrc, skill, 'SKILL.md');
60
+ const content = readFileSync(skillFile, 'utf8');
61
+ const match = content.match(/^version:\s*(.+)$/m);
62
+ return match?.[1]?.trim() ?? 'unknown';
63
+ }
64
+
58
65
  if (availableSkills.length === 0) {
59
66
  console.log('No skills found in the skills/ directory.');
60
67
  process.exit(0);
@@ -176,6 +183,9 @@ async function main() {
176
183
  const src = join(skillsSrc, skill);
177
184
  const dest = join(platform.skillsDir, skill);
178
185
  try {
186
+ // Replace the installed skill directory wholesale so stale files from
187
+ // previous package versions do not survive upgrades.
188
+ rmSync(dest, { recursive: true, force: true });
179
189
  cpSync(src, dest, { recursive: true });
180
190
  console.log(` ✓ ${skill}`);
181
191
  installed++;
@@ -192,6 +202,14 @@ async function main() {
192
202
  console.log(`\n✅ Successfully installed ${totalInstalled} skill(s) to ${selectedPlatforms.length} platform(s)`);
193
203
  console.log('');
194
204
 
205
+ const installedVersions = selectedSkills
206
+ .map(skill => `${skill}@${getSkillVersion(skill)}`)
207
+ .join(', ');
208
+ console.log(`Installed skill versions: ${installedVersions}`);
209
+ console.log('Verify installed copies with:');
210
+ console.log(" rg -n '^version:' ~/.openclaw/skills/ogp*/SKILL.md ~/.claude/skills/ogp*/SKILL.md 2>/dev/null");
211
+ console.log('');
212
+
195
213
  // Platform-specific restart instructions
196
214
  const platformNames = selectedPlatforms.map(p => p.name);
197
215
  if (platformNames.includes('OpenClaw')) {
@@ -0,0 +1,614 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import fsp from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { setTimeout as sleep } from 'node:timers/promises';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const repoRoot = path.resolve(__dirname, '..');
13
+ const cliPath = path.join(repoRoot, 'dist', 'cli.js');
14
+
15
+ function timestamp() {
16
+ return new Date().toISOString().replace(/[:.]/g, '-');
17
+ }
18
+
19
+ function parseArgs(argv) {
20
+ const args = {
21
+ root: path.join(repoRoot, '.tmp', 'project-intent-test', timestamp()),
22
+ projectId: 'project-intent-smoke',
23
+ projectName: 'Project Intent Smoke Test',
24
+ skipBuild: false,
25
+ keepState: false,
26
+ ports: {
27
+ alpha: 18890,
28
+ beta: 18891
29
+ }
30
+ };
31
+
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ switch (arg) {
35
+ case '--root':
36
+ args.root = path.resolve(argv[++i]);
37
+ break;
38
+ case '--project-id':
39
+ args.projectId = argv[++i];
40
+ break;
41
+ case '--project-name':
42
+ args.projectName = argv[++i];
43
+ break;
44
+ case '--alpha-port':
45
+ args.ports.alpha = Number.parseInt(argv[++i], 10);
46
+ break;
47
+ case '--beta-port':
48
+ args.ports.beta = Number.parseInt(argv[++i], 10);
49
+ break;
50
+ case '--skip-build':
51
+ args.skipBuild = true;
52
+ break;
53
+ case '--keep-state':
54
+ args.keepState = true;
55
+ break;
56
+ case '--help':
57
+ case '-h':
58
+ printHelp();
59
+ process.exit(0);
60
+ default:
61
+ throw new Error(`Unknown argument: ${arg}`);
62
+ }
63
+ }
64
+
65
+ if (!Number.isInteger(args.ports.alpha) || !Number.isInteger(args.ports.beta)) {
66
+ throw new Error('Ports must be integers');
67
+ }
68
+
69
+ if (args.ports.alpha === args.ports.beta) {
70
+ throw new Error('Alpha and beta ports must differ');
71
+ }
72
+
73
+ return args;
74
+ }
75
+
76
+ function printHelp() {
77
+ console.log(`Project-intent end-to-end harness
78
+
79
+ Usage:
80
+ node scripts/test-project-intents.mjs [options]
81
+
82
+ Options:
83
+ --root <dir> State root (default: repo-local .tmp directory)
84
+ --project-id <id> Project ID to use
85
+ --project-name <name> Project name to use
86
+ --alpha-port <port> Alpha daemon port (default: 18890)
87
+ --beta-port <port> Beta daemon port (default: 18891)
88
+ --skip-build Skip npm run build
89
+ --keep-state Keep test state/logs after success
90
+ `);
91
+ }
92
+
93
+ function logSection(title) {
94
+ console.log(`\n=== ${title} ===`);
95
+ }
96
+
97
+ function formatShellCommand(command, args = [], envAdditions = {}) {
98
+ const envPrefix = Object.entries(envAdditions)
99
+ .map(([key, value]) => `${key}=${shellQuote(String(value))}`)
100
+ .join(' ');
101
+ const parts = [command, ...args].map(shellQuote);
102
+ return [envPrefix, ...parts].filter(Boolean).join(' ');
103
+ }
104
+
105
+ function shellQuote(value) {
106
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) {
107
+ return value;
108
+ }
109
+ return `'${value.replace(/'/g, `'\\''`)}'`;
110
+ }
111
+
112
+ async function ensureDir(dir) {
113
+ await fsp.mkdir(dir, { recursive: true });
114
+ }
115
+
116
+ async function writeJson(filePath, data) {
117
+ await ensureDir(path.dirname(filePath));
118
+ await fsp.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
119
+ }
120
+
121
+ async function readJson(filePath) {
122
+ const raw = await fsp.readFile(filePath, 'utf-8');
123
+ return JSON.parse(raw);
124
+ }
125
+
126
+ function getPeersPath(home) {
127
+ return path.join(home, 'peers.json');
128
+ }
129
+
130
+ function getProjectsPath(home) {
131
+ return path.join(home, 'projects.json');
132
+ }
133
+
134
+ function getConfigPath(home) {
135
+ return path.join(home, 'config.json');
136
+ }
137
+
138
+ function assert(condition, message) {
139
+ if (!condition) {
140
+ throw new Error(message);
141
+ }
142
+ }
143
+
144
+ async function runCommand(command, args, options = {}) {
145
+ const {
146
+ cwd = repoRoot,
147
+ env = process.env,
148
+ expectCode = 0,
149
+ label
150
+ } = options;
151
+
152
+ const printable = formatShellCommand(command, args, options.envAdditions || {});
153
+ console.log(`$ ${printable}`);
154
+
155
+ const child = spawn(command, args, {
156
+ cwd,
157
+ env,
158
+ stdio: ['ignore', 'pipe', 'pipe']
159
+ });
160
+
161
+ let stdout = '';
162
+ let stderr = '';
163
+
164
+ child.stdout.on('data', (chunk) => {
165
+ stdout += chunk.toString();
166
+ });
167
+ child.stderr.on('data', (chunk) => {
168
+ stderr += chunk.toString();
169
+ });
170
+
171
+ const code = await new Promise((resolve, reject) => {
172
+ child.on('error', reject);
173
+ child.on('close', resolve);
174
+ });
175
+
176
+ if (stdout.trim()) {
177
+ console.log(stdout.trimEnd());
178
+ }
179
+ if (stderr.trim()) {
180
+ console.error(stderr.trimEnd());
181
+ }
182
+
183
+ if (code !== expectCode) {
184
+ throw new Error(
185
+ `${label || command} exited with code ${code}; expected ${expectCode}`
186
+ );
187
+ }
188
+
189
+ return { code, stdout, stderr };
190
+ }
191
+
192
+ function startDaemon(home, name, logsDir) {
193
+ const env = {
194
+ ...process.env,
195
+ OGP_HOME: home
196
+ };
197
+
198
+ const stdoutLog = fs.createWriteStream(path.join(logsDir, `${name}.stdout.log`), { flags: 'a' });
199
+ const stderrLog = fs.createWriteStream(path.join(logsDir, `${name}.stderr.log`), { flags: 'a' });
200
+
201
+ const printable = formatShellCommand(process.execPath, [cliPath, 'start'], { OGP_HOME: home });
202
+ console.log(`$ ${printable}`);
203
+
204
+ const child = spawn(process.execPath, [cliPath, 'start'], {
205
+ cwd: repoRoot,
206
+ env,
207
+ stdio: ['ignore', 'pipe', 'pipe']
208
+ });
209
+
210
+ let stdout = '';
211
+ let stderr = '';
212
+
213
+ child.stdout.on('data', (chunk) => {
214
+ const text = chunk.toString();
215
+ stdout += text;
216
+ stdoutLog.write(text);
217
+ });
218
+ child.stderr.on('data', (chunk) => {
219
+ const text = chunk.toString();
220
+ stderr += text;
221
+ stderrLog.write(text);
222
+ });
223
+
224
+ return {
225
+ child,
226
+ home,
227
+ name,
228
+ getStdout: () => stdout,
229
+ getStderr: () => stderr,
230
+ closeLogs: () => {
231
+ stdoutLog.end();
232
+ stderrLog.end();
233
+ }
234
+ };
235
+ }
236
+
237
+ async function stopDaemon(daemon) {
238
+ if (!daemon?.child || daemon.child.exitCode !== null) {
239
+ daemon?.closeLogs?.();
240
+ return;
241
+ }
242
+
243
+ daemon.child.kill('SIGINT');
244
+ await Promise.race([
245
+ new Promise((resolve) => daemon.child.once('close', resolve)),
246
+ sleep(5000).then(() => {
247
+ daemon.child.kill('SIGKILL');
248
+ })
249
+ ]);
250
+ daemon.closeLogs();
251
+ }
252
+
253
+ async function waitForJson(url, daemon, timeoutMs = 15000) {
254
+ const started = Date.now();
255
+ while (Date.now() - started < timeoutMs) {
256
+ if (daemon?.child?.exitCode !== null) {
257
+ throw new Error(`${daemon.name} daemon exited early.\nstdout:\n${daemon.getStdout()}\nstderr:\n${daemon.getStderr()}`);
258
+ }
259
+
260
+ try {
261
+ const response = await fetch(url);
262
+ if (response.ok) {
263
+ return response.json();
264
+ }
265
+ } catch {
266
+ // Wait and retry while the daemon comes up.
267
+ }
268
+ await sleep(250);
269
+ }
270
+
271
+ throw new Error(`Timed out waiting for ${url}`);
272
+ }
273
+
274
+ async function waitForPeer(home, predicate, timeoutMs = 10000) {
275
+ const started = Date.now();
276
+ while (Date.now() - started < timeoutMs) {
277
+ if (fs.existsSync(getPeersPath(home))) {
278
+ const peers = await readJson(getPeersPath(home));
279
+ const match = peers.find(predicate);
280
+ if (match) {
281
+ return match;
282
+ }
283
+ }
284
+ await sleep(200);
285
+ }
286
+ throw new Error(`Timed out waiting for peer in ${home}`);
287
+ }
288
+
289
+ async function waitForProject(home, projectId, timeoutMs = 10000) {
290
+ const started = Date.now();
291
+ while (Date.now() - started < timeoutMs) {
292
+ if (fs.existsSync(getProjectsPath(home))) {
293
+ const projects = await readJson(getProjectsPath(home));
294
+ const match = projects.find((project) => project.id === projectId);
295
+ if (match) {
296
+ return match;
297
+ }
298
+ }
299
+ await sleep(200);
300
+ }
301
+ throw new Error(`Timed out waiting for project ${projectId} in ${home}`);
302
+ }
303
+
304
+ async function createGatewayConfig(home, gateway) {
305
+ const config = {
306
+ daemonPort: gateway.port,
307
+ openclawUrl: 'http://127.0.0.1:9',
308
+ openclawToken: 'test-token',
309
+ openclawHooksToken: 'test-hooks',
310
+ gatewayUrl: gateway.url,
311
+ displayName: gateway.displayName,
312
+ email: gateway.email,
313
+ stateDir: path.join(home, 'state'),
314
+ agentId: gateway.agentId,
315
+ platform: 'openclaw'
316
+ };
317
+
318
+ await ensureDir(home);
319
+ await ensureDir(config.stateDir);
320
+ await writeJson(getConfigPath(home), config);
321
+ }
322
+
323
+ function getContributionSummaries(project) {
324
+ return (project.topics || []).flatMap((topic) =>
325
+ (topic.contributions || []).map((contribution) => contribution.summary)
326
+ );
327
+ }
328
+
329
+ async function main() {
330
+ const args = parseArgs(process.argv.slice(2));
331
+ const runRoot = args.root;
332
+ const logsDir = path.join(runRoot, 'logs');
333
+ const alphaHome = path.join(runRoot, 'alpha');
334
+ const betaHome = path.join(runRoot, 'beta');
335
+
336
+ const alpha = {
337
+ home: alphaHome,
338
+ displayName: 'Alpha Test Gateway',
339
+ email: 'alpha@example.test',
340
+ agentId: 'alpha',
341
+ port: args.ports.alpha,
342
+ url: `http://127.0.0.1:${args.ports.alpha}`
343
+ };
344
+
345
+ const beta = {
346
+ home: betaHome,
347
+ displayName: 'Beta Test Gateway',
348
+ email: 'beta@example.test',
349
+ agentId: 'beta',
350
+ port: args.ports.beta,
351
+ url: `http://127.0.0.1:${args.ports.beta}`
352
+ };
353
+
354
+ let alphaDaemon;
355
+ let betaDaemon;
356
+
357
+ await ensureDir(logsDir);
358
+ await createGatewayConfig(alpha.home, alpha);
359
+ await createGatewayConfig(beta.home, beta);
360
+
361
+ try {
362
+ logSection('Build');
363
+ if (!args.skipBuild) {
364
+ await runCommand('npm', ['run', 'build'], { cwd: repoRoot, label: 'build' });
365
+ } else {
366
+ console.log('Skipping build because --skip-build was provided.');
367
+ }
368
+
369
+ assert(fs.existsSync(cliPath), `CLI build artifact not found: ${cliPath}`);
370
+
371
+ logSection('Start Local Gateways');
372
+ alphaDaemon = startDaemon(alpha.home, 'alpha', logsDir);
373
+ betaDaemon = startDaemon(beta.home, 'beta', logsDir);
374
+
375
+ const alphaCard = await waitForJson(`${alpha.url}/.well-known/ogp`, alphaDaemon);
376
+ const betaCard = await waitForJson(`${beta.url}/.well-known/ogp`, betaDaemon);
377
+ const betaRuntimeSenderId = betaCard.publicKey.substring(0, 32);
378
+
379
+ console.log(`Alpha online: ${alphaCard.displayName} (${alpha.url})`);
380
+ console.log(`Beta online: ${betaCard.displayName} (${beta.url})`);
381
+
382
+ logSection('Federate Alpha -> Beta');
383
+ await runCommand(
384
+ process.execPath,
385
+ [cliPath, 'federation', 'request', beta.url, 'beta-local', '--alias', 'beta-local'],
386
+ {
387
+ cwd: repoRoot,
388
+ env: { ...process.env, OGP_HOME: alpha.home },
389
+ envAdditions: { OGP_HOME: alpha.home },
390
+ label: 'alpha federation request'
391
+ }
392
+ );
393
+
394
+ const alphaPeerOnBeta = await waitForPeer(
395
+ beta.home,
396
+ (peer) => peer.gatewayUrl === alpha.url && peer.status === 'pending'
397
+ );
398
+
399
+ await runCommand(
400
+ process.execPath,
401
+ [
402
+ cliPath,
403
+ 'federation',
404
+ 'approve',
405
+ alphaPeerOnBeta.id,
406
+ '--intents',
407
+ 'message,agent-comms,project.join,project.contribute,project.query,project.status'
408
+ ],
409
+ {
410
+ cwd: repoRoot,
411
+ env: { ...process.env, OGP_HOME: beta.home },
412
+ envAdditions: { OGP_HOME: beta.home },
413
+ label: 'beta federation approve'
414
+ }
415
+ );
416
+
417
+ const betaPeerOnAlpha = await waitForPeer(
418
+ alpha.home,
419
+ (peer) => peer.gatewayUrl === beta.url && peer.status === 'approved'
420
+ );
421
+ const refreshedAlphaPeerOnBeta = await waitForPeer(
422
+ beta.home,
423
+ (peer) => peer.gatewayUrl === alpha.url && peer.status === 'approved'
424
+ );
425
+
426
+ console.log(`Alpha sees beta as approved peer: ${betaPeerOnAlpha.id}`);
427
+ console.log(`Beta sees alpha as approved peer: ${refreshedAlphaPeerOnBeta.id}`);
428
+
429
+ logSection('Owner-Side Local Project Smoke');
430
+ await runCommand(
431
+ process.execPath,
432
+ [cliPath, 'project', 'create', args.projectId, args.projectName, '--description', 'Project-intent test harness'],
433
+ {
434
+ cwd: repoRoot,
435
+ env: { ...process.env, OGP_HOME: alpha.home },
436
+ envAdditions: { OGP_HOME: alpha.home },
437
+ label: 'alpha project create'
438
+ }
439
+ );
440
+
441
+ await runCommand(
442
+ process.execPath,
443
+ [
444
+ cliPath,
445
+ 'project',
446
+ 'contribute',
447
+ args.projectId,
448
+ 'progress',
449
+ 'Alpha created the test project',
450
+ '--metadata',
451
+ JSON.stringify({ stage: 'owner-local-smoke' }),
452
+ '--local-only'
453
+ ],
454
+ {
455
+ cwd: repoRoot,
456
+ env: { ...process.env, OGP_HOME: alpha.home },
457
+ envAdditions: { OGP_HOME: alpha.home },
458
+ label: 'alpha local contribution'
459
+ }
460
+ );
461
+
462
+ const alphaProjectAfterLocal = await waitForProject(alpha.home, args.projectId);
463
+ assert(
464
+ getContributionSummaries(alphaProjectAfterLocal).includes('Alpha created the test project'),
465
+ 'Alpha local contribution did not persist'
466
+ );
467
+
468
+ logSection('Membership Isolation Before Join');
469
+ await runCommand(
470
+ process.execPath,
471
+ [cliPath, 'project', 'query-peer', refreshedAlphaPeerOnBeta.id, args.projectId, '--limit', '5'],
472
+ {
473
+ cwd: repoRoot,
474
+ env: { ...process.env, OGP_HOME: beta.home },
475
+ envAdditions: { OGP_HOME: beta.home },
476
+ expectCode: 1,
477
+ label: 'beta pre-join query-peer'
478
+ }
479
+ );
480
+
481
+ logSection('Join Flow');
482
+ await runCommand(
483
+ process.execPath,
484
+ [
485
+ cliPath,
486
+ 'project',
487
+ 'request-join',
488
+ refreshedAlphaPeerOnBeta.id,
489
+ args.projectId,
490
+ args.projectName,
491
+ '--description',
492
+ 'Joined through local harness'
493
+ ],
494
+ {
495
+ cwd: repoRoot,
496
+ env: { ...process.env, OGP_HOME: beta.home },
497
+ envAdditions: { OGP_HOME: beta.home },
498
+ label: 'beta request-join'
499
+ }
500
+ );
501
+
502
+ const alphaProjectAfterJoin = await waitForProject(alpha.home, args.projectId);
503
+ const betaProjectAfterJoin = await waitForProject(beta.home, args.projectId);
504
+
505
+ assert(
506
+ alphaProjectAfterJoin.members.includes(betaRuntimeSenderId),
507
+ 'Alpha project did not record beta using its runtime sender identity'
508
+ );
509
+ assert(
510
+ betaProjectAfterJoin.members.includes(beta.email),
511
+ 'Beta local project did not record beta email as a local member'
512
+ );
513
+
514
+ logSection('Remote Contribution And Query');
515
+ await runCommand(
516
+ process.execPath,
517
+ [
518
+ cliPath,
519
+ 'project',
520
+ 'send-contribution',
521
+ refreshedAlphaPeerOnBeta.id,
522
+ args.projectId,
523
+ 'decision',
524
+ 'Beta confirmed remote contribution path',
525
+ '--metadata',
526
+ JSON.stringify({ source: 'beta', assertion: 'remote-send' })
527
+ ],
528
+ {
529
+ cwd: repoRoot,
530
+ env: { ...process.env, OGP_HOME: beta.home },
531
+ envAdditions: { OGP_HOME: beta.home },
532
+ label: 'beta send-contribution'
533
+ }
534
+ );
535
+
536
+ const alphaProjectAfterRemoteContribution = await waitForProject(alpha.home, args.projectId);
537
+ assert(
538
+ getContributionSummaries(alphaProjectAfterRemoteContribution).includes('Beta confirmed remote contribution path'),
539
+ 'Alpha project did not receive beta contribution'
540
+ );
541
+
542
+ await runCommand(
543
+ process.execPath,
544
+ [cliPath, 'project', 'query-peer', refreshedAlphaPeerOnBeta.id, args.projectId, '--limit', '10'],
545
+ {
546
+ cwd: repoRoot,
547
+ env: { ...process.env, OGP_HOME: beta.home },
548
+ envAdditions: { OGP_HOME: beta.home },
549
+ label: 'beta query-peer post-join'
550
+ }
551
+ );
552
+
553
+ logSection('Status Request Path');
554
+ await runCommand(
555
+ process.execPath,
556
+ [cliPath, 'project', 'status-peer', refreshedAlphaPeerOnBeta.id, args.projectId],
557
+ {
558
+ cwd: repoRoot,
559
+ env: { ...process.env, OGP_HOME: beta.home },
560
+ envAdditions: { OGP_HOME: beta.home },
561
+ label: 'beta status-peer'
562
+ }
563
+ );
564
+
565
+ logSection('Validation Summary');
566
+ console.log('Validated:');
567
+ console.log('- Two isolated local gateways booted with separate OGP_HOME directories.');
568
+ console.log('- Federation request + approval completed.');
569
+ console.log('- Local owner project creation and contribution persisted.');
570
+ console.log('- Pre-join project query was denied as expected.');
571
+ console.log('- Remote project join succeeded.');
572
+ console.log('- Remote contribution arrived on the owner gateway.');
573
+ console.log('- Remote query succeeded after membership was granted.');
574
+ console.log('- Remote status request path completed without transport failure.');
575
+
576
+ console.log('\nManual replay commands:');
577
+ console.log(formatShellCommand(process.execPath, [cliPath, 'federation', 'request', beta.url, 'beta-local', '--alias', 'beta-local'], { OGP_HOME: alpha.home }));
578
+ console.log(formatShellCommand(process.execPath, [cliPath, 'federation', 'approve', alphaPeerOnBeta.id, '--intents', 'message,agent-comms,project.join,project.contribute,project.query,project.status'], { OGP_HOME: beta.home }));
579
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'create', args.projectId, args.projectName, '--description', 'Project-intent test harness'], { OGP_HOME: alpha.home }));
580
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'contribute', args.projectId, 'progress', 'Alpha created the test project', '--metadata', JSON.stringify({ stage: 'owner-local-smoke' }), '--local-only'], { OGP_HOME: alpha.home }));
581
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'request-join', refreshedAlphaPeerOnBeta.id, args.projectId, args.projectName, '--description', 'Joined through local harness'], { OGP_HOME: beta.home }));
582
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'send-contribution', refreshedAlphaPeerOnBeta.id, args.projectId, 'decision', 'Beta confirmed remote contribution path', '--metadata', JSON.stringify({ source: 'beta', assertion: 'remote-send' })], { OGP_HOME: beta.home }));
583
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'query-peer', refreshedAlphaPeerOnBeta.id, args.projectId, '--limit', '10'], { OGP_HOME: beta.home }));
584
+ console.log(formatShellCommand(process.execPath, [cliPath, 'project', 'status-peer', refreshedAlphaPeerOnBeta.id, args.projectId], { OGP_HOME: beta.home }));
585
+
586
+ console.log(`\nState root: ${runRoot}`);
587
+ console.log(`Alpha logs: ${path.join(logsDir, 'alpha.stdout.log')} / ${path.join(logsDir, 'alpha.stderr.log')}`);
588
+ console.log(`Beta logs: ${path.join(logsDir, 'beta.stdout.log')} / ${path.join(logsDir, 'beta.stderr.log')}`);
589
+
590
+ if (!args.keepState) {
591
+ await stopDaemon(alphaDaemon);
592
+ await stopDaemon(betaDaemon);
593
+ await fsp.rm(runRoot, { recursive: true, force: true });
594
+ console.log('\nCleaned up temporary state. Re-run with --keep-state to inspect files.');
595
+ return;
596
+ }
597
+
598
+ await stopDaemon(alphaDaemon);
599
+ await stopDaemon(betaDaemon);
600
+ console.log('\nKept state on disk because --keep-state was provided.');
601
+ } catch (error) {
602
+ console.error(`\nProject-intent harness failed: ${error instanceof Error ? error.message : String(error)}`);
603
+ console.error(`State root retained at: ${runRoot}`);
604
+ process.exitCode = 1;
605
+ } finally {
606
+ await stopDaemon(alphaDaemon);
607
+ await stopDaemon(betaDaemon);
608
+ }
609
+ }
610
+
611
+ main().catch((error) => {
612
+ console.error(error);
613
+ process.exit(1);
614
+ });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill_name: ogp
3
- version: 2.5.0
3
+ version: 2.6.0
4
4
  description: >
5
5
  OGP (Open Gateway Protocol) — federated agent communication, peer management,
6
6
  and project collaboration across OpenClaw and Hermes gateways. Use when the user
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill_name: ogp-agent-comms
3
- version: 0.5.0
3
+ version: 0.6.0
4
4
  description: Interactive wizard to configure agent-to-agent communication policies (updated for multi-framework `--for` workflows, OGP 0.2.24+ peer identity, and 0.2.28+ multi-agent routing)
5
5
  trigger: Use when the user wants to configure how their agent responds to incoming agent-comms messages from federated peers
6
6
  ---