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,11 +1,20 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { BrainclawLocalReleaseManifestSchema, } from './schema.js';
6
7
  export const DEFAULT_LOCAL_RELEASES_DIR = '.releases';
7
8
  export const DEFAULT_LOCAL_RELEASE_MANIFEST_PATH = `${DEFAULT_LOCAL_RELEASES_DIR}/brainclaw-local.json`;
9
+ export const DEFAULT_NPM_UPDATE_PACKAGE = 'brainclaw';
10
+ export const DEFAULT_NPM_UPDATE_DIST_TAG = 'latest';
11
+ export const DEFAULT_INSTALLABLE_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
8
12
  const SEMVER_RE = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
13
+ const DEFAULT_NPM_UPDATE_SOURCE = {
14
+ type: 'npm',
15
+ package_name: DEFAULT_NPM_UPDATE_PACKAGE,
16
+ dist_tag: DEFAULT_NPM_UPDATE_DIST_TAG,
17
+ };
9
18
  let cachedCliVersion;
10
19
  export function getInstalledBrainclawVersion() {
11
20
  if (cachedCliVersion) {
@@ -87,8 +96,10 @@ export function assessBrainclawVersion(config) {
87
96
  message,
88
97
  };
89
98
  }
90
- export function checkBrainclawInstallableUpdate(config, cwd) {
91
- const source = config?.brainclaw_update_source;
99
+ export function checkBrainclawInstallableUpdate(config, cwd, options = {}) {
100
+ const source = config?.brainclaw_update_source
101
+ ?? (options.useDefaultNpmSource ? DEFAULT_NPM_UPDATE_SOURCE : undefined);
102
+ const defaultSource = !config?.brainclaw_update_source && source?.type === 'npm';
92
103
  if (!source) {
93
104
  return {
94
105
  checked: false,
@@ -103,22 +114,181 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
103
114
  };
104
115
  }
105
116
  if (source.type === 'npm') {
106
- const packageName = source.package_name?.trim() || 'brainclaw';
107
- const distTag = source.dist_tag?.trim() || 'latest';
117
+ return checkNpmInstallableUpdate(source, config, cwd, options, defaultSource);
118
+ }
119
+ return checkLocalPackInstallableUpdate(source.manifest_path, config, cwd);
120
+ }
121
+ export function renderBrainclawInstallableUpdateNotice(updateCheck) {
122
+ if (!updateCheck || updateCheck.status !== 'update_available') {
123
+ return null;
124
+ }
125
+ const lines = [updateCheck.message];
126
+ if (updateCheck.install_command) {
127
+ lines.push(`Install: ${updateCheck.install_command}`);
128
+ }
129
+ if (updateCheck.release_notes) {
130
+ lines.push(`Why update: ${updateCheck.release_notes}`);
131
+ }
132
+ return lines.join('\n');
133
+ }
134
+ export function publishLocalBrainclawRelease(cwd, options = {}) {
135
+ const workspacePackage = readWorkspaceBrainclawPackage(cwd);
136
+ const outputDir = path.resolve(cwd, options.outputDir ?? DEFAULT_LOCAL_RELEASES_DIR);
137
+ const manifestPath = path.resolve(cwd, options.manifestPath ?? DEFAULT_LOCAL_RELEASE_MANIFEST_PATH);
138
+ fs.mkdirSync(outputDir, { recursive: true });
139
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
140
+ const packResult = spawnSync(resolveNpmCommand(), resolveNpmPackArgs(outputDir), {
141
+ cwd,
142
+ encoding: 'utf-8',
143
+ timeout: 120000,
144
+ });
145
+ if (packResult.error) {
146
+ throw new Error(`Failed to run npm pack: ${packResult.error.message}`);
147
+ }
148
+ if (packResult.status !== 0) {
149
+ throw new Error(firstNonEmptyLine(packResult.stderr) ?? firstNonEmptyLine(packResult.stdout) ?? 'npm pack failed');
150
+ }
151
+ const artifactFilename = parsePackedFilename(packResult.stdout);
152
+ if (!artifactFilename) {
153
+ throw new Error('npm pack did not report the generated tarball filename.');
154
+ }
155
+ const artifactAbsolutePath = path.join(outputDir, artifactFilename);
156
+ if (!fs.existsSync(artifactAbsolutePath)) {
157
+ throw new Error(`npm pack reported ${artifactFilename}, but the tarball was not found in ${outputDir}.`);
158
+ }
159
+ const manifestArtifactPath = toManifestRelativePath(path.relative(path.dirname(manifestPath), artifactAbsolutePath));
160
+ const projectArtifactPath = toManifestRelativePath(path.relative(cwd, artifactAbsolutePath));
161
+ const projectManifestPath = toPortablePath(path.relative(cwd, manifestPath));
162
+ const installCommand = `npm install -g "${projectArtifactPath}"`;
163
+ const releaseNotes = options.releaseNotes?.trim() || null;
164
+ const manifest = BrainclawLocalReleaseManifestSchema.parse({
165
+ version: 1,
166
+ channel: 'local-pack',
167
+ package_name: workspacePackage.name,
168
+ latest_installable_version: workspacePackage.version,
169
+ published_at: new Date().toISOString(),
170
+ artifact_path: manifestArtifactPath,
171
+ install_command: installCommand,
172
+ release_notes: releaseNotes ?? undefined,
173
+ });
174
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
175
+ return {
176
+ package_name: workspacePackage.name,
177
+ workspace_version: workspacePackage.version,
178
+ manifest_path: projectManifestPath,
179
+ artifact_path: projectArtifactPath,
180
+ install_command: installCommand,
181
+ release_notes: releaseNotes,
182
+ };
183
+ }
184
+ function checkNpmInstallableUpdate(source, config, cwd, options, defaultSource) {
185
+ const packageName = source.package_name?.trim() || DEFAULT_NPM_UPDATE_PACKAGE;
186
+ const distTag = source.dist_tag?.trim() || DEFAULT_NPM_UPDATE_DIST_TAG;
187
+ if (packageName.length === 0) {
108
188
  return {
109
189
  checked: false,
110
190
  source_type: 'npm',
111
- source_description: `${packageName}@${distTag}`,
191
+ source_description: null,
112
192
  latest_installable_version: null,
113
193
  artifact_path: null,
114
194
  install_command: null,
115
195
  release_notes: null,
116
- status: 'unsupported_source',
117
- message: 'The npm update source is modeled in config but is not implemented yet in this build.',
196
+ status: 'invalid_config',
197
+ message: 'brainclaw_update_source.package_name must not be empty.',
198
+ default_source: defaultSource,
118
199
  };
119
200
  }
120
- const manifestPath = source.manifest_path.trim();
121
- if (manifestPath.length === 0) {
201
+ if (distTag.length === 0) {
202
+ return {
203
+ checked: false,
204
+ source_type: 'npm',
205
+ source_description: packageName,
206
+ latest_installable_version: null,
207
+ artifact_path: null,
208
+ install_command: null,
209
+ release_notes: null,
210
+ status: 'invalid_config',
211
+ message: 'brainclaw_update_source.dist_tag must not be empty.',
212
+ default_source: defaultSource,
213
+ };
214
+ }
215
+ try {
216
+ const lookup = (options.npmLookup ?? lookupNpmDistTags)(packageName, {
217
+ cwd,
218
+ now: options.now,
219
+ cacheTtlMs: options.cacheTtlMs,
220
+ });
221
+ const latestVersion = normalizeConfiguredVersion(lookup.dist_tags[distTag]);
222
+ if (!latestVersion) {
223
+ return {
224
+ checked: true,
225
+ source_type: 'npm',
226
+ source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
227
+ latest_installable_version: null,
228
+ artifact_path: null,
229
+ install_command: null,
230
+ release_notes: config?.brainclaw_upgrade_message?.trim() || null,
231
+ status: 'check_failed',
232
+ message: `The npm channel ${packageName}@${distTag} did not resolve to a valid semver release.`,
233
+ checked_at: lookup.checked_at,
234
+ cached: lookup.cached,
235
+ default_source: defaultSource,
236
+ };
237
+ }
238
+ const installCommand = config?.brainclaw_upgrade_command?.trim()
239
+ || `npm install -g ${packageName}@${latestVersion}`;
240
+ const releaseNotes = config?.brainclaw_upgrade_message?.trim() || null;
241
+ const installedVersion = getInstalledBrainclawVersion();
242
+ if (compareVersions(installedVersion, latestVersion) < 0) {
243
+ return {
244
+ checked: true,
245
+ source_type: 'npm',
246
+ source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
247
+ latest_installable_version: latestVersion,
248
+ artifact_path: null,
249
+ install_command: installCommand,
250
+ release_notes: releaseNotes,
251
+ status: 'update_available',
252
+ message: `A newer installable brainclaw build is available from npm: ${latestVersion} (installed ${installedVersion}).`,
253
+ checked_at: lookup.checked_at,
254
+ cached: lookup.cached,
255
+ default_source: defaultSource,
256
+ };
257
+ }
258
+ return {
259
+ checked: true,
260
+ source_type: 'npm',
261
+ source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
262
+ latest_installable_version: latestVersion,
263
+ artifact_path: null,
264
+ install_command: installCommand,
265
+ release_notes: releaseNotes,
266
+ status: 'up_to_date',
267
+ message: `Installed brainclaw ${installedVersion} is up to date for npm channel ${packageName}@${distTag}.`,
268
+ checked_at: lookup.checked_at,
269
+ cached: lookup.cached,
270
+ default_source: defaultSource,
271
+ };
272
+ }
273
+ catch (error) {
274
+ const message = error instanceof Error ? error.message : String(error);
275
+ return {
276
+ checked: true,
277
+ source_type: 'npm',
278
+ source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
279
+ latest_installable_version: null,
280
+ artifact_path: null,
281
+ install_command: null,
282
+ release_notes: config?.brainclaw_upgrade_message?.trim() || null,
283
+ status: 'check_failed',
284
+ message: `Failed to check npm installable updates: ${message}`,
285
+ default_source: defaultSource,
286
+ };
287
+ }
288
+ }
289
+ function checkLocalPackInstallableUpdate(manifestPath, config, cwd) {
290
+ const trimmedManifestPath = manifestPath.trim();
291
+ if (trimmedManifestPath.length === 0) {
122
292
  return {
123
293
  checked: false,
124
294
  source_type: 'local-pack',
@@ -129,11 +299,12 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
129
299
  release_notes: null,
130
300
  status: 'invalid_config',
131
301
  message: 'brainclaw_update_source.manifest_path must not be empty.',
302
+ default_source: false,
132
303
  };
133
304
  }
134
- const resolvedManifestPath = path.isAbsolute(manifestPath)
135
- ? manifestPath
136
- : path.resolve(cwd, manifestPath);
305
+ const resolvedManifestPath = path.isAbsolute(trimmedManifestPath)
306
+ ? trimmedManifestPath
307
+ : path.resolve(cwd, trimmedManifestPath);
137
308
  if (!fs.existsSync(resolvedManifestPath)) {
138
309
  return {
139
310
  checked: true,
@@ -145,6 +316,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
145
316
  release_notes: null,
146
317
  status: 'check_failed',
147
318
  message: `The configured local-pack manifest was not found: ${resolvedManifestPath}`,
319
+ default_source: false,
148
320
  };
149
321
  }
150
322
  try {
@@ -162,6 +334,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
162
334
  release_notes: manifest.release_notes?.trim() || config?.brainclaw_upgrade_message?.trim() || null,
163
335
  status: 'check_failed',
164
336
  message: `The local-pack manifest has an invalid latest_installable_version: ${manifest.latest_installable_version}`,
337
+ default_source: false,
165
338
  };
166
339
  }
167
340
  const artifactPath = manifest.artifact_path
@@ -182,6 +355,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
182
355
  release_notes: releaseNotes,
183
356
  status: 'update_available',
184
357
  message: `A newer installable brainclaw build is available: ${latestVersion} (installed ${installedVersion}).`,
358
+ default_source: false,
185
359
  };
186
360
  }
187
361
  return {
@@ -194,6 +368,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
194
368
  release_notes: releaseNotes,
195
369
  status: 'up_to_date',
196
370
  message: `Installed brainclaw ${installedVersion} is up to date for the configured local-pack channel.`,
371
+ default_source: false,
197
372
  };
198
373
  }
