feat: include issue comments in worker task context (#148)
This commit is contained in:
@@ -34,6 +34,8 @@ export type DispatchOpts = {
|
||||
toLabel: string;
|
||||
/** Function to transition labels (injected to avoid provider dependency) */
|
||||
transitionLabel: (issueId: number, from: string, to: string) => Promise<void>;
|
||||
/** Issue provider for fetching comments */
|
||||
provider: import("./providers/provider.js").IssueProvider;
|
||||
/** Plugin config for model resolution */
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
/** Orchestrator's session key (used as spawnedBy for subagent tracking) */
|
||||
@@ -63,6 +65,7 @@ export async function buildTaskMessage(opts: {
|
||||
repo: string;
|
||||
baseBranch: string;
|
||||
groupId: string;
|
||||
comments?: Array<{ author: string; body: string; created_at: string }>;
|
||||
}): Promise<string> {
|
||||
const {
|
||||
workspaceDir, projectName, role, issueId, issueTitle,
|
||||
@@ -81,10 +84,24 @@ export async function buildTaskMessage(opts: {
|
||||
``,
|
||||
issueTitle,
|
||||
issueDescription ? `\n${issueDescription}` : "",
|
||||
];
|
||||
|
||||
// Include comments if present
|
||||
if (opts.comments && opts.comments.length > 0) {
|
||||
parts.push(``, `## Comments`);
|
||||
// Limit to last 20 comments to avoid bloating context
|
||||
const recentComments = opts.comments.slice(-20);
|
||||
for (const comment of recentComments) {
|
||||
const date = new Date(comment.created_at).toLocaleString();
|
||||
parts.push(``, `**${comment.author}** (${date}):`, comment.body);
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(
|
||||
``,
|
||||
`Repo: ${repo} | Branch: ${baseBranch} | ${issueUrl}`,
|
||||
`Project group ID: ${groupId}`,
|
||||
];
|
||||
);
|
||||
|
||||
if (roleInstructions) {
|
||||
parts.push(``, `---`, ``, roleInstructions.trim());
|
||||
@@ -123,7 +140,7 @@ export async function dispatchTask(
|
||||
const {
|
||||
workspaceDir, agentId, groupId, project, issueId, issueTitle,
|
||||
issueDescription, issueUrl, role, level, fromLabel, toLabel,
|
||||
transitionLabel, pluginConfig,
|
||||
transitionLabel, provider, pluginConfig,
|
||||
} = opts;
|
||||
|
||||
const model = resolveModel(role, level, pluginConfig);
|
||||
@@ -131,10 +148,14 @@ export async function dispatchTask(
|
||||
const existingSessionKey = getSessionForLevel(worker, level);
|
||||
const sessionAction = existingSessionKey ? "send" : "spawn";
|
||||
|
||||
// Fetch comments to include in task context
|
||||
const comments = await provider.listComments(issueId);
|
||||
|
||||
const taskMessage = await buildTaskMessage({
|
||||
workspaceDir, projectName: project.name, role, issueId,
|
||||
issueTitle, issueDescription, issueUrl,
|
||||
repo: project.repo, baseBranch: project.baseBranch, groupId,
|
||||
comments,
|
||||
});
|
||||
|
||||
await transitionLabel(issueId, fromLabel, toLabel);
|
||||
|
||||
@@ -65,6 +65,14 @@ export class GitHubProvider implements IssueProvider {
|
||||
return toIssue(JSON.parse(raw) as GhIssue);
|
||||
}
|
||||
|
||||
async listComments(issueId: number): Promise<import("./provider.js").IssueComment[]> {
|
||||
try {
|
||||
const raw = await this.gh(["api", `repos/:owner/:repo/issues/${issueId}/comments`, "--jq", ".[] | {author: .user.login, body: .body, created_at: .created_at}"]);
|
||||
if (!raw) return [];
|
||||
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async transitionLabel(issueId: number, from: StateLabel, to: StateLabel): Promise<void> {
|
||||
const issue = await this.getIssue(issueId);
|
||||
const stateLabels = issue.labels.filter((l) => STATE_LABELS.includes(l as StateLabel));
|
||||
|
||||
@@ -52,6 +52,21 @@ export class GitLabProvider implements IssueProvider {
|
||||
return JSON.parse(raw) as Issue;
|
||||
}
|
||||
|
||||
async listComments(issueId: number): Promise<import("./provider.js").IssueComment[]> {
|
||||
try {
|
||||
const raw = await this.glab(["api", `projects/:id/issues/${issueId}/notes`, "--paginate"]);
|
||||
const notes = JSON.parse(raw) as Array<{ author: { username: string }; body: string; created_at: string; system: boolean }>;
|
||||
// Filter out system notes (e.g. "changed label", "closed issue")
|
||||
return notes
|
||||
.filter((note) => !note.system)
|
||||
.map((note) => ({
|
||||
author: note.author.username,
|
||||
body: note.body,
|
||||
created_at: note.created_at,
|
||||
}));
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async transitionLabel(issueId: number, from: StateLabel, to: StateLabel): Promise<void> {
|
||||
const issue = await this.getIssue(issueId);
|
||||
const stateLabels = issue.labels.filter((l) => STATE_LABELS.includes(l as StateLabel));
|
||||
|
||||
@@ -24,12 +24,19 @@ export type Issue = {
|
||||
web_url: string;
|
||||
};
|
||||
|
||||
export type IssueComment = {
|
||||
author: string;
|
||||
body: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export interface IssueProvider {
|
||||
ensureLabel(name: string, color: string): Promise<void>;
|
||||
ensureAllStateLabels(): Promise<void>;
|
||||
createIssue(title: string, description: string, label: StateLabel, assignees?: string[]): Promise<Issue>;
|
||||
listIssuesByLabel(label: StateLabel): Promise<Issue[]>;
|
||||
getIssue(issueId: number): Promise<Issue>;
|
||||
listComments(issueId: number): Promise<IssueComment[]>;
|
||||
transitionLabel(issueId: number, from: StateLabel, to: StateLabel): Promise<void>;
|
||||
closeIssue(issueId: number): Promise<void>;
|
||||
reopenIssue(issueId: number): Promise<void>;
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function projectTick(opts: {
|
||||
/** Only attempt this role. Used by work_start to fill the other slot. */
|
||||
targetRole?: "dev" | "qa";
|
||||
/** Optional provider override (for testing). Uses createProvider if omitted. */
|
||||
provider?: Pick<IssueProvider, "listIssuesByLabel" | "transitionLabel">;
|
||||
provider?: Pick<IssueProvider, "listIssuesByLabel" | "transitionLabel" | "listComments">;
|
||||
}): Promise<TickResult> {
|
||||
const { workspaceDir, groupId, agentId, sessionKey, pluginConfig, dryRun, maxPickups, targetRole } = opts;
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function projectTick(opts: {
|
||||
issueTitle: issue.title, issueDescription: issue.description ?? "", issueUrl: issue.web_url,
|
||||
role, level: selectedLevel, fromLabel: currentLabel, toLabel: targetLabel,
|
||||
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
|
||||
provider: provider as IssueProvider,
|
||||
pluginConfig, sessionKey,
|
||||
});
|
||||
pickups.push({
|
||||
|
||||
@@ -97,6 +97,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
issueTitle: issue.title, issueDescription: issue.description ?? "", issueUrl: issue.web_url,
|
||||
role, level: selectedLevel, fromLabel: currentLabel, toLabel: targetLabel,
|
||||
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
|
||||
provider,
|
||||
pluginConfig, sessionKey: ctx.sessionKey,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user