@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,12 +2,41 @@
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
3
  // tasks/transform.js — Full mission → features → issues → code pipeline
4
4
  //
5
- // Reads the mission, analyzes the current state, identifies what to build next,
6
- // and either creates features, issues, or code.
5
+ // Uses runCopilotSession with lean prompts: the model explores via tools
6
+ // instead of having all context front-loaded into the prompt.
7
7
 
8
8
  import * as core from "@actions/core";
9
- import { writeFileSync, existsSync } from "fs";
10
- import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection, filterIssues, summariseIssue, extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION } from "../copilot.js";
9
+ import { existsSync, readdirSync, statSync } from "fs";
10
+ import { join, resolve } from "path";
11
+ import { readOptionalFile, formatPathsSection, 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) for the lean prompt.
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
+ const lines = Math.round(stat.size / 40); // rough estimate
30
+ return `${f} (~${lines} lines, ${stat.size} bytes)`;
31
+ } catch {
32
+ return String(f);
33
+ }
34
+ })
35
+ .slice(0, 30); // cap listing at 30 files
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
11
40
 
12
41
  /**
13
42
  * Run the full transformation pipeline from mission to code.
@@ -16,7 +45,7 @@ import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection, fi
16
45
  * @returns {Promise<Object>} Result with outcome, tokensUsed, model
17
46
  */
18
47
  export async function transform(context) {
19
- const { config, instructions, writablePaths, testCommand, model, octokit, repo, issueNumber } = context;
48
+ const { config, instructions, writablePaths, testCommand, model, octokit, repo, issueNumber, logFilePath, screenshotFilePath } = context;
20
49
  const t = config.tuning || {};
21
50
 
22
51
  // Read mission (required)
@@ -32,39 +61,21 @@ export async function transform(context) {
32
61
  return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
33
62
  }
34
63
 
35
- const features = scanDirectory(config.paths.features.path, ".md", { fileLimit: t.featuresScan || 10 });
36
- const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
37
- fileLimit: t.sourceScan || 10,
38
- contentLimit: t.sourceContent || 5000,
39
- recursive: true,
40
- sortByMtime: true,
41
- clean: true,
42
- outline: true,
43
- });
44
- const webFiles = scanDirectory(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], {
45
- fileLimit: t.sourceScan || 10,
46
- contentLimit: t.sourceContent || 5000,
47
- recursive: true,
48
- sortByMtime: true,
49
- clean: true,
50
- });
51
-
52
- const { data: rawIssues } = await octokit.rest.issues.listForRepo({
53
- ...repo,
54
- state: "open",
55
- per_page: t.issuesScan || 20,
56
- });
57
- const openIssues = filterIssues(rawIssues, { staleDays: t.staleDays || 30 });
58
-
59
64
  // Fetch target issue if specified
