@xn-intenton-z2a/agentic-lib 7.2.21 → 7.2.22

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.
@@ -30,6 +30,12 @@ const flags = args.slice(1);
30
30
 
31
31
  let initChanges = 0;
32
32
  const TASK_COMMANDS = ["transform", "maintain-features", "maintain-library", "fix-code"];
33
+ const TASK_AGENT_MAP = {
34
+ "transform": "agent-issue-resolution",
35
+ "fix-code": "agent-apply-fix",
36
+ "maintain-features": "agent-maintain-features",
37
+ "maintain-library": "agent-maintain-library",
38
+ };
33
39
  const INIT_COMMANDS = ["init", "update", "reset"];
34
40
  const ALL_COMMANDS = [...INIT_COMMANDS, ...TASK_COMMANDS, "version", "mcp", "iterate"];
35
41
 
@@ -366,21 +372,30 @@ async function runIterate() {
366
372
  // ─── Task Runner ─────────────────────────────────────────────────────
367
373
 
368
374
  async function runTask(taskName) {
375
+ // Task commands are now aliases for iterate --agent <agent-name>
376
+ const agentName = TASK_AGENT_MAP[taskName];
377
+ if (!agentName) {
378
+ console.error(`Unknown task: ${taskName}`);
379
+ return 1;
380
+ }
381
+
369
382
  console.log("");
370
- console.log(`=== agentic-lib ${taskName} ===`);
383
+ console.log(`=== agentic-lib ${taskName} (→ iterate --agent ${agentName}) ===`);
371
384
  console.log(`Target: ${target}`);
372
385
  console.log(`Model: ${model}`);
373
386
  console.log(`Dry-run: ${dryRun}`);
374
387
  console.log("");
375
388
 
376
- // Load config from shared module
377
389
  const { loadConfig } = await import("../src/copilot/config.js");
378
- const config = loadConfig(resolve(target, "agentic-lib.toml"));
379
- const effectiveModel = model || config.model;
390
+ let config;
391
+ try {
392
+ config = loadConfig(resolve(target, "agentic-lib.toml"));
393
+ } catch {
394
+ config = { tuning: {}, model: "gpt-5-mini", paths: {}, writablePaths: [], readOnlyPaths: [] };
395
+ }
396
+ const effectiveModel = model || config.model || "gpt-5-mini";
380
397
 
381
- console.log(`[config] supervisor=${config.supervisor}`);
382
- console.log(`[config] writable=${config.writablePaths.join(", ")}`);
383
- console.log(`[config] test=${config.testScript}`);
398
+ console.log(`[config] writable=${(config.writablePaths || []).join(", ")}`);
384
399
  console.log("");
385
400
 
386
401
  if (dryRun) {
@@ -388,62 +403,50 @@ async function runTask(taskName) {
388
403
  return 0;
389
404
  }
390
405
 
391
- // Change to target directory so relative paths in config work
392
- const originalCwd = process.cwd();
393
- process.chdir(target);
394
-
395
406
  try {
396
- const context = {
397
- config,
398
- writablePaths: config.writablePaths,
399
- model: effectiveModel,
400
- testCommand: config.testScript,
401
- logger: { info: console.log, warning: console.warn, error: console.error, debug: () => {} },
402
- };
403
-
404
- let result;
405
- switch (taskName) {
406
- case "transform": {
407
- const { transform } = await import("../src/copilot/tasks/transform.js");
408
- result = await transform(context);
409
- break;
410
- }
411
- case "maintain-features": {
412
- const { maintainFeatures } = await import("../src/copilot/tasks/maintain-features.js");
413
- result = await maintainFeatures(context);
414
- break;
415
- }
416
- case "maintain-library": {
417
- const { maintainLibrary } = await import("../src/copilot/tasks/maintain-library.js");
418
- result = await maintainLibrary(context);
419
- break;
420
- }
421
- case "fix-code": {
422
- const { fixCode } = await import("../src/copilot/tasks/fix-code.js");
423
- result = await fixCode(context);
424
- break;
425
- }
426
- default:
427
- console.error(`Unknown task: ${taskName}`);
428
- return 1;
407
+ const { loadAgentPrompt } = await import("../src/copilot/agents.js");
408
+ const { runHybridSession } = await import("../src/copilot/hybrid-session.js");
409
+ const { gatherLocalContext, gatherGitHubContext, buildUserPrompt } = await import("../src/copilot/context.js");
410
+
411
+ const agentPrompt = loadAgentPrompt(agentName);
412
+ const localContext = gatherLocalContext(target, config);
413
+
414
+ let githubContext;
415
+ if (issueNumber || prNumber) {
416
+ githubContext = gatherGitHubContext({
417
+ issueNumber: issueNumber || undefined,
418
+ prNumber: prNumber || undefined,
419
+ workspacePath: target,
420
+ });
429
421
  }
430
422
 
423
+ const userPrompt = buildUserPrompt(agentName, localContext, githubContext, { tuning: config.tuning });
424
+
425
+ const result = await runHybridSession({
426
+ workspacePath: target,
427
+ model: effectiveModel,
428
+ tuning: config.tuning || {},
429
+ timeoutMs,
430
+ agentPrompt,
431
+ userPrompt,
432
+ writablePaths: config.writablePaths?.length > 0 ? config.writablePaths : undefined,
433
+ });
434
+
431
435
  console.log("");
432
436
  console.log(`=== ${taskName} completed ===`);
433
- console.log(`Outcome: ${result.outcome}`);
434
- if (result.details) console.log(`Details: ${result.details}`);
435
- if (result.tokensUsed) console.log(`Tokens: ${result.tokensUsed} (in=${result.inputTokens} out=${result.outputTokens})`);
437
+ console.log(`Success: ${result.success}`);
438
+ console.log(`Session time: ${result.sessionTime}s`);
439
+ console.log(`Tool calls: ${result.toolCalls}`);
440
+ console.log(`Tokens: ${result.tokensIn + result.tokensOut} (in=${result.tokensIn} out=${result.tokensOut})`);
436
441
  if (result.narrative) console.log(`Narrative: ${result.narrative}`);
437
442
  console.log("");
438
- return 0;
443
+ return result.success ? 0 : 1;
439
444
  } catch (err) {
440
445
  console.error("");
441
446
  console.error(`=== ${taskName} FAILED ===`);
442
447
  console.error(err.message);
443
448
  if (err.stack) console.error(err.stack);
444
449
  return 1;
445
- } finally {
446
- process.chdir(originalCwd);
447
450
  }
448
451
  }
449
452
 
@@ -1229,6 +1232,7 @@ function runInit() {
1229
1232
 
1230
1233
  initWorkflows();
1231
1234
  initActions(agenticDir);
1235
+ initDirContents("copilot", resolve(agenticDir, "copilot"), "Copilot (shared modules)");
1232
1236
  initDirContents("agents", resolve(agenticDir, "agents"), "Agents");
1233
1237
  initDirContents("seeds", resolve(agenticDir, "seeds"), "Seeds");
1234
1238
  initScripts(agenticDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xn-intenton-z2a/agentic-lib",
3
- "version": "7.2.21",
3
+ "version": "7.2.22",
4
4
  "description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
- // config-loader.js — Parse agentic-lib.toml and resolve paths
3
+ // config-loader.js — Thin re-export from shared src/copilot/config.js
4
4
  //
5
- // TOML-only configuration. The config file is required.
6
- // All defaults are defined here in one place.
5
+ // Phase 4: Configuration logic now lives in src/copilot/config.js.
6
+ // This file re-exports for backwards compatibility with existing imports.
7
7
 
8
8
  import { readFileSync, existsSync } from "fs";
9
9
  import { dirname, join } from "path";
@@ -306,3 +306,4 @@ export function getWritablePaths(config, override) {
306
306
  }
307
307
  return config.writablePaths;
308
308
  }
309
+ export { loadConfig, getWritablePaths } from "../../copilot/config.js";
@@ -1,545 +1,63 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
- // copilot.js — Shared utilities for Copilot SDK task handlers
3
+ // copilot.js — Thin re-export layer from shared src/copilot/ module.
4
4
  //
5
- // Extracts repeated patterns from the 8 task handlers into reusable functions.
5
+ // Phase 4: All Copilot SDK logic now lives in src/copilot/.
6
+ // This file re-exports with an @actions/core logger for backwards compatibility
7
+ // with existing task handlers that import from "../copilot.js".
6
8
 
7
- import { CopilotClient, approveAll } from "@github/copilot-sdk";
8
- import { readFileSync, readdirSync, existsSync, statSync } from "fs";
9
- import { join } from "path";
10
- import { createAgentTools } from "./tools.js";
11
9
  import * as core from "@actions/core";
12
10
 
13
- // Models known to support the reasoningEffort SessionConfig parameter.
14
- // Updated from Copilot SDK ModelInfo.supportedReasoningEfforts (v0.1.30).
15
- // When in doubt, omit reasoning-effort — the SDK uses its default.
16
- const MODELS_SUPPORTING_REASONING_EFFORT = new Set(["gpt-5-mini", "o4-mini"]);
11
+ // Re-export pure functions directly no logger needed
12
+ export {
13
+ cleanSource,
14
+ generateOutline,
15
+ filterIssues,
16
+ summariseIssue,
17
+ extractFeatureSummary,
18
+ extractNarrative,
19
+ NARRATIVE_INSTRUCTION,
20
+ isRateLimitError,
21
+ retryDelayMs,
22
+ supportsReasoningEffort,
23
+ } from "../../copilot/session.js";
24
+
25
+ // Re-export path formatting directly — pure function
26
+ export { formatPathsSection } from "../../copilot/session.js";
27
+
28
+ // Logger that delegates to @actions/core
29
+ const actionsLogger = {
30
+ info: (...args) => core.info(args.join(" ")),
31
+ warning: (...args) => core.warning(args.join(" ")),
32
+ error: (...args) => core.error(args.join(" ")),
33
+ debug: (...args) => core.debug(args.join(" ")),
34
+ };
35
+
36
+ // Wrap functions that take a logger parameter to inject the @actions/core logger
37
+ import {
38
+ readOptionalFile as _readOptionalFile,
39
+ scanDirectory as _scanDirectory,
40
+ buildClientOptions as _buildClientOptions,
41
+ logTuningParam as _logTuningParam,
42
+ runCopilotTask as _runCopilotTask,
43
+ } from "../../copilot/session.js";
17
44
 
18
- /**
19
- * Strip noise from source code that has zero information value.
20
- * Removes license headers, collapses blank lines, strips linter directives.
21
- *
22
- * @param {string} raw - Raw source code
23
- * @returns {string} Cleaned source code
24
- */
25
- export function cleanSource(raw) {
26
- let cleaned = raw;
27
- cleaned = cleaned.replace(/^\/\/\s*SPDX-License-Identifier:.*\n/gm, "");
28
- cleaned = cleaned.replace(/^\/\/\s*Copyright.*\n/gm, "");
29
- cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
30
- cleaned = cleaned.replace(/^\s*\/\/\s*eslint-disable.*\n/gm, "");
31
- cleaned = cleaned.replace(/^\s*\/\*\s*eslint-disable[\s\S]*?\*\/\s*\n/gm, "");
32
- return cleaned.trimStart();
33
- }
34
-
35
- /**
36
- * Generate a structural outline of a source file using regex-based extraction.
37
- * Captures imports, exports, function/class declarations with line numbers.
38
- *
39
- * @param {string} raw - Raw source code
40
- * @param {string} filePath - File path for the header line
41
- * @returns {string} Structural outline
42
- */
43
- export function generateOutline(raw, filePath) {
44
- const lines = raw.split("\n");
45
- const sizeKB = (raw.length / 1024).toFixed(1);
46
- const parts = [`// file: ${filePath} (${lines.length} lines, ${sizeKB}KB)`];
47
-
48
- const importSources = [];
49
- for (const l of lines) {
50
- const m = l.match(/^import\s.*from\s+["']([^"']+)["']/);
51
- if (m) importSources.push(m[1]);
52
- }
53
- if (importSources.length > 0) parts.push(`// imports: ${importSources.join(", ")}`);
54
-
55
- const exportNames = [];
56
- for (const l of lines) {
57
- const m = l.match(/^export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/);
58
- if (m) exportNames.push(m[1]);
59
- }
60
- if (exportNames.length > 0) parts.push(`// exports: ${exportNames.join(", ")}`);
61
-
62
- parts.push("//");
63
-
64
- for (let i = 0; i < lines.length; i++) {
65
- const l = lines[i];
66
- const funcMatch = l.match(/^(export\s+)?(async\s+)?function\s+(\w+)\s*\(/);
67
- if (funcMatch) {
68
- parts.push(`// function ${funcMatch[3]}() — line ${i + 1}`);
69
- continue;
70
- }
71
- const classMatch = l.match(/^(export\s+)?(default\s+)?class\s+(\w+)/);
72
- if (classMatch) {
73
- parts.push(`// class ${classMatch[3]} — line ${i + 1}`);
74
- continue;
75
- }
76
- const methodMatch = l.match(/^\s+(async\s+)?(\w+)\s*\([^)]*\)\s*\{/);
77
- if (methodMatch && !["if", "for", "while", "switch", "catch", "try"].includes(methodMatch[2])) {
78
- parts.push(`// ${methodMatch[2]}() — line ${i + 1}`);
79
- }
80
- }
81
-
82
- return parts.join("\n");
83
- }
84
-
85
- /**
86
- * Filter issues by recency, init epoch, and label quality.
87
- *
88
- * @param {Array} issues - GitHub issue objects
89
- * @param {Object} [options]
90
- * @param {number} [options.staleDays=30] - Issues older than this with no activity are excluded
91
- * @param {boolean} [options.excludeBotOnly=true] - Exclude issues with only bot labels
92
- * @param {string} [options.initTimestamp] - ISO timestamp; exclude issues created before this epoch
93
- * @returns {Array} Filtered issues
94
- */
95
- export function filterIssues(issues, options = {}) {
96
- const { staleDays = 30, excludeBotOnly = true, initTimestamp } = options;
97
- const cutoff = Date.now() - staleDays * 86400000;
98
- const initEpoch = initTimestamp ? new Date(initTimestamp).getTime() : 0;
99
-
100
- return issues.filter((issue) => {
101
- // Exclude issues created before the most recent init
102
- if (initEpoch > 0) {
103
- const created = new Date(issue.created_at).getTime();
104
- if (created < initEpoch) return false;
105
- }
106
-
107
- const lastActivity = new Date(issue.updated_at || issue.created_at).getTime();
108
- if (lastActivity < cutoff) return false;
109
-
110
- if (excludeBotOnly) {
111
- const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name));
112
- const botLabels = ["automated", "stale", "bot", "wontfix"];
113
- if (labels.length > 0 && labels.every((l) => botLabels.includes(l))) return false;
114
- }
115
-
116
- return true;
117
- });
118
- }
119
-
120
- /**
121
- * Create a compact summary of an issue for inclusion in prompts.
122
- *
123
- * @param {Object} issue - GitHub issue object
124
- * @param {number} [bodyLimit=500] - Max chars for issue body
125
- * @returns {string} Compact issue summary
126
- */
127
- export function summariseIssue(issue, bodyLimit = 500) {
128
- const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name)).join(", ") || "no labels";
129
- const age = Math.floor((Date.now() - new Date(issue.created_at).getTime()) / 86400000);
130
- const body = (issue.body || "").substring(0, bodyLimit).replace(/\n+/g, " ").trim();
131
- return `#${issue.number}: ${issue.title} [${labels}] (${age}d old)${body ? `\n ${body}` : ""}`;
45
+ export function readOptionalFile(filePath, limit) {
46
+ return _readOptionalFile(filePath, limit);
132
47
  }
133
48
 
134
- /**
135
- * Extract a structured summary from a feature markdown file.
136
- *
137
- * @param {string} content - Feature file content
138
- * @param {string} fileName - Feature file name
139
- * @returns {string} Structured feature summary
140
- */
141
- export function extractFeatureSummary(content, fileName) {
142
- const lines = content.split("\n");
143
- const title = lines.find((l) => l.startsWith("#"))?.replace(/^#+\s*/, "") || fileName;
144
- const checked = (content.match(/- \[x\]/gi) || []).length;
145
- const unchecked = (content.match(/- \[ \]/g) || []).length;
146
- const total = checked + unchecked;
147
-
148
- const parts = [`Feature: ${title}`];
149
- if (total > 0) {
150
- parts.push(`Status: ${checked}/${total} items complete`);
151
- const remaining = [];
152
- for (const line of lines) {
153
- if (/- \[ \]/.test(line)) {
154
- remaining.push(line.replace(/^[\s-]*\[ \]\s*/, "").trim());
155
- }
156
- }
157
- if (remaining.length > 0) {
158
- parts.push(`Remaining: ${remaining.map((r) => `[ ] ${r}`).join(", ")}`);
159
- }
160
- }
161
- return parts.join("\n");
49
+ export function scanDirectory(dirPath, extensions, options = {}) {
50
+ return _scanDirectory(dirPath, extensions, options, actionsLogger);
162
51
  }
163
52
 
164
- /**
165
- * Build the CopilotClient options for authentication.
166
- *
167
- * Auth strategy (in order of preference):
168
- * 1. COPILOT_GITHUB_TOKEN env var → override GITHUB_TOKEN/GH_TOKEN in subprocess env
169
- * so the Copilot CLI's auto-login finds the PAT instead of the Actions token.
170
- * 2. Fall back to whatever auth is available (GITHUB_TOKEN, gh CLI login, etc.)
171
- *
172
- * Note: Passing githubToken directly to CopilotClient causes 400 on models.list.
173
- * Instead we override the env vars so the CLI subprocess picks up the right token
174
- * via its auto-login flow (useLoggedInUser: true).
175
- *
176
- * @param {string} [githubToken] - Optional token; falls back to COPILOT_GITHUB_TOKEN env var.
177
- */
178
53
  export function buildClientOptions(githubToken) {
179
- const copilotToken = githubToken || process.env.COPILOT_GITHUB_TOKEN;
180
- if (!copilotToken) {
181
- throw new Error("COPILOT_GITHUB_TOKEN is required. Set it as a repository secret.");
182
- }
183
- core.info("[copilot] COPILOT_GITHUB_TOKEN found — overriding subprocess env");
184
- const env = { ...process.env };
185
- // Override both GITHUB_TOKEN and GH_TOKEN so the Copilot CLI
186
- // subprocess uses the Copilot PAT for its auto-login flow
187
- env.GITHUB_TOKEN = copilotToken;
188
- env.GH_TOKEN = copilotToken;
189
- return { env };
54
+ return _buildClientOptions(githubToken, actionsLogger);
190
55
  }
191
56
 
192
- /**
193
- * Log tuning parameter application with profile context.
194
- *
195
- * @param {string} param - Parameter name
196
- * @param {*} value - Resolved value being applied
197
- * @param {string} profileName - Profile the default came from
198
- * @param {string} model - Model being used
199
- * @param {Object} [clip] - Optional clipping info { available, requested }
200
- */
201
57
  export function logTuningParam(param, value, profileName, model, clip) {
202
- const clipInfo = clip
203
- ? ` (requested=${clip.requested}, available=${clip.available}, excess=${clip.requested - clip.available})`
204
- : "";
205
- core.info(`[tuning] ${param}=${value} profile=${profileName} model=${model}${clipInfo}`);
206
- }
207
-
208
- /**
209
- * Check if a model supports reasoningEffort.
210
- * Uses the static allowlist; at runtime the SDK's models.list() could be used
211
- * but that requires an authenticated client which isn't available at config time.
212
- *
213
- * @param {string} model - Model name
214
- * @returns {boolean}
215
- */
216
- export function supportsReasoningEffort(model) {
217
- return MODELS_SUPPORTING_REASONING_EFFORT.has(model);
218
- }
219
-
220
- /**
221
- * Detect whether an error is an HTTP 429 Too Many Requests (rate limit) response.
222
- * The Copilot SDK may surface this as an Error with a status, statusCode, or message.
223
- *
224
- * @param {unknown} err - The error to test
225
- * @returns {boolean}
226
- */
227
- export function isRateLimitError(err) {
228
- if (!err || typeof err !== "object") return false;
229
- const status = err.status ?? err.statusCode ?? err.code;
230
- if (status === 429 || status === "429") return true;
231
- const msg = (err.message || "").toLowerCase();
232
- return msg.includes("429") || msg.includes("too many requests") || msg.includes("rate limit");
233
- }
234
-
235
- /**
236
- * Extract the retry delay in milliseconds from a rate-limit error.
237
- * Tries the Retry-After header (seconds) first, then falls back to exponential backoff.
238
- *
239
- * @param {unknown} err - The error (may have headers or retryAfter)
240
- * @param {number} attempt - Zero-based attempt number (0 = first retry)
241
- * @param {number} [baseDelayMs=60000] - Base delay for exponential backoff (ms)
242
- * @returns {number} Milliseconds to wait before retrying
243
- */
244
- export function retryDelayMs(err, attempt, baseDelayMs = 60000) {
245
- const retryAfterHeader =
246
- err?.headers?.["retry-after"] ?? err?.retryAfter ?? err?.response?.headers?.["retry-after"];
247
- if (retryAfterHeader != null) {
248
- const parsed = Number(retryAfterHeader);
249
- if (!isNaN(parsed) && parsed > 0) return parsed * 1000;
250
- }
251
- return baseDelayMs * Math.pow(2, attempt);
252
- }
253
-
254
- /**
255
- * Run a Copilot SDK session and return the response.
256
- * Handles the full lifecycle: create client → create session → send → stop.
257
- * Retries automatically on HTTP 429 rate-limit responses (up to maxRetries times).
258
- *
259
- * @param {Object} options
260
- * @param {string} options.model - Copilot SDK model name
261
- * @param {string} options.systemMessage - System message content
262
- * @param {string} options.prompt - The prompt to send
263
- * @param {string[]} options.writablePaths - Paths the agent may modify
264
- * @param {string} [options.githubToken] - Optional token; falls back to COPILOT_GITHUB_TOKEN env var.
265
- * @param {Object} [options.tuning] - Tuning config (reasoningEffort, infiniteSessions)
266
- * @param {string} [options.profileName] - Profile name for logging
267
- * @param {number} [options.maxRetries=3] - Maximum number of retry attempts on rate-limit errors
268
- * @returns {Promise<{content: string, tokensUsed: number}>}
269
- */
270
- export async function runCopilotTask({
271
- model,
272
- systemMessage,
273
- prompt,
274
- writablePaths,
275
- githubToken,
276
- tuning,
277
- profileName,
278
- maxRetries = 3,
279
- }) {
280
- const profile = profileName || tuning?.profileName || "unknown";
281
-
282
- // Attempt 0 is the initial call; attempts 1..maxRetries are retries after 429s.
283
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
284
- try {
285
- return await _runCopilotTaskOnce({ model, systemMessage, prompt, writablePaths, githubToken, tuning, profileName: profile });
286
- } catch (err) {
287
- if (isRateLimitError(err) && attempt < maxRetries) {
288
- const delayMs = retryDelayMs(err, attempt);
289
- core.warning(
290
- `[copilot] Rate limit (429) hit — waiting ${Math.round(delayMs / 1000)}s before retry ${attempt + 1}/${maxRetries}`,
291
- );
292
- await new Promise((resolve) => setTimeout(resolve, delayMs));
293
- } else {
294
- throw err;
295
- }
296
- }
297
- }
298
- // Unreachable: loop always returns or throws, but satisfies static analysis.
299
- throw new Error("[copilot] runCopilotTask: all retry attempts exhausted");
300
- }
301
-
302
- async function _runCopilotTaskOnce({
303
- model,
304
- systemMessage,
305
- prompt,
306
- writablePaths,
307
- githubToken,
308
- tuning,
309
- profileName,
310
- }) {
311
- const profile = profileName || tuning?.profileName || "unknown";
312
- core.info(
313
- `[copilot] Creating client (model=${model}, promptLen=${prompt.length}, writablePaths=${writablePaths.length}, tuning=${tuning?.reasoningEffort || "default"}, profile=${profile})`,
314
- );
315
-
316
- const clientOptions = buildClientOptions(githubToken);
317
- const client = new CopilotClient(clientOptions);
318
-
319
- try {
320
- core.info("[copilot] Creating session...");
321
- const sessionConfig = {
322
- model,
323
- systemMessage: { content: systemMessage },
324
- tools: createAgentTools(writablePaths),
325
- onPermissionRequest: approveAll,
326
- workingDirectory: process.cwd(),
327
- };
328
-
329
- // Only set reasoningEffort for models that support it
330
- if (tuning?.reasoningEffort && tuning.reasoningEffort !== "none") {
331
- if (supportsReasoningEffort(model)) {
332
- sessionConfig.reasoningEffort = tuning.reasoningEffort;
333
- logTuningParam("reasoningEffort", tuning.reasoningEffort, profile, model);
334
- } else {
335
- core.info(
336
- `[copilot] Skipping reasoningEffort="${tuning.reasoningEffort}" — not supported by model "${model}". Only supported by: ${[...MODELS_SUPPORTING_REASONING_EFFORT].join(", ")}`,
337
- );
338
- }
339
- }
340
-
341
- if (tuning?.infiniteSessions === true) {
342
- sessionConfig.infiniteSessions = {};
343
- logTuningParam("infiniteSessions", true, profile, model);
344
- }
345
-
346
- // Log scan/context tuning params
347
- if (tuning?.featuresScan) logTuningParam("featuresScan", tuning.featuresScan, profile, model);
348
- if (tuning?.sourceScan) logTuningParam("sourceScan", tuning.sourceScan, profile, model);
349
- if (tuning?.sourceContent) logTuningParam("sourceContent", tuning.sourceContent, profile, model);
350
- if (tuning?.issuesScan) logTuningParam("issuesScan", tuning.issuesScan, profile, model);
351
- if (tuning?.documentSummary) logTuningParam("documentSummary", tuning.documentSummary, profile, model);
352
- if (tuning?.discussionComments) logTuningParam("discussionComments", tuning.discussionComments, profile, model);
353
-
354
- const session = await client.createSession(sessionConfig);
355
- core.info(`[copilot] Session created: ${session.sessionId}`);
356
-
357
- // Check auth status now that client is connected
358
- try {
359
- const authStatus = await client.getAuthStatus();
360
- core.info(`[copilot] Auth status: ${JSON.stringify(authStatus)}`);
361
- } catch (authErr) {
362
- core.warning(`[copilot] Auth check failed: ${authErr.message}`);
363
- }
364
-
365
- // Accumulate usage from assistant.usage events (tokens are NOT on the sendAndWait response)
366
- let totalInputTokens = 0;
367
- let totalOutputTokens = 0;
368
- let totalCost = 0;
369
-
370
- // Register wildcard event handler for ALL events
371
- session.on((event) => {
372
- const eventType = event?.type || "unknown";
373
- if (eventType === "assistant.message") {
374
- const preview = event?.data?.content?.substring(0, 100) || "(no content)";
375
- core.info(`[copilot] event=${eventType}: ${preview}...`);
376
- } else if (eventType === "assistant.usage") {
377
- const d = event?.data || {};
378
- const input = d.inputTokens || 0;
379
- const output = d.outputTokens || 0;
380
- const cacheRead = d.cacheReadTokens || 0;
381
- const cost = d.cost || 0;
382
- totalInputTokens += input;
383
- totalOutputTokens += output;
384
- totalCost += cost;
385
- core.info(
386
- `[copilot] event=${eventType}: model=${d.model} input=${input} output=${output} cacheRead=${cacheRead} cost=${cost}`,
387
- );
388
- } else if (eventType === "session.idle") {
389
- core.info(`[copilot] event=${eventType}`);
390
- } else if (eventType === "session.error") {
391
- core.error(`[copilot] event=${eventType}: ${JSON.stringify(event?.data || event)}`);
392
- } else {
393
- core.info(`[copilot] event=${eventType}: ${JSON.stringify(event?.data || event).substring(0, 200)}`);
394
- }
395
- });
396
-
397
- core.info("[copilot] Sending prompt and waiting for idle...");
398
- const response = await session.sendAndWait({ prompt }, 600000);
399
- core.info(`[copilot] sendAndWait resolved`);
400
- const tokensUsed = totalInputTokens + totalOutputTokens;
401
- const content = response?.data?.content || "";
402
-
403
- return { content, tokensUsed, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, cost: totalCost };
404
- } finally {
405
- await client.stop();
406
- }
407
- }
408
-
409
- /**
410
- * Extract a [NARRATIVE] line from an LLM response.
411
- * Returns the text after the tag, or a fallback summary.
412
- *
413
- * @param {string} content - Raw LLM response content
414
- * @param {string} [fallback] - Fallback if no [NARRATIVE] tag found
415
- * @returns {string} The narrative sentence
416
- */
417
- export function extractNarrative(content, fallback) {
418
- if (!content) return fallback || "";
419
- const match = content.match(/\[NARRATIVE\]\s*(.+)/);
420
- if (match) return match[1].trim();
421
- return fallback || "";
422
- }
423
-
424
- /**
425
- * Narrative solicitation to append to system messages.
426
- * Asks the LLM to end its response with a one-sentence summary.
427
- */
428
- export const NARRATIVE_INSTRUCTION =
429
- "\n\nAfter completing your task, end your response with a line starting with [NARRATIVE] followed by one plain English sentence describing what you did and why, for the activity log.";
430
-
431
- /**
432
- * Read a file, returning empty string on failure. For optional context files.
433
- *
434
- * @param {string} filePath - Path to read
435
- * @param {number} [limit] - Maximum characters to return
436
- * @returns {string}
437
- */
438
- export function readOptionalFile(filePath, limit) {
439
- try {
440
- const content = readFileSync(filePath, "utf8");
441
- return limit ? content.substring(0, limit) : content;
442
- } catch (err) {
443
- core.debug(`[readOptionalFile] ${filePath}: ${err.message}`);
444
- return "";
445
- }
446
- }
447
-
448
- /**
449
- * Scan a directory for files matching an extension, returning name+content pairs.
450
- *
451
- * @param {string} dirPath - Directory to scan
452
- * @param {string|string[]} extensions - File extension(s) to match (e.g. '.md', ['.js', '.ts'])
453
- * @param {Object} [options]
454
- * @param {number} [options.fileLimit=10] - Max files to return
455
- * @param {number} [options.contentLimit] - Max chars per file content
456
- * @param {boolean} [options.recursive=false] - Scan recursively
457
- * @param {boolean} [options.sortByMtime=false] - Sort files by modification time (most recent first)
458
- * @param {boolean} [options.clean=false] - Strip source noise (license headers, blank lines, linter directives)
459
- * @param {boolean} [options.outline=false] - Generate structural outline when content exceeds limit
460
- * @returns {Array<{name: string, content: string}>}
461
- */
462
- export function scanDirectory(dirPath, extensions, options = {}) {
463
- const { fileLimit = 10, contentLimit, recursive = false, sortByMtime = false, clean = false, outline = false } = options;
464
- const exts = Array.isArray(extensions) ? extensions : [extensions];
465
-
466
- if (!existsSync(dirPath)) return [];
467
-
468
- const allFiles = readdirSync(dirPath, recursive ? { recursive: true } : undefined).filter((f) =>
469
- exts.some((ext) => String(f).endsWith(ext)),
470
- );
471
-
472
- if (sortByMtime) {
473
- allFiles.sort((a, b) => {
474
- try {
475
- return statSync(join(dirPath, String(b))).mtimeMs - statSync(join(dirPath, String(a))).mtimeMs;
476
- } catch {
477
- return 0;
478
- }
479
- });
480
- }
481
-
482
- const clipped = allFiles.slice(0, fileLimit);
483
- if (allFiles.length > fileLimit) {
484
- core.info(
485
- `[scanDirectory] Clipped ${dirPath}: ${allFiles.length} files found, returning ${fileLimit} (excess=${allFiles.length - fileLimit})`,
486
- );
487
- }
488
-
489
- return clipped.map((f) => {
490
- const fileName = String(f);
491
- try {
492
- let raw = readFileSync(join(dirPath, fileName), "utf8");
493
- if (clean) raw = cleanSource(raw);
494
-
495
- let content;
496
- if (outline && contentLimit && raw.length > contentLimit) {
497
- const outlineText = generateOutline(raw, fileName);
498
- const halfLimit = Math.floor(contentLimit / 2);
499
- content = outlineText + "\n\n" + raw.substring(0, halfLimit);
500
- core.info(
501
- `[scanDirectory] Outlined ${fileName}: ${raw.length} chars → outline + ${halfLimit} chars`,
502
- );
503
- } else {
504
- content = contentLimit ? raw.substring(0, contentLimit) : raw;
505
- if (contentLimit && raw.length > contentLimit) {
506
- core.info(
507
- `[scanDirectory] Clipped ${fileName}: ${raw.length} chars, returning ${contentLimit} (excess=${raw.length - contentLimit})`,
508
- );
509
- }
510
- }
511
- return { name: fileName, content };
512
- } catch (err) {
513
- core.debug(`[scanDirectory] ${join(dirPath, fileName)}: ${err.message}`);
514
- return { name: fileName, content: "" };
515
- }
516
- });
58
+ return _logTuningParam(param, value, profileName, model, clip, actionsLogger);
517
59
  }
518
60
 
519
- /**
520
- * Format the writable/read-only paths section for a prompt.
521
- *
522
- * @param {string[]} writablePaths
523
- * @param {string[]} [readOnlyPaths=[]]
524
- * @param {Object} [contextFiles] - Optional raw file contents to include
525
- * @param {string} [contextFiles.configToml] - Raw agentic-lib.toml text
526
- * @param {string} [contextFiles.packageJson] - Raw package.json text
527
- * @returns {string}
528
- */
529
- export function formatPathsSection(writablePaths, readOnlyPaths = [], contextFiles) {
530
- const lines = [
531
- "## File Paths",
532
- "### Writable (you may modify these)",
533
- writablePaths.length > 0 ? writablePaths.map((p) => `- ${p}`).join("\n") : "- (none)",
534
- "",
535
- "### Read-Only (for context only, do NOT modify)",
536
- readOnlyPaths.length > 0 ? readOnlyPaths.map((p) => `- ${p}`).join("\n") : "- (none)",
537
- ];
538
- if (contextFiles?.configToml) {
539
- lines.push("", "### Configuration (agentic-lib.toml)", "```toml", contextFiles.configToml, "```");
540
- }
541
- if (contextFiles?.packageJson) {
542
- lines.push("", "### Dependencies (package.json)", "```json", contextFiles.packageJson, "```");
543
- }
544
- return lines.join("\n");
61
+ export async function runCopilotTask(options) {
62
+ return _runCopilotTask({ ...options, logger: actionsLogger });
545
63
  }
@@ -1,142 +1,27 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  // Copyright (C) 2025-2026 Polycode Limited
3
- // tools.js — Shared tool definitions for agentic-step task handlers
3
+ // tools.js — Thin re-export from shared src/copilot/tools.js
4
4
  //
5
- // Defines file I/O and shell tools using the Copilot SDK's defineTool().
6
- // The agent has no built-in filesystem access — these tools give it the
7
- // ability to read, write, list files, and run commands.
5
+ // Phase 4: Tool definitions now live in src/copilot/tools.js.
6
+ // This file re-exports for backwards compatibility.
7
+ //
8
+ // Note: The shared tools.js uses a (writablePaths, logger, defineToolFn) signature.
9
+ // The old Actions tools.js used (writablePaths) with @actions/core and @github/copilot-sdk.
10
+ // This wrapper adapts the signature for existing callers in copilot.js.
8
11
 
9
- import { defineTool } from "@github/copilot-sdk";
10
- import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "fs";
11
- import { execSync } from "child_process";
12
- import { dirname, resolve } from "path";
13
- import { isPathWritable } from "./safety.js";
14
12
  import * as core from "@actions/core";
13
+ import { defineTool } from "@github/copilot-sdk";
14
+ import { createAgentTools as _createAgentTools, isPathWritable } from "../../copilot/tools.js";
15
15
 
16
- /**
17
- * Create the standard set of agent tools.
18
- *
19
- * @param {string[]} writablePaths - Paths the agent is allowed to write to
20
- * @returns {import('@github/copilot-sdk').Tool[]} Array of tools for createSession()
21
- */
22
- export function createAgentTools(writablePaths) {
23
- const readFile = defineTool("read_file", {
24
- description: "Read the contents of a file at the given path.",
25
- parameters: {
26
- type: "object",
27
- properties: {
28
- path: { type: "string", description: "Absolute or relative file path to read" },
29
- },
30
- required: ["path"],
31
- },
32
- handler: ({ path }) => {
33
- const resolved = resolve(path);
34
- core.info(`[tool] read_file: ${resolved}`);
35
- if (!existsSync(resolved)) {
36
- return { error: `File not found: ${resolved}` };
37
- }
38
- try {
39
- const content = readFileSync(resolved, "utf8");
40
- return { content };
41
- } catch (err) {
42
- return { error: `Failed to read ${resolved}: ${err.message}` };
43
- }
44
- },
45
- });
46
-
47
- const writeFile = defineTool("write_file", {
48
- description:
49
- "Write content to a file. The file will be created if it does not exist. Parent directories will be created automatically. Only writable paths are allowed.",
50
- parameters: {
51
- type: "object",
52
- properties: {
53
- path: { type: "string", description: "Absolute or relative file path to write" },
54
- content: { type: "string", description: "The full content to write to the file" },
55
- },
56
- required: ["path", "content"],
57
- },
58
- handler: ({ path, content }) => {
59
- const resolved = resolve(path);
60
- core.info(`[tool] write_file: ${resolved}`);
61
- if (!isPathWritable(resolved, writablePaths)) {
62
- return { error: `Path is not writable: ${path}. Writable paths: ${writablePaths.join(", ")}` };
63
- }
64
- try {
65
- const dir = dirname(resolved);
66
- if (!existsSync(dir)) {
67
- mkdirSync(dir, { recursive: true });
68
- }
69
- writeFileSync(resolved, content, "utf8");
70
- return { success: true, path: resolved };
71
- } catch (err) {
72
- return { error: `Failed to write ${resolved}: ${err.message}` };
73
- }
74
- },
75
- });
76
-
77
- const listFiles = defineTool("list_files", {
78
- description: "List files and directories at the given path. Returns names with a trailing / for directories.",
79
- parameters: {
80
- type: "object",
81
- properties: {
82
- path: { type: "string", description: "Directory path to list" },
83
- recursive: { type: "boolean", description: "Whether to list recursively (default false)" },
84
- },
85
- required: ["path"],
86
- },
87
- handler: ({ path, recursive }) => {
88
- const resolved = resolve(path);
89
- core.info(`[tool] list_files: ${resolved} (recursive=${!!recursive})`);
90
- if (!existsSync(resolved)) {
91
- return { error: `Directory not found: ${resolved}` };
92
- }
93
- try {
94
- const entries = readdirSync(resolved, { withFileTypes: true, recursive: !!recursive });
95
- const names = entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
96
- return { files: names };
97
- } catch (err) {
98
- return { error: `Failed to list ${resolved}: ${err.message}` };
99
- }
100
- },
101
- });
102
-
103
- const runCommand = defineTool("run_command", {
104
- description:
105
- "Run a shell command and return its stdout and stderr. Use this to run tests, build, lint, or inspect the environment.",
106
- parameters: {
107
- type: "object",
108
- properties: {
109
- command: { type: "string", description: "The shell command to execute" },
110
- cwd: { type: "string", description: "Working directory for the command (default: current directory)" },
111
- },
112
- required: ["command"],
113
- },
114
- handler: ({ command, cwd }) => {
115
- const workDir = cwd ? resolve(cwd) : process.cwd();
116
- core.info(`[tool] run_command: ${command} (cwd=${workDir})`);
117
- const blocked = /\bgit\s+(commit|push|add|reset|checkout|rebase|merge|stash)\b/;
118
- if (blocked.test(command)) {
119
- core.info(`[tool] BLOCKED git write command: ${command}`);
120
- return { error: "Git write commands are not allowed. Use read_file/write_file tools instead." };
121
- }
122
- try {
123
- const stdout = execSync(command, {
124
- cwd: workDir,
125
- encoding: "utf8",
126
- timeout: 120000,
127
- maxBuffer: 1024 * 1024,
128
- });
129
- return { stdout, exitCode: 0 };
130
- } catch (err) {
131
- return {
132
- stdout: err.stdout || "",
133
- stderr: err.stderr || "",
134
- exitCode: err.status || 1,
135
- error: err.message,
136
- };
137
- }
138
- },
139
- });
16
+ const actionsLogger = {
17
+ info: (...args) => core.info(args.join(" ")),
18
+ warning: (...args) => core.warning(args.join(" ")),
19
+ error: (...args) => core.error(args.join(" ")),
20
+ debug: (...args) => core.debug(args.join(" ")),
21
+ };
140
22
 
141
- return [readFile, writeFile, listFiles, runCommand];
23
+ export function createAgentTools(writablePaths) {
24
+ return _createAgentTools(writablePaths, actionsLogger, defineTool);
142
25
  }
26
+
27
+ export { isPathWritable };
@@ -10,11 +10,13 @@ import { resolve, dirname } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
 
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const pkgRoot = resolve(__dirname, "../..");
14
13
 
14
+ // Search for the SDK relative to this file's location. Works in both:
15
+ // - npm package: src/copilot/sdk.js → ../../node_modules/ or ../actions/agentic-step/node_modules/
16
+ // - consumer repo: .github/agentic-lib/copilot/sdk.js → ../actions/agentic-step/node_modules/
15
17
  const SDK_LOCATIONS = [
16
- resolve(pkgRoot, "node_modules/@github/copilot-sdk/dist/index.js"),
17
- resolve(pkgRoot, "src/actions/agentic-step/node_modules/@github/copilot-sdk/dist/index.js"),
18
+ resolve(__dirname, "../../node_modules/@github/copilot-sdk/dist/index.js"),
19
+ resolve(__dirname, "../actions/agentic-step/node_modules/@github/copilot-sdk/dist/index.js"),
18
20
  ];
19
21
 
20
22
  let _sdk = null;
@@ -17,7 +17,7 @@
17
17
  "author": "",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@xn-intenton-z2a/agentic-lib": "^7.2.21"
20
+ "@xn-intenton-z2a/agentic-lib": "^7.2.22"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@playwright/test": "^1.58.0",
@@ -1,73 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-only
2
- // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/tasks/fix-code.js — Fix failing tests (shared)
4
- //
5
- // In CLI/local mode: runs tests, feeds failures to agent.
6
- // In Actions mode: can also resolve merge conflicts and PR check failures.
7
-
8
- import { execSync } from "child_process";
9
- import {
10
- runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
11
- extractNarrative, NARRATIVE_INSTRUCTION,
12
- } from "../session.js";
13
- import { defaultLogger } from "../logger.js";
14
-
15
- /**
16
- * Fix failing code — local mode (no GitHub context needed).
17
- * Runs tests, feeds failure output to the agent.
18
- */
19
- export async function fixCode(context) {
20
- const { config, instructions, writablePaths, testCommand, model, logger = defaultLogger } = context;
21
- const t = config.tuning || {};
22
- const workDir = context.workingDirectory || process.cwd();
23
-
24
- // Run tests to get failure output
25
- let testOutput;
26
- let testsPassed = false;
27
- try {
28
- testOutput = execSync(testCommand, { cwd: workDir, encoding: "utf8", timeout: 120000 });
29
- testsPassed = true;
30
- } catch (err) {
31
- testOutput = `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`;
32
- }
33
-
34
- if (testsPassed) {
35
- logger.info("Tests already pass — nothing to fix");
36
- return { outcome: "nop", details: "Tests already pass" };
37
- }
38
-
39
- const mission = readOptionalFile(config.paths.mission.path);
40
- const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
41
- fileLimit: t.sourceScan || 10,
42
- contentLimit: t.sourceContent || 5000,
43
- recursive: true, sortByMtime: true, clean: true,
44
- }, logger);
45
-
46
- const agentInstructions = instructions || "Fix the failing tests by modifying the source code.";
47
-
48
- const prompt = [
49
- "## Instructions", agentInstructions, "",
50
- ...(mission ? ["## Mission", mission, ""] : []),
51
- "## Failing Test Output",
52
- "```", testOutput.substring(0, 8000), "```", "",
53
- `## Current Source Files (${sourceFiles.length})`,
54
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
55
- formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
56
- "## Constraints",
57
- `- Run \`${testCommand}\` to validate your fixes`,
58
- "- Make minimal changes to fix the failing tests",
59
- "- Do not introduce new features — focus on making the build green",
60
- ].join("\n");
61
-
62
- const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
63
- model,
64
- systemMessage: "You are an autonomous coding agent fixing failing tests. Analyze the error output and make minimal, targeted changes to fix it." + NARRATIVE_INSTRUCTION,
65
- prompt, writablePaths, tuning: t, logger, workingDirectory: workDir,
66
- });
67
-
68
- return {
69
- outcome: "fix-applied", tokensUsed, inputTokens, outputTokens, cost, model,
70
- details: `Applied fix based on test failures`,
71
- narrative: extractNarrative(resultContent, "Fixed failing tests."),
72
- };
73
- }
@@ -1,61 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-only
2
- // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/tasks/maintain-features.js — Feature lifecycle (shared)
4
-
5
- import { existsSync } from "fs";
6
- import {
7
- runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
8
- extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION,
9
- } from "../session.js";
10
- import { defaultLogger } from "../logger.js";
11
-
12
- export async function maintainFeatures(context) {
13
- const { config, instructions, writablePaths, model, logger = defaultLogger } = context;
14
- const t = config.tuning || {};
15
-
16
- if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
17
- return { outcome: "nop", details: "Mission already complete" };
18
- }
19
-
20
- const mission = readOptionalFile(config.paths.mission.path);
21
- const featuresPath = config.paths.features.path;
22
- const featureLimit = config.paths.features.limit;
23
- const features = scanDirectory(featuresPath, ".md", { fileLimit: t.featuresScan || 10 }, logger);
24
-
25
- features.sort((a, b) => {
26
- const aInc = /- \[ \]/.test(a.content) ? 0 : 1;
27
- const bInc = /- \[ \]/.test(b.content) ? 0 : 1;
28
- return aInc - bInc || a.name.localeCompare(b.name);
29
- });
30
-
31
- const libraryDocs = scanDirectory(config.paths.library.path, ".md", { contentLimit: t.documentSummary || 1000 }, logger);
32
-
33
- const prompt = [
34
- "## Instructions", instructions || "Maintain the feature set by creating, updating, or pruning features.", "",
35
- "## Mission", mission, "",
36
- `## Current Features (${features.length}/${featureLimit} max)`,
37
- ...features.map((f) => `### ${f.name}\n${f.content}`), "",
38
- ...(libraryDocs.length > 0 ? [
39
- `## Library Documents (${libraryDocs.length})`,
40
- ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`), "",
41
- ] : []),
42
- "## Your Task",
43
- `1. Review each existing feature — delete if implemented or irrelevant.`,
44
- `2. If fewer than ${featureLimit} features, create new ones aligned with the mission.`,
45
- "3. Ensure each feature has clear, testable acceptance criteria.", "",
46
- formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
47
- "## Constraints", `- Maximum ${featureLimit} feature files`,
48
- ].join("\n");
49
-
50
- const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
51
- model,
52
- systemMessage: "You are a feature lifecycle manager. Create, update, and prune feature specification files to keep the project focused on its mission." + NARRATIVE_INSTRUCTION,
53
- prompt, writablePaths, tuning: t, logger,
54
- });
55
-
56
- return {
57
- outcome: "features-maintained", tokensUsed, inputTokens, outputTokens, cost, model,
58
- details: `Maintained features (${features.length} existing, limit ${featureLimit})`,
59
- narrative: extractNarrative(resultContent, `Maintained ${features.length} features.`),
60
- };
61
- }
@@ -1,66 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-only
2
- // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/tasks/maintain-library.js — Library management (shared)
4
-
5
- import { existsSync } from "fs";
6
- import {
7
- runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
8
- extractNarrative, NARRATIVE_INSTRUCTION,
9
- } from "../session.js";
10
- import { defaultLogger } from "../logger.js";
11
-
12
- export async function maintainLibrary(context) {
13
- const { config, instructions, writablePaths, model, logger = defaultLogger } = context;
14
- const t = config.tuning || {};
15
-
16
- if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
17
- logger.info("Mission complete — skipping library maintenance");
18
- return { outcome: "nop", details: "Mission already complete" };
19
- }
20
-
21
- const sourcesPath = config.paths.librarySources.path;
22
- const sources = readOptionalFile(sourcesPath);
23
- const mission = readOptionalFile(config.paths.mission.path);
24
- const hasUrls = /https?:\/\//.test(sources);
25
-
26
- const libraryPath = config.paths.library.path;
27
- const libraryLimit = config.paths.library.limit;
28
- const libraryDocs = scanDirectory(libraryPath, ".md", { contentLimit: t.documentSummary || 500 }, logger);
29
-
30
- let prompt;
31
- if (!hasUrls) {
32
- prompt = [
33
- "## Instructions", instructions || "Maintain the library by updating documents from sources.", "",
34
- "## Mission", mission || "(no mission)", "",
35
- "## Current SOURCES.md", sources || "(empty)", "",
36
- "## Your Task",
37
- `Populate ${sourcesPath} with 3-8 relevant reference URLs.`, "",
38
- formatPathsSection(writablePaths, config.readOnlyPaths, config),
39
- ].join("\n");
40
- } else {
41
- prompt = [
42
- "## Instructions", instructions || "Maintain the library by updating documents from sources.", "",
43
- "## Sources", sources, "",
44
- `## Current Library Documents (${libraryDocs.length}/${libraryLimit} max)`,
45
- ...libraryDocs.map((d) => `### ${d.name}\n${d.content}`), "",
46
- "## Your Task",
47
- "1. Read each URL in SOURCES.md and extract technical content.",
48
- "2. Create or update library documents.", "",
49
- formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
50
- "## Constraints", `- Maximum ${libraryLimit} library documents`,
51
- ].join("\n");
52
- }
53
-
54
- const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
55
- model,
56
- systemMessage: "You are a knowledge librarian. Maintain a library of technical documents extracted from web sources." + NARRATIVE_INSTRUCTION,
57
- prompt, writablePaths, tuning: t, logger,
58
- });
59
-
60
- return {
61
- outcome: hasUrls ? "library-maintained" : "sources-discovered",
62
- tokensUsed, inputTokens, outputTokens, cost, model,
63
- details: hasUrls ? `Maintained library (${libraryDocs.length} docs)` : "Discovered sources from mission",
64
- narrative: extractNarrative(resultContent, hasUrls ? "Maintained library." : "Discovered sources."),
65
- };
66
- }
@@ -1,120 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-only
2
- // Copyright (C) 2025-2026 Polycode Limited
3
- // src/copilot/tasks/transform.js — Transform task (shared)
4
- //
5
- // Ported from src/actions/agentic-step/tasks/transform.js.
6
- // GitHub context (octokit, issues) is optional for local CLI use.
7
-
8
- import { existsSync } from "fs";
9
- import {
10
- runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
11
- filterIssues, summariseIssue, extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION,
12
- } from "../session.js";
13
- import { defaultLogger } from "../logger.js";
14
-
15
- export async function transform(context) {
16
- const { config, instructions, writablePaths, testCommand, model, logger = defaultLogger } = context;
17
- // octokit + repo are optional (absent in CLI mode)
18
- const octokit = context.octokit || null;
19
- const repo = context.repo || null;
20
- const issueNumber = context.issueNumber || null;
21
- const t = config.tuning || {};
22
-
23
- const mission = readOptionalFile(config.paths.mission.path);
24
- if (!mission) {
25
- logger.warning(`No mission file found at ${config.paths.mission.path}`);
26
- return { outcome: "nop", details: "No mission file found" };
27
- }
28
-
29
- if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
30
- logger.info("Mission complete — skipping transformation");
31
- return { outcome: "nop", details: "Mission already complete" };
32
- }
33
-
34
- const features = scanDirectory(config.paths.features.path, ".md", { fileLimit: t.featuresScan || 10 }, logger);
35
- const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
36
- fileLimit: t.sourceScan || 10,
37
- contentLimit: t.sourceContent || 5000,
38
- recursive: true, sortByMtime: true, clean: true, outline: true,
39
- }, logger);
40
- const webFiles = scanDirectory(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], {
41
- fileLimit: t.sourceScan || 10,
42
- contentLimit: t.sourceContent || 5000,
43
- recursive: true, sortByMtime: true, clean: true,
44
- }, logger);
45
-
46
- // GitHub issues (optional)
47
- let openIssues = [];
48
- let rawIssuesCount = 0;
49
- if (octokit && repo) {
50
- const { data: rawIssues } = await octokit.rest.issues.listForRepo({ ...repo, state: "open", per_page: t.issuesScan || 20 });
51
- rawIssuesCount = rawIssues.length;
52
- openIssues = filterIssues(rawIssues, { staleDays: t.staleDays || 30 });
53
- }
54
-
55
- let targetIssue = null;
56
- if (issueNumber && octokit && repo) {
57
- try {
58
- const { data: issue } = await octokit.rest.issues.get({ ...repo, issue_number: Number(issueNumber) });
59
- targetIssue = issue;
60
- } catch (err) {
61
- logger.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
62
- }
63
- }
64
-
65
- const agentInstructions = instructions || "Transform the repository toward its mission by identifying the next best action.";
66
-
67
- const prompt = [
68
- "## Instructions", agentInstructions, "",
69
- ...(targetIssue ? [
70
- `## Target Issue #${targetIssue.number}: ${targetIssue.title}`,
71
- targetIssue.body || "(no description)",
72
- `Labels: ${targetIssue.labels.map((l) => l.name).join(", ") || "none"}`,
73
- "", "**Focus your transformation on resolving this specific issue.**", "",
74
- ] : []),
75
- "## Mission", mission, "",
76
- `## Current Features (${features.length})`,
77
- ...features.map((f) => `### ${f.name}\n${extractFeatureSummary(f.content, f.name)}`), "",
78
- `## Current Source Files (${sourceFiles.length})`,
79
- ...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
80
- ...(webFiles.length > 0 ? [
81
- `## Website Files (${webFiles.length})`,
82
- ...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
83
- ] : []),
84
- ...(openIssues.length > 0 ? [
85
- `## Open Issues (${openIssues.length})`,
86
- ...openIssues.slice(0, t.issuesScan || 20).map((i) => summariseIssue(i, t.issueBodyLimit || 500)), "",
87
- ] : []),
88
- "## Output Artifacts",
89
- `Save output artifacts to \`${config.paths.examples?.path || "examples/"}\`.`, "",
90
- "## Your Task",
91
- "Analyze the mission, features, source code, and open issues.",
92
- "Determine the single most impactful next step to transform this repository.", "Then implement that step.", "",
93
- "## When NOT to make changes",
94
- "If the existing code already satisfies all requirements:", "- Do NOT make cosmetic changes", "- Instead, report that the mission is satisfied", "",
95
- formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
96
- "## Constraints", `- Run \`${testCommand}\` to validate your changes`,
97
- ].join("\n");
98
-
99
- logger.info(`Transform prompt length: ${prompt.length} chars`);
100
-
101
- const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
102
- model,
103
- systemMessage: "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,
104
- prompt, writablePaths, tuning: t, logger,
105
- });
106
-
107
- const promptBudget = [
108
- { section: "mission", size: mission.length, files: "1", notes: "full" },
109
- { section: "features", size: features.reduce((s, f) => s + f.content.length, 0), files: `${features.length}`, notes: "" },
110
- { section: "source", size: sourceFiles.reduce((s, f) => s + f.content.length, 0), files: `${sourceFiles.length}`, notes: "" },
111
- { section: "issues", size: openIssues.length * 80, files: `${openIssues.length}`, notes: `${rawIssuesCount - openIssues.length} filtered` },
112
- ];
113
-
114
- return {
115
- outcome: "transformed", tokensUsed, inputTokens, outputTokens, cost, model,
116
- details: resultContent.substring(0, 500),
117
- narrative: extractNarrative(resultContent, "Transformation step completed."),
118
- promptBudget,
119
- };
120
- }