199
374
  catch (error) {
@@ -208,59 +383,10 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
208
383
  release_notes: null,
209
384
  status: 'check_failed',
210
385
  message: `Failed to read the configured local-pack manifest: ${message}`,
386
+ default_source: false,
211
387
  };
212
388
  }
213
389
  }
214
- export function publishLocalBrainclawRelease(cwd, options = {}) {
215
- const workspacePackage = readWorkspaceBrainclawPackage(cwd);
216
- const outputDir = path.resolve(cwd, options.outputDir ?? DEFAULT_LOCAL_RELEASES_DIR);
217
- const manifestPath = path.resolve(cwd, options.manifestPath ?? DEFAULT_LOCAL_RELEASE_MANIFEST_PATH);
218
- fs.mkdirSync(outputDir, { recursive: true });
219
- fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
220
- const packResult = spawnSync(resolveNpmCommand(), resolveNpmPackArgs(outputDir), {
221
- cwd,
222
- encoding: 'utf-8',
223
- timeout: 120000,
224
- });
225
- if (packResult.error) {
226
- throw new Error(`Failed to run npm pack: ${packResult.error.message}`);
227
- }
228
- if (packResult.status !== 0) {
229
- throw new Error(firstNonEmptyLine(packResult.stderr) ?? firstNonEmptyLine(packResult.stdout) ?? 'npm pack failed');
230
- }
231
- const artifactFilename = parsePackedFilename(packResult.stdout);
232
- if (!artifactFilename) {
233
- throw new Error('npm pack did not report the generated tarball filename.');
234
- }
235
- const artifactAbsolutePath = path.join(outputDir, artifactFilename);
236
- if (!fs.existsSync(artifactAbsolutePath)) {
237
- throw new Error(`npm pack reported ${artifactFilename}, but the tarball was not found in ${outputDir}.`);
238
- }
239
- const manifestArtifactPath = toManifestRelativePath(path.relative(path.dirname(manifestPath), artifactAbsolutePath));
240
- const projectArtifactPath = toManifestRelativePath(path.relative(cwd, artifactAbsolutePath));
241
- const projectManifestPath = toPortablePath(path.relative(cwd, manifestPath));
242
- const installCommand = `npm install -g "${projectArtifactPath}"`;
243
- const releaseNotes = options.releaseNotes?.trim() || null;
244
- const manifest = BrainclawLocalReleaseManifestSchema.parse({
245
- version: 1,
246
- channel: 'local-pack',
247
- package_name: workspacePackage.name,
248
- latest_installable_version: workspacePackage.version,
249
- published_at: new Date().toISOString(),
250
- artifact_path: manifestArtifactPath,
251
- install_command: installCommand,
252
- release_notes: releaseNotes ?? undefined,
253
- });
254
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
255
- return {
256
- package_name: workspacePackage.name,
257
- workspace_version: workspacePackage.version,
258
- manifest_path: projectManifestPath,
259
- artifact_path: projectArtifactPath,
260
- install_command: installCommand,
261
- release_notes: releaseNotes,
262
- };
263
- }
264
390
  function resolveNpmCommand() {
265
391
  return process.platform === 'win32' ? (process.env.ComSpec?.trim() || 'cmd.exe') : 'npm';
266
392
  }
