@xn-intenton-z2a/agentic-lib 7.4.8 → 7.4.9

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.
Files changed (45) hide show
  1. package/{src → .github}/agents/agent-apply-fix.md +10 -0
  2. package/{src → .github}/agents/agent-director.md +10 -0
  3. package/{src → .github}/agents/agent-discovery.md +8 -0
  4. package/{src → .github}/agents/agent-discussion-bot.md +9 -0
  5. package/{src → .github}/agents/agent-issue-resolution.md +12 -0
  6. package/{src → .github}/agents/agent-iterate.md +8 -0
  7. package/{src → .github}/agents/agent-maintain-features.md +8 -0
  8. package/{src → .github}/agents/agent-maintain-library.md +7 -0
  9. package/{src → .github}/agents/agent-review-issue.md +8 -0
  10. package/{src → .github}/agents/agent-supervisor.md +9 -0
  11. package/.github/workflows/agentic-lib-test.yml +4 -2
  12. package/.github/workflows/agentic-lib-workflow.yml +70 -26
  13. package/README.md +5 -7
  14. package/agentic-lib.toml +16 -38
  15. package/bin/agentic-lib.js +49 -60
  16. package/package.json +3 -4
  17. package/src/actions/agentic-step/action.yml +1 -1
  18. package/src/actions/agentic-step/copilot.js +0 -5
  19. package/src/actions/agentic-step/index.js +8 -1
  20. package/src/actions/agentic-step/logging.js +14 -2
  21. package/src/actions/agentic-step/tasks/direct.js +86 -65
  22. package/src/actions/agentic-step/tasks/discussions.js +198 -264
  23. package/src/actions/agentic-step/tasks/enhance-issue.js +84 -33
  24. package/src/actions/agentic-step/tasks/fix-code.js +111 -57
  25. package/src/actions/agentic-step/tasks/maintain-features.js +69 -52
  26. package/src/actions/agentic-step/tasks/maintain-library.js +57 -19
  27. package/src/actions/agentic-step/tasks/resolve-issue.js +43 -18
  28. package/src/actions/agentic-step/tasks/review-issue.js +117 -117
  29. package/src/actions/agentic-step/tasks/supervise.js +140 -151
  30. package/src/actions/agentic-step/tasks/transform.js +106 -258
  31. package/src/copilot/agents.js +2 -2
  32. package/src/copilot/config.js +2 -18
  33. package/src/copilot/{hybrid-session.js → copilot-session.js} +39 -7
  34. package/src/copilot/github-tools.js +514 -0
  35. package/src/copilot/guards.js +1 -1
  36. package/src/copilot/session.js +0 -141
  37. package/src/copilot/tools.js +4 -0
  38. package/src/iterate.js +1 -1
  39. package/src/scripts/push-to-logs.sh +1 -1
  40. package/src/seeds/zero-SCREENSHOT_INDEX.png +0 -0
  41. package/src/seeds/zero-package.json +1 -1
  42. package/src/agents/agentic-lib.yml +0 -66
  43. package/src/copilot/context.js +0 -457
  44. package/src/mcp/server.js +0 -830
  45. /package/{src → .github}/agents/agent-ready-issue.md +0 -0
@@ -2,26 +2,45 @@
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
3
  // tasks/review-issue.js — Review issues and close resolved ones
4
4
  //
5
- // Checks open issues against the current codebase to determine
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.
5
+ // Uses runCopilotSession with lean prompts: the model reads source files
6
+ // via tools to determine if issues have been resolved.
8
7
 
9
8
  import * as core from "@actions/core";
