@xn-intenton-z2a/agentic-lib 7.1.48 → 7.1.49

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.1.48",
3
+ "version": "7.1.49",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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(`${dirPath}${f}`, "utf8");
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}${f}: ${err.message}`);
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
- export async function enhanceIssue(context) {
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: "Issue already resolved" };
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: "Issue already has ready label" };
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: 500 });
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 { content: enhancedBody, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
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
- const failureDetails = failedChecks.map((cr) => `**${cr.name}:** ${cr.output?.summary || "Failed"}`).join("\n");
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 open issues and close those that have been resolved.
13
+ * Review a single issue against the current codebase.
13
14
  *
14
- * @param {Object} context - Task context from index.js
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
- export async function reviewIssue(context) {
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: 2000,
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: 2000,
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 { content: verdict, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
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: "updated",
33
- direction: "desc",
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` — Pick up next issue, generate code, open PR",
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: 2000,
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, 500)}`),
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, 10).map((i) => `- #${i.number}: ${i.title}`),
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 { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
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, 500)}`),
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, 10).map((i) => `- #${i.number}: ${i.title}`),
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,7 +14,7 @@
14
14
  "author": "",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@xn-intenton-z2a/agentic-lib": "^7.1.48"
17
+ "@xn-intenton-z2a/agentic-lib": "^7.1.49"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@vitest/coverage-v8": "^4.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 issue
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: ${{ github.ref_name }}
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:
@@ -50,7 +53,7 @@ permissions:
50
53
  discussions: write
51
54
 
52
55
  env:
53
- maxFixAttempts: "3"
56
+ maxFixAttempts: "3" # Corresponds to limits.attempts-per-branch in agentic-lib.toml
54
57
  stalePrDays: "3"
55
58
  configPath: ".github/agentic-lib/agents/agentic-lib.yml"
56
59
 
@@ -190,7 +193,36 @@ jobs:
190
193
  core.warning(`Post-transform review dispatch failed: ${err.message}`);
191
194
  }
192
195
 
193
- // ─── 4. Successful init dispatch discussions bot to announce new mission ───
196
+ // ─── 4. Stuck automerge recovery ─────────────────────────────────
197
+ try {
198
+ const { data: openPRs2 } = await github.rest.pulls.list({
199
+ owner, repo, state: 'open',
200
+ per_page: 10,
201
+ });
202
+ for (const pr of openPRs2) {
203
+ const hasAutomerge = pr.labels.some(l => l.name === 'automerge');
204
+ if (!hasAutomerge) continue;
205
+
206
+ const { data: fullPr } = await github.rest.pulls.get({
207
+ owner, repo, pull_number: pr.number,
208
+ });
209
+
210
+ if (fullPr.mergeable_state === 'clean' && fullPr.mergeable === true) {
211
+ core.info(`PR #${pr.number} is clean and mergeable but stuck — dispatching ci-automerge.`);
212
+ await github.rest.actions.createWorkflowDispatch({
213
+ owner, repo,
214
+ workflow_id: 'ci-automerge.yml',
215
+ ref: 'main',
216
+ inputs: {},
217
+ });
218
+ break; // One dispatch per evaluate cycle
219
+ }
220
+ }
221
+ } catch (err) {
222
+ core.warning(`Stuck automerge check failed: ${err.message}`);
223
+ }
224
+
225
+ // ─── 5. Successful init → dispatch discussions bot to announce new mission ───
194
226
  try {
195
227
  if (workflowRun && workflowName === 'init' && conclusion === 'success') {
196
228
  core.info('Init completed successfully — dispatching discussions bot to announce.');