@boopkit/cli 0.6.12 → 0.6.14

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.
Files changed (43) hide show
  1. package/README.md +27 -6
  2. package/dist/capability-versions.json +16 -16
  3. package/dist/cli.js +9 -1
  4. package/dist/commands/add.js +49 -2
  5. package/dist/commands/init.d.ts +17 -1
  6. package/dist/commands/init.js +151 -26
  7. package/dist/commands/loop-merge.d.ts +1 -0
  8. package/dist/commands/loop-merge.js +4 -2
  9. package/dist/commands/loop.d.ts +21 -2
  10. package/dist/commands/loop.js +105 -16
  11. package/dist/commands/loop.js.orig-pre-maxbuffer +1455 -0
  12. package/dist/commands/migrate-skill-edits.d.ts +24 -0
  13. package/dist/commands/migrate-skill-edits.js +190 -0
  14. package/dist/commands/status.d.ts +8 -0
  15. package/dist/commands/status.js +13 -0
  16. package/dist/commands/update.js +55 -7
  17. package/dist/lib/apply-skill-edits.d.ts +76 -0
  18. package/dist/lib/apply-skill-edits.js +307 -0
  19. package/dist/lib/execute.js +1 -0
  20. package/dist/lib/original-text.d.ts +43 -0
  21. package/dist/lib/original-text.js +165 -0
  22. package/dist/lib/scaffold.d.ts +41 -7
  23. package/dist/lib/scaffold.js +96 -15
  24. package/dist/lib/skill-edits.d.ts +88 -0
  25. package/dist/lib/skill-edits.js +293 -0
  26. package/dist/lib/update.d.ts +38 -2
  27. package/dist/lib/update.js +149 -6
  28. package/docs/init.md +132 -86
  29. package/docs/loop.md +16 -0
  30. package/docs/skills.md +1 -0
  31. package/package.json +3 -2
  32. package/profiles/base/profile.md +105 -0
  33. package/profiles/base/templates/.github/copilot-instructions.md +3 -0
  34. package/profiles/base/templates/.gitignore.hbs +6 -0
  35. package/profiles/base/templates/AGENTS.md.hbs +50 -0
  36. package/profiles/base/templates/CLAUDE.md.hbs +3 -0
  37. package/profiles/base/templates/README.md.hbs +15 -0
  38. package/profiles/base/templates/docs/README.md +12 -0
  39. package/profiles/base/templates/package.json.hbs +7 -0
  40. package/profiles/base/templates/reference/README.md +16 -0
  41. package/profiles/base/templates/specs/README.md +12 -0
  42. package/profiles/development/profile.md +8 -1
  43. package/src/templates/skill-project/PROJECT.md +28 -0
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
 
@@ -129,7 +148,7 @@ Adding a capability installs its skills into your project's `skills/` directory.
129
148
  - **`coding`** — boop-coding-beads-flow, boop-coding-development, boop-coding-commit-changes, boop-coding-review-changes, boop-coding-debugging
130
149
  - **`product-design`** — boop-product-design-engineering-design, boop-product-design-feature-documentation, boop-product-design-feature-requirements, boop-product-design-reference-integration, boop-product-design-thought-partnership, boop-writing-technical-prose, boop-writing-documents, boop-writing-semantic-line-breaks
131
150
  - **`project-mgmt`** — boop-project-mgmt-beads-planning, boop-project-mgmt-engineering-planning
132
- - **`skills`** — boop-skill-authoring, boop-skills-discovery, boop-skills-agent-surface-design, boop-get-smart, boop-so-what, boop-skills-hyperlinking
151
+ - **`skills`** — boop-skill-authoring, boop-skills-discovery, boop-skills-agent-surface-design, boop-get-smart, boop-so-what, boop-skills-hyperlinking, boop-skills-edit-triage
133
152
  - **`testing`** — boop-testing-tdd-light, boop-testing-testing-express-web, boop-testing-testing
134
153
  - **`ux`** — boop-ux-purpose-judgement, boop-ux-visual-judgement, boop-ux-eval-and-refine-workflow, boop-ux-app-ui-dev-workflow
135
154
  - **`workflows`** — boop-feature-dev-workflow, boop-light-dev-workflow, boop-refactor-dev-workflow, boop-test-audit-workflow, boop-architecture-audit-workflow, boop-issue-sweep-workflow, boop-website-dev-workflow, boop-design-handoff-workflow, boop-knowledge-backfill, boop-stray-disposition
@@ -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
@@ -1,22 +1,22 @@
1
1
  {
2
- "@boopkit/cap-api": "0.4.4",
3
- "@boopkit/cap-background-jobs": "0.4.2",
4
- "@boopkit/cap-cloud-storage": "0.4.0",
5
- "@boopkit/cap-coding": "0.4.4",
6
- "@boopkit/cap-database": "0.4.3",
7
- "@boopkit/cap-email": "0.4.2",
2
+ "@boopkit/cap-api": "0.4.5",
3
+ "@boopkit/cap-background-jobs": "0.4.3",
4
+ "@boopkit/cap-cloud-storage": "0.4.1",
5
+ "@boopkit/cap-coding": "0.4.5",
6
+ "@boopkit/cap-database": "0.4.4",
7
+ "@boopkit/cap-email": "0.4.3",
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.0",
12
- "@boopkit/cap-product-design": "0.6.7",
13
- "@boopkit/cap-project-mgmt": "0.4.9",
14
- "@boopkit/cap-services": "0.4.2",
15
- "@boopkit/cap-skills": "0.5.4",
16
- "@boopkit/cap-testing": "0.5.6",
17
- "@boopkit/cap-users": "0.8.0",
18
- "@boopkit/cap-ux": "0.2.2",
19
- "@boopkit/cap-web": "0.5.4",
20
- "@boopkit/cap-workflows": "0.2.0",
11
+ "@boopkit/cap-oauth-server": "0.1.4",
12
+ "@boopkit/cap-product-design": "0.6.8",
13
+ "@boopkit/cap-project-mgmt": "0.4.10",
14
+ "@boopkit/cap-services": "0.4.3",
15
+ "@boopkit/cap-skills": "0.5.5",
16
+ "@boopkit/cap-testing": "0.5.7",
17
+ "@boopkit/cap-users": "0.8.1",
18
+ "@boopkit/cap-ux": "0.2.3",
19
+ "@boopkit/cap-web": "0.5.5",
20
+ "@boopkit/cap-workflows": "0.2.1",
21
21
  "@boopkit/logger": "0.4.0"
22
22
  }
package/dist/cli.js CHANGED
@@ -43,6 +43,7 @@ const list_1 = require("./commands/list");
43
43
  const add_1 = require("./commands/add");
44
44
  const adopt_1 = require("./commands/adopt");
45
45
  const update_1 = require("./commands/update");
46
+ const migrate_skill_edits_1 = require("./commands/migrate-skill-edits");
46
47
  const loop_1 = require("./commands/loop");
47
48
  const loop_merge_1 = require("./commands/loop-merge");
48
49
  const pkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -57,7 +58,8 @@ program
57
58
  .argument('<name>', 'Project name')
58
59
  .option('-p, --profile <profile>', 'Profile to use', 'web-app')
59
60
  .option('--no-install', 'Skip npm install')
60
- .action(init_1.runInit);
61
+ .option('--no-beads', 'Skip bd init, so no beads database or Dolt server is created')
62
+ .action((name, options) => (0, init_1.runInit)(name, options));
61
63
  program
62
64
  .command('list')
63
65
  .description('List available profiles or capabilities')
@@ -94,6 +96,12 @@ program
94
96
  .option('--fix-scripts', 'Rewrite package.json scripts that contradict shipped runner settings')
95
97
  .option('--json', 'Output structured JSON')
96
98
  .action(update_1.runUpdate);
99
+ program
100
+ .command('migrate-skill-edits')
101
+ .description('Write a decision plan to .boop/skill-edits/ for each skill whose SKILL.md this project edited. ' +
102
+ 'Reports by default: changes no skill, PROJECT.md or config')
103
+ .option('--run', "Apply each decided plan: move its rules into PROJECT.md, back up SKILL.md, restore boop's copy and record its hash")
104
+ .action((options) => (options.run ? (0, migrate_skill_edits_1.runApplySkillEdits)() : (0, migrate_skill_edits_1.runMigrateSkillEdits)()));
97
105
  program
