brainclaw 1.21.0 → 1.23.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 (50) hide show
  1. package/dist/brainclaw-vscode.vsix +0 -0
  2. package/dist/cli/register-capture.js +15 -0
  3. package/dist/cli/register-cloud.js +63 -0
  4. package/dist/cli.js +2 -3
  5. package/dist/commands/cloud.js +198 -0
  6. package/dist/commands/loops-handlers.js +0 -1
  7. package/dist/commands/mcp-catalog.js +24 -256
  8. package/dist/commands/mcp-read-handlers.js +5 -1
  9. package/dist/commands/mcp-schemas.generated.js +811 -1
  10. package/dist/commands/mcp-write-coordination.js +16 -7
  11. package/dist/commands/mcp.js +45 -1
  12. package/dist/commands/memory-confirm.js +83 -0
  13. package/dist/commands/session-end.js +0 -102
  14. package/dist/commands/session-start.js +0 -23
  15. package/dist/commands/switch.js +24 -2
  16. package/dist/core/assignment-request-schema.js +112 -0
  17. package/dist/core/capture-schema.js +62 -0
  18. package/dist/core/claim-request-schema.js +72 -0
  19. package/dist/core/claims.js +0 -18
  20. package/dist/core/code-map/aggregate.js +36 -1
  21. package/dist/core/coordination.js +1 -3
  22. package/dist/core/federation-attestation.js +96 -0
  23. package/dist/core/federation-canonical.js +95 -0
  24. package/dist/core/federation-hpke.js +213 -0
  25. package/dist/core/federation-inbound.js +187 -0
  26. package/dist/core/federation-keyring.js +241 -0
  27. package/dist/core/federation-message.js +5 -5
  28. package/dist/core/federation-outbox-v2.js +125 -0
  29. package/dist/core/federation-pairing.js +213 -0
  30. package/dist/core/federation-projection.js +336 -0
  31. package/dist/core/federation-relay.js +223 -0
  32. package/dist/core/federation-state.js +270 -0
  33. package/dist/core/ids.js +5 -0
  34. package/dist/core/schema.js +0 -17
  35. package/dist/core/sequence-request-schema.js +93 -0
  36. package/dist/core/session-request-schema.js +90 -0
  37. package/dist/core/step-request-schema.js +112 -0
  38. package/dist/core/store-resolution.js +34 -5
  39. package/dist/core/warnings.js +37 -0
  40. package/dist/facts.js +9 -9
  41. package/dist/facts.json +8 -8
  42. package/docs/cli.md +70 -39
  43. package/docs/concepts/federation-v2-rfc.md +275 -0
  44. package/docs/index.md +1 -0
  45. package/docs/integrations/mcp.md +1 -1
  46. package/package.json +2 -2
  47. package/dist/cli/register-federation.js +0 -258
  48. package/dist/core/federation-cloud.js +0 -245
  49. package/dist/core/federation-outbox.js +0 -292
  50. package/dist/core/federation-signing.js +0 -115
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Schémas zod des entrées de la famille SESSION — `bclaw_session_start` et
3
+ * `bclaw_session_end` (pln#599 batch 2, troisième famille composite).
4
+ *
5
+ * ── LA PARTICULARITÉ DE CETTE FAMILLE : AUCUN CHAMP REQUIS ────────────────────
6
+ * Les deux outils ont un `properties` fourni mais PAS de clé `required`. C'est délibéré et
7
+ * doit être préservé au bit près : `bclaw_session_start` sans argument est l'appel normal,
8
+ * et l'identité comme le contexte se résolvent depuis l'ambiance.
9
+ *
10
+ * zod n'émet `required` que s'il existe au moins un champ non-optionnel — donc marquer
11
+ * TOUS les champs `.optional()` reproduit exactement l'absence de la clé. C'est le
12
+ * pendant du piège inverse rencontré sur la famille séquence : là-bas un requis était
13
+ * devenu optionnel (assouplissement) ; ici, oublier un `.optional()` créerait un requis
14
+ * là où il n'y en avait aucun — un DURCISSEMENT qui casserait l'appel sans argument.
15
+ *
16
+ * ── CE QUI N'EST PAS RESSERRÉ, DÉLIBÉRÉMENT ───────────────────────────────────
17
+ * `contextProfile` et `contextFormat` énumèrent leurs valeurs dans leur description mais
18
+ * restent des chaînes libres : les profils sont extensibles côté produit, et un enum
19
+ * publié figerait cette extensibilité. `maintenanceMode` garde en revanche son enum,
20
+ * parce qu'il en avait déjà un.
21
+ *
22
+ * ── GARDE-FOU DE GÉNÉRATION ───────────────────────────────────────────────────
23
+ * zod émet `additionalProperties: false` d'office ; le générateur le retire À LA RACINE
24
+ * uniquement (cf. OPEN_SCHEMAS dans scripts/build-mcp-schemas.mjs). Le laisser durcirait
25
+ * la surface ; le retirer plus profond l'assouplirait.
26
+ */
27
+ import { z } from 'zod';
28
+ /** Identité de l'appelant — commune à toutes les familles migrées. */
29
+ const CallerIdentity = {
30
+ agent: z.string().describe('Agent name.').optional(),
31
+ agentId: z.string().describe('Registered agent id.').optional(),
32
+ };
33
+ export const SessionStartRequestSchema = z.object({
34
+ ...CallerIdentity,
35
+ context: z.string().describe('Context target path.').optional(),
36
+ // Enum CONSERVÉ : il existait déjà dans le schéma manuel.
37
+ maintenanceMode: z
38
+ .enum(['fast', 'full'])
39
+ .describe('Maintenance mode. Default is full for explicit session-start calls; use fast to skip non-critical maintenance work.')
40
+ .optional(),
41
+ includeContext: z
42
+ .boolean()
43
+ .describe('Include project memory context in the response (equivalent to bclaw_get_context).')
44
+ .optional(),
45
+ includeBoard: z
46
+ .boolean()
47
+ .describe('Include agent board (plans, claims, handoffs) in the response (equivalent to bclaw_get_agent_board).')
48
+ .optional(),
49
+ // Chaîne libre : les profils sont extensibles, un enum publié figerait cette
50
+ // extensibilité et rejetterait un profil ajouté côté produit.
51
+ contextProfile: z
52
+ .string()
53
+ .describe('Context profile when includeContext is true: dev (default), dense, compact, copilot, quick, briefing, openclaw, ops, research. If unset, uses the agent default profile.')
54
+ .optional(),
55
+ contextFormat: z
56
+ .string()
57
+ .describe('Context format when includeContext is true: markdown, json, or template.')
58
+ .optional(),
59
+ });
60
+ export const SessionEndRequestSchema = z.object({
61
+ session: z.string().describe('Session ID.').optional(),
62
+ ...CallerIdentity,
63
+ summary: z.string().describe('Session summary text.').optional(),
64
+ narrative: z
65
+ .string()
66
+ .describe('Free-text narrative of what happened in the session and why. Goes beyond the auto-generated commit list: "Tried X, failed because Y, pivoted to Z. Watch out for A."')
67
+ .optional(),
68
+ autoReflect: z.boolean().describe('Auto-reflect session notes as candidates.').optional(),
69
+ autoRelease: z
70
+ .boolean()
71
+ .describe('Auto-release any active claims at session end.')
72
+ .optional(),
73
+ reflectHandoff: z
74
+ .boolean()
75
+ .describe('Materialize an open handoff from git commits since session start.')
76
+ .optional(),
77
+ dispatchReview: z
78
+ .boolean()
79
+ .describe('When used with reflectHandoff, auto-dispatch a code review if the reflected handoff is reviewable.')
80
+ .optional(),
81
+ reviewer: z
82
+ .string()
83
+ .describe('Explicit reviewer for the reflected handoff review dispatch.')
84
+ .optional(),
85
+ reflect: z
86
+ .boolean()
87
+ .describe('Emit the dogfooding reflection prompt (project + your surfaces/skills/tools). Default true — pass false to suppress on a trivial session. Capture actionable findings via bclaw_quick_capture.')
88
+ .optional(),
89
+ });
90
+ //# sourceMappingURL=session-request-schema.js.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Schémas zod des entrées de la famille STEP — `bclaw_add_step`, `bclaw_update_step`,
3
+ * `bclaw_complete_step`, `bclaw_delete_step` (pln#599 batch 2, quatrième famille).
4
+ *
5
+ * ── CE QUE CETTE FAMILLE APPREND, ET QUI CORRIGE UNE RÈGLE TROP VITE GÉNÉRALISÉE ─
6
+ * Sur la famille séquence j'avais formulé la consigne « retirer `additionalProperties`
7
+ * À LA RACINE UNIQUEMENT », au motif que le sous-schéma d'item de lane le portait déjà
8
+ * dans sa version manuelle. C'était vrai LÀ, et faux comme règle générale.
9
+ *
10
+ * Ici, le sous-objet `data` de `bclaw_add_step` n'a PAS d'`additionalProperties` dans la
11
+ * version écrite à la main. Appliquer « racine uniquement » y laisserait donc le
12
+ * `additionalProperties: false` émis par zod — exactement le DURCISSEMENT que la règle
13
+ * était censée empêcher, réintroduit par la règle elle-même.
14
+ *
15
+ * La consigne réelle n'a jamais été « racine » : c'est « reproduire le schéma manuel au
16
+ * bit près ». Le générateur porte désormais une profondeur de retrait PAR SCHÉMA
17
+ * (cf. OPEN_SCHEMAS dans scripts/build-mcp-schemas.mjs) au lieu d'une règle globale.
18
+ *
19
+ * ── DEUX FORMES D'APPEL COEXISTENT, DÉLIBÉRÉMENT ──────────────────────────────
20
+ * `bclaw_add_step` accepte la forme canonique `{ planId, data: {...} }` ET la forme
21
+ * historique `{ planId, text, assignee }`. Les deux sont publiées ; supprimer la seconde
22
+ * du schéma casserait les appelants existants. `title` reste un alias de `text`.
23
+ *
24
+ * ── CE QUI N'EST PAS RESSERRÉ ─────────────────────────────────────────────────
25
+ * `status` reste une chaîne libre bien que ses cinq valeurs soient énumérées dans sa
26
+ * description : en faire un enum serait un rejet nouveau sur des appels aujourd'hui
27
+ * acceptés, donc une décision à part entière.
28
+ */
29
+ import { z } from 'zod';
30
+ /** Identité de l'appelant — commune à toutes les familles migrées. */
31
+ const CallerIdentity = {
32
+ agent: z.string().describe('Agent name.').optional(),
33
+ agentId: z.string().describe('Registered agent id.').optional(),
34
+ };
35
+ /**
36
+ * Charge utile canonique d'un step. Ses champs sont TOUS optionnels et l'objet ne porte
37
+ * PAS d'`additionalProperties` — voir l'en-tête : c'est ce sous-objet qui a révélé que la
38
+ * profondeur de retrait devait être décidée par schéma.
39
+ */
40
+ const AddStepDataSchema = z
41
+ .object({
42
+ text: z.string().describe('Step description.').optional(),
43
+ title: z.string().describe('Alias for text.').optional(),
44
+ assignee: z.string().describe('Optional assignee.').optional(),
45
+ estimated_effort: z
46
+ .number()
47
+ .describe('Step-level estimate in minutes (pln#495). A duration string like "2h"/"30m" is also accepted and coerced.')
48
+ .optional(),
49
+ actual_effort: z
50
+ .string()
51
+ .describe('Step-level actual effort, free-form ("45m", "2h"), parsed when the estimation report runs.')
52
+ .optional(),
53
+ })
54
+ .describe('Canonical step payload: { text, title?, assignee? }. title is accepted as an alias for text.');
55
+ export const AddStepRequestSchema = z.object({
56
+ planId: z.string().describe('Plan item ID.'),
57
+ data: AddStepDataSchema.optional(),
58
+ // Forme HISTORIQUE, conservée : elle est publiée et des appelants s'en servent.
59
+ text: z.string().describe('Legacy top-level step description; prefer data.text.').optional(),
60
+ ...CallerIdentity,
61
+ assignee: z
62
+ .string()
63
+ .describe('Legacy top-level optional assignee; prefer data.assignee.')
64
+ .optional(),
65
+ project: z
66
+ .string()
67
+ .describe('Optional: name (or path/basename) of a linked project to add the step in. Defaults to the current project. Same resolution as canonical-grammar tools — accepts cross_project_links and workspace store-chain children.')
68
+ .optional(),
69
+ });
70
+ export const UpdateStepRequestSchema = z.object({
71
+ planId: z.string().describe('Plan item ID.'),
72
+ stepId: z.string().describe('Step ID to update.'),
73
+ // Chaîne libre, PAS un enum : les cinq valeurs sont documentées, pas imposées.
74
+ status: z
75
+ .string()
76
+ .describe('New status: todo, in_progress, testing, done, blocked.')
77
+ .optional(),
78
+ text: z.string().describe('New step text.').optional(),
79
+ assignee: z.string().describe('New assignee (empty string to unassign).').optional(),
80
+ estimated_effort: z
81
+ .number()
82
+ .describe('Step-level estimate in minutes (pln#495); a duration string is also coerced.')
83
+ .optional(),
84
+ actual_effort: z
85
+ .string()
86
+ .describe('Step-level actual effort, free-form ("45m", "2h").')
87
+ .optional(),
88
+ ...CallerIdentity,
89
+ project: z
90
+ .string()
91
+ .describe('Optional: name of a linked project to update the step in. Defaults to the current project.')
92
+ .optional(),
93
+ });
94
+ export const CompleteStepRequestSchema = z.object({
95
+ planId: z.string().describe('Plan item ID.'),
96
+ stepId: z.string().describe('Step ID to complete.'),
97
+ ...CallerIdentity,
98
+ project: z
99
+ .string()
100
+ .describe('Optional: name of a linked project to complete the step in. Defaults to the current project.')
101
+ .optional(),
102
+ });
103
+ export const DeleteStepRequestSchema = z.object({
104
+ planId: z.string().describe('Plan item ID.'),
105
+ stepId: z.string().describe('Step ID to delete.'),
106
+ ...CallerIdentity,
107
+ project: z
108
+ .string()
109
+ .describe('Optional: name of a linked project to delete the step from. Defaults to the current project.')
110
+ .optional(),
111
+ });
112
+ //# sourceMappingURL=step-request-schema.js.map
@@ -102,11 +102,7 @@ export function resolveTargetStore(cwd = process.cwd(), target = 'local', option
102
102
  export function resolveEffectiveCwd(options = {}) {
103
103
  return resolveEffectiveCwdInfo(options).cwd;
104
104
  }
105
- /**
106
- * Resolve the effective cwd and explain which selector won. Use this for MCP
107
- * facades that must echo their project scope to avoid silent cross-project reads.
108
- */
109
- export function resolveEffectiveCwdInfo(options = {}) {
105
+ function resolveEffectiveCwdInner(options, observed) {
110
106
  const baseCwd = path.resolve(options.baseCwd ?? process.cwd());
111
107
  // 1. Explicit --cwd flag
112
108
  if (options.explicitCwd) {
@@ -192,9 +188,18 @@ export function resolveEffectiveCwdInfo(options = {}) {
192
188
  // A named id is an exact-file lookup, so the record IS the one asked for; the
193
189
  // pid check covers the unnamed case. Anything else is a weak adoption.
194
190
  if (opts?.requireStrongIdentity && !explicitSessionId && session.pid !== process.pid) {
191
+ // Observe avant de rejeter : une session d'un AUTRE processus qui designe un autre
192
+ // projet est exactement le cas ou une ecriture peut partir ailleurs en silence.
193
+ const weak = session.active_project;
194
+ if (weak && !observed.sessionProject)
195
+ observed.sessionProject = { path: weak.path, name: weak.name };
195
196
  return undefined;
196
197
  }
197
198
  const sp = session.active_project;
199
+ // Retenu AVANT le controle d'adoption : un record trouvable qui designe un projet
200
+ // compte comme observation meme quand il n'est pas retenu.
201
+ if (sp && !observed.sessionProject)
202
+ observed.sessionProject = { path: sp.path, name: sp.name };
198
203
  if (sp && fs.existsSync(path.join(sp.path, MEMORY_DIR, 'config.yaml'))) {
199
204
  return { cwd: sp.path, active_source: 'session', resolved_project: { path: sp.path, name: sp.name } };
200
205
  }
@@ -287,6 +292,30 @@ export function resolveEffectiveCwdInfo(options = {}) {
287
292
  // 7. Default
288
293
  return { cwd: anchorCwd, active_source: 'cwd', resolved_project: projectInfo(anchorCwd) };
289
294
  }
295
+ /**
296
+ * Point d'entree unique de la resolution (pln#648 SUITE d).
297
+ *
298
+ * Enrichit le verdict d'un signal de divergence quand un record de session trouvable
299
+ * designait un AUTRE projet que celui retenu. Le calcul est purement local : la sonde a
300
+ * deja lu le record, aucune lecture disque n'est ajoutee.
301
+ */
302
+ export function resolveEffectiveCwdInfo(options = {}) {
303
+ const observed = {};
304
+ const result = resolveEffectiveCwdInner(options, observed);
305
+ const seen = observed.sessionProject;
306
+ if (!seen || result.active_source === 'session')
307
+ return result;
308
+ if (path.resolve(seen.path) === path.resolve(result.cwd))
309
+ return result;
310
+ return {
311
+ ...result,
312
+ session_divergence: {
313
+ session_project_path: seen.path,
314
+ session_project_name: seen.name,
315
+ resolved_via: result.active_source,
316
+ },
317
+ };
318
+ }
290
319
  function projectInfo(cwd) {
291
320
  try {
292
321
  const config = loadConfig(cwd);
@@ -48,6 +48,43 @@ export function pushStructuredWarning(warnings, details, input) {
48
48
  // Each owns its recovery path, which is the entire point of the structured
49
49
  // channel: `scope_already_claimed` used to be a dead-end string; now it names
50
50
  // the two calls that resolve it.
51
+ /**
52
+ * `autoExecute: true` sur `intent='consult'` — un no-op, dit sur le canal STRUCTURE
53
+ * (pln#626 phase 3).
54
+ *
55
+ * POURQUOI PAS UNE ERREUR DURE. Le plan laissait le choix entre refuser et implementer un
56
+ * vrai « consult run ». Refuser casserait des appelants existants pour un drapeau qui n'a
57
+ * jamais rien fait, et l'implementer est une decision produit — le plan lui-meme penche
58
+ * pour le spawn sur les cibles spawn-only, ce qui depasse une correction de surface.
59
+ *
60
+ * POURQUOI PAS SEULEMENT UN TEXTE. Phase 1 avait deja pousse un avertissement en texte
61
+ * libre, ce qui vaut mieux que le silence mais reste illisible pour une machine : un agent
62
+ * ne peut pas brancher dessus. Le code structure rend le refus DETECTABLE, et la
63
+ * next_action nomme les deux chemins qui spawnent reellement.
64
+ *
65
+ * C'est la moitie de la phase 3 qui ne demande aucun arbitrage : rendre le no-op
66
+ * observable. L'autre moitie — refuser ou spawner — reste au produit.
67
+ */
68
+ export function consultAutoExecuteNoOpWarning() {
69
+ return {
70
+ code: 'auto_execute_ignored_on_consult',
71
+ message: "autoExecute has no effect on intent='consult': consult delivers the RFC to the target "
72
+ + 'inbox(es) only and never spawns an agent — targets pick it up via their own bclaw_work.',
73
+ data: { intent: 'consult', auto_execute_honored: false },
74
+ next_actions: [
75
+ {
76
+ tool: 'bclaw_dispatch',
77
+ args: { intent: 'execute' },
78
+ when: 'to actually spawn workers on a sequence lane',
79
+ },
80
+ {
81
+ tool: 'bclaw_coordinate',
82
+ args: { intent: 'assign' },
83
+ when: 'to hand one scope to one agent and have it started',
84
+ },
85
+ ],
86
+ };
87
+ }
51
88
  export function agentValidationFailedWarning(input) {
52
89
  return {
53
90
  code: 'agent_validation_failed',
package/dist/facts.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.21.0 on 2026-08-07T12:11:23.699Z
2
+ // Source: brainclaw v1.23.0 on 2026-08-09T01:18:58.355Z
3
3
  export const FACTS = {
4
- "version": "1.21.0",
5
- "generated_at": "2026-08-07T12:11:23.699Z",
4
+ "version": "1.23.0",
5
+ "generated_at": "2026-08-09T01:18:58.355Z",
6
6
  "tools": {
7
7
  "count": 67,
8
8
  "published_count": 65,
@@ -474,7 +474,7 @@ export const FACTS = {
474
474
  },
475
475
  "bench": {
476
476
  "schema": "brainclaw.bench.v1",
477
- "generated_at": "2026-08-07T12:11:22.030Z",
477
+ "generated_at": "2026-08-09T01:18:56.213Z",
478
478
  "node_version": "v24.18.0",
479
479
  "platform": "linux-x64",
480
480
  "repeats": 3,
@@ -483,7 +483,7 @@ export const FACTS = {
483
483
  "name": "cold_onboard",
484
484
  "volume": "empty",
485
485
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
486
- "duration_ms_median": 71,
486
+ "duration_ms_median": 77,
487
487
  "payload_chars_median": 1640,
488
488
  "payload_tokens_est_median": 410
489
489
  },
@@ -491,15 +491,15 @@ export const FACTS = {
491
491
  "name": "warm_work",
492
492
  "volume": "medium",
493
493
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
494
- "duration_ms_median": 101,
495
- "payload_chars_median": 2625,
496
- "payload_tokens_est_median": 656
494
+ "duration_ms_median": 131,
495
+ "payload_chars_median": 2626,
496
+ "payload_tokens_est_median": 657
497
497
  },
498
498
  {
499
499
  "name": "first_edit",
500
500
  "volume": "medium",
501
501
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
502
- "duration_ms_median": 11,
502
+ "duration_ms_median": 14,
503
503
  "payload_chars_median": 499,
504
504
  "payload_tokens_est_median": 125
505
505
  }
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.21.0",
3
- "generated_at": "2026-08-07T12:11:23.699Z",
2
+ "version": "1.23.0",
3
+ "generated_at": "2026-08-09T01:18:58.355Z",
4
4
  "tools": {
5
5
  "count": 67,
6
6
  "published_count": 65,
@@ -472,7 +472,7 @@
472
472
  },
473
473
  "bench": {
474
474
  "schema": "brainclaw.bench.v1",
475
- "generated_at": "2026-08-07T12:11:22.030Z",
475
+ "generated_at": "2026-08-09T01:18:56.213Z",
476
476
  "node_version": "v24.18.0",
477
477
  "platform": "linux-x64",
478
478
  "repeats": 3,
@@ -481,7 +481,7 @@
481
481
  "name": "cold_onboard",
482
482
  "volume": "empty",
483
483
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
484
- "duration_ms_median": 71,
484
+ "duration_ms_median": 77,
485
485
  "payload_chars_median": 1640,
486
486
  "payload_tokens_est_median": 410
487
487
  },
@@ -489,15 +489,15 @@
489
489
  "name": "warm_work",
490
490
  "volume": "medium",
491
491
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
492
- "duration_ms_median": 101,
493
- "payload_chars_median": 2625,
494
- "payload_tokens_est_median": 656
492
+ "duration_ms_median": 131,
493
+ "payload_chars_median": 2626,
494
+ "payload_tokens_est_median": 657
495
495
  },
496
496
  {
497
497
  "name": "first_edit",
498
498
  "volume": "medium",
499
499
  "description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
500
- "duration_ms_median": 11,
500
+ "duration_ms_median": 14,
501
501
  "payload_chars_median": 499,
502
502
  "payload_tokens_est_median": 125
503
503
  }
package/docs/cli.md CHANGED
@@ -1749,46 +1749,9 @@ brainclaw push --remote origin --message "chore: push memory state" --json
1749
1749
 
1750
1750
  ## Federation
1751
1751
 
1752
- The `federation` command group manages cloud signal exchange with `app.brainclaw.dev`. Federation requires `BRAINCLAW_CLOUD_API_KEY` to be set. It is a planned premium tier for cross-machine and cross-organization coordination.
1752
+ The v1 `brainclaw federation` command group and the whole cloud egress path (`app.brainclaw.dev` push/pull, `cloud_sync` config, `BRAINCLAW_CLOUD_*` env vars) were removed in wave 1 of dec#156 / pln#651. There is no migration: the v1 format is abandoned, not deprecated. The v2 federation surface will be introduced by pln#651 wave 3 alongside the pairing CLI (`brainclaw cloud connect`).
1753
1753
 
1754
- ### `brainclaw federation push <message>`
1755
-
1756
- Push a test signal to the cloud. The signal is sent from the current project and agent to a target project or broadcast address.
1757
-
1758
- | Option | Description |
1759
- |---|---|
1760
- | `--type <type>` | Signal type (default: `runtime_note`). Accepted values: `signal`, `handoff`, `candidate`, `runtime_note`, `board_snapshot` |
1761
- | `--to-project <project>` | Target project name (default: `broadcast`) |
1762
- | `--to-agent <agent>` | Target agent name |
1763
-
1764
- ```bash
1765
- brainclaw federation push "Auth rollout complete" --to-project lodestar
1766
- brainclaw federation push "Blocked on payments" --type runtime_note --to-agent copilot
1767
- ```
1768
-
1769
- ### `brainclaw federation pull`
1770
-
1771
- Pull signals from the cloud inbox for the current agent.
1772
-
1773
- | Option | Description |
1774
- |---|---|
1775
- | `--agent <name>` | Agent name to pull for (default: auto-detected) |
1776
- | `--since <date>` | Only pull signals after this ISO date |
1777
- | `--limit <n>` | Maximum number of signals to pull (default: 20) |
1778
-
1779
- ```bash
1780
- brainclaw federation pull
1781
- brainclaw federation pull --since 2026-04-01
1782
- brainclaw federation pull --agent copilot --limit 50
1783
- ```
1784
-
1785
- ### `brainclaw federation status`
1786
-
1787
- Check cloud federation configuration: shows the configured cloud URL, whether the API key is set, and pings the cloud health endpoint if configured.
1788
-
1789
- ```bash
1790
- brainclaw federation status
1791
- ```
1754
+ Local cross-project federation (see below) is unaffected.
1792
1755
 
1793
1756
  ---
1794
1757
 
@@ -2244,4 +2207,72 @@ Show brainclaw context volume stats — tokens injected per agent and per MCP to
2244
2207
  |---|---|
2245
2208
  | `--json` | Output as JSON |
2246
2209
 
2210
+ ### `brainclaw cloud status`
2211
+
2212
+ Show the federation v2 connection state for this workspace: linked cloud project, role,
2213
+ current key epoch, the epochs actually readable on this device, and the three sync states.
2214
+
2215
+ | Option | Description |
2216
+ |---|---|
2217
+ | `--json` | Output as JSON |
2218
+
2219
+ Sync state is **visible by design** (dec#154): a cloud-originated operation materializes in
2220
+ the local journal as `pending`, `synced` or `conflict`, and a conflict is presented for
2221
+ resolution rather than resolved by a silent last-write-wins. The counts are read from the
2222
+ outbox on disk, not from a cached counter — a status that echoed a number the code had
2223
+ itself incremented would reassure precisely when you are checking because you doubt.
2224
+
2225
+ `readable_epochs` likewise comes from the keyring on disk. It can be shorter than the
2226
+ epochs the state declares: a partial backup restore leaves that exact disagreement, and
2227
+ the honest answer is what can actually be decrypted here.
2228
+
2229
+ ### `brainclaw cloud connect <invite-code> --url <url> --agent <id>`
2230
+
2231
+ Join a cloud project. This is a **key ceremony**, not a config write: it claims the invite,
2232
+ proves possession of this agent's Ed25519 identity, and attests the device's X25519
2233
+ encryption key with that same identity.
2234
+
2235
+ | Option | Description |
2236
+ |---|---|
2237
+ | `--url <url>` | Cloud deployment address (required) |
2238
+ | `--agent <id>` | Opaque agent identifier to enroll, 4–64 chars (required) |
2239
+ | `--json` | Output as JSON |
2240
+
2241
+ The human copies **only the invite code**, then compares **two fingerprints** with what the
2242
+ approver sees on their screen. No API key, no PEM, no agent_id, no environment variable
2243
+ (dec#8). Both fingerprints are printed in full, never truncated — a 16-character comparison
2244
+ collides far more easily than it looks.
2245
+
2246
+ The attestation is what stops a **phantom member**. Without it, the Cloud — which
2247
+ orchestrates the pairing — could insert its own key into the envelope list: end-to-end
2248
+ encryption whose key exchange is arbitrated by the very party it claims to neutralize.
2249
+
2250
+ A refused pairing writes **nothing** locally, so re-running is safe and leaves no orphan.
2251
+
2252
+ ### `brainclaw cloud await --url <url>`
2253
+
2254
+ Observe the human approval and activate the local pairing.
2255
+
2256
+ Deliberately a **separate command**: approval depends on a person, whose delay is not
2257
+ bounded. A command that blocked indefinitely on a third party would be a poor citizen in a
2258
+ script, and an interrupted pairing must stay resumable. Polling does **not** mutate local
2259
+ state — reading a status must never change the workspace.
2260
+
2261
+ ### `brainclaw cloud disconnect --url <url>`
2262
+
2263
+ Remove the local authorization and request remote revocation.
2264
+
2265
+ | Option | Description |
2266
+ |---|---|
2267
+ | `--url <url>` | Cloud deployment address (required) |
2268
+ | `--forget-keys` | Also erase this project's epoch keys — its sealed past becomes unreadable here |
2269
+
2270
+ Local state flips to revoked **even if the cloud is unreachable**: otherwise a lost device
2271
+ would stay authorized for want of a network, the opposite of what revocation must
2272
+ guarantee.
2273
+
2274
+ What disconnect does **not** do, stated rather than left implied: it does not erase data
2275
+ already pulled and decrypted locally, nor what other devices already hold. It withdraws an
2276
+ authorization; it does not rewrite the past (RFC §5.2).
2277
+
2247
2278
  This keeps end-user installs aware of published npm releases without requiring a local tarball channel. To keep beta testers on a different channel, set `brainclaw_update_source` to `type: npm` with a different `dist_tag`, such as `prelaunch`.