@boopkit/cli 0.4.1 → 0.6.1

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.
@@ -0,0 +1,436 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.pickHigherVersion = pickHigherVersion;
37
+ exports.mergePackageJsonContents = mergePackageJsonContents;
38
+ exports.isScopeComplete = isScopeComplete;
39
+ exports.findMergeCandidates = findMergeCandidates;
40
+ exports.runMerge = runMerge;
41
+ const child_process_1 = require("child_process");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const loop_1 = require("./loop");
45
+ // ── Semver helpers (package.json conflict resolution, subsumes boop-7rb) ──────
46
+ /** Strip range operators and parse a `x.y.z` prefix. Returns null if unparseable. */
47
+ function parseSemver(version) {
48
+ const m = version.trim().replace(/^[\^~>=<\s]+/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
49
+ if (!m)
50
+ return null;
51
+ return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
52
+ }
53
+ /**
54
+ * Pick the higher of two version specifiers for the same dependency key. Ties and
55
+ * unparseable specifiers prefer `ours` (the integration/main side) for stability.
56
+ */
57
+ function pickHigherVersion(ours, theirs) {
58
+ if (ours === theirs)
59
+ return ours;
60
+ const a = parseSemver(ours);
61
+ const b = parseSemver(theirs);
62
+ if (!a || !b)
63
+ return ours;
64
+ for (let i = 0; i < 3; i++) {
65
+ if (b[i] > a[i])
66
+ return theirs;
67
+ if (b[i] < a[i])
68
+ return ours;
69
+ }
70
+ return ours;
71
+ }
72
+ const DEP_MAPS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
73
+ /**
74
+ * Union-merge two package.json sides that conflicted during a rebase. Dependency
75
+ * maps are unioned (same-key version conflict → higher semver). Every other
76
+ * top-level key must already agree between the two sides; if any non-dependency
77
+ * key differs, the conflict is NOT known-safe and this throws so the caller punts
78
+ * to a human. Returns the merged JSON text (2-space indent, trailing newline).
79
+ */
80
+ function mergePackageJsonContents(oursText, theirsText) {
81
+ const ours = JSON.parse(oursText);
82
+ const theirs = JSON.parse(theirsText);
83
+ const merged = { ...ours };
84
+ for (const map of DEP_MAPS) {
85
+ const o = (ours[map] || {});
86
+ const t = (theirs[map] || {});
87
+ if (!ours[map] && !theirs[map])
88
+ continue;
89
+ const combined = { ...o };
90
+ for (const [name, version] of Object.entries(t)) {
91
+ combined[name] = name in combined ? pickHigherVersion(combined[name], version) : version;
92
+ }
93
+ // Sort keys for a deterministic, diff-friendly result.
94
+ merged[map] = Object.fromEntries(Object.entries(combined).sort(([a], [b]) => a.localeCompare(b)));
95
+ }
96
+ // Any non-dependency divergence is outside the known-safe pattern.
97
+ const keys = new Set([...Object.keys(ours), ...Object.keys(theirs)]);
98
+ for (const key of keys) {
99
+ if (DEP_MAPS.includes(key))
100
+ continue;
101
+ if (JSON.stringify(ours[key]) !== JSON.stringify(theirs[key])) {
102
+ throw new Error(`package.json conflict in non-dependency key "${key}" is not auto-resolvable`);
103
+ }
104
+ }
105
+ return JSON.stringify(merged, null, 2) + '\n';
106
+ }
107
+ // ── Scope-completion readiness ────────────────────────────────────────────────
108
+ function makeParentLookup(issues, getParentFn) {
109
+ const cache = new Map();
110
+ for (const i of issues)
111
+ cache.set(i.id, i.parent ?? null);
112
+ return (id) => {
113
+ if (cache.has(id))
114
+ return cache.get(id) ?? null;
115
+ const p = getParentFn(id);
116
+ cache.set(id, p);
117
+ return p;
118
+ };
119
+ }
120
+ /**
121
+ * A worktree is ready to merge when every beads issue in its scope is closed.
122
+ * That means: at least one closed in-scope issue exists AND zero open in-scope
123
+ * issues remain. `bd ready` is insufficient (it only returns *unblocked* issues),
124
+ * so we list by status and filter with the same scope rule the loop uses.
125
+ */
126
+ function isScopeComplete(scope, deps) {
127
+ const bdListFn = deps.bdListFn || defaultBdList(deps);
128
+ const getParentFn = deps.getParentFn || defaultGetParent(deps);
129
+ const open = bdListFn('open');
130
+ const closed = bdListFn('closed');
131
+ const getParent = makeParentLookup([...open, ...closed], getParentFn);
132
+ const inScope = (i) => (0, loop_1.isIssueInScope)(i, scope, getParent);
133
+ const openInScope = open.filter(inScope);
134
+ const closedInScope = closed.filter(inScope);
135
+ return openInScope.length === 0 && closedInScope.length > 0;
136
+ }
137
+ function defaultBdList(deps) {
138
+ const execSyncFn = deps.execSyncFn || defaultExec;
139
+ return (status) => {
140
+ try {
141
+ const raw = execSyncFn(`bd list --status=${status} --json 2>/dev/null`);
142
+ return JSON.parse(raw);
143
+ }
144
+ catch {
145
+ return [];
146
+ }
147
+ };
148
+ }
149
+ function defaultGetParent(deps) {
150
+ const execSyncFn = deps.execSyncFn || defaultExec;
151
+ return (id) => {
152
+ try {
153
+ const data = JSON.parse(execSyncFn(`bd show ${id} --json 2>/dev/null`));
154
+ return (data[0] || data).parent || null;
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ };
160
+ }
161
+ // ── Candidate discovery ───────────────────────────────────────────────────────
162
+ /**
163
+ * Enumerate registered git worktrees, read each `.boop-worktree.json`, and decide
164
+ * which are ready to merge. Worktrees without metadata (e.g. unrelated worktrees)
165
+ * are ignored.
166
+ */
167
+ function findMergeCandidates(deps) {
168
+ const projectDir = deps.projectDir || process.cwd();
169
+ const execSyncFn = deps.execSyncFn || defaultExec;
170
+ const existsSyncFn = deps.existsSyncFn || fs.existsSync;
171
+ const readFileFn = deps.readFileFn || ((p) => fs.readFileSync(p, 'utf8'));
172
+ let raw = '';
173
+ try {
174
+ raw = execSyncFn('git worktree list --porcelain', { cwd: projectDir });
175
+ }
176
+ catch {
177
+ return [];
178
+ }
179
+ const candidates = [];
180
+ for (const block of raw.split('\n\n')) {
181
+ const wtLine = block.split('\n').find((l) => l.startsWith('worktree '));
182
+ if (!wtLine)
183
+ continue;
184
+ const wtPath = path.resolve(wtLine.slice('worktree '.length).trim());
185
+ // Skip the integration tree itself.
186
+ if (wtPath === path.resolve(projectDir))
187
+ continue;
188
+ const metaPath = path.join(wtPath, '.boop-worktree.json');
189
+ if (!existsSyncFn(metaPath))
190
+ continue;
191
+ let meta;
192
+ try {
193
+ meta = JSON.parse(readFileFn(metaPath));
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ if (!meta.scope || !meta.branch)
199
+ continue;
200
+ candidates.push({
201
+ branch: meta.branch,
202
+ worktreePath: wtPath,
203
+ scope: meta.scope,
204
+ mode: meta.mode,
205
+ ready: isScopeComplete(meta.scope, deps),
206
+ });
207
+ }
208
+ return candidates;
209
+ }
210
+ // ── Merge engine ──────────────────────────────────────────────────────────────
211
+ const defaultExec = (cmd, opts) => (0, child_process_1.execSync)(cmd, { encoding: 'utf8', ...opts });
212
+ class MergeError extends Error {
213
+ constructor(message, detail) {
214
+ super(message);
215
+ this.detail = detail;
216
+ }
217
+ }
218
+ /** True if the integration tree is on `main` with a clean working tree. */
219
+ function assertCleanMain(execSyncFn, projectDir) {
220
+ const exec = execSyncFn;
221
+ const branch = exec('git rev-parse --abbrev-ref HEAD', { cwd: projectDir }).trim();
222
+ if (branch !== 'main') {
223
+ throw new Error(`Integration tree at ${projectDir} is on "${branch}", not "main". Checkout main before merging.`);
224
+ }
225
+ const status = exec('git status --porcelain', { cwd: projectDir }).trim();
226
+ if (status) {
227
+ throw new Error(`Integration tree at ${projectDir} has uncommitted changes; commit or stash before merging.`);
228
+ }
229
+ }
230
+ /**
231
+ * Rebase the candidate branch onto main inside its worktree, auto-resolving only
232
+ * package.json dependency conflicts. Throws MergeError (rebase aborted) on any
233
+ * other conflict.
234
+ */
235
+ function rebaseOntoMain(exec, wt) {
236
+ try {
237
+ exec('git rebase main', { cwd: wt, stdio: 'pipe' });
238
+ return;
239
+ }
240
+ catch {
241
+ // Conflict (or other failure). Inspect unmerged paths.
242
+ }
243
+ // Iteratively resolve package.json-only conflicts and continue.
244
+ for (let guard = 0; guard < 100; guard++) {
245
+ const conflicted = exec('git diff --name-only --diff-filter=U', { cwd: wt })
246
+ .split('\n')
247
+ .map((l) => l.trim())
248
+ .filter(Boolean);
249
+ if (conflicted.length === 0)
250
+ break; // nothing unmerged — fall through to continue/finish
251
+ const nonSafe = conflicted.filter((f) => path.basename(f) !== 'package.json');
252
+ if (nonSafe.length > 0) {
253
+ try {
254
+ exec('git rebase --abort', { cwd: wt });
255
+ }
256
+ catch {
257
+ /* leave it for the human */
258
+ }
259
+ throw new MergeError('rebase conflict not auto-resolvable', `Conflicting files: ${conflicted.join(', ')}`);
260
+ }
261
+ // All conflicts are package.json — union the two sides.
262
+ for (const file of conflicted) {
263
+ const ours = exec(`git show :2:${file}`, { cwd: wt });
264
+ const theirs = exec(`git show :3:${file}`, { cwd: wt });
265
+ let merged;
266
+ try {
267
+ merged = mergePackageJsonContents(ours, theirs);
268
+ }
269
+ catch (e) {
270
+ try {
271
+ exec('git rebase --abort', { cwd: wt });
272
+ }
273
+ catch {
274
+ /* noop */
275
+ }
276
+ throw new MergeError('package.json conflict not auto-resolvable', e.message);
277
+ }
278
+ fs.writeFileSync(path.join(wt, file), merged);
279
+ exec(`git add ${file}`, { cwd: wt });
280
+ }
281
+ try {
282
+ // GIT_EDITOR=true keeps `--continue` non-interactive (reuses the commit message).
283
+ exec('GIT_EDITOR=true git rebase --continue', { cwd: wt, stdio: 'pipe' });
284
+ return; // rebase finished
285
+ }
286
+ catch {
287
+ // More commits ahead may conflict again — loop and re-inspect.
288
+ }
289
+ }
290
+ }
291
+ function mergeOne(candidate, deps) {
292
+ const projectDir = deps.projectDir || process.cwd();
293
+ const exec = deps.execSyncFn || defaultExec;
294
+ const createIssue = deps.createIssueFn || defaultCreateIssue(deps);
295
+ const { branch, worktreePath: wt } = candidate;
296
+ assertCleanMain(exec, projectDir);
297
+ const origSha = exec('git rev-parse HEAD', { cwd: wt }).trim();
298
+ try {
299
+ rebaseOntoMain(exec, wt);
300
+ }
301
+ catch (e) {
302
+ const err = e;
303
+ const issueId = createIssue(`Merge conflict: ${branch}`, `Rebasing ${branch} onto main failed: ${err.message}\n\n${err.detail || ''}\n\nResolve by hand, then re-run \`boop loop --merge ${branch}\`.`);
304
+ return { branch, status: 'skipped', reason: err.message, issueId };
305
+ }
306
+ // Post-rebase test gate. Failure → restore the branch, never touch main.
307
+ try {
308
+ exec('npm test', { cwd: wt, stdio: 'pipe' });
309
+ }
310
+ catch (e) {
311
+ try {
312
+ exec(`git reset --hard ${origSha}`, { cwd: wt });
313
+ }
314
+ catch {
315
+ /* best-effort restore */
316
+ }
317
+ const issueId = createIssue(`Merge test failure: ${branch}`, `Tests failed after rebasing ${branch} onto main; branch reset to ${origSha} and main left untouched.\n\n${e.message}`);
318
+ return { branch, status: 'skipped', reason: 'tests failed post-rebase', issueId };
319
+ }
320
+ // Fast-forward into main (guaranteed possible: branch was just rebased onto it).
321
+ exec(`git merge --ff-only ${branch}`, { cwd: projectDir });
322
+ // Cleanup: remove the worktree and delete the now-merged branch.
323
+ try {
324
+ exec(`git worktree remove ${wt}`, { cwd: projectDir });
325
+ }
326
+ catch {
327
+ try {
328
+ exec(`git worktree remove --force ${wt}`, { cwd: projectDir });
329
+ }
330
+ catch {
331
+ /* leave the worktree; merge already succeeded */
332
+ }
333
+ }
334
+ try {
335
+ exec(`git branch -d ${branch}`, { cwd: projectDir });
336
+ }
337
+ catch {
338
+ /* branch may be kept by the operator; non-fatal */
339
+ }
340
+ return { branch, status: 'merged' };
341
+ }
342
+ function defaultCreateIssue(deps) {
343
+ const exec = deps.execSyncFn || defaultExec;
344
+ return (title, description) => {
345
+ try {
346
+ const raw = exec(`bd create ${JSON.stringify(title)} --description=${JSON.stringify(description)} -t bug -p 1 --json`);
347
+ return JSON.parse(raw).id || null;
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ };
353
+ }
354
+ // ── Orchestrator ──────────────────────────────────────────────────────────────
355
+ async function runMerge(options, deps) {
356
+ const d = deps || {};
357
+ const projectDir = d.projectDir || process.cwd();
358
+ d.projectDir = projectDir;
359
+ const sleepFn = d.sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
360
+ try {
361
+ (d.assertServerModeFn || ((dir) => (0, loop_1.assertDoltServerMode)(dir, undefined)))(projectDir);
362
+ }
363
+ catch (e) {
364
+ console.error(`\x1b[31m[bd-merge]\x1b[0m ${e.message}`);
365
+ process.exitCode = 1;
366
+ return;
367
+ }
368
+ const dryRun = !options.run;
369
+ const watch = options.watch || false;
370
+ const pollInterval = parseInt(options.pollInterval || '60', 10);
371
+ const explicitBranch = typeof options.merge === 'string' ? options.merge : undefined;
372
+ const collect = () => {
373
+ const all = findMergeCandidates(d);
374
+ if (explicitBranch) {
375
+ // Explicit override: merge the named branch regardless of readiness.
376
+ const match = all.find((c) => c.branch === explicitBranch || c.branch === `work/${explicitBranch}`);
377
+ if (match)
378
+ return [{ ...match, ready: true }];
379
+ console.error(`\x1b[31m[bd-merge]\x1b[0m No worktree found for branch "${explicitBranch}". Known: ${all.map((c) => c.branch).join(', ') || 'none'}`);
380
+ return [];
381
+ }
382
+ return all.filter((c) => c.ready);
383
+ };
384
+ const outcomes = [];
385
+ do {
386
+ const candidates = collect();
387
+ if (dryRun) {
388
+ console.log('\x1b[33m[bd-merge]\x1b[0m DRY RUN — add --run to execute');
389
+ if (candidates.length === 0) {
390
+ console.log('\x1b[36m[bd-merge]\x1b[0m No branches ready to merge.');
391
+ }
392
+ for (const c of candidates) {
393
+ console.log(` \x1b[34m${c.branch}\x1b[0m (scope ${c.scope}, ${c.mode}) — would rebase onto main, npm test, ff-merge, cleanup ${c.worktreePath}`);
394
+ }
395
+ break;
396
+ }
397
+ if (candidates.length === 0 && !watch) {
398
+ console.log('\x1b[36m[bd-merge]\x1b[0m No branches ready to merge.');
399
+ }
400
+ for (const c of candidates) {
401
+ console.log(`\x1b[32m[bd-merge]\x1b[0m Merging \x1b[34m${c.branch}\x1b[0m (scope ${c.scope})...`);
402
+ let outcome;
403
+ try {
404
+ outcome = mergeOne(c, d);
405
+ }
406
+ catch (e) {
407
+ // Unexpected failure (e.g. dirty main) — isolate and continue.
408
+ outcome = { branch: c.branch, status: 'skipped', reason: e.message };
409
+ console.error(`\x1b[31m[bd-merge]\x1b[0m ${c.branch}: ${e.message}`);
410
+ }
411
+ outcomes.push(outcome);
412
+ if (outcome.status === 'merged') {
413
+ console.log(`\x1b[32m[bd-merge]\x1b[0m ✓ merged ${c.branch} into main; cleaned up worktree`);
414
+ }
415
+ else {
416
+ console.warn(`\x1b[33m[bd-merge]\x1b[0m ⤳ skipped ${c.branch}: ${outcome.reason}${outcome.issueId ? ` (filed ${outcome.issueId})` : ''}`);
417
+ }
418
+ }
419
+ if (watch) {
420
+ console.log(`\x1b[36m[bd-merge]\x1b[0m Watching for ready branches; next poll in ${pollInterval}s...`);
421
+ await sleepFn(pollInterval * 1000);
422
+ }
423
+ } while (watch);
424
+ if (!dryRun && !watch) {
425
+ const merged = outcomes.filter((o) => o.status === 'merged').length;
426
+ const skipped = outcomes.filter((o) => o.status === 'skipped').length;
427
+ console.log('');
428
+ console.log('\x1b[32m[bd-merge]\x1b[0m ━━━ Merge summary ━━━');
429
+ console.log(`\x1b[32m[bd-merge]\x1b[0m Merged: ${merged}`);
430
+ if (skipped > 0)
431
+ console.log(`\x1b[33m[bd-merge]\x1b[0m Skipped: ${skipped}`);
432
+ }
433
+ if (options.json) {
434
+ console.log(JSON.stringify({ outcomes }, null, 2));
435
+ }
436
+ }
@@ -15,6 +15,8 @@ export interface LoopOptions {
15
15
  skipPerms?: boolean;
16
16
  bare?: boolean;
17
17
  json?: boolean;
18
+ merge?: boolean | string;
19
+ watch?: boolean;
18
20
  }
19
21
  interface ClaudeRunResult {
20
22
  exitCode: number;
@@ -24,6 +26,79 @@ export interface PlanResult {
24
26
  featureId: string | null;
25
27
  costUsd: number | null;
26
28
  }
29
+ export type WorktreeMode = 'work-flow' | 'prefix';
30
+ export interface WorktreeContext {
31
+ path: string;
32
+ branch: string;
33
+ scope: string;
34
+ mode: WorktreeMode;
35
+ }
36
+ /**
37
+ * Hard requirement: beads must run in Dolt server mode so concurrent worktrees
38
+ * share one issue DB over TCP. Reads `.beads/metadata.json` and throws an
39
+ * actionable error on embedded/missing — call before any `bd` work.
40
+ */
41
+ export declare function assertDoltServerMode(projectDir: string, deps?: {
42
+ existsSyncFn?: (p: string) => boolean;
43
+ readFileFn?: (p: string, enc: string) => string;
44
+ }): void;
45
+ /**
46
+ * Discover the running shared Dolt server's port from `.beads/dolt-server.port`.
47
+ * Worktrees never start their own server — they reuse this one over TCP.
48
+ * Returns null when no port file is present (caller errors clearly). Liveness
49
+ * (pid still running) is best-effort and never blocks discovery.
50
+ */
51
+ export declare function discoverDoltServer(projectDir: string, deps?: {
52
+ existsSyncFn?: (p: string) => boolean;
53
+ readFileFn?: (p: string, enc: string) => string;
54
+ isPidAliveFn?: (pid: number) => boolean;
55
+ }): {
56
+ port: number;
57
+ pid?: number;
58
+ alive?: boolean;
59
+ } | null;
60
+ /**
61
+ * Create (or reuse) the loop's own git worktree at `../boop-<scope>` on branch
62
+ * `work/<scope>`, then pre-warm `npm install --prefer-offline`. This is what
63
+ * makes N loops run concurrently without colliding — each gets its own tree and
64
+ * branch; main is never checked out.
65
+ *
66
+ * Collision (Decision #4): if a worktree already exists at the path on the same
67
+ * branch, reuse it (and skip install if node_modules is present); if the path is
68
+ * occupied by something else, throw a clear error rather than silently suffixing.
69
+ */
70
+ export declare function createWorktree(scope: string, mode: WorktreeMode, projectDir: string, deps?: {
71
+ execSyncFn?: (cmd: string, opts: {
72
+ encoding: string;
73
+ cwd?: string;
74
+ stdio?: unknown;
75
+ }) => string;
76
+ existsSyncFn?: (p: string) => boolean;
77
+ copyFileFn?: (src: string, dest: string) => void;
78
+ readdirFn?: (p: string) => string[];
79
+ readFileFn?: (p: string) => string;
80
+ appendFileFn?: (p: string, data: string) => void;
81
+ }): WorktreeContext;
82
+ /**
83
+ * Write `.boop-worktree.json` into the worktree root. This is the contract
84
+ * Slice 2 (`boop loop --merge`) reads — `mode` drives its readiness query.
85
+ */
86
+ export declare function writeWorktreeMetadata(ctx: WorktreeContext, meta: {
87
+ workPath?: string;
88
+ flowPath?: string;
89
+ boopVersion: string;
90
+ startTime: string;
91
+ operator?: string;
92
+ budget?: string;
93
+ model?: string;
94
+ }, deps?: {
95
+ writeFileFn?: (p: string, data: string) => void;
96
+ }): Record<string, unknown>;
97
+ interface ReadyIssue {
98
+ id: string;
99
+ issue_type: string;
100
+ parent?: string;
101
+ }
27
102
  export declare function buildPlanningPrompt(workContent: string, flowContent: string, prefix?: string): string;
28
103
  export type RunClaudeFn = (prompt: string, opts: {
29
104
  budget: string;
@@ -32,6 +107,7 @@ export type RunClaudeFn = (prompt: string, opts: {
32
107
  sessionId: string;
33
108
  resume?: boolean;
34
109
  bare?: boolean;
110
+ cwd?: string;
35
111
  }, logFile: string, readableLogFile?: string) => Promise<ClaudeRunResult>;
36
112
  export declare function planWork(opts: {
37
113
  workPath: string;
@@ -55,9 +131,76 @@ export declare function findFallbackFeature(features: Array<{
55
131
  export declare function extractFeatureId(text: string): string | null;
56
132
  export declare function isUsageLimitText(text: string): boolean;
57
133
  export declare function isStandaloneUsageLimitMessage(text: string): boolean;
134
+ /**
135
+ * Resolve the model for a loop run. Precedence: an explicit `--model` flag, then
136
+ * the `ANTHROPIC_MODEL` environment variable, then the built-in default. The env
137
+ * fallback is what lets a nested `boop loop` (spawned by a Claude session that
138
+ * the parent loop launched with ANTHROPIC_MODEL set) inherit the same model
139
+ * instead of silently dropping to the default. Keep the default (claude-opus-4-8)
140
+ * in sync with the `--model` help text in cli.ts.
141
+ */
142
+ export declare function resolveModel(optionModel: string | undefined, env?: NodeJS.ProcessEnv): string;
143
+ /**
144
+ * Whether the loop should resume an issue that a session left open. A `blocked`
145
+ * issue was deliberately halted by the prior session because it judged it could
146
+ * not safely proceed — resuming just spends budget re-halting and risks the
147
+ * resume prompt being read as authorization to work past the block. Stop and
148
+ * surface it for the operator instead. Any other open status is genuinely
149
+ * unfinished work and is fine to resume.
150
+ */
151
+ export declare function shouldResumeUnclosedIssue(status: string | undefined): boolean;
58
152
  export declare function isIssueInScope(issue: {
59
153
  id: string;
60
154
  parent?: string;
61
155
  }, prefix: string, getParentById: (id: string) => string | null): boolean;
156
+ /**
157
+ * A workflow-owned feature root is a feature-type issue that owns child tasks
158
+ * (the workflow steps: understand → plan → … → closeout). Once those children
159
+ * close, `bd ready` surfaces the feature root itself — but it is a *container*,
160
+ * not directly claimable work. Claiming it lets a worker session absorb
161
+ * punch-list fixes or whole workflow steps (the boop-qna / wishfleet-O7 bug).
162
+ *
163
+ * `hasChildren` is only consulted for feature-type issues, so non-features never
164
+ * pay the cost of the children lookup.
165
+ */
166
+ export declare function isClaimableReadyIssue(issue: {
167
+ id: string;
168
+ issue_type: string;
169
+ }, hasChildren: (id: string) => boolean): boolean;
170
+ /**
171
+ * Decide whether the loop should close a container root itself, as a safety net,
172
+ * when its workflow chain has completed. The intended owner of this close is the
173
+ * closeout child task (see the flow SKILL's closeout step), but if that task never
174
+ * closed the root — or no closeout task exists — the container would be orphaned
175
+ * open forever once filtered from claimable work. Applies to both container types
176
+ * the loop supports: a `feature` (feature-dev flow) or an `epic` (light-dev flow).
177
+ * Only close when the container is still open and every child is closed.
178
+ */
179
+ export declare function shouldCloseFeatureRoot(feature: {
180
+ status: string;
181
+ issue_type: string;
182
+ } | null, children: {
183
+ status: string;
184
+ }[]): boolean;
185
+ export declare function getMatchingReadyIssues(prefix: string, includeFeatures: boolean, deps?: {
186
+ execSyncFn?: (cmd: string, opts: {
187
+ encoding: string;
188
+ }) => string;
189
+ }): ReadyIssue[];
190
+ /**
191
+ * Default per-issue budget cap in USD. The planning phase and each task each get
192
+ * this cap. Single source of truth — imported by cli.ts for its --budget help
193
+ * text so the documented default and the runtime fallback can't drift.
194
+ */
195
+ export declare const DEFAULT_BUDGET_USD = "25.00";
196
+ /**
197
+ * Sent when resuming a session that ended without closing its issue. The opener
198
+ * must NOT read as operator authorization (the old "Approved. Continue..." text
199
+ * could be taken as permission to work past a block): an agent that marked the
200
+ * issue `blocked` made a deliberate safety call, and the block stands. The loop
201
+ * also skips resuming a `blocked` issue entirely (see shouldResumeUnclosedIssue)
202
+ * — this clause is the belt-and-suspenders for a block raised mid-resume.
203
+ */
204
+ export declare const RESUME_PROMPT = "The previous session ended without closing this issue. Reload the state and continue.\n\nIMPORTANT: If the previous session marked this issue blocked with a diagnostic, the block stands. Do not re-open it or work around it. Read the issue's notes and the previous session's commits, confirm the block is still appropriate, and stop without taking further action.\n\nOtherwise, complete the workflow:\n- Finish any remaining implementation\n- Run tests to verify\n- Commit changes referencing the issue ID\n- Close the issue with bd close\n- Push with: git pull --rebase && git push\n\nDo not pause for review of completed work. Complete everything now.";
62
205
  export declare function runLoop(options: LoopOptions): Promise<void>;
63
206
  export {};