10
- import { runCopilotTask, scanDirectory } from "../copilot.js";
9
+ import { existsSync, readdirSync, statSync } from "fs";
10
+ import { join } from "path";
11
+ import { extractNarrative, NARRATIVE_INSTRUCTION } from "../copilot.js";
12
+ import { runCopilotSession } from "../../../copilot/copilot-session.js";
13
+ import { createGitHubTools, createGitTools } from "../../../copilot/github-tools.js";
14
+
15
+ /**
16
+ * Build a file listing summary (names + sizes, not content).
17
+ */
18
+ function buildFileListing(dirPath, extensions) {
19
+ if (!dirPath || !existsSync(dirPath)) return [];
20
+ const exts = Array.isArray(extensions) ? extensions : [extensions];
21
+ try {
22
+ const files = readdirSync(dirPath, { recursive: true });
23
+ return files
24
+ .filter((f) => exts.some((ext) => String(f).endsWith(ext)))
25
+ .map((f) => {
26
+ const fullPath = join(dirPath, String(f));
27
+ try {
28
+ const stat = statSync(fullPath);
29
+ return `${f} (~${Math.round(stat.size / 40)} lines)`;
30
+ } catch {
31
+ return String(f);
32
+ }
33
+ })
34
+ .slice(0, 30);
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
11
39
 
12
40
  /**
13
41
  * Review a single issue against the current codebase.
14
- *
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
22
- * @returns {Promise<Object>} Result with outcome, tokensUsed, model
23
42
  */
24
- async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, instructions, model, tuning: t }) {
43
+ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, instructions, model, tuning: t, logFilePath, screenshotFilePath }) {
25
44
  const { data: issue } = await octokit.rest.issues.get({
26
45
  ...repo,
27
46
  issue_number: Number(targetIssueNumber),
@@ -31,52 +50,14 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
31
50
  return { outcome: "nop", details: `Issue #${targetIssueNumber} is already closed` };
32
51
  }
33
52
 
34
- const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
35
- contentLimit: t.sourceContent || 5000,
36
- fileLimit: t.sourceScan || 20,
37
- recursive: true,
38
- sortByMtime: true,
39
- clean: true,
40
- outline: true,
41
- });
42
- const testFiles = scanDirectory(config.paths.tests.path, [".test.js", ".test.ts"], {
43
- contentLimit: t.testContent || t.sourceContent || 5000,
44
- fileLimit: t.sourceScan || 20,
45
- recursive: true,
46
- sortByMtime: true,
47
- clean: true,
48
- });
49
- const webFiles = scanDirectory(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], {
50
- fileLimit: t.sourceScan || 20,
51
- contentLimit: t.sourceContent || 5000,
52
- recursive: true,
53
- sortByMtime: true,
54
- clean: true,
55
- });
56
- const docsFiles = scanDirectory(config.paths.documentation?.path || "docs/", [".md"], {
57
- fileLimit: t.featuresScan || 10,
58
- contentLimit: t.documentSummary || 2000,
59
- });
53
+ const sourceFiles = buildFileListing(config.paths.source.path, [".js", ".ts"]);
54
+ const testFiles = buildFileListing(config.paths.tests.path, [".js", ".ts"]);
55
+ const webFiles = buildFileListing(config.paths.web?.path || "src/web/", [".html", ".css", ".js"]);
60
56
 
61
57
  const agentInstructions = instructions || "Review whether this issue has been resolved by the current codebase.";
62
58
 
63
- // Gather recent commits since init for context
64
- let recentCommitsSummary = [];
65
- try {
66
- const initTimestamp = config.init?.timestamp || null;
67
- const since = initTimestamp || new Date(Date.now() - 7 * 86400000).toISOString();
68
- const { data: commits } = await octokit.rest.repos.listCommits({
69
- ...repo,
70
- sha: "main",
71
- since,
72
- per_page: 10,
73
- });
74
- recentCommitsSummary = commits.map((c) => {
75
- const msg = c.commit.message.split("\n")[0];
76
- const sha = c.sha.substring(0, 7);
77
- return `- [${sha}] ${msg} (${c.commit.author?.date || ""})`;
78
- });
79
- } catch { /* ignore */ }
59
+ // Shared mutable state to capture the verdict
60
+ const verdictResult = { verdict: "", resolved: false };
80
61
 
81
62
  const prompt = [
82
63
  "## Instructions",
@@ -85,53 +66,76 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
85
66
  `## Issue #${targetIssueNumber}: ${issue.title}`,
86
67
  issue.body || "(no description)",
87
68
  "",
88
- `## Current Source (${sourceFiles.length} files)`,
89
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
90
- "",
91
- `## Current Tests (${testFiles.length} files)`,
92
- ...testFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
93
- "",
94
- ...(webFiles.length > 0
95
- ? [
96
- `## Website Files (${webFiles.length} files)`,
97
- "The website in `src/web/` uses the JS library.",
98
- ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
99
- "",
100
- ]
101
- : []),
102
- ...(docsFiles.length > 0
103
- ? [`## Documentation (${docsFiles.length} files)`, ...docsFiles.map((f) => `- ${f.name}`), ""]
104
- : []),
105
- ...(recentCommitsSummary.length > 0
106
- ? [`## Recent Commits (since init)`, ...recentCommitsSummary, ""]
107
- : []),
108
- config.configToml ? `## Configuration (agentic-lib.toml)\n\`\`\`toml\n${config.configToml}\n\`\`\`` : "",
109
- config.packageJson ? `## Dependencies (package.json)\n\`\`\`json\n${config.packageJson}\n\`\`\`` : "",
69
+ "## Repository Structure",
70
+ `Source files (${sourceFiles.length}): ${sourceFiles.join(", ") || "none"}`,
71
+ `Test files (${testFiles.length}): ${testFiles.join(", ") || "none"}`,
72
+ ...(webFiles.length > 0 ? [`Website files (${webFiles.length}): ${webFiles.join(", ")}`] : []),
110
73
  "",
111
74
  "## Your Task",
112
- "Determine if this issue has been resolved by the current code.",
113
- "Respond with exactly one of:",
114
- '- "RESOLVED: <reason>" if the issue is satisfied by the current code',
115
- '- "OPEN: <reason>" if the issue is not yet resolved',
75
+ "Read the relevant source and test files using read_file to determine if this issue has been resolved.",
76
+ "Use git_diff or git_status for additional context if needed.",
77
+ "Then call report_verdict with your determination.",
78
+ "",
79
+ "**You MUST call report_verdict exactly once.**",
116
80
  ].join("\n");
