brainclaw 1.20.4 → 1.22.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 +63 -0
- package/dist/cli.js +2 -3
- package/dist/commands/cloud.js +198 -0
- package/dist/commands/export.js +3 -3
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp-write-claims.js +162 -46
- package/dist/commands/mcp-write-entities.js +67 -0
- package/dist/commands/mcp.js +64 -1
- package/dist/commands/session-end.js +0 -102
- package/dist/commands/session-start.js +0 -23
- package/dist/commands/switch.js +41 -12
- package/dist/core/actions.js +25 -1
- package/dist/core/agent-files.js +19 -0
- package/dist/core/agentruns.js +68 -10
- package/dist/core/assignments.js +94 -19
- package/dist/core/claims.js +13 -24
- package/dist/core/config.js +58 -0
- package/dist/core/context-diff.js +28 -11
- package/dist/core/coordination.js +1 -3
- package/dist/core/entity-locator.js +404 -0
- package/dist/core/federation-attestation.js +96 -0
- package/dist/core/federation-canonical.js +95 -0
- package/dist/core/federation-hpke.js +213 -0
- package/dist/core/federation-inbound.js +187 -0
- package/dist/core/federation-keyring.js +241 -0
- package/dist/core/federation-message.js +5 -5
- package/dist/core/federation-outbox-v2.js +125 -0
- package/dist/core/federation-pairing.js +213 -0
- package/dist/core/federation-projection.js +336 -0
- package/dist/core/federation-relay.js +223 -0
- package/dist/core/federation-state.js +270 -0
- package/dist/core/identity.js +9 -1
- package/dist/core/ids.js +5 -0
- package/dist/core/io.js +39 -1
- package/dist/core/operations/relocate.js +40 -10
- package/dist/core/schema.js +24 -17
- package/dist/core/sequence.js +47 -6
- package/dist/core/store-resolution.js +99 -26
- package/dist/core/workspace-projects.js +23 -2
- package/dist/core/worktree.js +59 -1
- package/dist/facts.js +7 -7
- package/dist/facts.json +6 -6
- package/docs/cli.md +73 -40
- package/docs/concepts/federation-v2-rfc.md +275 -0
- package/docs/index.md +1 -0
- package/package.json +2 -2
- package/dist/cli/register-federation.js +0 -258
- package/dist/core/federation-cloud.js +0 -245
- package/dist/core/federation-outbox.js +0 -292
- package/dist/core/federation-signing.js +0 -115
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { runCloudStatus, runCloudConnect, runCloudAwait, runCloudDisconnect, } from '../commands/cloud.js';
|
|
2
|
+
/** Adresse du cloud. Fournie par `--url`, sans quoi la commande demande de la préciser. */
|
|
3
|
+
const DEFAULT_URL_HINT = 'https://<votre-déploiement>.workers.dev';
|
|
4
|
+
function fail(message) {
|
|
5
|
+
console.error(message);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fédération v2 (pln#651 étapes 3 et 4).
|
|
10
|
+
*
|
|
11
|
+
* `connect` est une CÉRÉMONIE DE CLÉS, pas une écriture de configuration — voir
|
|
12
|
+
* src/core/federation-pairing.ts. L'humain ne manipule qu'un code d'invitation et compare
|
|
13
|
+
* deux empreintes ; aucune clé d'API, aucun PEM, aucune variable d'environnement (dec#8).
|
|
14
|
+
*/
|
|
15
|
+
export function registerCloudCommands(program) {
|
|
16
|
+
const cloud = program
|
|
17
|
+
.command('cloud')
|
|
18
|
+
.description('Fédération cloud v2 : appairage attesté, état de connexion, epochs de clés');
|
|
19
|
+
cloud
|
|
20
|
+
.command('status')
|
|
21
|
+
.description("Affiche le projet lié, le rôle, l'epoch de clé courant et les trois états de sync")
|
|
22
|
+
.option('--json', 'Sortie JSON')
|
|
23
|
+
.action((options) => {
|
|
24
|
+
runCloudStatus({ json: options.json });
|
|
25
|
+
});
|
|
26
|
+
cloud
|
|
27
|
+
.command('connect <invite-code>')
|
|
28
|
+
.description("Rejoint un projet cloud : réclame l'invitation, prouve la possession de l'identité et atteste la clé de chiffrement de cet appareil")
|
|
29
|
+
.requiredOption('--url <url>', `Adresse du déploiement cloud (ex. ${DEFAULT_URL_HINT})`)
|
|
30
|
+
.requiredOption('--agent <id>', "Identifiant d'agent à enrôler (opaque, 4 à 64 caractères)")
|
|
31
|
+
.option('--json', 'Sortie JSON')
|
|
32
|
+
.action(async (inviteCode, options) => {
|
|
33
|
+
await runCloudConnect({
|
|
34
|
+
inviteCode,
|
|
35
|
+
url: options.url,
|
|
36
|
+
agentId: options.agent,
|
|
37
|
+
json: options.json,
|
|
38
|
+
}).catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
|
|
39
|
+
});
|
|
40
|
+
cloud
|
|
41
|
+
.command('await')
|
|
42
|
+
.description("Constate l'approbation humaine et active l'appairage local")
|
|
43
|
+
// Commande DISTINCTE de connect : l'approbation dépend d'un humain dont le délai
|
|
44
|
+
// n'est pas borné. Bloquer indéfiniment sur un tiers ferait un mauvais citoyen dans
|
|
45
|
+
// un script, et une interruption doit rester reprenable.
|
|
46
|
+
.requiredOption('--url <url>', 'Adresse du déploiement cloud')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
await runCloudAwait({ url: options.url })
|
|
49
|
+
.catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
|
|
50
|
+
});
|
|
51
|
+
cloud
|
|
52
|
+
.command('disconnect')
|
|
53
|
+
.description("Retire l'autorisation locale et demande la révocation distante")
|
|
54
|
+
.requiredOption('--url <url>', 'Adresse du déploiement cloud')
|
|
55
|
+
// Effacer le trousseau rend DÉFINITIVEMENT illisible tout ce qui a été scellé sous
|
|
56
|
+
// ces epochs. Ce n'est donc pas le défaut : on le demande explicitement.
|
|
57
|
+
.option('--forget-keys', "Efface aussi les clés d'epoch de ce projet (le passé scellé devient illisible ici)")
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
await runCloudDisconnect({ url: options.url, forgetKeys: options.forgetKeys })
|
|
60
|
+
.catch((err) => fail(`Erreur : ${err instanceof Error ? err.message : String(err)}`));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=register-cloud.js.map
|
package/dist/cli.js
CHANGED
|
@@ -13,8 +13,8 @@ import { registerPlanningCommands } from './cli/register-planning.js';
|
|
|
13
13
|
import { registerCoordinationCommands } from './cli/register-coordination.js';
|
|
14
14
|
import { registerReviewCommands } from './cli/register-review.js';
|
|
15
15
|
import { registerMemoryContextCommands } from './cli/register-memory-context.js';
|
|
16
|
-
import { registerFederationCommands } from './cli/register-federation.js';
|
|
17
16
|
import { registerCodeMapCommands } from './cli/register-code-map.js';
|
|
17
|
+
import { registerCloudCommands } from './cli/register-cloud.js';
|
|
18
18
|
const program = new Command();
|
|
19
19
|
function parseLeadingGlobalOptions(argv) {
|
|
20
20
|
const result = {};
|
|
@@ -171,8 +171,8 @@ registerPlanningCommands(program);
|
|
|
171
171
|
registerCoordinationCommands(program);
|
|
172
172
|
registerReviewCommands(program);
|
|
173
173
|
registerMemoryContextCommands(program);
|
|
174
|
-
registerFederationCommands(program);
|
|
175
174
|
registerCodeMapCommands(program);
|
|
175
|
+
registerCloudCommands(program);
|
|
176
176
|
// ─── Command-order shim (pln#622 PR5) ────────────────────────────────────────
|
|
177
177
|
// Commander renders `--help` in registration order. The per-family register
|
|
178
178
|
// modules above would otherwise reorder the help output relative to the
|
|
@@ -290,7 +290,6 @@ const ORIGINAL_COMMAND_ORDER = [
|
|
|
290
290
|
'switch',
|
|
291
291
|
'who',
|
|
292
292
|
'worktree',
|
|
293
|
-
'federation',
|
|
294
293
|
// Gated by BRAINCLAW_ENABLE_CODEV: listed here so the order is right in both
|
|
295
294
|
// modes — when not registered they are simply absent from the live array and
|
|
296
295
|
// the sort skips them naturally.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brainclaw cloud connect / status / disconnect` — fédération v2 (pln#651 étapes 3 et 4).
|
|
3
|
+
*
|
|
4
|
+
* `connect` n'est pas une écriture de configuration : c'est une CÉRÉMONIE DE CLÉS. Elle
|
|
5
|
+
* réclame une invitation, prouve la possession de l'identité Ed25519, atteste la clé de
|
|
6
|
+
* chiffrement X25519 de l'appareil, puis attend une approbation humaine qui compare des
|
|
7
|
+
* empreintes. Ce qui la rend indissociable de la distribution des clés est écrit dans
|
|
8
|
+
* src/core/federation-pairing.ts.
|
|
9
|
+
*
|
|
10
|
+
* L'HUMAIN NE COPIE QU'UN CODE D'INVITATION, et compare deux empreintes. Aucune clé
|
|
11
|
+
* d'API, aucun PEM, aucun agent_id, aucune variable d'environnement (dec#8).
|
|
12
|
+
*
|
|
13
|
+
* dec#154 exige que l'état de sync soit VISIBLE : un état pending/synced/conflict que seul
|
|
14
|
+
* le code consulte transformerait le relais cloud en autorité silencieuse. C'est ce que
|
|
15
|
+
* `status` rend.
|
|
16
|
+
*/
|
|
17
|
+
import { summarizeConnection, loadConnectionState, saveConnectionState } from '../core/federation-state.js';
|
|
18
|
+
import { forgetProjectEpochs } from '../core/federation-keyring.js';
|
|
19
|
+
import { beginPairing, checkPairingApproval, completePairing, requestRevocation, PairingError, } from '../core/federation-pairing.js';
|
|
20
|
+
import { resolveEffectiveCwd } from '../core/store-resolution.js';
|
|
21
|
+
import { nowISO } from '../core/ids.js';
|
|
22
|
+
export function runCloudStatus(options = {}) {
|
|
23
|
+
const cwd = options.cwd ?? resolveEffectiveCwd();
|
|
24
|
+
const summary = summarizeConnection(cwd);
|
|
25
|
+
if (options.json) {
|
|
26
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (summary.stage === 'unpaired') {
|
|
30
|
+
console.log('Cloud : non appairé.');
|
|
31
|
+
console.log(" Aucun sync n'a lieu tant qu'aucun appairage n'a été fait explicitement.");
|
|
32
|
+
// Énoncé volontairement, parce que c'est le défaut que la v2 ferme : en v1 la seule
|
|
33
|
+
// présence de BRAINCLAW_CLOUD_API_KEY valait consentement, et jusqu'à 100 signaux
|
|
34
|
+
// étaient tirés puis écrits dans le store local à chaque démarrage de session.
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(`Cloud : ${summary.stage}${summary.connected ? '' : ' (pas encore actif)'}`);
|
|
38
|
+
console.log(` Projet : ${summary.cloud_project_id ?? '—'}`);
|
|
39
|
+
console.log(` Rôle : ${summary.role ?? '—'}`);
|
|
40
|
+
console.log(` Appareil : ${summary.device_fingerprint?.slice(0, 16) ?? '—'}…`);
|
|
41
|
+
console.log(` Epoch : ${summary.current_epoch} (lisibles : ${summary.readable_epochs.join(', ') || 'aucun'})`);
|
|
42
|
+
console.log(` Sync : ${summary.sync.pending} en attente · ${summary.sync.synced} synchronisé(s) · ${summary.sync.conflict} conflit(s)`);
|
|
43
|
+
if (summary.last_pull_at)
|
|
44
|
+
console.log(` Dernier pull : ${summary.last_pull_at}`);
|
|
45
|
+
if (!summary.recovery.ready) {
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(` ⚠ Récupération : ${summary.recovery.reason}`);
|
|
48
|
+
}
|
|
49
|
+
if (summary.sync.conflict > 0) {
|
|
50
|
+
console.log('');
|
|
51
|
+
// Un conflit ne se résout JAMAIS par last-write-wins silencieux (dec#154) : il est
|
|
52
|
+
// présenté, avec une proposition, et attend une décision.
|
|
53
|
+
console.log(` ${summary.sync.conflict} opération(s) en conflit attendent une résolution explicite.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Transport HTTP ────────────────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Transport réel. Injecté partout ailleurs pour que la cérémonie soit exerçable sans
|
|
59
|
+
* réseau — les tests font tourner un cloud simulé qui VÉRIFIE réellement les signatures,
|
|
60
|
+
* ce qu'un serveur acceptant tout ne prouverait pas.
|
|
61
|
+
*/
|
|
62
|
+
export function httpTransport(baseUrl) {
|
|
63
|
+
const url = (p) => `${baseUrl.replace(/\/+$/, '')}${p}`;
|
|
64
|
+
const call = async (method, path, body) => {
|
|
65
|
+
const res = await fetch(url(path), {
|
|
66
|
+
method,
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
// Une réponse non-JSON (page d'erreur de proxy, 502 HTML) ne doit pas lever une
|
|
71
|
+
// exception de parsing qui masquerait le vrai statut HTTP — c'est lui qui porte
|
|
72
|
+
// l'information utile.
|
|
73
|
+
let parsed = {};
|
|
74
|
+
try {
|
|
75
|
+
parsed = (await res.json());
|
|
76
|
+
}
|
|
77
|
+
catch { /* corps non-JSON */ }
|
|
78
|
+
return { status: res.status, body: parsed };
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
post: (path, body) => call('POST', path, body),
|
|
82
|
+
get: (path) => call('GET', path),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function runCloudConnect(options) {
|
|
86
|
+
const cwd = options.cwd ?? resolveEffectiveCwd();
|
|
87
|
+
const transport = options.transport ?? httpTransport(options.url);
|
|
88
|
+
let handle;
|
|
89
|
+
try {
|
|
90
|
+
handle = await beginPairing({
|
|
91
|
+
inviteCode: options.inviteCode,
|
|
92
|
+
agentId: options.agentId,
|
|
93
|
+
transport,
|
|
94
|
+
cwd,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err instanceof PairingError) {
|
|
99
|
+
console.error(`Appairage interrompu à l'étape « ${err.stage} » : ${err.message}`);
|
|
100
|
+
// Fail-closed : un appairage refusé ne laisse aucune trace locale, donc relancer la
|
|
101
|
+
// commande est sûr et ne laisse pas d'orphelin à nettoyer.
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
if (options.json) {
|
|
108
|
+
console.log(JSON.stringify({
|
|
109
|
+
enrollment_id: handle.enrollment_id,
|
|
110
|
+
cloud_project_id: handle.cloud_project_id,
|
|
111
|
+
device_id: handle.device.device_id,
|
|
112
|
+
fingerprints: handle.fingerprints,
|
|
113
|
+
awaiting: 'human_approval',
|
|
114
|
+
}, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log('Preuve de possession acceptée. Attestation de clé enregistrée.');
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(' Projet cloud : ' + handle.cloud_project_id);
|
|
120
|
+
console.log(' Enrôlement : ' + handle.enrollment_id);
|
|
121
|
+
console.log('');
|
|
122
|
+
// CES DEUX EMPREINTES SONT LE CŒUR DE LA CÉRÉMONIE. La personne qui approuve voit les
|
|
123
|
+
// mêmes à l'écran ; leur comparaison hors bande est ce qui ferme l'attaque de l'homme
|
|
124
|
+
// du milieu sur l'appairage. Affichées EN ENTIER, pas tronquées : une comparaison sur
|
|
125
|
+
// 16 caractères se collisionne bien plus facilement qu'elle n'en a l'air.
|
|
126
|
+
console.log(" Empreinte d'identité (Ed25519) : " + handle.fingerprints.identity);
|
|
127
|
+
console.log(' Empreinte de chiffrement (X25519) : ' + handle.fingerprints.encryption);
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' → Faites vérifier CES DEUX EMPREINTES par la personne qui approuve.');
|
|
130
|
+
console.log(" Si elles diffèrent de ce qu'elle voit, REFUSEZ : quelqu'un s'est interposé.");
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(" En attente d'approbation humaine. Constatez-la avec : brainclaw cloud await");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Constate l'approbation et bascule l'état local en actif.
|
|
136
|
+
*
|
|
137
|
+
* SÉPARÉ DE `connect` : l'approbation dépend d'un humain, dont le délai n'est pas borné.
|
|
138
|
+
* Une commande qui bloquerait indéfiniment sur un tiers serait un mauvais citoyen dans un
|
|
139
|
+
* script, et un appairage interrompu par un Ctrl-C doit rester reprenable.
|
|
140
|
+
*/
|
|
141
|
+
export async function runCloudAwait(options) {
|
|
142
|
+
const cwd = options.cwd ?? resolveEffectiveCwd();
|
|
143
|
+
const state = loadConnectionState(cwd);
|
|
144
|
+
if (!state?.enrollment.enrollment_id) {
|
|
145
|
+
console.error("Aucun appairage en cours sur ce workspace. Lancez d'abord : brainclaw cloud connect");
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const transport = options.transport ?? httpTransport(options.url);
|
|
150
|
+
const result = await checkPairingApproval({ enrollmentId: state.enrollment.enrollment_id, transport, cwd });
|
|
151
|
+
if (!result.approved) {
|
|
152
|
+
console.log(`Toujours en attente (état distant : ${result.state}).`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const next = completePairing({ role: result.role, cwd });
|
|
156
|
+
console.log(`Appairage approuvé. Rôle : ${next.enrollment.role ?? '—'}.`);
|
|
157
|
+
console.log('');
|
|
158
|
+
// Le premier pull est en LECTURE SEULE et non destructif (RFC §5.2 phase 4) : rien
|
|
159
|
+
// n'est matérialisé dans la mémoire locale tant que la vérification d'origine n'existe
|
|
160
|
+
// pas. Le dire évite qu'on croie la synchronisation déjà active.
|
|
161
|
+
console.log(" Aucune donnée n'est encore matérialisée : la vérification à la réception");
|
|
162
|
+
console.log(" (signature d'origine, anti-rejeu) est livrée à l'étape suivante.");
|
|
163
|
+
}
|
|
164
|
+
export async function runCloudDisconnect(options) {
|
|
165
|
+
const cwd = options.cwd ?? resolveEffectiveCwd();
|
|
166
|
+
const state = loadConnectionState(cwd);
|
|
167
|
+
if (!state) {
|
|
168
|
+
console.log("Ce workspace n'est pas appairé — rien à déconnecter.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let revocationNote = "aucune révocation distante demandée (pas d'enrôlement connu)";
|
|
172
|
+
if (state.enrollment.enrollment_id) {
|
|
173
|
+
const transport = options.transport ?? httpTransport(options.url);
|
|
174
|
+
const res = await requestRevocation({ enrollmentId: state.enrollment.enrollment_id, transport });
|
|
175
|
+
revocationNote = res.revoked
|
|
176
|
+
? 'autorisation distante révoquée'
|
|
177
|
+
: `révocation distante NON confirmée (${res.detail ?? 'raison inconnue'})`;
|
|
178
|
+
}
|
|
179
|
+
// L'état local passe en 'revoked' MÊME SI le cloud est injoignable : sinon un appareil
|
|
180
|
+
// perdu resterait autorisé faute de réseau, exactement l'inverse de ce qu'une
|
|
181
|
+
// révocation doit garantir.
|
|
182
|
+
saveConnectionState({ ...state, enrollment: { ...state.enrollment, stage: 'revoked', updated_at: nowISO() } }, cwd);
|
|
183
|
+
let keysNote = "trousseau conservé (utilisez --forget-keys pour l'effacer)";
|
|
184
|
+
if (options.forgetKeys) {
|
|
185
|
+
const removed = forgetProjectEpochs(state.cloud_project_id);
|
|
186
|
+
keysNote = `${removed} clé(s) d'epoch effacée(s) — le passé scellé de ce projet devient illisible ici`;
|
|
187
|
+
}
|
|
188
|
+
console.log('Déconnecté.');
|
|
189
|
+
console.log(` ${revocationNote}`);
|
|
190
|
+
console.log(` ${keysNote}`);
|
|
191
|
+
console.log('');
|
|
192
|
+
// Énoncé plutôt que tu : prétendre l'inverse serait une promesse que la cryptographie
|
|
193
|
+
// ne tient pas (RFC §5.2).
|
|
194
|
+
console.log(" Ce qui N'EST PAS effacé : les données déjà tirées et déchiffrées localement,");
|
|
195
|
+
console.log(" et ce que d'autres appareils détiennent déjà. Un disconnect retire une");
|
|
196
|
+
console.log(' autorisation ; il ne réécrit pas le passé.');
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=cloud.js.map
|
package/dist/commands/export.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadConfig, saveConfig } from '../core/config.js';
|
|
|
6
6
|
import { isAgentIntegrationName, upsertAgentIntegrationDeclaration } from '../core/agent-integrations.js';
|
|
7
7
|
import { resolveInstructions, loadInstructions } from '../core/instructions.js';
|
|
8
8
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
9
|
-
import { AGENT_EXPORT_REGISTRY, resolveExportTarget, resolveExportTargetByFormat, resolveLiveCompanionPath, writeExportFile, writeLiveCompanionFile, buildHygieneSection, describeAutoConfigWrite, writeExportCompanionFiles, collectExportGitignoreEntries, ensureGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, } from '../core/agent-files.js';
|
|
9
|
+
import { AGENT_EXPORT_REGISTRY, resolveExportTarget, resolveExportTargetByFormat, resolveLiveCompanionPath, writeExportFile, writeLiveCompanionFile, buildHygieneSection, describeAutoConfigWrite, writeExportCompanionFiles, collectExportGitignoreEntries, ensureGitignoreEntries, BRAINCLAW_EXCLUSIVE_DIRECTORIES, BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES, } from '../core/agent-files.js';
|
|
10
10
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
11
11
|
import { listClaims } from '../core/claims.js';
|
|
12
12
|
import { listCandidates } from '../core/candidates.js';
|
|
@@ -61,7 +61,7 @@ export function runExport(options) {
|
|
|
61
61
|
});
|
|
62
62
|
if (liveResult)
|
|
63
63
|
gitignoreEntries.push(liveResult.relativePath);
|
|
64
|
-
ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES]);
|
|
64
|
+
ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES, ...BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES]);
|
|
65
65
|
declareAgentIntegrationFromTarget(cwd, target.agentName, 'manual');
|
|
66
66
|
console.log(`✔ Written to ${target.relativePath} (${result.created ? 'created' : 'updated'})`);
|
|
67
67
|
if (liveResult) {
|
|
@@ -102,7 +102,7 @@ function runExportDetect(cwd, options) {
|
|
|
102
102
|
const gitignoreEntries = collectExportGitignoreEntries(cwd, target.relativePath, autoConfigs);
|
|
103
103
|
if (liveResult)
|
|
104
104
|
gitignoreEntries.push(liveResult.relativePath);
|
|
105
|
-
ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES]);
|
|
105
|
+
ensureGitignoreEntries(cwd, [...gitignoreEntries, ...BRAINCLAW_EXCLUSIVE_DIRECTORIES, ...BRAINCLAW_PROTOCOL_ARTIFACT_IGNORES]);
|
|
106
106
|
declareAgentIntegrationFromTarget(cwd, target.agentName, detected ? 'detected' : 'manual');
|
|
107
107
|
const source = detected ? `${detected.name} [${detected.detection_source}]` : 'fallback (no agent detected)';
|
|
108
108
|
console.log(`✔ Detected: ${source}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import readline from 'node:readline/promises';
|
|
4
|
+
import { clearEnumerationMemo } from '../core/entity-locator.js';
|
|
4
5
|
import { registerAgentIdentity, resolveDefaultAgentName, resolveExistingCurrentAgent } from '../core/agent-registry.js';
|
|
5
6
|
import { MEMORY_DIR, memoryExists, ensureMemoryDir, memoryPath, writeFileAtomic } from '../core/io.js';
|
|
6
7
|
import { emptyState, loadState, saveState } from '../core/state.js';
|
|
@@ -371,6 +372,16 @@ export async function runInit(options = {}) {
|
|
|
371
372
|
console.log(`Tip: run 'brainclaw init' again later to refresh the detected agent's integration files on this project.`);
|
|
372
373
|
}
|
|
373
374
|
console.log(`Tip: in an agent session, call the bclaw_work MCP tool (intent: "consult") to load the shared memory; from a terminal, 'brainclaw context --json' does the same.`);
|
|
375
|
+
// A STORE JUST CAME INTO EXISTENCE, so the routing memo's candidate list is stale.
|
|
376
|
+
//
|
|
377
|
+
// `clearEnumerationMemo` was documented as being "for tests, and for any caller that has
|
|
378
|
+
// just created a store" — and that second caller did not exist (Fable audit found the
|
|
379
|
+
// claim describing intent rather than code). The consequence was small but real: for up
|
|
380
|
+
// to the memo TTL, a mutation routed right after `brainclaw init` / `bclaw_init_project`
|
|
381
|
+
// could not see the new project. Wiring it HERE rather than in the MCP handler covers
|
|
382
|
+
// every path that materialises a store, since both the CLI and the tool go through
|
|
383
|
+
// runInit.
|
|
384
|
+
clearEnumerationMemo();
|
|
374
385
|
}
|
|
375
386
|
function safeRunMachinePrereqs(agentName) {
|
|
376
387
|
try {
|