brainclaw 1.23.0 → 1.25.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.
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-cloud.js +121 -13
- package/dist/cli/register-code-map.js +9 -2
- package/dist/commands/cloud.js +534 -39
- package/dist/commands/code-map.js +119 -2
- package/dist/commands/mcp-catalog.js +46 -0
- package/dist/commands/mcp.js +54 -2
- package/dist/core/code-map/backend.js +158 -1
- package/dist/core/code-map/export.js +212 -0
- package/dist/core/code-map/freshness.js +3 -2
- package/dist/core/code-map/impact.js +377 -0
- package/dist/core/code-map/indexes.js +27 -3
- package/dist/core/code-map/lang/typescript/config.js +271 -0
- package/dist/core/code-map/lang/typescript/index.js +20 -4
- package/dist/core/code-map/query.js +76 -13
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +1 -0
- package/dist/core/code-map/types.js +15 -0
- package/dist/core/federation-emit.js +283 -0
- package/dist/core/federation-grant-transport.js +196 -0
- package/dist/core/federation-grant.js +223 -0
- package/dist/core/federation-keyring.js +39 -0
- package/dist/core/federation-opaque-ids.js +111 -0
- package/dist/core/federation-outbox-v2.js +36 -2
- package/dist/core/federation-pairing.js +87 -12
- package/dist/core/federation-pull.js +523 -0
- package/dist/core/federation-push.js +287 -0
- package/dist/core/federation-rotation.js +124 -0
- package/dist/core/federation-state.js +81 -6
- package/dist/core/protocol-tool-policy.js +3 -0
- package/dist/core/worktree.js +89 -2
- package/dist/facts.js +14 -11
- package/dist/facts.json +13 -10
- package/docs/cli.md +8 -0
- package/docs/code-map.md +24 -1
- package/docs/design/federation-onboarding-usecases.md +254 -0
- package/docs/design/pairing-v3-brief.md +80 -0
- package/docs/integrations/mcp.md +5 -2
- package/docs/mcp-schema-changelog.md +11 -1
- package/package.json +1 -1
package/dist/core/worktree.js
CHANGED
|
@@ -7,6 +7,7 @@ import yaml from 'yaml';
|
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { parsePorcelainZ, isSystemDirtyPath } from './dirty-scope.js';
|
|
10
|
+
import { entityRecordDirs } from './io.js';
|
|
10
11
|
/** Normalizes a path for use in git CLI arguments (forward slashes on Windows). */
|
|
11
12
|
function gitPath(p) {
|
|
12
13
|
return p.replace(/\\/g, '/');
|
|
@@ -1548,6 +1549,68 @@ export function probeLocalBranch(mainWorktreePath, branchName) {
|
|
|
1548
1549
|
export function isGitRepo(cwd) {
|
|
1549
1550
|
return runGit(['rev-parse', '--is-inside-work-tree'], cwd).ok;
|
|
1550
1551
|
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Comparison key for worktree paths: PHYSICAL identity when the path exists
|
|
1554
|
+
* (realpath expands Windows 8.3 short names — `RUNNER~1` and `runneradmin`
|
|
1555
|
+
* are the same directory but different strings, and git always reports the
|
|
1556
|
+
* long form while a claim may carry the short one), else plain resolution.
|
|
1557
|
+
* Forward slashes, case-folded on win32.
|
|
1558
|
+
*/
|
|
1559
|
+
function worktreePathKey(p) {
|
|
1560
|
+
let resolved;
|
|
1561
|
+
try {
|
|
1562
|
+
resolved = fs.realpathSync.native(p);
|
|
1563
|
+
}
|
|
1564
|
+
catch {
|
|
1565
|
+
resolved = path.resolve(p);
|
|
1566
|
+
}
|
|
1567
|
+
resolved = resolved.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1568
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Worktree paths referenced by an ACTIVE, non-expired claim — the set the GC
|
|
1572
|
+
* must never touch.
|
|
1573
|
+
*
|
|
1574
|
+
* Read directly from the claims record dirs (both layouts, pln#649) instead of
|
|
1575
|
+
* claims.ts: claims.ts imports worktree.ts, so the dependency can only point
|
|
1576
|
+
* this way. The parse is deliberately lenient — an unreadable claim simply does
|
|
1577
|
+
* not protect anything; it never blocks the GC of OTHER worktrees.
|
|
1578
|
+
*
|
|
1579
|
+
* Scope note: dispatch claims are project-local, so the project store is the
|
|
1580
|
+
* right authority here; workspace-level claims (cross-project) never carry a
|
|
1581
|
+
* lane worktree_path.
|
|
1582
|
+
*/
|
|
1583
|
+
function activeClaimWorktreePaths(cwd) {
|
|
1584
|
+
const out = new Set();
|
|
1585
|
+
const now = new Date();
|
|
1586
|
+
for (const dir of entityRecordDirs('claims', cwd)) {
|
|
1587
|
+
let files;
|
|
1588
|
+
try {
|
|
1589
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
for (const f of files) {
|
|
1595
|
+
try {
|
|
1596
|
+
const claim = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
|
|
1597
|
+
if (claim.status !== 'active')
|
|
1598
|
+
continue;
|
|
1599
|
+
if (typeof claim.worktree_path !== 'string' || !claim.worktree_path)
|
|
1600
|
+
continue;
|
|
1601
|
+
// Mirror isClaimExpired: a zombie claim past its expiry must not make a
|
|
1602
|
+
// worktree un-GC-able forever.
|
|
1603
|
+
if (claim.expires_at && new Date(claim.expires_at) < now)
|
|
1604
|
+
continue;
|
|
1605
|
+
out.add(worktreePathKey(claim.worktree_path));
|
|
1606
|
+
}
|
|
1607
|
+
catch {
|
|
1608
|
+
/* lenient by design — see above */
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return out;
|
|
1613
|
+
}
|
|
1551
1614
|
/**
|
|
1552
1615
|
* Removes worktrees whose branch has been fully merged into the current branch
|
|
1553
1616
|
* (typically master/main after a merge). Also removes brainclaw-managed
|
|
@@ -1561,6 +1624,16 @@ export function isGitRepo(cwd) {
|
|
|
1561
1624
|
* - content (`git cherry HEAD <branch>`, patch-id): catches squash merges,
|
|
1562
1625
|
* which is GitHub's default merge strategy on this repo and previously left
|
|
1563
1626
|
* every squashed lane un-GC-able forever.
|
|
1627
|
+
*
|
|
1628
|
+
* ACTIVE-CLAIM GATE (incident 2026-08-10): a freshly-dispatched lane worktree
|
|
1629
|
+
* has no commits of its own — its branch IS an ancestor of HEAD, so both merged
|
|
1630
|
+
* probes say "merged" — and before the agent's first write it has no uncommitted
|
|
1631
|
+
* changes either. Both historical gates therefore pass during a lane's startup
|
|
1632
|
+
* window, and the post-merge hook destroyed a live codex lane 7 minutes after
|
|
1633
|
+
* spawn (worktree emptied under the running agent). The coordination store is
|
|
1634
|
+
* the authority on liveness: a worktree referenced by an active claim is
|
|
1635
|
+
* untouchable — merged or not, clean or not, force or not. The escape hatch is
|
|
1636
|
+
* releasing the claim, never bypassing it.
|
|
1564
1637
|
*/
|
|
1565
1638
|
export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
|
|
1566
1639
|
const result = { removed: [], skipped: [], pruned: false };
|
|
@@ -1580,9 +1653,16 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
|
|
|
1580
1653
|
.filter(Boolean)
|
|
1581
1654
|
: []);
|
|
1582
1655
|
const worktrees = listWorktrees(mainWorktreePath);
|
|
1656
|
+
const protectedPaths = activeClaimWorktreePaths(mainWorktreePath);
|
|
1583
1657
|
for (const wt of worktrees) {
|
|
1584
1658
|
if (wt.is_main)
|
|
1585
1659
|
continue;
|
|
1660
|
+
// Active-claim gate — see the function doc. Checked BEFORE the merged
|
|
1661
|
+
// probes and BEFORE `force`: a live dispatched lane is never GC-able.
|
|
1662
|
+
if (protectedPaths.has(worktreePathKey(wt.path))) {
|
|
1663
|
+
result.skipped.push({ path: wt.path, reason: 'active claim' });
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1586
1666
|
// trp#926 — a lane's branch is "merged" if EITHER git says its commits are
|
|
1587
1667
|
// ancestors of HEAD (fast-forward / merge-commit) OR every commit's patch
|
|
1588
1668
|
// is already on HEAD (squash-merge, catching GitHub's default strategy).
|
|
@@ -1621,7 +1701,7 @@ export function cleanMergedWorktrees(mainWorktreePath, options = {}) {
|
|
|
1621
1701
|
}
|
|
1622
1702
|
}
|
|
1623
1703
|
// Clean orphan brainclaw worktree directories (no matching git worktree)
|
|
1624
|
-
cleanOrphanWorktreeDirs(mainWorktreePath, worktrees, result, options.dryRun);
|
|
1704
|
+
cleanOrphanWorktreeDirs(mainWorktreePath, worktrees, result, options.dryRun, protectedPaths);
|
|
1625
1705
|
return result;
|
|
1626
1706
|
}
|
|
1627
1707
|
/** A worker whose heartbeat file was touched within this window looks alive. */
|
|
@@ -1722,7 +1802,7 @@ export function gcWorktreeIfHarvested(mainWorktreePath, worktreePath, options =
|
|
|
1722
1802
|
* Removes brainclaw-managed worktree directories under ~/.brainclaw/worktrees/
|
|
1723
1803
|
* that no longer have a corresponding git worktree entry.
|
|
1724
1804
|
*/
|
|
1725
|
-
function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryRun) {
|
|
1805
|
+
function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryRun, protectedPaths = new Set()) {
|
|
1726
1806
|
const base = worktreesBaseDir(mainWorktreePath);
|
|
1727
1807
|
if (!fs.existsSync(base))
|
|
1728
1808
|
return;
|
|
@@ -1740,6 +1820,13 @@ function cleanOrphanWorktreeDirs(mainWorktreePath, activeWorktrees, result, dryR
|
|
|
1740
1820
|
const dirPath = path.resolve(path.join(base, entry.name));
|
|
1741
1821
|
if (activePaths.has(dirPath))
|
|
1742
1822
|
continue;
|
|
1823
|
+
// Active-claim gate: a dir whose git admin entry vanished can still host a
|
|
1824
|
+
// LIVE agent (the 2026-08-10 incident left exactly this state behind). If a
|
|
1825
|
+
// claim still points here, it is not debris.
|
|
1826
|
+
if (protectedPaths.has(worktreePathKey(dirPath))) {
|
|
1827
|
+
result.skipped.push({ path: dirPath, reason: 'active claim' });
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1743
1830
|
// This directory is not referenced by any git worktree — it's orphaned
|
|
1744
1831
|
if (dryRun) {
|
|
1745
1832
|
result.removed.push(dirPath);
|
package/dist/facts.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
|
|
2
|
-
// Source: brainclaw v1.
|
|
2
|
+
// Source: brainclaw v1.25.0 on 2026-08-10T23:31:26.947Z
|
|
3
3
|
export const FACTS = {
|
|
4
|
-
"version": "1.
|
|
5
|
-
"generated_at": "2026-08-
|
|
4
|
+
"version": "1.25.0",
|
|
5
|
+
"generated_at": "2026-08-10T23:31:26.947Z",
|
|
6
6
|
"tools": {
|
|
7
|
-
"count":
|
|
8
|
-
"published_count":
|
|
7
|
+
"count": 70,
|
|
8
|
+
"published_count": 68,
|
|
9
9
|
"names": [
|
|
10
10
|
"bclaw_bootstrap",
|
|
11
11
|
"bclaw_release_notes",
|
|
@@ -34,6 +34,9 @@ export const FACTS = {
|
|
|
34
34
|
"bclaw_code_status",
|
|
35
35
|
"bclaw_code_find",
|
|
36
36
|
"bclaw_code_brief",
|
|
37
|
+
"bclaw_code_impact",
|
|
38
|
+
"bclaw_code_export",
|
|
39
|
+
"bclaw_code_outline",
|
|
37
40
|
"bclaw_code_refresh",
|
|
38
41
|
"bclaw_dispatch",
|
|
39
42
|
"bclaw_send_message",
|
|
@@ -474,7 +477,7 @@ export const FACTS = {
|
|
|
474
477
|
},
|
|
475
478
|
"bench": {
|
|
476
479
|
"schema": "brainclaw.bench.v1",
|
|
477
|
-
"generated_at": "2026-08-
|
|
480
|
+
"generated_at": "2026-08-10T23:31:25.464Z",
|
|
478
481
|
"node_version": "v24.18.0",
|
|
479
482
|
"platform": "linux-x64",
|
|
480
483
|
"repeats": 3,
|
|
@@ -483,7 +486,7 @@ export const FACTS = {
|
|
|
483
486
|
"name": "cold_onboard",
|
|
484
487
|
"volume": "empty",
|
|
485
488
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
486
|
-
"duration_ms_median":
|
|
489
|
+
"duration_ms_median": 59,
|
|
487
490
|
"payload_chars_median": 1640,
|
|
488
491
|
"payload_tokens_est_median": 410
|
|
489
492
|
},
|
|
@@ -491,15 +494,15 @@ export const FACTS = {
|
|
|
491
494
|
"name": "warm_work",
|
|
492
495
|
"volume": "medium",
|
|
493
496
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
494
|
-
"duration_ms_median":
|
|
495
|
-
"payload_chars_median":
|
|
496
|
-
"payload_tokens_est_median":
|
|
497
|
+
"duration_ms_median": 88,
|
|
498
|
+
"payload_chars_median": 2625,
|
|
499
|
+
"payload_tokens_est_median": 656
|
|
497
500
|
},
|
|
498
501
|
{
|
|
499
502
|
"name": "first_edit",
|
|
500
503
|
"volume": "medium",
|
|
501
504
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
502
|
-
"duration_ms_median":
|
|
505
|
+
"duration_ms_median": 10,
|
|
503
506
|
"payload_chars_median": 499,
|
|
504
507
|
"payload_tokens_est_median": 125
|
|
505
508
|
}
|
package/dist/facts.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"generated_at": "2026-08-
|
|
2
|
+
"version": "1.25.0",
|
|
3
|
+
"generated_at": "2026-08-10T23:31:26.947Z",
|
|
4
4
|
"tools": {
|
|
5
|
-
"count":
|
|
6
|
-
"published_count":
|
|
5
|
+
"count": 70,
|
|
6
|
+
"published_count": 68,
|
|
7
7
|
"names": [
|
|
8
8
|
"bclaw_bootstrap",
|
|
9
9
|
"bclaw_release_notes",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"bclaw_code_status",
|
|
33
33
|
"bclaw_code_find",
|
|
34
34
|
"bclaw_code_brief",
|
|
35
|
+
"bclaw_code_impact",
|
|
36
|
+
"bclaw_code_export",
|
|
37
|
+
"bclaw_code_outline",
|
|
35
38
|
"bclaw_code_refresh",
|
|
36
39
|
"bclaw_dispatch",
|
|
37
40
|
"bclaw_send_message",
|
|
@@ -472,7 +475,7 @@
|
|
|
472
475
|
},
|
|
473
476
|
"bench": {
|
|
474
477
|
"schema": "brainclaw.bench.v1",
|
|
475
|
-
"generated_at": "2026-08-
|
|
478
|
+
"generated_at": "2026-08-10T23:31:25.464Z",
|
|
476
479
|
"node_version": "v24.18.0",
|
|
477
480
|
"platform": "linux-x64",
|
|
478
481
|
"repeats": 3,
|
|
@@ -481,7 +484,7 @@
|
|
|
481
484
|
"name": "cold_onboard",
|
|
482
485
|
"volume": "empty",
|
|
483
486
|
"description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
|
|
484
|
-
"duration_ms_median":
|
|
487
|
+
"duration_ms_median": 59,
|
|
485
488
|
"payload_chars_median": 1640,
|
|
486
489
|
"payload_tokens_est_median": 410
|
|
487
490
|
},
|
|
@@ -489,15 +492,15 @@
|
|
|
489
492
|
"name": "warm_work",
|
|
490
493
|
"volume": "medium",
|
|
491
494
|
"description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
|
|
492
|
-
"duration_ms_median":
|
|
493
|
-
"payload_chars_median":
|
|
494
|
-
"payload_tokens_est_median":
|
|
495
|
+
"duration_ms_median": 88,
|
|
496
|
+
"payload_chars_median": 2625,
|
|
497
|
+
"payload_tokens_est_median": 656
|
|
495
498
|
},
|
|
496
499
|
{
|
|
497
500
|
"name": "first_edit",
|
|
498
501
|
"volume": "medium",
|
|
499
502
|
"description": "code_find + code_brief on the fresh-agent path (missing index, first touch).",
|
|
500
|
-
"duration_ms_median":
|
|
503
|
+
"duration_ms_median": 10,
|
|
501
504
|
"payload_chars_median": 499,
|
|
502
505
|
"payload_tokens_est_median": 125
|
|
503
506
|
}
|
package/docs/cli.md
CHANGED
|
@@ -652,6 +652,14 @@ Search the symbol index by name (function / class / component / hook / type). Re
|
|
|
652
652
|
### `brainclaw code-map brief <target> [--limit <n>]`
|
|
653
653
|
|
|
654
654
|
Given a symbol or path, return a ranked reading list (`suggested_files_to_read`) plus related memory (decisions/traps/constraints) — what to read before editing.
|
|
655
|
+
### `brainclaw code-map export <symbol-or-path> [--direction outgoing|incoming|both]`
|
|
656
|
+
|
|
657
|
+
Export a compact **local** Code Map subgraph around one symbol or file. The default
|
|
658
|
+
is one hop in both directions; hard caps always apply (depth 4, 100 nodes, 200
|
|
659
|
+
edges), so the command never defaults to a whole-graph export. `--max-nodes`,
|
|
660
|
+
`--max-edges`, and `--depth` only tighten the result; `--min-confidence` has a
|
|
661
|
+
hard floor of 0.5. JSON keeps each edge's `kind`, `source`, and `confidence`.
|
|
662
|
+
Use `--format mermaid` for a diagram projected from that same JSON model.
|
|
655
663
|
|
|
656
664
|
```bash
|
|
657
665
|
brainclaw code-map refresh --all
|
package/docs/code-map.md
CHANGED
|
@@ -24,6 +24,7 @@ rebuilds it.
|
|
|
24
24
|
brainclaw memory.
|
|
25
25
|
- **To locate** a function/class/component/hook by name without grepping:
|
|
26
26
|
`code-map find <query>` (or `bclaw_code_find`).
|
|
27
|
+
- **To inspect a bounded local dependency neighborhood**: `code-map export <symbol-or-path>` (or `bclaw_code_export`) returns compact nodes and edges, not a repository graph dump.
|
|
27
28
|
- **To check coverage / staleness**: `code-map status` (or `bclaw_code_status`).
|
|
28
29
|
- **After pulling changes or doing work**: `code-map refresh` to bring the index
|
|
29
30
|
back to `fresh`.
|
|
@@ -95,9 +96,30 @@ Read-only. Builds a reading brief for a symbol or file: a ranked
|
|
|
95
96
|
brainclaw code-map brief App
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
### `brainclaw code-map export <symbol-or-path>`
|
|
100
|
+
|
|
101
|
+
Read-only export of a **local** persisted subgraph around one symbol or file. It
|
|
102
|
+
never refreshes, reparses, calls a service, or silently turns into a whole-project
|
|
103
|
+
graph export. The default is one hop in both directions; limits are always
|
|
104
|
+
reported and hard-capped at depth 4, 100 nodes, and 200 edges.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
brainclaw code-map export useAuth --direction incoming --depth 2 --json
|
|
108
|
+
brainclaw code-map export src/hooks/useAuth.ts --format mermaid
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--direction` is `outgoing`, `incoming`, or `both` (default). `--max-nodes` and
|
|
112
|
+
`--max-edges` can tighten the response only; `--min-confidence` cannot be set
|
|
113
|
+
below 0.5. JSON is canonical and includes compact `nodes`, `edges`, root IDs,
|
|
114
|
+
limits, truncation flags, and a freshness badge. Every edge retains `kind`,
|
|
115
|
+
`source`, and `confidence`; low-confidence nodes/relations are excluded so an
|
|
116
|
+
extraction heuristic cannot appear indistinguishable from a high-confidence
|
|
117
|
+
relation. `--format mermaid` adds a Mermaid rendering projected from those exact
|
|
118
|
+
JSON nodes and edges—never from a second traversal.
|
|
119
|
+
|
|
98
120
|
## MCP tools
|
|
99
121
|
|
|
100
|
-
Capable agents should prefer the MCP surface. The
|
|
122
|
+
Capable agents should prefer the MCP surface. The read tools mirror the CLI and
|
|
101
123
|
all return a `freshness_badge`:
|
|
102
124
|
|
|
103
125
|
| Tool | Kind | Purpose |
|
|
@@ -105,6 +127,7 @@ all return a `freshness_badge`:
|
|
|
105
127
|
| `bclaw_code_status` | read | Store presence, freshness badge, index stats. Never refreshes. |
|
|
106
128
|
| `bclaw_code_find` | read | Ranked symbol-index search (`query`, optional `limit`). Never refreshes. |
|
|
107
129
|
| `bclaw_code_brief` | read | Reading brief for a symbol/path (`target`, optional `limit`, files capped at 12). Never refreshes. |
|
|
130
|
+
| `bclaw_code_export` | read | Bounded local subgraph around required `target`; direction/depth/node/edge caps, confidence filtering, and optional Mermaid projection. Never refreshes. |
|
|
108
131
|
| `bclaw_code_refresh` | write | Rebuild the index. `scope` = `"changed"` (default) or `"all"`. Fails fast on a live lock. |
|
|
109
132
|
|
|
110
133
|
The read tools never trigger a parse — if `bclaw_code_status` /
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Fédération cloud — cartographie des cas d'usage et des parcours
|
|
2
|
+
|
|
3
|
+
> Objectif fixé par l'opérateur (2026-08-09) : identifier **tous** les cas pour qu'un ou
|
|
4
|
+
> plusieurs humains, sur un ou plusieurs projets, avec un ou plusieurs agents, utilisent la
|
|
5
|
+
> fédération **simplement**. Point de départ : l'onboarding côté cloud, avec l'idée d'un
|
|
6
|
+
> appairage par agent via une URL d'activation.
|
|
7
|
+
>
|
|
8
|
+
> Chaque affirmation « état actuel » de ce document a été **mesurée sur le code ou en
|
|
9
|
+
> production cette session** — les références (dec#, trp#) pointent la mesure.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Le vocabulaire d'abord — quatre identités, pas deux
|
|
14
|
+
|
|
15
|
+
Tout le reste du document repose sur cette distinction. La confusion entre ces quatre
|
|
16
|
+
notions est la cause directe des deux pièges déjà rencontrés (trp#1610, trp#1625).
|
|
17
|
+
|
|
18
|
+
| Identité | Ce qu'elle est | Sa preuve | Où elle vit |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **Compte humain** | La personne — s'inscrit, approuve, administre | session web (email + mot de passe) | cloud (`users`) |
|
|
21
|
+
| **Appareil** | La machine — détient les clés de **déchiffrement** | clé X25519, empreinte comparée à l'appairage | local (`~/.brainclaw/keys/`) + cloud (`enrollments`) |
|
|
22
|
+
| **Agent** | Le logiciel (claude-code, codex…) — **signe** ce qu'il émet | clé Ed25519, attestée à l'appairage | local (registre d'agents) + cloud (`agents`) |
|
|
23
|
+
| **Projet** | Le magasin `.brainclaw/` et sa projection aveugle | — | local (source de vérité) + cloud (projection, dec#154) |
|
|
24
|
+
|
|
25
|
+
Relations cibles (dec#158/159) : un humain **possède** des appareils ; un appareil
|
|
26
|
+
**héberge** des agents ; un enrôlement lie *(agent, appareil, humain propriétaire, projet)*.
|
|
27
|
+
|
|
28
|
+
**Écart mesuré aujourd'hui** : l'enrôlement lie agent + appareil mais **pas l'humain**
|
|
29
|
+
(aucun `owner_user_id`), et l'état local ne connaît qu'**un** appairage par workspace, sans
|
|
30
|
+
mémoriser quel agent (trp#1625).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. La matrice des situations
|
|
35
|
+
|
|
36
|
+
Quatre axes : humains (1/N) × machines (1/N) × agents (1/N) × projets (1/N).
|
|
37
|
+
Les combinaisons se ramènent à six situations réellement distinctes :
|
|
38
|
+
|
|
39
|
+
| # | Situation | État aujourd'hui | Ce qui bloque |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| S1 | 1 humain, 1 machine, 1 agent, 1 projet | ✅ **fonctionne, vérifié en prod** | — |
|
|
42
|
+
| S2 | + agents supplémentaires sur la même machine | ❌ | `connection.json` singleton : le 2ᵉ `connect` **écrase** le 1ᵉʳ (trp#1625) |
|
|
43
|
+
| S3 | + machines supplémentaires (même humain) | ❌ | pas de **remise de clé d'epoch** : la 2ᵉ machine ne peut ni lire ni sceller (dec#159) |
|
|
44
|
+
| S4 | + humains supplémentaires (équipe) | ❌ | pas d'invitation par email (`404` si compte inexistant, zéro envoi d'email), pas de propriétaire d'appareil |
|
|
45
|
+
| S5 | plusieurs projets | 🟡 | 1 workspace = 1 projet : correct par construction ; clés API scopées par projet (vérifié : `403` croisé) ; mais l'URL du cloud n'est pas persistée, à repasser à chaque commande |
|
|
46
|
+
| S6 | agents sans humain au terminal (CI, headless) | ❌ non conçu | la cérémonie exige un humain qui compare des empreintes — cas à traiter explicitement, pas par contournement |
|
|
47
|
+
|
|
48
|
+
La règle de conception qui découle de dec#158 : **le solo est le cas dégénéré du modèle
|
|
49
|
+
d'équipe**, jamais une branche parallèle. S1 doit rester exactement « S4 où le même humain
|
|
50
|
+
joue tous les rôles et où les approbations s'effondrent en un geste ».
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. Les parcours, un par un
|
|
55
|
+
|
|
56
|
+
Convention : 🖥 = côté cloud (navigateur), ⌨ = côté machine (terminal), 👤 = geste humain
|
|
57
|
+
explicite. ✅/🟡/❌ = état mesuré de chaque étape.
|
|
58
|
+
|
|
59
|
+
### W1 — Solo : premier appairage (S1) — *fonctionne aujourd'hui*
|
|
60
|
+
|
|
61
|
+
Le cas « j'utilisais déjà brainclaw en local » : le magasin existe, le cloud est vide.
|
|
62
|
+
|
|
63
|
+
| # | Étape | Où | État |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| 1 | Créer un compte, créer/choisir le projet cloud | 🖥 | ✅ |
|
|
66
|
+
| 2 | « Connect an agent » → créer une invitation (rôle + libellé) → **code affiché une fois** (TTL 15 min, usage unique, seul le SHA-256 est stocké) | 🖥👤 | ✅ |
|
|
67
|
+
| 3 | `brainclaw cloud connect <code> --url <url> --agent <id>` **depuis le bon workspace** — le workspace appairé est affiché avec les empreintes | ⌨ | ✅ (garde trp#1610 livrée) |
|
|
68
|
+
| 4 | Comparer les **deux empreintes** terminal ↔ écran, approuver | 🖥👤 | ✅ |
|
|
69
|
+
| 5 | `brainclaw cloud await --url <url>` → appairage local actif, **genèse de la clé d'epoch 1** (premier appareil seulement) | ⌨ | ✅ (livré ce jour) |
|
|
70
|
+
| 6 | `brainclaw cloud push --url <url>` → plans + mémoire projet scellés, envoyés, stockés | ⌨ | ✅ (vérifié en prod : enveloppe en base, zéro fuite) |
|
|
71
|
+
|
|
72
|
+
**Frictions restantes de W1** (aucune bloquante) : l'URL doit être répétée à chaque commande
|
|
73
|
+
(l'état ne la persiste pas — mesuré) ; `connect` puis `await` sont deux commandes là où une
|
|
74
|
+
seule suffirait (`connect` pourrait attendre l'approbation en sondant) ; l'agent doit être
|
|
75
|
+
un identifiant opaque `[a-zA-Z0-9_-]{4,64}` et l'erreur ne le dit qu'après coup.
|
|
76
|
+
|
|
77
|
+
### W2 — Deuxième agent, même machine (S2) — *à construire*
|
|
78
|
+
|
|
79
|
+
Cible : les agents d'une même machine **partagent la clé X25519 de l'appareil** et signent
|
|
80
|
+
chacun avec leur Ed25519. C'est cohérent avec l'attestation existante (elle lie déjà un
|
|
81
|
+
Ed25519 à un X25519) : chaque agent atteste **la même** clé d'appareil.
|
|
82
|
+
|
|
83
|
+
| # | Étape | Où | État |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| 1 | Créer une invitation **par agent** (ou une invitation multi-usages ? → non : usage unique conservé, un code par agent) | 🖥👤 | ✅ (mécanique identique à W1) |
|
|
86
|
+
| 2 | `cloud connect <code> --agent <id2>` → détecte l'appairage existant, **réutilise la clé d'appareil**, ajoute un enrôlement à la liste | ⌨ | ❌ `connection.json` doit devenir une **liste d'appairages** `{agent, enrollment_id, role}` autour d'un `device` unique |
|
|
87
|
+
| 3 | Approbation par empreintes — l'empreinte de chiffrement **répète** celle de l'appareil, l'empreinte d'identité change par agent | 🖥👤 | 🟡 l'écran d'approbation existe ; afficher « appareil déjà connu » serait le bon signal |
|
|
88
|
+
| 4 | Chaque agent pousse sous sa propre signature ; l'origine (`origin_agent_id`) les distingue | ⌨ | ✅ le transport le fait déjà |
|
|
89
|
+
|
|
90
|
+
**Prérequis structurel** : migration de `connection.json` (forme v2 → v3) avec lecture
|
|
91
|
+
tolérante des deux formats — même discipline que la purge cloud (rien d'irréversible sans
|
|
92
|
+
chemin de retour).
|
|
93
|
+
|
|
94
|
+
### W3 — Deuxième machine, même humain (S3) — *bloqué sur la remise d'epoch*
|
|
95
|
+
|
|
96
|
+
| # | Étape | Où | État |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| 1 | Invitation + cérémonie sur la machine B (identique à W1 étapes 2–4) | 🖥⌨👤 | ✅ |
|
|
99
|
+
| 2 | La machine B est `active` **mais ne détient aucune clé d'epoch** : elle ne peut ni lire ni sceller | — | ⚠️ c'est l'état actuel : actif et inopérant, sans message |
|
|
100
|
+
| 3 | Une machine détentrice (A) **remet** les epochs autorisés : paquet HPKE scellé vers la X25519 attestée de B, manifeste signé (`epoch_grant`, dec#159) | ⌨ A | ❌ à construire — **le cœur du chantier** |
|
|
101
|
+
| 4 | B vérifie le manifeste, range les clés (`storeEpochPrivateKey` refuse déjà d'écraser une clé différente), relit le passé autorisé | ⌨ B | 🟡 primitives présentes, protocole absent |
|
|
102
|
+
|
|
103
|
+
**Point de vigilance déjà mesuré** : le premier appairage marque `recovery: true` et le
|
|
104
|
+
quorum de récupération (2 appareils) est **rapporté mais jamais appliqué** — le solo n'est
|
|
105
|
+
pas bloqué, mais la perte de l'unique machine = perte du passé, et rien ne l'affiche.
|
|
106
|
+
|
|
107
|
+
### W4 — Embarquer un deuxième développeur (S4) — *le parcours demandé, à construire*
|
|
108
|
+
|
|
109
|
+
Le principe directeur (convergence des deux critiques de l'idéation) : **deux approbations
|
|
110
|
+
de nature différente**. L'admin admet **l'humain** ; l'humain approuve **ses appareils**.
|
|
111
|
+
Un admin ne peut pas comparer les empreintes du terminal d'un tiers — le faire approuver à
|
|
112
|
+
sa place réduirait la cérémonie à un clic de confiance (dec#8).
|
|
113
|
+
|
|
114
|
+
| # | Étape | Où | État |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| 1 | Admin : « Inviter un membre » → email + rôle | 🖥👤 | ❌ aujourd'hui `404` si le compte n'existe pas |
|
|
117
|
+
| 2 | Le cloud envoie un email avec lien d'acceptation ; si le compte n'existe pas, le lien passe par l'inscription | 🖥 | ❌ **zéro envoi d'email dans le backend** (une skill `cloudflare-email-service` est disponible pour le construire) |
|
|
118
|
+
| 3 | Le membre accepte → `project_members` actif avec son rôle | 🖥👤 | 🟡 la table et les rôles existent, le flux non |
|
|
119
|
+
| 4 | Le membre crée **ses** invitations d'agent (portée : ses propres appareils) et fait W1/W2 sur ses machines | 🖥⌨👤 | ❌ nécessite `owner_user_id` sur `enrollments` + revendication de propriétaire **dans le payload d'attestation signé** (sinon la liaison humain↔appareil ne vaut que la parole du cloud) |
|
|
120
|
+
| 5 | L'**admin voit** les appareils du membre (métadonnées, pas les clés) ; le **membre** les approuve | 🖥 | ❌ l'écran d'approbation ne filtre pas par propriétaire |
|
|
121
|
+
| 6 | Un custodian remet les epochs selon l'**horizon** choisi (tout / à partir de maintenant / borné) | ⌨ | ❌ même chantier que W3-3 + **arbitrage produit** (voir §7) |
|
|
122
|
+
|
|
123
|
+
### W5 — Révoquer (agent, appareil, ou humain)
|
|
124
|
+
|
|
125
|
+
| # | Étape | Où | État |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| 1 | Révoquer l'enrôlement (bouton « Revoke ») | 🖥👤 | ✅ existe |
|
|
128
|
+
| 2 | Supprimer / renommer l'agent dans le registre cloud | 🖥 | ❌ **aucun `DELETE /agents/:id`**, le `PATCH` ne change que le statut — c'est le manque constaté par l'opérateur |
|
|
129
|
+
| 3 | Rotation d'epoch N+1, remise aux lecteurs restants | ⌨ | ❌ dépend de W3-3 |
|
|
130
|
+
| 4 | Affichage honnête : « ne lit plus le **futur** ; conserve ce qu'il avait déjà déchiffré » | 🖥 | ✅ le texte existe sur la page connect |
|
|
131
|
+
|
|
132
|
+
### W6 — Perte de machine / récupération
|
|
133
|
+
|
|
134
|
+
RFC §5.3 : un porteur restant approuve la nouvelle clé, enveloppe les epochs historiques
|
|
135
|
+
autorisés, révoque l'ancienne. Même mécanique que W3-3 avec un autorisateur différent —
|
|
136
|
+
**toute solution qui inventerait un second canal de transfert de clés créerait un second
|
|
137
|
+
endroit où une clé peut fuir** (conséquence 3 de dec#158).
|
|
138
|
+
État : ❌ (bloqué sur la remise d'epoch) + ⚠️ quorum non appliqué (mesuré).
|
|
139
|
+
|
|
140
|
+
### W7 — Quotidien : synchroniser
|
|
141
|
+
|
|
142
|
+
| # | Étape | État |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| 1 | `cloud push` — scelle, met en file, envoie ; refus des trois filets listés un par un | ✅ vérifié en prod |
|
|
145
|
+
| 2 | **Pull** — tirer les enveloppes des autres, vérifier (`verifyInbound` : roster, signature, AAD, anti-rejeu), matérialiser localement | ❌ **`verifyInbound` n'a aucun appelant de production** — symétrique exact de l'émission avant ce matin ; le « Materialized N signals » des sessions vient du chemin **v1** |
|
|
146
|
+
| 3 | Relais dashboard : changer statut/priorité depuis le web, appliqué localement (`applyCloudCommand`) | 🟡 primitives présentes des deux côtés, câblage du poll absent |
|
|
147
|
+
| 4 | Automatisation : push en fin de session, pull en début (hooks existants) | ❌ manuel aujourd'hui |
|
|
148
|
+
| 5 | Conflits : `409` → recalage signé une fois, sinon visible en `conflict` | ✅ |
|
|
149
|
+
|
|
150
|
+
### W8 — Multi-projets (S5)
|
|
151
|
+
|
|
152
|
+
Fonctionne par construction (1 workspace = 1 projet, table d'ids opaques cloisonnée par
|
|
153
|
+
projet cloud — testé). Reste : persister l'URL du cloud par appairage, et le sélecteur de
|
|
154
|
+
projet web existe déjà.
|
|
155
|
+
|
|
156
|
+
### W9 — Agent headless / CI (S6) — *à concevoir, pas à contourner*
|
|
157
|
+
|
|
158
|
+
La cérémonie exige un humain au terminal. Pour un runner CI éphémère, trois options à
|
|
159
|
+
trancher **plus tard** (aucune n'est urgente) : appairage du runner par son propriétaire au
|
|
160
|
+
provisioning ; « appareil de service » à rôle réduit (écriture seule, jamais custodian) ;
|
|
161
|
+
ou exclusion assumée (le CI passe par un membre humain). À documenter comme limite tant que
|
|
162
|
+
non conçu.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 4. L'URL d'activation — ce qu'elle change, ce qu'elle ne doit jamais porter
|
|
167
|
+
|
|
168
|
+
L'idée de l'opérateur est bonne et **peu coûteuse** : l'URL est un véhicule pour le code
|
|
169
|
+
d'invitation existant, pas un nouveau mécanisme.
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
https://app.brainclaw.dev/a/<code> ← partageable à l'humain OU collée à l'agent
|
|
173
|
+
brainclaw cloud connect <url|code> --agent x ← la CLI accepte les deux formes
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Ce que ça améliore : une seule chose à copier (aujourd'hui : code + URL + savoir où les
|
|
177
|
+
mettre) ; un agent à qui on colle l'URL peut en extraire le code ET l'adresse du
|
|
178
|
+
déploiement — ce qui règle au passage la non-persistance de l'URL.
|
|
179
|
+
|
|
180
|
+
**Les invariants qui rendent l'URL sûre** (tous déjà en place pour le code) :
|
|
181
|
+
usage unique, TTL 15 min, seul le SHA-256 stocké, et surtout — **l'URL ne donne aucun droit
|
|
182
|
+
de lecture**. Elle n'ouvre que le droit de *candidater* ; la sécurité reste dans la
|
|
183
|
+
comparaison d'empreintes à l'approbation, et les clés d'epoch ne voyagent **jamais** dans
|
|
184
|
+
une URL.
|
|
185
|
+
|
|
186
|
+
**Le piège à refuser explicitement** : mettre dans l'URL de quoi éviter l'approbation
|
|
187
|
+
(« lien magique » qui active). Ce serait le modèle moltbook, voir ci-dessous.
|
|
188
|
+
|
|
189
|
+
## 5. Pourquoi pas l'auto-enregistrement (le modèle moltbook), et ce qu'on en garde
|
|
190
|
+
|
|
191
|
+
Le modèle « l'agent s'enregistre tout seul dans l'application » échoue sur trois points,
|
|
192
|
+
tous structurels :
|
|
193
|
+
|
|
194
|
+
1. **Aucune liaison de propriété** — n'importe quel processus connaissant l'URL devient un
|
|
195
|
+
agent du projet ; l'identité se squatte.
|
|
196
|
+
2. **Le cloud peut fabriquer des agents** — sans approbation humaine par empreintes, un
|
|
197
|
+
cloud hostile insère son propre « membre fantôme » dans le projet, et le chiffrement de
|
|
198
|
+
bout en bout devient décoratif (c'est exactement l'attaque que l'attestation
|
|
199
|
+
X25519-par-Ed25519 ferme, migrations 0022/0025).
|
|
200
|
+
3. **Rien ne lie l'agent à une clé** — un agent enregistré sans attestation ne peut pas
|
|
201
|
+
recevoir d'epoch de façon vérifiable.
|
|
202
|
+
|
|
203
|
+
**Ce qu'on en garde** : la simplicité du geste — *un seul artefact à transmettre*. C'est
|
|
204
|
+
précisément l'URL d'activation (§4) : la simplicité de moltbook, la cérémonie en dessous.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 6. Besoins transverses (indépendants des parcours)
|
|
209
|
+
|
|
210
|
+
| Besoin | Pourquoi | État |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| Envoi d'email (invitations, notifications d'approbation) | W4 | ❌ — skill `cloudflare-email-service` disponible |
|
|
213
|
+
| CRUD agents cloud (`DELETE`, renommage, purge des agents v1 périmés) | W5, demande opérateur | ❌ |
|
|
214
|
+
| État local multi-appairage (`connection.json` v3 : un `device`, une liste d'agents) | W2, trp#1625 | ❌ |
|
|
215
|
+
| Persistance de l'URL du cloud dans l'état d'appairage | toutes les commandes | ❌ (mesuré) |
|
|
216
|
+
| `connect` qui enchaîne l'attente d'approbation (supprime `await` du chemin nominal) | W1 friction | ❌ |
|
|
217
|
+
| Pull v2 (`verifyInbound` câblé + matérialisation + curseur de feed) | W7 — sans lui, la fédération est **unidirectionnelle** | ❌ |
|
|
218
|
+
| Application du quorum de récupération (aujourd'hui rapporté, jamais bloquant) | W6 | ❌ |
|
|
219
|
+
| Écran « appareils par humain » (l'admin voit, le propriétaire approuve) | W4 | ❌ |
|
|
220
|
+
|
|
221
|
+
## 7. Les arbitrages qui vous appartiennent (aucun n'est technique)
|
|
222
|
+
|
|
223
|
+
Repris de dec#159 — à trancher **avant** W3/W4, parce qu'irréversibles par nature :
|
|
224
|
+
|
|
225
|
+
1. **Horizon par défaut d'un nouveau membre** : rien / depuis l'adhésion / tout.
|
|
226
|
+
Recommandation technique : minimal (élargir reste toujours possible, reprendre jamais).
|
|
227
|
+
2. **Qui est custodian** des remises de clés : le premier appareil ? tout membre `admin` ?
|
|
228
|
+
un quorum ? — et l'exigence d'indépendance des appareils de récupération.
|
|
229
|
+
3. **Disponibilité** : accepter qu'un nouveau membre attende qu'un custodian soit en ligne,
|
|
230
|
+
ou financer une récupération explicitement détentrice de clés (coffre).
|
|
231
|
+
4. **Équivocation du cloud** : aucun témoin / gossip d'équipe / journal de transparence.
|
|
232
|
+
Le cloud hostile qui montre des rosters différents à deux humains reste indétectable
|
|
233
|
+
sans canal hors bande — c'est une limite à afficher, pas à taire.
|
|
234
|
+
|
|
235
|
+
## 8. Ordre de construction proposé
|
|
236
|
+
|
|
237
|
+
Chaque tranche est livrable et vérifiable seule ; les deux premières ne demandent aucun
|
|
238
|
+
arbitrage :
|
|
239
|
+
|
|
240
|
+
1. **Fondations sans arbitrage** — état multi-appairage (W2), URL persistée + URL
|
|
241
|
+
d'activation (§4), `connect` qui attend, CRUD agents (W5-2). *Débloque S2 et la demande
|
|
242
|
+
opérateur immédiate.*
|
|
243
|
+
2. **Pull v2** (W7-2) — la fédération devient bidirectionnelle ; sans lui, un deuxième
|
|
244
|
+
appareil n'aurait de toute façon rien à lire.
|
|
245
|
+
3. **Invitation d'humains** (W4-1..3, email inclus) — après l'arbitrage §7-1 au minimum.
|
|
246
|
+
4. **Remise d'epoch** (W3/W4-6/W6) — le gros œuvre, après les arbitrages §7-1/2/3.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
*Références : dec#154 (cloud = projection), dec#155 (relais sans contexte), dec#156 (v2
|
|
251
|
+
cassante), dec#158 (appairage deux niveaux), dec#159 (synthèse idéation, epoch grants),
|
|
252
|
+
dec#160 (six divergences de contrat, résolues), trp#1610 (connect appaire le cwd),
|
|
253
|
+
trp#1625 (connection.json singleton), critiques `CRITIQUE-codex.md` /
|
|
254
|
+
`CRITIQUE-claude-code.md` (worktrees de l'idéation du 2026-08-09).*
|