@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 select existing repositories, create a private Backburner demo repository, or skip repository setup for now. 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.
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
 
@@ -192,7 +192,9 @@ export class CodexCliAgent {
192
192
  : {})
193
193
  };
194
194
  const parsedThreadId = extractThreadIdFromCodexOutput(stdout);
195
- const providerLimitSignal = detectProviderLimit("codex", exitCode, stdout, stderr);
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
- if (result.providerLimitSignal) {
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 = result.providerLimitSignal;
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 = result.providerLimitSignal.provider;
366
- providerLimitResetTime = result.providerLimitSignal.resetTime;
366
+ providerLimitQueueProviders = actionableProviderLimitSignal.provider;
367
+ providerLimitResetTime = actionableProviderLimitSignal.resetTime;
367
368
  }
368
369
  }
369
- const isProviderLimit = !!result.providerLimitSignal;
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 ?? result.providerLimitSignal.provider, providerLimitResetTime, input.logger);
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: result.providerLimitSignal.provider } : { waitingOnProvider: undefined })
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 ${result.providerLimitSignal.provider} due to rate limit).`);
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 {
@@ -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 options = [
59
- hasDiscoveredRepos
60
- ? "Select existing repositories (Backburner will create/update its labels on them)"
61
- : "Enter an existing repository manually",
62
- ...(ctx.githubUser !== undefined ? ["Create a private Backburner demo repo"] : []),
63
- "Skip repository setup for now"
64
- ];
65
- const choice = await prompter.choose("How do you want to set up repositories?", options, 0);
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 options = [
76
- "Add an existing repository manually",
77
- ...(ctx.githubUser !== undefined ? ["Create a private Backburner demo repo"] : []),
78
- "Skip repository setup for now"
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}?`, false);
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
- ...(repoSyncRecord?.localPath ? { localRepoPath: repoSyncRecord.localPath } : {})
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backburner/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Local GitHub-based agent orchestration CLI",
6
6
  "license": "MIT",