brainclaw 0.19.10 → 0.19.12

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.
@@ -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') {