60
- let targetIssue = null;
65
+ let targetIssueSection = "";
61
66
  if (issueNumber) {
62
67
  try {
63
68
  const { data: issue } = await octokit.rest.issues.get({
64
69
  ...repo,
65
70
  issue_number: Number(issueNumber),
66
71
  });
67
- targetIssue = issue;
72
+ targetIssueSection = [
73
+ `## Target Issue #${issue.number}: ${issue.title}`,
74
+ issue.body || "(no description)",
75
+ `Labels: ${issue.labels.map((l) => l.name).join(", ") || "none"}`,
76
+ "",
77
+ "**Focus your transformation on resolving this specific issue.**",
78
+ ].join("\n");
68
79
  } catch (err) {
69
80
  core.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
70
81
  }
@@ -72,267 +83,104 @@ export async function transform(context) {
72
83
 
73
84
  const agentInstructions =
74
85
  instructions || "Transform the repository toward its mission by identifying the next best action.";
75
- const readOnlyPaths = config.readOnlyPaths;
76
86
 
77
- // TDD mode: split into test-first + implementation phases
78
- if (config.tdd === true) {
79
- return await transformTdd({
80
- config,
81
- instructions: agentInstructions,
82
- writablePaths,
83
- readOnlyPaths,
84
- testCommand,
85
- model,
86
- mission,
87
- features,
88
- sourceFiles,
89
- webFiles,
90
- openIssues,
91
- tuning: t,
92
- });
93
- }
87
+ // ── Build lean prompt (structure + mission, not file contents) ──────
88
+ const sourceFiles = buildFileListing(config.paths.source.path, [".js", ".ts"]);
89
+ const testFiles = buildFileListing(config.paths.tests.path, [".js", ".ts"]);
90
+ const webFiles = buildFileListing(config.paths.web?.path || "src/web/", [".html", ".css", ".js"]);
91
+ const featureFiles = buildFileListing(config.paths.features.path, [".md"]);
94
92
 
95
93
  const prompt = [
96
94
  "## Instructions",
97
95
  agentInstructions,
98
96
  "",
99
- ...(targetIssue
100
- ? [
101
- `## Target Issue #${targetIssue.number}: ${targetIssue.title}`,
102
- targetIssue.body || "(no description)",
103
- `Labels: ${targetIssue.labels.map((l) => l.name).join(", ") || "none"}`,
104
- "",
105
- "**Focus your transformation on resolving this specific issue.**",
106
- "",
107
- ]
108
- : []),
97
+ ...(targetIssueSection ? [targetIssueSection, ""] : []),
109
98
  "## Mission",
110
99
  mission,
111
100
  "",
112
- `## Current Features (${features.length})`,
113
- ...features.map((f) => `### ${f.name}\n${extractFeatureSummary(f.content, f.name)}`),
114
- "",
115
- `## Current Source Files (${sourceFiles.length})`,
116
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
117
- "",
118
- ...(webFiles.length > 0
119
- ? [
120
- `## Website Files (${webFiles.length})`,
121
- "The website in `src/web/` uses the JS library.",
122
- "When transforming source code, also update the website to use the library's new/changed features.",
123
- "`src/web/lib.js` re-exports from `../lib/main.js` so the page imports the real library directly. Keep `main.js` browser-safe (guard Node APIs behind `typeof process` checks). NEVER duplicate library functions inline in the web page.",
124
- ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
125
- "",
126
- ]
127
- : []),
128
- `## Open Issues (${openIssues.length})`,
129
- ...openIssues.slice(0, t.issuesScan || 20).map((i) => summariseIssue(i, t.issueBodyLimit || 500)),
130
- "",
131
- "## Output Artifacts",
132
- "If your changes produce output artifacts (plots, visualizations, data files, usage examples),",
133
- `save them to the \`${config.paths.examples?.path || "examples/"}\` directory.`,
134
- "This directory is for demonstrating what the code can do.",
101
+ "## Repository Structure",
102
+ `Source files (${sourceFiles.length}): ${sourceFiles.join(", ") || "none"}`,
103
+ `Test files (${testFiles.length}): ${testFiles.join(", ") || "none"}`,
104
+ `Features (${featureFiles.length}): ${featureFiles.join(", ") || "none"}`,
105
+ ...(webFiles.length > 0 ? [
106
+ `Website files (${webFiles.length}): ${webFiles.join(", ")}`,
107
+ "The website in `src/web/` uses the JS library. `src/web/lib.js` re-exports from `../lib/main.js`.",
108
+ "When transforming source code, also update the website to use the library's new/changed features.",
109
+ ] : []),
135
110
  "",
136
111
  "## Your Task",
137
- "Analyze the mission, features, source code, and open issues.",
112
+ "Analyze the mission and open issues (use list_issues tool).",
113
+ "Read the source files you need (use read_file tool).",
138
114
  "Determine the single most impactful next step to transform this repository.",
139
- "Then implement that step.",
115
+ "Then implement that step, writing files and running run_tests to verify.",
140
116
  "",
141
117
  "## When NOT to make changes",
142
118
  "If the existing code already satisfies all requirements in MISSION.md and all open issues have been addressed:",
143
119
  "- Do NOT make cosmetic changes (reformatting, renaming, reordering)",
144
120
  "- Do NOT add features beyond what MISSION.md specifies",
145
- "- Do NOT rewrite working code for stylistic preferences",
146
121
  "- Instead, report that the mission is satisfied and make no file changes",
147
122
  "",
148
- formatPathsSection(writablePaths, readOnlyPaths, config),
123
+ formatPathsSection(writablePaths, config.readOnlyPaths, config),
149
124
  "",
150
125
  "## Constraints",
151
- `- Run \`${testCommand}\` to validate your changes`,
126
+ `- Run \`${testCommand}\` via run_tests to validate your changes`,
127
+ "- Use list_issues to see open issues, get_issue for full details",
128
+ "- Use read_file to read source files you need (don't guess at contents)",
152
129
  ].join("\n");
153
130
 
154
- core.info(`Transform prompt length: ${prompt.length} chars`);
155
-
156
- const {
157
- content: resultContent,
158
- tokensUsed,
159
- inputTokens,
160
- outputTokens,
161
- cost,
162
- } = await runCopilotTask({
163
- model,
164
- systemMessage:
165
- "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." + NARRATIVE_INSTRUCTION,
166
- prompt,
167
- writablePaths,
168
- tuning: t,
169
- });
170
-
171
- core.info(`Transformation step completed (${tokensUsed} tokens)`);
131
+ core.info(`Transform lean prompt length: ${prompt.length} chars`);
172
132
 
173
- // Detect mission-complete hint: if the LLM indicates mission satisfaction, log it
174
- // but do NOT write MISSION_COMPLETE.md — the supervisor is the single authority
175
- // for mission lifecycle declarations (it also handles bot announcements)
176
- const lowerResult = resultContent.toLowerCase();
177
- if (lowerResult.includes("mission is satisfied") || lowerResult.includes("mission is complete") || lowerResult.includes("no changes needed")) {
178
- core.info("Transform indicates mission may be complete — supervisor will verify on next cycle");
133
+ // ── Build attachments (mission + log + screenshot) ─────────────────
134
+ const attachments = [];
135
+ const missionPath = resolve(config.paths.mission.path);
136
+ if (existsSync(missionPath)) {
137
+ attachments.push({ type: "file", path: missionPath });
179
138
  }
180
-
181
- const promptBudget = [
182
- { section: "mission", size: mission.length, files: "1", notes: "full" },
183
- { section: "features", size: features.reduce((s, f) => s + f.content.length, 0), files: `${features.length}`, notes: "" },
184
- { section: "source", size: sourceFiles.reduce((s, f) => s + f.content.length, 0), files: `${sourceFiles.length}`, notes: sourceFiles.some((f) => f.content.includes("// file:")) ? "some outlined" : "all full" },
185
- { section: "issues", size: openIssues.length * 80, files: `${openIssues.length}`, notes: `${rawIssues.length - openIssues.length} filtered` },
186
- ];
187
-
188
- return {
189
- outcome: "transformed",
190
- tokensUsed,
191
- inputTokens,
192
- outputTokens,
193
- cost,
194
- model,
195
- details: resultContent.substring(0, 500),
196
- narrative: extractNarrative(resultContent, "Transformation step completed."),
197
- promptBudget,
198
- contextNotes: `Transformed with ${sourceFiles.length} source files (mtime-sorted, cleaned), ${features.length} features, ${openIssues.length} issues (${rawIssues.length - openIssues.length} stale filtered).`,
139
+ if (logFilePath) attachments.push({ type: "file", path: logFilePath });
140
+ if (screenshotFilePath) attachments.push({ type: "file", path: screenshotFilePath });
141
+
142
+ // ── System prompt ──────────────────────────────────────────────────
143
+ const systemPrompt =
144
+ "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." + NARRATIVE_INSTRUCTION;
145
+
146
+ // ── Create custom tools (GitHub API + git) ─────────────────────────
147
+ const createTools = (defineTool, _wp, logger) => {
148
+ const ghTools = createGitHubTools(octokit, repo, defineTool, logger);
149
+ const gitTools = createGitTools(defineTool, logger);
150
+ return [...ghTools, ...gitTools];
199
151
  };
200
- }
201
-
202
- /**
203
- * TDD-mode transformation: Phase 1 creates a failing test, Phase 2 writes implementation.
204
- */
205
- async function transformTdd({
206
- config: _config,
207
- instructions,
208
- writablePaths,
209
- readOnlyPaths,
210
- testCommand,
211
- model,
212
- mission,
213
- features,
214
- sourceFiles,
215
- webFiles,
216
- openIssues,
217
- tuning: t,
218
- }) {
219
- let totalTokens = 0;
220
-
221
- // Phase 1: Create a failing test
222
- core.info("TDD Phase 1: Creating failing test");
223
-
224
- const testPrompt = [
225
- "## Instructions",
226
- instructions,
227
- "",
228
- "## Mode: TDD Phase 1 — Write Failing Test",
229
- "You are in TDD mode. In this phase, you must ONLY write a test.",
230
- "The test should capture the next feature requirement based on the mission and current state.",
231
- "The test MUST fail against the current codebase (it tests something not yet implemented).",
232
- "Do NOT write any implementation code in this phase.",
233
- "",
234
- "## Mission",
235
- mission,
236
- "",
237
- `## Current Features (${features.length})`,
238
- ...features.map((f) => `### ${f.name}\n${extractFeatureSummary(f.content, f.name)}`),
239
- "",
240
- `## Current Source Files (${sourceFiles.length})`,
241
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
242
- "",
243
- ...(webFiles.length > 0
244
- ? [
245
- `## Website Files (${webFiles.length})`,
246
- "The website in `src/web/` uses the JS library.",
247
- ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
248
- "",
249
- ]
250
- : []),
251
- `## Open Issues (${openIssues.length})`,
252
- ...openIssues.slice(0, t.issuesScan || 20).map((i) => summariseIssue(i, t.issueBodyLimit || 500)),
253
- "",
254
- formatPathsSection(writablePaths, readOnlyPaths, _config),
255
- "",
256
- "## Constraints",
257
- "- Write ONLY test code in this phase",
258
- "- The test must fail when run (it tests unimplemented functionality)",
259
- `- Run \`${testCommand}\` to confirm the test fails`,
260
- ].join("\n");
261
152
 
262
- const phase1 = await runCopilotTask({
153
+ // ── Run hybrid session ─────────────────────────────────────────────
154
+ const result = await runCopilotSession({
155
+ workspacePath: process.cwd(),
263
156
  model,
264
- systemMessage:
265
- "You are a TDD agent. In this phase, write ONLY a failing test that captures the next feature requirement. Do not write implementation code.",
266
- prompt: testPrompt,
267
- writablePaths,
268
157
  tuning: t,
269
- });
270
- totalTokens += phase1.tokensUsed;
271
- const testResult = phase1.content;
272
-
273
- core.info(`TDD Phase 1 completed (${totalTokens} tokens): test created`);
274
-
275
- // Phase 2: Write implementation to make the test pass
276
- core.info("TDD Phase 2: Writing implementation");
277
-
278
- const implPrompt = [
279
- "## Instructions",
280
- instructions,
281
- "",
282
- "## Mode: TDD Phase 2 — Write Implementation",
283
- "A failing test has been written in Phase 1. Your job is to write the MINIMUM implementation",
284
- "code needed to make the test pass. Do not modify the test.",
285
- "",
286
- "## What was done in Phase 1",
287
- testResult.substring(0, 1000),
288
- "",
289
- "## Mission",
290
- mission,
291
- "",
292
- `## Current Source Files (${sourceFiles.length})`,
293
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
294
- "",
295
- ...(webFiles.length > 0
296
- ? [
297
- `## Website Files (${webFiles.length})`,
298
- "The website in `src/web/` uses the JS library. Update it to reflect any new features.",
299
- ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
300
- "",
301
- ]
302
- : []),
303
- formatPathsSection(writablePaths, readOnlyPaths, _config),
304
- "",
305
- "## Output Artifacts",
306
- "If your changes produce output artifacts (plots, visualizations, data files, usage examples),",
307
- `save them to the \`${_config.paths.examples?.path || "examples/"}\` directory.`,
308
- "This directory is for demonstrating what the code can do.",
309
- "",
310
- "## Constraints",
311
- "- Write implementation code to make the failing test pass",
312
- "- Do NOT modify the test file created in Phase 1",
313
- `- Run \`${testCommand}\` to confirm all tests pass`,
314
- ].join("\n");
315
-
316
- const phase2 = await runCopilotTask({
317
- model,
318
- systemMessage:
319
- "You are a TDD agent. A failing test was written in Phase 1. Write the minimum implementation to make it pass. Do not modify the test." + NARRATIVE_INSTRUCTION,
320
- prompt: implPrompt,
158
+ agentPrompt: systemPrompt,
159
+ userPrompt: prompt,
321
160
  writablePaths,
322
- tuning: t,
161
+ createTools,
162
+ attachments,
163
+ excludedTools: ["dispatch_workflow", "close_issue", "label_issue", "post_discussion_comment"],
164
+ logger: { info: core.info, warning: core.warning, error: core.error, debug: core.debug },
323
165
  });
324
- totalTokens += phase2.tokensUsed;
325
166
 
326
- core.info(`TDD Phase 2 completed (total ${totalTokens} tokens)`);
167
+ core.info(`Transformation step completed (${result.tokensIn + result.tokensOut} tokens)`);
168
+
169
+ // Detect mission-complete hint
170
+ const lowerResult = (result.agentMessage || "").toLowerCase();
171
+ if (lowerResult.includes("mission is satisfied") || lowerResult.includes("mission is complete") || lowerResult.includes("no changes needed")) {
172
+ core.info("Transform indicates mission may be complete — supervisor will verify on next cycle");
173
+ }
327
174
 
328
175
  return {
329
- outcome: "transformed-tdd",
330
- tokensUsed: totalTokens,
331
- inputTokens: (phase1.inputTokens || 0) + (phase2.inputTokens || 0),
332
- outputTokens: (phase1.outputTokens || 0) + (phase2.outputTokens || 0),
333
- cost: (phase1.cost || 0) + (phase2.cost || 0),
176
+ outcome: result.testsPassed ? "transformed" : "transformed",
177
+ tokensUsed: result.tokensIn + result.tokensOut,
178
+ inputTokens: result.tokensIn,
179
+ outputTokens: result.tokensOut,
180
+ cost: 0,
334
181
  model,
335
- details: `TDD transformation: Phase 1 (failing test) + Phase 2 (implementation). ${testResult.substring(0, 200)}`,
336
- narrative: extractNarrative(phase2.content, "TDD transformation: wrote failing test then implementation."),
182
+ details: (result.agentMessage || "").substring(0, 500),
183
+ narrative: result.narrative || extractNarrative(result.agentMessage, "Transformation step completed."),
184
+ contextNotes: `Lean prompt transform: ${result.toolCalls} tool calls, ${result.testRuns} test runs, ${result.filesWritten} files written in ${result.sessionTime}s.`,
337
185
  };
338
186
  }
@@ -1,13 +1,13 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/agents.js — Load agent prompt files from src/agents/
3
+ // src/copilot/agents.js — Load agent prompt files from .github/agents/
4
4
 
5
5
  import { readFileSync, readdirSync } from "fs";
6
6
  import { resolve, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const agentsDir = resolve(__dirname, "..", "agents");
10
+ const agentsDir = resolve(__dirname, "..", "..", ".github", "agents");
11
11
 
12
12
  /**
13
13
  * Load an agent prompt file by name.
@@ -66,14 +66,8 @@ const FALLBACK_TUNING = {
66
66
  reasoningEffort: "medium",
67
67
  infiniteSessions: true,
68
68
  transformationBudget: 32,
69
- featuresScan: 10,
70
- sourceScan: 10,
71
- sourceContent: 5000,
72
- testContent: 3000,
73
69
  issuesScan: 20,
74
- issueBodyLimit: 500,
75
70
  staleDays: 30,
76
- documentSummary: 2000,
77
71
  discussionComments: 10,
78
72
  };
79
73
 
@@ -95,14 +89,8 @@ function parseTuningProfile(profileSection) {
95
89
  reasoningEffort: profileSection["reasoning-effort"] || "medium",
96
90
  infiniteSessions: profileSection["infinite-sessions"] ?? true,
97
91
  transformationBudget: profileSection["transformation-budget"] || 32,
98
- featuresScan: profileSection["max-feature-files"] || 10,
99
- sourceScan: profileSection["max-source-files"] || 10,
100
- sourceContent: profileSection["max-source-chars"] || 5000,
101
- testContent: profileSection["max-test-chars"] || 3000,
102
92
  issuesScan: profileSection["max-issues"] || 20,
103
- issueBodyLimit: profileSection["issue-body-limit"] || 500,
104
93
  staleDays: profileSection["stale-days"] || 30,
105
- documentSummary: profileSection["max-summary-chars"] || 2000,
106
94
  discussionComments: profileSection["max-discussion-comments"] || 10,
107
95
  };
108
96
  }
@@ -158,14 +146,8 @@ function resolveTuning(tuningSection, profilesSection) {
158
146
  }
159
147
  const numericOverrides = {
160
148
  "transformation-budget": "transformationBudget",
161
- "max-feature-files": "featuresScan",
162
- "max-source-files": "sourceScan",
163
- "max-source-chars": "sourceContent",
164
- "max-test-chars": "testContent",
165
149
  "max-issues": "issuesScan",
166
- "issue-body-limit": "issueBodyLimit",
167
150
  "stale-days": "staleDays",
168
- "max-summary-chars": "documentSummary",
169
151
  "max-discussion-comments": "discussionComments",
170
152
  };
171
153
  for (const [tomlKey, jsKey] of Object.entries(numericOverrides)) {
@@ -279,6 +261,8 @@ export function loadConfig(configPath) {
279
261
  seeding: toml.seeding || {},
280
262
  intentionBot: {
281
263
  intentionFilepath: bot["log-file"] || "intentïon.md",
264
+ logBranch: bot["log-branch"] || "agentic-lib-logs",
265
+ screenshotFile: bot["screenshot-file"] || "SCREENSHOT_INDEX.png",
282
266
  },
283
267
  init: toml.init || null,
284
268
  tdd: toml.tdd === true,
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/hybrid-session.js — Single-session hybrid iterator (Phase 2)
3
+ // src/copilot/copilot-session.js — Single-session Copilot SDK runner
4
4
  //
5
5
  // Replaces the old multi-session runIterationLoop with a single persistent
6
6
  // Copilot SDK session that drives its own tool loop. Hooks provide
@@ -63,10 +63,13 @@ function formatToolArgs(toolName, args) {
63
63
  * @param {string[]} [options.writablePaths] - Writable paths for tool safety (default: workspace)
64
64
  * @param {number} [options.maxRetries=2] - Max retries on rate-limit errors
65
65
  * @param {number} [options.maxToolCalls] - Max tool calls before graceful stop (undefined = unlimited)
66
+ * @param {Function} [options.createTools] - (defineTool, writablePaths, logger) => Tool[] — custom tool factory
67
+ * @param {string[]} [options.excludedTools] - Tool names to exclude from the session
68
+ * @param {Array} [options.attachments] - File/directory attachments for sendAndWait
66
69
  * @param {Object} [options.logger]
67
70
  * @returns {Promise<HybridResult>}
68
71
  */
69
- export async function runHybridSession({
72
+ export async function runCopilotSession({
70
73
  workspacePath,
71
74
  model = "gpt-5-mini",
72
75
  githubToken,
@@ -77,6 +80,9 @@ export async function runHybridSession({
77
80
  writablePaths,
78
81
  maxRetries = 2,
79
82
  maxToolCalls,
83
+ createTools,
84
+ excludedTools,
85
+ attachments,
80
86
  logger = defaultLogger,
81
87
  }) {
82
88
  const { CopilotClient, approveAll, defineTool } = await getSDK();
@@ -132,9 +138,10 @@ export async function runHybridSession({
132
138
  });
133
139
 
134
140
  // ── Build full tool set ─────────────────────────────────────────────
135
- // 4 standard tools (read_file, write_file, list_files, run_command) + run_tests
141
+ // 4 standard tools (read_file, write_file, list_files, run_command) + run_tests + custom
136
142
  const agentTools = createAgentTools(effectiveWritablePaths, logger, defineTool);
137
- const allTools = [...agentTools, runTestsTool];
143
+ const customTools = createTools ? createTools(defineTool, effectiveWritablePaths, logger) : [];
144
+ const allTools = [...agentTools, runTestsTool, ...customTools];
138
145
 
139
146
  // ── Build system prompt with narrative instruction ─────────────────
140
147
  const basePrompt = agentPrompt || [
@@ -155,16 +162,17 @@ export async function runHybridSession({
155
162
 
156
163
  const sessionConfig = {
157
164
  model,
158
- systemMessage: { mode: "replace", content: systemPrompt },
165
+ systemMessage: { mode: "append", content: systemPrompt },
159
166
  tools: allTools,
160
167
  onPermissionRequest: approveAll,
161
168
  workingDirectory: wsPath,
169
+ ...(excludedTools && excludedTools.length > 0 ? { excludedTools } : {}),
162
170
  hooks: {
163
171
  onPreToolUse: (input) => {
164
172
  // Enforce tool-call budget
165
173
  if (maxToolCalls && metrics.toolCalls.length >= maxToolCalls) {
166
174
  logger.warning(` [tool] Budget reached (${maxToolCalls} calls) — denying ${input.toolName}`);
167
- return { action: "deny", reason: `Tool call budget exhausted (${maxToolCalls} max). Wrap up your work.` };
175
+ return { permissionDecision: "deny", permissionDecisionReason: `Tool call budget exhausted (${maxToolCalls} max). Wrap up your work.` };
168
176
  }
169
177
  const n = metrics.toolCalls.length + 1;
170
178
  const elapsed = ((Date.now() - metrics.startTime) / 1000).toFixed(0);
@@ -173,11 +181,27 @@ export async function runHybridSession({
173
181
  logger.info(` [tool #${n} +${elapsed}s] ${input.toolName}${detail}`);
174
182
  },
175
183
  onPostToolUse: (input) => {
184
+ const hookOutput = {};
185
+
176
186
  if (/write|edit|create/i.test(input.toolName)) {
177
187
  const path = input.toolArgs?.file_path || input.toolArgs?.path || "unknown";
178
188
  metrics.filesWritten.add(path);
179
189
  logger.info(` → wrote ${path}`);
180
190
  }
191
+
192
+ // Truncate large read_file results to prevent context overflow
193
+ if (input.toolName === "read_file" || input.toolName === "view") {
194
+ const resultText = input.toolResult?.textResultForLlm || "";
195
+ const MAX_READ_CHARS = 20000;
196
+ if (resultText.length > MAX_READ_CHARS) {
197
+ hookOutput.modifiedResult = {
198
+ ...input.toolResult,
199
+ textResultForLlm: resultText.substring(0, MAX_READ_CHARS) + `\n... (truncated from ${resultText.length} chars)`,
200
+ };
201
+ logger.info(` → truncated read_file output: ${resultText.length} → ${MAX_READ_CHARS} chars`);
202
+ }
203
+ }
204
+
181
205
  if (input.toolName === "run_tests" || input.toolName === "run_command" || input.toolName === "bash") {
182
206
  const result = input.toolResult?.textResultForLlm || input.toolResult || "";
183
207
  const resultStr = typeof result === "string" ? result : JSON.stringify(result);
@@ -188,8 +212,12 @@ export async function runHybridSession({
188
212
  } else if (failed) {
189
213
  const failMatch = resultStr.match(/(\d+)\s*(failed|fail)/i);
190
214
  logger.info(` → tests FAILED${failMatch ? ` (${failMatch[1]} failures)` : ""}`);
215
+ // Inject guidance when tests fail
216
+ hookOutput.additionalContext = "Focus on the failing tests. Read the test file to understand expectations before changing source code.";
191
217
  }
192
218
  }
219
+
220
+ return Object.keys(hookOutput).length > 0 ? hookOutput : undefined;
193
221
  },
194
222
  onErrorOccurred: (input) => {
195
223
  metrics.errors.push({ error: input.error, context: input.errorContext, time: Date.now() });
@@ -281,9 +309,13 @@ export async function runHybridSession({
281
309
  let endReason = "complete";
282
310
 
283
311
  // Send with rate-limit retry
312
+ const sendOptions = { prompt };
313
+ if (attachments && attachments.length > 0) {
314
+ sendOptions.attachments = attachments;
315
+ }
284
316
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
285
317
  try {
286
- response = await session.sendAndWait({ prompt }, timeoutMs);
318
+ response = await session.sendAndWait(sendOptions, timeoutMs);
287
319
  break;
288
320
  } catch (err) {
289
321
  if (isRateLimitError(err) && attempt < maxRetries) {