@backburner/cli 0.1.1 → 0.1.3

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Backburner is a local GitHub-driven agent orchestration CLI. It uses GitHub labels, issues, and pull requests as the control plane, keeps configuration and state as readable JSON on your machine, and dispatches eligible work to provider CLIs that you already have installed locally.
4
4
 
5
- Backburner currently supports:
5
+ Backburner supports:
6
6
 
7
7
  - `backburner init` for interactive or non-interactive local setup
8
8
  - `backburner run` for one orchestration cycle
@@ -75,7 +75,7 @@ npm run install:local -- --prefix "$env:TEMP\backburner-local"
75
75
  & "$env:TEMP\backburner-local\backburner.cmd" journal --json
76
76
  ```
77
77
 
78
- Once the current package is published:
78
+ Install from npm:
79
79
 
80
80
  ```sh
81
81
  npm install -g @backburner/cli
@@ -117,14 +117,16 @@ Useful `init` flags:
117
117
  --yes, -y Accept defaults without prompts
118
118
  ```
119
119
 
120
- `backburner init` checks local requirements, confirms `gh auth status`, discovers repositories, detects provider CLIs, attempts model-list discovery when supported by those CLIs, and writes these files into the management directory:
120
+ `backburner init` checks local requirements, confirms `gh auth status`, discovers repositories, detects provider CLIs, attempts model-list discovery when supported by those CLIs, writes configuration, and creates the configured Backburner labels on each selected repository.
121
121
 
122
122
  - `repos.json`
123
123
  - `agents.json`
124
124
 
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, and creating strong and cheap agents for each one. `--yes` does not create a private demo repository.
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. 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
+
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.
128
130
 
129
131
  ## Default Directories
130
132
 
@@ -160,13 +162,13 @@ Minimal `repos.json`:
160
162
  "localPath": "your-github-user/example-repo",
161
163
  "enabled": true,
162
164
  "labels": {
163
- "agentInProgress": "agent-in-progress",
164
- "agentProductApproved": "agent-product-approved",
165
- "agentPlanApproved": "agent-plan-approved",
166
- "agentReview": "agent-review",
167
- "planBreakdownNeeded": "plan-breakdown-needed",
168
- "agentSyncParentBranch": "agent-sync-parent-branch",
169
- "prepareForMerge": "prepare-for-merge"
165
+ "agentInProgress": "backburner:in-progress",
166
+ "agentProductApproved": "backburner:product-approved",
167
+ "agentPlanApproved": "backburner:plan-approved",
168
+ "agentReview": "backburner:review",
169
+ "planBreakdownNeeded": "backburner:plan-breakdown-needed",
170
+ "agentSyncParentBranch": "backburner:sync-parent-branch",
171
+ "prepareForMerge": "backburner:prepare-for-merge"
170
172
  }
171
173
  }
172
174
  ]
@@ -215,14 +217,14 @@ Agent selection filters enabled agents by required role and `modelClass`, then b
215
217
 
216
218
  ## Label Control Plane
217
219
 
218
- Configure labels per repository in `repos.json`. The common defaults are:
220
+ Configure labels per repository in `repos.json`. The workflow labels are:
219
221
 
220
- - `agent-product-approved`: approves product discovery and allows Backburner to create the implementation PR/worktree flow for an issue
221
- - `agent-plan-approved`: approves an implementation plan and allows implementation tasks to run
222
- - `agent-review`: requests an explicit code review; review tasks are dispatched when the PR has this label and its head SHA changed since the last review
223
- - `agent-sync-parent-branch`: authorizes Backburner to sync a blocked parent implementation branch from the repository default branch
222
+ - `agentProductApproved`: approves product discovery and allows Backburner to create the implementation PR/worktree flow for an issue
223
+ - `agentPlanApproved`: approves an implementation plan and allows implementation tasks to run
224
+ - `agentReview`: requests an explicit code review; review tasks are dispatched when the PR has this label and its head SHA changed since the last review
225
+ - `agentSyncParentBranch`: authorizes Backburner to sync a blocked parent implementation branch from the repository default branch
224
226
 
225
- Backburner also uses `agent-in-progress`, `plan-breakdown-needed`, and `prepare-for-merge` when those labels are configured.
227
+ Backburner also uses `agentInProgress`, `planBreakdownNeeded`, and `prepareForMerge` when those labels are configured. Onboarding writes `repos.json` with these label values by default: `backburner:product-approved`, `backburner:plan-approved`, `backburner:review`, `backburner:in-progress`, `backburner:plan-breakdown-needed`, `backburner:sync-parent-branch`, and `backburner:prepare-for-merge`. It creates or updates the configured labels through `gh label create --force`. Custom label names, including older unprefixed names, are supported when configured in `repos.json`.
226
228
 
227
229
  ## Commands
228
230
 
@@ -315,7 +317,7 @@ Generated files are readable JSON. Control-plane state is written under `<output
315
317
 
