@xn-intenton-z2a/agentic-lib 7.2.20 → 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.
@@ -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
  }