brainclaw 0.19.10 → 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/dist/cli.js +1 -0
- package/dist/commands/bootstrap.js +17 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/mcp.js +69 -2
- package/dist/core/bootstrap.js +416 -60
- package/dist/core/schema.js +28 -0
- package/docs/cli.md +5 -0
- package/docs/integrations/mcp.md +18 -1
- package/docs/mcp-schema-changelog.md +6 -0
- package/docs/quickstart.md +19 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -515,6 +515,7 @@ program
|
|
|
515
515
|
.option('--refresh', 'Force a fresh bootstrap scan instead of reusing the current profile')
|
|
516
516
|
.option('--interview', 'Render the adaptive interview prompts instead of the bootstrap summary')
|
|
517
517
|
.option('--audience <audience>', 'Target interview prompts for cli, ide_chat, or any')
|
|
518
|
+
.option('--answers-file <path>', 'Load structured bootstrap interview answers from a JSON file')
|
|
518
519
|
.option('--apply', 'Import the current bootstrap proposal into canonical memory')
|
|
519
520
|
.option('--uninstall', 'Deactivate the last bootstrap import managed by this workspace')
|
|
520
521
|
.option('-y, --yes', 'Skip confirmation prompts for apply/uninstall')
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import readline from 'node:readline/promises';
|
|
2
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
4
|
import { memoryExists } from '../core/io.js';
|
|
4
5
|
import { applyBootstrapImport, renderBootstrapInterview, renderBootstrapSummary, runBootstrapProfile, uninstallBootstrapImport, } from '../core/bootstrap.js';
|
|
6
|
+
import { BootstrapInterviewAnswerSchema } from '../core/schema.js';
|
|
5
7
|
export async function runBootstrap(options = {}) {
|
|
6
8
|
const cwd = options.cwd ?? process.cwd();
|
|
7
9
|
if (!memoryExists(cwd)) {
|
|
@@ -14,6 +16,7 @@ export async function runBootstrap(options = {}) {
|
|
|
14
16
|
process.exit(1);
|
|
15
17
|
}
|
|
16
18
|
const audience = resolveBootstrapInterviewAudience(options.audience);
|
|
19
|
+
const interviewAnswers = loadBootstrapInterviewAnswers(options.answersFile);
|
|
17
20
|
if (options.uninstall) {
|
|
18
21
|
await confirmBootstrapAction('Remove the last bootstrap import?', options.yes);
|
|
19
22
|
const result = uninstallBootstrapImport(cwd);
|
|
@@ -21,7 +24,7 @@ export async function runBootstrap(options = {}) {
|
|
|
21
24
|
console.log('No bootstrap import receipt found.');
|
|
22
25
|
return;
|
|
23
26
|
}
|
|
24
|
-
console.log(`✔ Bootstrap uninstall completed: ${result.deactivatedCount} instruction(s) deactivated, ${result.skippedCount} artifact(s) skipped.`);
|
|
27
|
+
console.log(`✔ Bootstrap uninstall completed: ${result.deactivatedCount} instruction(s) deactivated, ${result.deletedCount} artifact(s) deleted, ${result.skippedCount} artifact(s) skipped.`);
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
if (options.apply) {
|
|
@@ -29,6 +32,7 @@ export async function runBootstrap(options = {}) {
|
|
|
29
32
|
const result = applyBootstrapImport({
|
|
30
33
|
target: options.for,
|
|
31
34
|
refresh: options.refresh,
|
|
35
|
+
interviewAnswers,
|
|
32
36
|
cwd,
|
|
33
37
|
});
|
|
34
38
|
console.log(`✔ Bootstrap import applied: ${result.createdCount} item(s) created, ${result.skippedCount} suggestion(s) skipped.`);
|
|
@@ -40,6 +44,7 @@ export async function runBootstrap(options = {}) {
|
|
|
40
44
|
const result = runBootstrapProfile({
|
|
41
45
|
target: options.for,
|
|
42
46
|
refresh: options.refresh,
|
|
47
|
+
interviewAnswers,
|
|
43
48
|
cwd,
|
|
44
49
|
});
|
|
45
50
|
if (options.json) {
|
|
@@ -76,6 +81,17 @@ function resolveBootstrapInterviewAudience(value) {
|
|
|
76
81
|
}
|
|
77
82
|
throw new Error(`Unsupported bootstrap interview audience '${value}'. Use cli, ide_chat, or any.`);
|
|
78
83
|
}
|
|
84
|
+
function loadBootstrapInterviewAnswers(filepath) {
|
|
85
|
+
if (!filepath) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const raw = fs.readFileSync(filepath, 'utf-8');
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (!Array.isArray(parsed)) {
|
|
91
|
+
throw new Error(`Bootstrap interview answers file must contain a JSON array: ${filepath}`);
|
|
92
|
+
}
|
|
93
|
+
return parsed.map((entry) => BootstrapInterviewAnswerSchema.parse(entry));
|
|
94
|
+
}
|
|
79
95
|
async function confirmBootstrapAction(question, yes) {
|
|
80
96
|
if (yes) {
|
|
81
97
|
return;
|
package/dist/commands/init.js
CHANGED
|
@@ -243,6 +243,7 @@ export async function runInit(options = {}) {
|
|
|
243
243
|
if ((onboardingPreflight.importPlan.interview?.question_count ?? 0) > 0) {
|
|
244
244
|
console.log('');
|
|
245
245
|
console.log(`Interview: run 'brainclaw bootstrap --interview --audience cli' for terminal agents or '--audience ide_chat' for IDE chat agents.`);
|
|
246
|
+
console.log(`Apply confirmed answers: write a JSON answers file and run 'brainclaw bootstrap --answers-file <path> --apply'.`);
|
|
246
247
|
}
|
|
247
248
|
else if ((onboardingPreflight.profile.gaps?.length ?? 0) > 0) {
|
|
248
249
|
console.log('');
|
package/dist/commands/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import readline from 'node:readline';
|
|
|
2
2
|
import { Worker } from 'node:worker_threads';
|
|
3
3
|
import { getTriggeredItems, renderTriggeredItems } from '../core/lifecycle.js';
|
|
4
4
|
import { resolveCrossProjectTarget, writeCrossProjectNote } from '../core/cross-project.js';
|
|
5
|
-
import { renderBootstrapSummary, runBootstrapProfile } from '../core/bootstrap.js';
|
|
5
|
+
import { applyBootstrapImport, renderBootstrapInterview, renderBootstrapSummary, runBootstrapProfile, uninstallBootstrapImport } from '../core/bootstrap.js';
|
|
6
6
|
import { buildAgentToolingContext, renderAgentToolingSummary } from '../core/agent-context.js';
|
|
7
7
|
import { buildCoordinationSnapshot } from '../core/coordination.js';
|
|
8
8
|
import { buildContext, renderContextMarkdown, renderContextPromptTemplate } from '../core/context.js';
|
|
@@ -30,6 +30,7 @@ import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
|
30
30
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
31
31
|
import { resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
|
|
32
32
|
import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
|
|
33
|
+
import { BootstrapInterviewAnswerSchema } from '../core/schema.js';
|
|
33
34
|
export const SCHEMA_VERSION = '0.6.0';
|
|
34
35
|
export const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2024-11-05'];
|
|
35
36
|
export const MCP_SERVER_NOT_INITIALIZED = -32002;
|
|
@@ -67,6 +68,15 @@ export const MCP_READ_TOOLS = [
|
|
|
67
68
|
properties: {
|
|
68
69
|
target: { type: 'string', description: 'Optional path or scope to tailor the bootstrap.' },
|
|
69
70
|
refresh: { type: 'boolean', description: 'Force a fresh bootstrap scan.' },
|
|
71
|
+
audience: { type: 'string', description: 'Optional interview audience filter: cli, ide_chat, or any.' },
|
|
72
|
+
interview: { type: 'boolean', description: 'Render interview text instead of the summary text.' },
|
|
73
|
+
apply: { type: 'boolean', description: 'Apply the current import proposal into canonical memory.' },
|
|
74
|
+
uninstall: { type: 'boolean', description: 'Uninstall the last bootstrap-managed import.' },
|
|
75
|
+
interviewAnswers: {
|
|
76
|
+
type: 'array',
|
|
77
|
+
description: 'Optional structured interview answers. Each answer may include question_id, response_text, response_items, response_boolean, and explicit suggestions.',
|
|
78
|
+
items: { type: 'object' },
|
|
79
|
+
},
|
|
70
80
|
},
|
|
71
81
|
},
|
|
72
82
|
},
|
|
@@ -506,6 +516,18 @@ export function createToolErrorResponse(kind, message, details) {
|
|
|
506
516
|
},
|
|
507
517
|
}, true);
|
|
508
518
|
}
|
|
519
|
+
function normalizeBootstrapInterviewAnswersArg(value) {
|
|
520
|
+
if (!Array.isArray(value)) {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
return value.map((entry) => BootstrapInterviewAnswerSchema.parse(entry));
|
|
524
|
+
}
|
|
525
|
+
function normalizeBootstrapInterviewAudienceArg(value) {
|
|
526
|
+
if (value === 'cli' || value === 'ide_chat' || value === 'any') {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
return 'any';
|
|
530
|
+
}
|
|
509
531
|
function requireObjectParams(params, id) {
|
|
510
532
|
if (params === undefined) {
|
|
511
533
|
return {};
|
|
@@ -1040,19 +1062,64 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1040
1062
|
};
|
|
1041
1063
|
}
|
|
1042
1064
|
if (name === 'bclaw_bootstrap') {
|
|
1065
|
+
const interviewAnswers = normalizeBootstrapInterviewAnswersArg(args.interviewAnswers);
|
|
1066
|
+
if (args.apply && args.uninstall) {
|
|
1067
|
+
throw new Error('bclaw_bootstrap does not allow apply and uninstall at the same time.');
|
|
1068
|
+
}
|
|
1069
|
+
if (args.uninstall) {
|
|
1070
|
+
const result = uninstallBootstrapImport(cwd);
|
|
1071
|
+
const text = !result.receipt
|
|
1072
|
+
? 'No bootstrap import receipt found.'
|
|
1073
|
+
: `Bootstrap uninstall completed: ${result.deactivatedCount} instruction(s) deactivated, ${result.deletedCount} artifact(s) deleted, ${result.skippedCount} artifact(s) skipped.`;
|
|
1074
|
+
return {
|
|
1075
|
+
content: [{ type: 'text', text }],
|
|
1076
|
+
structuredContent: {
|
|
1077
|
+
receipt: result.receipt,
|
|
1078
|
+
deactivated_count: result.deactivatedCount,
|
|
1079
|
+
deleted_count: result.deletedCount,
|
|
1080
|
+
skipped_count: result.skippedCount,
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (args.apply) {
|
|
1085
|
+
const applied = applyBootstrapImport({
|
|
1086
|
+
target: args.target,
|
|
1087
|
+
refresh: args.refresh,
|
|
1088
|
+
interviewAnswers,
|
|
1089
|
+
cwd,
|
|
1090
|
+
});
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{
|
|
1093
|
+
type: 'text',
|
|
1094
|
+
text: `Bootstrap import applied: ${applied.createdCount} item(s) created, ${applied.skippedCount} suggestion(s) skipped.`,
|
|
1095
|
+
}],
|
|
1096
|
+
structuredContent: {
|
|
1097
|
+
created_count: applied.createdCount,
|
|
1098
|
+
skipped_count: applied.skippedCount,
|
|
1099
|
+
receipt: applied.receipt,
|
|
1100
|
+
import_plan: applied.proposal,
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1043
1104
|
const result = runBootstrapProfile({
|
|
1044
1105
|
target: args.target,
|
|
1045
1106
|
refresh: args.refresh,
|
|
1107
|
+
interviewAnswers,
|
|
1046
1108
|
cwd,
|
|
1047
1109
|
});
|
|
1110
|
+
const audience = normalizeBootstrapInterviewAudienceArg(args.audience);
|
|
1111
|
+
const text = args.interview
|
|
1112
|
+
? renderBootstrapInterview(result, audience)
|
|
1113
|
+
: renderBootstrapSummary(result);
|
|
1048
1114
|
return {
|
|
1049
|
-
content: [{ type: 'text', text
|
|
1115
|
+
content: [{ type: 'text', text }],
|
|
1050
1116
|
structuredContent: {
|
|
1051
1117
|
summary: result.profile.summary,
|
|
1052
1118
|
target: result.profile.target,
|
|
1053
1119
|
repo_fingerprint: result.profile.repo_fingerprint,
|
|
1054
1120
|
sources_scanned: result.profile.sources_scanned,
|
|
1055
1121
|
workspace_kind: result.profile.workspace_kind,
|
|
1122
|
+
onboarding_mode: result.profile.onboarding_mode,
|
|
1056
1123
|
confidence: result.profile.confidence,
|
|
1057
1124
|
native_instruction_files: result.profile.native_instruction_files,
|
|
1058
1125
|
gaps: result.profile.gaps,
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
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 { generateId, generateIdWithLabel, nowISO } from './ids.js';
|
|
6
7
|
import { memoryPath, resolveEntityDir, withStoreLock, writeFileAtomic } from './io.js';
|
|
7
|
-
import { BootstrapApplicationReceiptSchema, BootstrapInterviewPlanSchema, BootstrapInterviewQuestionSchema, BootstrapImportPlanDocumentSchema, BootstrapProfileDocumentSchema, BootstrapSuggestionDocumentSchema, MemorySeedDocumentSchema, } from './schema.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';
|
|
12
13
|
import { createInstruction, loadInstructions, saveInstruction } from './instructions.js';
|
|
13
14
|
import { resolveCurrentAgentName } from './agent-registry.js';
|
|
14
|
-
import { loadState } from './state.js';
|
|
15
|
+
import { loadState, persistState } from './state.js';
|
|
15
16
|
import { generateMarkdown } from './markdown.js';
|
|
16
17
|
const README_CANDIDATES = ['README.md', 'README', 'README.txt', 'README.mdx'];
|
|
17
18
|
const DOC_HINTS = ['docs', 'doc'];
|
|
@@ -47,25 +48,56 @@ const NATIVE_INSTRUCTION_DIRS = [
|
|
|
47
48
|
export function runBootstrapProfile(options = {}) {
|
|
48
49
|
const cwd = options.cwd ?? process.cwd();
|
|
49
50
|
const target = normalizeTarget(options.target);
|
|
51
|
+
const interviewAnswers = normalizeBootstrapInterviewAnswers(options.interviewAnswers);
|
|
50
52
|
const existing = loadBootstrapProfile(cwd);
|
|
51
53
|
const existingPlan = loadBootstrapImportPlan(cwd);
|
|
52
54
|
const lastApplication = loadBootstrapApplication(cwd);
|
|
53
55
|
const existingFingerprint = currentRepoFingerprint(cwd);
|
|
54
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;
|
|
55
75
|
return {
|
|
56
76
|
profile: existing,
|
|
57
|
-
seeds
|
|
58
|
-
importPlan
|
|
77
|
+
seeds,
|
|
78
|
+
importPlan,
|
|
59
79
|
lastApplication,
|
|
60
80
|
reusedProfile: true,
|
|
61
81
|
};
|
|
62
82
|
}
|
|
63
83
|
const artifacts = buildBootstrapArtifacts({ cwd, target, repoFingerprint: existingFingerprint });
|
|
64
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;
|
|
65
97
|
return {
|
|
66
98
|
profile: artifacts.profile,
|
|
67
99
|
seeds: artifacts.seeds,
|
|
68
|
-
importPlan
|
|
100
|
+
importPlan,
|
|
69
101
|
lastApplication,
|
|
70
102
|
reusedProfile: false,
|
|
71
103
|
};
|
|
@@ -120,6 +152,9 @@ export function renderBootstrapSummary(result) {
|
|
|
120
152
|
if (result.profile.workspace_kind) {
|
|
121
153
|
lines.push(`Workspace kind: ${result.profile.workspace_kind}`);
|
|
122
154
|
}
|
|
155
|
+
if (result.profile.onboarding_mode) {
|
|
156
|
+
lines.push(`Onboarding mode: ${result.profile.onboarding_mode}`);
|
|
157
|
+
}
|
|
123
158
|
if (result.profile.confidence) {
|
|
124
159
|
lines.push(`Confidence: ${result.profile.confidence}`);
|
|
125
160
|
}
|
|
@@ -132,6 +167,9 @@ export function renderBootstrapSummary(result) {
|
|
|
132
167
|
lines.push(`Open gaps: ${result.profile.gaps.join(' | ')}`);
|
|
133
168
|
}
|
|
134
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
|
+
}
|
|
135
173
|
if (result.profile.repo_fingerprint) {
|
|
136
174
|
lines.push(`Repo fingerprint: ${result.profile.repo_fingerprint}`);
|
|
137
175
|
}
|
|
@@ -156,7 +194,7 @@ export function renderBootstrapSummary(result) {
|
|
|
156
194
|
lines.push('');
|
|
157
195
|
lines.push('Adaptive interview:');
|
|
158
196
|
for (const question of result.importPlan.interview.questions.slice(0, 6)) {
|
|
159
|
-
lines.push(`- [${question.priority}/${question.audience}] ${question.prompt}`);
|
|
197
|
+
lines.push(`- [${question.priority}/${question.audience}] [${question.id}] ${question.prompt}`);
|
|
160
198
|
}
|
|
161
199
|
}
|
|
162
200
|
if (result.seeds.length > 0) {
|
|
@@ -180,9 +218,12 @@ export function renderBootstrapInterview(result, audience = 'any') {
|
|
|
180
218
|
const lines = [interview.summary, `Audience: ${audience}`];
|
|
181
219
|
lines.push('');
|
|
182
220
|
for (const [index, question] of questions.entries()) {
|
|
183
|
-
lines.push(`${index + 1}. ${question.prompt}`);
|
|
221
|
+
lines.push(`${index + 1}. [${question.id}] ${question.prompt}`);
|
|
184
222
|
lines.push(` Why: ${question.rationale}`);
|
|
185
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
|
+
}
|
|
186
227
|
}
|
|
187
228
|
return lines.join('\n');
|
|
188
229
|
}
|
|
@@ -239,6 +280,12 @@ function buildBootstrapArtifacts(input) {
|
|
|
239
280
|
nativeInstructionFiles,
|
|
240
281
|
seedCount: uniqueSeeds.length,
|
|
241
282
|
});
|
|
283
|
+
const onboardingMode = inferOnboardingMode({
|
|
284
|
+
workspaceKind: workspace.kind,
|
|
285
|
+
readmePresent: Boolean(readmePath),
|
|
286
|
+
nativeInstructionFiles,
|
|
287
|
+
confidence,
|
|
288
|
+
});
|
|
242
289
|
const gaps = inferBootstrapGaps({
|
|
243
290
|
workspaceKind: workspace.kind,
|
|
244
291
|
readmePresent: Boolean(readmePath),
|
|
@@ -253,6 +300,7 @@ function buildBootstrapArtifacts(input) {
|
|
|
253
300
|
repoAnalysis,
|
|
254
301
|
seeds: uniqueSeeds,
|
|
255
302
|
target: input.target,
|
|
303
|
+
onboardingMode,
|
|
256
304
|
confidence,
|
|
257
305
|
gaps,
|
|
258
306
|
});
|
|
@@ -260,6 +308,7 @@ function buildBootstrapArtifacts(input) {
|
|
|
260
308
|
cwd: input.cwd,
|
|
261
309
|
target: input.target,
|
|
262
310
|
workspaceKind: workspace.kind,
|
|
311
|
+
onboardingMode,
|
|
263
312
|
confidence,
|
|
264
313
|
gaps,
|
|
265
314
|
seeds: uniqueSeeds,
|
|
@@ -276,6 +325,7 @@ function buildBootstrapArtifacts(input) {
|
|
|
276
325
|
seed_count: uniqueSeeds.length,
|
|
277
326
|
target: input.target,
|
|
278
327
|
workspace_kind: workspace.kind,
|
|
328
|
+
onboarding_mode: onboardingMode,
|
|
279
329
|
confidence,
|
|
280
330
|
native_instruction_files: nativeInstructionFiles,
|
|
281
331
|
gaps,
|
|
@@ -748,6 +798,7 @@ function dedupeSeeds(seeds) {
|
|
|
748
798
|
function buildSummary(input) {
|
|
749
799
|
const parts = [];
|
|
750
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}.`);
|
|
751
802
|
parts.push(`Confidence: ${input.confidence}.`);
|
|
752
803
|
parts.push(`Repository mode looks ${input.repoAnalysis.recommendedMode}.`);
|
|
753
804
|
if (input.agentsPresent) {
|
|
@@ -786,7 +837,7 @@ function buildBootstrapImportPlan(input) {
|
|
|
786
837
|
if (!suggestion) {
|
|
787
838
|
continue;
|
|
788
839
|
}
|
|
789
|
-
const suggestionKey =
|
|
840
|
+
const suggestionKey = genericSuggestionIdentityKey(suggestion);
|
|
790
841
|
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
791
842
|
continue;
|
|
792
843
|
}
|
|
@@ -809,7 +860,7 @@ function buildBootstrapImportPlan(input) {
|
|
|
809
860
|
if (!suggestion) {
|
|
810
861
|
continue;
|
|
811
862
|
}
|
|
812
|
-
const suggestionKey =
|
|
863
|
+
const suggestionKey = genericSuggestionIdentityKey(suggestion);
|
|
813
864
|
if (seenSuggestionKeys.has(suggestionKey) || activeInstructionKeys.has(suggestionKey)) {
|
|
814
865
|
continue;
|
|
815
866
|
}
|
|
@@ -819,9 +870,6 @@ function buildBootstrapImportPlan(input) {
|
|
|
819
870
|
schema_version: DERIVED_SCHEMA_VERSION,
|
|
820
871
|
}));
|
|
821
872
|
}
|
|
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
873
|
const interview = buildBootstrapInterviewPlan({
|
|
826
874
|
workspaceKind: input.workspaceKind,
|
|
827
875
|
gaps: input.gaps,
|
|
@@ -830,15 +878,35 @@ function buildBootstrapImportPlan(input) {
|
|
|
830
878
|
.filter((seed) => seed.source_kind === 'native_instruction')
|
|
831
879
|
.map((seed) => seed.source_ref))],
|
|
832
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` : ''}.`;
|
|
833
898
|
return BootstrapImportPlanDocumentSchema.parse({
|
|
834
899
|
schema_version: DERIVED_SCHEMA_VERSION,
|
|
835
900
|
derived_at: nowISO(),
|
|
836
901
|
target: input.target,
|
|
837
902
|
workspace_kind: input.workspaceKind,
|
|
903
|
+
onboarding_mode: input.onboardingMode,
|
|
838
904
|
confidence: input.confidence,
|
|
839
905
|
summary,
|
|
840
906
|
requires_confirmation: true,
|
|
841
907
|
gaps: input.gaps,
|
|
908
|
+
confirmed_suggestion_count: confirmedSuggestionCount,
|
|
909
|
+
interview_answer_count: input.interviewAnswers?.length ?? 0,
|
|
842
910
|
suggestion_count: suggestions.length,
|
|
843
911
|
suggestions,
|
|
844
912
|
interview,
|
|
@@ -846,37 +914,38 @@ function buildBootstrapImportPlan(input) {
|
|
|
846
914
|
}
|
|
847
915
|
function buildBootstrapInterviewPlan(input) {
|
|
848
916
|
const questions = [];
|
|
849
|
-
const add = (prompt, rationale, priority, audience, responseKind, gapKeys) => {
|
|
917
|
+
const add = (prompt, rationale, priority, audience, responseKind, gapKeys, targetHints) => {
|
|
850
918
|
questions.push(BootstrapInterviewQuestionSchema.parse({
|
|
851
|
-
id:
|
|
919
|
+
id: bootstrapInterviewQuestionId(prompt, audience),
|
|
852
920
|
prompt,
|
|
853
921
|
rationale,
|
|
854
922
|
priority,
|
|
855
923
|
audience,
|
|
856
924
|
response_kind: responseKind,
|
|
857
925
|
gap_keys: gapKeys,
|
|
926
|
+
target_hints: targetHints,
|
|
858
927
|
}));
|
|
859
928
|
};
|
|
860
929
|
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']);
|
|
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']);
|
|
865
934
|
}
|
|
866
935
|
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']);
|
|
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']);
|
|
868
937
|
}
|
|
869
938
|
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']);
|
|
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']);
|
|
871
940
|
}
|
|
872
941
|
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']);
|
|
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']);
|
|
874
943
|
}
|
|
875
944
|
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']);
|
|
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']);
|
|
877
946
|
}
|
|
878
947
|
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']);
|
|
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']);
|
|
880
949
|
}
|
|
881
950
|
const summary = questions.length === 0
|
|
882
951
|
? 'No adaptive interview questions are needed right now.'
|
|
@@ -891,6 +960,139 @@ function buildBootstrapInterviewPlan(input) {
|
|
|
891
960
|
questions,
|
|
892
961
|
});
|
|
893
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
|
+
}
|
|
894
1096
|
function seedToBootstrapSuggestion(seed, allowSummaryFallback) {
|
|
895
1097
|
if (seed.seed_kind !== 'agent_rule' && seed.seed_kind !== 'command') {
|
|
896
1098
|
return undefined;
|
|
@@ -968,8 +1170,14 @@ function normalizeBootstrapSuggestionTags(tags) {
|
|
|
968
1170
|
function isBootstrapSummarySeed(text) {
|
|
969
1171
|
return text.startsWith('Agent guide: ') || text.startsWith('Native agent guidance from ');
|
|
970
1172
|
}
|
|
971
|
-
function
|
|
972
|
-
|
|
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()}`;
|
|
973
1181
|
}
|
|
974
1182
|
function instructionIdentityKey(text, layer, scope) {
|
|
975
1183
|
return `${layer}:${scope ?? '*'}:${text.trim().toLowerCase()}`;
|
|
@@ -986,40 +1194,149 @@ export function applyBootstrapImport(options = {}) {
|
|
|
986
1194
|
skippedCount: 0,
|
|
987
1195
|
};
|
|
988
1196
|
}
|
|
989
|
-
const activeInstructionKeys = new Set(loadInstructions(cwd)
|
|
990
|
-
.filter((entry) => entry.active)
|
|
991
|
-
.map((entry) => instructionIdentityKey(entry.text, entry.layer, entry.scope)));
|
|
992
1197
|
const managedArtifacts = [];
|
|
993
1198
|
let createdCount = 0;
|
|
994
1199
|
let skippedCount = 0;
|
|
995
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;
|
|
996
1231
|
for (const suggestion of proposal.suggestions) {
|
|
997
|
-
|
|
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))) {
|
|
998
1237
|
skippedCount++;
|
|
999
1238
|
continue;
|
|
1000
1239
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
+
}
|
|
1005
1333
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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++;
|
|
1334
|
+
}
|
|
1335
|
+
if (stateChanged) {
|
|
1336
|
+
persistState(state, cwd, { writeProjectMarkdown: false });
|
|
1020
1337
|
}
|
|
1021
1338
|
if (createdCount > 0) {
|
|
1022
|
-
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd)));
|
|
1339
|
+
writeFileAtomic(memoryPath('project.md', cwd), generateMarkdown(loadState(cwd), cwd));
|
|
1023
1340
|
}
|
|
1024
1341
|
});
|
|
1025
1342
|
const receipt = BootstrapApplicationReceiptSchema.parse({
|
|
@@ -1046,30 +1363,59 @@ export function uninstallBootstrapImport(cwd) {
|
|
|
1046
1363
|
return {
|
|
1047
1364
|
receipt,
|
|
1048
1365
|
deactivatedCount: 0,
|
|
1366
|
+
deletedCount: 0,
|
|
1049
1367
|
skippedCount: 0,
|
|
1050
1368
|
};
|
|
1051
1369
|
}
|
|
1052
|
-
const instructions = loadInstructions(resolvedCwd);
|
|
1053
1370
|
let deactivatedCount = 0;
|
|
1371
|
+
let deletedCount = 0;
|
|
1054
1372
|
let skippedCount = 0;
|
|
1055
1373
|
withStoreLock(resolvedCwd, () => {
|
|
1374
|
+
const state = loadState(resolvedCwd);
|
|
1375
|
+
const instructions = loadInstructions(resolvedCwd);
|
|
1376
|
+
let stateChanged = false;
|
|
1056
1377
|
for (const artifact of receipt.managed_artifacts) {
|
|
1057
|
-
if (artifact.kind
|
|
1058
|
-
|
|
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++;
|
|
1059
1388
|
continue;
|
|
1060
1389
|
}
|
|
1061
|
-
const
|
|
1062
|
-
|
|
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) {
|
|
1063
1408
|
skippedCount++;
|
|
1064
1409
|
continue;
|
|
1065
1410
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1411
|
+
stateChanged = true;
|
|
1412
|
+
deletedCount++;
|
|
1413
|
+
}
|
|
1414
|
+
if (stateChanged) {
|
|
1415
|
+
persistState(state, resolvedCwd, { writeProjectMarkdown: false });
|
|
1070
1416
|
}
|
|
1071
|
-
if (deactivatedCount > 0) {
|
|
1072
|
-
writeFileAtomic(memoryPath('project.md', resolvedCwd), generateMarkdown(loadState(resolvedCwd)));
|
|
1417
|
+
if (deactivatedCount > 0 || deletedCount > 0) {
|
|
1418
|
+
writeFileAtomic(memoryPath('project.md', resolvedCwd), generateMarkdown(loadState(resolvedCwd), resolvedCwd));
|
|
1073
1419
|
}
|
|
1074
1420
|
});
|
|
1075
1421
|
const nextReceipt = BootstrapApplicationReceiptSchema.parse({
|
|
@@ -1080,6 +1426,7 @@ export function uninstallBootstrapImport(cwd) {
|
|
|
1080
1426
|
return {
|
|
1081
1427
|
receipt: nextReceipt,
|
|
1082
1428
|
deactivatedCount,
|
|
1429
|
+
deletedCount,
|
|
1083
1430
|
skippedCount,
|
|
1084
1431
|
};
|
|
1085
1432
|
}
|
|
@@ -1125,6 +1472,15 @@ function inferBootstrapConfidence(input) {
|
|
|
1125
1472
|
}
|
|
1126
1473
|
return 'low';
|
|
1127
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
|
+
}
|
|
1128
1484
|
function inferBootstrapGaps(input) {
|
|
1129
1485
|
const gaps = [];
|
|
1130
1486
|
if (input.workspaceKind === 'empty') {
|
package/dist/core/schema.js
CHANGED
|
@@ -485,6 +485,7 @@ export const BootstrapProfileDocumentSchema = z.object({
|
|
|
485
485
|
seed_count: z.number().int().nonnegative(),
|
|
486
486
|
target: z.string().optional(),
|
|
487
487
|
workspace_kind: z.enum(['empty', 'existing']).optional(),
|
|
488
|
+
onboarding_mode: z.enum(['empty_workspace', 'existing_documented', 'existing_sparse']).optional(),
|
|
488
489
|
confidence: MemorySeedConfidenceSchema.optional(),
|
|
489
490
|
native_instruction_files: z.array(z.string()).default([]),
|
|
490
491
|
gaps: z.array(z.string()).default([]),
|
|
@@ -503,6 +504,9 @@ export const BootstrapSuggestionDocumentSchema = z.object({
|
|
|
503
504
|
scope: z.string().optional(),
|
|
504
505
|
tags: z.array(z.string()).default([]),
|
|
505
506
|
related_paths: z.array(z.string()).optional(),
|
|
507
|
+
category: ConstraintCategorySchema.optional(),
|
|
508
|
+
outcome: DecisionOutcomeSchema.optional(),
|
|
509
|
+
severity: SeveritySchema.optional(),
|
|
506
510
|
reversible: z.boolean().default(true),
|
|
507
511
|
});
|
|
508
512
|
export const BootstrapInterviewAudienceSchema = z.enum(['cli', 'ide_chat', 'any']);
|
|
@@ -514,6 +518,7 @@ export const BootstrapInterviewQuestionSchema = z.object({
|
|
|
514
518
|
audience: BootstrapInterviewAudienceSchema.default('any'),
|
|
515
519
|
response_kind: z.enum(['short_text', 'long_text', 'boolean', 'list']).default('short_text'),
|
|
516
520
|
gap_keys: z.array(z.string()).default([]),
|
|
521
|
+
target_hints: z.array(BootstrapSuggestionTargetSchema).default([]),
|
|
517
522
|
});
|
|
518
523
|
export const BootstrapInterviewPlanSchema = z.object({
|
|
519
524
|
schema_version: z.number().int().positive().optional(),
|
|
@@ -524,15 +529,38 @@ export const BootstrapInterviewPlanSchema = z.object({
|
|
|
524
529
|
question_count: z.number().int().nonnegative(),
|
|
525
530
|
questions: z.array(BootstrapInterviewQuestionSchema).default([]),
|
|
526
531
|
});
|
|
532
|
+
export const BootstrapInterviewAnswerSuggestionSchema = z.object({
|
|
533
|
+
target: BootstrapSuggestionTargetSchema,
|
|
534
|
+
text: z.string(),
|
|
535
|
+
rationale: z.string().optional(),
|
|
536
|
+
confidence: MemorySeedConfidenceSchema.optional(),
|
|
537
|
+
layer: z.enum(['global', 'project', 'agent']).optional(),
|
|
538
|
+
scope: z.string().optional(),
|
|
539
|
+
tags: z.array(z.string()).default([]),
|
|
540
|
+
related_paths: z.array(z.string()).optional(),
|
|
541
|
+
category: ConstraintCategorySchema.optional(),
|
|
542
|
+
outcome: DecisionOutcomeSchema.optional(),
|
|
543
|
+
severity: SeveritySchema.optional(),
|
|
544
|
+
});
|
|
545
|
+
export const BootstrapInterviewAnswerSchema = z.object({
|
|
546
|
+
question_id: z.string(),
|
|
547
|
+
response_text: z.string().optional(),
|
|
548
|
+
response_items: z.array(z.string()).default([]),
|
|
549
|
+
response_boolean: z.boolean().optional(),
|
|
550
|
+
suggestions: z.array(BootstrapInterviewAnswerSuggestionSchema).default([]),
|
|
551
|
+
});
|
|
527
552
|
export const BootstrapImportPlanDocumentSchema = z.object({
|
|
528
553
|
schema_version: z.number().int().positive().optional(),
|
|
529
554
|
derived_at: z.string(),
|
|
530
555
|
target: z.string().optional(),
|
|
531
556
|
workspace_kind: z.enum(['empty', 'existing']).optional(),
|
|
557
|
+
onboarding_mode: z.enum(['empty_workspace', 'existing_documented', 'existing_sparse']).optional(),
|
|
532
558
|
confidence: MemorySeedConfidenceSchema.optional(),
|
|
533
559
|
summary: z.string(),
|
|
534
560
|
requires_confirmation: z.boolean().default(true),
|
|
535
561
|
gaps: z.array(z.string()).default([]),
|
|
562
|
+
confirmed_suggestion_count: z.number().int().nonnegative().default(0),
|
|
563
|
+
interview_answer_count: z.number().int().nonnegative().default(0),
|
|
536
564
|
suggestion_count: z.number().int().nonnegative(),
|
|
537
565
|
suggestions: z.array(BootstrapSuggestionDocumentSchema).default([]),
|
|
538
566
|
interview: BootstrapInterviewPlanSchema.optional(),
|
package/docs/cli.md
CHANGED
|
@@ -115,6 +115,7 @@ For capable agents, the MCP equivalent is `bclaw_bootstrap`. Use the CLI when a
|
|
|
115
115
|
| `--refresh` | Force refresh even if bootstrap data is recent |
|
|
116
116
|
| `--interview` | Render adaptive interview prompts instead of the bootstrap summary |
|
|
117
117
|
| `--audience <audience>` | Filter interview prompts for `cli`, `ide_chat`, or `any` |
|
|
118
|
+
| `--answers-file <path>` | Load structured interview answers from JSON and enrich the import proposal before preview/apply |
|
|
118
119
|
| `--apply` | Import the current bootstrap proposal into canonical memory |
|
|
119
120
|
| `--uninstall` | Deactivate the last bootstrap-managed import |
|
|
120
121
|
| `-y, --yes` | Skip confirmation prompts for apply/uninstall |
|
|
@@ -123,9 +124,13 @@ For capable agents, the MCP equivalent is `bclaw_bootstrap`. Use the CLI when a
|
|
|
123
124
|
brainclaw bootstrap --for copilot
|
|
124
125
|
brainclaw bootstrap --for claude --refresh --json
|
|
125
126
|
brainclaw bootstrap --interview --audience cli
|
|
127
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --json
|
|
128
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --apply -y
|
|
126
129
|
brainclaw bootstrap --apply -y
|
|
127
130
|
```
|
|
128
131
|
|
|
132
|
+
`--answers-file` expects a JSON array keyed by interview question IDs. Each entry can provide free-form answers (`response_text`, `response_items`, `response_boolean`) and optional explicit `suggestions` to confirm durable imports such as `decision`, `constraint`, `instruction`, or `trap`.
|
|
133
|
+
|
|
129
134
|
### `brainclaw env`
|
|
130
135
|
|
|
131
136
|
Display environment and tooling detection information.
|
package/docs/integrations/mcp.md
CHANGED
|
@@ -34,7 +34,7 @@ This keeps session continuity inside Brainclaw instead of pushing the agent back
|
|
|
34
34
|
| Tool | Purpose |
|
|
35
35
|
|---|---|
|
|
36
36
|
| `bclaw_get_context` | Ranked prompt-ready context, supports `digest: true` |
|
|
37
|
-
| `bclaw_bootstrap` | Derive brownfield bootstrap signals, adaptive interview prompts,
|
|
37
|
+
| `bclaw_bootstrap` | Derive brownfield bootstrap signals, return adaptive interview prompts, accept structured interview answers, and preview/apply a selective import proposal |
|
|
38
38
|
| `bclaw_get_execution_context` | Inspect local execution context and agent tooling |
|
|
39
39
|
| `bclaw_write_note` | Record a runtime note, supports `autoReflect: true` |
|
|
40
40
|
| `bclaw_read_handoff` | Read active handoffs |
|
|
@@ -65,6 +65,23 @@ brainclaw mcp
|
|
|
65
65
|
|
|
66
66
|
In practice, most agents pick this up through generated MCP config such as `.mcp.json`, `~/.cursor/mcp.json`, or other agent-specific config files written by `brainclaw setup`, `brainclaw init`, or `brainclaw export`.
|
|
67
67
|
|
|
68
|
+
## Bootstrap Through MCP
|
|
69
|
+
|
|
70
|
+
For agent-first onboarding, `bclaw_bootstrap` is the nominal path:
|
|
71
|
+
|
|
72
|
+
1. call `bclaw_bootstrap` to get the current `import_plan` and adaptive interview questions
|
|
73
|
+
2. collect answers in the agent surface
|
|
74
|
+
3. call `bclaw_bootstrap` again with `interviewAnswers` to preview confirmed `decision`, `constraint`, `instruction`, or `trap` suggestions
|
|
75
|
+
4. call `bclaw_bootstrap` with `apply: true` to create canonical memory
|
|
76
|
+
5. call `bclaw_bootstrap` with `uninstall: true` to revert the last bootstrap-managed import
|
|
77
|
+
|
|
78
|
+
Interview answers are keyed by question ID and may contain:
|
|
79
|
+
|
|
80
|
+
- `response_text`
|
|
81
|
+
- `response_items`
|
|
82
|
+
- `response_boolean`
|
|
83
|
+
- optional explicit `suggestions` when the agent wants to confirm exact canonical memory items
|
|
84
|
+
|
|
68
85
|
## Important Rule
|
|
69
86
|
|
|
70
87
|
If the agent has MCP available, do not treat the CLI as the primary runtime interface.
|
|
@@ -27,6 +27,12 @@ This document tracks all breaking and notable changes to the brainclaw MCP serve
|
|
|
27
27
|
- `bclaw_bootstrap` now returns adaptive interview prompts alongside the import proposal when bootstrap confidence is incomplete
|
|
28
28
|
- `structuredContent.import_plan.interview` exposes `summary`, `question_count`, and audience-tagged questions
|
|
29
29
|
- Questions can be targeted to `cli`, `ide_chat`, or `any`
|
|
30
|
+
- Interview questions now expose stable IDs and `target_hints`
|
|
31
|
+
- `structuredContent.onboarding_mode` distinguishes `empty_workspace`, `existing_documented`, and `existing_sparse`
|
|
32
|
+
- `structuredContent.import_plan.confirmed_suggestion_count` reports how many suggestions were confirmed by interview answers
|
|
33
|
+
- `bclaw_bootstrap` now accepts `interviewAnswers`, `apply`, `uninstall`, `audience`, and `interview`
|
|
34
|
+
- capable agents can preview confirmed selective imports through MCP before applying them
|
|
35
|
+
- bootstrap apply/uninstall now covers selective canonical memory imports beyond instructions
|
|
30
36
|
|
|
31
37
|
**Changed**
|
|
32
38
|
- MCP schema version bumped to 0.6.0 to reflect new metadata discovery capabilities
|
package/docs/quickstart.md
CHANGED
|
@@ -103,10 +103,28 @@ brainclaw bootstrap --interview --audience cli
|
|
|
103
103
|
brainclaw bootstrap --interview --audience ide_chat
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Use the returned question IDs to prepare a small JSON answers file when the interview needs to confirm durable memory:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
"question_id": "biq_example",
|
|
112
|
+
"response_items": ["Use agents sequentially in one checkout."],
|
|
113
|
+
"suggestions": []
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Preview the enriched import proposal:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --json
|
|
122
|
+
```
|
|
123
|
+
|
|
106
124
|
### Apply or rollback managed imports
|
|
107
125
|
|
|
108
126
|
```bash
|
|
109
|
-
brainclaw bootstrap --apply
|
|
127
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --apply
|
|
110
128
|
brainclaw bootstrap --uninstall
|
|
111
129
|
```
|
|
112
130
|
|