@xn-intenton-z2a/agentic-lib 7.2.6 → 7.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -17
- package/bin/agentic-lib.js +260 -496
- package/package.json +2 -3
- package/src/actions/agentic-step/tasks/direct.js +7 -0
- package/src/actions/agentic-step/tasks/supervise.js +7 -0
- package/src/agents/agent-apply-fix.md +5 -2
- package/src/agents/agent-discovery.md +52 -0
- package/src/agents/agent-issue-resolution.md +18 -0
- package/src/agents/agent-iterate.md +45 -0
- package/src/copilot/agents.js +39 -0
- package/src/copilot/config.js +308 -0
- package/src/copilot/context.js +318 -0
- package/src/copilot/hybrid-session.js +330 -0
- package/src/copilot/logger.js +43 -0
- package/src/copilot/sdk.js +36 -0
- package/src/copilot/session.js +372 -0
- package/src/copilot/tasks/fix-code.js +73 -0
- package/src/copilot/tasks/maintain-features.js +61 -0
- package/src/copilot/tasks/maintain-library.js +66 -0
- package/src/copilot/tasks/transform.js +120 -0
- package/src/copilot/tools.js +141 -0
- package/src/mcp/server.js +43 -25
- package/src/seeds/zero-README.md +31 -0
- package/src/seeds/zero-behaviour.test.js +8 -0
- package/src/seeds/zero-package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// src/copilot/sdk.js — Find and import the Copilot SDK
|
|
4
|
+
//
|
|
5
|
+
// The SDK may be in the root node_modules or nested under
|
|
6
|
+
// src/actions/agentic-step/node_modules/. This module handles discovery.
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { resolve, dirname } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkgRoot = resolve(__dirname, "../..");
|
|
14
|
+
|
|
15
|
+
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
|
+
];
|
|
19
|
+
|
|
20
|
+
let _sdk = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dynamically import the Copilot SDK, searching known locations.
|
|
24
|
+
* @returns {Promise<{CopilotClient: *, approveAll: *, defineTool: *}>}
|
|
25
|
+
*/
|
|
26
|
+
export async function getSDK() {
|
|
27
|
+
if (_sdk) return _sdk;
|
|
28
|
+
const sdkPath = SDK_LOCATIONS.find((p) => existsSync(p));
|
|
29
|
+
if (!sdkPath) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"@github/copilot-sdk not found. Run: npm ci\nSearched: " + SDK_LOCATIONS.join(", "),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
_sdk = await import(sdkPath);
|
|
35
|
+
return _sdk;
|
|
36
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// src/copilot/session.js — Copilot SDK session management (shared between Actions + CLI)
|
|
4
|
+
//
|
|
5
|
+
// Ported from src/actions/agentic-step/copilot.js with logger abstraction
|
|
6
|
+
// replacing direct @actions/core imports.
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { defaultLogger } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
// Models known to support the reasoningEffort SessionConfig parameter.
|
|
13
|
+
const MODELS_SUPPORTING_REASONING_EFFORT = new Set(["gpt-5-mini", "o4-mini"]);
|
|
14
|
+
|
|
15
|
+
// ── Source utilities ────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export function cleanSource(raw) {
|
|
18
|
+
let cleaned = raw;
|
|
19
|
+
cleaned = cleaned.replace(/^\/\/\s*SPDX-License-Identifier:.*\n/gm, "");
|
|
20
|
+
cleaned = cleaned.replace(/^\/\/\s*Copyright.*\n/gm, "");
|
|
21
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
22
|
+
cleaned = cleaned.replace(/^\s*\/\/\s*eslint-disable.*\n/gm, "");
|
|
23
|
+
cleaned = cleaned.replace(/^\s*\/\*\s*eslint-disable[\s\S]*?\*\/\s*\n/gm, "");
|
|
24
|
+
return cleaned.trimStart();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function generateOutline(raw, filePath) {
|
|
28
|
+
const lines = raw.split("\n");
|
|
29
|
+
const sizeKB = (raw.length / 1024).toFixed(1);
|
|
30
|
+
const parts = [`// file: ${filePath} (${lines.length} lines, ${sizeKB}KB)`];
|
|
31
|
+
|
|
32
|
+
const importSources = [];
|
|
33
|
+
for (const l of lines) {
|
|
34
|
+
const m = l.match(/^import\s.*from\s+["']([^"']+)["']/);
|
|
35
|
+
if (m) importSources.push(m[1]);
|
|
36
|
+
}
|
|
37
|
+
if (importSources.length > 0) parts.push(`// imports: ${importSources.join(", ")}`);
|
|
38
|
+
|
|
39
|
+
const exportNames = [];
|
|
40
|
+
for (const l of lines) {
|
|
41
|
+
const m = l.match(/^export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/);
|
|
42
|
+
if (m) exportNames.push(m[1]);
|
|
43
|
+
}
|
|
44
|
+
if (exportNames.length > 0) parts.push(`// exports: ${exportNames.join(", ")}`);
|
|
45
|
+
|
|
46
|
+
parts.push("//");
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const l = lines[i];
|
|
49
|
+
const funcMatch = l.match(/^(export\s+)?(async\s+)?function\s+(\w+)\s*\(/);
|
|
50
|
+
if (funcMatch) { parts.push(`// function ${funcMatch[3]}() — line ${i + 1}`); continue; }
|
|
51
|
+
const classMatch = l.match(/^(export\s+)?(default\s+)?class\s+(\w+)/);
|
|
52
|
+
if (classMatch) { parts.push(`// class ${classMatch[3]} — line ${i + 1}`); continue; }
|
|
53
|
+
const methodMatch = l.match(/^\s+(async\s+)?(\w+)\s*\([^)]*\)\s*\{/);
|
|
54
|
+
if (methodMatch && !["if", "for", "while", "switch", "catch", "try"].includes(methodMatch[2])) {
|
|
55
|
+
parts.push(`// ${methodMatch[2]}() — line ${i + 1}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return parts.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Issue/feature utilities ─────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function filterIssues(issues, options = {}) {
|
|
64
|
+
const { staleDays = 30, excludeBotOnly = true, initTimestamp } = options;
|
|
65
|
+
const cutoff = Date.now() - staleDays * 86400000;
|
|
66
|
+
const initEpoch = initTimestamp ? new Date(initTimestamp).getTime() : 0;
|
|
67
|
+
|
|
68
|
+
return issues.filter((issue) => {
|
|
69
|
+
if (initEpoch > 0 && new Date(issue.created_at).getTime() < initEpoch) return false;
|
|
70
|
+
const lastActivity = new Date(issue.updated_at || issue.created_at).getTime();
|
|
71
|
+
if (lastActivity < cutoff) return false;
|
|
72
|
+
if (excludeBotOnly) {
|
|
73
|
+
const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name));
|
|
74
|
+
const botLabels = ["automated", "stale", "bot", "wontfix"];
|
|
75
|
+
if (labels.length > 0 && labels.every((l) => botLabels.includes(l))) return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function summariseIssue(issue, bodyLimit = 500) {
|
|
82
|
+
const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name)).join(", ") || "no labels";
|
|
83
|
+
const age = Math.floor((Date.now() - new Date(issue.created_at).getTime()) / 86400000);
|
|
84
|
+
const body = (issue.body || "").substring(0, bodyLimit).replace(/\n+/g, " ").trim();
|
|
85
|
+
return `#${issue.number}: ${issue.title} [${labels}] (${age}d old)${body ? `\n ${body}` : ""}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function extractFeatureSummary(content, fileName) {
|
|
89
|
+
const lines = content.split("\n");
|
|
90
|
+
const title = lines.find((l) => l.startsWith("#"))?.replace(/^#+\s*/, "") || fileName;
|
|
91
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
92
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
93
|
+
const total = checked + unchecked;
|
|
94
|
+
const parts = [`Feature: ${title}`];
|
|
95
|
+
if (total > 0) {
|
|
96
|
+
parts.push(`Status: ${checked}/${total} items complete`);
|
|
97
|
+
const remaining = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (/- \[ \]/.test(line)) remaining.push(line.replace(/^[\s-]*\[ \]\s*/, "").trim());
|
|
100
|
+
}
|
|
101
|
+
if (remaining.length > 0) parts.push(`Remaining: ${remaining.map((r) => `[ ] ${r}`).join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
return parts.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── File utilities ──────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export function readOptionalFile(filePath, limit) {
|
|
109
|
+
try {
|
|
110
|
+
const content = readFileSync(filePath, "utf8");
|
|
111
|
+
return limit ? content.substring(0, limit) : content;
|
|
112
|
+
} catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function scanDirectory(dirPath, extensions, options = {}, logger = defaultLogger) {
|
|
118
|
+
const { fileLimit = 10, contentLimit, recursive = false, sortByMtime = false, clean = false, outline = false } = options;
|
|
119
|
+
const exts = Array.isArray(extensions) ? extensions : [extensions];
|
|
120
|
+
if (!existsSync(dirPath)) return [];
|
|
121
|
+
|
|
122
|
+
const allFiles = readdirSync(dirPath, recursive ? { recursive: true } : undefined).filter((f) =>
|
|
123
|
+
exts.some((ext) => String(f).endsWith(ext)),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (sortByMtime) {
|
|
127
|
+
allFiles.sort((a, b) => {
|
|
128
|
+
try { return statSync(join(dirPath, String(b))).mtimeMs - statSync(join(dirPath, String(a))).mtimeMs; }
|
|
129
|
+
catch { return 0; }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const clipped = allFiles.slice(0, fileLimit);
|
|
134
|
+
if (allFiles.length > fileLimit) {
|
|
135
|
+
logger.info(`[scanDirectory] Clipped ${dirPath}: ${allFiles.length} files, returning ${fileLimit}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return clipped.map((f) => {
|
|
139
|
+
const fileName = String(f);
|
|
140
|
+
try {
|
|
141
|
+
let raw = readFileSync(join(dirPath, fileName), "utf8");
|
|
142
|
+
if (clean) raw = cleanSource(raw);
|
|
143
|
+
let content;
|
|
144
|
+
if (outline && contentLimit && raw.length > contentLimit) {
|
|
145
|
+
const outlineText = generateOutline(raw, fileName);
|
|
146
|
+
const halfLimit = Math.floor(contentLimit / 2);
|
|
147
|
+
content = outlineText + "\n\n" + raw.substring(0, halfLimit);
|
|
148
|
+
} else {
|
|
149
|
+
content = contentLimit ? raw.substring(0, contentLimit) : raw;
|
|
150
|
+
}
|
|
151
|
+
return { name: fileName, content };
|
|
152
|
+
} catch {
|
|
153
|
+
return { name: fileName, content: "" };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function formatPathsSection(writablePaths, readOnlyPaths = [], contextFiles) {
|
|
159
|
+
const lines = [
|
|
160
|
+
"## File Paths",
|
|
161
|
+
"### Writable (you may modify these)",
|
|
162
|
+
writablePaths.length > 0 ? writablePaths.map((p) => `- ${p}`).join("\n") : "- (none)",
|
|
163
|
+
"",
|
|
164
|
+
"### Read-Only (for context only, do NOT modify)",
|
|
165
|
+
readOnlyPaths.length > 0 ? readOnlyPaths.map((p) => `- ${p}`).join("\n") : "- (none)",
|
|
166
|
+
];
|
|
167
|
+
if (contextFiles?.configToml) {
|
|
168
|
+
lines.push("", "### Configuration (agentic-lib.toml)", "```toml", contextFiles.configToml, "```");
|
|
169
|
+
}
|
|
170
|
+
if (contextFiles?.packageJson) {
|
|
171
|
+
lines.push("", "### Dependencies (package.json)", "```json", contextFiles.packageJson, "```");
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Narrative ───────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function extractNarrative(content, fallback) {
|
|
179
|
+
if (!content) return fallback || "";
|
|
180
|
+
const match = content.match(/\[NARRATIVE\]\s*(.+)/);
|
|
181
|
+
if (match) return match[1].trim();
|
|
182
|
+
return fallback || "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const NARRATIVE_INSTRUCTION =
|
|
186
|
+
"\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.";
|
|
187
|
+
|
|
188
|
+
// ── Auth ────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export function buildClientOptions(githubToken, logger = defaultLogger) {
|
|
191
|
+
const copilotToken = githubToken || process.env.COPILOT_GITHUB_TOKEN;
|
|
192
|
+
if (!copilotToken) {
|
|
193
|
+
throw new Error("COPILOT_GITHUB_TOKEN is required. Set it in your environment.");
|
|
194
|
+
}
|
|
195
|
+
logger.info("[copilot] COPILOT_GITHUB_TOKEN found — overriding subprocess env");
|
|
196
|
+
const env = { ...process.env };
|
|
197
|
+
env.GITHUB_TOKEN = copilotToken;
|
|
198
|
+
env.GH_TOKEN = copilotToken;
|
|
199
|
+
return { env };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Tuning helpers ──────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export function supportsReasoningEffort(model) {
|
|
205
|
+
return MODELS_SUPPORTING_REASONING_EFFORT.has(model);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function logTuningParam(param, value, profileName, model, clip, logger = defaultLogger) {
|
|
209
|
+
const clipInfo = clip ? ` (requested=${clip.requested}, available=${clip.available})` : "";
|
|
210
|
+
logger.info(`[tuning] ${param}=${value} profile=${profileName} model=${model}${clipInfo}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Rate limit ──────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
export function isRateLimitError(err) {
|
|
216
|
+
if (!err || typeof err !== "object") return false;
|
|
217
|
+
const status = err.status ?? err.statusCode ?? err.code;
|
|
218
|
+
if (status === 429 || status === "429") return true;
|
|
219
|
+
const msg = (err.message || "").toLowerCase();
|
|
220
|
+
return msg.includes("429") || msg.includes("too many requests") || msg.includes("rate limit");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function retryDelayMs(err, attempt, baseDelayMs = 60000) {
|
|
224
|
+
const retryAfterHeader = err?.headers?.["retry-after"] ?? err?.retryAfter ?? err?.response?.headers?.["retry-after"];
|
|
225
|
+
if (retryAfterHeader != null) {
|
|
226
|
+
const parsed = Number(retryAfterHeader);
|
|
227
|
+
if (!isNaN(parsed) && parsed > 0) return parsed * 1000;
|
|
228
|
+
}
|
|
229
|
+
return baseDelayMs * Math.pow(2, attempt);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Core session runner ─────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Run a Copilot SDK task: create client → session → send prompt → return result.
|
|
236
|
+
* Retries on 429 rate limits.
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} options
|
|
239
|
+
* @param {string} options.model
|
|
240
|
+
* @param {string} options.systemMessage
|
|
241
|
+
* @param {string} options.prompt
|
|
242
|
+
* @param {string[]} options.writablePaths
|
|
243
|
+
* @param {string} [options.githubToken]
|
|
244
|
+
* @param {Object} [options.tuning]
|
|
245
|
+
* @param {string} [options.profileName]
|
|
246
|
+
* @param {string} [options.workingDirectory]
|
|
247
|
+
* @param {number} [options.maxRetries=3]
|
|
248
|
+
* @param {number} [options.timeoutMs=600000]
|
|
249
|
+
* @param {Object} [options.hooks] - SDK lifecycle hooks
|
|
250
|
+
* @param {Object} [options.logger]
|
|
251
|
+
* @param {Function} [options.createTools] - (defineTool, writablePaths, logger) => Tool[]
|
|
252
|
+
* @returns {Promise<{content: string, tokensUsed: number, inputTokens: number, outputTokens: number, cost: number}>}
|
|
253
|
+
*/
|
|
254
|
+
export async function runCopilotTask({
|
|
255
|
+
model,
|
|
256
|
+
systemMessage,
|
|
257
|
+
prompt,
|
|
258
|
+
writablePaths,
|
|
259
|
+
githubToken,
|
|
260
|
+
tuning,
|
|
261
|
+
profileName,
|
|
262
|
+
workingDirectory,
|
|
263
|
+
maxRetries = 3,
|
|
264
|
+
timeoutMs = 600000,
|
|
265
|
+
hooks,
|
|
266
|
+
logger = defaultLogger,
|
|
267
|
+
createTools,
|
|
268
|
+
}) {
|
|
269
|
+
const profile = profileName || tuning?.profileName || "unknown";
|
|
270
|
+
|
|
271
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
272
|
+
try {
|
|
273
|
+
return await _runOnce({
|
|
274
|
+
model, systemMessage, prompt, writablePaths, githubToken,
|
|
275
|
+
tuning, profileName: profile, workingDirectory, timeoutMs, hooks, logger, createTools,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (isRateLimitError(err) && attempt < maxRetries) {
|
|
279
|
+
const delayMs = retryDelayMs(err, attempt);
|
|
280
|
+
logger.warning(`[copilot] Rate limit (429) — waiting ${Math.round(delayMs / 1000)}s before retry ${attempt + 1}/${maxRetries}`);
|
|
281
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
282
|
+
} else {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw new Error("[copilot] runCopilotTask: all retry attempts exhausted");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function _runOnce({
|
|
291
|
+
model, systemMessage, prompt, writablePaths, githubToken,
|
|
292
|
+
tuning, profileName, workingDirectory, timeoutMs, hooks, logger, createTools,
|
|
293
|
+
}) {
|
|
294
|
+
const { getSDK } = await import("./sdk.js");
|
|
295
|
+
const { CopilotClient, approveAll, defineTool } = await getSDK();
|
|
296
|
+
const { createAgentTools } = await import("./tools.js");
|
|
297
|
+
|
|
298
|
+
logger.info(`[copilot] Creating client (model=${model}, promptLen=${prompt.length}, profile=${profileName})`);
|
|
299
|
+
|
|
300
|
+
const clientOptions = buildClientOptions(githubToken, logger);
|
|
301
|
+
const client = new CopilotClient(clientOptions);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const tools = createTools
|
|
305
|
+
? createTools(defineTool, writablePaths, logger)
|
|
306
|
+
: createAgentTools(writablePaths, logger, defineTool);
|
|
307
|
+
|
|
308
|
+
const sessionConfig = {
|
|
309
|
+
model,
|
|
310
|
+
systemMessage: { content: systemMessage },
|
|
311
|
+
tools,
|
|
312
|
+
onPermissionRequest: approveAll,
|
|
313
|
+
workingDirectory: workingDirectory || process.cwd(),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Conditional tuning params
|
|
317
|
+
if (tuning?.reasoningEffort && tuning.reasoningEffort !== "none") {
|
|
318
|
+
if (supportsReasoningEffort(model)) {
|
|
319
|
+
sessionConfig.reasoningEffort = tuning.reasoningEffort;
|
|
320
|
+
logTuningParam("reasoningEffort", tuning.reasoningEffort, profileName, model, null, logger);
|
|
321
|
+
} else {
|
|
322
|
+
logger.info(`[copilot] Skipping reasoningEffort="${tuning.reasoningEffort}" — not supported by "${model}"`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (tuning?.infiniteSessions === true) {
|
|
326
|
+
sessionConfig.infiniteSessions = {};
|
|
327
|
+
logTuningParam("infiniteSessions", true, profileName, model, null, logger);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Lifecycle hooks (Phase 2)
|
|
331
|
+
if (hooks) {
|
|
332
|
+
sessionConfig.hooks = hooks;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const session = await client.createSession(sessionConfig);
|
|
336
|
+
logger.info(`[copilot] Session created: ${session.sessionId}`);
|
|
337
|
+
|
|
338
|
+
// Token accumulation via events
|
|
339
|
+
let totalInputTokens = 0;
|
|
340
|
+
let totalOutputTokens = 0;
|
|
341
|
+
let totalCost = 0;
|
|
342
|
+
|
|
343
|
+
session.on((event) => {
|
|
344
|
+
const eventType = event?.type || "unknown";
|
|
345
|
+
if (eventType === "assistant.message") {
|
|
346
|
+
const preview = event?.data?.content?.substring(0, 100) || "(no content)";
|
|
347
|
+
logger.info(`[copilot] event=${eventType}: ${preview}...`);
|
|
348
|
+
} else if (eventType === "assistant.usage") {
|
|
349
|
+
const d = event?.data || {};
|
|
350
|
+
totalInputTokens += d.inputTokens || 0;
|
|
351
|
+
totalOutputTokens += d.outputTokens || 0;
|
|
352
|
+
totalCost += d.cost || 0;
|
|
353
|
+
logger.info(`[copilot] event=${eventType}: input=${d.inputTokens || 0} output=${d.outputTokens || 0}`);
|
|
354
|
+
} else if (eventType === "session.error") {
|
|
355
|
+
logger.error(`[copilot] event=${eventType}: ${JSON.stringify(event?.data || event)}`);
|
|
356
|
+
} else if (eventType !== "session.idle") {
|
|
357
|
+
logger.debug(`[copilot] event=${eventType}`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
logger.info("[copilot] Sending prompt...");
|
|
362
|
+
const response = await session.sendAndWait({ prompt }, timeoutMs);
|
|
363
|
+
logger.info("[copilot] sendAndWait resolved");
|
|
364
|
+
|
|
365
|
+
const tokensUsed = totalInputTokens + totalOutputTokens;
|
|
366
|
+
const content = response?.data?.content || "";
|
|
367
|
+
|
|
368
|
+
return { content, tokensUsed, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, cost: totalCost };
|
|
369
|
+
} finally {
|
|
370
|
+
await client.stop();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
}
|