@@ -270,6 +396,12 @@ function resolveNpmPackArgs(outputDir) {
270
396
  }
271
397
  return ['pack', '--json', '--pack-destination', outputDir];
272
398
  }
399
+ function resolveNpmViewArgs(packageName) {
400
+ if (process.platform === 'win32') {
401
+ return ['/d', '/s', '/c', 'npm', 'view', packageName, 'dist-tags', '--json'];
402
+ }
403
+ return ['view', packageName, 'dist-tags', '--json'];
404
+ }
273
405
  function findOwnPackageJson() {
274
406
  let currentDir = path.dirname(fileURLToPath(import.meta.url));
275
407
  while (true) {
@@ -331,6 +463,115 @@ function parsePackedFilename(stdout) {
331
463
  return firstNonEmptyLine(stdout);
332
464
  }
333
465
  }
466
+ function lookupNpmDistTags(packageName, options) {
467
+ const now = options.now ?? new Date();
468
+ const cachePath = resolveNpmUpdateCachePath(packageName);
469
+ const ttlMs = options.cacheTtlMs ?? DEFAULT_INSTALLABLE_UPDATE_CACHE_TTL_MS;
470
+ const cached = readCachedNpmDistTags(cachePath, packageName, now, ttlMs);
471
+ if (cached) {
472
+ return cached;
473
+ }
474
+ const viewResult = spawnSync(resolveNpmCommand(), resolveNpmViewArgs(packageName), {
475
+ cwd: options.cwd,
476
+ encoding: 'utf-8',
477
+ timeout: 15000,
478
+ });
479
+ if (viewResult.error) {
480
+ throw new Error(`npm view failed: ${viewResult.error.message}`);
481
+ }
482
+ if (viewResult.status !== 0) {
483
+ throw new Error(firstNonEmptyLine(viewResult.stderr) ?? firstNonEmptyLine(viewResult.stdout) ?? 'npm view failed');
484
+ }
485
+ const distTags = parseNpmDistTags(viewResult.stdout);
486
+ const result = {
487
+ dist_tags: distTags,
488
+ checked_at: now.toISOString(),
489
+ cached: false,
490
+ };
491
+ writeCachedNpmDistTags(cachePath, {
492
+ version: 1,
493
+ package_name: packageName,
494
+ fetched_at: result.checked_at,
495
+ dist_tags: distTags,
496
+ });
497
+ return result;
498
+ }
499
+ function parseNpmDistTags(stdout) {
500
+ let parsed;
501
+ try {
502
+ parsed = JSON.parse(stdout);
503
+ }
504
+ catch (error) {
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ throw new Error(`npm view returned invalid JSON: ${message}`);
507
+ }
508
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
509
+ throw new Error('npm view did not return a dist-tag object.');
510
+ }
511
+ const distTags = {};
512
+ for (const [key, value] of Object.entries(parsed)) {
513
+ if (typeof value === 'string' && value.trim().length > 0) {
514
+ distTags[key] = value.trim();
515
+ }
516
+ }
517
+ if (Object.keys(distTags).length === 0) {
518
+ throw new Error('npm view returned an empty dist-tag object.');
519
+ }
520
+ return distTags;
521
+ }
522
+ function resolveNpmUpdateCachePath(packageName) {
523
+ const safeName = packageName.replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '') || DEFAULT_NPM_UPDATE_PACKAGE;
524
+ return path.join(os.homedir(), '.brainclaw', 'cache', 'installable-updates', `${safeName}.json`);
525
+ }
526
+ function readCachedNpmDistTags(cachePath, packageName, now, ttlMs) {
527
+ if (!fs.existsSync(cachePath)) {
528
+ return undefined;
529
+ }
530
+ try {
531
+ const parsed = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
532
+ if (parsed.version !== 1
533
+ || parsed.package_name !== packageName
534
+ || typeof parsed.fetched_at !== 'string'
535
+ || !parsed.dist_tags
536
+ || typeof parsed.dist_tags !== 'object'
537
+ || Array.isArray(parsed.dist_tags)) {
538
+ return undefined;
539
+ }
540
+ const fetchedAt = Date.parse(parsed.fetched_at);
541
+ if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > ttlMs) {
542
+ return undefined;
543
+ }
544
+ const distTags = {};
545
+ for (const [key, value] of Object.entries(parsed.dist_tags)) {
546
+ if (typeof value === 'string' && value.trim().length > 0) {
547
+ distTags[key] = value.trim();
548
+ }
549
+ }
550
+ if (Object.keys(distTags).length === 0) {
551
+ return undefined;
552
+ }
553
+ return {
554
+ dist_tags: distTags,
555
+ checked_at: parsed.fetched_at,
556
+ cached: true,
557
+ };
558
+ }
559
+ catch {
560
+ return undefined;
561
+ }
562
+ }
563
+ function writeCachedNpmDistTags(cachePath, document) {
564
+ try {
565
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
566
+ fs.writeFileSync(cachePath, `${JSON.stringify(document, null, 2)}\n`, 'utf-8');
567
+ }
568
+ catch {
569
+ // Cache writes are best-effort only.
570
+ }
571
+ }
572
+ function formatNpmSourceDescription(packageName, distTag, defaultSource) {
573
+ return defaultSource ? `${packageName}@${distTag} (default npm channel)` : `${packageName}@${distTag}`;
574
+ }
334
575
  function firstNonEmptyLine(value) {
335
576
  return value
336
577
  .split(/\r?\n/)
@@ -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,12 +124,16 @@ 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
- Display environment and tooling detection information.
136
+ Display environment, tooling detection, and installable Brainclaw update information.
132
137
 
133
138
  | Option | Description |
134
139
  |---|---|
@@ -141,6 +146,8 @@ brainclaw env --json
141
146
  brainclaw env --agent-tooling
142
147
  ```
143
148
 
149
+ When the project does not pin a local release channel, `brainclaw env` checks the public npm `latest` channel for installable updates. Projects can override that with `brainclaw_update_source`, for example to use `prelaunch` or a local-pack manifest.
150
+
144
151
  ---
145
152
 
146
153
  ## Memory Management
@@ -1248,3 +1255,10 @@ brainclaw version
1248
1255
  brainclaw version --check
1249
1256
  brainclaw version --publish-local --release-notes "Add estimation-report command"
1250
1257
  ```
1258
+
1259
+ `brainclaw version --check` now follows this order:
1260
+
1261
+ - if `brainclaw_update_source` is configured, use it
1262
+ - otherwise, fall back to the public npm channel `brainclaw@latest`
1263
+
1264
+ This keeps end-user installs aware of published npm releases without requiring a local tarball channel. To keep beta testers on a different channel, set `brainclaw_update_source` to `type: npm` with a different `dist_tag`, such as `prelaunch`.
@@ -16,16 +16,19 @@ MCP matters because Brainclaw's value is mostly in dynamic state:
16
16
 
17
17
  Static files still help, but they age immediately. MCP is the stronger path for live coordination.
18
18
 
19
+ That now also includes Brainclaw's own install channel state: `bclaw_get_execution_context` surfaces whether a newer npm or local-pack build is available, so the agent can notice upgrades without relying on a human to run `brainclaw version --check`.
20
+
19
21
  ## Recommended Agent Pattern
20
22
 
21
23
  The default dynamic workflow is:
22
24
 
23
25
  1. `bclaw_session_start` to open work and get the current board/context
24
- 2. `bclaw_get_context` when the target path or task changes
25
- 3. `bclaw_list_plans` and `bclaw_list_claims` to inspect active work
26
- 4. `bclaw_claim` before editing
27
- 5. `bclaw_write_note` for runtime observations
28
- 6. `bclaw_session_end` to close cleanly and hand work off
26
+ 2. `bclaw_get_execution_context` early in the session when the agent needs local tooling signals or package update visibility
27
+ 3. `bclaw_get_context` when the target path or task changes
28
+ 4. `bclaw_list_plans` and `bclaw_list_claims` to inspect active work
29
+ 5. `bclaw_claim` before editing
30
+ 6. `bclaw_write_note` for runtime observations
31
+ 7. `bclaw_session_end` to close cleanly and hand work off
29
32
 
30
33
  This keeps session continuity inside Brainclaw instead of pushing the agent back to manual CLI usage.
31
34
 
@@ -34,8 +37,8 @@ This keeps session continuity inside Brainclaw instead of pushing the agent back
34
37
  | Tool | Purpose |
35
38
  |---|---|
36
39
  | `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 |
38
- | `bclaw_get_execution_context` | Inspect local execution context and agent tooling |
40
+ | `bclaw_bootstrap` | Derive brownfield bootstrap signals, return adaptive interview prompts, accept structured interview answers, and preview/apply a selective import proposal |
41
+ | `bclaw_get_execution_context` | Inspect local execution context, installable update status, and agent tooling |
39
42
  | `bclaw_write_note` | Record a runtime note, supports `autoReflect: true` |
40
43
  | `bclaw_read_handoff` | Read active handoffs |
41
44
  | `bclaw_get_agent_board` | Coordination snapshot |
@@ -65,6 +68,25 @@ brainclaw mcp
65
68
 
66
69
  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
70
 
71
+ By default, installable update checks use the public npm channel `brainclaw@latest`. Projects that need a different channel can override `brainclaw_update_source`, for example with `type: npm` and `dist_tag: prelaunch`, or with `type: local-pack` for local tarball workflows.
72
+
73
+ ## Bootstrap Through MCP
74
+
75
+ For agent-first onboarding, `bclaw_bootstrap` is the nominal path:
76
+
77
+ 1. call `bclaw_bootstrap` to get the current `import_plan` and adaptive interview questions
78
+ 2. collect answers in the agent surface
79
+ 3. call `bclaw_bootstrap` again with `interviewAnswers` to preview confirmed `decision`, `constraint`, `instruction`, or `trap` suggestions
80
+ 4. call `bclaw_bootstrap` with `apply: true` to create canonical memory
81
+ 5. call `bclaw_bootstrap` with `uninstall: true` to revert the last bootstrap-managed import
82
+
83
+ Interview answers are keyed by question ID and may contain:
84
+
85
+ - `response_text`
86
+ - `response_items`
87
+ - `response_boolean`
88
+ - optional explicit `suggestions` when the agent wants to confirm exact canonical memory items
89
+
68
90
  ## Important Rule
69
91
 
70
92
  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