brainclaw 1.14.0 → 1.16.0

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 (63) hide show
  1. package/README.md +16 -263
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-capture.js +209 -0
  4. package/dist/cli/register-code-map.js +19 -0
  5. package/dist/cli/register-coordination.js +472 -0
  6. package/dist/cli/register-federation.js +258 -0
  7. package/dist/cli/register-lifecycle.js +436 -0
  8. package/dist/cli/register-memory-context.js +502 -0
  9. package/dist/cli/register-planning.js +167 -0
  10. package/dist/cli/register-review.js +149 -0
  11. package/dist/cli/shared.js +5 -0
  12. package/dist/cli.js +212 -2015
  13. package/dist/commands/dispatch-watch.js +25 -2
  14. package/dist/commands/harvest.js +31 -6
  15. package/dist/commands/mcp-catalog.js +1438 -0
  16. package/dist/commands/mcp-contract.js +33 -0
  17. package/dist/commands/mcp-presentation.js +27 -0
  18. package/dist/commands/mcp-read-handlers.js +72 -36
  19. package/dist/commands/mcp-write-admin.js +328 -0
  20. package/dist/commands/mcp-write-claims.js +864 -0
  21. package/dist/commands/mcp-write-coordination.js +1825 -0
  22. package/dist/commands/mcp-write-entities.js +620 -0
  23. package/dist/commands/mcp-write-memory.js +451 -0
  24. package/dist/commands/mcp-write-sequences.js +116 -0
  25. package/dist/commands/mcp-write-support.js +367 -0
  26. package/dist/commands/mcp.js +261 -5570
  27. package/dist/commands/update-handoff.js +28 -42
  28. package/dist/core/agent-capability.js +31 -14
  29. package/dist/core/agent-files.js +1 -1
  30. package/dist/core/agent-registry.js +51 -3
  31. package/dist/core/claims.js +18 -0
  32. package/dist/core/coordination.js +5 -2
  33. package/dist/core/cross-project.js +35 -1
  34. package/dist/core/dispatcher.js +34 -20
  35. package/dist/core/entity-operations.js +335 -12
  36. package/dist/core/entity-registry.js +72 -9
  37. package/dist/core/execution.js +28 -4
  38. package/dist/core/facade-schema.js +30 -4
  39. package/dist/core/federation-cloud.js +142 -11
  40. package/dist/core/federation-outbox.js +292 -0
  41. package/dist/core/federation-signing.js +115 -0
  42. package/dist/core/handoff-review.js +35 -0
  43. package/dist/core/io.js +6 -0
  44. package/dist/core/protocol-tool-policy.js +113 -0
  45. package/dist/core/review-loop-close.js +115 -0
  46. package/dist/core/schema.js +25 -2
  47. package/dist/core/security-detectors.js +35 -6
  48. package/dist/core/security.js +32 -12
  49. package/dist/core/worktree.js +98 -9
  50. package/dist/facts.js +13 -11
  51. package/dist/facts.json +12 -10
  52. package/docs/PROTOCOL.md +7 -3
  53. package/docs/concepts/coordinator-runbook.md +3 -0
  54. package/docs/concepts/dispatch-lifecycle.md +4 -4
  55. package/docs/concepts/loop-engine.md +3 -1
  56. package/docs/concepts/troubleshooting.md +1 -1
  57. package/docs/integrations/codex.md +3 -3
  58. package/docs/integrations/overview.md +1 -1
  59. package/docs/mcp-schema-changelog.md +153 -2
  60. package/docs/playbooks/orchestration.md +1 -1
  61. package/docs/product/entity-model-audit.md +3 -2
  62. package/docs/security.md +22 -1
  63. package/package.json +3 -1
