@boopkit/cli 0.6.11 → 0.6.13

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 CHANGED
@@ -24,7 +24,8 @@ boop init my-app
24
24
  cd my-app
25
25
  ```
26
26
 
27
- `boop init` creates the project directory, applies the `web-app` profile, installs capability packages from npm, and scaffolds files into your project — including a `.boop.json` manifest that records what profile and capabilities are installed.
27
+ `boop init` creates the project directory, applies the `web-app` profile, installs capability packages from npm, and scaffolds files into your project — including a `.boop/config.json` manifest that records what profile and capabilities are installed.
28
+ It finishes by committing the scaffold to a new git repository and setting up the project's beads issue tracker in Dolt server mode.
28
29
 
29
30
  ```bash
30
31
  # See what was installed, what's configured, and what to do next
@@ -52,7 +53,9 @@ boop add users-and-accounts
52
53
 
53
54
  Start simple, grow deliberately. That's the shape of a boop project: a strong starting point you can extend without collecting scaffolding debt.
54
55
 
55
- A **profile** is a named starting point. It defines the kernel (TypeScript config, `package.json`, `src/server.ts`, deployment setup) and a set of default capabilities.
56
+ A **profile** is a named starting point.
57
+ It defines a set of default capabilities, and what `boop init` writes before they are installed:
58
+ either a kernel (TypeScript config, `package.json`, `src/server.ts`, deployment setup) or a `templates/` folder of its own.
56
59
 
57
60
  A **capability** is a composable feature bundle. Each capability package ships:
58
61
 