117
81
 
118
- const {
119
- content: verdict,
120
- tokensUsed,
121
- inputTokens,
122
- outputTokens,
123
- cost,
124
- } = await runCopilotTask({
82
+ const systemPrompt =
83
+ "You are a code reviewer determining if a GitHub issue has been resolved by the current codebase. Read the source files, analyze them against the issue requirements, and report your verdict." +
84
+ NARRATIVE_INSTRUCTION;
85
+
86
+ const createTools = (defineTool, _wp, logger) => {
87
+ const ghTools = createGitHubTools(octokit, repo, defineTool, logger);
88
+ const gitTools = createGitTools(defineTool, logger);
89
+
90
+ const reportVerdict = defineTool("report_verdict", {
91
+ description: "Report whether the issue is resolved or still open. Call this exactly once.",
92
+ parameters: {
93
+ type: "object",
94
+ properties: {
95
+ resolved: { type: "boolean", description: "true if the issue is resolved, false if still open" },
96
+ reason: { type: "string", description: "Explanation of why the issue is or is not resolved" },
97
+ files_reviewed: { type: "number", description: "Number of source files read during review" },
98
+ },
99
+ required: ["resolved", "reason"],
100
+ },
101
+ handler: async ({ resolved, reason, files_reviewed }) => {
102
+ verdictResult.verdict = `${resolved ? "RESOLVED" : "OPEN"}: ${reason}`;
103
+ verdictResult.resolved = resolved;
104
+ verdictResult.filesReviewed = files_reviewed || 0;
105
+ return { textResultForLlm: `Verdict recorded: ${resolved ? "RESOLVED" : "OPEN"}` };
106
+ },
107
+ });
108
+
109
+ return [...ghTools, ...gitTools, reportVerdict];
110
+ };
111
+
112
+ const attachments = [];
113
+ if (logFilePath) attachments.push({ type: "file", path: logFilePath });
114
+ if (screenshotFilePath) attachments.push({ type: "file", path: screenshotFilePath });
115
+
116
+ const result = await runCopilotSession({
117
+ workspacePath: process.cwd(),
125
118
  model,
126
- systemMessage: "You are a code reviewer determining if GitHub issues have been resolved.",
127
- prompt,
128
- writablePaths: [],
129
119
  tuning: t,
120
+ agentPrompt: systemPrompt,
121
+ userPrompt: prompt,
122
+ writablePaths: [],
123
+ createTools,
124
+ attachments,
125
+ excludedTools: ["write_file", "run_command", "run_tests", "dispatch_workflow", "close_issue", "label_issue", "post_discussion_comment"],
126
+ logger: { info: core.info, warning: core.warning, error: core.error, debug: core.debug },
130
127
  });
131
128
 
132
- // Strip leading markdown formatting (e.g., **RESOLVED** or *RESOLVED*)
133
- const normalised = verdict.replace(/^[*_`#>\s-]+/, "").toUpperCase();
134
- if (normalised.startsWith("RESOLVED")) {
129
+ const tokensUsed = result.tokensIn + result.tokensOut;
130
+
131
+ // If the model didn't call report_verdict, try to infer from the message
132
+ if (!verdictResult.verdict && result.agentMessage) {
133
+ const normalised = result.agentMessage.replace(/^[*_`#>\s-]+/, "").toUpperCase();
134
+ verdictResult.resolved = normalised.startsWith("RESOLVED");
135
+ verdictResult.verdict = result.agentMessage.substring(0, 500);
136
+ }
137
+
138
+ if (verdictResult.resolved) {
135
139
  await octokit.rest.issues.createComment({
136
140
  ...repo,
137
141
  issue_number: Number(targetIssueNumber),
@@ -140,10 +144,9 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
140
144
  "",
141
145
  `**Task:** review-issue`,
142
146
  `**Model:** ${model}`,
143
- `**Source files reviewed:** ${sourceFiles.length}`,
144
- `**Test files reviewed:** ${testFiles.length}`,
147
+ `**Files reviewed:** ${verdictResult.filesReviewed || "unknown"}`,
145
148
  "",
146
- verdict,
149
+ verdictResult.verdict,
147
150
  ].join("\n"),
148
151
  });
149
152
  await octokit.rest.issues.update({
@@ -156,25 +159,25 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
156
159
  return {
157
160
  outcome: "issue-closed",
158
161
  tokensUsed,
159
- inputTokens,
160
- outputTokens,
161
- cost,
162
+ inputTokens: result.tokensIn,
163
+ outputTokens: result.tokensOut,
164
+ cost: 0,
162
165
  model,
163
- details: `Closed issue #${targetIssueNumber}: ${verdict.substring(0, 200)}`,
164
- narrative: `Reviewed issue #${targetIssueNumber} and closed it as resolved.`,
166
+ details: `Closed issue #${targetIssueNumber}: ${verdictResult.verdict.substring(0, 200)}`,
167
+ narrative: result.narrative || `Reviewed issue #${targetIssueNumber} and closed it as resolved.`,
165
168
  };
166
169
  }
167
170
 
168
- core.info(`Issue #${targetIssueNumber} still open: ${verdict.substring(0, 100)}`);
171
+ core.info(`Issue #${targetIssueNumber} still open: ${verdictResult.verdict.substring(0, 100)}`);
169
172
  return {
170
173
  outcome: "issue-still-open",
171
174
  tokensUsed,
172
- inputTokens,
173
- outputTokens,
174
- cost,
175
+ inputTokens: result.tokensIn,
176
+ outputTokens: result.tokensOut,
177
+ cost: 0,
175
178
  model,
176
- details: `Issue #${targetIssueNumber} remains open: ${verdict.substring(0, 200)}`,
177
- narrative: `Reviewed issue #${targetIssueNumber} — still open, not yet resolved.`,
179
+ details: `Issue #${targetIssueNumber} remains open: ${verdictResult.verdict.substring(0, 200)}`,
180
+ narrative: result.narrative || `Reviewed issue #${targetIssueNumber} — still open, not yet resolved.`,
178
181
  };
179
182
  }
180
183
 
@@ -221,18 +224,17 @@ async function findUnreviewedIssues(octokit, repo, limit) {
221
224
 
222
225
  /**
223
226
  * Review open issues and close those that have been resolved.
224
- * When no issueNumber is provided, reviews up to 3 issues in batch mode.
225
227
  *
226
228
  * @param {Object} context - Task context from index.js
227
229
  * @returns {Promise<Object>} Result with outcome, tokensUsed, model
228
230
  */
229
231
  export async function reviewIssue(context) {
230
- const { octokit, repo, config, issueNumber, instructions, model } = context;
232
+ const { octokit, repo, config, issueNumber, instructions, model, logFilePath, screenshotFilePath } = context;
231
233
  const t = config.tuning || {};
232
234
 
233
235
  // Single issue mode
234
236
  if (issueNumber) {
235
- return reviewSingleIssue({ octokit, repo, config, targetIssueNumber: issueNumber, instructions, model, tuning: t });
237
+ return reviewSingleIssue({ octokit, repo, config, targetIssueNumber: issueNumber, instructions, model, tuning: t, logFilePath, screenshotFilePath });
236
238
  }
237
239
 
238
240
  // Batch mode: find up to 3 unreviewed issues
@@ -245,18 +247,16 @@ export async function reviewIssue(context) {
245
247
  let totalTokens = 0;
246
248
  let totalInputTokens = 0;
247
249
  let totalOutputTokens = 0;
248
- let totalCost = 0;
249
250
 
250
251
  for (const num of issueNumbers) {
251
252
  core.info(`Batch reviewing issue #${num} (${results.length + 1}/${issueNumbers.length})`);
252
253
  const result = await reviewSingleIssue({
253
- octokit, repo, config, targetIssueNumber: num, instructions, model, tuning: t,
254
+ octokit, repo, config, targetIssueNumber: num, instructions, model, tuning: t, logFilePath, screenshotFilePath,
254
255
  });
255
256
  results.push(result);
256
257
  totalTokens += result.tokensUsed || 0;
257
258
  totalInputTokens += result.inputTokens || 0;
258
259
  totalOutputTokens += result.outputTokens || 0;
259
- totalCost += result.cost || 0;
260
260
  }
261
261
 
262
262
  const closed = results.filter((r) => r.outcome === "issue-closed").length;
@@ -267,7 +267,7 @@ export async function reviewIssue(context) {
267
267
  tokensUsed: totalTokens,
268
268
  inputTokens: totalInputTokens,
269
269
  outputTokens: totalOutputTokens,
270
- cost: totalCost,
270
+ cost: 0,
271
271
  model,
272
272
  details: `Batch reviewed ${reviewed} issues, closed ${closed}. ${results
273
273
  .map((r) => r.details)