@@ -0,0 +1,258 @@
1
+ export function registerFederationCommands(program) {
2
+ // --- federation cloud ---
3
+ const federationCmd = program
4
+ .command('federation')
5
+ .description('Cloud federation — sync signals with app.brainclaw.dev');
6
+ federationCmd
7
+ .command('push <message>')
8
+ .description('Push a test signal to the cloud')
9
+ .option('--type <type>', 'Signal type', 'runtime_note')
10
+ .option('--to-project <project>', 'Target project name')
11
+ .option('--to-agent <agent>', 'Target agent name')
12
+ .action(async (message, options) => {
13
+ const { pushSignalToCloud, isCloudConfigured } = await import('../core/federation-cloud.js');
14
+ const { createFederationMessage } = await import('../core/federation-message.js');
15
+ const { loadConfig } = await import('../core/config.js');
16
+ const { resolveCurrentAgentName } = await import('../core/agent-registry.js');
17
+ if (!isCloudConfigured()) {
18
+ console.error('Error: cloud not configured. Set BRAINCLAW_CLOUD_API_KEY env var.');
19
+ process.exit(1);
20
+ }
21
+ const config = loadConfig();
22
+ const agent = resolveCurrentAgentName() ?? 'unknown';
23
+ const msg = createFederationMessage({
24
+ version: 1,
25
+ from: { project_name: config.project_name, project_path: process.cwd(), agent_name: agent },
26
+ to: {
27
+ project_name: options.toProject ?? 'broadcast',
28
+ project_path: '',
29
+ // Wire --to-agent into the message (was declared but dropped, so every
30
+ // push went out as a broadcast with to_agent NULL — found during the
31
+ // cross-machine E2E, pln#365). Omitted → undefined → broadcast, as before.
32
+ ...(options.toAgent ? { agent_name: options.toAgent } : {}),
33
+ },
34
+ type: options.type,
35
+ payload: { text: message },
36
+ });
37
+ const ok = await pushSignalToCloud(msg);
38
+ if (ok) {
39
+ console.log(`✔ Signal pushed to cloud: [${msg.id}] ${message}`);
40
+ }
41
+ else {
42
+ console.error('Error: failed to push signal to cloud.');
43
+ process.exit(1);
44
+ }
45
+ });
46
+ federationCmd
47
+ .command('pull')
48
+ .description('Pull signals from the cloud inbox')
49
+ .option('--agent <name>', 'Agent name to pull for')
50
+ .option('--since <date>', 'Only pull signals after this ISO date')
51
+ .option('--limit <n>', 'Max signals to pull', '20')
52
+ .action(async (options) => {
53
+ const { pullSignalsFromCloud, isCloudConfigured } = await import('../core/federation-cloud.js');
54
+ const { resolveCurrentAgentName } = await import('../core/agent-registry.js');
55
+ if (!isCloudConfigured()) {
56
+ console.error('Error: cloud not configured. Set BRAINCLAW_CLOUD_API_KEY env var.');
57
+ process.exit(1);
58
+ }
59
+ const agent = options.agent ?? resolveCurrentAgentName() ?? 'unknown';
60
+ const signals = await pullSignalsFromCloud(agent, {
61
+ since: options.since,
62
+ limit: parseInt(options.limit, 10),
63
+ });
64
+ if (signals.length === 0) {
65
+ console.log('No signals in cloud inbox.');
66
+ return;
67
+ }
68
+ console.log(`${signals.length} signal(s) from cloud:\n`);
69
+ for (const s of signals) {
70
+ const payload = typeof s.payload === 'object' && s.payload !== null ? s.payload.text ?? JSON.stringify(s.payload) : String(s.payload);
71
+ console.log(` [${s.id}] ${s.type} from ${s.from.project_name}/${s.from.agent_name}`);
72
+ console.log(` ${String(payload).slice(0, 120)}`);
73
+ console.log(` ${s.created_at}\n`);
74
+ }
75
+ });
76
+ federationCmd
77
+ .command('status')
78
+ .description('Diagnose cloud federation: config, health, signing identity, approved agent')
79
+ .action(async () => {
80
+ const { diagnoseCloudBridge } = await import('../core/federation-cloud.js');
81
+ const d = await diagnoseCloudBridge();
82
+ const yn = (b) => (b ? 'yes' : 'no');
83
+ console.log(`Cloud URL: ${d.apiUrl}`);
84
+ console.log(`API Key: ${process.env.BRAINCLAW_CLOUD_API_KEY ? '***configured***' : (d.configured ? 'from config' : 'NOT SET')}`);
85
+ console.log(`Configured: ${yn(d.configured)}`);
86
+ console.log(`Opted-in: ${yn(d.enabled)}`);
87
+ console.log(`Project: ${d.projectId ?? '(none)'}`);
88
+ console.log(`Require signed writes: ${yn(d.requireSigned)}`);
89
+ if (d.health) {
90
+ console.log(d.health.ok
91
+ ? `Cloud status: ${d.health.status} (v${d.health.version})`
92
+ : `Cloud unreachable: ${d.health.error ?? 'unknown error'}`);
93
+ }
94
+ if (d.signing.available) {
95
+ console.log('\nSigning identity:');
96
+ console.log(` Agent: ${d.signing.agentName} [${d.signing.cloudAgentId}]`);
97
+ console.log(` Key present: yes`);
98
+ console.log(` Fingerprint: ${d.signing.fingerprint.slice(0, 16)}…`);
99
+ }
100
+ else {
101
+ console.log(`\nSigning identity: unavailable — ${d.signing.reason}`);
102
+ }
103
+ if (d.approvedAgent) {
104
+ console.log('\nApproved agent (cloud):');
105
+ if (d.approvedAgent.found) {
106
+ console.log(` Status: ${d.approvedAgent.status ?? '(unknown)'}`);
107
+ console.log(` Trust: ${d.approvedAgent.trustLevel ?? '(unknown)'}`);
108
+ console.log(` Key match: ${d.approvedAgent.fingerprintMatch ? 'yes ✔' : 'NO ✗ (local key does not match the registered key)'}`);
109
+ }
110
+ else {
111
+ console.log(` Not found${d.approvedAgent.error ? ` — ${d.approvedAgent.error}` : ''}`);
112
+ }
113
+ }
114
+ if (d.requireSigned && !d.signing.available) {
115
+ console.log('\n⚠ require_signed is set but no signing identity is available — the bridge will refuse to push (fail-closed).');
116
+ }
117
+ });
118
+ federationCmd
119
+ .command('identity')
120
+ .description('Show this agent\'s federation signing identity (public key to approve in the cloud UI)')
121
+ .option('--agent <name>', 'Agent name (defaults to the current agent)')
122
+ .option('--json', 'Output as JSON')
123
+ .action(async (options) => {
124
+ const { resolveOrAutoRegisterAgentIdentity, ensureAgentSigningKey, resolveCurrentAgentName } = await import('../core/agent-registry.js');
125
+ const agentName = options.agent ?? resolveCurrentAgentName();
126
+ const { identity } = resolveOrAutoRegisterAgentIdentity({ agentName, cwd: process.cwd() });
127
+ const key = ensureAgentSigningKey(identity.agent_id);
128
+ if (options.json) {
129
+ console.log(JSON.stringify({
130
+ agent_name: identity.agent_name,
131
+ local_agent_id: identity.agent_id,
132
+ fingerprint: key.fingerprint,
133
+ public_key_pem: key.publicKeyPem,
134
+ }, null, 2));
135
+ return;
136
+ }
137
+ console.log(`Agent name: ${identity.agent_name}`);
138
+ console.log(`Local agent id: ${identity.agent_id}`);
139
+ console.log(`Fingerprint: ${key.fingerprint}`);
140
+ console.log('\nPublic key — paste into the cloud UI (project → Agents → Register / approve an agent):\n');
141
+ console.log(key.publicKeyPem.trim());
142
+ console.log('\nThen, on this machine, configure the bridge with the cloud agent id shown after approval:');
143
+ console.log(` export BRAINCLAW_AGENT_NAME=${identity.agent_name}`);
144
+ console.log(' export BRAINCLAW_CLOUD_AGENT_ID=<agt_... returned by the UI>');
145
+ console.log(' (plus BRAINCLAW_CLOUD_API_KEY, BRAINCLAW_PROJECT_ID) — then run `brainclaw federation status`.');
146
+ });
147
+ federationCmd
148
+ .command('sync')
149
+ .description('Drain the federation outbox — push signed claim upserts to the cloud')
150
+ .option('--entity <type>', 'Entity type to sync (increment 1: claim)', 'claim')
151
+ .option('--limit <n>', 'Max records to push this run')
152
+ .option('--dry-run', 'Reconcile + list pending records without any network calls')
153
+ .option('--json', 'Output as JSON')
154
+ .action(async (options) => {
155
+ const outbox = await import('../core/federation-outbox.js');
156
+ const { pushClaimToCloud, isCloudConfigured } = await import('../core/federation-cloud.js');
157
+ const cwd = process.cwd();
158
+ const PARK_AFTER = 5;
159
+ const reconciled = outbox.reconcileOutbox(cwd);
160
+ let records = outbox.listOutboxRecords(cwd);
161
+ if (options.limit)
162
+ records = records.slice(0, parseInt(options.limit, 10));
163
+ const counts = { synced: 0, superseded: 0, parked: reconciled.parked, dropped: reconciled.dropped, retry: 0 };
164
+ const lines = [];
165
+ const emit = (line) => { lines.push(line); if (!options.json)
166
+ console.log(line); };
167
+ if (options.dryRun) {
168
+ for (const r of records)
169
+ emit(`pending ${r.record.entity_type} ${r.record.entity_id} r${r.record.rev} (${r.record.to_status})`);
170
+ const out = { dry_run: true, reconciled, pending: records.length, records: lines };
171
+ if (options.json)
172
+ console.log(JSON.stringify(out, null, 2));
173
+ else
174
+ console.log(`\npending=${records.length} reconciled_dropped=${reconciled.dropped} reconciled_parked=${reconciled.parked}`);
175
+ return;
176
+ }
177
+ if (!isCloudConfigured(cwd)) {
178
+ console.error('Error: cloud not configured (set BRAINCLAW_CLOUD_API_KEY or cloud_sync). Records left in outbox.');
179
+ process.exit(3);
180
+ }
181
+ let failClosed = false;
182
+ for (const r of records) {
183
+ const res = await pushClaimToCloud(r.record.payload, cwd);
184
+ const tag = `${r.record.entity_id} r${r.record.rev}`;
185
+ if (res.kind === 'not_configured' || res.kind === 'fail_closed') {
186
+ failClosed = true;
187
+ emit(`fail-closed ${tag} (${res.kind}) — not sent`);
188
+ break; // same config for all remaining records
189
+ }
190
+ if (res.kind === 'network_error') {
191
+ const attempts = r.record.attempts + 1;
192
+ if (attempts >= PARK_AFTER) {
193
+ outbox.parkRecord(r, `network error x${attempts}: ${res.error}`, cwd);
194
+ counts.parked++;
195
+ emit(`park ${tag} (network x${attempts}: ${res.error})`);
196
+ }
197
+ else {
198
+ outbox.recordAttempt(r, { http_status: null, error: res.error }, cwd);
199
+ counts.retry++;
200
+ emit(`retry ${tag} (network: ${res.error})`);
201
+ }
202
+ continue;
203
+ }
204
+ const { httpStatus, code } = res;
205
+ if (httpStatus === 200 || httpStatus === 201) {
206
+ outbox.archiveToSent(r, { http_status: httpStatus }, cwd);
207
+ counts.synced++;
208
+ emit(`pushed ${tag} → ${httpStatus}`);
209
+ }
210
+ else if (httpStatus === 409 && (code === 'STALE' || code === 'stale_version')) {
211
+ outbox.archiveToSent(r, { http_status: httpStatus }, cwd);
212
+ counts.superseded++;
213
+ emit(`superseded ${tag} → 409 ${code} (cloud has a newer rev)`);
214
+ }
215
+ else if (httpStatus === 409) {
216
+ outbox.parkRecord(r, `409 ${code ?? 'conflict'}`, cwd);
217
+ counts.parked++;
218
+ emit(`PARK ${tag} → 409 ${code ?? 'conflict'} (divergence — inspect)`);
219
+ }
220
+ else if (httpStatus === 403) {
221
+ outbox.parkRecord(r, `403 ${code ?? 'forbidden'}`, cwd);
222
+ counts.parked++;
223
+ emit(`PARK ${tag} → 403 ${code ?? 'forbidden'}`);
224
+ }
225
+ else if (httpStatus >= 500) {
226
+ const attempts = r.record.attempts + 1;
227
+ if (attempts >= PARK_AFTER) {
228
+ outbox.parkRecord(r, `5xx x${attempts} (last ${httpStatus})`, cwd);
229
+ counts.parked++;
230
+ emit(`park ${tag} → ${httpStatus} (x${attempts})`);
231
+ }
232
+ else {
233
+ outbox.recordAttempt(r, { http_status: httpStatus, error: null }, cwd);
234
+ counts.retry++;
235
+ emit(`retry ${tag} → ${httpStatus}`);
236
+ }
237
+ }
238
+ else {
239
+ outbox.parkRecord(r, `${httpStatus} ${code ?? 'client error'}`, cwd);
240
+ counts.parked++;
241
+ emit(`PARK ${tag} → ${httpStatus} ${code ?? ''}`.trim());
242
+ }
243
+ }
244
+ const summary = `synced=${counts.synced} superseded=${counts.superseded} retry=${counts.retry} parked=${counts.parked} dropped=${counts.dropped}`;
245
+ if (options.json)
246
+ console.log(JSON.stringify({ ...counts, fail_closed: failClosed, records: lines }, null, 2));
247
+ else
248
+ console.log(`\n${summary}`);
249
+ if (failClosed)
250
+ process.exit(3);
251
+ if (counts.parked > 0)
252
+ process.exit(2);
253
+ if (counts.retry > 0)
254
+ process.exit(1);
255
+ // exit 0
256
+ });
257
+ }
258
+ //# sourceMappingURL=register-federation.js.map