brainclaw 0.19.7 → 0.19.10
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/README.md +225 -126
- package/dist/cli.js +8 -3
- package/dist/commands/bootstrap.js +72 -3
- package/dist/commands/init.js +20 -0
- package/dist/commands/mcp.js +7 -1
- package/dist/core/bootstrap.js +587 -4
- package/dist/core/migration.js +6 -2
- package/dist/core/schema.js +69 -0
- package/docs/cli.md +21 -1
- package/docs/integrations/agents.md +42 -29
- package/docs/integrations/claude-code.md +4 -4
- package/docs/integrations/codex.md +5 -5
- package/docs/integrations/copilot.md +3 -2
- package/docs/integrations/cursor.md +4 -4
- package/docs/integrations/mcp.md +53 -24
- package/docs/integrations/overview.md +53 -40
- package/docs/mcp-schema-changelog.md +3 -0
- package/docs/product/positioning.md +17 -18
- package/docs/quickstart.md +87 -55
- package/package.json +7 -7
package/dist/core/bootstrap.js
CHANGED
|
@@ -3,27 +3,60 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { JsonStore } from './json-store.js';
|
|
5
5
|
import { generateId, nowISO } from './ids.js';
|
|
6
|
-
import { resolveEntityDir } from './io.js';
|
|
7
|
-
import { BootstrapProfileDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
6
|
+
import { memoryPath, resolveEntityDir, withStoreLock, writeFileAtomic } from './io.js';
|
|
7
|
+
import { BootstrapApplicationReceiptSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
8
8
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
9
9
|
import { analyzeRepository } from './repo-analysis.js';
|
|
10
10
|
import { buildExecutionContext, compactExecutionContext } from './execution-context.js';
|
|
11
11
|
import { buildAgentToolingContext } from './agent-context.js';
|
|
12
|
+
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
13
|
+
import { resolveCurrentAgentName } from './agent-registry.js';
|
|
14
|
+
import { loadState } from './state.js';
|
|
15
|
+
import { generateMarkdown } from './markdown.js';
|
|
12
16
|
const README_CANDIDATES = ['README.md', 'README', 'README.txt', 'README.mdx'];
|
|
13
17
|
const DOC_HINTS = ['docs', 'doc'];
|
|
14
18
|
const MAKEFILE_NAME = 'Makefile';
|
|
15
19
|
const PROFILE_FILE = 'profile.json';
|
|
20
|
+
const IMPORT_PLAN_FILE = 'import-plan.json';
|
|
21
|
+
const APPLICATION_FILE = 'last-application.json';
|
|
16
22
|
const HOTSPOT_LIMIT = 3;
|
|
17
23
|
const DERIVED_SCHEMA_VERSION = 2;
|
|
24
|
+
const EMPTY_WORKSPACE_IGNORED = new Set([
|
|
25
|
+
'.git',
|
|
26
|
+
'.brainclaw',
|
|
27
|
+
'.gitignore',
|
|
28
|
+
'.gitattributes',
|
|
29
|
+
'.gitmodules',
|
|
30
|
+
'.DS_Store',
|
|
31
|
+
'Thumbs.db',
|
|
32
|
+
'internal-docs',
|
|
33
|
+
]);
|
|
34
|
+
const NATIVE_INSTRUCTION_FILES = [
|
|
35
|
+
'AGENTS.md',
|
|
36
|
+
'CLAUDE.md',
|
|
37
|
+
'GEMINI.md',
|
|
38
|
+
'.windsurfrules',
|
|
39
|
+
'.github/copilot-instructions.md',
|
|
40
|
+
];
|
|
41
|
+
const NATIVE_INSTRUCTION_DIRS = [
|
|
42
|
+
'.cursor/rules',
|
|
43
|
+
'.roo/rules',
|
|
44
|
+
'.continue/rules',
|
|
45
|
+
'.clinerules',
|
|
46
|
+
];
|
|
18
47
|
export function runBootstrapProfile(options = {}) {
|
|
19
48
|
const cwd = options.cwd ?? process.cwd();
|
|
20
49
|
const target = normalizeTarget(options.target);
|
|
21
50
|
const existing = loadBootstrapProfile(cwd);
|
|
51
|
+
const existingPlan = loadBootstrapImportPlan(cwd);
|
|
52
|
+
const lastApplication = loadBootstrapApplication(cwd);
|
|
22
53
|
const existingFingerprint = currentRepoFingerprint(cwd);
|
|
23
|
-
if (!options.refresh && existing && isProfileReusable(existing, target, existingFingerprint)) {
|
|
54
|
+
if (!options.refresh && existing && existingPlan && isProfileReusable(existing, target, existingFingerprint)) {
|
|
24
55
|
return {
|
|
25
56
|
profile: existing,
|
|
26
57
|
seeds: listBootstrapSeeds(cwd),
|
|
58
|
+
importPlan: existingPlan,
|
|
59
|
+
lastApplication,
|
|
27
60
|
reusedProfile: true,
|
|
28
61
|
};
|
|
29
62
|
}
|
|
@@ -32,6 +65,8 @@ export function runBootstrapProfile(options = {}) {
|
|
|
32
65
|
return {
|
|
33
66
|
profile: artifacts.profile,
|
|
34
67
|
seeds: artifacts.seeds,
|
|
68
|
+
importPlan: artifacts.importPlan,
|
|
69
|
+
lastApplication,
|
|
35
70
|
reusedProfile: false,
|
|
36
71
|
};
|
|
37
72
|
}
|
|
@@ -82,8 +117,21 @@ export function selectDerivedSignals(target, maxSignals, cwd) {
|
|
|
82
117
|
}
|
|
83
118
|
export function renderBootstrapSummary(result) {
|
|
84
119
|
const lines = [result.profile.summary];
|
|
120
|
+
if (result.profile.workspace_kind) {
|
|
121
|
+
lines.push(`Workspace kind: ${result.profile.workspace_kind}`);
|
|
122
|
+
}
|
|
123
|
+
if (result.profile.confidence) {
|
|
124
|
+
lines.push(`Confidence: ${result.profile.confidence}`);
|
|
125
|
+
}
|
|
85
126
|
lines.push(`Sources scanned: ${result.profile.sources_scanned.join(', ') || 'none'}`);
|
|
86
127
|
lines.push(`Seed count: ${result.profile.seed_count}`);
|
|
128
|
+
if ((result.profile.native_instruction_files?.length ?? 0) > 0) {
|
|
129
|
+
lines.push(`Native instruction files: ${result.profile.native_instruction_files.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
if ((result.profile.gaps?.length ?? 0) > 0) {
|
|
132
|
+
lines.push(`Open gaps: ${result.profile.gaps.join(' | ')}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push(`Import suggestions: ${result.importPlan.suggestion_count}`);
|
|
87
135
|
if (result.profile.repo_fingerprint) {
|
|
88
136
|
lines.push(`Repo fingerprint: ${result.profile.repo_fingerprint}`);
|
|
89
137
|
}
|
|
@@ -93,6 +141,24 @@ export function renderBootstrapSummary(result) {
|
|
|
93
141
|
if (result.reusedProfile) {
|
|
94
142
|
lines.push('Reused existing bootstrap profile.');
|
|
95
143
|
}
|
|
144
|
+
if (result.lastApplication && !result.lastApplication.uninstalled_at) {
|
|
145
|
+
lines.push(`Last bootstrap import: ${result.lastApplication.managed_artifacts.length} managed artifact(s) from ${result.lastApplication.applied_at}`);
|
|
146
|
+
}
|
|
147
|
+
if (result.importPlan.suggestions.length > 0) {
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('Import proposal:');
|
|
150
|
+
for (const suggestion of result.importPlan.suggestions.slice(0, 10)) {
|
|
151
|
+
const scope = suggestion.scope ? `:${suggestion.scope}` : '';
|
|
152
|
+
lines.push(`- [${suggestion.target}/${suggestion.confidence}] <${suggestion.layer ?? 'global'}${scope}> ${suggestion.text}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if ((result.importPlan.interview?.question_count ?? 0) > 0) {
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('Adaptive interview:');
|
|
158
|
+
for (const question of result.importPlan.interview.questions.slice(0, 6)) {
|
|
159
|
+
lines.push(`- [${question.priority}/${question.audience}] ${question.prompt}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
96
162
|
if (result.seeds.length > 0) {
|
|
97
163
|
lines.push('');
|
|
98
164
|
lines.push('Derived signals:');
|
|
@@ -102,9 +168,29 @@ export function renderBootstrapSummary(result) {
|
|
|
102
168
|
}
|
|
103
169
|
return lines.join('\n');
|
|
104
170
|
}
|
|
171
|
+
export function renderBootstrapInterview(result, audience = 'any') {
|
|
172
|
+
const interview = result.importPlan.interview;
|
|
173
|
+
if (!interview || interview.question_count === 0) {
|
|
174
|
+
return 'No adaptive interview questions are needed right now.';
|
|
175
|
+
}
|
|
176
|
+
const questions = interview.questions.filter((question) => audience === 'any' || question.audience === 'any' || question.audience === audience);
|
|
177
|
+
if (questions.length === 0) {
|
|
178
|
+
return `No adaptive interview questions are targeted to ${audience}.`;
|
|
179
|
+
}
|
|
180
|
+
const lines = [interview.summary, `Audience: ${audience}`];
|
|
181
|
+
lines.push('');
|
|
182
|
+
for (const [index, question] of questions.entries()) {
|
|
183
|
+
lines.push(`${index + 1}. ${question.prompt}`);
|
|
184
|
+
lines.push(` Why: ${question.rationale}`);
|
|
185
|
+
lines.push(` Expected answer: ${question.response_kind}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|
|
105
189
|
function buildBootstrapArtifacts(input) {
|
|
106
190
|
const sourcesScanned = [];
|
|
107
191
|
const seeds = [];
|
|
192
|
+
const workspace = classifyWorkspace(input.cwd);
|
|
193
|
+
const nativeInstructionFiles = discoverNativeInstructionFiles(input.cwd);
|
|
108
194
|
const readmePath = findFirstExisting(input.cwd, README_CANDIDATES);
|
|
109
195
|
if (readmePath) {
|
|
110
196
|
sourcesScanned.push('README');
|
|
@@ -116,6 +202,10 @@ function buildBootstrapArtifacts(input) {
|
|
|
116
202
|
sourcesScanned.push('AGENTS.md');
|
|
117
203
|
seeds.push(...extractAgentsSeeds(agentsPath, input.target));
|
|
118
204
|
}
|
|
205
|
+
if (nativeInstructionFiles.length > 0) {
|
|
206
|
+
sourcesScanned.push('native_instructions');
|
|
207
|
+
seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(input.cwd, relativePath)), input.cwd, input.target));
|
|
208
|
+
}
|
|
119
209
|
const manifestResult = extractManifestSeeds(input.cwd, input.target);
|
|
120
210
|
if (manifestResult.seeds.length > 0) {
|
|
121
211
|
sourcesScanned.push(...manifestResult.sources);
|
|
@@ -142,12 +232,37 @@ function buildBootstrapArtifacts(input) {
|
|
|
142
232
|
seeds.push(...gitProbe.hotspotSeeds);
|
|
143
233
|
}
|
|
144
234
|
const uniqueSeeds = dedupeSeeds(seeds);
|
|
235
|
+
const confidence = inferBootstrapConfidence({
|
|
236
|
+
workspaceKind: workspace.kind,
|
|
237
|
+
readmePresent: Boolean(readmePath),
|
|
238
|
+
agentsPresent,
|
|
239
|
+
nativeInstructionFiles,
|
|
240
|
+
seedCount: uniqueSeeds.length,
|
|
241
|
+
});
|
|
242
|
+
const gaps = inferBootstrapGaps({
|
|
243
|
+
workspaceKind: workspace.kind,
|
|
244
|
+
readmePresent: Boolean(readmePath),
|
|
245
|
+
nativeInstructionFiles,
|
|
246
|
+
seedCount: uniqueSeeds.length,
|
|
247
|
+
});
|
|
145
248
|
const summary = buildSummary({
|
|
249
|
+
workspaceKind: workspace.kind,
|
|
146
250
|
agentsPresent,
|
|
251
|
+
nativeInstructionFiles,
|
|
147
252
|
gitAvailable: gitProbe.available,
|
|
148
253
|
repoAnalysis,
|
|
149
254
|
seeds: uniqueSeeds,
|
|
150
255
|
target: input.target,
|
|
256
|
+
confidence,
|
|
257
|
+
gaps,
|
|
258
|
+
});
|
|
259
|
+
const importPlan = buildBootstrapImportPlan({
|
|
260
|
+
cwd: input.cwd,
|
|
261
|
+
target: input.target,
|
|
262
|
+
workspaceKind: workspace.kind,
|
|
263
|
+
confidence,
|
|
264
|
+
gaps,
|
|
265
|
+
seeds: uniqueSeeds,
|
|
151
266
|
});
|
|
152
267
|
return {
|
|
153
268
|
profile: BootstrapProfileDocumentSchema.parse({
|
|
@@ -160,11 +275,16 @@ function buildBootstrapArtifacts(input) {
|
|
|
160
275
|
agents_md_present: agentsPresent,
|
|
161
276
|
seed_count: uniqueSeeds.length,
|
|
162
277
|
target: input.target,
|
|
278
|
+
workspace_kind: workspace.kind,
|
|
279
|
+
confidence,
|
|
280
|
+
native_instruction_files: nativeInstructionFiles,
|
|
281
|
+
gaps,
|
|
163
282
|
}),
|
|
164
283
|
seeds: uniqueSeeds.map((seed) => MemorySeedDocumentSchema.parse({
|
|
165
284
|
...seed,
|
|
166
285
|
schema_version: DERIVED_SCHEMA_VERSION,
|
|
167
286
|
})),
|
|
287
|
+
importPlan,
|
|
168
288
|
};
|
|
169
289
|
}
|
|
170
290
|
function extractReadmeSeeds(filepath, target) {
|
|
@@ -236,6 +356,80 @@ function extractAgentsSeeds(filepath, target) {
|
|
|
236
356
|
}
|
|
237
357
|
return seeds;
|
|
238
358
|
}
|
|
359
|
+
export function loadBootstrapImportPlan(cwd) {
|
|
360
|
+
const filepath = bootstrapImportPlanPath(cwd);
|
|
361
|
+
if (!fs.existsSync(filepath)) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
return loadVersionedJsonFile('bootstrap_import_plan', filepath).document;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export function loadBootstrapApplication(cwd) {
|
|
372
|
+
const filepath = bootstrapApplicationPath(cwd);
|
|
373
|
+
if (!fs.existsSync(filepath)) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
return loadVersionedJsonFile('bootstrap_application', filepath).document;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function extractNativeInstructionSeeds(filepaths, cwd, target) {
|
|
384
|
+
const seeds = [];
|
|
385
|
+
for (const filepath of filepaths) {
|
|
386
|
+
if (path.basename(filepath) === 'AGENTS.md') {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const relativePath = path.relative(cwd, filepath).replace(/\\/g, '/');
|
|
390
|
+
const raw = fs.readFileSync(filepath, 'utf-8');
|
|
391
|
+
const lines = raw.split(/\r?\n/);
|
|
392
|
+
const heading = lines.find((line) => line.trim().startsWith('#'));
|
|
393
|
+
const firstContent = lines.find((line) => line.trim().length > 0);
|
|
394
|
+
const label = heading
|
|
395
|
+
? heading.replace(/^#+\s*/, '').trim()
|
|
396
|
+
: firstContent?.trim().replace(/^[-*]\s+/, '') ?? relativePath;
|
|
397
|
+
seeds.push(createSeed({
|
|
398
|
+
text: `Native agent guidance from ${path.basename(relativePath)}: ${label}`,
|
|
399
|
+
seedKind: 'agent_rule',
|
|
400
|
+
sourceKind: 'native_instruction',
|
|
401
|
+
sourceRef: relativePath,
|
|
402
|
+
confidence: 'high',
|
|
403
|
+
tags: ['bootstrap', 'agent', 'native-context'],
|
|
404
|
+
relatedPaths: target ? [target] : undefined,
|
|
405
|
+
}));
|
|
406
|
+
let extracted = 0;
|
|
407
|
+
for (const line of lines) {
|
|
408
|
+
const trimmed = line.trim();
|
|
409
|
+
if (!/^([-*]|\d+\.)\s+/.test(trimmed)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const text = trimmed.replace(/^([-*]|\d+\.)\s+/, '').trim();
|
|
413
|
+
if (!text) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
seeds.push(createSeed({
|
|
417
|
+
text,
|
|
418
|
+
seedKind: 'agent_rule',
|
|
419
|
+
sourceKind: 'native_instruction',
|
|
420
|
+
sourceRef: relativePath,
|
|
421
|
+
confidence: 'medium',
|
|
422
|
+
tags: ['bootstrap', 'agent', 'native-context'],
|
|
423
|
+
relatedPaths: target ? [target] : undefined,
|
|
424
|
+
}));
|
|
425
|
+
extracted++;
|
|
426
|
+
if (extracted >= 5) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return seeds;
|
|
432
|
+
}
|
|
239
433
|
function extractManifestSeeds(cwd, target) {
|
|
240
434
|
const sources = [];
|
|
241
435
|
const seeds = [];
|
|
@@ -468,6 +662,7 @@ function probeGit(cwd, target) {
|
|
|
468
662
|
function persistBootstrapArtifacts(artifacts, cwd) {
|
|
469
663
|
ensureBootstrapDirs(cwd);
|
|
470
664
|
saveVersionedJsonFile('bootstrap_profile', bootstrapProfilePath(cwd), artifacts.profile);
|
|
665
|
+
saveVersionedJsonFile('bootstrap_import_plan', bootstrapImportPlanPath(cwd), artifacts.importPlan);
|
|
471
666
|
const store = bootstrapSeedStore(cwd);
|
|
472
667
|
const existingIds = new Set(store.list().map((seed) => seed.id));
|
|
473
668
|
for (const seed of artifacts.seeds) {
|
|
@@ -502,6 +697,12 @@ function bootstrapSeedsDir(cwd) {
|
|
|
502
697
|
function bootstrapProfilePath(cwd) {
|
|
503
698
|
return path.join(bootstrapDir(cwd), PROFILE_FILE);
|
|
504
699
|
}
|
|
700
|
+
function bootstrapImportPlanPath(cwd) {
|
|
701
|
+
return path.join(bootstrapDir(cwd), IMPORT_PLAN_FILE);
|
|
702
|
+
}
|
|
703
|
+
function bootstrapApplicationPath(cwd) {
|
|
704
|
+
return path.join(bootstrapDir(cwd), APPLICATION_FILE);
|
|
705
|
+
}
|
|
505
706
|
function isProfileReusable(profile, target, currentFingerprint) {
|
|
506
707
|
if ((profile.target ?? undefined) !== target) {
|
|
507
708
|
return false;
|
|
@@ -546,11 +747,15 @@ function dedupeSeeds(seeds) {
|
|
|
546
747
|
}
|
|
547
748
|
function buildSummary(input) {
|
|
548
749
|
const parts = [];
|
|
549
|
-
parts.push(`Bootstrap summary${input.target ? ` for ${input.target}` : ''}: ${input.seeds.length} derived signal(s).`);
|
|
750
|
+
parts.push(`Bootstrap summary${input.target ? ` for ${input.target}` : ''}: ${input.workspaceKind} workspace, ${input.seeds.length} derived signal(s).`);
|
|
751
|
+
parts.push(`Confidence: ${input.confidence}.`);
|
|
550
752
|
parts.push(`Repository mode looks ${input.repoAnalysis.recommendedMode}.`);
|
|
551
753
|
if (input.agentsPresent) {
|
|
552
754
|
parts.push('AGENTS.md detected and summarized.');
|
|
553
755
|
}
|
|
756
|
+
if (input.nativeInstructionFiles.length > 0) {
|
|
757
|
+
parts.push(`${input.nativeInstructionFiles.length} native instruction file(s) detected.`);
|
|
758
|
+
}
|
|
554
759
|
if (input.gitAvailable) {
|
|
555
760
|
parts.push('Git history available for hotspot detection.');
|
|
556
761
|
}
|
|
@@ -558,8 +763,386 @@ function buildSummary(input) {
|
|
|
558
763
|
if (commandCount > 0) {
|
|
559
764
|
parts.push(`${commandCount} command-oriented hint(s) found.`);
|
|
560
765
|
}
|
|
766
|
+
if (input.gaps.length > 0) {
|
|
767
|
+
parts.push(`Needs follow-up on: ${input.gaps.join('; ')}.`);
|
|
768
|
+
}
|
|
561
769
|
return parts.join(' ');
|
|
562
770
|
}
|
|
771
|
+
function buildBootstrapImportPlan(input) {
|
|
772
|
+
const activeInstructionKeys = new Set(loadInstructions(input.cwd)
|
|
773
|
+
.filter((entry) => entry.active)
|
|
774
|
+
.map((entry) => instructionIdentityKey(entry.text, entry.layer, entry.scope)));
|
|
775
|
+
const suggestions = [];
|
|
776
|
+
const seenSuggestionKeys = new Set();
|
|
777
|
+
const importedSources = new Set();
|
|
778
|
+
const groupedBySource = new Map();
|
|
779
|
+
for (const seed of input.seeds) {
|
|
780
|
+
const bucket = groupedBySource.get(seed.source_ref) ?? [];
|
|
781
|
+
bucket.push(seed);
|
|
782
|
+
groupedBySource.set(seed.source_ref, bucket);
|
|
783
|
+
}
|
|
784
|
+
for (const seed of input.seeds) {
|
|
785
|
+
const suggestion = seedToBootstrapSuggestion(seed, false);
|
|
786
|
+
if (!suggestion) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const suggestionKey = suggestionIdentityKey(suggestion);
|
|
790
|
+
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
seenSuggestionKeys.add(suggestionKey);
|
|
794
|
+
importedSources.add(seed.source_ref);
|
|
795
|
+
suggestions.push(BootstrapSuggestionDocumentSchema.parse({
|
|
796
|
+
...suggestion,
|
|
797
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
for (const [sourceRef, seeds] of groupedBySource.entries()) {
|
|
801
|
+
if (importedSources.has(sourceRef)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const summarySeed = seeds.find((seed) => isBootstrapSummarySeed(seed.text));
|
|
805
|
+
if (!summarySeed) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const suggestion = seedToBootstrapSuggestion(summarySeed, true);
|
|
809
|
+
if (!suggestion) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const suggestionKey = suggestionIdentityKey(suggestion);
|
|
813
|
+
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
seenSuggestionKeys.add(suggestionKey);
|
|
817
|
+
suggestions.push(BootstrapSuggestionDocumentSchema.parse({
|
|
818
|
+
...suggestion,
|
|
819
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
820
|
+
}));
|
|
821
|
+
}
|
|
822
|
+
const summary = suggestions.length === 0
|
|
823
|
+
? 'No safe bootstrap imports are ready yet; review the gaps and use an interview/import step before promoting derived context.'
|
|
824
|
+
: `${suggestions.length} bootstrap instruction suggestion(s) are ready to import after review.`;
|
|
825
|
+
const interview = buildBootstrapInterviewPlan({
|
|
826
|
+
workspaceKind: input.workspaceKind,
|
|
827
|
+
gaps: input.gaps,
|
|
828
|
+
confidence: input.confidence,
|
|
829
|
+
nativeInstructionSources: [...new Set(input.seeds
|
|
830
|
+
.filter((seed) => seed.source_kind === 'native_instruction')
|
|
831
|
+
.map((seed) => seed.source_ref))],
|
|
832
|
+
});
|
|
833
|
+
return BootstrapImportPlanDocumentSchema.parse({
|
|
834
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
835
|
+
derived_at: nowISO(),
|
|
836
|
+
target: input.target,
|
|
837
|
+
workspace_kind: input.workspaceKind,
|
|
838
|
+
confidence: input.confidence,
|
|
839
|
+
summary,
|
|
840
|
+
requires_confirmation: true,
|
|
841
|
+
gaps: input.gaps,
|
|
842
|
+
suggestion_count: suggestions.length,
|
|
843
|
+
suggestions,
|
|
844
|
+
interview,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
function buildBootstrapInterviewPlan(input) {
|
|
848
|
+
const questions = [];
|
|
849
|
+
const add = (prompt, rationale, priority, audience, responseKind, gapKeys) => {
|
|
850
|
+
questions.push(BootstrapInterviewQuestionSchema.parse({
|
|
851
|
+
id: generateId('bootstrap_suggestions'),
|
|
852
|
+
prompt,
|
|
853
|
+
rationale,
|
|
854
|
+
priority,
|
|
855
|
+
audience,
|
|
856
|
+
response_kind: responseKind,
|
|
857
|
+
gap_keys: gapKeys,
|
|
858
|
+
}));
|
|
859
|
+
};
|
|
860
|
+
if (input.workspaceKind === 'empty') {
|
|
861
|
+
add('What is this workspace trying to build in one sentence?', 'An empty workspace needs a product intent anchor before Brainclaw can create durable guidance.', 'high', 'any', 'short_text', ['project intent is not documented yet']);
|
|
862
|
+
add('Which coding agents do you expect to use here, and should they work mostly sequentially or in parallel later?', 'Bootstrap should capture collaboration expectations early, especially before worktree isolation exists.', 'high', 'any', 'list', ['agent workflow expectations should be captured explicitly']);
|
|
863
|
+
add('For a CLI-only agent, what should the very first safe action be after reading context?', 'Pure CLI agents need an explicit first action because they do not benefit from rich IDE affordances.', 'medium', 'cli', 'short_text', ['agent workflow expectations should be captured explicitly']);
|
|
864
|
+
add('For an IDE chat agent, what should it ask or inspect before editing code?', 'IDE chat agents can ask targeted follow-up questions; capturing that expectation avoids drift between surfaces.', 'medium', 'ide_chat', 'short_text', ['agent workflow expectations should be captured explicitly']);
|
|
865
|
+
}
|
|
866
|
+
if (input.gaps.includes('no README-level project overview detected')) {
|
|
867
|
+
add('What is the current purpose of this existing project, and what would you want a new agent to understand first?', 'When README context is missing, Brainclaw needs an explicit project overview to avoid weak brownfield inference.', 'high', 'any', 'long_text', ['no README-level project overview detected']);
|
|
868
|
+
}
|
|
869
|
+
if (input.gaps.includes('no native agent instruction files detected')) {
|
|
870
|
+
add('Should Brainclaw treat this project as agent-guided even though no native agent instruction files were found?', 'This determines whether Brainclaw should synthesize workflow guidance or stay memory-only for now.', 'medium', 'any', 'boolean', ['no native agent instruction files detected']);
|
|
871
|
+
}
|
|
872
|
+
if (input.gaps.includes('derived context is sparse and may need an interview')) {
|
|
873
|
+
add('Which constraints or conventions are important enough that every future agent should see them immediately?', 'Sparse derived context should be turned into a small set of high-signal shared instructions or constraints.', 'high', 'any', 'list', ['derived context is sparse and may need an interview']);
|
|
874
|
+
}
|
|
875
|
+
if (input.nativeInstructionSources.length > 0) {
|
|
876
|
+
add(`Which parts of ${input.nativeInstructionSources.join(', ')} should become durable Brainclaw memory, and which parts should remain local agent guidance only?`, 'Native agent files are derived context first; Brainclaw needs a selective import decision instead of silently promoting them.', 'medium', 'ide_chat', 'long_text', ['native instruction import boundary']);
|
|
877
|
+
}
|
|
878
|
+
if (questions.length === 0 && input.confidence !== 'high') {
|
|
879
|
+
add('What is still ambiguous enough that a future agent would likely ask you before proceeding?', 'Fallback question when bootstrap confidence is not high but no specific gap generated a dedicated prompt.', 'medium', 'any', 'long_text', ['confidence fallback']);
|
|
880
|
+
}
|
|
881
|
+
const summary = questions.length === 0
|
|
882
|
+
? 'No adaptive interview questions are needed right now.'
|
|
883
|
+
: `${questions.length} adaptive interview question(s) are ready to turn bootstrap gaps into confirmed shared memory.`;
|
|
884
|
+
return BootstrapInterviewPlanSchema.parse({
|
|
885
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
886
|
+
derived_at: nowISO(),
|
|
887
|
+
workspace_kind: input.workspaceKind,
|
|
888
|
+
audience: 'any',
|
|
889
|
+
summary,
|
|
890
|
+
question_count: questions.length,
|
|
891
|
+
questions,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function seedToBootstrapSuggestion(seed, allowSummaryFallback) {
|
|
895
|
+
if (seed.seed_kind !== 'agent_rule' && seed.seed_kind !== 'command') {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
if (seed.seed_kind === 'agent_rule' && isBootstrapSummarySeed(seed.text) && !allowSummaryFallback) {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
const target = inferBootstrapInstructionTarget(seed);
|
|
902
|
+
if (!target) {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
id: generateId('bootstrap_suggestions'),
|
|
907
|
+
target: 'instruction',
|
|
908
|
+
text: seed.text,
|
|
909
|
+
rationale: renderBootstrapSuggestionRationale(seed),
|
|
910
|
+
confidence: seed.confidence,
|
|
911
|
+
source_seed_ids: [seed.id],
|
|
912
|
+
source_refs: [seed.source_ref],
|
|
913
|
+
layer: target.layer,
|
|
914
|
+
scope: target.scope,
|
|
915
|
+
tags: normalizeBootstrapSuggestionTags(seed.tags),
|
|
916
|
+
related_paths: seed.related_paths,
|
|
917
|
+
reversible: true,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function inferBootstrapInstructionTarget(seed) {
|
|
921
|
+
if (seed.source_kind === 'agents_md') {
|
|
922
|
+
return { layer: 'global' };
|
|
923
|
+
}
|
|
924
|
+
if (seed.seed_kind === 'command') {
|
|
925
|
+
return { layer: 'global' };
|
|
926
|
+
}
|
|
927
|
+
if (seed.source_kind !== 'native_instruction') {
|
|
928
|
+
return { layer: 'global' };
|
|
929
|
+
}
|
|
930
|
+
const ref = seed.source_ref.replace(/\\/g, '/');
|
|
931
|
+
if (ref === 'CLAUDE.md')
|
|
932
|
+
return { layer: 'agent', scope: 'claude-code' };
|
|
933
|
+
if (ref === 'GEMINI.md')
|
|
934
|
+
return { layer: 'agent', scope: 'antigravity' };
|
|
935
|
+
if (ref === '.windsurfrules')
|
|
936
|
+
return { layer: 'agent', scope: 'windsurf' };
|
|
937
|
+
if (ref === '.github/copilot-instructions.md')
|
|
938
|
+
return { layer: 'agent', scope: 'github-copilot' };
|
|
939
|
+
if (ref.startsWith('.cursor/rules/'))
|
|
940
|
+
return { layer: 'agent', scope: 'cursor' };
|
|
941
|
+
if (ref.startsWith('.roo/rules/'))
|
|
942
|
+
return { layer: 'agent', scope: 'roo' };
|
|
943
|
+
if (ref.startsWith('.continue/rules/'))
|
|
944
|
+
return { layer: 'agent', scope: 'continue' };
|
|
945
|
+
if (ref.startsWith('.clinerules/'))
|
|
946
|
+
return { layer: 'agent', scope: 'cline' };
|
|
947
|
+
return { layer: 'global' };
|
|
948
|
+
}
|
|
949
|
+
function renderBootstrapSuggestionRationale(seed) {
|
|
950
|
+
switch (seed.source_kind) {
|
|
951
|
+
case 'agents_md':
|
|
952
|
+
return 'Derived from AGENTS.md';
|
|
953
|
+
case 'native_instruction':
|
|
954
|
+
return `Derived from native agent instruction file ${seed.source_ref}`;
|
|
955
|
+
case 'readme':
|
|
956
|
+
return `Derived from ${seed.source_ref}`;
|
|
957
|
+
case 'manifest':
|
|
958
|
+
return `Derived from ${seed.source_ref}`;
|
|
959
|
+
default:
|
|
960
|
+
return `Derived from ${seed.source_ref}`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function normalizeBootstrapSuggestionTags(tags) {
|
|
964
|
+
const normalized = tags.filter((tag) => tag !== 'bootstrap' && tag !== 'native-context');
|
|
965
|
+
normalized.push('bootstrap-import');
|
|
966
|
+
return [...new Set(normalized)];
|
|
967
|
+
}
|
|
968
|
+
function isBootstrapSummarySeed(text) {
|
|
969
|
+
return text.startsWith('Agent guide: ') || text.startsWith('Native agent guidance from ');
|
|
970
|
+
}
|
|
971
|
+
function suggestionIdentityKey(suggestion) {
|
|
972
|
+
return instructionIdentityKey(suggestion.text, suggestion.layer ?? 'global', suggestion.scope);
|
|
973
|
+
}
|
|
974
|
+
function instructionIdentityKey(text, layer, scope) {
|
|
975
|
+
return `${layer}:${scope ?? '*'}:${text.trim().toLowerCase()}`;
|
|
976
|
+
}
|
|
977
|
+
export function applyBootstrapImport(options = {}) {
|
|
978
|
+
const cwd = options.cwd ?? process.cwd();
|
|
979
|
+
const result = runBootstrapProfile(options);
|
|
980
|
+
const proposal = result.importPlan;
|
|
981
|
+
if (proposal.suggestions.length === 0) {
|
|
982
|
+
return {
|
|
983
|
+
proposal,
|
|
984
|
+
receipt: loadBootstrapApplication(cwd),
|
|
985
|
+
createdCount: 0,
|
|
986
|
+
skippedCount: 0,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const activeInstructionKeys = new Set(loadInstructions(cwd)
|
|
990
|
+
.filter((entry) => entry.active)
|
|
991
|
+
.map((entry) => instructionIdentityKey(entry.text, entry.layer, entry.scope)));
|
|
992
|
+
const managedArtifacts = [];
|
|
993
|
+
let createdCount = 0;
|
|
994
|
+
let skippedCount = 0;
|
|
995
|
+
withStoreLock(cwd, () => {
|
|
996
|
+
for (const suggestion of proposal.suggestions) {
|
|
997
|
+
if (suggestion.target !== 'instruction') {
|
|
998
|
+
skippedCount++;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const identityKey = suggestionIdentityKey(suggestion);
|
|
1002
|
+
if (activeInstructionKeys.has(identityKey)) {
|
|
1003
|
+
skippedCount++;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const entry = createInstruction(suggestion.text, {
|
|
1007
|
+
layer: suggestion.layer ?? 'global',
|
|
1008
|
+
scope: suggestion.scope,
|
|
1009
|
+
tags: suggestion.tags,
|
|
1010
|
+
author: resolveCurrentAgentName(cwd),
|
|
1011
|
+
}, cwd);
|
|
1012
|
+
activeInstructionKeys.add(identityKey);
|
|
1013
|
+
managedArtifacts.push({
|
|
1014
|
+
kind: 'instruction',
|
|
1015
|
+
id: entry.id,
|
|
1016
|
+
suggestion_id: suggestion.id,
|
|
1017
|
+
rollback_action: 'deactivate',
|
|
1018
|
+
});
|
|
1019
|
+
createdCount++;
|
|
1020
|
+
}
|
|
1021
|
+
if (createdCount > 0) {
|
|
1022
|
+
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd)));
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
const receipt = BootstrapApplicationReceiptSchema.parse({
|
|
1026
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
1027
|
+
applied_at: nowISO(),
|
|
1028
|
+
proposal_derived_at: proposal.derived_at,
|
|
1029
|
+
target: proposal.target,
|
|
1030
|
+
workspace_kind: proposal.workspace_kind,
|
|
1031
|
+
managed_artifacts: managedArtifacts,
|
|
1032
|
+
suggestion_ids: managedArtifacts.map((artifact) => artifact.suggestion_id),
|
|
1033
|
+
});
|
|
1034
|
+
saveVersionedJsonFile('bootstrap_application', bootstrapApplicationPath(cwd), receipt);
|
|
1035
|
+
return {
|
|
1036
|
+
proposal,
|
|
1037
|
+
receipt,
|
|
1038
|
+
createdCount,
|
|
1039
|
+
skippedCount,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
export function uninstallBootstrapImport(cwd) {
|
|
1043
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1044
|
+
const receipt = loadBootstrapApplication(resolvedCwd);
|
|
1045
|
+
if (!receipt || receipt.uninstalled_at) {
|
|
1046
|
+
return {
|
|
1047
|
+
receipt,
|
|
1048
|
+
deactivatedCount: 0,
|
|
1049
|
+
skippedCount: 0,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const instructions = loadInstructions(resolvedCwd);
|
|
1053
|
+
let deactivatedCount = 0;
|
|
1054
|
+
let skippedCount = 0;
|
|
1055
|
+
withStoreLock(resolvedCwd, () => {
|
|
1056
|
+
for (const artifact of receipt.managed_artifacts) {
|
|
1057
|
+
if (artifact.kind !== 'instruction') {
|
|
1058
|
+
skippedCount++;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const instruction = instructions.find((entry) => entry.id === artifact.id);
|
|
1062
|
+
if (!instruction || !instruction.active) {
|
|
1063
|
+
skippedCount++;
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
instruction.active = false;
|
|
1067
|
+
instruction.updated_at = nowISO();
|
|
1068
|
+
saveInstruction(instruction, resolvedCwd);
|
|
1069
|
+
deactivatedCount++;
|
|
1070
|
+
}
|
|
1071
|
+
if (deactivatedCount > 0) {
|
|
1072
|
+
writeFileAtomic(memoryPath('project.md', resolvedCwd), generateMarkdown(loadState(resolvedCwd)));
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
const nextReceipt = BootstrapApplicationReceiptSchema.parse({
|
|
1076
|
+
...receipt,
|
|
1077
|
+
uninstalled_at: nowISO(),
|
|
1078
|
+
});
|
|
1079
|
+
saveVersionedJsonFile('bootstrap_application', bootstrapApplicationPath(resolvedCwd), nextReceipt);
|
|
1080
|
+
return {
|
|
1081
|
+
receipt: nextReceipt,
|
|
1082
|
+
deactivatedCount,
|
|
1083
|
+
skippedCount,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function classifyWorkspace(cwd) {
|
|
1087
|
+
const visibleEntries = fs.readdirSync(cwd)
|
|
1088
|
+
.filter((entry) => !EMPTY_WORKSPACE_IGNORED.has(entry));
|
|
1089
|
+
return {
|
|
1090
|
+
kind: visibleEntries.length === 0 ? 'empty' : 'existing',
|
|
1091
|
+
visibleEntries,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function discoverNativeInstructionFiles(cwd) {
|
|
1095
|
+
const discovered = new Set();
|
|
1096
|
+
for (const relativePath of NATIVE_INSTRUCTION_FILES) {
|
|
1097
|
+
const filepath = path.join(cwd, relativePath);
|
|
1098
|
+
if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
|
|
1099
|
+
discovered.add(relativePath);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const relativeDir of NATIVE_INSTRUCTION_DIRS) {
|
|
1103
|
+
const dir = path.join(cwd, relativeDir);
|
|
1104
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
for (const entry of fs.readdirSync(dir).sort()) {
|
|
1108
|
+
if (!/\.(md|mdc)$/i.test(entry)) {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
discovered.add(path.posix.join(relativeDir.replace(/\\/g, '/'), entry));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return [...discovered].sort((left, right) => left.localeCompare(right));
|
|
1115
|
+
}
|
|
1116
|
+
function inferBootstrapConfidence(input) {
|
|
1117
|
+
if (input.workspaceKind === 'empty') {
|
|
1118
|
+
return input.seedCount > 3 ? 'medium' : 'low';
|
|
1119
|
+
}
|
|
1120
|
+
if (input.readmePresent && (input.agentsPresent || input.nativeInstructionFiles.length > 0) && input.seedCount >= 6) {
|
|
1121
|
+
return 'high';
|
|
1122
|
+
}
|
|
1123
|
+
if (input.readmePresent || input.nativeInstructionFiles.length > 0 || input.seedCount >= 4) {
|
|
1124
|
+
return 'medium';
|
|
1125
|
+
}
|
|
1126
|
+
return 'low';
|
|
1127
|
+
}
|
|
1128
|
+
function inferBootstrapGaps(input) {
|
|
1129
|
+
const gaps = [];
|
|
1130
|
+
if (input.workspaceKind === 'empty') {
|
|
1131
|
+
gaps.push('project intent is not documented yet');
|
|
1132
|
+
gaps.push('agent workflow expectations should be captured explicitly');
|
|
1133
|
+
return gaps;
|
|
1134
|
+
}
|
|
1135
|
+
if (!input.readmePresent) {
|
|
1136
|
+
gaps.push('no README-level project overview detected');
|
|
1137
|
+
}
|
|
1138
|
+
if (input.nativeInstructionFiles.length === 0) {
|
|
1139
|
+
gaps.push('no native agent instruction files detected');
|
|
1140
|
+
}
|
|
1141
|
+
if (input.seedCount < 4) {
|
|
1142
|
+
gaps.push('derived context is sparse and may need an interview');
|
|
1143
|
+
}
|
|
1144
|
+
return gaps;
|
|
1145
|
+
}
|
|
563
1146
|
function scoreSeed(seed, target) {
|
|
564
1147
|
let score = seedKindWeight(seed.seed_kind) + confidenceWeight(seed.confidence);
|
|
565
1148
|
if (!target) {
|