@backburner/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- package/package.json +61 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { RepoDiscoveryService } from "../services/repo-discovery.js";
|
|
2
|
+
export class DiscoverReposStep {
|
|
3
|
+
commandRunner;
|
|
4
|
+
codeRoot;
|
|
5
|
+
name = "discover-repos";
|
|
6
|
+
description = "Discovering repositories";
|
|
7
|
+
constructor(commandRunner, codeRoot) {
|
|
8
|
+
this.commandRunner = commandRunner;
|
|
9
|
+
this.codeRoot = codeRoot;
|
|
10
|
+
}
|
|
11
|
+
async run(ctx, _prompter) {
|
|
12
|
+
const service = new RepoDiscoveryService(this.commandRunner);
|
|
13
|
+
const [githubRepos, localRepos] = await Promise.all([
|
|
14
|
+
service.discoverGitHubRepos(),
|
|
15
|
+
service.discoverLocalRepos([this.codeRoot])
|
|
16
|
+
]);
|
|
17
|
+
ctx.discoveredRepos = service.mergeRepos(githubRepos, localRepos);
|
|
18
|
+
const localCount = ctx.discoveredRepos.filter((r) => r.isLocalClone).length;
|
|
19
|
+
const remoteOnlyCount = ctx.discoveredRepos.length - localCount;
|
|
20
|
+
if (ctx.discoveredRepos.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
status: "warning",
|
|
23
|
+
message: "No repositories discovered",
|
|
24
|
+
details: [
|
|
25
|
+
"No GitHub repos found and no git repos found under the code root.",
|
|
26
|
+
"You can enter a repository manually in the next step."
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
status: "passed",
|
|
32
|
+
message: `Found ${ctx.discoveredRepos.length} repositories`,
|
|
33
|
+
details: [
|
|
34
|
+
`${localCount} with a local clone`,
|
|
35
|
+
`${remoteOnlyCount} on GitHub without a local clone`
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { ConfigWriter } from "../config-writer.js";
|
|
3
|
+
export class GenerateConfigStep {
|
|
4
|
+
name = "generate-config";
|
|
5
|
+
description = "Generating configuration";
|
|
6
|
+
async run(ctx, prompter) {
|
|
7
|
+
const writer = new ConfigWriter();
|
|
8
|
+
const existing = await writer.detectExisting(ctx.managementDir);
|
|
9
|
+
if (existing.reposExists || existing.agentsExists) {
|
|
10
|
+
const existingFiles = [];
|
|
11
|
+
if (existing.reposExists)
|
|
12
|
+
existingFiles.push(existing.reposFile);
|
|
13
|
+
if (existing.agentsExists)
|
|
14
|
+
existingFiles.push(existing.agentsFile);
|
|
15
|
+
process.stdout.write("\nExisting configuration found:\n");
|
|
16
|
+
for (const f of existingFiles) {
|
|
17
|
+
process.stdout.write(` ${f}\n`);
|
|
18
|
+
}
|
|
19
|
+
const overwrite = await prompter.confirm("Overwrite existing configuration?", false);
|
|
20
|
+
if (!overwrite) {
|
|
21
|
+
ctx.warnings.push("Configuration was not overwritten. Edit the files manually if needed.");
|
|
22
|
+
return {
|
|
23
|
+
status: "skipped",
|
|
24
|
+
message: "Existing configuration preserved",
|
|
25
|
+
details: existingFiles.map((f) => `Kept: ${f}`)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const draft = buildConfigDraft(ctx);
|
|
30
|
+
ctx.configDraft = draft;
|
|
31
|
+
try {
|
|
32
|
+
const result = await writer.write(draft, {
|
|
33
|
+
overwriteRepos: true,
|
|
34
|
+
overwriteAgents: true
|
|
35
|
+
});
|
|
36
|
+
const details = [];
|
|
37
|
+
if (!result.skippedExisting.includes(result.reposFile)) {
|
|
38
|
+
details.push(`Written: ${result.reposFile}`);
|
|
39
|
+
}
|
|
40
|
+
if (!result.skippedExisting.includes(result.agentsFile)) {
|
|
41
|
+
details.push(`Written: ${result.agentsFile}`);
|
|
42
|
+
}
|
|
43
|
+
for (const f of result.skippedExisting) {
|
|
44
|
+
details.push(`Skipped (already exists): ${f}`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
status: "passed",
|
|
48
|
+
message: "Configuration written",
|
|
49
|
+
details
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
status: "failed",
|
|
55
|
+
message: `Failed to write configuration: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
|
+
issues: [
|
|
57
|
+
{
|
|
58
|
+
title: "Configuration write failed",
|
|
59
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
60
|
+
suggestedFix: `Check that ${ctx.managementDir} is writable`,
|
|
61
|
+
canAutoRemediate: false,
|
|
62
|
+
manualSteps: [
|
|
63
|
+
`Create the management directory if it does not exist: "${ctx.managementDir}"`,
|
|
64
|
+
`Confirm your user can write to the management directory: "${ctx.managementDir}"`
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildConfigDraft(ctx) {
|
|
73
|
+
const repos = ctx.selectedRepos.map((repo) => buildRepoDraft(repo, ctx.codeRoot));
|
|
74
|
+
const existing = ctx.configDraft;
|
|
75
|
+
return {
|
|
76
|
+
codeRoot: ctx.codeRoot,
|
|
77
|
+
managementDir: ctx.managementDir,
|
|
78
|
+
outputsDir: ctx.outputsDir,
|
|
79
|
+
logDir: ctx.logDir,
|
|
80
|
+
workspacesDir: ctx.workspacesDir,
|
|
81
|
+
allowedUsers: existing?.allowedUsers ??
|
|
82
|
+
(ctx.githubUser !== undefined ? [ctx.githubUser] : []),
|
|
83
|
+
repos,
|
|
84
|
+
agents: existing?.agents ?? []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildRepoDraft(repo, codeRoot) {
|
|
88
|
+
let localPath;
|
|
89
|
+
if (repo.localPath !== undefined) {
|
|
90
|
+
localPath = path.relative(codeRoot, repo.localPath);
|
|
91
|
+
// Fall back to owner/name if the path escapes the code root
|
|
92
|
+
if (localPath.startsWith("..")) {
|
|
93
|
+
localPath = `${repo.owner}/${repo.name}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
localPath = `${repo.owner}/${repo.name}`;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
owner: repo.owner,
|
|
101
|
+
name: repo.name,
|
|
102
|
+
localPath,
|
|
103
|
+
defaultBranch: repo.defaultBranch || "main",
|
|
104
|
+
enabled: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { ToolChecker } from "../validators/tool-checker.js";
|
|
2
|
+
import { GitHubAuthChecker } from "../validators/github-auth.js";
|
|
3
|
+
import { GitAccessChecker } from "../validators/git-access.js";
|
|
4
|
+
import { WorkspaceChecker } from "../validators/workspace.js";
|
|
5
|
+
export class ReadinessValidationStep {
|
|
6
|
+
commandRunner;
|
|
7
|
+
platformHelper;
|
|
8
|
+
name = "readiness-validation";
|
|
9
|
+
description = "Running final readiness check";
|
|
10
|
+
constructor(commandRunner, platformHelper) {
|
|
11
|
+
this.commandRunner = commandRunner;
|
|
12
|
+
this.platformHelper = platformHelper;
|
|
13
|
+
}
|
|
14
|
+
async run(ctx, _prompter) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const details = [];
|
|
17
|
+
// 1. Tool check
|
|
18
|
+
const toolChecker = new ToolChecker(this.commandRunner, this.platformHelper);
|
|
19
|
+
const toolResults = await toolChecker.checkAll();
|
|
20
|
+
for (const r of toolResults) {
|
|
21
|
+
if (r.available) {
|
|
22
|
+
details.push(`✓ ${r.name}: ${r.version ?? "found"}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
details.push(`✗ ${r.name}: not found`);
|
|
26
|
+
if (r.issue !== undefined)
|
|
27
|
+
issues.push(r.issue);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 2. GitHub auth check
|
|
31
|
+
const authChecker = new GitHubAuthChecker(this.commandRunner);
|
|
32
|
+
const authResult = await authChecker.check();
|
|
33
|
+
if (authResult.authenticated) {
|
|
34
|
+
details.push(`✓ GitHub: authenticated as ${authResult.username ?? ctx.githubUser ?? "unknown"}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
details.push("✗ GitHub: not authenticated");
|
|
38
|
+
if (authResult.issue !== undefined)
|
|
39
|
+
issues.push(authResult.issue);
|
|
40
|
+
}
|
|
41
|
+
// 3. Git access for repos that have a local clone
|
|
42
|
+
const accessChecker = new GitAccessChecker(this.commandRunner);
|
|
43
|
+
const reposWithClone = ctx.selectedRepos.filter((r) => r.localPath !== undefined);
|
|
44
|
+
const skippedCount = ctx.selectedRepos.length - reposWithClone.length;
|
|
45
|
+
for (const repo of reposWithClone) {
|
|
46
|
+
const access = await accessChecker.check(repo.localPath);
|
|
47
|
+
if (access.canFetch && access.canPush) {
|
|
48
|
+
details.push(`✓ ${repo.owner}/${repo.name}: fetch and push OK`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (!access.canFetch) {
|
|
52
|
+
details.push(`✗ ${repo.owner}/${repo.name}: cannot fetch`);
|
|
53
|
+
if (access.fetchIssue !== undefined)
|
|
54
|
+
issues.push(access.fetchIssue);
|
|
55
|
+
}
|
|
56
|
+
if (!access.canPush) {
|
|
57
|
+
details.push(`⚠ ${repo.owner}/${repo.name}: cannot push (agents will not be able to commit)`);
|
|
58
|
+
if (access.pushIssue !== undefined)
|
|
59
|
+
issues.push(access.pushIssue);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (skippedCount > 0) {
|
|
64
|
+
details.push(`- ${skippedCount} repo${skippedCount === 1 ? "" : "s"} without a local clone — git access check skipped`);
|
|
65
|
+
}
|
|
66
|
+
// 4. Ensure configured directories exist and are writable
|
|
67
|
+
const wsChecker = new WorkspaceChecker(this.platformHelper);
|
|
68
|
+
const directoryPaths = [
|
|
69
|
+
ctx.managementDir,
|
|
70
|
+
ctx.outputsDir,
|
|
71
|
+
ctx.logDir,
|
|
72
|
+
ctx.workspacesDir
|
|
73
|
+
];
|
|
74
|
+
for (const dirPath of directoryPaths) {
|
|
75
|
+
const check = await wsChecker.check(dirPath);
|
|
76
|
+
if (check.writable) {
|
|
77
|
+
if (!check.exists) {
|
|
78
|
+
try {
|
|
79
|
+
await wsChecker.ensureDir(check.path);
|
|
80
|
+
details.push(`✓ ${check.path}: created`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
details.push(`✗ ${check.path}: could not be created`);
|
|
84
|
+
issues.push({
|
|
85
|
+
title: `Cannot create directory: ${check.path}`,
|
|
86
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
87
|
+
suggestedFix: "Choose a different directory or fix the permissions",
|
|
88
|
+
canAutoRemediate: false,
|
|
89
|
+
...(check.issue?.manualSteps !== undefined
|
|
90
|
+
? { manualSteps: check.issue.manualSteps }
|
|
91
|
+
: {})
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
details.push(`✓ ${check.path}: exists and writable`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
details.push(`✗ ${check.path}: not writable`);
|
|
101
|
+
if (check.issue !== undefined)
|
|
102
|
+
issues.push(check.issue);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 5. Providers
|
|
106
|
+
const available = ctx.discoveredProviders.filter((p) => p.available);
|
|
107
|
+
if (available.length === 0) {
|
|
108
|
+
details.push("✗ No AI providers available");
|
|
109
|
+
issues.push({
|
|
110
|
+
title: "No AI providers installed",
|
|
111
|
+
cause: "None of agy, gemini, claude, codex, or opencode were found on PATH",
|
|
112
|
+
suggestedFix: "Install at least one AI provider before running agents",
|
|
113
|
+
canAutoRemediate: false,
|
|
114
|
+
manualSteps: [
|
|
115
|
+
"Install antigravity: https://antigravity.google/cli",
|
|
116
|
+
"Install gemini CLI",
|
|
117
|
+
"Install claude: https://claude.ai/code",
|
|
118
|
+
"Install codex: https://openai.com/codex",
|
|
119
|
+
"Install opencode: https://opencode.ai"
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
details.push(`✓ AI providers: ${available.map((p) => p.name).join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
const hasStrongProductAgent = ctx.configDraft?.agents.some((agent) => agent.enabled && agent.modelClass === "strong" && agent.roles.includes("product")) ?? false;
|
|
127
|
+
if (hasStrongProductAgent) {
|
|
128
|
+
details.push("✓ Product role: enabled strong agent configured");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
details.push("✗ Product role: no enabled strong agent configured");
|
|
132
|
+
issues.push({
|
|
133
|
+
title: "No Product agent configured",
|
|
134
|
+
cause: "Product Discovery and Product Spec tasks require an enabled strong agent with the product role",
|
|
135
|
+
suggestedFix: "Add product to the roles for an enabled strong agent in agents.json",
|
|
136
|
+
canAutoRemediate: false,
|
|
137
|
+
manualSteps: [
|
|
138
|
+
'Edit management/agents.json and add "product" to a strong enabled agent roles array',
|
|
139
|
+
"Re-run backburner init or the readiness check"
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (issues.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
status: "passed",
|
|
146
|
+
message: "All readiness checks passed",
|
|
147
|
+
details
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const hasFatal = toolResults.some((r) => !r.available) || !authResult.authenticated;
|
|
151
|
+
return {
|
|
152
|
+
status: hasFatal ? "failed" : "warning",
|
|
153
|
+
message: `${issues.length} issue${issues.length === 1 ? "" : "s"} found`,
|
|
154
|
+
details,
|
|
155
|
+
issues
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class SelectReposStep {
|
|
2
|
+
name = "select-repos";
|
|
3
|
+
description = "Select repositories to manage";
|
|
4
|
+
async run(ctx, prompter) {
|
|
5
|
+
const { discoveredRepos } = ctx;
|
|
6
|
+
if (discoveredRepos.length === 0) {
|
|
7
|
+
return await this.promptManualEntry(ctx, prompter);
|
|
8
|
+
}
|
|
9
|
+
const byOwner = new Map();
|
|
10
|
+
for (const repo of discoveredRepos) {
|
|
11
|
+
const group = byOwner.get(repo.owner) ?? [];
|
|
12
|
+
group.push(repo);
|
|
13
|
+
byOwner.set(repo.owner, group);
|
|
14
|
+
}
|
|
15
|
+
const selected = [];
|
|
16
|
+
for (const [owner, repos] of byOwner) {
|
|
17
|
+
const options = repos.map((repo) => {
|
|
18
|
+
const note = repo.isLocalClone
|
|
19
|
+
? ` [local: ${repo.localPath ?? "?"}]`
|
|
20
|
+
: " [GitHub only]";
|
|
21
|
+
return `${repo.name}${note}`;
|
|
22
|
+
});
|
|
23
|
+
const preSelected = repos.flatMap((repo, i) => (repo.isLocalClone ? [i] : []));
|
|
24
|
+
const selectedIndices = await prompter.multiSelect(`Select repositories from ${owner}/:`, options, preSelected);
|
|
25
|
+
for (const i of selectedIndices) {
|
|
26
|
+
const repo = repos[i];
|
|
27
|
+
if (repo !== undefined)
|
|
28
|
+
selected.push(repo);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (selected.length === 0) {
|
|
32
|
+
const fallback = await prompter.confirm("No repositories selected. Add one manually?", true);
|
|
33
|
+
if (fallback) {
|
|
34
|
+
return await this.promptManualEntry(ctx, prompter);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
status: "warning",
|
|
38
|
+
message: "No repositories selected",
|
|
39
|
+
details: ["You can add repositories by editing repos.json later."]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
ctx.selectedRepos = selected;
|
|
43
|
+
return {
|
|
44
|
+
status: "passed",
|
|
45
|
+
message: `${ctx.selectedRepos.length} repositor${ctx.selectedRepos.length === 1 ? "y" : "ies"} selected`,
|
|
46
|
+
details: ctx.selectedRepos.map((r) => {
|
|
47
|
+
const note = r.localPath !== undefined ? ` (${r.localPath})` : " (no local clone)";
|
|
48
|
+
return `${r.owner}/${r.name}${note}`;
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async promptManualEntry(ctx, prompter) {
|
|
53
|
+
const owner = (await prompter.ask("Repository owner (e.g. myorg):")).trim();
|
|
54
|
+
const name = (await prompter.ask("Repository name (e.g. my-repo):")).trim();
|
|
55
|
+
if (owner.length === 0 || name.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
status: "warning",
|
|
58
|
+
message: "No repository entered",
|
|
59
|
+
details: ["You can add repositories by editing repos.json later."]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const localPath = (await prompter.ask(`Local path to the clone (leave empty if not cloned):`)).trim();
|
|
63
|
+
const repo = {
|
|
64
|
+
owner,
|
|
65
|
+
name,
|
|
66
|
+
isLocalClone: localPath.length > 0,
|
|
67
|
+
...(localPath.length > 0 ? { localPath } : {})
|
|
68
|
+
};
|
|
69
|
+
ctx.discoveredRepos = [repo];
|
|
70
|
+
ctx.selectedRepos = [repo];
|
|
71
|
+
return {
|
|
72
|
+
status: "passed",
|
|
73
|
+
message: `1 repository selected: ${owner}/${name}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { deriveOverallStatus } from "../engine.js";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { defaultBackburnerCodeRoot, defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot } from "../../utils/paths.js";
|
|
4
|
+
export class ShowSummaryStep {
|
|
5
|
+
name = "show-summary";
|
|
6
|
+
description = "Summary";
|
|
7
|
+
async run(ctx, _prompter) {
|
|
8
|
+
// Derive status from steps that have already completed — the engine will
|
|
9
|
+
// not have returned yet when this step runs, so we cannot rely on a
|
|
10
|
+
// finalResult callback. ctx.stepResults is already fully populated up to
|
|
11
|
+
// (but not including) this step.
|
|
12
|
+
const priorResults = Array.from(ctx.stepResults.values());
|
|
13
|
+
const overallStatus = deriveOverallStatus(priorResults);
|
|
14
|
+
const succeeded = overallStatus === "passed" || overallStatus === "warning";
|
|
15
|
+
const lines = ["", "=== Backburner Onboarding Complete ===", ""];
|
|
16
|
+
// Paths
|
|
17
|
+
lines.push("Directories:");
|
|
18
|
+
lines.push(` Code root: ${ctx.codeRoot}`);
|
|
19
|
+
lines.push(` Management: ${ctx.managementDir}`);
|
|
20
|
+
lines.push(` Outputs: ${ctx.outputsDir}`);
|
|
21
|
+
lines.push(` Logs: ${ctx.logDir}`);
|
|
22
|
+
lines.push(` Workspaces: ${ctx.workspacesDir}`);
|
|
23
|
+
// Repositories
|
|
24
|
+
if (ctx.selectedRepos.length > 0) {
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push("Repositories:");
|
|
27
|
+
for (const repo of ctx.selectedRepos) {
|
|
28
|
+
const note = repo.localPath !== undefined ? ` (${repo.localPath})` : " (no local clone)";
|
|
29
|
+
lines.push(` - ${repo.owner}/${repo.name}${note}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("Repositories: none selected");
|
|
35
|
+
}
|
|
36
|
+
// Providers
|
|
37
|
+
const available = ctx.discoveredProviders.filter((p) => p.available);
|
|
38
|
+
if (available.length > 0) {
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("Providers:");
|
|
41
|
+
for (const p of available) {
|
|
42
|
+
lines.push(` - ${p.name}${p.version !== undefined ? ` (${p.version})` : ""}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Agents
|
|
46
|
+
const agents = ctx.configDraft?.agents ?? [];
|
|
47
|
+
if (agents.length > 0) {
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push("Configured agents:");
|
|
50
|
+
for (const a of agents) {
|
|
51
|
+
const model = a.model !== undefined ? ` (${a.model})` : "";
|
|
52
|
+
lines.push(` - ${a.id}: ${a.provider}${model} [${a.modelClass}]`);
|
|
53
|
+
lines.push(` Roles: ${a.roles.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Warnings
|
|
57
|
+
if (ctx.warnings.length > 0) {
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("Warnings:");
|
|
60
|
+
for (const w of ctx.warnings) {
|
|
61
|
+
lines.push(` ⚠ ${w}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (succeeded) {
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("Next steps:");
|
|
67
|
+
lines.push(" 1. Create the configured GitHub labels on each managed repository");
|
|
68
|
+
lines.push(" 2. Start the orchestrator:");
|
|
69
|
+
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");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("Fix the issues listed above, then re-run: backburner init");
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
process.stdout.write(lines.join("\n"));
|
|
79
|
+
return { status: "passed", message: "Summary displayed" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function buildRunCommand(ctx) {
|
|
83
|
+
const args = ["backburner", "run"];
|
|
84
|
+
if (ctx.codeRoot !== defaultBackburnerCodeRoot()) {
|
|
85
|
+
args.push("--code-root", ctx.codeRoot);
|
|
86
|
+
}
|
|
87
|
+
if (ctx.managementDir !== defaultBackburnerManagementRoot()) {
|
|
88
|
+
args.push("--management-dir", ctx.managementDir);
|
|
89
|
+
}
|
|
90
|
+
if (ctx.outputsDir !== defaultBackburnerOutputsRoot()) {
|
|
91
|
+
args.push("--outputs-dir", ctx.outputsDir);
|
|
92
|
+
}
|
|
93
|
+
if (ctx.logDir !== path.join(ctx.outputsDir, "logs")) {
|
|
94
|
+
args.push("--log-dir", ctx.logDir);
|
|
95
|
+
}
|
|
96
|
+
return args.join(" ");
|
|
97
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class WelcomeStep {
|
|
2
|
+
name = "welcome";
|
|
3
|
+
description = "Welcome";
|
|
4
|
+
async run(_ctx, _prompter) {
|
|
5
|
+
const lines = [
|
|
6
|
+
"",
|
|
7
|
+
"Welcome to Backburner!",
|
|
8
|
+
"",
|
|
9
|
+
"This wizard configures Backburner to manage your GitHub repositories using local",
|
|
10
|
+
"AI agents. Work flows through GitHub issues, comments, labels, and pull",
|
|
11
|
+
"requests. Backburner runs locally and uses your existing credentials.",
|
|
12
|
+
"",
|
|
13
|
+
"You will be guided through:",
|
|
14
|
+
" 1. Checking required tools",
|
|
15
|
+
" 2. Validating GitHub authentication",
|
|
16
|
+
" 3. Selecting repositories to manage",
|
|
17
|
+
" 4. Configuring directories",
|
|
18
|
+
" 5. Detecting available AI providers",
|
|
19
|
+
" 6. Assigning models to agent roles",
|
|
20
|
+
" 7. Generating configuration files",
|
|
21
|
+
" 8. Running a final readiness check",
|
|
22
|
+
""
|
|
23
|
+
];
|
|
24
|
+
process.stdout.write(lines.join("\n"));
|
|
25
|
+
return { status: "passed", message: "Ready to begin" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class GitAccessChecker {
|
|
2
|
+
commandRunner;
|
|
3
|
+
constructor(commandRunner) {
|
|
4
|
+
this.commandRunner = commandRunner;
|
|
5
|
+
}
|
|
6
|
+
async checkFetch(repoPath, remote = "origin") {
|
|
7
|
+
try {
|
|
8
|
+
await this.commandRunner.run("git", ["fetch", "--dry-run", remote], {
|
|
9
|
+
cwd: repoPath,
|
|
10
|
+
timeoutMs: 30_000
|
|
11
|
+
});
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async checkPush(repoPath, remote = "origin") {
|
|
19
|
+
try {
|
|
20
|
+
await this.commandRunner.run("git", ["push", "--dry-run", "--porcelain", remote, "HEAD"], { cwd: repoPath, timeoutMs: 30_000 });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async check(repoPath) {
|
|
28
|
+
const [canFetch, canPush] = await Promise.all([
|
|
29
|
+
this.checkFetch(repoPath),
|
|
30
|
+
this.checkPush(repoPath)
|
|
31
|
+
]);
|
|
32
|
+
const result = { canFetch, canPush };
|
|
33
|
+
if (!canFetch) {
|
|
34
|
+
result.fetchIssue = {
|
|
35
|
+
title: "Cannot fetch from remote",
|
|
36
|
+
cause: `git fetch --dry-run failed for repository at "${repoPath}"`,
|
|
37
|
+
suggestedFix: "Verify the remote URL and network or credential access",
|
|
38
|
+
canAutoRemediate: false,
|
|
39
|
+
manualSteps: [
|
|
40
|
+
`Open a terminal in the repository directory: "${repoPath}"`,
|
|
41
|
+
"Run `git remote -v` to verify the remote URL",
|
|
42
|
+
"Run `gh auth setup-git` to configure git to use your GitHub credentials",
|
|
43
|
+
"Run `git fetch origin` to attempt the fetch manually"
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!canPush) {
|
|
48
|
+
result.pushIssue = {
|
|
49
|
+
title: "Cannot push to remote",
|
|
50
|
+
cause: `git push --dry-run failed for repository at "${repoPath}"`,
|
|
51
|
+
suggestedFix: "Verify push permissions on GitHub",
|
|
52
|
+
canAutoRemediate: false,
|
|
53
|
+
manualSteps: [
|
|
54
|
+
`Open a terminal in the repository directory: "${repoPath}"`,
|
|
55
|
+
"Run `gh auth setup-git` to configure git to use your GitHub credentials",
|
|
56
|
+
"Run `git push --dry-run origin HEAD` to attempt the push manually",
|
|
57
|
+
"Check that you have write access to the repository on GitHub"
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Substrings in `gh auth status` error output that indicate the user is simply not logged in. */
|
|
2
|
+
const AUTH_ERROR_SIGNALS = [
|
|
3
|
+
"not logged",
|
|
4
|
+
"not authenticated",
|
|
5
|
+
"You are not",
|
|
6
|
+
"no credentials",
|
|
7
|
+
"authentication required"
|
|
8
|
+
];
|
|
9
|
+
export class GitHubAuthChecker {
|
|
10
|
+
commandRunner;
|
|
11
|
+
constructor(commandRunner) {
|
|
12
|
+
this.commandRunner = commandRunner;
|
|
13
|
+
}
|
|
14
|
+
async check() {
|
|
15
|
+
try {
|
|
16
|
+
const result = await this.commandRunner.run("gh", ["auth", "status"], {
|
|
17
|
+
timeoutMs: 15_000
|
|
18
|
+
});
|
|
19
|
+
const output = result.stdout + result.stderr;
|
|
20
|
+
const username = extractUsername(output);
|
|
21
|
+
return {
|
|
22
|
+
authenticated: true,
|
|
23
|
+
...(username !== undefined ? { username } : {})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
const isAuthError = AUTH_ERROR_SIGNALS.some((signal) => message.toLowerCase().includes(signal.toLowerCase()));
|
|
29
|
+
if (isAuthError) {
|
|
30
|
+
return {
|
|
31
|
+
authenticated: false,
|
|
32
|
+
issue: {
|
|
33
|
+
title: "Not authenticated with GitHub",
|
|
34
|
+
cause: "The GitHub CLI is not authenticated with a GitHub account",
|
|
35
|
+
suggestedFix: "Run: gh auth login",
|
|
36
|
+
canAutoRemediate: false,
|
|
37
|
+
manualSteps: [
|
|
38
|
+
"In PowerShell, Command Prompt, or a POSIX shell, run: gh auth login",
|
|
39
|
+
"Follow the prompts to authenticate with your GitHub account",
|
|
40
|
+
"Then re-run: backburner init"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Unexpected failure (network error, Enterprise config, CLI crash, etc.).
|
|
46
|
+
// Surface it honestly rather than misreporting it as a login problem.
|
|
47
|
+
return {
|
|
48
|
+
authenticated: false,
|
|
49
|
+
issue: {
|
|
50
|
+
title: "GitHub CLI check failed unexpectedly",
|
|
51
|
+
cause: message,
|
|
52
|
+
suggestedFix: "Check that the GitHub CLI is installed and working, then re-run: backburner init",
|
|
53
|
+
canAutoRemediate: false,
|
|
54
|
+
manualSteps: [
|
|
55
|
+
"Run `gh auth status` manually to see the full error",
|
|
56
|
+
"Run `gh --version` to verify the CLI is installed and on PATH"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async getAuthenticatedUser() {
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.commandRunner.run("gh", ["api", "user", "--jq", ".login"], { timeoutMs: 10_000 });
|
|
65
|
+
const user = result.stdout.trim();
|
|
66
|
+
return user.length > 0 ? user : undefined;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function extractUsername(output) {
|
|
74
|
+
const patterns = [
|
|
75
|
+
/Logged in to github\.com account ([^\s]+)/i,
|
|
76
|
+
/account:\s+([^\s]+)/i,
|
|
77
|
+
/as ([^\s]+)\s*\(/i
|
|
78
|
+
];
|
|
79
|
+
for (const pattern of patterns) {
|
|
80
|
+
const match = output.match(pattern);
|
|
81
|
+
if (match?.[1])
|
|
82
|
+
return match[1];
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|