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 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;
@@ -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('');
@@ -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: renderBootstrapSummary(result) }],
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,
@@ -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: listBootstrapSeeds(cwd),
58
- importPlan: existingPlan,
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: artifacts.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 = suggestionIdentityKey(suggestion);
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 = suggestionIdentityKey(suggestion);
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: generateId('bootstrap_suggestions'),
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 suggestionIdentityKey(suggestion) {
972
- return instructionIdentityKey(suggestion.text, suggestion.layer ?? 'global', suggestion.scope);
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
- if (suggestion.target !== 'instruction') {
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
- const identityKey = suggestionIdentityKey(suggestion);
1002
- if (activeInstructionKeys.has(identityKey)) {
1003
- skippedCount++;
1004
- continue;
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
- const entry = createInstruction(suggestion.text, {
1007
- layer: suggestion.layer ?? 'global',
1008
- scope: suggestion.scope,
1009
- tags: suggestion.tags,
1010
- author: resolveCurrentAgentName(cwd),
1011
- }, cwd);
1012
- activeInstructionKeys.add(identityKey);
1013
- managedArtifacts.push({
1014
- kind: 'instruction',
1015
- id: entry.id,
1016
- suggestion_id: suggestion.id,
1017
- rollback_action: 'deactivate',
1018
- });
1019
- createdCount++;
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 !== 'instruction') {
1058
- skippedCount++;
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 instruction = instructions.find((entry) => entry.id === artifact.id);
1062
- if (!instruction || !instruction.active) {
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
- instruction.active = false;
1067
- instruction.updated_at = nowISO();
1068
- saveInstruction(instruction, resolvedCwd);
1069
- deactivatedCount++;
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') {
@@ -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.
@@ -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, and an import proposal when memory is still sparse |
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
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "0.19.10",
3
+ "version": "0.19.11",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {