@boopkit/cli 0.8.0 → 0.8.2
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 +8 -6
- package/dist/capability-catalog.json +10 -3
- package/dist/capability-versions.json +13 -12
- package/dist/cli.js +0 -0
- package/dist/commands/add.js +35 -7
- package/dist/commands/adopt.js +1 -1
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +12 -5
- package/dist/commands/loop-merge.d.ts +11 -0
- package/dist/commands/loop-merge.js +135 -14
- package/dist/commands/loop.d.ts +5 -1
- package/dist/commands/loop.js +38 -19
- package/dist/commands/migrate-skill-edits.js +2 -2
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.js +27 -2
- package/dist/commands/update.js +157 -21
- package/dist/lib/capability.js +4 -1
- package/dist/lib/execute.js +30 -3
- package/dist/lib/install-layout.d.ts +5 -0
- package/dist/lib/install-layout.js +8 -0
- package/dist/lib/manifest.d.ts +1 -1
- package/dist/lib/moves.d.ts +88 -0
- package/dist/lib/moves.js +381 -0
- package/dist/lib/original-text.d.ts +2 -0
- package/dist/lib/original-text.js +15 -7
- package/dist/lib/preflight.d.ts +14 -1
- package/dist/lib/preflight.js +53 -17
- package/dist/lib/project.d.ts +17 -0
- package/dist/lib/project.js +26 -0
- package/dist/lib/scaffold.d.ts +12 -1
- package/dist/lib/scaffold.js +43 -4
- package/dist/lib/stale-scripts.js +54 -14
- package/dist/lib/update.d.ts +36 -2
- package/dist/lib/update.js +82 -4
- package/docs/init.md +22 -0
- package/docs/loop.md +13 -3
- package/docs/skills.md +23 -15
- package/package.json +1 -1
- package/profiles/base/profile.md +13 -10
- package/profiles/development/profile.md +10 -8
- package/profiles/web-app/profile.md +6 -4
- package/profiles/web-app/reference/message-queue-patterns.ref.md +8 -13
- package/src/templates/kernel/tests/app.test.ts.hbs +17 -0
package/README.md
CHANGED
|
@@ -129,7 +129,8 @@ Pick what the product needs. `boop add <name>` handles the wiring — npm depend
|
|
|
129
129
|
| Name | What it adds |
|
|
130
130
|
|------|--------------|
|
|
131
131
|
| `coding` | Development conventions and coding skills for AI agents |
|
|
132
|
-
| `
|
|
132
|
+
| `typescript` | The TypeScript toolchain: Jest with ts-jest, `tsconfig.test.json`, and the `test` and `typecheck` scripts |
|
|
133
|
+
| `testing` | Testing principles and the TDD-Lite workflow skills |
|
|
133
134
|
| `project-mgmt` | Beads issue-tracking and planning skills |
|
|
134
135
|
| `product-design` | Product design skills — feature requirements, engineering design, documentation, design thought-partnership, and technical prose |
|
|
135
136
|
| `ux` | UX judgement (purpose + visual) and the app-UI evaluation and delivery workflows |
|
|
@@ -146,12 +147,13 @@ Adding a capability installs its skills into your project's `skills/` directory.
|
|
|
146
147
|
**Development capabilities (skills only — no runtime code)**
|
|
147
148
|
|
|
148
149
|
- **`coding`** — boop-coding-beads-flow, boop-coding-development, boop-coding-commit-changes, boop-coding-review-changes, boop-coding-debugging
|
|
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
|
|
150
|
-
- **`project-mgmt`** — boop-project-mgmt-beads-planning, boop-project-mgmt-engineering-planning
|
|
150
|
+
- **`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-knowledge-backfill, boop-product-design-docs-sweep-workflow, boop-product-design-reference-restructure-workflow, boop-product-design-thought-partnership, boop-writing-technical-prose, boop-writing-documents, boop-writing-semantic-line-breaks
|
|
151
|
+
- **`project-mgmt`** — boop-project-mgmt-beads-planning, boop-project-mgmt-engineering-planning, boop-project-mgmt-issue-sweep-workflow, boop-project-mgmt-stray-disposition
|
|
151
152
|
- **`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
|
|
152
|
-
- **`testing`** — boop-testing-tdd-light, boop-testing-testing
|
|
153
|
+
- **`testing`** — boop-testing-tdd-light, boop-testing-testing
|
|
154
|
+
- **`typescript`** — boop-typescript-testing
|
|
153
155
|
- **`ux`** — boop-ux-purpose-judgement, boop-ux-visual-judgement, boop-ux-eval-and-refine-workflow, boop-ux-app-ui-dev-workflow
|
|
154
|
-
- **`workflows`** — boop-feature-dev-workflow, boop-light-dev-workflow, boop-refactor-dev-workflow, boop-test-audit-workflow, boop-architecture-audit-workflow
|
|
156
|
+
- **`workflows`** — boop-feature-dev-workflow, boop-light-dev-workflow, boop-refactor-dev-workflow, boop-test-audit-workflow, boop-architecture-audit-workflow
|
|
155
157
|
|
|
156
158
|
**Application capabilities**
|
|
157
159
|
|
|
@@ -162,7 +164,7 @@ Adding a capability installs its skills into your project's `skills/` directory.
|
|
|
162
164
|
- **`email`** — boop-email-email-design
|
|
163
165
|
- **`services`** — boop-services-controllers, boop-services-services
|
|
164
166
|
- **`users-and-accounts`** — boop-users-user-data-modeling
|
|
165
|
-
- **`web`** — boop-web-web-ui-design
|
|
167
|
+
- **`web`** — boop-web-web-ui-design, boop-web-css, boop-web-testing, boop-web-website-dev-workflow, boop-web-design-handoff-workflow, boop-web-design-handoff-bundle
|
|
166
168
|
|
|
167
169
|
_One-line description of each skill: see the bundled `docs/skills.md`._
|
|
168
170
|
<!-- SKILLS-INDEX:END -->
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"name": "project-mgmt",
|
|
98
|
-
"description": "Project management with beads",
|
|
98
|
+
"description": "Project management with beads: planning, issue sweeps and stray disposition",
|
|
99
99
|
"type": "development",
|
|
100
100
|
"package": "@boopkit/cap-project-mgmt",
|
|
101
101
|
"requires": []
|
|
@@ -116,11 +116,18 @@
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"name": "testing",
|
|
119
|
-
"description": "
|
|
119
|
+
"description": "Testing method: the testing principles and the TDD-Lite workflow",
|
|
120
120
|
"type": "development",
|
|
121
121
|
"package": "@boopkit/cap-testing",
|
|
122
122
|
"requires": []
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
"name": "typescript",
|
|
126
|
+
"description": "TypeScript toolchain: Jest with ts-jest, and the whole-program type check",
|
|
127
|
+
"type": "development",
|
|
128
|
+
"package": "@boopkit/cap-typescript",
|
|
129
|
+
"requires": []
|
|
130
|
+
},
|
|
124
131
|
{
|
|
125
132
|
"name": "users-and-accounts",
|
|
126
133
|
"description": "Users, identities, properties, and authentication via Prisma",
|
|
@@ -148,7 +155,7 @@
|
|
|
148
155
|
},
|
|
149
156
|
{
|
|
150
157
|
"name": "workflows",
|
|
151
|
-
"description": "Reusable end-to-end development and
|
|
158
|
+
"description": "Reusable end-to-end development and audit workflows for code",
|
|
152
159
|
"type": "development",
|
|
153
160
|
"package": "@boopkit/cap-workflows",
|
|
154
161
|
"requires": []
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"@boopkit/cap-api": "0.4.5",
|
|
3
|
-
"@boopkit/cap-background-jobs": "0.5.
|
|
3
|
+
"@boopkit/cap-background-jobs": "0.5.1",
|
|
4
4
|
"@boopkit/cap-cloud-storage": "0.5.0",
|
|
5
|
-
"@boopkit/cap-coding": "0.4.
|
|
6
|
-
"@boopkit/cap-database": "0.4.
|
|
5
|
+
"@boopkit/cap-coding": "0.4.9",
|
|
6
|
+
"@boopkit/cap-database": "0.4.6",
|
|
7
7
|
"@boopkit/cap-email": "0.4.4",
|
|
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.3.
|
|
12
|
-
"@boopkit/cap-product-design": "0.6.
|
|
13
|
-
"@boopkit/cap-project-mgmt": "0.4.
|
|
11
|
+
"@boopkit/cap-oauth-server": "0.3.3",
|
|
12
|
+
"@boopkit/cap-product-design": "0.6.13",
|
|
13
|
+
"@boopkit/cap-project-mgmt": "0.4.15",
|
|
14
14
|
"@boopkit/cap-services": "0.4.3",
|
|
15
|
-
"@boopkit/cap-skills": "0.5.
|
|
16
|
-
"@boopkit/cap-testing": "0.5.
|
|
17
|
-
"@boopkit/cap-
|
|
18
|
-
"@boopkit/cap-
|
|
19
|
-
"@boopkit/cap-
|
|
20
|
-
"@boopkit/cap-
|
|
15
|
+
"@boopkit/cap-skills": "0.5.8",
|
|
16
|
+
"@boopkit/cap-testing": "0.5.10",
|
|
17
|
+
"@boopkit/cap-typescript": "0.1.0",
|
|
18
|
+
"@boopkit/cap-users": "0.9.1",
|
|
19
|
+
"@boopkit/cap-ux": "0.2.5",
|
|
20
|
+
"@boopkit/cap-web": "0.5.8",
|
|
21
|
+
"@boopkit/cap-workflows": "0.2.4",
|
|
21
22
|
"@boopkit/logger": "0.4.0"
|
|
22
23
|
}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands/add.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.runAdd = runAdd;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
39
40
|
const child_process_1 = require("child_process");
|
|
40
41
|
const preflight_1 = require("../lib/preflight");
|
|
41
42
|
const execute_1 = require("../lib/execute");
|
|
@@ -61,33 +62,51 @@ function runAdd(capabilityName, options) {
|
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
const targetDir = process.cwd();
|
|
64
|
-
// --- Phase 0:
|
|
65
|
+
// --- Phase 0: Make the capability package readable without touching the project ---
|
|
65
66
|
// When the CLI is installed globally, capability packages aren't in
|
|
66
|
-
// node_modules yet.
|
|
67
|
-
//
|
|
67
|
+
// node_modules yet. Pre-flight needs the package's capability.json and
|
|
68
|
+
// scaffold files, so install it into a staging folder for pre-flight to read;
|
|
69
|
+
// the project gets it only once the checks pass.
|
|
68
70
|
// With --tag, install from that dist-tag even when a package is present
|
|
69
71
|
// (the operator asked for that version), and skip npm's cached registry
|
|
70
72
|
// metadata, which can predate a just-published beta.
|
|
73
|
+
let stagingDir;
|
|
74
|
+
let installCommand;
|
|
71
75
|
if (options.tag || !(0, capability_1.getPackageDir)(capabilityName, [targetDir])) {
|
|
72
76
|
const catalogEntry = (0, capability_1.getCatalogEntry)(capabilityName);
|
|
73
77
|
if (catalogEntry?.package) {
|
|
74
78
|
const spec = options.tag ? `${catalogEntry.package}@${options.tag}` : catalogEntry.package;
|
|
75
79
|
const flags = options.tag ? ' --prefer-online' : '';
|
|
76
|
-
|
|
80
|
+
installCommand = `npm install ${spec}${flags}`;
|
|
81
|
+
stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'boop-add-'));
|
|
82
|
+
console.error(`Fetching ${spec}...`);
|
|
77
83
|
try {
|
|
78
|
-
(0, child_process_1.execSync)(
|
|
84
|
+
(0, child_process_1.execSync)(`${installCommand} --prefix ${JSON.stringify(stagingDir)}`, { cwd: stagingDir, stdio: 'pipe' });
|
|
79
85
|
}
|
|
80
86
|
catch (err) {
|
|
81
87
|
console.error(`\x1b[31mERR\x1b[0m Failed to install ${spec}: ${err.message}`);
|
|
88
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
82
89
|
process.exitCode = 1;
|
|
83
90
|
return;
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
}
|
|
94
|
+
try {
|
|
95
|
+
addCapability(targetDir, capabilityName, options, stagingDir, installCommand);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (stagingDir)
|
|
99
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function addCapability(targetDir, capabilityName, options, stagingDir, installCommand) {
|
|
87
103
|
// --- Phase 1: Pre-flight (read-only) ---
|
|
88
104
|
let plan;
|
|
89
105
|
try {
|
|
90
|
-
plan = (0, preflight_1.planAdd)(targetDir, capabilityName
|
|
106
|
+
plan = (0, preflight_1.planAdd)(targetDir, capabilityName, {
|
|
107
|
+
searchPaths: stagingDir ? [targetDir, stagingDir] : [targetDir],
|
|
108
|
+
installCommand,
|
|
109
|
+
});
|
|
91
110
|
}
|
|
92
111
|
catch (err) {
|
|
93
112
|
if (err instanceof SyntaxError) {
|
|
@@ -107,7 +126,7 @@ function runAdd(capabilityName, options) {
|
|
|
107
126
|
}
|
|
108
127
|
throw err;
|
|
109
128
|
}
|
|
110
|
-
const { manifest, cap, projectManifest, filesToCopy, warnings } = plan;
|
|
129
|
+
const { manifest, cap, projectManifest, filesToCopy, warnings, creates } = plan;
|
|
111
130
|
// Emit any warnings from pre-flight
|
|
112
131
|
for (const w of warnings) {
|
|
113
132
|
console.error(w);
|
|
@@ -166,6 +185,7 @@ function runAdd(capabilityName, options) {
|
|
|
166
185
|
console.log(JSON.stringify({
|
|
167
186
|
capability: capabilityName,
|
|
168
187
|
project: projectManifest.name,
|
|
188
|
+
created: creates,
|
|
169
189
|
files: fileEntries,
|
|
170
190
|
skills: skillEntries,
|
|
171
191
|
nextSteps,
|
|
@@ -190,6 +210,14 @@ function runAdd(capabilityName, options) {
|
|
|
190
210
|
// Human-readable output
|
|
191
211
|
console.log(`\n${'─'.repeat(40)}`);
|
|
192
212
|
console.log(`Added capability "${capabilityName}" to ${projectManifest.name}\n`);
|
|
213
|
+
if (creates.includes('package.json')) {
|
|
214
|
+
console.log('Created package.json — a minimal one, because boop records each capability as an npm package there.');
|
|
215
|
+
}
|
|
216
|
+
if (creates.includes('.boop/config.json')) {
|
|
217
|
+
console.log(`Created .boop/config.json — this project is now a boop project, with profile "${projectManifest.profile}".`);
|
|
218
|
+
}
|
|
219
|
+
if (creates.length > 0)
|
|
220
|
+
console.log('');
|
|
193
221
|
if (fileEntries.length > 0) {
|
|
194
222
|
console.log('Files:');
|
|
195
223
|
for (const f of fileEntries) {
|
package/dist/commands/adopt.js
CHANGED
|
@@ -65,7 +65,7 @@ function runAdopt(capabilityName, options) {
|
|
|
65
65
|
throw err;
|
|
66
66
|
}
|
|
67
67
|
if (!manifest) {
|
|
68
|
-
console.error('Not a boop project (no .boop/config.json found). Run `boop init` to create one.');
|
|
68
|
+
console.error('Not a boop project (no .boop/config.json found). Run `boop init` in an empty folder, or `boop add <capability>` in an existing project, to create one.');
|
|
69
69
|
process.exitCode = 1;
|
|
70
70
|
return;
|
|
71
71
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -17,5 +17,10 @@ export interface InitDeps {
|
|
|
17
17
|
* pins @beads/bd as a devDependency, then whatever is on PATH.
|
|
18
18
|
*/
|
|
19
19
|
export declare function findBd(projectDir: string, searchPath?: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* The `bd init` arguments that give a project a beads database in Dolt server
|
|
22
|
+
* mode, which boop loop requires. bd derives its database name from the prefix.
|
|
23
|
+
*/
|
|
24
|
+
export declare function bdInitArgs(projectName: string): string[];
|
|
20
25
|
export declare function runInit(name: string, options: InitOptions, deps?: InitDeps): void;
|
|
21
26
|
export {};
|
package/dist/commands/init.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.findBd = findBd;
|
|
37
|
+
exports.bdInitArgs = bdInitArgs;
|
|
37
38
|
exports.runInit = runInit;
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
@@ -106,6 +107,13 @@ function commitScaffold(targetDir, name, run) {
|
|
|
106
107
|
console.error(`Warning: Unable to commit the scaffold: ${err.message}`);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The `bd init` arguments that give a project a beads database in Dolt server
|
|
112
|
+
* mode, which boop loop requires. bd derives its database name from the prefix.
|
|
113
|
+
*/
|
|
114
|
+
function bdInitArgs(projectName) {
|
|
115
|
+
return ['init', '--non-interactive', '--server', '-p', projectName];
|
|
116
|
+
}
|
|
109
117
|
/** Commit the scaffold, then run `bd init` in the new project; report and return false rather than throw. */
|
|
110
118
|
function initBeads(targetDir, name, args, deps) {
|
|
111
119
|
const findBdFn = deps.findBdFn ?? findBd;
|
|
@@ -490,18 +498,17 @@ function runInit(name, options, deps = {}) {
|
|
|
490
498
|
// they exist but creates them when they don't, and the templates never
|
|
491
499
|
// overwrite an existing file. Server mode, because boop loop refuses a
|
|
492
500
|
// project whose beads runs embedded. Git is touched only here, just before
|
|
493
|
-
// bd init, because that is when a repository gets created anyway.
|
|
494
|
-
|
|
495
|
-
const bdInitArgs = ['init', '--non-interactive', '--server', '-p', projectName];
|
|
501
|
+
// bd init, because that is when a repository gets created anyway.
|
|
502
|
+
const initArgs = bdInitArgs(projectName);
|
|
496
503
|
const beadsPending = installedCapabilities.includes('project-mgmt') &&
|
|
497
|
-
!(shouldInitBeads && initBeads(targetDir, projectName,
|
|
504
|
+
!(shouldInitBeads && initBeads(targetDir, projectName, initArgs, deps));
|
|
498
505
|
console.log(`\nNext steps:`);
|
|
499
506
|
console.log(` cd ${name}`);
|
|
500
507
|
if (!shouldInstall) {
|
|
501
508
|
console.log(' npm install');
|
|
502
509
|
}
|
|
503
510
|
if (beadsPending) {
|
|
504
|
-
console.log(` bd ${
|
|
511
|
+
console.log(` bd ${initArgs.join(' ')}`);
|
|
505
512
|
}
|
|
506
513
|
console.log(' boop status ← check what needs configuration');
|
|
507
514
|
if (kernel) {
|
|
@@ -80,5 +80,16 @@ export declare function isScopeComplete(scope: string, deps: MergeDeps): boolean
|
|
|
80
80
|
* are ignored.
|
|
81
81
|
*/
|
|
82
82
|
export declare function findMergeCandidates(deps: MergeDeps): MergeCandidate[];
|
|
83
|
+
/**
|
|
84
|
+
* A readable, bounded issue description for a post-rebase test failure: the
|
|
85
|
+
* cause, the restored commit, the first failures, the Jest summary, and where
|
|
86
|
+
* the full output lives.
|
|
87
|
+
*/
|
|
88
|
+
export declare function testFailureDescription(o: {
|
|
89
|
+
branch: string;
|
|
90
|
+
origSha: string;
|
|
91
|
+
output: string;
|
|
92
|
+
logPath: string | null;
|
|
93
|
+
}): string;
|
|
83
94
|
export declare function runMerge(options: LoopOptions, deps?: MergeDeps): Promise<void>;
|
|
84
95
|
export {};
|
|
@@ -37,6 +37,7 @@ exports.pickHigherVersion = pickHigherVersion;
|
|
|
37
37
|
exports.mergePackageJsonContents = mergePackageJsonContents;
|
|
38
38
|
exports.isScopeComplete = isScopeComplete;
|
|
39
39
|
exports.findMergeCandidates = findMergeCandidates;
|
|
40
|
+
exports.testFailureDescription = testFailureDescription;
|
|
40
41
|
exports.runMerge = runMerge;
|
|
41
42
|
const child_process_1 = require("child_process");
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
@@ -212,35 +213,76 @@ function findMergeCandidates(deps) {
|
|
|
212
213
|
// ── Merge engine ──────────────────────────────────────────────────────────────
|
|
213
214
|
const defaultExec = (cmd, opts) => (0, child_process_1.execSync)(cmd, { encoding: 'utf8', ...opts });
|
|
214
215
|
class MergeError extends Error {
|
|
215
|
-
constructor(message, detail) {
|
|
216
|
+
constructor(message, detail, title = 'Merge conflict') {
|
|
216
217
|
super(message);
|
|
217
218
|
this.detail = detail;
|
|
219
|
+
this.title = title;
|
|
218
220
|
}
|
|
219
221
|
}
|
|
220
|
-
|
|
222
|
+
// Files bd rewrites in the main checkout on every command, including the ones a
|
|
223
|
+
// loop session runs from its own worktree. A change to them is beads bookkeeping,
|
|
224
|
+
// not work in progress, so it does not block a merge on its own.
|
|
225
|
+
const BEADS_EXPORT_FILES = ['.beads/interactions.jsonl', '.beads/issues.jsonl'];
|
|
226
|
+
/**
|
|
227
|
+
* Throws unless the integration tree is on `main` with no uncommitted changes
|
|
228
|
+
* beyond beads' export files. Returns the export files that are uncommitted.
|
|
229
|
+
*/
|
|
221
230
|
function assertCleanMain(execSyncFn, projectDir) {
|
|
222
231
|
const exec = execSyncFn;
|
|
223
232
|
const branch = exec('git rev-parse --abbrev-ref HEAD', { cwd: projectDir }).trim();
|
|
224
233
|
if (branch !== 'main') {
|
|
225
234
|
throw new Error(`Integration tree at ${projectDir} is on "${branch}", not "main". Checkout main before merging.`);
|
|
226
235
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
// Not trimmed: each porcelain line starts with a two-column status that may be a space.
|
|
237
|
+
const changed = exec('git status --porcelain', { cwd: projectDir })
|
|
238
|
+
.split('\n')
|
|
239
|
+
.filter((l) => l.trim())
|
|
240
|
+
.map((l) => l.slice(3));
|
|
241
|
+
const others = changed.filter((f) => !BEADS_EXPORT_FILES.includes(f));
|
|
242
|
+
if (others.length > 0) {
|
|
243
|
+
throw new Error(`Integration tree at ${projectDir} has uncommitted changes (${others.join(', ')}); commit or stash before merging.`);
|
|
244
|
+
}
|
|
245
|
+
return changed;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Throws if the branch changes a beads export file that is uncommitted in main,
|
|
249
|
+
* since the fast-forward would then run over that uncommitted change.
|
|
250
|
+
*/
|
|
251
|
+
function assertBranchAvoids(exec, projectDir, branch, dirty) {
|
|
252
|
+
if (dirty.length === 0)
|
|
253
|
+
return;
|
|
254
|
+
const touched = exec(`git diff --name-only main...${branch}`, { cwd: projectDir })
|
|
255
|
+
.split('\n')
|
|
256
|
+
.map((l) => l.trim());
|
|
257
|
+
const overlap = dirty.filter((f) => touched.includes(f));
|
|
258
|
+
if (overlap.length > 0) {
|
|
259
|
+
throw new Error(`${branch} changes ${overlap.join(', ')}, which is uncommitted in the integration tree at ${projectDir}; commit it in main before merging.`);
|
|
230
260
|
}
|
|
231
261
|
}
|
|
232
262
|
/**
|
|
233
263
|
* Rebase the candidate branch onto main inside its worktree, auto-resolving only
|
|
234
264
|
* package.json dependency conflicts. Throws MergeError (rebase aborted) on any
|
|
235
|
-
* other conflict
|
|
265
|
+
* other conflict, and on any rebase that does not complete, so the branch is
|
|
266
|
+
* never tested or merged un-rebased.
|
|
236
267
|
*/
|
|
237
268
|
function rebaseOntoMain(exec, wt) {
|
|
269
|
+
// git refuses to rebase over uncommitted tracked changes (`npm install` can
|
|
270
|
+
// rewrite package-lock.json); checking first lets the issue name the files.
|
|
271
|
+
const uncommitted = exec('git status --porcelain --untracked-files=no', { cwd: wt })
|
|
272
|
+
.split('\n')
|
|
273
|
+
.filter((l) => l.trim())
|
|
274
|
+
.map((l) => l.slice(3));
|
|
275
|
+
if (uncommitted.length > 0) {
|
|
276
|
+
throw new MergeError(`worktree has uncommitted changes (${uncommitted.join(', ')})`, `Uncommitted files in ${wt}: ${uncommitted.join(', ')}. Commit or discard them in the worktree.`, 'Merge blocked');
|
|
277
|
+
}
|
|
278
|
+
let failure = '';
|
|
238
279
|
try {
|
|
239
280
|
exec('git rebase main', { cwd: wt, stdio: 'pipe' });
|
|
240
281
|
return;
|
|
241
282
|
}
|
|
242
|
-
catch {
|
|
283
|
+
catch (e) {
|
|
243
284
|
// Conflict (or other failure). Inspect unmerged paths.
|
|
285
|
+
failure = commandOutput(e);
|
|
244
286
|
}
|
|
245
287
|
// Iteratively resolve package.json-only conflicts and continue.
|
|
246
288
|
for (let guard = 0; guard < 100; guard++) {
|
|
@@ -285,29 +327,40 @@ function rebaseOntoMain(exec, wt) {
|
|
|
285
327
|
exec('GIT_EDITOR=true git rebase --continue', { cwd: wt, stdio: 'pipe' });
|
|
286
328
|
return; // rebase finished
|
|
287
329
|
}
|
|
288
|
-
catch {
|
|
330
|
+
catch (e) {
|
|
289
331
|
// More commits ahead may conflict again — loop and re-inspect.
|
|
332
|
+
failure = commandOutput(e);
|
|
290
333
|
}
|
|
291
334
|
}
|
|
335
|
+
// No unmerged paths, yet the rebase did not finish: git refused it outright
|
|
336
|
+
// (e.g. untracked files main would overwrite). Abort any partial rebase.
|
|
337
|
+
try {
|
|
338
|
+
exec('git rebase --abort', { cwd: wt, stdio: 'pipe' });
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
/* no rebase in progress — git refused before starting */
|
|
342
|
+
}
|
|
343
|
+
throw new MergeError('rebase did not complete', `git said:\n\n${failure.trim()}`, 'Merge blocked');
|
|
292
344
|
}
|
|
293
345
|
function mergeOne(candidate, deps) {
|
|
294
346
|
const projectDir = deps.projectDir || process.cwd();
|
|
295
347
|
const exec = deps.execSyncFn || defaultExec;
|
|
296
348
|
const createIssue = deps.createIssueFn || defaultCreateIssue(deps);
|
|
297
349
|
const { branch, worktreePath: wt } = candidate;
|
|
298
|
-
assertCleanMain(exec, projectDir);
|
|
350
|
+
const dirtyExports = assertCleanMain(exec, projectDir);
|
|
351
|
+
assertBranchAvoids(exec, projectDir, branch, dirtyExports);
|
|
299
352
|
const origSha = exec('git rev-parse HEAD', { cwd: wt }).trim();
|
|
300
353
|
try {
|
|
301
354
|
rebaseOntoMain(exec, wt);
|
|
302
355
|
}
|
|
303
356
|
catch (e) {
|
|
304
357
|
const err = e;
|
|
305
|
-
const issueId = createIssue(
|
|
358
|
+
const issueId = createIssue(`${err.title}: ${branch}`, `Rebasing ${branch} onto main failed: ${err.message}\n\n${err.detail || ''}\n\nResolve by hand, then re-run \`boop loop --merge ${branch}\`.`);
|
|
306
359
|
return { branch, status: 'skipped', reason: err.message, issueId };
|
|
307
360
|
}
|
|
308
361
|
// Post-rebase test gate. Failure → restore the branch, never touch main.
|
|
309
362
|
try {
|
|
310
|
-
exec('npm test', { cwd: wt, stdio: 'pipe' });
|
|
363
|
+
exec('npm test', { cwd: wt, stdio: 'pipe', maxBuffer: TEST_OUTPUT_MAX_BUFFER });
|
|
311
364
|
}
|
|
312
365
|
catch (e) {
|
|
313
366
|
try {
|
|
@@ -316,7 +369,9 @@ function mergeOne(candidate, deps) {
|
|
|
316
369
|
catch {
|
|
317
370
|
/* best-effort restore */
|
|
318
371
|
}
|
|
319
|
-
const
|
|
372
|
+
const output = commandOutput(e);
|
|
373
|
+
const logPath = writeTestLog(projectDir, branch, output);
|
|
374
|
+
const issueId = createIssue(`Merge test failure: ${branch}`, testFailureDescription({ branch, origSha, output, logPath }));
|
|
320
375
|
return { branch, status: 'skipped', reason: 'tests failed post-rebase', issueId };
|
|
321
376
|
}
|
|
322
377
|
// Fast-forward into main (guaranteed possible: branch was just rebased onto it).
|
|
@@ -341,11 +396,77 @@ function mergeOne(candidate, deps) {
|
|
|
341
396
|
}
|
|
342
397
|
return { branch, status: 'merged' };
|
|
343
398
|
}
|
|
399
|
+
// A Jest run's stderr can run to hundreds of kilobytes; past the 1 MiB default
|
|
400
|
+
// execSync throws ENOBUFS and the summary at the end is lost.
|
|
401
|
+
const TEST_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
402
|
+
const DESCRIPTION_FAILURES_MAX = 4000;
|
|
403
|
+
const DESCRIPTION_SUMMARY_MAX = 2000;
|
|
404
|
+
/** stdout + stderr of a failed execSync, falling back to the error message. */
|
|
405
|
+
function commandOutput(e) {
|
|
406
|
+
const err = e;
|
|
407
|
+
const text = [err.stdout, err.stderr]
|
|
408
|
+
.map((s) => (s == null ? '' : String(s)))
|
|
409
|
+
.filter(Boolean)
|
|
410
|
+
.join('\n');
|
|
411
|
+
return text || err.message || '';
|
|
412
|
+
}
|
|
413
|
+
/** Save the full test output beside the loop's other run logs. Returns the path, or null. */
|
|
414
|
+
function writeTestLog(projectDir, branch, output) {
|
|
415
|
+
try {
|
|
416
|
+
const logDir = path.join(projectDir, 'labs', 'claude-runs');
|
|
417
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
418
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
419
|
+
const logPath = path.join(logDir, `_merge_${branch.replace(/[^\w.-]+/g, '_')}_${timestamp}.log`);
|
|
420
|
+
fs.writeFileSync(logPath, output);
|
|
421
|
+
return logPath;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A readable, bounded issue description for a post-rebase test failure: the
|
|
429
|
+
* cause, the restored commit, the first failures, the Jest summary, and where
|
|
430
|
+
* the full output lives.
|
|
431
|
+
*/
|
|
432
|
+
function testFailureDescription(o) {
|
|
433
|
+
const lines = o.output.split('\n');
|
|
434
|
+
const summaryAt = lines.map((l) => l.startsWith('Test Suites:')).lastIndexOf(true);
|
|
435
|
+
const summary = summaryAt >= 0 ? lines.slice(summaryAt).join('\n').trim() : '';
|
|
436
|
+
const firstFailureAt = lines.findIndex((l) => l.trimStart().startsWith('● '));
|
|
437
|
+
const failures = firstFailureAt >= 0
|
|
438
|
+
? lines.slice(firstFailureAt, summaryAt >= 0 ? summaryAt : undefined).join('\n')
|
|
439
|
+
: summary
|
|
440
|
+
? ''
|
|
441
|
+
: o.output.slice(-DESCRIPTION_FAILURES_MAX);
|
|
442
|
+
const parts = [
|
|
443
|
+
`Tests failed after rebasing ${o.branch} onto main; branch reset to ${o.origSha} and main left untouched.`,
|
|
444
|
+
];
|
|
445
|
+
if (failures)
|
|
446
|
+
parts.push(`First failures:\n\n${truncate(failures.trim(), DESCRIPTION_FAILURES_MAX)}`);
|
|
447
|
+
if (summary)
|
|
448
|
+
parts.push(`Summary:\n\n${truncate(summary, DESCRIPTION_SUMMARY_MAX)}`);
|
|
449
|
+
parts.push(o.logPath ? `Full test output: ${o.logPath}` : 'The full test output could not be saved.');
|
|
450
|
+
return parts.join('\n\n');
|
|
451
|
+
}
|
|
452
|
+
function truncate(text, max) {
|
|
453
|
+
return text.length <= max ? text : `${text.slice(0, max)}\n… (truncated)`;
|
|
454
|
+
}
|
|
455
|
+
// bd runs from an argument array, not a shell: titles and descriptions carry
|
|
456
|
+
// branch names, error messages and test output, where backticks and `$(...)`
|
|
457
|
+
// would otherwise run as commands. `env` is passed explicitly so a PATH set on
|
|
458
|
+
// this module's process.env (as a test sandbox does) decides which bd runs.
|
|
344
459
|
function defaultCreateIssue(deps) {
|
|
345
|
-
const
|
|
460
|
+
const projectDir = deps.projectDir || process.cwd();
|
|
346
461
|
return (title, description) => {
|
|
347
462
|
try {
|
|
348
|
-
const raw =
|
|
463
|
+
const raw = (0, child_process_1.execFileSync)('bd', ['create', title, '--description', description, '-t', 'bug', '-p', '1', '--json'], {
|
|
464
|
+
cwd: projectDir,
|
|
465
|
+
env: process.env,
|
|
466
|
+
encoding: 'utf8',
|
|
467
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
468
|
+
maxBuffer: loop_1.BD_READ_MAX_BUFFER,
|
|
469
|
+
});
|
|
349
470
|
return JSON.parse(raw).id || null;
|
|
350
471
|
}
|
|
351
472
|
catch {
|
package/dist/commands/loop.d.ts
CHANGED
|
@@ -66,7 +66,10 @@ export interface WorktreeContext {
|
|
|
66
66
|
/**
|
|
67
67
|
* Hard requirement: beads must run in Dolt server mode so concurrent worktrees
|
|
68
68
|
* share one issue DB over TCP. Reads `.beads/metadata.json` and throws an
|
|
69
|
-
* actionable error on embedded/missing — call before any `bd` work.
|
|
69
|
+
* actionable error on embedded/missing — call before any `bd` work. A project
|
|
70
|
+
* with no `.beads/` at all (one started with `boop add`) is told the `bd init`
|
|
71
|
+
* command `boop init` would have run. The messages name no skill file, because
|
|
72
|
+
* an installed project may not have the one boop's own repository does.
|
|
70
73
|
*/
|
|
71
74
|
export declare function assertDoltServerMode(projectDir: string, deps?: {
|
|
72
75
|
existsSyncFn?: (p: string) => boolean;
|
|
@@ -325,6 +328,7 @@ export declare function splitStreamLines(carry: string, chunk: string): {
|
|
|
325
328
|
lines: string[];
|
|
326
329
|
rest: string;
|
|
327
330
|
};
|
|
331
|
+
export declare function sessionEnv(model: string, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
328
332
|
export declare function runLoop(options: LoopOptions, deps?: {
|
|
329
333
|
/** Runs one Claude session for an issue. Tests substitute this; the planning phase is not affected. */
|
|
330
334
|
runClaudeFn?: RunClaudeFn;
|