@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,105 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { workflowRegistry } from "../workflows/registry.js";
|
|
3
|
+
import { roleForTask } from "./selection.js";
|
|
4
|
+
export function resolveProviderSessionScope(task, agent) {
|
|
5
|
+
if (!task.workstreamId) {
|
|
6
|
+
throw new Error(`Task ${task.id} has no workstreamId; provider session scope requires a workstream identity.`);
|
|
7
|
+
}
|
|
8
|
+
const { localRepoPath, branchName } = task.context;
|
|
9
|
+
if (!localRepoPath || !branchName) {
|
|
10
|
+
throw new Error(`Task ${task.id} is missing localRepoPath or branchName; provider session scope cannot be resolved.`);
|
|
11
|
+
}
|
|
12
|
+
const definition = workflowRegistry.getDefinition(task.kind);
|
|
13
|
+
if (agent.modelClass !== definition.modelClass) {
|
|
14
|
+
throw new Error(`Agent model class "${agent.modelClass}" does not match required model class "${definition.modelClass}" for task "${task.kind}".`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
repoKey: task.repoSlug,
|
|
18
|
+
workstreamId: task.workstreamId,
|
|
19
|
+
job: roleForTask(task),
|
|
20
|
+
modelClass: agent.modelClass,
|
|
21
|
+
configuredAgentId: agent.id,
|
|
22
|
+
provider: agent.provider,
|
|
23
|
+
localRepoPath,
|
|
24
|
+
branchName
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function getProviderSessionRecordId(scope) {
|
|
28
|
+
const components = [
|
|
29
|
+
scope.repoKey,
|
|
30
|
+
scope.workstreamId,
|
|
31
|
+
scope.job,
|
|
32
|
+
scope.modelClass,
|
|
33
|
+
scope.provider,
|
|
34
|
+
scope.configuredAgentId,
|
|
35
|
+
scope.localRepoPath,
|
|
36
|
+
scope.branchName
|
|
37
|
+
];
|
|
38
|
+
const data = components.join("|");
|
|
39
|
+
return createHash("sha256").update(data).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
export function evaluateProviderSessionDecision(input) {
|
|
42
|
+
if (input.capability === "unsupported") {
|
|
43
|
+
if (input.existingRecord?.providerSessionId || input.existingRecord?.providerSessionName) {
|
|
44
|
+
throw new Error("Invalid state: unsupported capability record cannot carry provider identity.");
|
|
45
|
+
}
|
|
46
|
+
return { mode: "fallback", reason: "unsupported" };
|
|
47
|
+
}
|
|
48
|
+
if (input.capability === "unverified") {
|
|
49
|
+
if (input.existingRecord?.providerSessionId || input.existingRecord?.providerSessionName) {
|
|
50
|
+
throw new Error("Invalid state: unverified capability record cannot carry provider identity.");
|
|
51
|
+
}
|
|
52
|
+
return { mode: "fallback", reason: "unverified" };
|
|
53
|
+
}
|
|
54
|
+
if (!input.nativeResumeEnabled) {
|
|
55
|
+
return { mode: "fallback", reason: "native_resume_not_enabled" };
|
|
56
|
+
}
|
|
57
|
+
const getNewSessionDecision = () => {
|
|
58
|
+
const uuid = randomUUID();
|
|
59
|
+
const displayName = `backburner:${input.requestedScope.repoKey}:${input.taskKind}:${input.requestedScope.workstreamId}`;
|
|
60
|
+
return {
|
|
61
|
+
mode: "new_session",
|
|
62
|
+
providerSessionId: uuid,
|
|
63
|
+
providerSessionName: displayName
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
if (!input.existingRecord) {
|
|
67
|
+
if (input.requestedScope.provider === "claude") {
|
|
68
|
+
return getNewSessionDecision();
|
|
69
|
+
}
|
|
70
|
+
return { mode: "fallback", reason: "no_session_record" };
|
|
71
|
+
}
|
|
72
|
+
// Exact scope comparison
|
|
73
|
+
const sE = input.existingRecord.scope;
|
|
74
|
+
const sR = input.requestedScope;
|
|
75
|
+
const match = sE.repoKey === sR.repoKey &&
|
|
76
|
+
sE.workstreamId === sR.workstreamId &&
|
|
77
|
+
sE.job === sR.job &&
|
|
78
|
+
sE.modelClass === sR.modelClass &&
|
|
79
|
+
sE.configuredAgentId === sR.configuredAgentId &&
|
|
80
|
+
sE.provider === sR.provider &&
|
|
81
|
+
sE.localRepoPath === sR.localRepoPath &&
|
|
82
|
+
sE.branchName === sR.branchName;
|
|
83
|
+
if (!match) {
|
|
84
|
+
return { mode: "fallback", reason: "scope_mismatch" };
|
|
85
|
+
}
|
|
86
|
+
if (input.existingRecord.capability === "unsupported") {
|
|
87
|
+
return { mode: "fallback", reason: "unsupported" };
|
|
88
|
+
}
|
|
89
|
+
if (input.existingRecord.capability === "unverified") {
|
|
90
|
+
return { mode: "fallback", reason: "unverified" };
|
|
91
|
+
}
|
|
92
|
+
if (input.existingRecord.status === "closed") {
|
|
93
|
+
if (input.requestedScope.provider === "claude") {
|
|
94
|
+
return getNewSessionDecision();
|
|
95
|
+
}
|
|
96
|
+
return { mode: "fallback", reason: "no_session_record" };
|
|
97
|
+
}
|
|
98
|
+
if (input.existingRecord.status === "stale") {
|
|
99
|
+
if (input.requestedScope.provider === "claude") {
|
|
100
|
+
return getNewSessionDecision();
|
|
101
|
+
}
|
|
102
|
+
return { mode: "fallback", reason: "no_session_record" };
|
|
103
|
+
}
|
|
104
|
+
return { mode: "native_resume_candidate", record: input.existingRecord };
|
|
105
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Task } from "../tasks/task.js";
|
|
2
|
+
export class GitHubPullRequestReviewPublisher {
|
|
3
|
+
gitHubGateway;
|
|
4
|
+
logger;
|
|
5
|
+
reposById;
|
|
6
|
+
constructor(gitHubGateway, repos, logger) {
|
|
7
|
+
this.gitHubGateway = gitHubGateway;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
this.reposById = new Map(repos.map((repo) => [repo.id, repo]));
|
|
10
|
+
}
|
|
11
|
+
async publishReview(task, execution) {
|
|
12
|
+
if (task.kind !== "review_pr" || execution.taskStatus !== "completed") {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const review = execution.result?.review;
|
|
16
|
+
if (!review) {
|
|
17
|
+
this.logger?.warn(`Completed review task ${task.id} did not return a structured review result.`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const repo = this.reposById.get(task.repoId);
|
|
21
|
+
const pullRequestNumber = Task.from(task).owningPullRequestNumber();
|
|
22
|
+
if (!repo || pullRequestNumber === undefined) {
|
|
23
|
+
this.logger?.warn(`Could not resolve repo or pull request number for review task ${task.id}.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const pr = await this.gitHubGateway.getPullRequest(repo.owner, repo.name, pullRequestNumber);
|
|
28
|
+
const changedFiles = await this.gitHubGateway.listPullRequestChangedFiles(repo.owner, repo.name, pullRequestNumber);
|
|
29
|
+
const headSha = pr.headSha;
|
|
30
|
+
const validation = this.validateReview(review, changedFiles);
|
|
31
|
+
if (!validation) {
|
|
32
|
+
this.logger?.warn(`Review task ${task.id} returned an invalid review result (missing summary or action).`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const reviewBody = buildReviewBody(validation);
|
|
36
|
+
try {
|
|
37
|
+
await this.gitHubGateway.submitPullRequestReview({
|
|
38
|
+
owner: repo.owner,
|
|
39
|
+
name: repo.name,
|
|
40
|
+
pullRequestNumber,
|
|
41
|
+
event: "COMMENT",
|
|
42
|
+
body: reviewBody,
|
|
43
|
+
...(headSha ? { commitId: headSha } : {}),
|
|
44
|
+
comments: validation.validInlineComments
|
|
45
|
+
});
|
|
46
|
+
this.logger?.info(`Published PR review for ${repo.owner}/${repo.name}#${pullRequestNumber}: ` +
|
|
47
|
+
`${validation.validInlineComments.length} inline, ${validation.movedToSummary.length} moved to summary.`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (validation.validInlineComments.length > 0) {
|
|
51
|
+
this.logger?.warn(`Failed to submit PR review with inline comments, retrying summary-only. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
const fallbackValidation = {
|
|
53
|
+
...validation,
|
|
54
|
+
validInlineComments: [],
|
|
55
|
+
movedToSummary: [
|
|
56
|
+
...validation.movedToSummary,
|
|
57
|
+
...validation.validInlineComments.map((c) => ({
|
|
58
|
+
comment: c,
|
|
59
|
+
reason: "GitHub rejected inline placement"
|
|
60
|
+
}))
|
|
61
|
+
]
|
|
62
|
+
};
|
|
63
|
+
const fallbackBody = buildReviewBody(fallbackValidation);
|
|
64
|
+
try {
|
|
65
|
+
await this.gitHubGateway.submitPullRequestReview({
|
|
66
|
+
owner: repo.owner,
|
|
67
|
+
name: repo.name,
|
|
68
|
+
pullRequestNumber,
|
|
69
|
+
event: "COMMENT",
|
|
70
|
+
body: fallbackBody,
|
|
71
|
+
...(headSha ? { commitId: headSha } : {})
|
|
72
|
+
});
|
|
73
|
+
this.logger?.info(`Published fallback PR review for ${repo.owner}/${repo.name}#${pullRequestNumber} with all comments moved to summary.`);
|
|
74
|
+
}
|
|
75
|
+
catch (fallbackError) {
|
|
76
|
+
this.logger?.warn(`Failed to submit summary-only PR review, posting a PR thread comment instead. Error: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
77
|
+
await this.gitHubGateway.postIssueComment({
|
|
78
|
+
owner: repo.owner,
|
|
79
|
+
name: repo.name,
|
|
80
|
+
issueNumber: pullRequestNumber,
|
|
81
|
+
body: fallbackBody
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await this.gitHubGateway.postIssueComment({
|
|
87
|
+
owner: repo.owner,
|
|
88
|
+
name: repo.name,
|
|
89
|
+
issueNumber: pullRequestNumber,
|
|
90
|
+
body: reviewBody
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.logger?.error?.(`Failed to publish PR review for ${repo.owner}/${repo.name}#${pullRequestNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
// Let it propagate so safelyPublishReview can handle it (currently it swallows it)
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
validateReview(review, changedFiles) {
|
|
102
|
+
const summary = review.summary?.trim();
|
|
103
|
+
if (!summary || !review.action) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const validInlineComments = [];
|
|
107
|
+
const movedToSummary = [];
|
|
108
|
+
const changedFileByPath = new Map(changedFiles.map((f) => [f.path, f]));
|
|
109
|
+
if (review.inlineComments) {
|
|
110
|
+
for (const comment of review.inlineComments) {
|
|
111
|
+
const shapeResult = validateInlineComment(comment);
|
|
112
|
+
if (!shapeResult.isValid) {
|
|
113
|
+
movedToSummary.push({ comment, reason: shapeResult.reason });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const normalized = shapeResult.normalized;
|
|
117
|
+
const path = normalized.path;
|
|
118
|
+
if (path === undefined) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const anchorResult = validateInlineCommentAnchor(normalized, changedFileByPath.get(path));
|
|
122
|
+
if (anchorResult.isValid) {
|
|
123
|
+
validInlineComments.push(normalized);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
movedToSummary.push({ comment: normalized, reason: anchorResult.reason });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
validInlineComments,
|
|
132
|
+
movedToSummary,
|
|
133
|
+
normalizedReview: {
|
|
134
|
+
action: review.action,
|
|
135
|
+
summary
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function validateInlineCommentAnchor(comment, changedFile) {
|
|
141
|
+
if (!changedFile) {
|
|
142
|
+
return { isValid: false, reason: "file is not in the PR diff" };
|
|
143
|
+
}
|
|
144
|
+
const { line, side, startLine, startSide } = comment;
|
|
145
|
+
if (line === undefined) {
|
|
146
|
+
return { isValid: false, reason: "missing line number" };
|
|
147
|
+
}
|
|
148
|
+
// Check line/side
|
|
149
|
+
const targetLines = side === "LEFT" ? changedFile.leftCommentableLines : changedFile.rightCommentableLines;
|
|
150
|
+
if (!targetLines.includes(line)) {
|
|
151
|
+
return { isValid: false, reason: `line ${line} on ${side} side is not available in the PR diff` };
|
|
152
|
+
}
|
|
153
|
+
// Check range if present
|
|
154
|
+
if (startLine !== undefined) {
|
|
155
|
+
const effectiveStartSide = startSide || side;
|
|
156
|
+
if (effectiveStartSide === side) {
|
|
157
|
+
// Single-side range: require every line in the range to be commentable
|
|
158
|
+
for (let l = startLine; l <= line; l++) {
|
|
159
|
+
if (!targetLines.includes(l)) {
|
|
160
|
+
return {
|
|
161
|
+
isValid: false,
|
|
162
|
+
reason: `line ${l} in range ${startLine}-${line} on ${side} side is not available in the PR diff`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Cross-side range: conservative MVP rule
|
|
169
|
+
const startTargetLines = effectiveStartSide === "LEFT" ? changedFile.leftCommentableLines : changedFile.rightCommentableLines;
|
|
170
|
+
if (!startTargetLines.includes(startLine)) {
|
|
171
|
+
return {
|
|
172
|
+
isValid: false,
|
|
173
|
+
reason: `start line ${startLine} on ${effectiveStartSide} side is not available in the PR diff`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { isValid: true };
|
|
179
|
+
}
|
|
180
|
+
function validateInlineComment(comment) {
|
|
181
|
+
const path = comment.path?.trim();
|
|
182
|
+
const body = comment.body?.trim();
|
|
183
|
+
const line = comment.line;
|
|
184
|
+
if (!path)
|
|
185
|
+
return { isValid: false, reason: "missing path" };
|
|
186
|
+
if (!body)
|
|
187
|
+
return { isValid: false, reason: "missing body" };
|
|
188
|
+
if (line === undefined || !Number.isInteger(line) || line < 1) {
|
|
189
|
+
return { isValid: false, reason: "invalid line number" };
|
|
190
|
+
}
|
|
191
|
+
const side = comment.side === "LEFT" || comment.side === "RIGHT" ? comment.side : "RIGHT";
|
|
192
|
+
let startLine = comment.startLine;
|
|
193
|
+
let startSide = comment.startSide;
|
|
194
|
+
if (startLine !== undefined) {
|
|
195
|
+
if (!Number.isInteger(startLine) || startLine < 1) {
|
|
196
|
+
return { isValid: false, reason: "invalid startLine" };
|
|
197
|
+
}
|
|
198
|
+
if (startLine > line) {
|
|
199
|
+
return { isValid: false, reason: "startLine must be <= line" };
|
|
200
|
+
}
|
|
201
|
+
if (!startSide) {
|
|
202
|
+
startSide = side;
|
|
203
|
+
}
|
|
204
|
+
else if (startSide !== "LEFT" && startSide !== "RIGHT") {
|
|
205
|
+
return { isValid: false, reason: "invalid startSide" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
isValid: true,
|
|
210
|
+
normalized: {
|
|
211
|
+
path,
|
|
212
|
+
line,
|
|
213
|
+
body,
|
|
214
|
+
side,
|
|
215
|
+
...(startLine !== undefined ? { startLine } : {}),
|
|
216
|
+
...(startSide !== undefined ? { startSide } : {})
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function buildReviewBody(validation) {
|
|
221
|
+
const { normalizedReview, movedToSummary, validInlineComments } = validation;
|
|
222
|
+
const lines = [
|
|
223
|
+
withBotPrefix(`Review result: ${formatReviewAction(normalizedReview.action)}`),
|
|
224
|
+
"",
|
|
225
|
+
normalizedReview.summary
|
|
226
|
+
];
|
|
227
|
+
if (movedToSummary.length > 0) {
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(`Inline comment publication: ${validInlineComments.length} posted inline, ${movedToSummary.length} moved into this summary.`);
|
|
230
|
+
lines.push("");
|
|
231
|
+
lines.push("Moved from inline comments:");
|
|
232
|
+
for (const { comment, reason } of movedToSummary) {
|
|
233
|
+
const location = `${comment.path}:${comment.line}`;
|
|
234
|
+
lines.push(`- ${location}: ${comment.body} (Reason: ${reason})`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return lines.join("\n").trim();
|
|
238
|
+
}
|
|
239
|
+
function formatReviewAction(action) {
|
|
240
|
+
switch (action) {
|
|
241
|
+
case "APPROVE":
|
|
242
|
+
return "approved";
|
|
243
|
+
case "REQUEST_CHANGES":
|
|
244
|
+
return "changes requested";
|
|
245
|
+
case "COMMENT":
|
|
246
|
+
return "comment";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function withBotPrefix(value) {
|
|
250
|
+
const trimmed = value.trim();
|
|
251
|
+
if (trimmed.startsWith("🤖")) {
|
|
252
|
+
return trimmed;
|
|
253
|
+
}
|
|
254
|
+
return `🤖 ${trimmed}`;
|
|
255
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { workflowRegistry } from "../workflows/registry.js";
|
|
2
|
+
export class NoProviderAvailableError extends Error {
|
|
3
|
+
role;
|
|
4
|
+
modelClass;
|
|
5
|
+
limitedProviders;
|
|
6
|
+
constructor(role, modelClass, limitedProviders, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.role = role;
|
|
9
|
+
this.modelClass = modelClass;
|
|
10
|
+
this.limitedProviders = limitedProviders;
|
|
11
|
+
this.name = "NoProviderAvailableError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function selectAgentForTask(task, agents, providerLimits, runtimeCompatibility) {
|
|
15
|
+
const definition = workflowRegistry.getDefinition(task.kind);
|
|
16
|
+
const requiredRole = resolveTaskRole(task, definition.role);
|
|
17
|
+
const requiredModelClass = definition.modelClass;
|
|
18
|
+
const candidates = agents.filter((agent) => agent.enabled &&
|
|
19
|
+
agent.roles.includes(requiredRole) &&
|
|
20
|
+
agent.modelClass === requiredModelClass);
|
|
21
|
+
if (candidates.length === 0) {
|
|
22
|
+
throw new Error(`No enabled agent configured for task ${task.id} with role "${requiredRole}" and modelClass "${requiredModelClass}"`);
|
|
23
|
+
}
|
|
24
|
+
const runtimeCompatibleCandidates = candidates.filter((agent) => providerRuntimeIsCompatible(agent.provider, runtimeCompatibility));
|
|
25
|
+
if (runtimeCompatibleCandidates.length === 0) {
|
|
26
|
+
if (runtimeCompatibility?.replacementForProvider) {
|
|
27
|
+
throw new NoProviderAvailableError(requiredRole, requiredModelClass, [runtimeCompatibility.replacementForProvider], `No runtime-compatible replacement provider for role "${requiredRole}" and modelClass "${requiredModelClass}" after ${runtimeCompatibility.replacementForProvider} was limited`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`No runtime-compatible agent configured for task ${task.id} with role "${requiredRole}" and modelClass "${requiredModelClass}"`);
|
|
30
|
+
}
|
|
31
|
+
const limitedProviders = new Set();
|
|
32
|
+
const limits = providerLimits ?? [];
|
|
33
|
+
for (const limit of limits) {
|
|
34
|
+
if (limit.status === "limited") {
|
|
35
|
+
limitedProviders.add(limit.provider);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const availableCandidates = runtimeCompatibleCandidates.filter((agent) => !limitedProviders.has(agent.provider));
|
|
39
|
+
if (availableCandidates.length === 0) {
|
|
40
|
+
const candidateProviders = Array.from(new Set(runtimeCompatibleCandidates.map((a) => a.provider)));
|
|
41
|
+
throw new NoProviderAvailableError(requiredRole, requiredModelClass, candidateProviders, `No available provider for role "${requiredRole}" and modelClass "${requiredModelClass}" (all matching providers: ${candidateProviders.join(", ")} are currently limited)`);
|
|
42
|
+
}
|
|
43
|
+
const index = Math.floor(Math.random() * availableCandidates.length);
|
|
44
|
+
return availableCandidates[index];
|
|
45
|
+
}
|
|
46
|
+
export function roleForTask(task) {
|
|
47
|
+
return resolveTaskRole(task, workflowRegistry.getDefinition(task.kind).role);
|
|
48
|
+
}
|
|
49
|
+
function resolveTaskRole(task, defaultRole) {
|
|
50
|
+
if (task.kind === "respond_issue_comment" &&
|
|
51
|
+
(task.workflowPhase === "product_discovery" || task.workflowPhase === "product_specification")) {
|
|
52
|
+
return "product";
|
|
53
|
+
}
|
|
54
|
+
return defaultRole;
|
|
55
|
+
}
|
|
56
|
+
function providerRuntimeIsCompatible(candidateProvider, compatibility) {
|
|
57
|
+
const sourceProvider = compatibility?.replacementForProvider;
|
|
58
|
+
if (!sourceProvider || candidateProvider === sourceProvider) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (!providerAllowListCoversRequiredTools(compatibility.providerBrokerToolAllowLists, sourceProvider, candidateProvider)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return providerCapabilitiesCoverRequiredTools(compatibility.capabilitiesByProvider, sourceProvider, candidateProvider);
|
|
65
|
+
}
|
|
66
|
+
function providerAllowListCoversRequiredTools(allowLists, sourceProvider, candidateProvider) {
|
|
67
|
+
if (!allowLists) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const requiredTools = allowLists.get(sourceProvider);
|
|
71
|
+
if (!requiredTools || requiredTools.length === 0) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const candidateTools = allowLists.get(candidateProvider);
|
|
75
|
+
if (!candidateTools) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return requiredTools.every((requiredTool) => allowListAllowsRequiredTool(candidateTools, requiredTool));
|
|
79
|
+
}
|
|
80
|
+
function allowListAllowsRequiredTool(candidateTools, requiredTool) {
|
|
81
|
+
if (candidateTools.includes("*") || candidateTools.includes(requiredTool)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (requiredTool.endsWith(".*")) {
|
|
85
|
+
return candidateTools.includes(requiredTool);
|
|
86
|
+
}
|
|
87
|
+
return candidateTools.some((candidateTool) => candidateTool.endsWith(".*") && requiredTool.startsWith(candidateTool.slice(0, -1)));
|
|
88
|
+
}
|
|
89
|
+
function providerCapabilitiesCoverRequiredTools(capabilitiesByProvider, sourceProvider, candidateProvider) {
|
|
90
|
+
if (!capabilitiesByProvider) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const sourceCapabilities = capabilitiesByProvider.get(sourceProvider);
|
|
94
|
+
if (!sourceCapabilities) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const requiredTools = visibleCapabilityTools(sourceCapabilities);
|
|
98
|
+
if (requiredTools.size === 0) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const candidateCapabilities = capabilitiesByProvider.get(candidateProvider);
|
|
102
|
+
if (!candidateCapabilities) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const candidateTools = visibleCapabilityTools(candidateCapabilities);
|
|
106
|
+
return [...requiredTools].every((tool) => candidateTools.has(tool));
|
|
107
|
+
}
|
|
108
|
+
function visibleCapabilityTools(capabilities) {
|
|
109
|
+
return new Set([
|
|
110
|
+
...capabilities.sensors.flatMap((sensor) => sensor.tools),
|
|
111
|
+
...capabilities.controllers.flatMap((controller) => controller.tools)
|
|
112
|
+
]);
|
|
113
|
+
}
|