@@ -67,7 +70,8 @@ Capabilities can declare `requires` relationships (e.g. `users-and-accounts` req
67
70
 
68
71
  ## Profiles
69
72
 
70
- A profile names a set of capabilities. Two ship today.
73
+ A profile names a set of capabilities.
74
+ Three ship today, and `boop init` can scaffold from two of them.
71
75
 
72
76
  ### `web-app`
73
77
 
@@ -75,7 +79,19 @@ Full-stack web application — Express + Handlebars + Tailwind CSS + PostgreSQL
75
79
 
76
80
  Ships with application capabilities (`web`, `api`, `services`, `database`), development skills (`coding`, `testing`, `project-mgmt`, `product-design`, `skills`, `workflows`), and the `hello` demo capability.
77
81
 
78
- This is the profile `boop init` scaffolds from — it carries a kernel, so there is an application to write.
82
+ This is the default profile for `boop init`.
83
+ It carries a kernel, so there is an application to write.
84
+
85
+ ### `base`
86
+
87
+ Planning, writing and issue tracking for a project that isn't an application: setting up an AWS environment, a research project, a migration plan.
88
+ Ships with `project-mgmt`, `product-design` and `skills`, and nothing code-shaped.
89
+
90
+ It carries no kernel, but it ships its own templates,
91
+ so `boop init my-project --profile base` writes a project with `specs/`, `reference/` and `docs/` folders,
92
+ agent instructions for work whose output is documents and tracked issues,
93
+ and a beads issue tracker ready to use.
94
+ When the project grows code of its own, `boop add coding` brings in the coding skills.
79
95
 
80
96
  ### `development`
81
97
 
@@ -83,7 +99,10 @@ The development capabilities on their own (`coding`, `testing`, `project-mgmt`,
83
99
 
84
100
  For a project with no application domain boop has anything to say about: a CLI, a library, a data pipeline. Those share no application layers at all, but they are developed the same way.
85
101
 
86
- It carries no kernel — no runtime, no port, no deploy target — so there is nothing for `boop init` to scaffold and `boop init --profile development` is refused. Use it by naming it as the `profile` in an existing project's `.boop/config.json`, or reach for `boop add` on the capabilities you want. Boop's own repository is the first project to declare it.
102
+ It carries neither a kernel nor templates — no runtime, no port, no deploy target, and no files of its own to write —
103
+ so there is nothing for `boop init` to scaffold and `boop init --profile development` is refused.
104
+ Use it by naming it as the `profile` in an existing project's `.boop/config.json`, or reach for `boop add` on the capabilities you want.
105
+ Boop's own repository is the first project to declare it.
87
106
 
88
107
  List profiles with `boop list profiles`.
89
108
 
@@ -173,6 +192,7 @@ Most days you'll use `init`, `add`, and `status`. The rest are there when you ne
173
192
  Flags:
174
193
 
175
194
  - `boop init --no-install` / `boop add --no-install` — skip `npm install`
195
+ - `boop init --no-beads` — skip the scaffold commit and `bd init`, so no git repository, beads database or Dolt server is created
176
196
  - `boop add --dry-run` — preview changes without writing
177
197
  - `boop update --force` — overwrite locally modified scaffold files (creates `.bak` backups)
178
198
  - `boop update --prune` — remove scaffold files no longer shipped by the capability
@@ -214,6 +234,7 @@ Boop itself is light — Node and npm. The other requirements come from whicheve
214
234
  - **Node.js 24+** (the `web-app` profile enforces this)
215
235
  - **npm** (or any package manager that respects workspaces)
216
236
  - **beads (`bd`) 1.1.2+** — if you use the agent workflows or `boop loop`. The tree-building workflows depend on `bd gate` and `bd epic close-eligible` (1.1.x) and on 1.1.2 dependency semantics (plain `blocks` edges; `waits-for` is not used), and `boop loop` needs beads in Dolt server mode. See [beads](https://github.com/gastownhall/beads).
237
+ - **`jq`** — optional, and only for `boop loop`. The loop projects its beads queue read through `jq` to keep it small; without `jq` it warns once and falls back to a larger read that still works. `brew install jq` on macOS.
217
238
  - **PostgreSQL** — if the `database` capability is installed
218
239
  - **RabbitMQ / CloudAMQP account** — if `background-jobs` is installed
219
240
  - **SendGrid account** — if `email` is installed
@@ -75,6 +75,17 @@
75
75
  "web"
76
76
  ]
77
77
  },
78
+ {
79
+ "name": "oauth-server",
80
+ "description": "Be an OAuth 2.1 authorization server and resource server, so Claude, Claude Code or any MCP client can act on behalf of one of your users",
81
+ "type": "application",
82
+ "package": "@boopkit/cap-oauth-server",
83
+ "requires": [
84
+ "users-and-accounts",
85
+ "web",
86
+ "database"
87
+ ]
88
+ },
78
89
  {
79
90
  "name": "product-design",
80
91
  "description": "Product design skills",
@@ -8,12 +8,13 @@
8
8
  "@boopkit/cap-hello": "0.4.1",
9
9
  "@boopkit/cap-noticeboard": "0.4.1",
10
10
  "@boopkit/cap-oauth-client": "0.1.1",
11
+ "@boopkit/cap-oauth-server": "0.1.2",
11
12
  "@boopkit/cap-product-design": "0.6.7",
12
13
  "@boopkit/cap-project-mgmt": "0.4.9",
13
14
  "@boopkit/cap-services": "0.4.2",
14
15
  "@boopkit/cap-skills": "0.5.4",
15
16
  "@boopkit/cap-testing": "0.5.6",
16
- "@boopkit/cap-users": "0.7.0",
17
+ "@boopkit/cap-users": "0.8.0",
17
18
  "@boopkit/cap-ux": "0.2.2",
18
19
  "@boopkit/cap-web": "0.5.4",
19
20
  "@boopkit/cap-workflows": "0.2.0",
package/dist/cli.js CHANGED
@@ -57,7 +57,8 @@ program
57
57
  .argument('<name>', 'Project name')
58
58
  .option('-p, --profile <profile>', 'Profile to use', 'web-app')
59
59
  .option('--no-install', 'Skip npm install')
60
- .action(init_1.runInit);
60
+ .option('--no-beads', 'Skip bd init, so no beads database or Dolt server is created')
61
+ .action((name, options) => (0, init_1.runInit)(name, options));
61
62
  program
62
63
  .command('list')
63
64
  .description('List available profiles or capabilities')
@@ -1,12 +1,56 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.runAdd = runAdd;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
4
39
  const child_process_1 = require("child_process");
5
40
  const preflight_1 = require("../lib/preflight");
6
41
  const execute_1 = require("../lib/execute");
7
42
  const interpolate_1 = require("../lib/interpolate");
8
43
  const list_1 = require("./list");
9
44
  const capability_1 = require("../lib/capability");
45
+ function hasDevScript(projectDir) {
46
+ try {
47
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
48
+ return Boolean(pkg.scripts?.dev);
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
10
54
  function runAdd(capabilityName, options) {
11
55
  // 0. Validate capability argument is provided
12
56
  if (!capabilityName) {
@@ -110,7 +154,10 @@ function runAdd(capabilityName, options) {
110
154
  nextSteps.push({ text: `Run \`npm run ${first}\` — ${desc}` });
111
155
  }
112
156
  }
113
- nextSteps.push({ text: 'Run `npm run dev` to start developing' });
157
+ // A project with no application code, such as a base scaffold, has no dev script
158
+ if (hasDevScript(targetDir)) {
159
+ nextSteps.push({ text: 'Run `npm run dev` to start developing' });
160
+ }
114
161
  if (options.json) {
115
162
  console.log(JSON.stringify({
116
163
  capability: capabilityName,
@@ -177,7 +224,7 @@ function runAdd(capabilityName, options) {
177
224
  console.log(` ${(0, interpolate_1.interpolate)(pi.verify, vars)}`);
178
225
  }
179
226
  }
180
- else {
227
+ else if (nextSteps.length > 0) {
181
228
  console.log('\nNext steps:');
182
229
  for (let i = 0; i < nextSteps.length; i++) {
183
230
  console.log(` ${i + 1}. ${nextSteps[i].text}`);
@@ -1,5 +1,21 @@
1
1
  export interface InitOptions {
2
2
  profile?: string;
3
3
  install?: boolean;
4
+ beads?: boolean;
4
5
  }
5
- export declare function runInit(name: string, options: InitOptions): void;
6
+ type RunFn = (file: string, args: string[], opts: {
7
+ cwd: string;
8
+ stdio: 'inherit' | 'pipe';
9
+ }) => unknown;
10
+ /** Replaceable in tests, so no test runs a real git or `bd init`, or starts a Dolt server. */
11
+ export interface InitDeps {
12
+ findBdFn?: (projectDir: string) => string | null;
13
+ execFileSyncFn?: RunFn;
14
+ }
15
+ /**
16
+ * Locate the bd binary: the project's own install first, because project-mgmt
17
+ * pins @beads/bd as a devDependency, then whatever is on PATH.
18
+ */
19
+ export declare function findBd(projectDir: string, searchPath?: string): string | null;
20
+ export declare function runInit(name: string, options: InitOptions, deps?: InitDeps): void;
21
+ export {};
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findBd = findBd;
36
37
  exports.runInit = runInit;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
@@ -44,9 +45,105 @@ const scaffold_1 = require("../lib/scaffold");
44
45
  const project_1 = require("../lib/project");
45
46
  const env_1 = require("../lib/env");
46
47
  const safe_io_1 = require("../lib/safe-io");
47
- function runInit(name, options) {
48
+ /**
49
+ * Locate the bd binary: the project's own install first, because project-mgmt
50
+ * pins @beads/bd as a devDependency, then whatever is on PATH.
51
+ */
52
+ function findBd(projectDir, searchPath = process.env.PATH ?? '') {
53
+ const dirs = [path.join(projectDir, 'node_modules', '.bin'), ...searchPath.split(path.delimiter).filter(Boolean)];
54
+ for (const dir of dirs) {
55
+ const candidate = path.join(dir, 'bd');
56
+ try {
57
+ fs.accessSync(candidate, fs.constants.X_OK);
58
+ return candidate;
59
+ }
60
+ catch {
61
+ // not in this directory — keep looking
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ function git(run, cwd, args) {
67
+ return String(run('git', args, { cwd, stdio: 'pipe' })).trim();
68
+ }
69
+ /** The top level of the repository the directory sits inside, or null when it is in none. */
70
+ function findEnclosingRepo(dir, run) {
71
+ try {
72
+ return git(run, dir, ['rev-parse', '--show-toplevel']) || null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ /** Only an identity the user configured counts; git would otherwise invent one from the hostname. */
79
+ function hasGitIdentity(dir, run) {
80
+ try {
81
+ return ['user.name', 'user.email'].every(key => git(run, dir, ['config', key]) !== '');
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ /**
88
+ * Commit the scaffold in a new repository, so that bd init's commit is the
89
+ * second one rather than the first, and git status is clean afterwards.
90
+ * bd init would otherwise create the repository and commit only the files
91
+ * it touched, leaving the rest of the scaffold untracked.
92
+ */
93
+ function commitScaffold(targetDir, name, run) {
94
+ if (!hasGitIdentity(targetDir, run)) {
95
+ console.error('Warning: Did not commit the scaffold: git has no user.name and user.email configured, and boop does not set them for you. ' +
96
+ 'Set them with git config --global, then commit the scaffold yourself.');
97
+ return;
98
+ }
99
+ try {
100
+ console.log('\nCommitting the scaffold...');
101
+ git(run, targetDir, ['init']);
102
+ git(run, targetDir, ['add', '-A']);
103
+ git(run, targetDir, ['commit', '-q', '-m', `chore: scaffold ${name} with boop init`]);
104
+ }
105
+ catch (err) {
106
+ console.error(`Warning: Unable to commit the scaffold: ${err.message}`);
107
+ }
108
+ }
109
+ /** Commit the scaffold, then run `bd init` in the new project; report and return false rather than throw. */
110
+ function initBeads(targetDir, name, args, deps) {
111
+ const findBdFn = deps.findBdFn ?? findBd;
112
+ const execFileSyncFn = deps.execFileSyncFn ?? child_process_1.execFileSync;
113
+ const bd = findBdFn(targetDir);
114
+ if (!bd) {
115
+ console.error('Warning: Unable to initialize beads: bd was not found on PATH or in node_modules/.bin. ' +
116
+ 'Install it (npm install, or brew install beads) and run the bd init command below.');
117
+ return false;
118
+ }
119
+ // Observed with bd 1.1.2: inside an enclosing repository, bd init commits its
120
+ // files into that repository and points its core.hooksPath at a folder that
121
+ // does not exist, silently disabling the hooks it had.
122
+ const enclosingRepo = findEnclosingRepo(targetDir, execFileSyncFn);
123
+ if (enclosingRepo) {
124
+ console.error(`Warning: Skipped beads setup: the project is inside another git repository, ${enclosingRepo}. ` +
125
+ 'bd init would commit into that repository and redirect its git hooks. ' +
126
+ 'To give the project a repository of its own, run git init and commit the scaffold inside it, then run the bd init command below.');
127
+ return false;
128
+ }
129
+ commitScaffold(targetDir, name, execFileSyncFn);
130
+ try {
131
+ console.log('\nInitializing beads...');
132
+ execFileSyncFn(bd, args, { cwd: targetDir, stdio: 'inherit' });
133
+ return true;
134
+ }
135
+ catch (err) {
136
+ console.error(`Warning: Unable to initialize beads: ${err.message}`);
137
+ console.error('The project was scaffolded and left in place; run the bd init command below from inside it.');
138
+ return false;
139
+ }
140
+ }
141
+ function runInit(name, options, deps = {}) {
48
142
  const profileName = options.profile ?? 'web-app';
49
143
  const shouldInstall = options.install !== false;
144
+ // BOOP_SKIP_BEADS is set for the whole test suite (tests/jest.setup.ts), so the
145
+ // many tests that run init never start a Dolt server.
146
+ const shouldInitBeads = options.beads !== false && !process.env.BOOP_SKIP_BEADS;
50
147
  // --- Validate ---
51
148
  // Guard: project name must not be empty
52
149
  if (!name || !name.trim()) {
@@ -55,6 +152,10 @@ function runInit(name, options) {
55
152
  return;
56
153
  }
57
154
  const targetDir = path.resolve(process.cwd(), name);
155
+ // The name argument can be a path. The project is named after its folder,
156
+ // because a path is not a valid npm package name, a heading, or a beads
157
+ // prefix; the raw argument is kept only for messages about the folder.
158
+ const projectName = path.basename(targetDir);
58
159
  // Guard: target directory must not exist or must be empty
59
160
  if (fs.existsSync(targetDir)) {
60
161
  const entries = fs.readdirSync(targetDir);
@@ -71,21 +172,22 @@ function runInit(name, options) {
71
172
  process.exitCode = 1;
72
173
  return;
73
174
  }
74
- // --- Guard: a profile with no kernel has no application to scaffold ---
75
- // `boop init` exists to write a running project from kernel templates. A
76
- // profile that names only development capabilities (profiles/development/)
77
- // has no runtime, port or deploy target to render them with, so refuse here
78
- // rather than emit a project that cannot start.
79
- if (!profile.kernel) {
80
- console.error(`Error: Profile "${profileName}" carries no kernel, so there is nothing to scaffold.`);
175
+ // --- Guard: a profile needs a kernel or templates of its own to scaffold ---
176
+ // The kernel templates render a running application, so a profile with no
177
+ // kernel (profiles/development/) has no runtime, port or deploy target to
178
+ // render them with. A kernel-less profile that ships its own templates/
179
+ // (profiles/base/) renders those instead; one with neither is refused here
180
+ // rather than emitting a project that cannot start.
181
+ const kernel = profile.kernel;
182
+ if (!kernel && !(0, scaffold_1.profileHasTemplates)(profileName)) {
183
+ console.error(`Error: Profile "${profileName}" carries neither a kernel nor a templates/ folder, so there is nothing to scaffold.`);
81
184
  console.error('It declares capabilities for a project that already exists: name it as the "profile" in that');
82
185
  console.error(`project's .boop/config.json, or run \`boop add\` for the capabilities you want.`);
83
186
  process.exitCode = 1;
84
187
  return;
85
188
  }
86
- const kernel = profile.kernel;
87
- // --- Node version check ---
88
- const requiredNodeVersion = kernel.nodeVersion;
189
+ // --- Node version check (a profile with no kernel names no runtime to check) ---
190
+ const requiredNodeVersion = kernel?.nodeVersion;
89
191
  if (requiredNodeVersion && !process.env.BOOP_SKIP_VERSION_CHECK) {
90
192
  const minVersion = requiredNodeVersion.replace(/^[>=^~]+/, '');
91
193
  const currentMajor = parseInt(process.versions.node.split('.')[0], 10);
@@ -138,18 +240,23 @@ function runInit(name, options) {
138
240
  }
139
241
  // --- Scaffold kernel ---
140
242
  const variables = {
141
- projectName: name,
142
- port: kernel.port,
143
- scripts: kernel.scripts,
144
- nodeVersion: kernel.nodeVersion ?? '>=18.0.0',
243
+ projectName,
244
+ ...(kernel && {
245
+ port: kernel.port,
246
+ scripts: kernel.scripts,
247
+ nodeVersion: kernel.nodeVersion ?? '>=18.0.0',
248
+ }),
145
249
  ...hookVars,
146
250
  };
147
- const kernelFiles = (0, scaffold_1.scaffoldKernel)(targetDir, variables);
251
+ const templatesDir = (0, scaffold_1.getTemplatesDir)(profileName);
252
+ const kernelFiles = (0, scaffold_1.scaffoldKernel)(targetDir, variables, false, templatesDir);
148
253
  // --- Merge kernel dependencies ---
149
- (0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
254
+ if (kernel) {
255
+ (0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
256
+ }
150
257
  // --- Create minimal manifest so capability hash storage can find config.json ---
151
258
  (0, project_1.writeProjectManifest)(targetDir, {
152
- name,
259
+ name: projectName,
153
260
  profile: profileName,
154
261
  boopVersion: '0.1.0',
155
262
  capabilities: [],
@@ -193,10 +300,12 @@ function runInit(name, options) {
193
300
  variables.hookInit = hookVars.hookInit;
194
301
  variables.hookPreRoutes = hookVars.hookPreRoutes;
195
302
  variables.hookPostRoutes = hookVars.hookPostRoutes;
196
- (0, scaffold_1.scaffoldKernel)(targetDir, variables, true);
303
+ (0, scaffold_1.scaffoldKernel)(targetDir, variables, true, templatesDir);
197
304
  // Re-scaffolding overwrites package.json (template has empty deps),
198
305
  // so re-merge kernel deps + cap package deps that were added earlier.
199
- (0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
306
+ if (kernel) {
307
+ (0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
308
+ }
200
309
  (0, scaffold_1.mergePackageJson)(targetDir, undefined, undefined, capDeps, { skipBackup: true });
201
310
  }
202
311
  // Post-install conflict check (when preflight was deferred)
@@ -294,12 +403,12 @@ function runInit(name, options) {
294
403
  }
295
404
  // --- Generate .env and .env.example ---
296
405
  if (envEntries.length > 0) {
297
- (0, env_1.generateEnvFiles)(targetDir, envEntries, name);
406
+ (0, env_1.generateEnvFiles)(targetDir, envEntries, projectName);
298
407
  }
299
408
  // --- Write final manifest, preserving capabilityMeta from hash storage ---
300
409
  const existingManifest = (0, project_1.readProjectManifest)(targetDir);
301
410
  const manifest = {
302
- name,
411
+ name: projectName,
303
412
  profile: profileName,
304
413
  boopVersion: '0.1.0',
305
414
  capabilities: installedCapabilities,
@@ -309,10 +418,10 @@ function runInit(name, options) {
309
418
  };
310
419
  (0, project_1.writeProjectManifest)(targetDir, manifest);
311
420
  // --- Print summary ---
312
- console.log(`\nCreated project "${name}" with profile "${profileName}"\n`);
421
+ console.log(`\nCreated project "${projectName}" with profile "${profileName}"\n`);
313
422
  const scriptCount = Object.keys(allScripts).length;
314
423
  const parts = [
315
- `${kernelFiles.length} kernel files`,
424
+ `${kernelFiles.length} ${kernel ? 'kernel' : 'template'} files`,
316
425
  `${installedCapabilities.length} capabilities`,
317
426
  `${capabilityFiles.length} capability files`,
318
427
  ...(scriptCount > 0 ? [`${scriptCount} scripts`] : []),
@@ -375,11 +484,26 @@ function runInit(name, options) {
375
484
  }
376
485
  }
377
486
  }
487
+ // --- Initialize beads (project-mgmt brings it) ---
488
+ // Last, because bd init appends to AGENTS.md, CLAUDE.md and .gitignore when
489
+ // they exist but creates them when they don't, and the templates never
490
+ // overwrite an existing file. Server mode, because boop loop refuses a
491
+ // project whose beads runs embedded. Git is touched only here, just before
492
+ // bd init, because that is when a repository gets created anyway. bd derives
493
+ // its database name from the prefix.
494
+ const bdInitArgs = ['init', '--non-interactive', '--server', '-p', projectName];
495
+ const beadsPending = installedCapabilities.includes('project-mgmt') &&
496
+ !(shouldInitBeads && initBeads(targetDir, projectName, bdInitArgs, deps));
378
497
  console.log(`\nNext steps:`);
379
498
  console.log(` cd ${name}`);
380
499
  if (!shouldInstall) {
381
500
  console.log(' npm install');
382
501
  }
502
+ if (beadsPending) {
503
+ console.log(` bd ${bdInitArgs.join(' ')}`);
504
+ }
383
505
  console.log(' boop status ← check what needs configuration');
384
- console.log(' npm run dev ← start development server');
506
+ if (kernel) {
507
+ console.log(' npm run dev ← start development server');
508
+ }
385
509
  }
@@ -39,6 +39,7 @@ export interface MergeDeps {
39
39
  execSyncFn?: (cmd: string, opts?: {
40
40
  cwd?: string;
41
41
  stdio?: unknown;
42
+ maxBuffer?: number;
42
43
  }) => string;
43
44
  existsSyncFn?: (p: string) => boolean;
44
45
  readFileFn?: (p: string) => string;
@@ -138,7 +138,9 @@ function defaultBdList(deps) {
138
138
  const execSyncFn = deps.execSyncFn || defaultExec;
139
139
  return (status) => {
140
140
  try {
141
- const raw = execSyncFn(`bd list --status=${status} --json 2>/dev/null`);
141
+ const raw = execSyncFn(`bd list --status=${status} --json 2>/dev/null`, {
142
+ maxBuffer: loop_1.BD_READ_MAX_BUFFER,
143
+ });
142
144
  return JSON.parse(raw);
143
145
  }
144
146
  catch {
@@ -150,7 +152,7 @@ function defaultGetParent(deps) {
150
152
  const execSyncFn = deps.execSyncFn || defaultExec;
151
153
  return (id) => {
152
154
  try {
153
- const data = JSON.parse(execSyncFn(`bd show ${id} --json 2>/dev/null`));
155
+ const data = JSON.parse(execSyncFn(`bd show ${id} --json 2>/dev/null`, { maxBuffer: loop_1.BD_READ_MAX_BUFFER }));
154
156
  return (data[0] || data).parent || null;
155
157
  }
156
158
  catch {
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Output cap for every `bd … --json` read the loop makes.
3
+ *
4
+ * Node's execSync defaults to 1 MiB and throws ENOBUFS past it. `bd ready --json`
5
+ * returns each issue WITH its full description, and `bd show --children --json`
6
+ * on a large container is the same trap — a mature backlog crosses 1 MiB and the
7
+ * read dies. The ready-queue read also projects its output (see
8
+ * READY_QUEUE_CMD); this cap is the backstop for the reads that cannot (boop-fl22).
9
+ */
10
+ export declare const BD_READ_MAX_BUFFER: number;
1
11
  export interface LoopOptions {
2
12
  run?: boolean;
3
13
  prefix?: string;
@@ -111,7 +121,7 @@ export declare function writeWorktreeMetadata(ctx: WorktreeContext, meta: {
111
121
  interface ReadyIssue {
112
122
  id: string;
113
123
  issue_type: string;
114
- parent?: string;
124
+ parent?: string | null;
115
125
  }
116
126
  export declare function buildPlanningPrompt(workContent: string, flowContent: string, prefix?: string): string;
117
127
  export type RunClaudeFn = (prompt: string, opts: {
@@ -135,6 +145,7 @@ export declare function planWork(opts: {
135
145
  runClaudeFn?: RunClaudeFn;
136
146
  execSyncFn?: (cmd: string, opts: {
137
147
  encoding: string;
148
+ maxBuffer?: number;
138
149
  }) => string;
139
150
  }): Promise<PlanResult>;
140
151
  export declare function findFallbackFeature(features: Array<{
@@ -165,7 +176,7 @@ export declare function resolveModel(optionModel: string | undefined, env?: Node
165
176
  export declare function shouldResumeUnclosedIssue(status: string | undefined): boolean;
166
177
  export declare function isIssueInScope(issue: {
167
178
  id: string;
168
- parent?: string;
179
+ parent?: string | null;
169
180
  }, prefix: string, getParentById: (id: string) => string | null): boolean;
170
181
  /**
171
182
  * A workflow-owned feature root is a feature-type issue that owns child tasks
@@ -196,9 +207,17 @@ export declare function shouldCloseFeatureRoot(feature: {
196
207
  } | null, children: {
197
208
  status: string;
198
209
  }[]): boolean;
210
+ /**
211
+ * Read the ready queue, filtered to what this loop may claim.
212
+ *
213
+ * THROWS on a failed read. It must never answer a broken read with an empty
214
+ * array: the caller treats "no issues" as a clean finish, so a swallowed
215
+ * failure makes the loop exit 0 with work undone (boop-fl22).
216
+ */
199
217
  export declare function getMatchingReadyIssues(prefix: string, includeFeatures: boolean, deps?: {
200
218
  execSyncFn?: (cmd: string, opts: {
201
219
  encoding: string;
220
+ maxBuffer?: number;
202
221
  }) => string;
203
222
  }): ReadyIssue[];
204
223
  /**