@xn-intenton-z2a/agentic-lib 7.4.5 → 7.4.7
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/.github/workflows/agentic-lib-bot.yml +5 -1
- package/.github/workflows/agentic-lib-workflow.yml +30 -6
- package/README.md +2 -2
- package/bin/agentic-lib.js +90 -50
- package/package.json +1 -1
- package/src/actions/agentic-step/config-loader.js +4 -304
- package/src/actions/agentic-step/copilot.js +45 -527
- package/src/actions/agentic-step/index.js +60 -328
- package/src/actions/agentic-step/metrics.js +115 -0
- package/src/actions/agentic-step/tools.js +19 -134
- package/src/copilot/context.js +158 -19
- package/src/copilot/guards.js +87 -0
- package/src/copilot/hybrid-session.js +16 -9
- package/src/copilot/sdk.js +5 -3
- package/src/copilot/telemetry.js +172 -0
- package/src/seeds/zero-package.json +1 -1
- package/src/copilot/tasks/fix-code.js +0 -73
- package/src/copilot/tasks/maintain-features.js +0 -61
- package/src/copilot/tasks/maintain-library.js +0 -66
- package/src/copilot/tasks/transform.js +0 -120
|
@@ -1,142 +1,27 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
2
|
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// tools.js —
|
|
3
|
+
// tools.js — Thin re-export from shared src/copilot/tools.js
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
+
export function createAgentTools(writablePaths) {
|
|
24
|
+
return _createAgentTools(writablePaths, actionsLogger, defineTool);
|
|
142
25
|
}
|
|
26
|
+
|
|
27
|
+
export { isPathWritable };
|
package/src/copilot/context.js
CHANGED
|
@@ -8,8 +8,33 @@
|
|
|
8
8
|
import { resolve } from "path";
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
10
|
import { scanDirectory, readOptionalFile, extractFeatureSummary, formatPathsSection, summariseIssue, filterIssues } from "./session.js";
|
|
11
|
+
import { readCumulativeCost } from "./telemetry.js";
|
|
11
12
|
import { defaultLogger } from "./logger.js";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Per-agent refinements that control context precision and prompt quality.
|
|
16
|
+
* These restore the tight scoping that existed in the old per-task handlers.
|
|
17
|
+
*/
|
|
18
|
+
const AGENT_REFINEMENTS = {
|
|
19
|
+
"agent-issue-resolution": {
|
|
20
|
+
sortFeatures: "incomplete-first",
|
|
21
|
+
includeWebFiles: true,
|
|
22
|
+
highlightTargetIssue: true,
|
|
23
|
+
trackPromptBudget: true,
|
|
24
|
+
},
|
|
25
|
+
"agent-apply-fix": {
|
|
26
|
+
emphasizeTestOutput: true,
|
|
27
|
+
},
|
|
28
|
+
"agent-maintain-features": {
|
|
29
|
+
sortFeatures: "incomplete-first",
|
|
30
|
+
injectLimit: "features",
|
|
31
|
+
},
|
|
32
|
+
"agent-maintain-library": {
|
|
33
|
+
checkSourcesForUrls: true,
|
|
34
|
+
injectLimit: "library",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* Context requirements per agent. Defines what context each agent needs.
|
|
15
40
|
* All fields are optional — the builder includes whatever is available.
|
|
@@ -90,6 +115,16 @@ export function gatherLocalContext(workspacePath, config, { logger = defaultLogg
|
|
|
90
115
|
const sourcesPath = paths.librarySources?.path || "SOURCES.md";
|
|
91
116
|
context.librarySources = readOptionalFile(resolve(wsPath, sourcesPath));
|
|
92
117
|
|
|
118
|
+
// Web files
|
|
119
|
+
const webPath = paths.web?.path || "src/web/";
|
|
120
|
+
const webDir = resolve(wsPath, webPath);
|
|
121
|
+
context.webFiles = scanDirectory(webDir, [".html", ".css", ".js"], {
|
|
122
|
+
fileLimit: tuning.sourceScan || 10,
|
|
123
|
+
contentLimit: tuning.sourceContent || 5000,
|
|
124
|
+
sortByMtime: true,
|
|
125
|
+
clean: true,
|
|
126
|
+
}, logger);
|
|
127
|
+
|
|
93
128
|
// Contributing guide
|
|
94
129
|
const contributingPath = paths.contributing?.path || "CONTRIBUTING.md";
|
|
95
130
|
context.contributing = readOptionalFile(resolve(wsPath, contributingPath), 2000);
|
|
@@ -202,26 +237,60 @@ export function gatherGitHubContext({ issueNumber, prNumber, discussionUrl, work
|
|
|
202
237
|
/**
|
|
203
238
|
* Build a user prompt for the given agent from available context.
|
|
204
239
|
*
|
|
240
|
+
* Returns an object with the assembled prompt and optional promptBudget metadata.
|
|
241
|
+
* The prompt includes config-driven limits and per-agent refinements that restore
|
|
242
|
+
* the context precision from the old per-task handlers.
|
|
243
|
+
*
|
|
205
244
|
* @param {string} agentName - Agent name (e.g. "agent-iterate")
|
|
206
245
|
* @param {Object} localContext - From gatherLocalContext()
|
|
207
246
|
* @param {Object} [githubContext] - From gatherGitHubContext() (optional)
|
|
208
247
|
* @param {Object} [options]
|
|
209
248
|
* @param {Object} [options.tuning] - Tuning config for limits
|
|
210
|
-
* @
|
|
249
|
+
* @param {Object} [options.config] - Full parsed config (for limits injection)
|
|
250
|
+
* @returns {{ prompt: string, promptBudget: Array|null }}
|
|
211
251
|
*/
|
|
212
|
-
export function buildUserPrompt(agentName, localContext, githubContext, { tuning } = {}) {
|
|
252
|
+
export function buildUserPrompt(agentName, localContext, githubContext, { tuning, config } = {}) {
|
|
213
253
|
const needs = AGENT_CONTEXT[agentName] || AGENT_CONTEXT["agent-iterate"];
|
|
254
|
+
const refinements = AGENT_REFINEMENTS[agentName] || {};
|
|
214
255
|
const sections = [];
|
|
256
|
+
const promptBudget = refinements.trackPromptBudget ? [] : null;
|
|
257
|
+
|
|
258
|
+
// Target issue — placed prominently when highlightTargetIssue is set
|
|
259
|
+
if (refinements.highlightTargetIssue && githubContext?.issueDetail) {
|
|
260
|
+
const issue = githubContext.issueDetail;
|
|
261
|
+
const issueSection = [
|
|
262
|
+
`# Target Issue #${issue.number}: ${issue.title}`,
|
|
263
|
+
issue.body || "(no description)",
|
|
264
|
+
`Labels: ${(issue.labels || []).map((l) => typeof l === "string" ? l : l.name).join(", ") || "none"}`,
|
|
265
|
+
"",
|
|
266
|
+
"**Focus your transformation on resolving this specific issue.**",
|
|
267
|
+
];
|
|
268
|
+
if (issue.comments?.length > 0) {
|
|
269
|
+
issueSection.push("\n## Comments");
|
|
270
|
+
for (const c of issue.comments.slice(-5)) {
|
|
271
|
+
issueSection.push(`**${c.author?.login || "unknown"}**: ${c.body}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const text = issueSection.join("\n");
|
|
275
|
+
sections.push(text);
|
|
276
|
+
if (promptBudget) promptBudget.push({ section: "target-issue", size: text.length, files: "1", notes: "" });
|
|
277
|
+
}
|
|
215
278
|
|
|
216
279
|
// Mission
|
|
217
280
|
if (needs.mission && localContext.mission) {
|
|
218
|
-
|
|
281
|
+
const text = `# Mission\n\n${localContext.mission}`;
|
|
282
|
+
sections.push(text);
|
|
283
|
+
if (promptBudget) promptBudget.push({ section: "mission", size: localContext.mission.length, files: "1", notes: "full" });
|
|
219
284
|
}
|
|
220
285
|
|
|
221
|
-
// Current test state
|
|
286
|
+
// Current test state — emphasised for fix-code agent
|
|
222
287
|
if (localContext.testOutput) {
|
|
223
288
|
const testPreview = localContext.testOutput.substring(0, 4000);
|
|
224
|
-
|
|
289
|
+
if (refinements.emphasizeTestOutput) {
|
|
290
|
+
sections.push(`# Failing Test Output\n\nThe tests are currently failing. Fix the root cause.\n\n\`\`\`\n${testPreview}\n\`\`\``);
|
|
291
|
+
} else {
|
|
292
|
+
sections.push(`# Current Test State\n\n\`\`\`\n${testPreview}\n\`\`\``);
|
|
293
|
+
}
|
|
225
294
|
}
|
|
226
295
|
|
|
227
296
|
// Source files
|
|
@@ -230,7 +299,20 @@ export function buildUserPrompt(agentName, localContext, githubContext, { tuning
|
|
|
230
299
|
for (const f of localContext.sourceFiles) {
|
|
231
300
|
sourceSection.push(`## ${f.name}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
232
301
|
}
|
|
233
|
-
|
|
302
|
+
const text = sourceSection.join("\n\n");
|
|
303
|
+
sections.push(text);
|
|
304
|
+
if (promptBudget) promptBudget.push({ section: "source", size: text.length, files: `${localContext.sourceFiles.length}`, notes: "" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Web files — only when refinements request it
|
|
308
|
+
if (refinements.includeWebFiles && localContext.webFiles?.length > 0) {
|
|
309
|
+
const webSection = [`# Website Files (${localContext.webFiles.length})`];
|
|
310
|
+
for (const f of localContext.webFiles) {
|
|
311
|
+
webSection.push(`## ${f.name}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
312
|
+
}
|
|
313
|
+
const text = webSection.join("\n\n");
|
|
314
|
+
sections.push(text);
|
|
315
|
+
if (promptBudget) promptBudget.push({ section: "web", size: text.length, files: `${localContext.webFiles.length}`, notes: "" });
|
|
234
316
|
}
|
|
235
317
|
|
|
236
318
|
// Test files
|
|
@@ -239,43 +321,75 @@ export function buildUserPrompt(agentName, localContext, githubContext, { tuning
|
|
|
239
321
|
for (const f of localContext.testFiles) {
|
|
240
322
|
testSection.push(`## ${f.name}\n\`\`\`\n${f.content}\n\`\`\``);
|
|
241
323
|
}
|
|
242
|
-
|
|
324
|
+
const text = testSection.join("\n\n");
|
|
325
|
+
sections.push(text);
|
|
326
|
+
if (promptBudget) promptBudget.push({ section: "tests", size: text.length, files: `${localContext.testFiles.length}`, notes: "" });
|
|
243
327
|
}
|
|
244
328
|
|
|
245
|
-
// Features
|
|
329
|
+
// Features — sorted incomplete-first when refinements request it
|
|
246
330
|
if (needs.features && localContext.features?.length > 0) {
|
|
247
|
-
|
|
248
|
-
|
|
331
|
+
let features = [...localContext.features];
|
|
332
|
+
if (refinements.sortFeatures === "incomplete-first") {
|
|
333
|
+
features.sort((a, b) => {
|
|
334
|
+
const aIncomplete = /Remaining:/.test(a) || /\[ \]/.test(a) ? 0 : 1;
|
|
335
|
+
const bIncomplete = /Remaining:/.test(b) || /\[ \]/.test(b) ? 0 : 1;
|
|
336
|
+
return aIncomplete - bIncomplete;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const limit = config?.paths?.features?.limit;
|
|
341
|
+
const header = limit
|
|
342
|
+
? `# Features (${features.length}/${limit} max)`
|
|
343
|
+
: `# Features (${features.length})`;
|
|
344
|
+
const featureSection = [header];
|
|
345
|
+
for (const f of features) {
|
|
249
346
|
featureSection.push(f);
|
|
250
347
|
}
|
|
251
|
-
|
|
348
|
+
const text = featureSection.join("\n\n");
|
|
349
|
+
sections.push(text);
|
|
350
|
+
if (promptBudget) promptBudget.push({ section: "features", size: text.length, files: `${features.length}`, notes: "" });
|
|
252
351
|
}
|
|
253
352
|
|
|
254
353
|
// Library
|
|
255
354
|
if (needs.library && localContext.libraryFiles?.length > 0) {
|
|
256
|
-
const
|
|
355
|
+
const libLimit = config?.paths?.library?.limit;
|
|
356
|
+
const header = libLimit
|
|
357
|
+
? `# Library Files (${localContext.libraryFiles.length}/${libLimit} max)`
|
|
358
|
+
: `# Library Files (${localContext.libraryFiles.length})`;
|
|
359
|
+
const libSection = [header];
|
|
257
360
|
for (const f of localContext.libraryFiles) {
|
|
258
361
|
libSection.push(`## ${f.name}\n${f.content}`);
|
|
259
362
|
}
|
|
260
363
|
sections.push(libSection.join("\n\n"));
|
|
261
364
|
}
|
|
262
365
|
|
|
263
|
-
// Library sources
|
|
366
|
+
// Library sources — vary strategy based on URL presence
|
|
264
367
|
if (needs.librarySources && localContext.librarySources) {
|
|
265
|
-
|
|
368
|
+
if (refinements.checkSourcesForUrls) {
|
|
369
|
+
const hasUrls = /https?:\/\//.test(localContext.librarySources);
|
|
370
|
+
if (!hasUrls) {
|
|
371
|
+
sections.push(`# Sources\n\n${localContext.librarySources}\n\nPopulate SOURCES.md with 3-8 relevant reference URLs aligned with the mission.`);
|
|
372
|
+
} else {
|
|
373
|
+
sections.push(`# Sources\n\n${localContext.librarySources}`);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
sections.push(`# Sources\n\n${localContext.librarySources}`);
|
|
377
|
+
}
|
|
266
378
|
}
|
|
267
379
|
|
|
268
|
-
// Issues (from GitHub context)
|
|
380
|
+
// Issues (from GitHub context) — not duplicated if target issue already shown
|
|
269
381
|
if (needs.issues && githubContext?.issues?.length > 0) {
|
|
270
382
|
const issueSection = [`# Open Issues (${githubContext.issues.length})`];
|
|
271
383
|
for (const issue of githubContext.issues) {
|
|
272
384
|
issueSection.push(summariseIssue(issue, tuning?.issueBodyLimit || 500));
|
|
273
385
|
}
|
|
274
|
-
|
|
386
|
+
const text = issueSection.join("\n\n");
|
|
387
|
+
sections.push(text);
|
|
388
|
+
if (promptBudget) promptBudget.push({ section: "issues", size: text.length, files: `${githubContext.issues.length}`, notes: "" });
|
|
275
389
|
}
|
|
276
390
|
|
|
277
|
-
// Specific issue detail
|
|
278
|
-
if (githubContext?.issueDetail) {
|
|
391
|
+
// Specific issue detail (only if not already highlighted as target issue)
|
|
392
|
+
if (!refinements.highlightTargetIssue && githubContext?.issueDetail) {
|
|
279
393
|
const issue = githubContext.issueDetail;
|
|
280
394
|
const issueSection = [`# Issue #${issue.number}: ${issue.title}\n\n${issue.body || "(no body)"}`];
|
|
281
395
|
if (issue.comments?.length > 0) {
|
|
@@ -306,6 +420,31 @@ export function buildUserPrompt(agentName, localContext, githubContext, { tuning
|
|
|
306
420
|
));
|
|
307
421
|
}
|
|
308
422
|
|
|
423
|
+
// Limits section — inject concrete numbers from agentic-lib.toml
|
|
424
|
+
if (config) {
|
|
425
|
+
const limitsLines = ["# Limits (from agentic-lib.toml)", ""];
|
|
426
|
+
const budget = config.transformationBudget || 0;
|
|
427
|
+
const featLimit = config.paths?.features?.limit;
|
|
428
|
+
const libLimit = config.paths?.library?.limit;
|
|
429
|
+
|
|
430
|
+
if (featLimit) limitsLines.push(`- Maximum feature files: ${featLimit}`);
|
|
431
|
+
if (libLimit) limitsLines.push(`- Maximum library documents: ${libLimit}`);
|
|
432
|
+
if (budget > 0) {
|
|
433
|
+
const intentionPath = config.intentionBot?.intentionFilepath;
|
|
434
|
+
const cumulativeCost = readCumulativeCost(intentionPath);
|
|
435
|
+
const remaining = Math.max(0, budget - cumulativeCost);
|
|
436
|
+
limitsLines.push(`- Transformation budget: ${budget} (used: ${cumulativeCost}, remaining: ${remaining})`);
|
|
437
|
+
}
|
|
438
|
+
if (config.featureDevelopmentIssuesWipLimit) limitsLines.push(`- Maximum concurrent feature issues: ${config.featureDevelopmentIssuesWipLimit}`);
|
|
439
|
+
if (config.maintenanceIssuesWipLimit) limitsLines.push(`- Maximum concurrent maintenance issues: ${config.maintenanceIssuesWipLimit}`);
|
|
440
|
+
if (config.attemptsPerBranch) limitsLines.push(`- Maximum attempts per branch: ${config.attemptsPerBranch}`);
|
|
441
|
+
if (config.attemptsPerIssue) limitsLines.push(`- Maximum attempts per issue: ${config.attemptsPerIssue}`);
|
|
442
|
+
|
|
443
|
+
if (limitsLines.length > 2) {
|
|
444
|
+
sections.push(limitsLines.join("\n"));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
309
448
|
// Instructions
|
|
310
449
|
sections.push([
|
|
311
450
|
"Implement this mission. Read the existing source code and tests,",
|
|
@@ -314,5 +453,5 @@ export function buildUserPrompt(agentName, localContext, githubContext, { tuning
|
|
|
314
453
|
"Start by reading the existing files, then implement the solution.",
|
|
315
454
|
].join("\n"));
|
|
316
455
|
|
|
317
|
-
return sections.join("\n\n");
|
|
456
|
+
return { prompt: sections.join("\n\n"), promptBudget };
|
|
318
457
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// guards.js — Short-circuit checks that skip LLM invocation when unnecessary
|
|
4
|
+
//
|
|
5
|
+
// Phase 4 Step 10a: Restores the guards that existed in the per-task handlers
|
|
6
|
+
// (transform.js, fix-code.js, maintain-features.js, maintain-library.js)
|
|
7
|
+
// before Phase 4 convergence replaced them with unconditional runHybridSession().
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { readCumulativeCost } from "./telemetry.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Task-to-guard mapping. Each task has an ordered list of guards.
|
|
16
|
+
* Guards are checked in order; the first that triggers returns skip.
|
|
17
|
+
*/
|
|
18
|
+
const TASK_GUARDS = {
|
|
19
|
+
"transform": ["no-mission", "mission-complete", "budget-exhausted"],
|
|
20
|
+
"fix-code": ["tests-pass", "budget-exhausted"],
|
|
21
|
+
"maintain-features": ["mission-complete", "budget-exhausted"],
|
|
22
|
+
"maintain-library": ["mission-complete", "budget-exhausted"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check whether a task should be skipped before invoking the LLM.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} taskName - Task name (e.g. "transform", "fix-code")
|
|
29
|
+
* @param {Object} config - Parsed agentic-lib config
|
|
30
|
+
* @param {string} workspacePath - Path to the workspace
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {Object} [options.logger] - Logger interface
|
|
33
|
+
* @returns {{ skip: boolean, reason: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function checkGuards(taskName, config, workspacePath, { logger } = {}) {
|
|
36
|
+
const guards = TASK_GUARDS[taskName];
|
|
37
|
+
if (!guards) return { skip: false, reason: "" };
|
|
38
|
+
|
|
39
|
+
const wsPath = resolve(workspacePath);
|
|
40
|
+
|
|
41
|
+
for (const guard of guards) {
|
|
42
|
+
switch (guard) {
|
|
43
|
+
case "no-mission": {
|
|
44
|
+
const missionPath = config.paths?.mission?.path || "MISSION.md";
|
|
45
|
+
if (!existsSync(resolve(wsPath, missionPath))) {
|
|
46
|
+
return { skip: true, reason: `No mission file found at ${missionPath}` };
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "mission-complete": {
|
|
52
|
+
// Match the old behaviour: skip if MISSION_COMPLETE.md exists
|
|
53
|
+
// unless supervisor is in maintenance mode
|
|
54
|
+
if (existsSync(resolve(wsPath, "MISSION_COMPLETE.md")) && config.supervisor !== "maintenance") {
|
|
55
|
+
return { skip: true, reason: "Mission already complete (MISSION_COMPLETE.md exists)" };
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "tests-pass": {
|
|
61
|
+
const testCommand = config.testScript || "npm test";
|
|
62
|
+
try {
|
|
63
|
+
execSync(testCommand, { cwd: wsPath, encoding: "utf8", timeout: 120000, stdio: "pipe" });
|
|
64
|
+
return { skip: true, reason: "Tests already pass — nothing to fix" };
|
|
65
|
+
} catch {
|
|
66
|
+
// Tests fail — proceed with fix-code
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "budget-exhausted": {
|
|
72
|
+
const budget = config.transformationBudget || 0;
|
|
73
|
+
if (budget > 0) {
|
|
74
|
+
const intentionFilepath = config.intentionBot?.intentionFilepath;
|
|
75
|
+
const filePath = intentionFilepath ? resolve(wsPath, intentionFilepath) : null;
|
|
76
|
+
const cumulativeCost = readCumulativeCost(filePath);
|
|
77
|
+
if (cumulativeCost >= budget) {
|
|
78
|
+
return { skip: true, reason: `Transformation budget exhausted (${cumulativeCost}/${budget})` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { skip: false, reason: "" };
|
|
87
|
+
}
|
|
@@ -62,6 +62,7 @@ function formatToolArgs(toolName, args) {
|
|
|
62
62
|
* @param {string} [options.userPrompt] - Override user prompt (instead of default mission prompt)
|
|
63
63
|
* @param {string[]} [options.writablePaths] - Writable paths for tool safety (default: workspace)
|
|
64
64
|
* @param {number} [options.maxRetries=2] - Max retries on rate-limit errors
|
|
65
|
+
* @param {number} [options.maxToolCalls] - Max tool calls before graceful stop (undefined = unlimited)
|
|
65
66
|
* @param {Object} [options.logger]
|
|
66
67
|
* @returns {Promise<HybridResult>}
|
|
67
68
|
*/
|
|
@@ -75,6 +76,7 @@ export async function runHybridSession({
|
|
|
75
76
|
userPrompt,
|
|
76
77
|
writablePaths,
|
|
77
78
|
maxRetries = 2,
|
|
79
|
+
maxToolCalls,
|
|
78
80
|
logger = defaultLogger,
|
|
79
81
|
}) {
|
|
80
82
|
const { CopilotClient, approveAll, defineTool } = await getSDK();
|
|
@@ -145,7 +147,7 @@ export async function runHybridSession({
|
|
|
145
147
|
const systemPrompt = basePrompt + NARRATIVE_INSTRUCTION;
|
|
146
148
|
|
|
147
149
|
// ── Session config ─────────────────────────────────────────────────
|
|
148
|
-
logger.info(`[
|
|
150
|
+
logger.info(`[agentic-lib] Creating session (model=${model}, workspace=${wsPath})`);
|
|
149
151
|
|
|
150
152
|
const client = new CopilotClient({
|
|
151
153
|
env: { ...process.env, GITHUB_TOKEN: copilotToken, GH_TOKEN: copilotToken },
|
|
@@ -159,6 +161,11 @@ export async function runHybridSession({
|
|
|
159
161
|
workingDirectory: wsPath,
|
|
160
162
|
hooks: {
|
|
161
163
|
onPreToolUse: (input) => {
|
|
164
|
+
// Enforce tool-call budget
|
|
165
|
+
if (maxToolCalls && metrics.toolCalls.length >= maxToolCalls) {
|
|
166
|
+
logger.warning(` [tool] Budget reached (${maxToolCalls} calls) — denying ${input.toolName}`);
|
|
167
|
+
return { action: "deny", reason: `Tool call budget exhausted (${maxToolCalls} max). Wrap up your work.` };
|
|
168
|
+
}
|
|
162
169
|
const n = metrics.toolCalls.length + 1;
|
|
163
170
|
const elapsed = ((Date.now() - metrics.startTime) / 1000).toFixed(0);
|
|
164
171
|
metrics.toolCalls.push({ tool: input.toolName, time: Date.now(), args: input.toolArgs });
|
|
@@ -211,15 +218,15 @@ export async function runHybridSession({
|
|
|
211
218
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
212
219
|
try {
|
|
213
220
|
session = await client.createSession(sessionConfig);
|
|
214
|
-
logger.info(`[
|
|
221
|
+
logger.info(`[agentic-lib] Session: ${session.sessionId}`);
|
|
215
222
|
break;
|
|
216
223
|
} catch (err) {
|
|
217
224
|
if (isRateLimitError(err) && attempt < maxRetries) {
|
|
218
225
|
const delayMs = retryDelayMs(err, attempt);
|
|
219
|
-
logger.warning(`[
|
|
226
|
+
logger.warning(`[agentic-lib] Rate limit on session creation — waiting ${Math.round(delayMs / 1000)}s (retry ${attempt + 1}/${maxRetries})`);
|
|
220
227
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
221
228
|
} else {
|
|
222
|
-
logger.error(`[
|
|
229
|
+
logger.error(`[agentic-lib] Failed to create session: ${err.message}`);
|
|
223
230
|
await client.stop();
|
|
224
231
|
throw err;
|
|
225
232
|
}
|
|
@@ -251,13 +258,13 @@ export async function runHybridSession({
|
|
|
251
258
|
// ── Try autopilot mode ──────────────────────────────────────────────
|
|
252
259
|
try {
|
|
253
260
|
await session.rpc.mode.set({ mode: "autopilot" });
|
|
254
|
-
logger.info("[
|
|
261
|
+
logger.info("[agentic-lib] Autopilot mode: active");
|
|
255
262
|
} catch {
|
|
256
|
-
logger.info("[
|
|
263
|
+
logger.info("[agentic-lib] Autopilot mode not available — using default mode");
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
// ── Send mission prompt ─────────────────────────────────────────────
|
|
260
|
-
logger.info("[
|
|
267
|
+
logger.info("[agentic-lib] Sending mission...\n");
|
|
261
268
|
|
|
262
269
|
const prompt = userPrompt || [
|
|
263
270
|
`# Mission\n\n${missionText}`,
|
|
@@ -281,10 +288,10 @@ export async function runHybridSession({
|
|
|
281
288
|
} catch (err) {
|
|
282
289
|
if (isRateLimitError(err) && attempt < maxRetries) {
|
|
283
290
|
const delayMs = retryDelayMs(err, attempt);
|
|
284
|
-
logger.warning(`[
|
|
291
|
+
logger.warning(`[agentic-lib] Rate limit on sendAndWait — waiting ${Math.round(delayMs / 1000)}s (retry ${attempt + 1}/${maxRetries})`);
|
|
285
292
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
286
293
|
} else {
|
|
287
|
-
logger.error(`[
|
|
294
|
+
logger.error(`[agentic-lib] Session error: ${err.message}`);
|
|
288
295
|
response = null;
|
|
289
296
|
endReason = "error";
|
|
290
297
|
break;
|
package/src/copilot/sdk.js
CHANGED
|
@@ -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(
|
|
17
|
-
resolve(
|
|
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;
|