@xn-intenton-z2a/agentic-lib 7.1.48 → 7.1.50
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/package.json +1 -1
- package/src/actions/agentic-step/copilot.js +3 -2
- package/src/actions/agentic-step/tasks/enhance-issue.js +82 -15
- package/src/actions/agentic-step/tasks/fix-code.js +41 -1
- package/src/actions/agentic-step/tasks/review-issue.js +123 -45
- package/src/actions/agentic-step/tasks/supervise.js +37 -11
- package/src/actions/agentic-step/tasks/transform.js +37 -7
- package/src/agents/agent-supervisor.md +4 -0
- package/src/seeds/zero-package.json +3 -3
- package/src/workflows/agent-flow-review.yml +1 -20
- package/src/workflows/agent-flow-transform.yml +107 -1
- package/src/workflows/agent-supervisor-schedule.yml +20 -0
- package/src/workflows/agent-supervisor.yml +35 -2
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { CopilotClient, approveAll } from "@github/copilot-sdk";
|
|
8
8
|
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
9
10
|
import { createAgentTools } from "./tools.js";
|
|
10
11
|
import * as core from "@actions/core";
|
|
11
12
|
|
|
@@ -157,10 +158,10 @@ export function scanDirectory(dirPath, extensions, options = {}) {
|
|
|
157
158
|
.slice(0, fileLimit)
|
|
158
159
|
.map((f) => {
|
|
159
160
|
try {
|
|
160
|
-
const content = readFileSync(
|
|
161
|
+
const content = readFileSync(join(dirPath, f), "utf8");
|
|
161
162
|
return { name: f, content: contentLimit ? content.substring(0, contentLimit) : content };
|
|
162
163
|
} catch (err) {
|
|
163
|
-
core.debug(`[scanDirectory] ${dirPath
|
|
164
|
+
core.debug(`[scanDirectory] ${join(dirPath, f)}: ${err.message}`);
|
|
164
165
|
return { name: f, content: "" };
|
|
165
166
|
}
|
|
166
167
|
});
|
|
@@ -4,26 +4,18 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Takes an issue and enhances it with clear, testable acceptance criteria
|
|
6
6
|
// so that the resolve-issue task can implement it effectively.
|
|
7
|
+
// Supports batch mode: when no issueNumber is provided, enhances up to 3 issues.
|
|
7
8
|
|
|
8
9
|
import * as core from "@actions/core";
|
|
9
10
|
import { isIssueResolved } from "../safety.js";
|
|
10
11
|
import { runCopilotTask, readOptionalFile, scanDirectory } from "../copilot.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Enhance a GitHub issue with testable acceptance criteria.
|
|
14
|
-
*
|
|
15
|
-
* @param {Object} context - Task context from index.js
|
|
16
|
-
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
14
|
+
* Enhance a single GitHub issue with testable acceptance criteria.
|
|
17
15
|
*/
|
|
18
|
-
|
|
19
|
-
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
20
|
-
|
|
21
|
-
if (!issueNumber) {
|
|
22
|
-
throw new Error("enhance-issue task requires issue-number input");
|
|
23
|
-
}
|
|
24
|
-
|
|
16
|
+
async function enhanceSingleIssue({ octokit, repo, config, issueNumber, instructions, model }) {
|
|
25
17
|
if (await isIssueResolved(octokit, repo, issueNumber)) {
|
|
26
|
-
return { outcome: "nop", details:
|
|
18
|
+
return { outcome: "nop", details: `Issue #${issueNumber} already resolved` };
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
const { data: issue } = await octokit.rest.issues.get({
|
|
@@ -32,11 +24,11 @@ export async function enhanceIssue(context) {
|
|
|
32
24
|
});
|
|
33
25
|
|
|
34
26
|
if (issue.labels.some((l) => l.name === "ready")) {
|
|
35
|
-
return { outcome: "nop", details:
|
|
27
|
+
return { outcome: "nop", details: `Issue #${issueNumber} already has ready label` };
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
const contributing = readOptionalFile(config.paths.contributing.path);
|
|
39
|
-
const features = scanDirectory(config.paths.features.path, ".md", { contentLimit:
|
|
31
|
+
const features = scanDirectory(config.paths.features.path, ".md", { contentLimit: 2000 });
|
|
40
32
|
|
|
41
33
|
const agentInstructions = instructions || "Enhance this issue with clear, testable acceptance criteria.";
|
|
42
34
|
|
|
@@ -61,7 +53,13 @@ export async function enhanceIssue(context) {
|
|
|
61
53
|
"Output ONLY the new issue body text, no markdown code fences.",
|
|
62
54
|
].join("\n");
|
|
63
55
|
|
|
64
|
-
const {
|
|
56
|
+
const {
|
|
57
|
+
content: enhancedBody,
|
|
58
|
+
tokensUsed,
|
|
59
|
+
inputTokens,
|
|
60
|
+
outputTokens,
|
|
61
|
+
cost,
|
|
62
|
+
} = await runCopilotTask({
|
|
65
63
|
model,
|
|
66
64
|
systemMessage: "You are a requirements analyst. Enhance GitHub issues with clear, testable acceptance criteria.",
|
|
67
65
|
prompt,
|
|
@@ -105,3 +103,72 @@ export async function enhanceIssue(context) {
|
|
|
105
103
|
details: `Enhanced issue #${issueNumber} with acceptance criteria`,
|
|
106
104
|
};
|
|
107
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Enhance a GitHub issue with testable acceptance criteria.
|
|
109
|
+
* When no issueNumber is provided, enhances up to 3 unready automated issues.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} context - Task context from index.js
|
|
112
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
113
|
+
*/
|
|
114
|
+
export async function enhanceIssue(context) {
|
|
115
|
+
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
116
|
+
|
|
117
|
+
// Single issue mode
|
|
118
|
+
if (issueNumber) {
|
|
119
|
+
return enhanceSingleIssue({ octokit, repo, config, issueNumber, instructions, model });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Batch mode: find up to 3 unready automated issues
|
|
123
|
+
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
124
|
+
...repo,
|
|
125
|
+
state: "open",
|
|
126
|
+
labels: "automated",
|
|
127
|
+
per_page: 20,
|
|
128
|
+
sort: "created",
|
|
129
|
+
direction: "asc",
|
|
130
|
+
});
|
|
131
|
+
const unready = openIssues.filter((i) => !i.labels.some((l) => l.name === "ready")).slice(0, 3);
|
|
132
|
+
|
|
133
|
+
if (unready.length === 0) {
|
|
134
|
+
return { outcome: "nop", details: "No unready automated issues to enhance" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const results = [];
|
|
138
|
+
let totalTokens = 0;
|
|
139
|
+
let totalInputTokens = 0;
|
|
140
|
+
let totalOutputTokens = 0;
|
|
141
|
+
let totalCost = 0;
|
|
142
|
+
|
|
143
|
+
for (const issue of unready) {
|
|
144
|
+
core.info(`Batch enhancing issue #${issue.number} (${results.length + 1}/${unready.length})`);
|
|
145
|
+
const result = await enhanceSingleIssue({
|
|
146
|
+
octokit,
|
|
147
|
+
repo,
|
|
148
|
+
config,
|
|
149
|
+
issueNumber: issue.number,
|
|
150
|
+
instructions,
|
|
151
|
+
model,
|
|
152
|
+
});
|
|
153
|
+
results.push(result);
|
|
154
|
+
totalTokens += result.tokensUsed || 0;
|
|
155
|
+
totalInputTokens += result.inputTokens || 0;
|
|
156
|
+
totalOutputTokens += result.outputTokens || 0;
|
|
157
|
+
totalCost += result.cost || 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const enhanced = results.filter((r) => r.outcome === "issue-enhanced").length;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
outcome: enhanced > 0 ? "issues-enhanced" : "nop",
|
|
164
|
+
tokensUsed: totalTokens,
|
|
165
|
+
inputTokens: totalInputTokens,
|
|
166
|
+
outputTokens: totalOutputTokens,
|
|
167
|
+
cost: totalCost,
|
|
168
|
+
model,
|
|
169
|
+
details: `Batch enhanced ${enhanced}/${results.length} issues. ${results
|
|
170
|
+
.map((r) => r.details)
|
|
171
|
+
.join("; ")
|
|
172
|
+
.substring(0, 500)}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -6,8 +6,36 @@
|
|
|
6
6
|
// generates fixes using the Copilot SDK, and pushes a commit.
|
|
7
7
|
|
|
8
8
|
import * as core from "@actions/core";
|
|
9
|
+
import { execSync } from "child_process";
|
|
9
10
|
import { runCopilotTask, formatPathsSection } from "../copilot.js";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract run_id from a check run's details_url.
|
|
14
|
+
* e.g. "https://github.com/owner/repo/actions/runs/12345" → "12345"
|
|
15
|
+
*/
|
|
16
|
+
function extractRunId(detailsUrl) {
|
|
17
|
+
if (!detailsUrl) return null;
|
|
18
|
+
const match = detailsUrl.match(/\/runs\/(\d+)/);
|
|
19
|
+
return match ? match[1] : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch actual test output from a GitHub Actions run log.
|
|
24
|
+
*/
|
|
25
|
+
function fetchRunLog(runId) {
|
|
26
|
+
try {
|
|
27
|
+
const output = execSync(`gh run view ${runId} --log-failed`, {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
timeout: 30000,
|
|
30
|
+
env: { ...process.env },
|
|
31
|
+
});
|
|
32
|
+
return output.substring(0, 8000);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
core.debug(`[fix-code] Could not fetch log for run ${runId}: ${err.message}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
/**
|
|
12
40
|
* Fix failing code on a pull request.
|
|
13
41
|
*
|
|
@@ -31,7 +59,19 @@ export async function fixCode(context) {
|
|
|
31
59
|
return { outcome: "nop", details: "No failing checks found" };
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
|
|
62
|
+
// Build detailed failure information with actual test logs where possible
|
|
63
|
+
const failureDetails = failedChecks
|
|
64
|
+
.map((cr) => {
|
|
65
|
+
const runId = extractRunId(cr.details_url);
|
|
66
|
+
let logContent = null;
|
|
67
|
+
if (runId) {
|
|
68
|
+
logContent = fetchRunLog(runId);
|
|
69
|
+
}
|
|
70
|
+
const detail = logContent || cr.output?.summary || "Failed";
|
|
71
|
+
return `**${cr.name}:**\n${detail}`;
|
|
72
|
+
})
|
|
73
|
+
.join("\n\n");
|
|
74
|
+
|
|
35
75
|
const agentInstructions = instructions || "Fix the failing tests by modifying the source code.";
|
|
36
76
|
const readOnlyPaths = config.readOnlyPaths;
|
|
37
77
|
|
|
@@ -4,56 +4,24 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Checks open issues against the current codebase to determine
|
|
6
6
|
// if they have been resolved, and closes them if so.
|
|
7
|
+
// Supports batch mode: when no issueNumber is provided, reviews up to 3 issues.
|
|
7
8
|
|
|
8
9
|
import * as core from "@actions/core";
|
|
9
10
|
import { runCopilotTask, scanDirectory } from "../copilot.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Review
|
|
13
|
+
* Review a single issue against the current codebase.
|
|
13
14
|
*
|
|
14
|
-
* @param {Object}
|
|
15
|
+
* @param {Object} params
|
|
16
|
+
* @param {Object} params.octokit - GitHub API client
|
|
17
|
+
* @param {Object} params.repo - { owner, repo }
|
|
18
|
+
* @param {Object} params.config - Loaded config
|
|
19
|
+
* @param {number} params.targetIssueNumber - Issue number to review
|
|
20
|
+
* @param {string} params.instructions - Agent instructions
|
|
21
|
+
* @param {string} params.model - Model name
|
|
15
22
|
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
23
|
*/
|
|
17
|
-
|
|
18
|
-
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
19
|
-
|
|
20
|
-
// If no specific issue, review the oldest open automated issue that hasn't been recently reviewed
|
|
21
|
-
let targetIssueNumber = issueNumber;
|
|
22
|
-
if (!targetIssueNumber) {
|
|
23
|
-
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
24
|
-
...repo,
|
|
25
|
-
state: "open",
|
|
26
|
-
labels: "automated",
|
|
27
|
-
per_page: 5,
|
|
28
|
-
sort: "created",
|
|
29
|
-
direction: "asc",
|
|
30
|
-
});
|
|
31
|
-
if (openIssues.length === 0) {
|
|
32
|
-
return { outcome: "nop", details: "No open automated issues to review" };
|
|
33
|
-
}
|
|
34
|
-
// Try each issue, skipping ones that already have a recent automated review comment
|
|
35
|
-
for (const candidate of openIssues) {
|
|
36
|
-
const { data: comments } = await octokit.rest.issues.listComments({
|
|
37
|
-
...repo,
|
|
38
|
-
issue_number: candidate.number,
|
|
39
|
-
per_page: 5,
|
|
40
|
-
sort: "created",
|
|
41
|
-
direction: "desc",
|
|
42
|
-
});
|
|
43
|
-
const hasRecentReview = comments.some(
|
|
44
|
-
(c) => c.body?.includes("**Automated Review Result:**") && Date.now() - new Date(c.created_at).getTime() < 86400000,
|
|
45
|
-
);
|
|
46
|
-
if (!hasRecentReview) {
|
|
47
|
-
targetIssueNumber = candidate.number;
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Fall back to the oldest if all have been recently reviewed
|
|
52
|
-
if (!targetIssueNumber) {
|
|
53
|
-
targetIssueNumber = openIssues[0].number;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
24
|
+
async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, instructions, model }) {
|
|
57
25
|
const { data: issue } = await octokit.rest.issues.get({
|
|
58
26
|
...repo,
|
|
59
27
|
issue_number: Number(targetIssueNumber),
|
|
@@ -64,13 +32,19 @@ export async function reviewIssue(context) {
|
|
|
64
32
|
}
|
|
65
33
|
|
|
66
34
|
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
67
|
-
contentLimit:
|
|
35
|
+
contentLimit: 5000,
|
|
36
|
+
fileLimit: 20,
|
|
68
37
|
recursive: true,
|
|
69
38
|
});
|
|
70
39
|
const testFiles = scanDirectory(config.paths.tests.path, [".test.js", ".test.ts"], {
|
|
71
|
-
contentLimit:
|
|
40
|
+
contentLimit: 5000,
|
|
41
|
+
fileLimit: 20,
|
|
72
42
|
recursive: true,
|
|
73
43
|
});
|
|
44
|
+
const docsFiles = scanDirectory(config.paths.documentation?.path || "docs/", [".md"], {
|
|
45
|
+
fileLimit: 10,
|
|
46
|
+
contentLimit: 2000,
|
|
47
|
+
});
|
|
74
48
|
|
|
75
49
|
const agentInstructions = instructions || "Review whether this issue has been resolved by the current codebase.";
|
|
76
50
|
|
|
@@ -87,6 +61,9 @@ export async function reviewIssue(context) {
|
|
|
87
61
|
`## Current Tests (${testFiles.length} files)`,
|
|
88
62
|
...testFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
89
63
|
"",
|
|
64
|
+
...(docsFiles.length > 0
|
|
65
|
+
? [`## Documentation (${docsFiles.length} files)`, ...docsFiles.map((f) => `- ${f.name}`), ""]
|
|
66
|
+
: []),
|
|
90
67
|
config.configToml ? `## Configuration (agentic-lib.toml)\n\`\`\`toml\n${config.configToml}\n\`\`\`` : "",
|
|
91
68
|
config.packageJson ? `## Dependencies (package.json)\n\`\`\`json\n${config.packageJson}\n\`\`\`` : "",
|
|
92
69
|
"",
|
|
@@ -97,7 +74,13 @@ export async function reviewIssue(context) {
|
|
|
97
74
|
'- "OPEN: <reason>" if the issue is not yet resolved',
|
|
98
75
|
].join("\n");
|
|
99
76
|
|
|
100
|
-
const {
|
|
77
|
+
const {
|
|
78
|
+
content: verdict,
|
|
79
|
+
tokensUsed,
|
|
80
|
+
inputTokens,
|
|
81
|
+
outputTokens,
|
|
82
|
+
cost,
|
|
83
|
+
} = await runCopilotTask({
|
|
101
84
|
model,
|
|
102
85
|
systemMessage: "You are a code reviewer determining if GitHub issues have been resolved.",
|
|
103
86
|
prompt,
|
|
@@ -150,3 +133,98 @@ export async function reviewIssue(context) {
|
|
|
150
133
|
details: `Issue #${targetIssueNumber} remains open: ${verdict.substring(0, 200)}`,
|
|
151
134
|
};
|
|
152
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Find unreviewed automated issues (no recent automated review comment).
|
|
139
|
+
*/
|
|
140
|
+
async function findUnreviewedIssues(octokit, repo, limit) {
|
|
141
|
+
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
142
|
+
...repo,
|
|
143
|
+
state: "open",
|
|
144
|
+
labels: "automated",
|
|
145
|
+
per_page: limit + 5,
|
|
146
|
+
sort: "created",
|
|
147
|
+
direction: "asc",
|
|
148
|
+
});
|
|
149
|
+
if (openIssues.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
const unreviewed = [];
|
|
152
|
+
for (const candidate of openIssues) {
|
|
153
|
+
if (unreviewed.length >= limit) break;
|
|
154
|
+
const { data: comments } = await octokit.rest.issues.listComments({
|
|
155
|
+
...repo,
|
|
156
|
+
issue_number: candidate.number,
|
|
157
|
+
per_page: 5,
|
|
158
|
+
sort: "created",
|
|
159
|
+
direction: "desc",
|
|
160
|
+
});
|
|
161
|
+
const hasRecentReview = comments.some(
|
|
162
|
+
(c) =>
|
|
163
|
+
c.body?.includes("**Automated Review Result:**") && Date.now() - new Date(c.created_at).getTime() < 86400000,
|
|
164
|
+
);
|
|
165
|
+
if (!hasRecentReview) {
|
|
166
|
+
unreviewed.push(candidate.number);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fall back to oldest if all have been recently reviewed
|
|
171
|
+
if (unreviewed.length === 0 && openIssues.length > 0) {
|
|
172
|
+
unreviewed.push(openIssues[0].number);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return unreviewed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Review open issues and close those that have been resolved.
|
|
180
|
+
* When no issueNumber is provided, reviews up to 3 issues in batch mode.
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} context - Task context from index.js
|
|
183
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
184
|
+
*/
|
|
185
|
+
export async function reviewIssue(context) {
|
|
186
|
+
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
187
|
+
|
|
188
|
+
// Single issue mode
|
|
189
|
+
if (issueNumber) {
|
|
190
|
+
return reviewSingleIssue({ octokit, repo, config, targetIssueNumber: issueNumber, instructions, model });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Batch mode: find up to 3 unreviewed issues
|
|
194
|
+
const issueNumbers = await findUnreviewedIssues(octokit, repo, 3);
|
|
195
|
+
if (issueNumbers.length === 0) {
|
|
196
|
+
return { outcome: "nop", details: "No open automated issues to review" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const results = [];
|
|
200
|
+
let totalTokens = 0;
|
|
201
|
+
let totalInputTokens = 0;
|
|
202
|
+
let totalOutputTokens = 0;
|
|
203
|
+
let totalCost = 0;
|
|
204
|
+
|
|
205
|
+
for (const num of issueNumbers) {
|
|
206
|
+
core.info(`Batch reviewing issue #${num} (${results.length + 1}/${issueNumbers.length})`);
|
|
207
|
+
const result = await reviewSingleIssue({ octokit, repo, config, targetIssueNumber: num, instructions, model });
|
|
208
|
+
results.push(result);
|
|
209
|
+
totalTokens += result.tokensUsed || 0;
|
|
210
|
+
totalInputTokens += result.inputTokens || 0;
|
|
211
|
+
totalOutputTokens += result.outputTokens || 0;
|
|
212
|
+
totalCost += result.cost || 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const closed = results.filter((r) => r.outcome === "issue-closed").length;
|
|
216
|
+
const reviewed = results.length;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
outcome: closed > 0 ? "issues-closed" : "issues-reviewed",
|
|
220
|
+
tokensUsed: totalTokens,
|
|
221
|
+
inputTokens: totalInputTokens,
|
|
222
|
+
outputTokens: totalOutputTokens,
|
|
223
|
+
cost: totalCost,
|
|
224
|
+
model,
|
|
225
|
+
details: `Batch reviewed ${reviewed} issues, closed ${closed}. ${results
|
|
226
|
+
.map((r) => r.details)
|
|
227
|
+
.join("; ")
|
|
228
|
+
.substring(0, 500)}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -29,16 +29,16 @@ async function gatherContext(octokit, repo, config) {
|
|
|
29
29
|
...repo,
|
|
30
30
|
state: "open",
|
|
31
31
|
per_page: 20,
|
|
32
|
-
sort: "
|
|
33
|
-
direction: "
|
|
32
|
+
sort: "created",
|
|
33
|
+
direction: "asc",
|
|
34
|
+
});
|
|
35
|
+
const issuesOnly = openIssues.filter((i) => !i.pull_request);
|
|
36
|
+
const oldestReadyIssue = issuesOnly.find((i) => i.labels.some((l) => l.name === "ready"));
|
|
37
|
+
const issuesSummary = issuesOnly.map((i) => {
|
|
38
|
+
const age = Math.floor((Date.now() - new Date(i.created_at).getTime()) / 86400000);
|
|
39
|
+
const labels = i.labels.map((l) => l.name).join(", ");
|
|
40
|
+
return `#${i.number}: ${i.title} [${labels || "no labels"}] (${age}d old)`;
|
|
34
41
|
});
|
|
35
|
-
const issuesSummary = openIssues
|
|
36
|
-
.filter((i) => !i.pull_request)
|
|
37
|
-
.map((i) => {
|
|
38
|
-
const age = Math.floor((Date.now() - new Date(i.created_at).getTime()) / 86400000);
|
|
39
|
-
const labels = i.labels.map((l) => l.name).join(", ");
|
|
40
|
-
return `#${i.number}: ${i.title} [${labels || "no labels"}] (${age}d old)`;
|
|
41
|
-
});
|
|
42
42
|
|
|
43
43
|
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
44
44
|
...repo,
|
|
@@ -72,6 +72,7 @@ async function gatherContext(octokit, repo, config) {
|
|
|
72
72
|
libraryNames,
|
|
73
73
|
libraryLimit,
|
|
74
74
|
issuesSummary,
|
|
75
|
+
oldestReadyIssue,
|
|
75
76
|
prsSummary,
|
|
76
77
|
workflowsSummary,
|
|
77
78
|
supervisor: config.supervisor,
|
|
@@ -117,6 +118,9 @@ function buildPrompt(ctx, agentInstructions) {
|
|
|
117
118
|
"```",
|
|
118
119
|
"",
|
|
119
120
|
...(ctx.packageJson ? ["### Dependencies (package.json)", "```json", ctx.packageJson, "```", ""] : []),
|
|
121
|
+
...(ctx.oldestReadyIssue
|
|
122
|
+
? [`### Oldest Ready Issue`, `#${ctx.oldestReadyIssue.number}: ${ctx.oldestReadyIssue.title}`, ""]
|
|
123
|
+
: []),
|
|
120
124
|
`### Issue Limits`,
|
|
121
125
|
`Feature development WIP limit: ${ctx.featureIssuesWipLimit}`,
|
|
122
126
|
`Maintenance WIP limit: ${ctx.maintenanceIssuesWipLimit}`,
|
|
@@ -126,7 +130,7 @@ function buildPrompt(ctx, agentInstructions) {
|
|
|
126
130
|
"Pick one or more actions. Output them in the format below.",
|
|
127
131
|
"",
|
|
128
132
|
"### Workflow Dispatches",
|
|
129
|
-
"- `dispatch:agent-flow-transform
|
|
133
|
+
"- `dispatch:agent-flow-transform | issue-number: <N>` — Pick up issue #N, generate code, open PR. Always specify the issue-number of the oldest ready issue.",
|
|
130
134
|
"- `dispatch:agent-flow-maintain` — Refresh feature definitions and library docs",
|
|
131
135
|
"- `dispatch:agent-flow-review` — Close resolved issues, enhance issue criteria",
|
|
132
136
|
"- `dispatch:agent-flow-fix-code | pr-number: <N>` — Fix a failing PR",
|
|
@@ -190,6 +194,26 @@ async function executeDispatch(octokit, repo, actionName, params) {
|
|
|
190
194
|
const workflowFile = actionName.replace("dispatch:", "") + ".yml";
|
|
191
195
|
const inputs = {};
|
|
192
196
|
if (params["pr-number"]) inputs["pr-number"] = params["pr-number"];
|
|
197
|
+
if (params["issue-number"]) inputs["issue-number"] = params["issue-number"];
|
|
198
|
+
|
|
199
|
+
// Guard: skip transform dispatch if one is already running
|
|
200
|
+
if (workflowFile === "agent-flow-transform.yml") {
|
|
201
|
+
try {
|
|
202
|
+
const { data: runs } = await octokit.rest.actions.listWorkflowRuns({
|
|
203
|
+
...repo,
|
|
204
|
+
workflow_id: "agent-flow-transform.yml",
|
|
205
|
+
status: "in_progress",
|
|
206
|
+
per_page: 1,
|
|
207
|
+
});
|
|
208
|
+
if (runs.total_count > 0) {
|
|
209
|
+
core.info("Transform workflow already running — skipping dispatch");
|
|
210
|
+
return "skipped:transform-already-running";
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
core.warning(`Could not check transform status: ${err.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
193
217
|
core.info(`Dispatching workflow: ${workflowFile}`);
|
|
194
218
|
await octokit.rest.actions.createWorkflowDispatch({ ...repo, workflow_id: workflowFile, ref: "main", inputs });
|
|
195
219
|
return `dispatched:${workflowFile}`;
|
|
@@ -229,11 +253,13 @@ async function executeRespondDiscussions(octokit, repo, params) {
|
|
|
229
253
|
const url = params["discussion-url"] || "";
|
|
230
254
|
if (message) {
|
|
231
255
|
core.info(`Dispatching discussions bot with response: ${message.substring(0, 100)}`);
|
|
256
|
+
const inputs = { message };
|
|
257
|
+
if (url) inputs["discussion-url"] = url;
|
|
232
258
|
await octokit.rest.actions.createWorkflowDispatch({
|
|
233
259
|
...repo,
|
|
234
260
|
workflow_id: "agent-discussions-bot.yml",
|
|
235
261
|
ref: "main",
|
|
236
|
-
inputs
|
|
262
|
+
inputs,
|
|
237
263
|
});
|
|
238
264
|
return `respond-discussions:${url || "no-url"}`;
|
|
239
265
|
}
|
|
@@ -15,7 +15,7 @@ import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection } f
|
|
|
15
15
|
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
16
|
*/
|
|
17
17
|
export async function transform(context) {
|
|
18
|
-
const { config, instructions, writablePaths, testCommand, model, octokit, repo } = context;
|
|
18
|
+
const { config, instructions, writablePaths, testCommand, model, octokit, repo, issueNumber } = context;
|
|
19
19
|
|
|
20
20
|
// Read mission (required)
|
|
21
21
|
const mission = readOptionalFile(config.paths.mission.path);
|
|
@@ -26,7 +26,7 @@ export async function transform(context) {
|
|
|
26
26
|
|
|
27
27
|
const features = scanDirectory(config.paths.features.path, ".md");
|
|
28
28
|
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
29
|
-
contentLimit:
|
|
29
|
+
contentLimit: 5000,
|
|
30
30
|
recursive: true,
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -36,6 +36,20 @@ export async function transform(context) {
|
|
|
36
36
|
per_page: 20,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
// Fetch target issue if specified
|
|
40
|
+
let targetIssue = null;
|
|
41
|
+
if (issueNumber) {
|
|
42
|
+
try {
|
|
43
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
44
|
+
...repo,
|
|
45
|
+
issue_number: Number(issueNumber),
|
|
46
|
+
});
|
|
47
|
+
targetIssue = issue;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
core.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
const agentInstructions =
|
|
40
54
|
instructions || "Transform the repository toward its mission by identifying the next best action.";
|
|
41
55
|
const readOnlyPaths = config.readOnlyPaths;
|
|
@@ -60,17 +74,27 @@ export async function transform(context) {
|
|
|
60
74
|
"## Instructions",
|
|
61
75
|
agentInstructions,
|
|
62
76
|
"",
|
|
77
|
+
...(targetIssue
|
|
78
|
+
? [
|
|
79
|
+
`## Target Issue #${targetIssue.number}: ${targetIssue.title}`,
|
|
80
|
+
targetIssue.body || "(no description)",
|
|
81
|
+
`Labels: ${targetIssue.labels.map((l) => l.name).join(", ") || "none"}`,
|
|
82
|
+
"",
|
|
83
|
+
"**Focus your transformation on resolving this specific issue.**",
|
|
84
|
+
"",
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
63
87
|
"## Mission",
|
|
64
88
|
mission,
|
|
65
89
|
"",
|
|
66
90
|
`## Current Features (${features.length})`,
|
|
67
|
-
...features.map((f) => `### ${f.name}\n${f.content.substring(0,
|
|
91
|
+
...features.map((f) => `### ${f.name}\n${f.content.substring(0, 2000)}`),
|
|
68
92
|
"",
|
|
69
93
|
`## Current Source Files (${sourceFiles.length})`,
|
|
70
94
|
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
71
95
|
"",
|
|
72
96
|
`## Open Issues (${openIssues.length})`,
|
|
73
|
-
...openIssues.slice(0,
|
|
97
|
+
...openIssues.slice(0, 20).map((i) => `- #${i.number}: ${i.title}`),
|
|
74
98
|
"",
|
|
75
99
|
"## Output Artifacts",
|
|
76
100
|
"If your changes produce output artifacts (plots, visualizations, data files, usage examples),",
|
|
@@ -90,7 +114,13 @@ export async function transform(context) {
|
|
|
90
114
|
|
|
91
115
|
core.info(`Transform prompt length: ${prompt.length} chars`);
|
|
92
116
|
|
|
93
|
-
const {
|
|
117
|
+
const {
|
|
118
|
+
content: resultContent,
|
|
119
|
+
tokensUsed,
|
|
120
|
+
inputTokens,
|
|
121
|
+
outputTokens,
|
|
122
|
+
cost,
|
|
123
|
+
} = await runCopilotTask({
|
|
94
124
|
model,
|
|
95
125
|
systemMessage:
|
|
96
126
|
"You are an autonomous code transformation agent. Your goal is to advance the repository toward its mission by making the most impactful change possible in a single step.",
|
|
@@ -145,13 +175,13 @@ async function transformTdd({
|
|
|
145
175
|
mission,
|
|
146
176
|
"",
|
|
147
177
|
`## Current Features (${features.length})`,
|
|
148
|
-
...features.map((f) => `### ${f.name}\n${f.content.substring(0,
|
|
178
|
+
...features.map((f) => `### ${f.name}\n${f.content.substring(0, 2000)}`),
|
|
149
179
|
"",
|
|
150
180
|
`## Current Source Files (${sourceFiles.length})`,
|
|
151
181
|
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
152
182
|
"",
|
|
153
183
|
`## Open Issues (${openIssues.length})`,
|
|
154
|
-
...openIssues.slice(0,
|
|
184
|
+
...openIssues.slice(0, 20).map((i) => `- #${i.number}: ${i.title}`),
|
|
155
185
|
"",
|
|
156
186
|
formatPathsSection(writablePaths, readOnlyPaths, _config),
|
|
157
187
|
"",
|
|
@@ -64,6 +64,10 @@ When the transform agent has implemented all major features but minor polish rem
|
|
|
64
64
|
2. `set-schedule:weekly` — reduce to weekly maintenance check-ins
|
|
65
65
|
3. Check that `docs/` contains evidence of the library working before declaring done
|
|
66
66
|
|
|
67
|
+
## Prerequisites
|
|
68
|
+
|
|
69
|
+
- The `set-schedule` action requires a `WORKFLOW_TOKEN` secret (classic PAT with `workflow` scope) to push workflow file changes to main.
|
|
70
|
+
|
|
67
71
|
## Guidelines
|
|
68
72
|
|
|
69
73
|
- Pick multiple actions when appropriate — concurrent work is encouraged.
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"author": "",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@xn-intenton-z2a/agentic-lib": "^7.1.
|
|
17
|
+
"@xn-intenton-z2a/agentic-lib": "^7.1.50"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@vitest/coverage-v8": "^4.0.
|
|
21
|
-
"vitest": "^4.0.
|
|
20
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
21
|
+
"vitest": "^4.0.18"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=24.0.0"
|
|
@@ -92,29 +92,11 @@ jobs:
|
|
|
92
92
|
with:
|
|
93
93
|
node-version: "24"
|
|
94
94
|
|
|
95
|
-
- name: Find issue to enhance
|
|
96
|
-
id: find-issue
|
|
97
|
-
uses: actions/github-script@v7
|
|
98
|
-
with:
|
|
99
|
-
script: |
|
|
100
|
-
const { data: issues } = await github.rest.issues.listForRepo({
|
|
101
|
-
...context.repo,
|
|
102
|
-
state: 'open',
|
|
103
|
-
labels: 'automated',
|
|
104
|
-
per_page: 20,
|
|
105
|
-
sort: 'created',
|
|
106
|
-
direction: 'asc',
|
|
107
|
-
});
|
|
108
|
-
const unready = issues.filter(i => !i.labels.some(l => l.name === 'ready'));
|
|
109
|
-
core.setOutput('issueNumber', unready.length > 0 ? String(unready[0].number) : '');
|
|
110
|
-
|
|
111
95
|
- name: Install agentic-step dependencies
|
|
112
|
-
if: steps.find-issue.outputs.issueNumber != ''
|
|
113
96
|
working-directory: .github/agentic-lib/actions/agentic-step
|
|
114
97
|
run: npm ci
|
|
115
98
|
|
|
116
|
-
- name: Enhance
|
|
117
|
-
if: steps.find-issue.outputs.issueNumber != ''
|
|
99
|
+
- name: Enhance issues (batch)
|
|
118
100
|
uses: ./.github/agentic-lib/actions/agentic-step
|
|
119
101
|
env:
|
|
120
102
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -123,5 +105,4 @@ jobs:
|
|
|
123
105
|
task: "enhance-issue"
|
|
124
106
|
config: ${{ env.configPath }}
|
|
125
107
|
instructions: ".github/agentic-lib/agents/agent-ready-issue.md"
|
|
126
|
-
issue-number: ${{ steps.find-issue.outputs.issueNumber }}
|
|
127
108
|
model: ${{ needs.params.outputs.model }}
|
|
@@ -14,6 +14,11 @@ concurrency:
|
|
|
14
14
|
on:
|
|
15
15
|
workflow_dispatch:
|
|
16
16
|
inputs:
|
|
17
|
+
issue-number:
|
|
18
|
+
description: "Issue number to target (optional)"
|
|
19
|
+
type: string
|
|
20
|
+
required: false
|
|
21
|
+
default: ""
|
|
17
22
|
model:
|
|
18
23
|
description: "Copilot SDK model to use"
|
|
19
24
|
type: choice
|
|
@@ -53,6 +58,20 @@ jobs:
|
|
|
53
58
|
with:
|
|
54
59
|
fetch-depth: 0
|
|
55
60
|
|
|
61
|
+
- name: Determine branch name
|
|
62
|
+
id: branch
|
|
63
|
+
shell: bash
|
|
64
|
+
run: |
|
|
65
|
+
ISSUE_NUMBER="${{ inputs.issue-number }}"
|
|
66
|
+
if [ -n "$ISSUE_NUMBER" ]; then
|
|
67
|
+
BRANCH="agentic-lib-issue-${ISSUE_NUMBER}"
|
|
68
|
+
else
|
|
69
|
+
BRANCH="agentic-lib-transform-${{ github.run_id }}"
|
|
70
|
+
fi
|
|
71
|
+
git checkout -b "${BRANCH}" 2>/dev/null || git checkout "${BRANCH}"
|
|
72
|
+
echo "branchName=${BRANCH}" >> $GITHUB_OUTPUT
|
|
73
|
+
echo "issueNumber=${ISSUE_NUMBER}" >> $GITHUB_OUTPUT
|
|
74
|
+
|
|
56
75
|
- uses: actions/setup-node@v4
|
|
57
76
|
with:
|
|
58
77
|
node-version: "24"
|
|
@@ -86,10 +105,97 @@ jobs:
|
|
|
86
105
|
instructions: ".github/agentic-lib/agents/agent-issue-resolution.md"
|
|
87
106
|
test-command: "npm test"
|
|
88
107
|
model: ${{ needs.params.outputs.model }}
|
|
108
|
+
issue-number: ${{ inputs.issue-number }}
|
|
89
109
|
writable-paths: ${{ steps.config.outputs.writablePaths }}
|
|
90
110
|
|
|
91
111
|
- name: Commit and push changes
|
|
92
112
|
uses: ./.github/agentic-lib/actions/commit-if-changed
|
|
93
113
|
with:
|
|
94
114
|
commit-message: "agentic-step: transform"
|
|
95
|
-
push-ref: ${{
|
|
115
|
+
push-ref: ${{ steps.branch.outputs.branchName }}
|
|
116
|
+
outputs:
|
|
117
|
+
branchName: ${{ steps.branch.outputs.branchName }}
|
|
118
|
+
issueNumber: ${{ steps.branch.outputs.issueNumber }}
|
|
119
|
+
|
|
120
|
+
create-pr:
|
|
121
|
+
needs: [params, transform]
|
|
122
|
+
if: always() && needs.transform.result == 'success'
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
steps:
|
|
125
|
+
- name: Create PR if branch has changes
|
|
126
|
+
uses: actions/github-script@v7
|
|
127
|
+
with:
|
|
128
|
+
script: |
|
|
129
|
+
const owner = context.repo.owner;
|
|
130
|
+
const repo = context.repo.repo;
|
|
131
|
+
const branchName = '${{ needs.transform.outputs.branchName }}';
|
|
132
|
+
const issueNumber = '${{ needs.transform.outputs.issueNumber }}';
|
|
133
|
+
|
|
134
|
+
if (!branchName) {
|
|
135
|
+
core.info('No branch name — skipping PR creation');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if branch has commits ahead of main
|
|
140
|
+
try {
|
|
141
|
+
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
|
|
142
|
+
owner, repo,
|
|
143
|
+
basehead: `main...${branchName}`,
|
|
144
|
+
});
|
|
145
|
+
if (comparison.ahead_by === 0) {
|
|
146
|
+
core.info('Branch has no new commits — skipping PR creation');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
core.info(`Branch comparison failed (branch may not exist): ${err.message}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if PR already exists for this branch
|
|
155
|
+
const { data: existingPRs } = await github.rest.pulls.list({
|
|
156
|
+
owner, repo,
|
|
157
|
+
state: 'open',
|
|
158
|
+
head: `${owner}:${branchName}`,
|
|
159
|
+
per_page: 1,
|
|
160
|
+
});
|
|
161
|
+
if (existingPRs.length > 0) {
|
|
162
|
+
core.info(`PR already exists: #${existingPRs[0].number}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create PR
|
|
167
|
+
const title = issueNumber
|
|
168
|
+
? `fix: resolve issue #${issueNumber}`
|
|
169
|
+
: `agentic-step: transform (run ${context.runId})`;
|
|
170
|
+
const body = issueNumber
|
|
171
|
+
? `Closes #${issueNumber}\n\nAutomated transformation targeting issue #${issueNumber}.`
|
|
172
|
+
: `Automated transformation run.`;
|
|
173
|
+
|
|
174
|
+
const { data: pr } = await github.rest.pulls.create({
|
|
175
|
+
owner, repo,
|
|
176
|
+
title,
|
|
177
|
+
body,
|
|
178
|
+
head: branchName,
|
|
179
|
+
base: 'main',
|
|
180
|
+
});
|
|
181
|
+
core.info(`Created PR #${pr.number}: ${pr.html_url}`);
|
|
182
|
+
|
|
183
|
+
// Add automerge label
|
|
184
|
+
await github.rest.issues.addLabels({
|
|
185
|
+
owner, repo,
|
|
186
|
+
issue_number: pr.number,
|
|
187
|
+
labels: ['automerge'],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Add in-progress label to issue if specified
|
|
191
|
+
if (issueNumber) {
|
|
192
|
+
try {
|
|
193
|
+
await github.rest.issues.addLabels({
|
|
194
|
+
owner, repo,
|
|
195
|
+
issue_number: parseInt(issueNumber),
|
|
196
|
+
labels: ['in-progress'],
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
core.warning(`Could not label issue #${issueNumber}: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -94,6 +94,20 @@ jobs:
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Always stamp the file with current date so the commit is never empty
|
|
98
|
+
// (GitHub re-registers cron schedules on push)
|
|
99
|
+
const dateStamp = `# Schedule updated: ${new Date().toISOString()}`;
|
|
100
|
+
const stampRegex = /^# Schedule updated: .*/m;
|
|
101
|
+
if (stampRegex.test(content)) {
|
|
102
|
+
content = content.replace(stampRegex, dateStamp);
|
|
103
|
+
} else {
|
|
104
|
+
// Insert after the header comment block
|
|
105
|
+
content = content.replace(
|
|
106
|
+
/^(# .*\n)+/m,
|
|
107
|
+
(match) => match + dateStamp + '\n'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
fs.writeFileSync(workflowPath, content);
|
|
98
112
|
core.info(`Updated supervisor schedule to: ${frequency} (cron: ${cron || 'none'})`);
|
|
99
113
|
|
|
@@ -146,3 +160,9 @@ jobs:
|
|
|
146
160
|
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
|
147
161
|
git commit -m "supervisor: set schedule to ${FREQUENCY}, model to ${MODEL:-gpt-5-mini}"
|
|
148
162
|
git push origin main
|
|
163
|
+
|
|
164
|
+
- name: Dispatch supervisor
|
|
165
|
+
if: inputs.frequency != 'off'
|
|
166
|
+
env:
|
|
167
|
+
GH_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
|
168
|
+
run: gh workflow run agent-supervisor.yml --repo $GITHUB_REPOSITORY
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
name: agent-supervisor
|
|
18
18
|
run-name: "agent-supervisor [${{ github.ref_name }}]"
|
|
19
|
+
concurrency:
|
|
20
|
+
group: agentic-lib-supervisor
|
|
21
|
+
cancel-in-progress: false
|
|
19
22
|
|
|
20
23
|
on:
|
|
21
24
|
workflow_run:
|
|
@@ -27,6 +30,7 @@ on:
|
|
|
27
30
|
- agent-flow-review
|
|
28
31
|
- ci-automerge
|
|
29
32
|
- init
|
|
33
|
+
- agent-discussions-bot
|
|
30
34
|
types:
|
|
31
35
|
- completed
|
|
32
36
|
workflow_dispatch:
|
|
@@ -50,7 +54,7 @@ permissions:
|
|
|
50
54
|
discussions: write
|
|
51
55
|
|
|
52
56
|
env:
|
|
53
|
-
maxFixAttempts: "3"
|
|
57
|
+
maxFixAttempts: "3" # Corresponds to limits.attempts-per-branch in agentic-lib.toml
|
|
54
58
|
stalePrDays: "3"
|
|
55
59
|
configPath: ".github/agentic-lib/agents/agentic-lib.yml"
|
|
56
60
|
|
|
@@ -190,7 +194,36 @@ jobs:
|
|
|
190
194
|
core.warning(`Post-transform review dispatch failed: ${err.message}`);
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
// ─── 4.
|
|
197
|
+
// ─── 4. Stuck automerge recovery ─────────────────────────────────
|
|
198
|
+
try {
|
|
199
|
+
const { data: openPRs2 } = await github.rest.pulls.list({
|
|
200
|
+
owner, repo, state: 'open',
|
|
201
|
+
per_page: 10,
|
|
202
|
+
});
|
|
203
|
+
for (const pr of openPRs2) {
|
|
204
|
+
const hasAutomerge = pr.labels.some(l => l.name === 'automerge');
|
|
205
|
+
if (!hasAutomerge) continue;
|
|
206
|
+
|
|
207
|
+
const { data: fullPr } = await github.rest.pulls.get({
|
|
208
|
+
owner, repo, pull_number: pr.number,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (fullPr.mergeable_state === 'clean' && fullPr.mergeable === true) {
|
|
212
|
+
core.info(`PR #${pr.number} is clean and mergeable but stuck — dispatching ci-automerge.`);
|
|
213
|
+
await github.rest.actions.createWorkflowDispatch({
|
|
214
|
+
owner, repo,
|
|
215
|
+
workflow_id: 'ci-automerge.yml',
|
|
216
|
+
ref: 'main',
|
|
217
|
+
inputs: {},
|
|
218
|
+
});
|
|
219
|
+
break; // One dispatch per evaluate cycle
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
core.warning(`Stuck automerge check failed: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── 5. Successful init → dispatch discussions bot to announce new mission ───
|
|
194
227
|
try {
|
|
195
228
|
if (workflowRun && workflowName === 'init' && conclusion === 'success') {
|
|
196
229
|
core.info('Init completed successfully — dispatching discussions bot to announce.');
|