brainclaw 0.19.7 → 0.19.11
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 +9 -3
- package/dist/commands/bootstrap.js +88 -3
- package/dist/commands/init.js +21 -0
- package/dist/commands/mcp.js +76 -3
- package/dist/core/bootstrap.js +945 -6
- package/dist/core/migration.js +6 -2
- package/dist/core/schema.js +97 -0
- package/docs/cli.md +26 -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 +70 -24
- package/docs/integrations/overview.md +53 -40
- package/docs/mcp-schema-changelog.md +9 -0
- package/docs/product/positioning.md +17 -18
- package/docs/quickstart.md +102 -52
- package/package.json +7 -7
package/dist/core/bootstrap.js
CHANGED
|
@@ -1,37 +1,104 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { JsonStore } from './json-store.js';
|
|
5
|
-
import { generateId, nowISO } from './ids.js';
|
|
6
|
-
import { resolveEntityDir } from './io.js';
|
|
7
|
-
import { BootstrapProfileDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
6
|
+
import { generateId, generateIdWithLabel, nowISO } from './ids.js';
|
|
7
|
+
import { memoryPath, resolveEntityDir, withStoreLock, writeFileAtomic } from './io.js';
|
|
8
|
+
import { BootstrapApplicationReceiptSchema, BootstrapInterviewAnswerSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.js';
|
|
8
9
|
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
9
10
|
import { analyzeRepository } from './repo-analysis.js';
|
|
10
11
|
import { buildExecutionContext, compactExecutionContext } from './execution-context.js';
|
|
11
12
|
import { buildAgentToolingContext } from './agent-context.js';
|
|
13
|
+
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
14
|
+
import { resolveCurrentAgentName } from './agent-registry.js';
|
|
15
|
+
import { loadState, persistState } from './state.js';
|
|
16
|
+
import { generateMarkdown } from './markdown.js';
|
|
12
17
|
const README_CANDIDATES = ['README.md', 'README', 'README.txt', 'README.mdx'];
|
|
13
18
|
const DOC_HINTS = ['docs', 'doc'];
|
|
14
19
|
const MAKEFILE_NAME = 'Makefile';
|
|
15
20
|
const PROFILE_FILE = 'profile.json';
|
|
21
|
+
const IMPORT_PLAN_FILE = 'import-plan.json';
|
|
22
|
+
const APPLICATION_FILE = 'last-application.json';
|
|
16
23
|
const HOTSPOT_LIMIT = 3;
|
|
17
24
|
const DERIVED_SCHEMA_VERSION = 2;
|
|
25
|
+
const EMPTY_WORKSPACE_IGNORED = new Set([
|
|
26
|
+
'.git',
|
|
27
|
+
'.brainclaw',
|
|
28
|
+
'.gitignore',
|
|
29
|
+
'.gitattributes',
|
|
30
|
+
'.gitmodules',
|
|
31
|
+
'.DS_Store',
|
|
32
|
+
'Thumbs.db',
|
|
33
|
+
'internal-docs',
|
|
34
|
+
]);
|
|
35
|
+
const NATIVE_INSTRUCTION_FILES = [
|
|
36
|
+
'AGENTS.md',
|
|
37
|
+
'CLAUDE.md',
|
|
38
|
+
'GEMINI.md',
|
|
39
|
+
'.windsurfrules',
|
|
40
|
+
'.github/copilot-instructions.md',
|
|
41
|
+
];
|
|
42
|
+
const NATIVE_INSTRUCTION_DIRS = [
|
|
43
|
+
'.cursor/rules',
|
|
44
|
+
'.roo/rules',
|
|
45
|
+
'.continue/rules',
|
|
46
|
+
'.clinerules',
|
|
47
|
+
];
|
|
18
48
|
export function runBootstrapProfile(options = {}) {
|
|
19
49
|
const cwd = options.cwd ?? process.cwd();
|
|
20
50
|
const target = normalizeTarget(options.target);
|
|
51
|
+
const interviewAnswers = normalizeBootstrapInterviewAnswers(options.interviewAnswers);
|
|
21
52
|
const existing = loadBootstrapProfile(cwd);
|
|
53
|
+
const existingPlan = loadBootstrapImportPlan(cwd);
|
|
54
|
+
const lastApplication = loadBootstrapApplication(cwd);
|
|
22
55
|
const existingFingerprint = currentRepoFingerprint(cwd);
|
|
23
|
-
if (!options.refresh && existing && isProfileReusable(existing, target, existingFingerprint)) {
|
|
56
|
+
if (!options.refresh && existing && existingPlan && isProfileReusable(existing, target, existingFingerprint)) {
|
|
57
|
+
const seeds = listBootstrapSeeds(cwd);
|
|
58
|
+
const importPlan = interviewAnswers.length > 0
|
|
59
|
+
? buildBootstrapImportPlan({
|
|
60
|
+
cwd,
|
|
61
|
+
target,
|
|
62
|
+
workspaceKind: existing.workspace_kind ?? 'existing',
|
|
63
|
+
onboardingMode: existing.onboarding_mode ?? inferOnboardingMode({
|
|
64
|
+
workspaceKind: existing.workspace_kind ?? 'existing',
|
|
65
|
+
confidence: existing.confidence ?? 'low',
|
|
66
|
+
readmePresent: existing.sources_scanned.includes('README'),
|
|
67
|
+
nativeInstructionFiles: existing.native_instruction_files,
|
|
68
|
+
}),
|
|
69
|
+
confidence: existing.confidence ?? 'low',
|
|
70
|
+
gaps: existing.gaps,
|
|
71
|
+
seeds,
|
|
72
|
+
interviewAnswers,
|
|
73
|
+
})
|
|
74
|
+
: existingPlan;
|
|
24
75
|
return {
|
|
25
76
|
profile: existing,
|
|
26
|
-
seeds
|
|
77
|
+
seeds,
|
|
78
|
+
importPlan,
|
|
79
|
+
lastApplication,
|
|
27
80
|
reusedProfile: true,
|
|
28
81
|
};
|
|
29
82
|
}
|
|
30
83
|
const artifacts = buildBootstrapArtifacts({ cwd, target, repoFingerprint: existingFingerprint });
|
|
31
84
|
persistBootstrapArtifacts(artifacts, cwd);
|
|
85
|
+
const importPlan = interviewAnswers.length > 0
|
|
86
|
+
? buildBootstrapImportPlan({
|
|
87
|
+
cwd,
|
|
88
|
+
target,
|
|
89
|
+
workspaceKind: artifacts.profile.workspace_kind ?? 'existing',
|
|
90
|
+
onboardingMode: artifacts.profile.onboarding_mode ?? 'existing_sparse',
|
|
91
|
+
confidence: artifacts.profile.confidence ?? 'low',
|
|
92
|
+
gaps: artifacts.profile.gaps,
|
|
93
|
+
seeds: artifacts.seeds,
|
|
94
|
+
interviewAnswers,
|
|
95
|
+
})
|
|
96
|
+
: artifacts.importPlan;
|
|
32
97
|
return {
|
|
33
98
|
profile: artifacts.profile,
|
|
34
99
|
seeds: artifacts.seeds,
|
|
100
|
+
importPlan,
|
|
101
|
+
lastApplication,
|
|
35
102
|
reusedProfile: false,
|
|
36
103
|
};
|
|
37
104
|
}
|
|
@@ -82,8 +149,27 @@ export function selectDerivedSignals(target, maxSignals, cwd) {
|
|
|
82
149
|
}
|
|
83
150
|
export function renderBootstrapSummary(result) {
|
|
84
151
|
const lines = [result.profile.summary];
|
|
152
|
+
if (result.profile.workspace_kind) {
|
|
153
|
+
lines.push(`Workspace kind: ${result.profile.workspace_kind}`);
|
|
154
|
+
}
|
|
155
|
+
if (result.profile.onboarding_mode) {
|
|
156
|
+
lines.push(`Onboarding mode: ${result.profile.onboarding_mode}`);
|
|
157
|
+
}
|
|
158
|
+
if (result.profile.confidence) {
|
|
159
|
+
lines.push(`Confidence: ${result.profile.confidence}`);
|
|
160
|
+
}
|
|
85
161
|
lines.push(`Sources scanned: ${result.profile.sources_scanned.join(', ') || 'none'}`);
|
|
86
162
|
lines.push(`Seed count: ${result.profile.seed_count}`);
|
|
163
|
+
if ((result.profile.native_instruction_files?.length ?? 0) > 0) {
|
|
164
|
+
lines.push(`Native instruction files: ${result.profile.native_instruction_files.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
if ((result.profile.gaps?.length ?? 0) > 0) {
|
|
167
|
+
lines.push(`Open gaps: ${result.profile.gaps.join(' | ')}`);
|
|
168
|
+
}
|
|
169
|
+
lines.push(`Import suggestions: ${result.importPlan.suggestion_count}`);
|
|
170
|
+
if ((result.importPlan.confirmed_suggestion_count ?? 0) > 0) {
|
|
171
|
+
lines.push(`Confirmed by interview: ${result.importPlan.confirmed_suggestion_count}`);
|
|
172
|
+
}
|
|
87
173
|
if (result.profile.repo_fingerprint) {
|
|
88
174
|
lines.push(`Repo fingerprint: ${result.profile.repo_fingerprint}`);
|
|
89
175
|
}
|
|
@@ -93,6 +179,24 @@ export function renderBootstrapSummary(result) {
|
|
|
93
179
|
if (result.reusedProfile) {
|
|
94
180
|
lines.push('Reused existing bootstrap profile.');
|
|
95
181
|
}
|
|
182
|
+
if (result.lastApplication && !result.lastApplication.uninstalled_at) {
|
|
183
|
+
lines.push(`Last bootstrap import: ${result.lastApplication.managed_artifacts.length} managed artifact(s) from ${result.lastApplication.applied_at}`);
|
|
184
|
+
}
|
|
185
|
+
if (result.importPlan.suggestions.length > 0) {
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push('Import proposal:');
|
|
188
|
+
for (const suggestion of result.importPlan.suggestions.slice(0, 10)) {
|
|
189
|
+
const scope = suggestion.scope ? `:${suggestion.scope}` : '';
|
|
190
|
+
lines.push(`- [${suggestion.target}/${suggestion.confidence}] <${suggestion.layer ?? 'global'}${scope}> ${suggestion.text}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if ((result.importPlan.interview?.question_count ?? 0) > 0) {
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push('Adaptive interview:');
|
|
196
|
+
for (const question of result.importPlan.interview.questions.slice(0, 6)) {
|
|
197
|
+
lines.push(`- [${question.priority}/${question.audience}] [${question.id}] ${question.prompt}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
96
200
|
if (result.seeds.length > 0) {
|
|
97
201
|
lines.push('');
|
|
98
202
|
lines.push('Derived signals:');
|
|
@@ -102,9 +206,32 @@ export function renderBootstrapSummary(result) {
|
|
|
102
206
|
}
|
|
103
207
|
return lines.join('\n');
|
|
104
208
|
}
|
|
209
|
+
export function renderBootstrapInterview(result, audience = 'any') {
|
|
210
|
+
const interview = result.importPlan.interview;
|
|
211
|
+
if (!interview || interview.question_count === 0) {
|
|
212
|
+
return 'No adaptive interview questions are needed right now.';
|
|
213
|
+
}
|
|
214
|
+
const questions = interview.questions.filter((question) => audience === 'any' || question.audience === 'any' || question.audience === audience);
|
|
215
|
+
if (questions.length === 0) {
|
|
216
|
+
return `No adaptive interview questions are targeted to ${audience}.`;
|
|
217
|
+
}
|
|
218
|
+
const lines = [interview.summary, `Audience: ${audience}`];
|
|
219
|
+
lines.push('');
|
|
220
|
+
for (const [index, question] of questions.entries()) {
|
|
221
|
+
lines.push(`${index + 1}. [${question.id}] ${question.prompt}`);
|
|
222
|
+
lines.push(` Why: ${question.rationale}`);
|
|
223
|
+
lines.push(` Expected answer: ${question.response_kind}`);
|
|
224
|
+
if (question.target_hints.length > 0) {
|
|
225
|
+
lines.push(` Target hints: ${question.target_hints.join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
105
230
|
function buildBootstrapArtifacts(input) {
|
|
106
231
|
const sourcesScanned = [];
|
|
107
232
|
const seeds = [];
|
|
233
|
+
const workspace = classifyWorkspace(input.cwd);
|
|
234
|
+
const nativeInstructionFiles = discoverNativeInstructionFiles(input.cwd);
|
|
108
235
|
const readmePath = findFirstExisting(input.cwd, README_CANDIDATES);
|
|
109
236
|
if (readmePath) {
|
|
110
237
|
sourcesScanned.push('README');
|
|
@@ -116,6 +243,10 @@ function buildBootstrapArtifacts(input) {
|
|
|
116
243
|
sourcesScanned.push('AGENTS.md');
|
|
117
244
|
seeds.push(...extractAgentsSeeds(agentsPath, input.target));
|
|
118
245
|
}
|
|
246
|
+
if (nativeInstructionFiles.length > 0) {
|
|
247
|
+
sourcesScanned.push('native_instructions');
|
|
248
|
+
seeds.push(...extractNativeInstructionSeeds(nativeInstructionFiles.map((relativePath) => path.join(input.cwd, relativePath)), input.cwd, input.target));
|
|
249
|
+
}
|
|
119
250
|
const manifestResult = extractManifestSeeds(input.cwd, input.target);
|
|
120
251
|
if (manifestResult.seeds.length > 0) {
|
|
121
252
|
sourcesScanned.push(...manifestResult.sources);
|
|
@@ -142,12 +273,45 @@ function buildBootstrapArtifacts(input) {
|
|
|
142
273
|
seeds.push(...gitProbe.hotspotSeeds);
|
|
143
274
|
}
|
|
144
275
|
const uniqueSeeds = dedupeSeeds(seeds);
|
|
276
|
+
const confidence = inferBootstrapConfidence({
|
|
277
|
+
workspaceKind: workspace.kind,
|
|
278
|
+
readmePresent: Boolean(readmePath),
|
|
279
|
+
agentsPresent,
|
|
280
|
+
nativeInstructionFiles,
|
|
281
|
+
seedCount: uniqueSeeds.length,
|
|
282
|
+
});
|
|
283
|
+
const onboardingMode = inferOnboardingMode({
|
|
284
|
+
workspaceKind: workspace.kind,
|
|
285
|
+
readmePresent: Boolean(readmePath),
|
|
286
|
+
nativeInstructionFiles,
|
|
287
|
+
confidence,
|
|
288
|
+
});
|
|
289
|
+
const gaps = inferBootstrapGaps({
|
|
290
|
+
workspaceKind: workspace.kind,
|
|
291
|
+
readmePresent: Boolean(readmePath),
|
|
292
|
+
nativeInstructionFiles,
|
|
293
|
+
seedCount: uniqueSeeds.length,
|
|
294
|
+
});
|
|
145
295
|
const summary = buildSummary({
|
|
296
|
+
workspaceKind: workspace.kind,
|
|
146
297
|
agentsPresent,
|
|
298
|
+
nativeInstructionFiles,
|
|
147
299
|
gitAvailable: gitProbe.available,
|
|
148
300
|
repoAnalysis,
|
|
149
301
|
seeds: uniqueSeeds,
|
|
150
302
|
target: input.target,
|
|
303
|
+
onboardingMode,
|
|
304
|
+
confidence,
|
|
305
|
+
gaps,
|
|
306
|
+
});
|
|
307
|
+
const importPlan = buildBootstrapImportPlan({
|
|
308
|
+
cwd: input.cwd,
|
|
309
|
+
target: input.target,
|
|
310
|
+
workspaceKind: workspace.kind,
|
|
311
|
+
onboardingMode,
|
|
312
|
+
confidence,
|
|
313
|
+
gaps,
|
|
314
|
+
seeds: uniqueSeeds,
|
|
151
315
|
});
|
|
152
316
|
return {
|
|
153
317
|
profile: BootstrapProfileDocumentSchema.parse({
|
|
@@ -160,11 +324,17 @@ function buildBootstrapArtifacts(input) {
|
|
|
160
324
|
agents_md_present: agentsPresent,
|
|
161
325
|
seed_count: uniqueSeeds.length,
|
|
162
326
|
target: input.target,
|
|
327
|
+
workspace_kind: workspace.kind,
|
|
328
|
+
onboarding_mode: onboardingMode,
|
|
329
|
+
confidence,
|
|
330
|
+
native_instruction_files: nativeInstructionFiles,
|
|
331
|
+
gaps,
|
|
163
332
|
}),
|
|
164
333
|
seeds: uniqueSeeds.map((seed) => MemorySeedDocumentSchema.parse({
|
|
165
334
|
...seed,
|
|
166
335
|
schema_version: DERIVED_SCHEMA_VERSION,
|
|
167
336
|
})),
|
|
337
|
+
importPlan,
|
|
168
338
|
};
|
|
169
339
|
}
|
|
170
340
|
function extractReadmeSeeds(filepath, target) {
|
|
@@ -236,6 +406,80 @@ function extractAgentsSeeds(filepath, target) {
|
|
|
236
406
|
}
|
|
237
407
|
return seeds;
|
|
238
408
|
}
|
|
409
|
+
export function loadBootstrapImportPlan(cwd) {
|
|
410
|
+
const filepath = bootstrapImportPlanPath(cwd);
|
|
411
|
+
if (!fs.existsSync(filepath)) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
return loadVersionedJsonFile('bootstrap_import_plan', filepath).document;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export function loadBootstrapApplication(cwd) {
|
|
422
|
+
const filepath = bootstrapApplicationPath(cwd);
|
|
423
|
+
if (!fs.existsSync(filepath)) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
return loadVersionedJsonFile('bootstrap_application', filepath).document;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function extractNativeInstructionSeeds(filepaths, cwd, target) {
|
|
434
|
+
const seeds = [];
|
|
435
|
+
for (const filepath of filepaths) {
|
|
436
|
+
if (path.basename(filepath) === 'AGENTS.md') {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const relativePath = path.relative(cwd, filepath).replace(/\\/g, '/');
|
|
440
|
+
const raw = fs.readFileSync(filepath, 'utf-8');
|
|
441
|
+
const lines = raw.split(/\r?\n/);
|
|
442
|
+
const heading = lines.find((line) => line.trim().startsWith('#'));
|
|
443
|
+
const firstContent = lines.find((line) => line.trim().length > 0);
|
|
444
|
+
const label = heading
|
|
445
|
+
? heading.replace(/^#+\s*/, '').trim()
|
|
446
|
+
: firstContent?.trim().replace(/^[-*]\s+/, '') ?? relativePath;
|
|
447
|
+
seeds.push(createSeed({
|
|
448
|
+
text: `Native agent guidance from ${path.basename(relativePath)}: ${label}`,
|
|
449
|
+
seedKind: 'agent_rule',
|
|
450
|
+
sourceKind: 'native_instruction',
|
|
451
|
+
sourceRef: relativePath,
|
|
452
|
+
confidence: 'high',
|
|
453
|
+
tags: ['bootstrap', 'agent', 'native-context'],
|
|
454
|
+
relatedPaths: target ? [target] : undefined,
|
|
455
|
+
}));
|
|
456
|
+
let extracted = 0;
|
|
457
|
+
for (const line of lines) {
|
|
458
|
+
const trimmed = line.trim();
|
|
459
|
+
if (!/^([-*]|\d+\.)\s+/.test(trimmed)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const text = trimmed.replace(/^([-*]|\d+\.)\s+/, '').trim();
|
|
463
|
+
if (!text) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
seeds.push(createSeed({
|
|
467
|
+
text,
|
|
468
|
+
seedKind: 'agent_rule',
|
|
469
|
+
sourceKind: 'native_instruction',
|
|
470
|
+
sourceRef: relativePath,
|
|
471
|
+
confidence: 'medium',
|
|
472
|
+
tags: ['bootstrap', 'agent', 'native-context'],
|
|
473
|
+
relatedPaths: target ? [target] : undefined,
|
|
474
|
+
}));
|
|
475
|
+
extracted++;
|
|
476
|
+
if (extracted >= 5) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return seeds;
|
|
482
|
+
}
|
|
239
483
|
function extractManifestSeeds(cwd, target) {
|
|
240
484
|
const sources = [];
|
|
241
485
|
const seeds = [];
|
|
@@ -468,6 +712,7 @@ function probeGit(cwd, target) {
|
|
|
468
712
|
function persistBootstrapArtifacts(artifacts, cwd) {
|
|
469
713
|
ensureBootstrapDirs(cwd);
|
|
470
714
|
saveVersionedJsonFile('bootstrap_profile', bootstrapProfilePath(cwd), artifacts.profile);
|
|
715
|
+
saveVersionedJsonFile('bootstrap_import_plan', bootstrapImportPlanPath(cwd), artifacts.importPlan);
|
|
471
716
|
const store = bootstrapSeedStore(cwd);
|
|
472
717
|
const existingIds = new Set(store.list().map((seed) => seed.id));
|
|
473
718
|
for (const seed of artifacts.seeds) {
|
|
@@ -502,6 +747,12 @@ function bootstrapSeedsDir(cwd) {
|
|
|
502
747
|
function bootstrapProfilePath(cwd) {
|
|
503
748
|
return path.join(bootstrapDir(cwd), PROFILE_FILE);
|
|
504
749
|
}
|
|
750
|
+
function bootstrapImportPlanPath(cwd) {
|
|
751
|
+
return path.join(bootstrapDir(cwd), IMPORT_PLAN_FILE);
|
|
752
|
+
}
|
|
753
|
+
function bootstrapApplicationPath(cwd) {
|
|
754
|
+
return path.join(bootstrapDir(cwd), APPLICATION_FILE);
|
|
755
|
+
}
|
|
505
756
|
function isProfileReusable(profile, target, currentFingerprint) {
|
|
506
757
|
if ((profile.target ?? undefined) !== target) {
|
|
507
758
|
return false;
|
|
@@ -546,11 +797,16 @@ function dedupeSeeds(seeds) {
|
|
|
546
797
|
}
|
|
547
798
|
function buildSummary(input) {
|
|
548
799
|
const parts = [];
|
|
549
|
-
parts.push(`Bootstrap summary${input.target ? ` for ${input.target}` : ''}: ${input.seeds.length} derived signal(s).`);
|
|
800
|
+
parts.push(`Bootstrap summary${input.target ? ` for ${input.target}` : ''}: ${input.workspaceKind} workspace, ${input.seeds.length} derived signal(s).`);
|
|
801
|
+
parts.push(`Onboarding mode: ${input.onboardingMode}.`);
|
|
802
|
+
parts.push(`Confidence: ${input.confidence}.`);
|
|
550
803
|
parts.push(`Repository mode looks ${input.repoAnalysis.recommendedMode}.`);
|
|
551
804
|
if (input.agentsPresent) {
|
|
552
805
|
parts.push('AGENTS.md detected and summarized.');
|
|
553
806
|
}
|
|
807
|
+
if (input.nativeInstructionFiles.length > 0) {
|
|
808
|
+
parts.push(`${input.nativeInstructionFiles.length} native instruction file(s) detected.`);
|
|
809
|
+
}
|
|
554
810
|
if (input.gitAvailable) {
|
|
555
811
|
parts.push('Git history available for hotspot detection.');
|
|
556
812
|
}
|
|
@@ -558,8 +814,691 @@ function buildSummary(input) {
|
|
|
558
814
|
if (commandCount > 0) {
|
|
559
815
|
parts.push(`${commandCount} command-oriented hint(s) found.`);
|
|
560
816
|
}
|
|
817
|
+
if (input.gaps.length > 0) {
|
|
818
|
+
parts.push(`Needs follow-up on: ${input.gaps.join('; ')}.`);
|
|
819
|
+
}
|
|
561
820
|
return parts.join(' ');
|
|
562
821
|
}
|
|
822
|
+
function buildBootstrapImportPlan(input) {
|
|
823
|
+
const activeInstructionKeys = new Set(loadInstructions(input.cwd)
|
|
824
|
+
.filter((entry) => entry.active)
|
|
825
|
+
.map((entry) => instructionIdentityKey(entry.text, entry.layer, entry.scope)));
|
|
826
|
+
const suggestions = [];
|
|
827
|
+
const seenSuggestionKeys = new Set();
|
|
828
|
+
const importedSources = new Set();
|
|
829
|
+
const groupedBySource = new Map();
|
|
830
|
+
for (const seed of input.seeds) {
|
|
831
|
+
const bucket = groupedBySource.get(seed.source_ref) ?? [];
|
|
832
|
+
bucket.push(seed);
|
|
833
|
+
groupedBySource.set(seed.source_ref, bucket);
|
|
834
|
+
}
|
|
835
|
+
for (const seed of input.seeds) {
|
|
836
|
+
const suggestion = seedToBootstrapSuggestion(seed, false);
|
|
837
|
+
if (!suggestion) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const suggestionKey = genericSuggestionIdentityKey(suggestion);
|
|
841
|
+
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
seenSuggestionKeys.add(suggestionKey);
|
|
845
|
+
importedSources.add(seed.source_ref);
|
|
846
|
+
suggestions.push(BootstrapSuggestionDocumentSchema.parse({
|
|
847
|
+
...suggestion,
|
|
848
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
851
|
+
for (const [sourceRef, seeds] of groupedBySource.entries()) {
|
|
852
|
+
if (importedSources.has(sourceRef)) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const summarySeed = seeds.find((seed) => isBootstrapSummarySeed(seed.text));
|
|
856
|
+
if (!summarySeed) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const suggestion = seedToBootstrapSuggestion(summarySeed, true);
|
|
860
|
+
if (!suggestion) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const suggestionKey = genericSuggestionIdentityKey(suggestion);
|
|
864
|
+
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
seenSuggestionKeys.add(suggestionKey);
|
|
868
|
+
suggestions.push(BootstrapSuggestionDocumentSchema.parse({
|
|
869
|
+
...suggestion,
|
|
870
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
const interview = buildBootstrapInterviewPlan({
|
|
874
|
+
workspaceKind: input.workspaceKind,
|
|
875
|
+
gaps: input.gaps,
|
|
876
|
+
confidence: input.confidence,
|
|
877
|
+
nativeInstructionSources: [...new Set(input.seeds
|
|
878
|
+
.filter((seed) => seed.source_kind === 'native_instruction')
|
|
879
|
+
.map((seed) => seed.source_ref))],
|
|
880
|
+
});
|
|
881
|
+
const interviewSuggestions = buildBootstrapInterviewSuggestions(interview, input.interviewAnswers ?? []);
|
|
882
|
+
let confirmedSuggestionCount = 0;
|
|
883
|
+
for (const suggestion of interviewSuggestions) {
|
|
884
|
+
const suggestionKey = genericSuggestionIdentityKey(suggestion);
|
|
885
|
+
if ((suggestion.target === 'instruction' && activeInstructionKeys.has(suggestionKey)) || seenSuggestionKeys.has(suggestionKey)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
seenSuggestionKeys.add(suggestionKey);
|
|
889
|
+
confirmedSuggestionCount++;
|
|
890
|
+
suggestions.push(BootstrapSuggestionDocumentSchema.parse({
|
|
891
|
+
...suggestion,
|
|
892
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
const summary = suggestions.length === 0
|
|
896
|
+
? 'No safe bootstrap imports are ready yet; review the gaps and use an interview/import step before promoting derived context.'
|
|
897
|
+
: `${suggestions.length} bootstrap suggestion(s) are ready to import after review${confirmedSuggestionCount > 0 ? `, including ${confirmedSuggestionCount} confirmed via interview` : ''}.`;
|
|
898
|
+
return BootstrapImportPlanDocumentSchema.parse({
|
|
899
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
900
|
+
derived_at: nowISO(),
|
|
901
|
+
target: input.target,
|
|
902
|
+
workspace_kind: input.workspaceKind,
|
|
903
|
+
onboarding_mode: input.onboardingMode,
|
|
904
|
+
confidence: input.confidence,
|
|
905
|
+
summary,
|
|
906
|
+
requires_confirmation: true,
|
|
907
|
+
gaps: input.gaps,
|
|
908
|
+
confirmed_suggestion_count: confirmedSuggestionCount,
|
|
909
|
+
interview_answer_count: input.interviewAnswers?.length ?? 0,
|
|
910
|
+
suggestion_count: suggestions.length,
|
|
911
|
+
suggestions,
|
|
912
|
+
interview,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function buildBootstrapInterviewPlan(input) {
|
|
916
|
+
const questions = [];
|
|
917
|
+
const add = (prompt, rationale, priority, audience, responseKind, gapKeys, targetHints) => {
|
|
918
|
+
questions.push(BootstrapInterviewQuestionSchema.parse({
|
|
919
|
+
id: bootstrapInterviewQuestionId(prompt, audience),
|
|
920
|
+
prompt,
|
|
921
|
+
rationale,
|
|
922
|
+
priority,
|
|
923
|
+
audience,
|
|
924
|
+
response_kind: responseKind,
|
|
925
|
+
gap_keys: gapKeys,
|
|
926
|
+
target_hints: targetHints,
|
|
927
|
+
}));
|
|
928
|
+
};
|
|
929
|
+
if (input.workspaceKind === 'empty') {
|
|
930
|
+
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'], ['decision']);
|
|
931
|
+
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'], ['constraint', 'instruction']);
|
|
932
|
+
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'], ['instruction']);
|
|
933
|
+
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'], ['instruction']);
|
|
934
|
+
}
|
|
935
|
+
if (input.gaps.includes('no README-level project overview detected')) {
|
|
936
|
+
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'], ['decision']);
|
|
937
|
+
}
|
|
938
|
+
if (input.gaps.includes('no native agent instruction files detected')) {
|
|
939
|
+
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'], ['decision']);
|
|
940
|
+
}
|
|
941
|
+
if (input.gaps.includes('derived context is sparse and may need an interview')) {
|
|
942
|
+
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'], ['constraint']);
|
|
943
|
+
}
|
|
944
|
+
if (input.nativeInstructionSources.length > 0) {
|
|
945
|
+
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'], ['decision', 'instruction', 'constraint']);
|
|
946
|
+
}
|
|
947
|
+
if (questions.length === 0 && input.confidence !== 'high') {
|
|
948
|
+
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'], ['instruction', 'constraint']);
|
|
949
|
+
}
|
|
950
|
+
const summary = questions.length === 0
|
|
951
|
+
? 'No adaptive interview questions are needed right now.'
|
|
952
|
+
: `${questions.length} adaptive interview question(s) are ready to turn bootstrap gaps into confirmed shared memory.`;
|
|
953
|
+
return BootstrapInterviewPlanSchema.parse({
|
|
954
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
955
|
+
derived_at: nowISO(),
|
|
956
|
+
workspace_kind: input.workspaceKind,
|
|
957
|
+
audience: 'any',
|
|
958
|
+
summary,
|
|
959
|
+
question_count: questions.length,
|
|
960
|
+
questions,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function bootstrapInterviewQuestionId(prompt, audience) {
|
|
964
|
+
const digest = crypto.createHash('sha1').update(`${audience}:${prompt}`).digest('hex').slice(0, 8);
|
|
965
|
+
return `biq_${digest}`;
|
|
966
|
+
}
|
|
967
|
+
function normalizeBootstrapInterviewAnswers(answers) {
|
|
968
|
+
if (!answers || answers.length === 0) {
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
return answers.map((answer) => BootstrapInterviewAnswerSchema.parse(answer));
|
|
972
|
+
}
|
|
973
|
+
function buildBootstrapInterviewSuggestions(interview, answers) {
|
|
974
|
+
if (!interview || answers.length === 0) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
const questionsById = new Map(interview.questions.map((question) => [question.id, question]));
|
|
978
|
+
const suggestions = [];
|
|
979
|
+
for (const answer of answers) {
|
|
980
|
+
const question = questionsById.get(answer.question_id);
|
|
981
|
+
if (!question) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (answer.suggestions.length > 0) {
|
|
985
|
+
for (const suggestion of answer.suggestions) {
|
|
986
|
+
suggestions.push({
|
|
987
|
+
id: generateId('bootstrap_suggestions'),
|
|
988
|
+
target: suggestion.target,
|
|
989
|
+
text: suggestion.text.trim(),
|
|
990
|
+
rationale: suggestion.rationale ?? renderBootstrapInterviewRationale(question),
|
|
991
|
+
confidence: suggestion.confidence ?? 'high',
|
|
992
|
+
source_seed_ids: [],
|
|
993
|
+
source_refs: [`bootstrap-interview:${question.id}`],
|
|
994
|
+
layer: suggestion.layer,
|
|
995
|
+
scope: suggestion.scope,
|
|
996
|
+
tags: normalizeBootstrapSuggestionTags(['bootstrap-interview', ...suggestion.tags]),
|
|
997
|
+
related_paths: suggestion.related_paths,
|
|
998
|
+
category: suggestion.category,
|
|
999
|
+
outcome: suggestion.outcome,
|
|
1000
|
+
severity: suggestion.severity,
|
|
1001
|
+
reversible: true,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
suggestions.push(...deriveBootstrapSuggestionsFromAnswer(question, answer));
|
|
1007
|
+
}
|
|
1008
|
+
return suggestions.filter((suggestion) => suggestion.text.trim().length > 0);
|
|
1009
|
+
}
|
|
1010
|
+
function deriveBootstrapSuggestionsFromAnswer(question, answer) {
|
|
1011
|
+
const itemTexts = answer.response_items.map((entry) => entry.trim()).filter(Boolean);
|
|
1012
|
+
const text = answer.response_text?.trim();
|
|
1013
|
+
const base = {
|
|
1014
|
+
id: generateId('bootstrap_suggestions'),
|
|
1015
|
+
rationale: renderBootstrapInterviewRationale(question),
|
|
1016
|
+
confidence: 'high',
|
|
1017
|
+
source_seed_ids: [],
|
|
1018
|
+
source_refs: [`bootstrap-interview:${question.id}`],
|
|
1019
|
+
tags: normalizeBootstrapSuggestionTags(['bootstrap-interview']),
|
|
1020
|
+
related_paths: undefined,
|
|
1021
|
+
reversible: true,
|
|
1022
|
+
};
|
|
1023
|
+
if (question.gap_keys.includes('project intent is not documented yet') && text) {
|
|
1024
|
+
return [{ ...base, target: 'decision', text: `Project intent: ${text}` }];
|
|
1025
|
+
}
|
|
1026
|
+
if (question.gap_keys.includes('no README-level project overview detected') && text) {
|
|
1027
|
+
return [{ ...base, target: 'decision', text: `Project overview: ${text}` }];
|
|
1028
|
+
}
|
|
1029
|
+
if (question.prompt.startsWith('Which coding agents do you expect to use here') && (itemTexts.length > 0 || text)) {
|
|
1030
|
+
const values = itemTexts.length > 0 ? itemTexts : [text];
|
|
1031
|
+
return values.map((entry) => ({
|
|
1032
|
+
...base,
|
|
1033
|
+
id: generateId('bootstrap_suggestions'),
|
|
1034
|
+
target: 'constraint',
|
|
1035
|
+
text: `Agent workflow expectation: ${entry}`,
|
|
1036
|
+
category: 'process',
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
if (question.prompt.startsWith('For a CLI-only agent') && text) {
|
|
1040
|
+
return [{ ...base, target: 'instruction', text: `CLI-only agents should first: ${text}`, layer: 'global' }];
|
|
1041
|
+
}
|
|
1042
|
+
if (question.prompt.startsWith('For an IDE chat agent') && text) {
|
|
1043
|
+
return [{ ...base, target: 'instruction', text: `IDE chat agents should ask or inspect: ${text}`, layer: 'global' }];
|
|
1044
|
+
}
|
|
1045
|
+
if (question.gap_keys.includes('no native agent instruction files detected') && answer.response_boolean !== undefined) {
|
|
1046
|
+
return [{
|
|
1047
|
+
...base,
|
|
1048
|
+
target: 'decision',
|
|
1049
|
+
text: answer.response_boolean
|
|
1050
|
+
? 'Brainclaw should treat this project as agent-guided.'
|
|
1051
|
+
: 'Brainclaw should stay memory-only until agent guidance is clarified.',
|
|
1052
|
+
}];
|
|
1053
|
+
}
|
|
1054
|
+
if (question.gap_keys.includes('derived context is sparse and may need an interview') && (itemTexts.length > 0 || text)) {
|
|
1055
|
+
const values = itemTexts.length > 0 ? itemTexts : [text];
|
|
1056
|
+
return values.map((entry) => ({
|
|
1057
|
+
...base,
|
|
1058
|
+
id: generateId('bootstrap_suggestions'),
|
|
1059
|
+
target: 'constraint',
|
|
1060
|
+
text: entry,
|
|
1061
|
+
category: 'process',
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
if (question.gap_keys.includes('native instruction import boundary') && text) {
|
|
1065
|
+
return [{ ...base, target: 'decision', text: `Native instruction import boundary: ${text}` }];
|
|
1066
|
+
}
|
|
1067
|
+
if (question.gap_keys.includes('confidence fallback') && text) {
|
|
1068
|
+
return [{
|
|
1069
|
+
...base,
|
|
1070
|
+
target: question.target_hints.includes('constraint') ? 'constraint' : 'instruction',
|
|
1071
|
+
text,
|
|
1072
|
+
category: question.target_hints.includes('constraint') ? 'process' : undefined,
|
|
1073
|
+
layer: question.target_hints.includes('instruction') ? 'global' : undefined,
|
|
1074
|
+
}];
|
|
1075
|
+
}
|
|
1076
|
+
if (itemTexts.length > 0 && question.target_hints.includes('constraint')) {
|
|
1077
|
+
return itemTexts.map((entry) => ({
|
|
1078
|
+
...base,
|
|
1079
|
+
id: generateId('bootstrap_suggestions'),
|
|
1080
|
+
target: 'constraint',
|
|
1081
|
+
text: entry,
|
|
1082
|
+
category: 'process',
|
|
1083
|
+
}));
|
|
1084
|
+
}
|
|
1085
|
+
if (text && question.target_hints.includes('instruction')) {
|
|
1086
|
+
return [{ ...base, target: 'instruction', text, layer: 'global' }];
|
|
1087
|
+
}
|
|
1088
|
+
if (text && question.target_hints.includes('decision')) {
|
|
1089
|
+
return [{ ...base, target: 'decision', text }];
|
|
1090
|
+
}
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
function renderBootstrapInterviewRationale(question) {
|
|
1094
|
+
return `Confirmed via bootstrap interview answer to ${question.id}: ${question.prompt}`;
|
|
1095
|
+
}
|
|
1096
|
+
function seedToBootstrapSuggestion(seed, allowSummaryFallback) {
|
|
1097
|
+
if (seed.seed_kind !== 'agent_rule' && seed.seed_kind !== 'command') {
|
|
1098
|
+
return undefined;
|
|
1099
|
+
}
|
|
1100
|
+
if (seed.seed_kind === 'agent_rule' && isBootstrapSummarySeed(seed.text) && !allowSummaryFallback) {
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
const target = inferBootstrapInstructionTarget(seed);
|
|
1104
|
+
if (!target) {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
id: generateId('bootstrap_suggestions'),
|
|
1109
|
+
target: 'instruction',
|
|
1110
|
+
text: seed.text,
|
|
1111
|
+
rationale: renderBootstrapSuggestionRationale(seed),
|
|
1112
|
+
confidence: seed.confidence,
|
|
1113
|
+
source_seed_ids: [seed.id],
|
|
1114
|
+
source_refs: [seed.source_ref],
|
|
1115
|
+
layer: target.layer,
|
|
1116
|
+
scope: target.scope,
|
|
1117
|
+
tags: normalizeBootstrapSuggestionTags(seed.tags),
|
|
1118
|
+
related_paths: seed.related_paths,
|
|
1119
|
+
reversible: true,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function inferBootstrapInstructionTarget(seed) {
|
|
1123
|
+
if (seed.source_kind === 'agents_md') {
|
|
1124
|
+
return { layer: 'global' };
|
|
1125
|
+
}
|
|
1126
|
+
if (seed.seed_kind === 'command') {
|
|
1127
|
+
return { layer: 'global' };
|
|
1128
|
+
}
|
|
1129
|
+
if (seed.source_kind !== 'native_instruction') {
|
|
1130
|
+
return { layer: 'global' };
|
|
1131
|
+
}
|
|
1132
|
+
const ref = seed.source_ref.replace(/\\/g, '/');
|
|
1133
|
+
if (ref === 'CLAUDE.md')
|
|
1134
|
+
return { layer: 'agent', scope: 'claude-code' };
|
|
1135
|
+
if (ref === 'GEMINI.md')
|
|
1136
|
+
return { layer: 'agent', scope: 'antigravity' };
|
|
1137
|
+
if (ref === '.windsurfrules')
|
|
1138
|
+
return { layer: 'agent', scope: 'windsurf' };
|
|
1139
|
+
if (ref === '.github/copilot-instructions.md')
|
|
1140
|
+
return { layer: 'agent', scope: 'github-copilot' };
|
|
1141
|
+
if (ref.startsWith('.cursor/rules/'))
|
|
1142
|
+
return { layer: 'agent', scope: 'cursor' };
|
|
1143
|
+
if (ref.startsWith('.roo/rules/'))
|
|
1144
|
+
return { layer: 'agent', scope: 'roo' };
|
|
1145
|
+
if (ref.startsWith('.continue/rules/'))
|
|
1146
|
+
return { layer: 'agent', scope: 'continue' };
|
|
1147
|
+
if (ref.startsWith('.clinerules/'))
|
|
1148
|
+
return { layer: 'agent', scope: 'cline' };
|
|
1149
|
+
return { layer: 'global' };
|
|
1150
|
+
}
|
|
1151
|
+
function renderBootstrapSuggestionRationale(seed) {
|
|
1152
|
+
switch (seed.source_kind) {
|
|
1153
|
+
case 'agents_md':
|
|
1154
|
+
return 'Derived from AGENTS.md';
|
|
1155
|
+
case 'native_instruction':
|
|
1156
|
+
return `Derived from native agent instruction file ${seed.source_ref}`;
|
|
1157
|
+
case 'readme':
|
|
1158
|
+
return `Derived from ${seed.source_ref}`;
|
|
1159
|
+
case 'manifest':
|
|
1160
|
+
return `Derived from ${seed.source_ref}`;
|
|
1161
|
+
default:
|
|
1162
|
+
return `Derived from ${seed.source_ref}`;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function normalizeBootstrapSuggestionTags(tags) {
|
|
1166
|
+
const normalized = tags.filter((tag) => tag !== 'bootstrap' && tag !== 'native-context');
|
|
1167
|
+
normalized.push('bootstrap-import');
|
|
1168
|
+
return [...new Set(normalized)];
|
|
1169
|
+
}
|
|
1170
|
+
function isBootstrapSummarySeed(text) {
|
|
1171
|
+
return text.startsWith('Agent guide: ') || text.startsWith('Native agent guidance from ');
|
|
1172
|
+
}
|
|
1173
|
+
function genericSuggestionIdentityKey(suggestion) {
|
|
1174
|
+
if (suggestion.target === 'instruction') {
|
|
1175
|
+
return instructionIdentityKey(suggestion.text, suggestion.layer ?? 'global', suggestion.scope);
|
|
1176
|
+
}
|
|
1177
|
+
if (suggestion.target === 'trap') {
|
|
1178
|
+
return `trap:${suggestion.severity ?? 'medium'}:${suggestion.text.trim().toLowerCase()}`;
|
|
1179
|
+
}
|
|
1180
|
+
return `${suggestion.target}:${suggestion.text.trim().toLowerCase()}`;
|
|
1181
|
+
}
|
|
1182
|
+
function instructionIdentityKey(text, layer, scope) {
|
|
1183
|
+
return `${layer}:${scope ?? '*'}:${text.trim().toLowerCase()}`;
|
|
1184
|
+
}
|
|
1185
|
+
export function applyBootstrapImport(options = {}) {
|
|
1186
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1187
|
+
const result = runBootstrapProfile(options);
|
|
1188
|
+
const proposal = result.importPlan;
|
|
1189
|
+
if (proposal.suggestions.length === 0) {
|
|
1190
|
+
return {
|
|
1191
|
+
proposal,
|
|
1192
|
+
receipt: loadBootstrapApplication(cwd),
|
|
1193
|
+
createdCount: 0,
|
|
1194
|
+
skippedCount: 0,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
const managedArtifacts = [];
|
|
1198
|
+
let createdCount = 0;
|
|
1199
|
+
let skippedCount = 0;
|
|
1200
|
+
withStoreLock(cwd, () => {
|
|
1201
|
+
const state = loadState(cwd);
|
|
1202
|
+
const activeInstructionKeys = new Set(loadInstructions(cwd)
|
|
1203
|
+
.filter((entry) => entry.active)
|
|
1204
|
+
.map((entry) => instructionIdentityKey(entry.text, entry.layer, entry.scope)));
|
|
1205
|
+
const activeDecisionKeys = new Set(state.recent_decisions.map((entry) => genericSuggestionIdentityKey({
|
|
1206
|
+
target: 'decision',
|
|
1207
|
+
text: entry.text,
|
|
1208
|
+
layer: 'global',
|
|
1209
|
+
scope: undefined,
|
|
1210
|
+
severity: undefined,
|
|
1211
|
+
})));
|
|
1212
|
+
const activeConstraintKeys = new Set(state.active_constraints
|
|
1213
|
+
.filter((entry) => entry.status === 'active')
|
|
1214
|
+
.map((entry) => genericSuggestionIdentityKey({
|
|
1215
|
+
target: 'constraint',
|
|
1216
|
+
text: entry.text,
|
|
1217
|
+
layer: 'global',
|
|
1218
|
+
scope: undefined,
|
|
1219
|
+
severity: undefined,
|
|
1220
|
+
})));
|
|
1221
|
+
const activeTrapKeys = new Set(state.known_traps
|
|
1222
|
+
.filter((entry) => entry.status === 'active')
|
|
1223
|
+
.map((entry) => genericSuggestionIdentityKey({
|
|
1224
|
+
target: 'trap',
|
|
1225
|
+
text: entry.text,
|
|
1226
|
+
layer: 'global',
|
|
1227
|
+
scope: undefined,
|
|
1228
|
+
severity: entry.severity,
|
|
1229
|
+
})));
|
|
1230
|
+
let stateChanged = false;
|
|
1231
|
+
for (const suggestion of proposal.suggestions) {
|
|
1232
|
+
const identityKey = genericSuggestionIdentityKey(suggestion);
|
|
1233
|
+
if ((suggestion.target === 'instruction' && activeInstructionKeys.has(identityKey)) ||
|
|
1234
|
+
(suggestion.target === 'decision' && activeDecisionKeys.has(identityKey)) ||
|
|
1235
|
+
(suggestion.target === 'constraint' && activeConstraintKeys.has(identityKey)) ||
|
|
1236
|
+
(suggestion.target === 'trap' && activeTrapKeys.has(identityKey))) {
|
|
1237
|
+
skippedCount++;
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
switch (suggestion.target) {
|
|
1241
|
+
case 'instruction': {
|
|
1242
|
+
const entry = createInstruction(suggestion.text, {
|
|
1243
|
+
layer: suggestion.layer ?? 'global',
|
|
1244
|
+
scope: suggestion.scope,
|
|
1245
|
+
tags: suggestion.tags,
|
|
1246
|
+
author: resolveCurrentAgentName(cwd),
|
|
1247
|
+
}, cwd);
|
|
1248
|
+
activeInstructionKeys.add(identityKey);
|
|
1249
|
+
managedArtifacts.push({
|
|
1250
|
+
kind: 'instruction',
|
|
1251
|
+
id: entry.id,
|
|
1252
|
+
suggestion_id: suggestion.id,
|
|
1253
|
+
rollback_action: 'deactivate',
|
|
1254
|
+
});
|
|
1255
|
+
createdCount++;
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
case 'decision': {
|
|
1259
|
+
const { id, short_label } = generateIdWithLabel('recent_decisions', cwd);
|
|
1260
|
+
const entry = {
|
|
1261
|
+
id,
|
|
1262
|
+
short_label,
|
|
1263
|
+
text: suggestion.text,
|
|
1264
|
+
created_at: nowISO(),
|
|
1265
|
+
author: resolveCurrentAgentName(cwd),
|
|
1266
|
+
outcome: suggestion.outcome,
|
|
1267
|
+
tags: suggestion.tags ?? [],
|
|
1268
|
+
related_paths: suggestion.related_paths,
|
|
1269
|
+
};
|
|
1270
|
+
state.recent_decisions.push(entry);
|
|
1271
|
+
activeDecisionKeys.add(identityKey);
|
|
1272
|
+
managedArtifacts.push({
|
|
1273
|
+
kind: 'decision',
|
|
1274
|
+
id: entry.id,
|
|
1275
|
+
suggestion_id: suggestion.id,
|
|
1276
|
+
rollback_action: 'delete',
|
|
1277
|
+
});
|
|
1278
|
+
stateChanged = true;
|
|
1279
|
+
createdCount++;
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case 'constraint': {
|
|
1283
|
+
const { id, short_label } = generateIdWithLabel('active_constraints', cwd);
|
|
1284
|
+
const entry = {
|
|
1285
|
+
id,
|
|
1286
|
+
short_label,
|
|
1287
|
+
text: suggestion.text,
|
|
1288
|
+
created_at: nowISO(),
|
|
1289
|
+
author: resolveCurrentAgentName(cwd),
|
|
1290
|
+
status: 'active',
|
|
1291
|
+
category: suggestion.category,
|
|
1292
|
+
tags: suggestion.tags ?? [],
|
|
1293
|
+
related_paths: suggestion.related_paths,
|
|
1294
|
+
};
|
|
1295
|
+
state.active_constraints.push(entry);
|
|
1296
|
+
activeConstraintKeys.add(identityKey);
|
|
1297
|
+
managedArtifacts.push({
|
|
1298
|
+
kind: 'constraint',
|
|
1299
|
+
id: entry.id,
|
|
1300
|
+
suggestion_id: suggestion.id,
|
|
1301
|
+
rollback_action: 'delete',
|
|
1302
|
+
});
|
|
1303
|
+
stateChanged = true;
|
|
1304
|
+
createdCount++;
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
case 'trap': {
|
|
1308
|
+
const { id, short_label } = generateIdWithLabel('known_traps', cwd);
|
|
1309
|
+
const entry = {
|
|
1310
|
+
id,
|
|
1311
|
+
short_label,
|
|
1312
|
+
text: suggestion.text,
|
|
1313
|
+
created_at: nowISO(),
|
|
1314
|
+
author: resolveCurrentAgentName(cwd),
|
|
1315
|
+
status: 'active',
|
|
1316
|
+
severity: suggestion.severity ?? 'medium',
|
|
1317
|
+
tags: suggestion.tags ?? [],
|
|
1318
|
+
related_paths: suggestion.related_paths,
|
|
1319
|
+
visibility: 'shared',
|
|
1320
|
+
};
|
|
1321
|
+
state.known_traps.push(entry);
|
|
1322
|
+
activeTrapKeys.add(identityKey);
|
|
1323
|
+
managedArtifacts.push({
|
|
1324
|
+
kind: 'trap',
|
|
1325
|
+
id: entry.id,
|
|
1326
|
+
suggestion_id: suggestion.id,
|
|
1327
|
+
rollback_action: 'delete',
|
|
1328
|
+
});
|
|
1329
|
+
stateChanged = true;
|
|
1330
|
+
createdCount++;
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (stateChanged) {
|
|
1336
|
+
persistState(state, cwd, { writeProjectMarkdown: false });
|
|
1337
|
+
}
|
|
1338
|
+
if (createdCount > 0) {
|
|
1339
|
+
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd), cwd));
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
const receipt = BootstrapApplicationReceiptSchema.parse({
|
|
1343
|
+
schema_version: DERIVED_SCHEMA_VERSION,
|
|
1344
|
+
applied_at: nowISO(),
|
|
1345
|
+
proposal_derived_at: proposal.derived_at,
|
|
1346
|
+
target: proposal.target,
|
|
1347
|
+
workspace_kind: proposal.workspace_kind,
|
|
1348
|
+
managed_artifacts: managedArtifacts,
|
|
1349
|
+
suggestion_ids: managedArtifacts.map((artifact) => artifact.suggestion_id),
|
|
1350
|
+
});
|
|
1351
|
+
saveVersionedJsonFile('bootstrap_application', bootstrapApplicationPath(cwd), receipt);
|
|
1352
|
+
return {
|
|
1353
|
+
proposal,
|
|
1354
|
+
receipt,
|
|
1355
|
+
createdCount,
|
|
1356
|
+
skippedCount,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
export function uninstallBootstrapImport(cwd) {
|
|
1360
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
1361
|
+
const receipt = loadBootstrapApplication(resolvedCwd);
|
|
1362
|
+
if (!receipt || receipt.uninstalled_at) {
|
|
1363
|
+
return {
|
|
1364
|
+
receipt,
|
|
1365
|
+
deactivatedCount: 0,
|
|
1366
|
+
deletedCount: 0,
|
|
1367
|
+
skippedCount: 0,
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
let deactivatedCount = 0;
|
|
1371
|
+
let deletedCount = 0;
|
|
1372
|
+
let skippedCount = 0;
|
|
1373
|
+
withStoreLock(resolvedCwd, () => {
|
|
1374
|
+
const state = loadState(resolvedCwd);
|
|
1375
|
+
const instructions = loadInstructions(resolvedCwd);
|
|
1376
|
+
let stateChanged = false;
|
|
1377
|
+
for (const artifact of receipt.managed_artifacts) {
|
|
1378
|
+
if (artifact.kind === 'instruction') {
|
|
1379
|
+
const instruction = instructions.find((entry) => entry.id === artifact.id);
|
|
1380
|
+
if (!instruction || !instruction.active) {
|
|
1381
|
+
skippedCount++;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
instruction.active = false;
|
|
1385
|
+
instruction.updated_at = nowISO();
|
|
1386
|
+
saveInstruction(instruction, resolvedCwd);
|
|
1387
|
+
deactivatedCount++;
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
const beforeCounts = {
|
|
1391
|
+
decisions: state.recent_decisions.length,
|
|
1392
|
+
constraints: state.active_constraints.length,
|
|
1393
|
+
traps: state.known_traps.length,
|
|
1394
|
+
};
|
|
1395
|
+
if (artifact.kind === 'decision') {
|
|
1396
|
+
state.recent_decisions = state.recent_decisions.filter((entry) => entry.id !== artifact.id);
|
|
1397
|
+
}
|
|
1398
|
+
else if (artifact.kind === 'constraint') {
|
|
1399
|
+
state.active_constraints = state.active_constraints.filter((entry) => entry.id !== artifact.id);
|
|
1400
|
+
}
|
|
1401
|
+
else if (artifact.kind === 'trap') {
|
|
1402
|
+
state.known_traps = state.known_traps.filter((entry) => entry.id !== artifact.id);
|
|
1403
|
+
}
|
|
1404
|
+
const changed = beforeCounts.decisions !== state.recent_decisions.length
|
|
1405
|
+
|| beforeCounts.constraints !== state.active_constraints.length
|
|
1406
|
+
|| beforeCounts.traps !== state.known_traps.length;
|
|
1407
|
+
if (!changed) {
|
|
1408
|
+
skippedCount++;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
stateChanged = true;
|
|
1412
|
+
deletedCount++;
|
|
1413
|
+
}
|
|
1414
|
+
if (stateChanged) {
|
|
1415
|
+
persistState(state, resolvedCwd, { writeProjectMarkdown: false });
|
|
1416
|
+
}
|
|
1417
|
+
if (deactivatedCount > 0 || deletedCount > 0) {
|
|
1418
|
+
writeFileAtomic(memoryPath('project.md', resolvedCwd), generateMarkdown(loadState(resolvedCwd), resolvedCwd));
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
const nextReceipt = BootstrapApplicationReceiptSchema.parse({
|
|
1422
|
+
...receipt,
|
|
1423
|
+
uninstalled_at: nowISO(),
|
|
1424
|
+
});
|
|
1425
|
+
saveVersionedJsonFile('bootstrap_application', bootstrapApplicationPath(resolvedCwd), nextReceipt);
|
|
1426
|
+
return {
|
|
1427
|
+
receipt: nextReceipt,
|
|
1428
|
+
deactivatedCount,
|
|
1429
|
+
deletedCount,
|
|
1430
|
+
skippedCount,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function classifyWorkspace(cwd) {
|
|
1434
|
+
const visibleEntries = fs.readdirSync(cwd)
|
|
1435
|
+
.filter((entry) => !EMPTY_WORKSPACE_IGNORED.has(entry));
|
|
1436
|
+
return {
|
|
1437
|
+
kind: visibleEntries.length === 0 ? 'empty' : 'existing',
|
|
1438
|
+
visibleEntries,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function discoverNativeInstructionFiles(cwd) {
|
|
1442
|
+
const discovered = new Set();
|
|
1443
|
+
for (const relativePath of NATIVE_INSTRUCTION_FILES) {
|
|
1444
|
+
const filepath = path.join(cwd, relativePath);
|
|
1445
|
+
if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
|
|
1446
|
+
discovered.add(relativePath);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
for (const relativeDir of NATIVE_INSTRUCTION_DIRS) {
|
|
1450
|
+
const dir = path.join(cwd, relativeDir);
|
|
1451
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
for (const entry of fs.readdirSync(dir).sort()) {
|
|
1455
|
+
if (!/\.(md|mdc)$/i.test(entry)) {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
discovered.add(path.posix.join(relativeDir.replace(/\\/g, '/'), entry));
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return [...discovered].sort((left, right) => left.localeCompare(right));
|
|
1462
|
+
}
|
|
1463
|
+
function inferBootstrapConfidence(input) {
|
|
1464
|
+
if (input.workspaceKind === 'empty') {
|
|
1465
|
+
return input.seedCount > 3 ? 'medium' : 'low';
|
|
1466
|
+
}
|
|
1467
|
+
if (input.readmePresent && (input.agentsPresent || input.nativeInstructionFiles.length > 0) && input.seedCount >= 6) {
|
|
1468
|
+
return 'high';
|
|
1469
|
+
}
|
|
1470
|
+
if (input.readmePresent || input.nativeInstructionFiles.length > 0 || input.seedCount >= 4) {
|
|
1471
|
+
return 'medium';
|
|
1472
|
+
}
|
|
1473
|
+
return 'low';
|
|
1474
|
+
}
|
|
1475
|
+
function inferOnboardingMode(input) {
|
|
1476
|
+
if (input.workspaceKind === 'empty') {
|
|
1477
|
+
return 'empty_workspace';
|
|
1478
|
+
}
|
|
1479
|
+
if (input.readmePresent || input.nativeInstructionFiles.length > 0 || input.confidence === 'high') {
|
|
1480
|
+
return 'existing_documented';
|
|
1481
|
+
}
|
|
1482
|
+
return 'existing_sparse';
|
|
1483
|
+
}
|
|
1484
|
+
function inferBootstrapGaps(input) {
|
|
1485
|
+
const gaps = [];
|
|
1486
|
+
if (input.workspaceKind === 'empty') {
|
|
1487
|
+
gaps.push('project intent is not documented yet');
|
|
1488
|
+
gaps.push('agent workflow expectations should be captured explicitly');
|
|
1489
|
+
return gaps;
|
|
1490
|
+
}
|
|
1491
|
+
if (!input.readmePresent) {
|
|
1492
|
+
gaps.push('no README-level project overview detected');
|
|
1493
|
+
}
|
|
1494
|
+
if (input.nativeInstructionFiles.length === 0) {
|
|
1495
|
+
gaps.push('no native agent instruction files detected');
|
|
1496
|
+
}
|
|
1497
|
+
if (input.seedCount < 4) {
|
|
1498
|
+
gaps.push('derived context is sparse and may need an interview');
|
|
1499
|
+
}
|
|
1500
|
+
return gaps;
|
|
1501
|
+
}
|
|
563
1502
|
function scoreSeed(seed, target) {
|
|
564
1503
|
let score = seedKindWeight(seed.seed_kind) + confidenceWeight(seed.confidence);
|
|
565
1504
|
if (!target) {
|