316
318
  ## Platform Support
317
319
 
318
- Backburner is intended to run on macOS, Linux, and Windows with Node.js 20 or newer. The current CI workflow in this checkout runs on Ubuntu, macOS, and Windows with Node 20 and 22, and includes build, typecheck, tests, package dry-run, and local package install smoke checks.
320
+ Backburner is intended to run on macOS, Linux, and Windows with Node.js 20 or newer. The CI workflow runs on Ubuntu, macOS, and Windows with Node 20 and 22, and includes build, typecheck, tests, package dry-run, and local package install smoke checks.
319
321
 
320
322
  ## Verification
321
323
 
@@ -1,4 +1,3 @@
1
- import * as path from "node:path";
2
1
  import { ExecFileCommandRunner } from "../../utils/command-runner.js";
3
2
  import { loadConfig } from "../../config/loader.js";
4
3
  import { GhCliGitHubGateway } from "../../github/gateway.js";
@@ -9,11 +8,11 @@ import { PlanBreakdownSessionStore } from "../../workstreams/plan-breakdown-gene
9
8
  import { PlanBreakdownToolService } from "../../workstreams/plan-breakdown-generator/tools.js";
10
9
  import { WaveAssessmentSessionStore } from "../../workstreams/wave-assessment/store.js";
11
10
  import { WaveAssessmentToolService } from "../../workstreams/wave-assessment/tools.js";
12
- import { defaultBackburnerManagementRoot } from "../../utils/paths.js";
11
+ import { defaultBackburnerManagementRoot, resolveUserPath } from "../../utils/paths.js";
13
12
  import { defaultStderrLogger } from "../../utils/logger.js";
14
13
  import { DEFAULT_GITHUB_BROKER_PORT, readOptionalNumberFlag, readOptionalStringFlag, readStringFlag } from "../options.js";
