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.
- package/README.md +3 -0
- package/dist/cli.js +1 -0
- package/dist/commands/bootstrap.js +17 -1
- package/dist/commands/env.js +7 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/mcp.js +76 -3
- package/dist/commands/version.js +3 -1
- package/dist/commands/whoami.js +11 -1
- package/dist/core/bootstrap.js +416 -60
- package/dist/core/brainclaw-version.js +303 -62
- package/dist/core/schema.js +28 -0
- package/docs/cli.md +15 -1
- package/docs/integrations/mcp.md +29 -7
- package/docs/mcp-schema-changelog.md +6 -0
- package/docs/quickstart.md +22 -1
- package/package.json +1 -1
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') {
|