98
106
  .command('adopt')
99
107
  .description('Copy package source into the project for full ownership')
@@ -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)
@@ -258,8 +367,9 @@ function runInit(name, options) {
258
367
  }
259
368
  // --- Inject CSS imports for capability partials ---
260
369
  (0, scaffold_1.injectCssImports)(targetDir, cssCapMap, { skipBackup: true });
261
- // --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery ---
370
+ // --- Symlink skills/* into .claude/skills/ for Claude Code auto-discovery, and give each a PROJECT.md ---
262
371
  (0, scaffold_1.linkSkillsToClaude)(targetDir);
372
+ (0, scaffold_1.createSkillProjectFiles)(targetDir);
263
373
  // --- Merge capability scripts into package.json ---
264
374
  if (Object.keys(allScripts).length > 0) {
265
375
  const pkgPath = path.join(targetDir, 'package.json');
@@ -294,12 +404,12 @@ function runInit(name, options) {
294
404
  }
295
405
  // --- Generate .env and .env.example ---
296
406
  if (envEntries.length > 0) {
297
- (0, env_1.generateEnvFiles)(targetDir, envEntries, name);
407
+ (0, env_1.generateEnvFiles)(targetDir, envEntries, projectName);
298
408
  }
299
409
  // --- Write final manifest, preserving capabilityMeta from hash storage ---
300
410
  const existingManifest = (0, project_1.readProjectManifest)(targetDir);
301
411
  const manifest = {
302
- name,
412
+ name: projectName,
303
413
  profile: profileName,
304
414
  boopVersion: '0.1.0',
305
415
  capabilities: installedCapabilities,
@@ -309,10 +419,10 @@ function runInit(name, options) {
309
419
  };
310
420
  (0, project_1.writeProjectManifest)(targetDir, manifest);
311
421
  // --- Print summary ---
312
- console.log(`\nCreated project "${name}" with profile "${profileName}"\n`);
422
+ console.log(`\nCreated project "${projectName}" with profile "${profileName}"\n`);
313
423
  const scriptCount = Object.keys(allScripts).length;
314
424
  const parts = [
315
- `${kernelFiles.length} kernel files`,
425
+ `${kernelFiles.length} ${kernel ? 'kernel' : 'template'} files`,
316
426
  `${installedCapabilities.length} capabilities`,
317
427
  `${capabilityFiles.length} capability files`,
318
428
  ...(scriptCount > 0 ? [`${scriptCount} scripts`] : []),
@@ -375,11 +485,26 @@ function runInit(name, options) {
375
485
  }
376
486
  }
377
487
  }
488
+ // --- Initialize beads (project-mgmt brings it) ---
489
+ // Last, because bd init appends to AGENTS.md, CLAUDE.md and .gitignore when
490
+ // they exist but creates them when they don't, and the templates never
491
+ // overwrite an existing file. Server mode, because boop loop refuses a
492
+ // project whose beads runs embedded. Git is touched only here, just before
493
+ // bd init, because that is when a repository gets created anyway. bd derives
494
+ // its database name from the prefix.
495
+ const bdInitArgs = ['init', '--non-interactive', '--server', '-p', projectName];
496
+ const beadsPending = installedCapabilities.includes('project-mgmt') &&
497
+ !(shouldInitBeads && initBeads(targetDir, projectName, bdInitArgs, deps));
378
498
  console.log(`\nNext steps:`);
379
499
  console.log(` cd ${name}`);
380
500
  if (!shouldInstall) {
381
501
  console.log(' npm install');
382
502
  }
503
+ if (beadsPending) {
504
+ console.log(` bd ${bdInitArgs.join(' ')}`);
505
+ }
383
506
  console.log(' boop status ← check what needs configuration');
384
- console.log(' npm run dev ← start development server');
507
+ if (kernel) {
508
+ console.log(' npm run dev ← start development server');
509
+ }
385
510
  }
@@ -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
  /**