15
14
  export async function runBrokerSmokeCli(argv, logger = defaultStderrLogger) {
16
- const managementDir = path.resolve(readStringFlag(argv, "--management-dir", defaultBackburnerManagementRoot()));
15
+ const managementDir = resolveUserPath(readStringFlag(argv, "--management-dir", defaultBackburnerManagementRoot()));
17
16
  const host = "127.0.0.1";
18
17
  const config = await loadConfig(managementDir);
19
18
  const commandRunner = new ExecFileCommandRunner();
@@ -1,4 +1,3 @@
1
- import * as path from "node:path";
2
1
  import { ExecFileCommandRunner } from "../../utils/command-runner.js";
3
2
  import { loadConfig } from "../../config/loader.js";
4
3
  import { GhCliGitHubGateway } from "../../github/gateway.js";
@@ -9,11 +8,11 @@ import { PlanBreakdownSessionStore } from "../../workstreams/plan-breakdown-gene
9
8
  import { PlanBreakdownToolService } from "../../workstreams/plan-breakdown-generator/tools.js";
10
9
  import { WaveAssessmentSessionStore } from "../../workstreams/wave-assessment/store.js";
11
10
  import { WaveAssessmentToolService } from "../../workstreams/wave-assessment/tools.js";
12
- import { defaultBackburnerManagementRoot } from "../../utils/paths.js";
11
+ import { defaultBackburnerManagementRoot, resolveUserPath } from "../../utils/paths.js";
13
12
  import { defaultStderrLogger } from "../../utils/logger.js";
14
13
  import { DEFAULT_GITHUB_BROKER_PORT, readNumberFlag, readStringFlag } from "../options.js";
15
14
  export async function runBrokerCli(argv, logger = defaultStderrLogger) {
16
- const managementDir = path.resolve(readStringFlag(argv, "--management-dir", defaultBackburnerManagementRoot()));
15
+ const managementDir = resolveUserPath(readStringFlag(argv, "--management-dir", defaultBackburnerManagementRoot()));
17
16
  const port = readNumberFlag(argv, "--port", DEFAULT_GITHUB_BROKER_PORT);
18
17
  const host = readStringFlag(argv, "--host", "127.0.0.1");
19
18
  const config = await loadConfig(managementDir);
@@ -1,11 +1,10 @@
1
- import * as path from "node:path";
2
1
  import { JournalReader } from "../../journal/index.js";
3
2
  import { renderJournalEntries } from "../../journal/renderer.js";
4
- import { defaultBackburnerOutputsRoot, resolveOutputsJournalRoot } from "../../utils/paths.js";
3
+ import { defaultBackburnerOutputsRoot, resolveOutputsJournalRoot, resolveUserPath } from "../../utils/paths.js";
5
4
  import { CliUsageError } from "../errors.js";
6
5
  import { readOptionalStringFlag, readStringFlag } from "../options.js";
7
6
  export async function runJournalCli(argv, injectedJournal) {
8
- const outputsDir = path.resolve(readStringFlag(argv, "--outputs-dir", defaultBackburnerOutputsRoot()));
7
+ const outputsDir = resolveUserPath(readStringFlag(argv, "--outputs-dir", defaultBackburnerOutputsRoot()));
9
8
  const journalDir = resolveOutputsJournalRoot(outputsDir);
10
9
  const reader = injectedJournal ? injectedJournal.reader : new JournalReader(journalDir);
11
10
  const repoId = readOptionalStringFlag(argv, "--repo");
@@ -1,7 +1,6 @@
1
- import * as path from "node:path";
2
1
  import { loadConfig } from "../../config/loader.js";
3
2
  import { StateStore } from "../../state/loader.js";
4
- import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, resolveOutputsLogsRoot, resolveOutputsStateRoot } from "../../utils/paths.js";
3
+ import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, resolveOutputsLogsRoot, resolveOutputsStateRoot, resolveUserPath } from "../../utils/paths.js";
5
4
  import { RuntimeStatusTracker, TuiScreen } from "../tui/index.js";
6
5
  import { readIntervalMs, readOptionalStringFlag, readStringFlag } from "../options.js";
7
6
  import { runCli } from "./run.js";
@@ -73,7 +72,7 @@ export class SerializedCycleRunner {
73
72
  }
74
73
  export async function runInteractiveTui(argv) {
75
74
  const rest = argv.filter((arg) => arg !== "run");
76
- const managementDir = path.resolve(readStringFlag(rest, "--management-dir", defaultBackburnerManagementRoot()));
75
+ const managementDir = resolveUserPath(readStringFlag(rest, "--management-dir", defaultBackburnerManagementRoot()));
77
76
  let repoCount = 0;
78
77
  let initialConfig = null;
79
78
  try {
@@ -91,9 +90,9 @@ export async function runInteractiveTui(argv) {
91
90
  tracker.setIntervalMs(intervalMs);
92
91
  // Load initial state for the explorer immediately on start
93
92
  try {
94
- const outputsDir = path.resolve(readOptionalStringFlag(rest, "--outputs-dir") ?? defaultBackburnerOutputsRoot());
95
- const stateDir = path.resolve(resolveOutputsStateRoot(outputsDir));
96
- const logDir = path.resolve(resolveOutputsLogsRoot(outputsDir));
93
+ const outputsDir = resolveUserPath(readOptionalStringFlag(rest, "--outputs-dir") ?? defaultBackburnerOutputsRoot());
94
+ const stateDir = resolveUserPath(resolveOutputsStateRoot(outputsDir));
95
+ const logDir = resolveUserPath(resolveOutputsLogsRoot(outputsDir));
97
96
  const stateStore = new StateStore({ stateDir, executionDir: logDir });
98
97
  const state = await stateStore.load();
99
98
  tracker.setState(state);
@@ -114,9 +113,9 @@ export async function runInteractiveTui(argv) {
114
113
  logger: silentLogger,
115
114
  cycleNo: currentCycleNo
116
115
  });
117
- const outputsDir = path.resolve(readOptionalStringFlag(rest, "--outputs-dir") ?? defaultBackburnerOutputsRoot());
118
- const stateDir = path.resolve(resolveOutputsStateRoot(outputsDir));
119
- const logDir = path.resolve(resolveOutputsLogsRoot(outputsDir));
116
+ const outputsDir = resolveUserPath(readOptionalStringFlag(rest, "--outputs-dir") ?? defaultBackburnerOutputsRoot());
117
+ const stateDir = resolveUserPath(resolveOutputsStateRoot(outputsDir));
118
+ const logDir = resolveUserPath(resolveOutputsLogsRoot(outputsDir));
120
119
  const stateStore = new StateStore({ stateDir, executionDir: logDir });
121
120
  const state = await stateStore.load();
122
121
  try {
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { ExecFileCommandRunner } from "../utils/command-runner.js";
3
- import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot } from "../utils/paths.js";
3
+ import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot, resolveUserPath } from "../utils/paths.js";
4
4
  import { OnboardingEngine } from "../onboarding/engine.js";
5
5
  import { NodeReadlinePrompter, NonInteractivePrompter } from "../onboarding/prompt.js";
6
6
  import { WelcomeStep } from "../onboarding/steps/welcome.js";
@@ -12,6 +12,7 @@ import { ConfigurePathsStep } from "../onboarding/steps/configure-paths.js";
12
12
  import { DiscoverProvidersStep } from "../onboarding/steps/discover-providers.js";
13
13
  import { ConfigureModelsStep } from "../onboarding/steps/configure-models.js";
14
14
  import { GenerateConfigStep } from "../onboarding/steps/generate-config.js";
15
+ import { ProvisionLabelsStep } from "../onboarding/steps/provision-labels.js";
15
16
  import { ReadinessValidationStep } from "../onboarding/steps/readiness-validation.js";
16
17
  import { ShowSummaryStep } from "../onboarding/steps/show-summary.js";
17
18
  import { DemoRepoCreator } from "../onboarding/services/demo-repo.js";
@@ -38,14 +39,14 @@ export async function runInitCli(argv) {
38
39
  export async function runInit(options) {
39
40
  const commandRunner = new ExecFileCommandRunner();
40
41
  const { codeRoot, nonInteractive = false } = options;
41
- const resolvedCodeRoot = path.resolve(codeRoot);
42
- const resolvedOutputsDir = path.resolve(options.outputsDir ?? defaultBackburnerOutputsRoot());
42
+ const resolvedCodeRoot = resolveUserPath(codeRoot);
43
+ const resolvedOutputsDir = resolveUserPath(options.outputsDir ?? defaultBackburnerOutputsRoot());
43
44
  const ctx = {
44
45
  codeRoot: resolvedCodeRoot,
45
- managementDir: path.resolve(options.managementDir ?? defaultBackburnerManagementRoot()),
46
+ managementDir: resolveUserPath(options.managementDir ?? defaultBackburnerManagementRoot()),
46
47
  outputsDir: resolvedOutputsDir,
47
- logDir: path.resolve(options.logDir ?? path.join(resolvedOutputsDir, "logs")),
48
- workspacesDir: path.resolve(options.workspacesDir ?? defaultWorkspacesRootForCodeRoot(resolvedCodeRoot)),
48
+ logDir: resolveUserPath(options.logDir ?? path.join(resolvedOutputsDir, "logs")),
49
+ workspacesDir: resolveUserPath(options.workspacesDir ?? defaultWorkspacesRootForCodeRoot(resolvedCodeRoot)),
49
50
  discoveredRepos: [],
50
51
  selectedRepos: [],
51
52
  discoveredProviders: [],
@@ -57,11 +58,12 @@ export async function runInit(options) {
57
58
  new CheckRequirementsStep(commandRunner),
58
59
  new CheckGitHubAuthStep(commandRunner),
59
60
  new DiscoverReposStep(commandRunner, ctx.codeRoot),
60
- new SelectReposStep(new DemoRepoCreator(commandRunner)),
61
+ new SelectReposStep(new DemoRepoCreator(commandRunner), nonInteractive ? "existing" : "demo"),
61
62
  new ConfigurePathsStep(),
62
63
  new DiscoverProvidersStep(commandRunner),
63
64
  new ConfigureModelsStep(),
64
65
  new GenerateConfigStep(),
66
+ new ProvisionLabelsStep(commandRunner),
65
67
  new ReadinessValidationStep(commandRunner),
66
68
  new ShowSummaryStep()
67
69
  ];
@@ -15,19 +15,19 @@ import { createSmitheryMcpResolverFromEnv, reconcileMcpStatus } from "../../mcps
15
15
  import { StateStore } from "../../state/loader.js";
16
16
  import { ExecFileCommandRunner } from "../../utils/command-runner.js";
17
17
  import { defaultStderrLogger } from "../../utils/logger.js";
18
- import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, resolveOutputsStateRoot, resolveOutputsLogsRoot, resolveOutputsJournalRoot } from "../../utils/paths.js";
18
+ import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, resolveOutputsStateRoot, resolveOutputsLogsRoot, resolveOutputsJournalRoot, resolveUserPath } from "../../utils/paths.js";
19
19
  import { TriageFollowUpService } from "../../workflows/triage.js";
20
20
  import { ImplementationWorkflowService } from "../../worktrees/service.js";
21
21
  import { WorkspaceRecoveryService } from "../../worktrees/workspace-recovery.js";
22
22
  import { DEFAULT_GITHUB_BROKER_PORT as CLI_DEFAULT_GITHUB_BROKER_PORT, readNumberFlag, readOptionalStringFlag, readStringFlag } from "../options.js";
23
23
  export async function createRunContext(input) {
24
24
  const { rest, options } = input;
25
- const managementDir = path.resolve(options.managementDir ?? readStringFlag(rest, "--management-dir", defaultBackburnerManagementRoot()));
25
+ const managementDir = resolveUserPath(options.managementDir ?? readStringFlag(rest, "--management-dir", defaultBackburnerManagementRoot()));
26
26
  const configuredOutputsDir = options.outputsDir ?? readOptionalStringFlag(rest, "--outputs-dir");
27
- const outputsDir = path.resolve(configuredOutputsDir ?? defaultBackburnerOutputsRoot());
27
+ const outputsDir = resolveUserPath(configuredOutputsDir ?? defaultBackburnerOutputsRoot());
28
28
  const useLegacyStateDirWithoutOutputs = options.stateDir !== undefined && configuredOutputsDir === undefined;
29
29
  const logger = options.logger ?? defaultStderrLogger;
30
- const codeRoot = path.resolve(readOptionalStringFlag(rest, "--code-root") ?? defaultBackburnerCodeRoot());
30
+ const codeRoot = resolveUserPath(readOptionalStringFlag(rest, "--code-root") ?? defaultBackburnerCodeRoot());
31
31
  const brokerPort = readNumberFlag(rest, "--broker-port", CLI_DEFAULT_GITHUB_BROKER_PORT);
32
32
  const commandRunner = options.commandRunner ?? new ExecFileCommandRunner();
33
33
  const gitGateway = options.gitGateway ?? new GitCliGateway(commandRunner);
@@ -48,9 +48,9 @@ export async function createRunContext(input) {
48
48
  logger.info(`Preparing outputs root ${outputsDir}`);
49
49
  outputsGitOpsPrepared = await gitOpsLifecycle.prepareOutputsRoot(outputsDir);
50
50
  }
51
- const stateDir = path.resolve(options.stateDir ?? resolveOutputsStateRoot(outputsDir));
51
+ const stateDir = resolveUserPath(options.stateDir ?? resolveOutputsStateRoot(outputsDir));
52
52
  const defaultLogDir = options.stateDir !== undefined ? stateDir : resolveOutputsLogsRoot(outputsDir);
53
- const logDir = path.resolve(options.logDir ?? readStringFlag(rest, "--log-dir", defaultLogDir));
53
+ const logDir = resolveUserPath(options.logDir ?? readStringFlag(rest, "--log-dir", defaultLogDir));
54
54
  let managementRepo;
55
55
  try {
56
56
  const remoteResult = await commandRunner.run("git", ["remote", "get-url", "origin"], { cwd: managementDir });
@@ -1,6 +1,15 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { writeJsonFileAtomic } from "../utils/json.js";
4
+ export const DEFAULT_BACKBURNER_LABELS = {
5
+ agentInProgress: "backburner:in-progress",
6
+ agentPlanApproved: "backburner:plan-approved",
7
+ agentProductApproved: "backburner:product-approved",
8
+ agentReview: "backburner:review",
9
+ planBreakdownNeeded: "backburner:plan-breakdown-needed",
10
+ agentSyncParentBranch: "backburner:sync-parent-branch",
11
+ prepareForMerge: "backburner:prepare-for-merge"
12
+ };
4
13
  export class ConfigWriter {
5
14
  async detectExisting(managementDir) {
6
15
  const reposFile = path.join(managementDir, "repos.json");
@@ -55,15 +64,7 @@ function buildRepoEntry(repo) {
55
64
  defaultBranch: repo.defaultBranch,
56
65
  localPath: repo.localPath,
57
66
  enabled: repo.enabled,
58
- labels: {
59
- agentInProgress: "agent-in-progress",
60
- agentPlanApproved: "agent-plan-approved",
61
- agentProductApproved: "agent-product-approved",
62
- agentReview: "agent-review",
63
- planBreakdownNeeded: "plan-breakdown-needed",
64
- agentSyncParentBranch: "agent-sync-parent-branch",
65
- prepareForMerge: "prepare-for-merge"
66
- }
67
+ labels: repo.labels
67
68
  };
68
69
  }
69
70
  function buildAgentsJson(draft) {
@@ -0,0 +1,39 @@
1
+ const LABEL_TIMEOUT_MS = 30_000;
2
+ export class GitHubLabelProvisioner {
3
+ commandRunner;
4
+ constructor(commandRunner) {
5
+ this.commandRunner = commandRunner;
6
+ }
7
+ async provisionLabels(targets) {
8
+ const results = [];
9
+ for (const target of targets) {
10
+ const repoSlug = `${target.owner}/${target.name}`;
11
+ for (const label of target.labels) {
12
+ try {
13
+ await this.commandRunner.run("gh", [
14
+ "label",
15
+ "create",
16
+ label.name,
17
+ "--repo",
18
+ repoSlug,
19
+ "--description",
20
+ label.description,
21
+ "--color",
22
+ label.color,
23
+ "--force"
24
+ ], { timeoutMs: LABEL_TIMEOUT_MS });
25
+ results.push({ repoSlug, labelName: label.name, status: "created_or_updated" });
26
+ }
27
+ catch (err) {
28
+ results.push({
29
+ repoSlug,
30
+ labelName: label.name,
31
+ status: "failed",
32
+ message: err instanceof Error ? err.message : String(err)
33
+ });
34
+ }
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+ }
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot } from "../../utils/paths.js";
2
+ import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot, resolveUserPath } from "../../utils/paths.js";
3
3
  export class ConfigurePathsStep {
4
4
  name = "configure-paths";
5
5
  description = "Configure directories";
@@ -15,10 +15,10 @@ export class ConfigurePathsStep {
15
15
  const outputsDir = await prompter.ask("Outputs directory (state, logs):", defaultOutputs);
16
16
  const logDir = await prompter.ask("Log directory:", ctx.logDir || path.join(outputsDir || defaultOutputs, "logs"));
17
17
  const workspacesDir = await prompter.ask("Local workspaces directory (where Backburner creates git worktrees):", defaultWorkspaces);
18
- ctx.managementDir = managementDir || defaultManagement;
19
- ctx.outputsDir = outputsDir || defaultOutputs;
20
- ctx.logDir = logDir || path.join(ctx.outputsDir, "logs");
21
- ctx.workspacesDir = workspacesDir || defaultWorkspaces;
18
+ ctx.managementDir = resolveUserPath(managementDir || defaultManagement);
19
+ ctx.outputsDir = resolveUserPath(outputsDir || defaultOutputs);
20
+ ctx.logDir = resolveUserPath(logDir || path.join(ctx.outputsDir, "logs"));
21
+ ctx.workspacesDir = resolveUserPath(workspacesDir || defaultWorkspaces);
22
22
  return {
23
23
  status: "passed",
24
24
  message: "Directories configured",
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { ConfigWriter } from "../config-writer.js";
3
+ import { DEFAULT_BACKBURNER_LABELS } from "../config-writer.js";
3
4
  export class GenerateConfigStep {
4
5
  name = "generate-config";
5
6
  description = "Generating configuration";
@@ -101,6 +102,7 @@ function buildRepoDraft(repo, codeRoot) {
101
102
  name: repo.name,
102
103
  localPath,
103
104
  defaultBranch: repo.defaultBranch || "main",
104
- enabled: true
105
+ enabled: true,
106
+ labels: { ...DEFAULT_BACKBURNER_LABELS }
105
107
  };
106
108
  }
@@ -0,0 +1,104 @@
1
+ import { GitHubLabelProvisioner } from "../services/label-provisioner.js";
2
+ const LABEL_DEFINITIONS = {
3
+ agentInProgress: {
4
+ description: "Backburner is actively working on this item.",
5
+ color: "6F42C1"
6
+ },
7
+ agentProductApproved: {
8
+ description: "Approve Backburner to start product discovery from this issue.",
9
+ color: "2DA44E"
10
+ },
11
+ agentPlanApproved: {
12
+ description: "Approve Backburner to implement an accepted plan.",
13
+ color: "0969DA"
14
+ },
15
+ agentReview: {
16
+ description: "Ask Backburner to review this pull request.",
17
+ color: "BF8700"
18
+ },
19
+ planBreakdownNeeded: {
20
+ description: "Ask Backburner to break a plan into implementation packets.",
21
+ color: "8250DF"
22
+ },
23
+ agentSyncParentBranch: {
24
+ description: "Allow Backburner to sync the parent branch from the default branch.",
25
+ color: "1F883D"
26
+ },
27
+ prepareForMerge: {
28
+ description: "Ask Backburner to prepare this pull request for merge.",
29
+ color: "CF222E"
30
+ }
31
+ };
32
+ export class ProvisionLabelsStep {
33
+ commandRunner;
34
+ name = "provision-labels";
35
+ description = "Creating GitHub labels";
36
+ constructor(commandRunner) {
37
+ this.commandRunner = commandRunner;
38
+ }
39
+ async run(ctx, _prompter) {
40
+ const draft = ctx.configDraft;
41
+ if (draft === undefined || draft.repos.length === 0) {
42
+ return {
43
+ status: "skipped",
44
+ message: "Skipped: no repositories selected for label creation"
45
+ };
46
+ }
47
+ const targets = buildLabelTargets(draft);
48
+ const provisioner = new GitHubLabelProvisioner(this.commandRunner);
49
+ const results = await provisioner.provisionLabels(targets);
50
+ const failures = results.filter((result) => result.status === "failed");
51
+ if (failures.length > 0) {
52
+ ctx.warnings.push("Some GitHub labels could not be created. You can retry onboarding or create them manually from repos.json.");
53
+ return {
54
+ status: "warning",
55
+ message: `Created labels with ${failures.length} failure${failures.length === 1 ? "" : "s"}`,
56
+ details: [
57
+ ...failures.map((failure) => {
58
+ const suffix = failure.message !== undefined ? `: ${failure.message}` : "";
59
+ return `${failure.repoSlug}: ${failure.labelName}${suffix}`;
60
+ }),
61
+ ...buildManualLabelDetails(targets)
62
+ ]
63
+ };
64
+ }
65
+ return {
66
+ status: "passed",
67
+ message: `Created or updated ${results.length} GitHub label${results.length === 1 ? "" : "s"}`,
68
+ details: targets.map((target) => `${target.owner}/${target.name}: ${target.labels.length} labels`)
69
+ };
70
+ }
71
+ }
72
+ function buildManualLabelDetails(targets) {
73
+ return targets.flatMap((target) => {
74
+ const repoSlug = `${target.owner}/${target.name}`;
75
+ return [
76
+ `Manual labels page for ${repoSlug}: https://github.com/${repoSlug}/labels`,
77
+ `Required labels: ${target.labels.map((label) => label.name).join(", ")}`
78
+ ];
79
+ });
80
+ }
81
+ function buildLabelTargets(draft) {
82
+ return draft.repos.map((repo) => ({
83
+ owner: repo.owner,
84
+ name: repo.name,
85
+ labels: buildRepoLabels(repo)
86
+ }));
87
+ }
88
+ function buildRepoLabels(repo) {
89
+ return [
90
+ buildLabel("agentInProgress", repo.labels.agentInProgress),
91
+ buildLabel("agentProductApproved", repo.labels.agentProductApproved),
92
+ buildLabel("agentPlanApproved", repo.labels.agentPlanApproved),
93
+ buildLabel("agentReview", repo.labels.agentReview),
94
+ buildLabel("planBreakdownNeeded", repo.labels.planBreakdownNeeded),
95
+ buildLabel("agentSyncParentBranch", repo.labels.agentSyncParentBranch),
96
+ buildLabel("prepareForMerge", repo.labels.prepareForMerge)
97
+ ];
98
+ }
99
+ function buildLabel(key, name) {
100
+ return {
101
+ name,
102
+ ...LABEL_DEFINITIONS[key]
103
+ };
104
+ }
@@ -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"
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
  }
@@ -1,6 +1,9 @@
1
1
  import { deriveOverallStatus } from "../engine.js";
2
2
  import * as path from "node:path";
3
+ import { createRequire } from "node:module";
3
4
  import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot } from "../../utils/paths.js";
5
+ const require = createRequire(import.meta.url);
6
+ const terminalQr = require("qrcode-terminal");
4
7
  export class ShowSummaryStep {
5
8
  name = "show-summary";
6
9
  description = "Summary";
@@ -62,13 +65,23 @@ export class ShowSummaryStep {
62
65
  }
63
66
  }
64
67
  if (succeeded) {
68
+ const issueTarget = buildFirstIssueTarget(ctx);
65
69
  lines.push("");
66
70
  lines.push("Next steps:");
67
- lines.push(" 1. Create the configured GitHub labels on each managed repository");
68
- lines.push(" 2. Start the orchestrator:");
71
+ lines.push(" 1. Start the orchestrator:");
69
72
  lines.push(` ${buildRunCommand(ctx)}`);
70
- lines.push(" 3. Open an issue on a managed repo and apply the 'agent-plan-approved' label");
71
- lines.push(" 4. Watch Backburner pick it up on the next orchestration cycle");
73
+ if (issueTarget !== undefined) {
74
+ lines.push(" 2. Open a Backburner issue:");
75
+ lines.push(` ${issueTarget.prefilledIssueUrl}`);
76
+ lines.push(" 3. Watch Backburner pick it up on the next orchestration cycle");
77
+ lines.push("");
78
+ lines.push("Repository issues QR:");
79
+ lines.push(renderQr(issueTarget.repositoryIssuesUrl));
80
+ }
81
+ else {
82
+ lines.push(" 2. Add a repository to repos.json");
83
+ lines.push(" 3. Open an issue on a managed repo with the configured product-approved label");
84
+ }
72
85
  }
73
86
  else {
74
87
  lines.push("");
@@ -79,6 +92,43 @@ export class ShowSummaryStep {
79
92
  return { status: "passed", message: "Summary displayed" };
80
93
  }
81
94
  }
95
+ function buildFirstIssueTarget(ctx) {
96
+ const repo = ctx.configDraft?.repos[0];
97
+ if (repo === undefined) {
98
+ return undefined;
99
+ }
100
+ const url = new URL(`https://github.com/${repo.owner}/${repo.name}/issues/new`);
101
+ url.searchParams.set("title", "Initialize a TypeScript React Native kitchen measurement converter");
102
+ url.searchParams.set("body", [
103
+ "Create a small TypeScript React Native application for converting common kitchen measurements.",
104
+ "",
105
+ "Goal:",
106
+ "- Let a cook enter a quantity, choose a source unit, choose a target unit, and see the converted value.",
107
+ "",
108
+ "Requirements:",
109
+ "- Use TypeScript.",
110
+ "- Support cups, tablespoons, teaspoons, milliliters, liters, fluid ounces, ounces, grams, and kilograms.",
111
+ "- Include a simple conversion screen with clear input validation and useful empty/error states.",
112
+ "- Keep the implementation small and easy to review.",
113
+ "",
114
+ "Acceptance criteria:",
115
+ "- The app can convert at least 1 cup to tablespoons, milliliters, and fluid ounces.",
116
+ "- Invalid or empty input does not crash the app.",
117
+ "- Add or update tests for the conversion logic where the project test setup supports it."
118
+ ].join("\n"));
119
+ url.searchParams.set("labels", repo.labels.agentProductApproved);
120
+ return {
121
+ prefilledIssueUrl: url.toString(),
122
+ repositoryIssuesUrl: `https://github.com/${repo.owner}/${repo.name}/issues`
123
+ };
124
+ }
125
+ function renderQr(input) {
126
+ let rendered = "";
127
+ terminalQr.generate(input, { small: true }, (qr) => {
128
+ rendered = qr.trimEnd();
129
+ });
130
+ return rendered;
131
+ }
82
132
  function buildRunCommand(ctx) {
83
133
  const args = ["backburner", "run"];
84
134
  if (ctx.codeRoot !== defaultBackburnerCodeRoot()) {
@@ -18,7 +18,8 @@ export class WelcomeStep {
18
18
  " 5. Detecting available AI providers",
19
19
  " 6. Assigning models to agent roles",
20
20
  " 7. Generating configuration files",
21
- " 8. Running a final readiness check",
21
+ " 8. Creating configured GitHub labels",
22
+ " 9. Running a final readiness check",
22
23
  ""
23
24
  ];
24
25
  process.stdout.write(lines.join("\n"));
@@ -18,9 +18,21 @@ export const outputsJournalRoot = path.join(outputsRoot, "journal");
18
18
  export function resolveOutputsJournalRoot(outputsDir) {
19
19
  return path.join(outputsDir, "journal");
20
20
  }
21
+ export function expandHomePath(input) {
22
+ if (input === "~") {
23
+ return os.homedir();
24
+ }
25
+ if (input.startsWith("~/") || input.startsWith("~\\")) {
26
+ return path.join(os.homedir(), input.slice(2));
27
+ }
28
+ return input;
29
+ }
30
+ export function resolveUserPath(input) {
31
+ return path.resolve(expandHomePath(input));
32
+ }
21
33
  export function defaultBackburnerRoot() {
22
34
  if (process.env.BACKBURNER_ROOT !== undefined && process.env.BACKBURNER_ROOT.trim() !== "") {
23
- return process.env.BACKBURNER_ROOT;
35
+ return resolveUserPath(process.env.BACKBURNER_ROOT);
24
36
  }
25
37
  return path.join(os.homedir(), ".backburner");
26
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backburner/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Local GitHub-based agent orchestration CLI",
6
6
  "license": "MIT",
@@ -55,6 +55,7 @@
55
55
  "dependencies": {
56
56
  "@smithery/api": "^0.67.0",
57
57
  "cross-spawn": "^7.0.6",
58
+ "qrcode-terminal": "^0.12.0",
58
59
  "xstate": "^5.31.0",
59
60
  "zod": "^4.4.3"
60
61
  }