@boopkit/cli 0.6.1 → 0.6.8
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 +33 -2
- package/dist/capability-catalog.json +7 -0
- package/dist/capability-versions.json +9 -8
- package/dist/cli.js +4 -1
- package/dist/commands/loop.d.ts +21 -1
- package/dist/commands/loop.js +172 -5
- package/docs/loop.md +2 -1
- package/docs/skills.md +17 -2
- package/package.json +1 -1
- package/profiles/web-app/profile.md +1 -0
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ A profile is where a project starts. Today there's one; more will follow as the
|
|
|
73
73
|
|
|
74
74
|
Full-stack web application — Express + Handlebars + Tailwind CSS + PostgreSQL via Prisma, deployable to Heroku.
|
|
75
75
|
|
|
76
|
-
Ships with application capabilities (`web`, `api`, `services`, `database`), development skills (`coding`, `testing`, `project-mgmt`, `product-design`, `skills`), and the `hello` demo capability.
|
|
76
|
+
Ships with application capabilities (`web`, `api`, `services`, `database`), development skills (`coding`, `testing`, `project-mgmt`, `product-design`, `skills`, `workflows`), and the `hello` demo capability.
|
|
77
77
|
|
|
78
78
|
List profiles with `boop list profiles`.
|
|
79
79
|
|
|
@@ -105,8 +105,38 @@ Pick what the product needs. `boop add <name>` handles the wiring — npm depend
|
|
|
105
105
|
| `product-design` | Product design skills — feature requirements, engineering design, documentation, design thought-partnership, and technical prose |
|
|
106
106
|
| `ux` | UX judgement (purpose + visual) and the app-UI evaluation and delivery workflows |
|
|
107
107
|
| `skills` | Skill-authoring and skill-discovery guidance for extending the skill library |
|
|
108
|
+
| `workflows` | Reusable end-to-end dev/writing workflows — feature, light, refactor, test-audit, architecture-audit, issue-sweep, website, design-handoff, knowledge-backfill |
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
### Skills each capability ships
|
|
111
|
+
|
|
112
|
+
Adding a capability installs its skills into your project's `skills/` directory. Here's the full set, grouped by capability (one-line descriptions for each are in the bundled `docs/skills.md`):
|
|
113
|
+
|
|
114
|
+
<!-- SKILLS-INDEX:START -->
|
|
115
|
+
<!-- Generated by `npm run skills:index`. Do not edit by hand. -->
|
|
116
|
+
|
|
117
|
+
**Development capabilities (skills only — no runtime code)**
|
|
118
|
+
|
|
119
|
+
- **`coding`** — boop-coding-beads-flow, boop-coding-development, boop-coding-commit-changes, boop-coding-review-changes, boop-coding-debugging
|
|
120
|
+
- **`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
|
|
121
|
+
- **`project-mgmt`** — boop-project-mgmt-beads-planning, boop-project-mgmt-engineering-planning
|
|
122
|
+
- **`skills`** — boop-skill-authoring, boop-skills-discovery, boop-skills-agent-surface-design, boop-get-smart, boop-so-what
|
|
123
|
+
- **`testing`** — boop-testing-tdd-light, boop-testing-testing-express-web, boop-testing-testing
|
|
124
|
+
- **`ux`** — boop-ux-purpose-judgement, boop-ux-visual-judgement, boop-ux-eval-and-refine-workflow, boop-ux-app-ui-dev-workflow
|
|
125
|
+
- **`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
|
|
126
|
+
|
|
127
|
+
**Application capabilities**
|
|
128
|
+
|
|
129
|
+
- **`api`** — boop-api-rest-api-design
|
|
130
|
+
- **`background-jobs`** — boop-background-jobs-background-jobs-design
|
|
131
|
+
- **`cloud-storage`** — boop-cloud-storage-cloud-storage
|
|
132
|
+
- **`database`** — boop-database-prisma, boop-database-data-model
|
|
133
|
+
- **`email`** — boop-email-email-design
|
|
134
|
+
- **`services`** — boop-services-controllers, boop-services-services
|
|
135
|
+
- **`users-and-accounts`** — boop-users-user-data-modeling
|
|
136
|
+
- **`web`** — boop-web-web-ui-design
|
|
137
|
+
|
|
138
|
+
_One-line description of each skill: see the bundled `docs/skills.md`._
|
|
139
|
+
<!-- SKILLS-INDEX:END -->
|
|
110
140
|
|
|
111
141
|
### Demo
|
|
112
142
|
|
|
@@ -173,6 +203,7 @@ Boop itself is light — Node and npm. The other requirements come from whicheve
|
|
|
173
203
|
|
|
174
204
|
- **Node.js 24+** (the `web-app` profile enforces this)
|
|
175
205
|
- **npm** (or any package manager that respects workspaces)
|
|
206
|
+
- **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).
|
|
176
207
|
- **PostgreSQL** — if the `database` capability is installed
|
|
177
208
|
- **RabbitMQ / CloudAMQP account** — if `background-jobs` is installed
|
|
178
209
|
- **SendGrid account** — if `email` is installed
|
|
@@ -124,5 +124,12 @@
|
|
|
124
124
|
"type": "application",
|
|
125
125
|
"package": "@boopkit/cap-web",
|
|
126
126
|
"requires": []
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "workflows",
|
|
130
|
+
"description": "Reusable end-to-end development and writing workflows",
|
|
131
|
+
"type": "development",
|
|
132
|
+
"package": "@boopkit/cap-workflows",
|
|
133
|
+
"requires": []
|
|
127
134
|
}
|
|
128
135
|
]
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"@boopkit/cap-api": "0.4.2",
|
|
3
|
-
"@boopkit/cap-background-jobs": "0.4.
|
|
3
|
+
"@boopkit/cap-background-jobs": "0.4.2",
|
|
4
4
|
"@boopkit/cap-cloud-storage": "0.4.0",
|
|
5
5
|
"@boopkit/cap-coding": "0.4.2",
|
|
6
6
|
"@boopkit/cap-database": "0.4.3",
|
|
7
|
-
"@boopkit/cap-email": "0.4.
|
|
7
|
+
"@boopkit/cap-email": "0.4.2",
|
|
8
8
|
"@boopkit/cap-hello": "0.4.0",
|
|
9
9
|
"@boopkit/cap-noticeboard": "0.4.0",
|
|
10
|
-
"@boopkit/cap-product-design": "0.6.
|
|
11
|
-
"@boopkit/cap-project-mgmt": "0.4.
|
|
10
|
+
"@boopkit/cap-product-design": "0.6.5",
|
|
11
|
+
"@boopkit/cap-project-mgmt": "0.4.4",
|
|
12
12
|
"@boopkit/cap-services": "0.4.2",
|
|
13
|
-
"@boopkit/cap-skills": "0.5.
|
|
14
|
-
"@boopkit/cap-testing": "0.5.
|
|
13
|
+
"@boopkit/cap-skills": "0.5.2",
|
|
14
|
+
"@boopkit/cap-testing": "0.5.2",
|
|
15
15
|
"@boopkit/cap-users": "0.4.3",
|
|
16
|
-
"@boopkit/cap-ux": "0.2.
|
|
17
|
-
"@boopkit/cap-web": "0.5.
|
|
16
|
+
"@boopkit/cap-ux": "0.2.2",
|
|
17
|
+
"@boopkit/cap-web": "0.5.2",
|
|
18
|
+
"@boopkit/cap-workflows": "0.1.0",
|
|
18
19
|
"@boopkit/logger": "0.4.0"
|
|
19
20
|
}
|
package/dist/cli.js
CHANGED
|
@@ -119,11 +119,12 @@ program
|
|
|
119
119
|
.option('--run', 'actually execute (dry-run by default)')
|
|
120
120
|
.option('--max <n>', 'stop after N issues')
|
|
121
121
|
.option('--budget <amount>', `budget cap per issue in USD (default: ${loop_1.DEFAULT_BUDGET_USD})`)
|
|
122
|
-
.option('--model <model>', 'Claude model to use, applied to every spawned session and nested loop (falls back to $ANTHROPIC_MODEL, then claude-opus-
|
|
122
|
+
.option('--model <model>', 'Claude model to use, applied to every spawned session and nested loop (falls back to $ANTHROPIC_MODEL, then claude-opus-5). Sub-agents keep their own declared model unless they opt into model: inherit.')
|
|
123
123
|
.option('--poll', 'keep polling when no ready issues found')
|
|
124
124
|
.option('--poll-interval <seconds>', 'seconds between polls (default: 60)')
|
|
125
125
|
.option('--resume-limit <n>', 'max resume attempts per issue (default: 3)')
|
|
126
126
|
.option('--per-issue-branch', 'create a separate branch per issue')
|
|
127
|
+
.option('--reuse-session', 'issue-driven only: reuse one Claude session across a coherent --prefix run instead of a fresh one per issue, rotating to a fresh session when its context fills. Asserts the scope is coherent enough for carried context to help. Not valid with --work/--flow; mutually exclusive with --per-issue-branch')
|
|
127
128
|
.option('--no-branch', 'skip branch/worktree creation; run in place on the current branch')
|
|
128
129
|
.option('--no-skip-perms', 'do not skip permission prompts')
|
|
129
130
|
.option('--bare', 'force API key auth; skip OAuth/keychain, use ANTHROPIC_API_KEY strictly')
|
|
@@ -133,11 +134,13 @@ program
|
|
|
133
134
|
'Examples:',
|
|
134
135
|
' boop loop --work specs/feature.spec.md --flow skills/my-flow/SKILL.md --run',
|
|
135
136
|
' boop loop --prefix boop-abc --run --features',
|
|
137
|
+
' boop loop --prefix boop-abc --run --reuse-session # drain a coherent cluster on one session',
|
|
136
138
|
' boop loop --merge --run --watch',
|
|
137
139
|
'',
|
|
138
140
|
'Notes:',
|
|
139
141
|
' • Dry-run by default; add --run to execute.',
|
|
140
142
|
' • --work and --flow must be used together; --features and --watch only apply to their own mode.',
|
|
143
|
+
' • --reuse-session is issue-driven (--prefix) only and cannot combine with --per-issue-branch; the loop rotates to a fresh session on its own when context fills, so issues are never split. Off by default (a fresh session per issue).',
|
|
141
144
|
' • Merge mode rebases ready work/<id> branches onto main, runs tests, ff-merges, and cleans up worktrees.',
|
|
142
145
|
].join('\n'))
|
|
143
146
|
.action((options) => (options.merge ? (0, loop_merge_1.runMerge)(options) : (0, loop_1.runLoop)(options)));
|
package/dist/commands/loop.d.ts
CHANGED
|
@@ -17,10 +17,15 @@ export interface LoopOptions {
|
|
|
17
17
|
json?: boolean;
|
|
18
18
|
merge?: boolean | string;
|
|
19
19
|
watch?: boolean;
|
|
20
|
+
reuseSession?: boolean;
|
|
20
21
|
}
|
|
21
22
|
interface ClaudeRunResult {
|
|
22
23
|
exitCode: number;
|
|
23
24
|
limitHit: boolean;
|
|
25
|
+
/** Peak context-window occupancy observed on this run, in tokens. */
|
|
26
|
+
peakContext?: number;
|
|
27
|
+
/** The model's context window (tokens), from the result's modelUsage. */
|
|
28
|
+
contextWindow?: number;
|
|
24
29
|
}
|
|
25
30
|
export interface PlanResult {
|
|
26
31
|
featureId: string | null;
|
|
@@ -67,6 +72,15 @@ export declare function discoverDoltServer(projectDir: string, deps?: {
|
|
|
67
72
|
* branch, reuse it (and skip install if node_modules is present); if the path is
|
|
68
73
|
* occupied by something else, throw a clear error rather than silently suffixing.
|
|
69
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Best-effort resolution of the repo's default branch, used to move a launch dir
|
|
77
|
+
* off a loop feature branch it should never have been parked on (boop-6iw).
|
|
78
|
+
* Prefers `origin/HEAD`, then a local `main`/`master`, then `main`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveDefaultBranch(execSyncFn: (cmd: string, opts: {
|
|
81
|
+
encoding: string;
|
|
82
|
+
cwd?: string;
|
|
83
|
+
}) => string, cwd: string): string;
|
|
70
84
|
export declare function createWorktree(scope: string, mode: WorktreeMode, projectDir: string, deps?: {
|
|
71
85
|
execSyncFn?: (cmd: string, opts: {
|
|
72
86
|
encoding: string;
|
|
@@ -136,7 +150,7 @@ export declare function isStandaloneUsageLimitMessage(text: string): boolean;
|
|
|
136
150
|
* the `ANTHROPIC_MODEL` environment variable, then the built-in default. The env
|
|
137
151
|
* fallback is what lets a nested `boop loop` (spawned by a Claude session that
|
|
138
152
|
* 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-
|
|
153
|
+
* instead of silently dropping to the default. Keep the default (claude-opus-5)
|
|
140
154
|
* in sync with the `--model` help text in cli.ts.
|
|
141
155
|
*/
|
|
142
156
|
export declare function resolveModel(optionModel: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
@@ -202,5 +216,11 @@ export declare const DEFAULT_BUDGET_USD = "25.00";
|
|
|
202
216
|
* — this clause is the belt-and-suspenders for a block raised mid-resume.
|
|
203
217
|
*/
|
|
204
218
|
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.";
|
|
219
|
+
/**
|
|
220
|
+
* Decide whether a reused session should rotate before taking the next issue.
|
|
221
|
+
* Rotates once peak observed occupancy reaches ROTATE_FRACTION of the window.
|
|
222
|
+
* Pure so it is unit-testable without a running loop.
|
|
223
|
+
*/
|
|
224
|
+
export declare function shouldRotateSession(peakContext: number, contextWindow: number, fraction?: number): boolean;
|
|
205
225
|
export declare function runLoop(options: LoopOptions): Promise<void>;
|
|
206
226
|
export {};
|
package/dist/commands/loop.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.RESUME_PROMPT = exports.DEFAULT_BUDGET_USD = void 0;
|
|
37
37
|
exports.assertDoltServerMode = assertDoltServerMode;
|
|
38
38
|
exports.discoverDoltServer = discoverDoltServer;
|
|
39
|
+
exports.resolveDefaultBranch = resolveDefaultBranch;
|
|
39
40
|
exports.createWorktree = createWorktree;
|
|
40
41
|
exports.writeWorktreeMetadata = writeWorktreeMetadata;
|
|
41
42
|
exports.buildPlanningPrompt = buildPlanningPrompt;
|
|
@@ -50,6 +51,7 @@ exports.isIssueInScope = isIssueInScope;
|
|
|
50
51
|
exports.isClaimableReadyIssue = isClaimableReadyIssue;
|
|
51
52
|
exports.shouldCloseFeatureRoot = shouldCloseFeatureRoot;
|
|
52
53
|
exports.getMatchingReadyIssues = getMatchingReadyIssues;
|
|
54
|
+
exports.shouldRotateSession = shouldRotateSession;
|
|
53
55
|
exports.runLoop = runLoop;
|
|
54
56
|
const child_process_1 = require("child_process");
|
|
55
57
|
const fs = __importStar(require("fs"));
|
|
@@ -130,6 +132,31 @@ function discoverDoltServer(projectDir, deps) {
|
|
|
130
132
|
* branch, reuse it (and skip install if node_modules is present); if the path is
|
|
131
133
|
* occupied by something else, throw a clear error rather than silently suffixing.
|
|
132
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort resolution of the repo's default branch, used to move a launch dir
|
|
137
|
+
* off a loop feature branch it should never have been parked on (boop-6iw).
|
|
138
|
+
* Prefers `origin/HEAD`, then a local `main`/`master`, then `main`.
|
|
139
|
+
*/
|
|
140
|
+
function resolveDefaultBranch(execSyncFn, cwd) {
|
|
141
|
+
try {
|
|
142
|
+
const ref = execSyncFn('git symbolic-ref --short refs/remotes/origin/HEAD', { encoding: 'utf8', cwd }).trim();
|
|
143
|
+
if (ref)
|
|
144
|
+
return ref.replace(/^origin\//, '');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// origin/HEAD not set — fall through to local candidates.
|
|
148
|
+
}
|
|
149
|
+
for (const cand of ['main', 'master']) {
|
|
150
|
+
try {
|
|
151
|
+
execSyncFn(`git rev-parse --verify --quiet refs/heads/${cand}`, { encoding: 'utf8', cwd });
|
|
152
|
+
return cand;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// not present — try next
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return 'main';
|
|
159
|
+
}
|
|
133
160
|
function createWorktree(scope, mode, projectDir, deps) {
|
|
134
161
|
const execSyncFn = deps?.execSyncFn ||
|
|
135
162
|
((cmd, o) => (0, child_process_1.execSync)(cmd, o));
|
|
@@ -281,6 +308,32 @@ function createWorktree(scope, mode, projectDir, deps) {
|
|
|
281
308
|
branchExists = false;
|
|
282
309
|
}
|
|
283
310
|
if (branchExists) {
|
|
311
|
+
// Guard (boop-6iw): if the loop branch is checked out in the launch dir — e.g. a
|
|
312
|
+
// planning phase that ran `git checkout -b work/<id>` in place before the worktree
|
|
313
|
+
// existed — then `git worktree add <path> <branch>` fails with "already checked
|
|
314
|
+
// out at <launch-dir>". It also leaves the launch dir parked on the feature branch,
|
|
315
|
+
// so a concurrent session's next commit lands on the wrong branch. Move the launch
|
|
316
|
+
// dir back to its default branch first: it both unblocks the worktree add and undoes
|
|
317
|
+
// the mis-parking. (Flows should not create the branch during planning — see the
|
|
318
|
+
// light-dev-workflow skill — but this makes any that slip through non-fatal.)
|
|
319
|
+
let launchBranch = '';
|
|
320
|
+
try {
|
|
321
|
+
launchBranch = execSyncFn('git branch --show-current', { encoding: 'utf8', cwd: projectDir }).trim();
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Detached HEAD or no git — nothing of ours is parked; proceed.
|
|
325
|
+
}
|
|
326
|
+
if (launchBranch === branch) {
|
|
327
|
+
const defaultBranch = resolveDefaultBranch(execSyncFn, projectDir);
|
|
328
|
+
try {
|
|
329
|
+
execSyncFn(`git checkout ${defaultBranch}`, { encoding: 'utf8', cwd: projectDir });
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
throw new Error(`Loop branch "${branch}" is checked out in the launch directory (${projectDir}), which blocks ` +
|
|
333
|
+
`creating its worktree. Moving the launch dir to "${defaultBranch}" failed — likely uncommitted ` +
|
|
334
|
+
`changes there. Commit or stash them (or run \`git checkout ${defaultBranch}\`) and retry.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
284
337
|
execSyncFn(`git worktree add ${worktreePath} ${branch}`, { encoding: 'utf8', cwd: projectDir });
|
|
285
338
|
}
|
|
286
339
|
else {
|
|
@@ -539,11 +592,11 @@ function findClaudeBinary() {
|
|
|
539
592
|
* the `ANTHROPIC_MODEL` environment variable, then the built-in default. The env
|
|
540
593
|
* fallback is what lets a nested `boop loop` (spawned by a Claude session that
|
|
541
594
|
* the parent loop launched with ANTHROPIC_MODEL set) inherit the same model
|
|
542
|
-
* instead of silently dropping to the default. Keep the default (claude-opus-
|
|
595
|
+
* instead of silently dropping to the default. Keep the default (claude-opus-5)
|
|
543
596
|
* in sync with the `--model` help text in cli.ts.
|
|
544
597
|
*/
|
|
545
598
|
function resolveModel(optionModel, env = process.env) {
|
|
546
|
-
return optionModel || env.ANTHROPIC_MODEL || 'claude-opus-
|
|
599
|
+
return optionModel || env.ANTHROPIC_MODEL || 'claude-opus-5';
|
|
547
600
|
}
|
|
548
601
|
/**
|
|
549
602
|
* Whether the loop should resume an issue that a session left open. A `blocked`
|
|
@@ -793,6 +846,33 @@ Otherwise, complete the workflow:
|
|
|
793
846
|
- Push with: git pull --rebase && git push
|
|
794
847
|
|
|
795
848
|
Do not pause for review of completed work. Complete everything now.`;
|
|
849
|
+
/**
|
|
850
|
+
* Fraction of the context window at which a reused session is rotated for a fresh
|
|
851
|
+
* one. Deliberately well below Claude Code's auto-compaction point — compaction
|
|
852
|
+
* silently discards the context the reuse exists to keep, so we rotate first.
|
|
853
|
+
* The one knob a trial run should tune (specs/loop-session-reuse.spec.md).
|
|
854
|
+
*/
|
|
855
|
+
const ROTATE_FRACTION = 0.55;
|
|
856
|
+
/** Fallback context window (tokens) before the stream has reported a real one. */
|
|
857
|
+
const DEFAULT_CONTEXT_WINDOW = 1000000;
|
|
858
|
+
/**
|
|
859
|
+
* Live context-window occupancy for one turn, from a stream-json `usage` object:
|
|
860
|
+
* everything the model had to read this turn (fresh input + cached prompt).
|
|
861
|
+
*/
|
|
862
|
+
function contextOccupancy(usage) {
|
|
863
|
+
if (!usage)
|
|
864
|
+
return 0;
|
|
865
|
+
return (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Decide whether a reused session should rotate before taking the next issue.
|
|
869
|
+
* Rotates once peak observed occupancy reaches ROTATE_FRACTION of the window.
|
|
870
|
+
* Pure so it is unit-testable without a running loop.
|
|
871
|
+
*/
|
|
872
|
+
function shouldRotateSession(peakContext, contextWindow, fraction = ROTATE_FRACTION) {
|
|
873
|
+
const window = contextWindow > 0 ? contextWindow : DEFAULT_CONTEXT_WINDOW;
|
|
874
|
+
return peakContext >= fraction * window;
|
|
875
|
+
}
|
|
796
876
|
function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
797
877
|
return new Promise((resolve) => {
|
|
798
878
|
const args = [
|
|
@@ -837,6 +917,11 @@ function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
|
837
917
|
: process.env;
|
|
838
918
|
const child = (0, child_process_1.spawn)(claudeBin, args, { cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
|
|
839
919
|
let limitHit = false;
|
|
920
|
+
// Track context-window pressure from outside the agent (spec: the loop reads
|
|
921
|
+
// the stream it already parses; the agent is never told). Peak occupancy over
|
|
922
|
+
// the run drives session rotation; contextWindow comes from the result.
|
|
923
|
+
let peakContext = 0;
|
|
924
|
+
let contextWindow;
|
|
840
925
|
child.stdin.write(prompt);
|
|
841
926
|
child.stdin.end();
|
|
842
927
|
child.stdout.on('data', (chunk) => {
|
|
@@ -846,6 +931,10 @@ function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
|
846
931
|
try {
|
|
847
932
|
const msg = JSON.parse(line);
|
|
848
933
|
if (msg.type === 'assistant') {
|
|
934
|
+
// Live context occupancy for this turn (read from outside the agent).
|
|
935
|
+
const occ = contextOccupancy(msg.message?.usage);
|
|
936
|
+
if (occ > peakContext)
|
|
937
|
+
peakContext = occ;
|
|
849
938
|
const texts = (msg.message?.content || [])
|
|
850
939
|
.filter((c) => c.type === 'text')
|
|
851
940
|
.map((c) => c.text);
|
|
@@ -875,6 +964,16 @@ function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
|
875
964
|
if ((msg.is_error || msg.subtype === 'error') && isUsageLimitText(resultText)) {
|
|
876
965
|
limitHit = true;
|
|
877
966
|
}
|
|
967
|
+
// The result reports the model's context window under modelUsage.<model>.
|
|
968
|
+
const mu = msg.modelUsage;
|
|
969
|
+
if (mu) {
|
|
970
|
+
for (const m of Object.values(mu)) {
|
|
971
|
+
if (typeof m?.contextWindow === 'number' && m.contextWindow > 0) {
|
|
972
|
+
contextWindow = m.contextWindow;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
878
977
|
const cost = msg.total_cost_usd || '?';
|
|
879
978
|
const dur = Math.round((msg.duration_ms || 0) / 1000);
|
|
880
979
|
const turns = msg.num_turns || '?';
|
|
@@ -901,12 +1000,12 @@ function runClaude(prompt, opts, logFile, readableLogFile) {
|
|
|
901
1000
|
logStream.end();
|
|
902
1001
|
if (readableStream)
|
|
903
1002
|
readableStream.end();
|
|
904
|
-
resolve({ exitCode: code ?? 1, limitHit });
|
|
1003
|
+
resolve({ exitCode: code ?? 1, limitHit, peakContext, contextWindow });
|
|
905
1004
|
});
|
|
906
1005
|
});
|
|
907
1006
|
}
|
|
908
1007
|
async function processIssue(issue, opts) {
|
|
909
|
-
const sessionId = (0, crypto_1.randomUUID)();
|
|
1008
|
+
const sessionId = opts.session?.id ?? (0, crypto_1.randomUUID)();
|
|
910
1009
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
911
1010
|
const logFile = path.join(opts.logDir, `${issue.id}_${timestamp}.log`);
|
|
912
1011
|
const readableLogFile = path.join(opts.logDir, `${issue.id}_${timestamp}.readable.log`);
|
|
@@ -944,9 +1043,13 @@ async function processIssue(issue, opts) {
|
|
|
944
1043
|
skipPerms: opts.skipPerms,
|
|
945
1044
|
bare: opts.bare,
|
|
946
1045
|
sessionId,
|
|
1046
|
+
// Continue a carried session for this new issue (its normal prompt via --resume).
|
|
1047
|
+
resume: opts.session?.resume,
|
|
947
1048
|
cwd: opts.cwd,
|
|
948
1049
|
}, logFile, readableLogFile);
|
|
949
1050
|
let anyLimitHit = initialRun.limitHit;
|
|
1051
|
+
let peakContext = initialRun.peakContext ?? 0;
|
|
1052
|
+
let contextWindow = initialRun.contextWindow;
|
|
950
1053
|
if (initialRun.limitHit) {
|
|
951
1054
|
console.error('\x1b[31m[bd-loop]\x1b[0m Claude usage limit reached; stopping retries for this issue');
|
|
952
1055
|
}
|
|
@@ -977,6 +1080,10 @@ async function processIssue(issue, opts) {
|
|
|
977
1080
|
resume: true,
|
|
978
1081
|
cwd: opts.cwd,
|
|
979
1082
|
}, logFile, readableLogFile);
|
|
1083
|
+
if (resumeRun.peakContext && resumeRun.peakContext > peakContext)
|
|
1084
|
+
peakContext = resumeRun.peakContext;
|
|
1085
|
+
if (resumeRun.contextWindow)
|
|
1086
|
+
contextWindow = resumeRun.contextWindow;
|
|
980
1087
|
if (resumeRun.limitHit) {
|
|
981
1088
|
anyLimitHit = true;
|
|
982
1089
|
console.error('\x1b[31m[bd-loop]\x1b[0m Claude usage limit reached during resume; stopping retries for this issue');
|
|
@@ -999,7 +1106,7 @@ async function processIssue(issue, opts) {
|
|
|
999
1106
|
// Best effort
|
|
1000
1107
|
}
|
|
1001
1108
|
}
|
|
1002
|
-
return { issueId: issue.id, closed, resumes: resumeCount, logFile, limitHit: anyLimitHit };
|
|
1109
|
+
return { issueId: issue.id, closed, resumes: resumeCount, logFile, limitHit: anyLimitHit, sessionId, peakContext, contextWindow };
|
|
1003
1110
|
}
|
|
1004
1111
|
function sleep(ms) {
|
|
1005
1112
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1062,6 +1169,19 @@ async function runLoop(options) {
|
|
|
1062
1169
|
const skipPerms = options.skipPerms !== false; // default true
|
|
1063
1170
|
const bare = options.bare || false;
|
|
1064
1171
|
const poll = options.poll || false;
|
|
1172
|
+
const reuseSession = options.reuseSession || false;
|
|
1173
|
+
// --reuse-session is a --prefix-mode knob, mutually exclusive with the two things
|
|
1174
|
+
// that imply one-session-per-issue. Errors are plain, not silent winners.
|
|
1175
|
+
if (reuseSession && (work || flow)) {
|
|
1176
|
+
console.error('\x1b[31mERR\x1b[0m --reuse-session is only for --prefix runs, not --work/--flow (spec-driven children are already session-sized)');
|
|
1177
|
+
process.exitCode = 1;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (reuseSession && perIssueBranch) {
|
|
1181
|
+
console.error('\x1b[31mERR\x1b[0m --reuse-session and --per-issue-branch are mutually exclusive (a branch per issue implies a session per issue)');
|
|
1182
|
+
process.exitCode = 1;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1065
1185
|
const projectDir = process.cwd();
|
|
1066
1186
|
const logDir = path.join(projectDir, 'labs', 'claude-runs');
|
|
1067
1187
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -1171,6 +1291,14 @@ async function runLoop(options) {
|
|
|
1171
1291
|
let totalProcessed = 0;
|
|
1172
1292
|
let totalClosed = 0;
|
|
1173
1293
|
const results = [];
|
|
1294
|
+
// --reuse-session state: a healthy session survives to the next issue; a fresh
|
|
1295
|
+
// one is minted only on rotation (context full) or death. Off → activeSession
|
|
1296
|
+
// stays null and every issue mints its own, exactly as before.
|
|
1297
|
+
let activeSession = null;
|
|
1298
|
+
let sessionCount = 0;
|
|
1299
|
+
// Issue id at each session START (the first entry is the initial session, not a
|
|
1300
|
+
// rotation). Actual rotations are everything after the first — see the summary.
|
|
1301
|
+
const sessionStarts = [];
|
|
1174
1302
|
while (true) {
|
|
1175
1303
|
const nextId = prefix ? findNextIssue(prefix, includeFeatures) : null;
|
|
1176
1304
|
if (!nextId) {
|
|
@@ -1238,6 +1366,24 @@ async function runLoop(options) {
|
|
|
1238
1366
|
console.error(`\x1b[31m[bd-loop]\x1b[0m Failed to get issue details for ${nextId}`);
|
|
1239
1367
|
continue;
|
|
1240
1368
|
}
|
|
1369
|
+
// Decide the session this issue runs under. Reuse a healthy carried session;
|
|
1370
|
+
// otherwise (first issue, or the last one filled up) mint a fresh one. The
|
|
1371
|
+
// decision lands here, between issues, so rotation can never split an issue.
|
|
1372
|
+
let session;
|
|
1373
|
+
if (reuseSession) {
|
|
1374
|
+
if (activeSession && !shouldRotateSession(activeSession.peakContext, activeSession.contextWindow)) {
|
|
1375
|
+
session = { id: activeSession.id, resume: true };
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
if (activeSession) {
|
|
1379
|
+
console.log(`\x1b[36m[bd-loop]\x1b[0m Session context full (${Math.round(activeSession.peakContext / 1000)}k/${Math.round(activeSession.contextWindow / 1000)}k) — rotating to a fresh session at ${issue.id}`);
|
|
1380
|
+
}
|
|
1381
|
+
session = { id: (0, crypto_1.randomUUID)(), resume: false };
|
|
1382
|
+
activeSession = { id: session.id, peakContext: 0, contextWindow: DEFAULT_CONTEXT_WINDOW };
|
|
1383
|
+
sessionCount++;
|
|
1384
|
+
sessionStarts.push(issue.id);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1241
1387
|
const result = await processIssue(issue, {
|
|
1242
1388
|
budget,
|
|
1243
1389
|
model,
|
|
@@ -1248,10 +1394,22 @@ async function runLoop(options) {
|
|
|
1248
1394
|
noBranch,
|
|
1249
1395
|
logDir,
|
|
1250
1396
|
cwd: loopCwd,
|
|
1397
|
+
session,
|
|
1251
1398
|
});
|
|
1252
1399
|
results.push(result);
|
|
1253
1400
|
if (result.closed)
|
|
1254
1401
|
totalClosed++;
|
|
1402
|
+
// Fold this issue's observed context back into the carried session so the next
|
|
1403
|
+
// iteration's rotation decision is current. A limit/death drops the session so
|
|
1404
|
+
// the next issue starts clean (recovery lands between issues, same as rotation).
|
|
1405
|
+
if (reuseSession && activeSession) {
|
|
1406
|
+
if (result.peakContext)
|
|
1407
|
+
activeSession.peakContext = Math.max(activeSession.peakContext, result.peakContext);
|
|
1408
|
+
if (result.contextWindow)
|
|
1409
|
+
activeSession.contextWindow = result.contextWindow;
|
|
1410
|
+
if (result.limitHit)
|
|
1411
|
+
activeSession = null;
|
|
1412
|
+
}
|
|
1255
1413
|
if (result.limitHit) {
|
|
1256
1414
|
console.error('\x1b[31m[bd-loop]\x1b[0m Credit/usage limit hit — stopping loop');
|
|
1257
1415
|
break;
|
|
@@ -1270,6 +1428,14 @@ async function runLoop(options) {
|
|
|
1270
1428
|
if (totalProcessed > totalClosed) {
|
|
1271
1429
|
console.log(`\x1b[33m[bd-loop]\x1b[0m Not closed: ${totalProcessed - totalClosed}`);
|
|
1272
1430
|
}
|
|
1431
|
+
if (reuseSession) {
|
|
1432
|
+
console.log(`\x1b[32m[bd-loop]\x1b[0m Sessions: ${sessionCount} (--reuse-session)`);
|
|
1433
|
+
// The first session start is not a rotation; only entries after it are.
|
|
1434
|
+
const rotatedAt = sessionStarts.slice(1);
|
|
1435
|
+
if (rotatedAt.length > 0) {
|
|
1436
|
+
console.log(`\x1b[32m[bd-loop]\x1b[0m Rotated at: ${rotatedAt.join(', ')}`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1273
1439
|
}
|
|
1274
1440
|
console.log(`\x1b[32m[bd-loop]\x1b[0m Logs: ${logDir}/`);
|
|
1275
1441
|
if (worktreeCtx) {
|
|
@@ -1283,6 +1449,7 @@ async function runLoop(options) {
|
|
|
1283
1449
|
totalClosed,
|
|
1284
1450
|
results,
|
|
1285
1451
|
worktree: worktreeCtx ? { path: worktreeCtx.path, branch: worktreeCtx.branch, mode: worktreeCtx.mode } : null,
|
|
1452
|
+
sessions: reuseSession ? { count: sessionCount, rotatedAt: sessionStarts.slice(1) } : null,
|
|
1286
1453
|
}, null, 2));
|
|
1287
1454
|
}
|
|
1288
1455
|
}
|
package/docs/loop.md
CHANGED
|
@@ -52,8 +52,9 @@ If `--prefix` is also provided, it is passed to the planning session as a naming
|
|
|
52
52
|
| `--resume-limit <n>` | `3` | Maximum resume attempts if issue isn't closed |
|
|
53
53
|
| `--poll` | off | Keep polling when no ready issues found |
|
|
54
54
|
| `--poll-interval <s>` | `60` | Seconds between polls |
|
|
55
|
-
| `--model <model>` | `claude-opus-
|
|
55
|
+
| `--model <model>` | `claude-opus-5` | Claude model to use (falls back to `$ANTHROPIC_MODEL`, then this default) |
|
|
56
56
|
| `--per-issue-branch` | off | Create a `fix/<issue-id>` branch per issue (nested inside the worktree) |
|
|
57
|
+
| `--reuse-session` | off | (`--prefix` only) Reuse one Claude session across a coherent run of issues, rotating to a fresh session when its context fills. Mutually exclusive with `--per-issue-branch` |
|
|
57
58
|
| `--no-branch` | off | Opt out of worktree isolation; run in-place on the current branch |
|
|
58
59
|
| `--features` | off | Include feature-type issues (auto-enabled in planning mode) |
|
|
59
60
|
| `--no-skip-perms` | off | Don't skip Claude permission prompts |
|
package/docs/skills.md
CHANGED
|
@@ -21,7 +21,8 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
|
|
|
21
21
|
- **boop-product-design-feature-requirements** — When you need to write a feature spec, draft a spec document, or turn a brief or vague directive into requirements — i.e. define what a feature should accomplish before designing how to build it. This fires even when you frame the task to yourself as just 'writing a spec' or 'matching the house spec format': reading an existing spec to imitate its shape is not a substitute for the method, and the more competent you feel at the raw task the more this applies. Start with the user's jobs to be done, in frequency order; take and defend a stance; then derive goals, non-goals, and success metrics.
|
|
22
22
|
- **boop-product-design-reference-integration** — When you need to integrate new knowledge into the project's `reference/` directory after completing a feature, fixing a bug that changes behavior, or adding a capability.
|
|
23
23
|
- **boop-product-design-thought-partnership** — When the user wants to think out loud through a design — a feature, a capability, a strategic direction — be a thought partner, not an implementer: hold the conversation at user-and-value altitude, honor uncertainty with hypotheses rather than pretend-decisions, and keep software-shaped concerns downstream of the user's lived problems.
|
|
24
|
-
- **boop-writing-technical-prose** — When writing chat responses, bd issue bodies, or any artifact a fresh reader will pick up cold, write grammatical conversational English. Avoid the agent-shorthand patterns that compress meaning at the cost of legibility.
|
|
24
|
+
- **boop-writing-technical-prose** — When writing chat responses, summaries, bd issue bodies, specifications, specs, or any artifact a fresh reader will pick up cold, write grammatical conversational English. Avoid the agent-shorthand patterns that compress meaning at the cost of legibility.
|
|
25
|
+
- **boop-writing-documents** — When you need to write a document someone will read cold and act on — a proposal, strategy memo, analysis, recommendation, or status write-up — and want the discipline of the Amazon six-pager: narrative prose, a declared length budget, claims that carry their evidence, and the hard questions answered inside the document.
|
|
25
26
|
|
|
26
27
|
### `project-mgmt` — Project management with beads
|
|
27
28
|
|
|
@@ -33,6 +34,8 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
|
|
|
33
34
|
- **boop-skill-authoring** — When you need to add or update an agent skill in this project, follow this format and checklist.
|
|
34
35
|
- **boop-skills-discovery** — When you pick up a non-trivial task, sweep the available skills for ones relevant to the work before you start, as a backstop for an issue that missed a breadcrumb.
|
|
35
36
|
- **boop-skills-agent-surface-design** — When you design or audit tool surfaces (tools, schemas, APIs, conventions) that LLM agents will reach for, decide where to put the friction — adapt the system to the agent, or make the existing thing more discoverable. Last resort: prompt-engineer.
|
|
37
|
+
- **boop-get-smart** — When there is a recurring fault in your own behaviour during a chat, run the five whys to find its root cause, and agree a change with the operator that makes it less likely.
|
|
38
|
+
- **boop-so-what** — When the operator asks for the so-what, or says some version of 'thanks for the essay', compress what you just produced into three sentences about what they should care about.
|
|
36
39
|
|
|
37
40
|
### `testing` — Jest testing setup
|
|
38
41
|
|
|
@@ -47,6 +50,18 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
|
|
|
47
50
|
- **boop-ux-eval-and-refine-workflow** — When you are tasked with iteratively evaluating and improving an existing rendered page (discovery, not delivery), follow this workflow. Inner loop: capture → eval → recommend → implement → re-capture. Outer: knowledge integration.
|
|
48
51
|
- **boop-ux-app-ui-dev-workflow** — When you are tasked with improving an existing app-side page (app UX, not marketing) end-to-end and the input is a target page rather than a written spec, follow this beads-issue-per-step workflow. Sibling to feature-dev-workflow but stripped of backend phases. The judgement lenses are the planning input.
|
|
49
52
|
|
|
53
|
+
### `workflows` — Reusable end-to-end development and writing workflows
|
|
54
|
+
|
|
55
|
+
- **boop-feature-dev-workflow** — When you are tasked with building a feature end-to-end, follow this guidance.
|
|
56
|
+
- **boop-light-dev-workflow** — When you are extending an existing system (front-end, back-end, or full-stack) toward a goal — wiring something new from data and patterns that already exist, not building greenfield — follow this grounded flow: goal → investigate the real system → design → critique → decompose into one scoped epic + child issues → run them → close out. Loop-drivable via boop loop --work --flow; lighter than feature-dev-workflow.
|
|
57
|
+
- **boop-refactor-dev-workflow** — When you need to change a contract, cardinality, or shape that existing code already depends on — usually to make a feature buildable — follow this workflow instead of the feature workflow.
|
|
58
|
+
- **boop-test-audit-workflow** — Use when a set of tests is not testing the purpose of the code: mocks that verify data the test itself fed in, tests that exercise the platform rather than our code, or a stubbed seam where the integration is the actual risk. Also use when tests are so coupled to the shape of the implementation that they check a label, an exact string, or a call sequence rather than a behaviour. This workflow removes tests that cannot go red for a real reason, rewrites the ones that check a real goal at the wrong level, merges duplicates, and does not lose coverage. It explicitly does not target good tests merely because a coming change will make them need updating. The sibling of light-dev-workflow for reworking tests rather than building features.
|
|
59
|
+
- **boop-architecture-audit-workflow** — When you need to judge whether an area of shipped code is structurally healthy and decide what is worth fixing — undesired coupling, concerns bleeding across layers or domain boundaries, interfaces that are not composable because they are use-case specific or unparameterized — follow this process: scope one area and name the properties it should uphold → check the tests can catch you → measure before reading → grade every property and classify every finding against a named change → critique → operator gate → decompose → execute via refactor-dev-workflow. The sibling of test-audit-workflow for production code rather than tests. Not for finding bugs, and not for executing a structural change you have already decided on.
|
|
60
|
+
- **boop-issue-sweep-workflow** — When the bd queue has accumulated a noisy backlog of lingering P1/P2 items — paper-cuts, small refactors, dead code, cosmetic bugs, taxonomy drift — and you want to clear them autonomously in a sequence that supports the week's goals, follow this process: inventory → cluster by theme → pick a cluster with the operator → run autonomously in one sweep worktree → hand off for a single UAT pass → log. The work lands in a worktree for one operator review rather than on main; it arrives with no decisions left in it.
|
|
61
|
+
- **boop-website-dev-workflow** — When you are tasked with implementing website changes end-to-end (new pages, page rewrites, IA changes, content/CTA updates), follow this guidance.
|
|
62
|
+
- **boop-design-handoff-workflow** — When the operator hands off a design package (a README plus a drop-in view template and CSS) for a redesigned surface, follow this process: read the README, VERIFY its claims against the real code, surface any buried decision to the operator, file an issue, run it as a sub-agent (branch for UAT or merge-on-clean-visual-check), then close out with knowledge integration and archival. The README is confident; confidence is not correctness — verify before you run.
|
|
63
|
+
- **boop-knowledge-backfill** — When you need to integrate new knowledge from a set of specs or completed work that was not documented in reference at the time it shipped. This often happens when a feature is developed under a tight deadline and the team prioritizes shipping over documentation, or when work was done before the reference system was fully established.
|
|
64
|
+
|
|
50
65
|
## Application capabilities
|
|
51
66
|
|
|
52
67
|
### `api` — REST API with Express
|
|
@@ -85,4 +100,4 @@ Every capability ships **agent skills** — focused `SKILL.md` guides that `boop
|
|
|
85
100
|
|
|
86
101
|
---
|
|
87
102
|
|
|
88
|
-
|
|
103
|
+
Boop-maintainer skills (e.g. `boop-capability-development`, `boop-downstream-harvest`, `boop-loop-conductor`, `boop-dolt-recovery`) live in the repo's top-level `skills/` directory rather than in a capability package, so they do not ship and are not listed above.
|
package/package.json
CHANGED