@backburner/cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ Useful `init` flags:
|
|
|
124
124
|
|
|
125
125
|
If either file already exists, onboarding asks before overwriting it. With `--yes`, defaults are accepted non-interactively, including selecting existing discovered repositories, enabling every discovered provider, creating strong and cheap agents for each one, and creating or updating labels on selected repositories. `--yes` does not create a private demo repository.
|
|
126
126
|
|
|
127
|
-
Repository setup first asks whether to
|
|
127
|
+
Repository setup first asks whether to create a private Backburner demo repository, select existing repositories, or skip repository setup for now. When GitHub authentication is available, the private demo repository is the interactive default. Before choosing existing repositories, onboarding tells you that Backburner will create or update its labels on those repositories. In an interactive terminal, repository and provider selection use a checkbox picker: arrow keys move, space toggles, and enter confirms. Non-TTY sessions fall back to text input and accept comma-separated numbers. If you are authenticated with GitHub but do not want to onboard an existing local clone yet, onboarding can explicitly create a private `backburner-demo` repository and clone it under the configured code root.
|
|
128
128
|
|
|
129
129
|
Label creation is best-effort. If GitHub rejects a label write because of permissions or repository policy, onboarding continues and prints the repository labels URL plus the exact label names to create manually. The final summary prints the next `backburner run` command, a prefilled GitHub issue URL for the first configured repository, and a terminal QR code for opening that repository's issues page from a phone.
|
|
130
130
|
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -192,7 +192,9 @@ export class CodexCliAgent {
|
|
|
192
192
|
: {})
|
|
193
193
|
};
|
|
194
194
|
const parsedThreadId = extractThreadIdFromCodexOutput(stdout);
|
|
195
|
-
const providerLimitSignal =
|
|
195
|
+
const providerLimitSignal = exitCode === 0
|
|
196
|
+
? undefined
|
|
197
|
+
: detectProviderLimit("codex", exitCode, extractCodexProviderErrorText(stdout), stderr);
|
|
196
198
|
return {
|
|
197
199
|
taskStatus: execution.taskStatus,
|
|
198
200
|
execution,
|
|
@@ -259,3 +261,61 @@ function extractCodexStreamJsonAssistantText(streamText) {
|
|
|
259
261
|
}
|
|
260
262
|
return undefined;
|
|
261
263
|
}
|
|
264
|
+
function extractCodexProviderErrorText(streamText) {
|
|
265
|
+
if (!streamText.trim()) {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
const extractedErrors = [];
|
|
269
|
+
let sawJsonLine = false;
|
|
270
|
+
for (const line of streamText.split(/\r?\n/)) {
|
|
271
|
+
const trimmed = line.trim();
|
|
272
|
+
if (!trimmed) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const event = JSON.parse(trimmed);
|
|
277
|
+
sawJsonLine = true;
|
|
278
|
+
collectCodexErrorStrings(event, extractedErrors);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Non-JSON Codex output is a direct process surface and can be scanned.
|
|
282
|
+
extractedErrors.push(trimmed);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return sawJsonLine ? extractedErrors.join("\n") : streamText;
|
|
286
|
+
}
|
|
287
|
+
function collectCodexErrorStrings(value, out) {
|
|
288
|
+
if (!value || typeof value !== "object") {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const record = value;
|
|
292
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
293
|
+
const isErrorEvent = type.toLowerCase().includes("error");
|
|
294
|
+
if (isErrorEvent) {
|
|
295
|
+
for (const key of ["message", "error", "details"]) {
|
|
296
|
+
const text = stringifyCodexErrorField(record[key]);
|
|
297
|
+
if (text) {
|
|
298
|
+
out.push(text);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const errorText = stringifyCodexErrorField(record.error);
|
|
303
|
+
if (errorText && (isErrorEvent || record.status === "failed")) {
|
|
304
|
+
out.push(errorText);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function stringifyCodexErrorField(value) {
|
|
308
|
+
if (typeof value === "string") {
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
if (value && typeof value === "object") {
|
|
312
|
+
const record = value;
|
|
313
|
+
if (typeof record.message === "string") {
|
|
314
|
+
return record.message;
|
|
315
|
+
}
|
|
316
|
+
if (typeof record.code === "string") {
|
|
317
|
+
return record.code;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
@@ -310,11 +310,12 @@ export async function dispatchPendingTasks(input) {
|
|
|
310
310
|
let providerLimitQueueProviders;
|
|
311
311
|
let providerLimitResetTime;
|
|
312
312
|
let swappedAfterProviderLimit = false;
|
|
313
|
-
|
|
313
|
+
const actionableProviderLimitSignal = result.taskStatus === "completed" ? undefined : result.providerLimitSignal;
|
|
314
|
+
if (actionableProviderLimitSignal) {
|
|
314
315
|
try {
|
|
315
316
|
const limitState = await input.stateStore.load();
|
|
316
317
|
const providerLimits = [...(limitState.providerLimits ?? [])];
|
|
317
|
-
const signal =
|
|
318
|
+
const signal = actionableProviderLimitSignal;
|
|
318
319
|
const existingIdx = providerLimits.findIndex(l => l.provider === signal.provider);
|
|
319
320
|
const newRecord = mapSignalToRecord(signal, now());
|
|
320
321
|
if (existingIdx !== -1) {
|
|
@@ -362,15 +363,15 @@ export async function dispatchPendingTasks(input) {
|
|
|
362
363
|
}
|
|
363
364
|
catch (err) {
|
|
364
365
|
input.logger?.warn(`[dispatch] Failed to persist provider limit updates: ${err}`);
|
|
365
|
-
providerLimitQueueProviders =
|
|
366
|
-
providerLimitResetTime =
|
|
366
|
+
providerLimitQueueProviders = actionableProviderLimitSignal.provider;
|
|
367
|
+
providerLimitResetTime = actionableProviderLimitSignal.resetTime;
|
|
367
368
|
}
|
|
368
369
|
}
|
|
369
|
-
const isProviderLimit = !!
|
|
370
|
+
const isProviderLimit = !!actionableProviderLimitSignal;
|
|
370
371
|
const shouldQueueForProviderLimit = isProviderLimit && !swappedAfterProviderLimit;
|
|
371
372
|
const finalStatus = shouldQueueForProviderLimit ? "queued" : swappedAfterProviderLimit ? "pending" : result.taskStatus;
|
|
372
373
|
if (shouldQueueForProviderLimit && !preparingTask.context.notifiedQueued) {
|
|
373
|
-
await safelyNotifyQueued(input.lifecycleNotifier, preparingTask, providerLimitQueueProviders ??
|
|
374
|
+
await safelyNotifyQueued(input.lifecycleNotifier, preparingTask, providerLimitQueueProviders ?? actionableProviderLimitSignal.provider, providerLimitResetTime, input.logger);
|
|
374
375
|
}
|
|
375
376
|
tasks[index] = {
|
|
376
377
|
...preparingTask,
|
|
@@ -395,12 +396,12 @@ export async function dispatchPendingTasks(input) {
|
|
|
395
396
|
}
|
|
396
397
|
: {})
|
|
397
398
|
},
|
|
398
|
-
...(shouldQueueForProviderLimit ? { waitingOnProvider:
|
|
399
|
+
...(shouldQueueForProviderLimit ? { waitingOnProvider: actionableProviderLimitSignal.provider } : { waitingOnProvider: undefined })
|
|
399
400
|
};
|
|
400
401
|
await input.stateStore.writeExecutionState(executions);
|
|
401
402
|
await input.stateStore.writeTaskState(tasks);
|
|
402
403
|
if (shouldQueueForProviderLimit) {
|
|
403
|
-
console.log(`🤖 Task ${preparingTask.id} has been queued (waiting on provider ${
|
|
404
|
+
console.log(`🤖 Task ${preparingTask.id} has been queued (waiting on provider ${actionableProviderLimitSignal.provider} due to rate limit).`);
|
|
404
405
|
}
|
|
405
406
|
if (assignmentResolution.mode === "pinned") {
|
|
406
407
|
try {
|
package/dist/src/cli/init.js
CHANGED
|
@@ -58,7 +58,7 @@ export async function runInit(options) {
|
|
|
58
58
|
new CheckRequirementsStep(commandRunner),
|
|
59
59
|
new CheckGitHubAuthStep(commandRunner),
|
|
60
60
|
new DiscoverReposStep(commandRunner, ctx.codeRoot),
|
|
61
|
-
new SelectReposStep(new DemoRepoCreator(commandRunner)),
|
|
61
|
+
new SelectReposStep(new DemoRepoCreator(commandRunner), nonInteractive ? "existing" : "demo"),
|
|
62
62
|
new ConfigurePathsStep(),
|
|
63
63
|
new DiscoverProvidersStep(commandRunner),
|
|
64
64
|
new ConfigureModelsStep(),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { defaultDemoRepoName } from "../services/demo-repo.js";
|
|
2
2
|
export class SelectReposStep {
|
|
3
3
|
demoRepoCreator;
|
|
4
|
+
defaultSetupChoice;
|
|
4
5
|
name = "select-repos";
|
|
5
6
|
description = "Select repositories to manage";
|
|
6
|
-
constructor(demoRepoCreator) {
|
|
7
|
+
constructor(demoRepoCreator, defaultSetupChoice = "demo") {
|
|
7
8
|
this.demoRepoCreator = demoRepoCreator;
|
|
9
|
+
this.defaultSetupChoice = defaultSetupChoice;
|
|
8
10
|
}
|
|
9
11
|
async run(ctx, prompter) {
|
|
10
12
|
const { discoveredRepos } = ctx;
|
|
@@ -55,36 +57,52 @@ export class SelectReposStep {
|
|
|
55
57
|
}
|
|
56
58
|
async promptSetupChoice(ctx, prompter) {
|
|
57
59
|
const hasDiscoveredRepos = ctx.discoveredRepos.length > 0;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"Skip repository setup for now"
|
|
64
|
-
|
|
65
|
-
const
|
|
60
|
+
const existingLabel = hasDiscoveredRepos
|
|
61
|
+
? "Select existing repositories (Backburner will create/update its labels on them)"
|
|
62
|
+
: "Enter an existing repository manually";
|
|
63
|
+
const hasDemoOption = ctx.githubUser !== undefined;
|
|
64
|
+
const options = hasDemoOption
|
|
65
|
+
? ["Create a private Backburner demo repo", existingLabel, "Skip repository setup for now"]
|
|
66
|
+
: [existingLabel, "Skip repository setup for now"];
|
|
67
|
+
const defaultIndex = hasDemoOption && this.defaultSetupChoice === "existing" ? 1 : 0;
|
|
68
|
+
const choice = await prompter.choose("How do you want to set up repositories?", options, defaultIndex);
|
|
69
|
+
if (hasDemoOption) {
|
|
70
|
+
if (choice === 0) {
|
|
71
|
+
return "demo";
|
|
72
|
+
}
|
|
73
|
+
if (choice === 1) {
|
|
74
|
+
return "existing";
|
|
75
|
+
}
|
|
76
|
+
return "skip";
|
|
77
|
+
}
|
|
66
78
|
if (choice === 0) {
|
|
67
79
|
return "existing";
|
|
68
80
|
}
|
|
69
|
-
if (ctx.githubUser !== undefined && choice === 1) {
|
|
70
|
-
return "demo";
|
|
71
|
-
}
|
|
72
81
|
return "skip";
|
|
73
82
|
}
|
|
74
83
|
async promptNoSelection(ctx, prompter) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
const hasDemoOption = ctx.githubUser !== undefined;
|
|
85
|
+
const options = hasDemoOption
|
|
86
|
+
? [
|
|
87
|
+
"Create a private Backburner demo repo",
|
|
88
|
+
"Add an existing repository manually",
|
|
89
|
+
"Skip repository setup for now"
|
|
90
|
+
]
|
|
91
|
+
: ["Add an existing repository manually", "Skip repository setup for now"];
|
|
80
92
|
const defaultIndex = 0;
|
|
81
93
|
const choice = await prompter.choose("No repositories selected. How do you want to continue?", options, defaultIndex);
|
|
94
|
+
if (hasDemoOption) {
|
|
95
|
+
if (choice === 0) {
|
|
96
|
+
return await this.promptCreateDemoRepo(ctx, prompter);
|
|
97
|
+
}
|
|
98
|
+
if (choice === 1) {
|
|
99
|
+
return await this.promptManualEntry(ctx, prompter);
|
|
100
|
+
}
|
|
101
|
+
return this.noRepositoryWarning();
|
|
102
|
+
}
|
|
82
103
|
if (choice === 0) {
|
|
83
104
|
return await this.promptManualEntry(ctx, prompter);
|
|
84
105
|
}
|
|
85
|
-
if (ctx.githubUser !== undefined && choice === 1) {
|
|
86
|
-
return await this.promptCreateDemoRepo(ctx, prompter);
|
|
87
|
-
}
|
|
88
106
|
return this.noRepositoryWarning();
|
|
89
107
|
}
|
|
90
108
|
async promptCreateDemoRepo(ctx, prompter) {
|
|
@@ -106,7 +124,7 @@ export class SelectReposStep {
|
|
|
106
124
|
details: ["Repository names can contain letters, numbers, dots, underscores, and hyphens."]
|
|
107
125
|
};
|
|
108
126
|
}
|
|
109
|
-
const confirmed = await prompter.confirm(`Create private GitHub repository ${ctx.githubUser}/${repoName} and clone it under ${ctx.codeRoot}?`,
|
|
127
|
+
const confirmed = await prompter.confirm(`Create private GitHub repository ${ctx.githubUser}/${repoName} and clone it under ${ctx.codeRoot}?`, true);
|
|
110
128
|
if (!confirmed) {
|
|
111
129
|
return this.noRepositoryWarning();
|
|
112
130
|
}
|
|
@@ -11,7 +11,7 @@ export const planBreakdownHandler = {
|
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
13
|
function derivePlanBreakdownTask(context, env) {
|
|
14
|
-
const { workstream, attention, issue, activePullRequest, repoConfig, repoSyncRecord } = context;
|
|
14
|
+
const { workstream, attention, issue, activePullRequest, activeWorktree, repoConfig, repoSyncRecord } = context;
|
|
15
15
|
if (hasGeneratedPlanBreakdown(workstream)) {
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
@@ -23,6 +23,13 @@ function derivePlanBreakdownTask(context, env) {
|
|
|
23
23
|
if (!source) {
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
+
const prWorktree = source.kind === "pull_request" &&
|
|
27
|
+
activeWorktree &&
|
|
28
|
+
activePullRequest &&
|
|
29
|
+
activePullRequest.id === source.id &&
|
|
30
|
+
activeWorktree.branchName === activePullRequest.headBranch
|
|
31
|
+
? activeWorktree
|
|
32
|
+
: undefined;
|
|
26
33
|
return buildTaskRecord(buildTaskId("plan_breakdown", source.id), "plan_breakdown", source, repoSyncRecord, {
|
|
27
34
|
reason: `The "${repoConfig.labels.planBreakdownNeeded}" label is present, requesting a technical breakdown before implementation.`,
|
|
28
35
|
summary: `Break down implementation for ${source.kind.replace("_", " ")} #${source.number}: ${source.title}`,
|
|
@@ -40,6 +47,16 @@ function derivePlanBreakdownTask(context, env) {
|
|
|
40
47
|
issueBody: source.body
|
|
41
48
|
}
|
|
42
49
|
: {}),
|
|
43
|
-
...(
|
|
50
|
+
...(prWorktree
|
|
51
|
+
? {
|
|
52
|
+
localRepoPath: prWorktree.path,
|
|
53
|
+
branchName: prWorktree.branchName,
|
|
54
|
+
planDocumentPath: prWorktree.planDocumentPath,
|
|
55
|
+
...(prWorktree.productSpecDocumentPath ? { productSpecDocumentPath: prWorktree.productSpecDocumentPath } : {}),
|
|
56
|
+
logDocumentPath: prWorktree.logDocumentPath
|
|
57
|
+
}
|
|
58
|
+
: repoSyncRecord?.localPath
|
|
59
|
+
? { localRepoPath: repoSyncRecord.localPath }
|
|
60
|
+
: {})
|
|
44
61
|
}), workstream.id);
|
|
45
62
|
}
|