@xn-intenton-z2a/agentic-lib 7.1.60 → 7.1.62
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 +60 -7
- package/.github/workflows/agentic-lib-init.yml +7 -7
- package/.github/workflows/agentic-lib-schedule.yml +4 -4
- package/.github/workflows/agentic-lib-test.yml +12 -4
- package/.github/workflows/agentic-lib-workflow.yml +74 -32
- package/README.md +51 -8
- package/agentic-lib.toml +98 -19
- package/bin/agentic-lib.js +69 -5
- package/package.json +12 -12
- package/src/actions/agentic-step/action.yml +6 -2
- package/src/actions/agentic-step/config-loader.js +113 -57
- package/src/actions/agentic-step/copilot.js +259 -16
- package/src/actions/agentic-step/index.js +39 -2
- package/src/actions/agentic-step/logging.js +75 -0
- package/src/actions/agentic-step/tasks/discussions.js +17 -1
- package/src/actions/agentic-step/tasks/maintain-features.js +21 -1
- package/src/actions/agentic-step/tasks/maintain-library.js +7 -0
- package/src/actions/agentic-step/tasks/review-issue.js +6 -1
- package/src/actions/agentic-step/tasks/supervise.js +36 -4
- package/src/actions/agentic-step/tasks/transform.js +49 -6
- package/src/agents/agent-discussion-bot.md +20 -0
- package/src/agents/agent-supervisor.md +17 -0
- package/src/agents/agentic-lib.yml +1 -3
- package/src/iterate.js +285 -0
- package/src/mcp/server.js +24 -127
- package/src/seeds/zero-README.md +2 -2
- package/src/seeds/zero-package.json +1 -1
|
@@ -5,11 +5,154 @@
|
|
|
5
5
|
// Extracts repeated patterns from the 8 task handlers into reusable functions.
|
|
6
6
|
|
|
7
7
|
import { CopilotClient, approveAll } from "@github/copilot-sdk";
|
|
8
|
-
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
8
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
import { createAgentTools } from "./tools.js";
|
|
11
11
|
import * as core from "@actions/core";
|
|
12
12
|
|
|
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"]);
|
|
17
|
+
|
|
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 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
|
+
* @returns {Array} Filtered issues
|
|
93
|
+
*/
|
|
94
|
+
export function filterIssues(issues, options = {}) {
|
|
95
|
+
const { staleDays = 30, excludeBotOnly = true } = options;
|
|
96
|
+
const cutoff = Date.now() - staleDays * 86400000;
|
|
97
|
+
|
|
98
|
+
return issues.filter((issue) => {
|
|
99
|
+
const lastActivity = new Date(issue.updated_at || issue.created_at).getTime();
|
|
100
|
+
if (lastActivity < cutoff) return false;
|
|
101
|
+
|
|
102
|
+
if (excludeBotOnly) {
|
|
103
|
+
const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name));
|
|
104
|
+
const botLabels = ["automated", "stale", "bot", "wontfix"];
|
|
105
|
+
if (labels.length > 0 && labels.every((l) => botLabels.includes(l))) return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a compact summary of an issue for inclusion in prompts.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} issue - GitHub issue object
|
|
116
|
+
* @param {number} [bodyLimit=500] - Max chars for issue body
|
|
117
|
+
* @returns {string} Compact issue summary
|
|
118
|
+
*/
|
|
119
|
+
export function summariseIssue(issue, bodyLimit = 500) {
|
|
120
|
+
const labels = (issue.labels || []).map((l) => (typeof l === "string" ? l : l.name)).join(", ") || "no labels";
|
|
121
|
+
const age = Math.floor((Date.now() - new Date(issue.created_at).getTime()) / 86400000);
|
|
122
|
+
const body = (issue.body || "").substring(0, bodyLimit).replace(/\n+/g, " ").trim();
|
|
123
|
+
return `#${issue.number}: ${issue.title} [${labels}] (${age}d old)${body ? `\n ${body}` : ""}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extract a structured summary from a feature markdown file.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} content - Feature file content
|
|
130
|
+
* @param {string} fileName - Feature file name
|
|
131
|
+
* @returns {string} Structured feature summary
|
|
132
|
+
*/
|
|
133
|
+
export function extractFeatureSummary(content, fileName) {
|
|
134
|
+
const lines = content.split("\n");
|
|
135
|
+
const title = lines.find((l) => l.startsWith("#"))?.replace(/^#+\s*/, "") || fileName;
|
|
136
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
137
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
138
|
+
const total = checked + unchecked;
|
|
139
|
+
|
|
140
|
+
const parts = [`Feature: ${title}`];
|
|
141
|
+
if (total > 0) {
|
|
142
|
+
parts.push(`Status: ${checked}/${total} items complete`);
|
|
143
|
+
const remaining = [];
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (/- \[ \]/.test(line)) {
|
|
146
|
+
remaining.push(line.replace(/^[\s-]*\[ \]\s*/, "").trim());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (remaining.length > 0) {
|
|
150
|
+
parts.push(`Remaining: ${remaining.map((r) => `[ ] ${r}`).join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return parts.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
13
156
|
/**
|
|
14
157
|
* Build the CopilotClient options for authentication.
|
|
15
158
|
*
|
|
@@ -38,6 +181,34 @@ export function buildClientOptions(githubToken) {
|
|
|
38
181
|
return { env };
|
|
39
182
|
}
|
|
40
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Log tuning parameter application with profile context.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} param - Parameter name
|
|
188
|
+
* @param {*} value - Resolved value being applied
|
|
189
|
+
* @param {string} profileName - Profile the default came from
|
|
190
|
+
* @param {string} model - Model being used
|
|
191
|
+
* @param {Object} [clip] - Optional clipping info { available, requested }
|
|
192
|
+
*/
|
|
193
|
+
export function logTuningParam(param, value, profileName, model, clip) {
|
|
194
|
+
const clipInfo = clip
|
|
195
|
+
? ` (requested=${clip.requested}, available=${clip.available}, excess=${clip.requested - clip.available})`
|
|
196
|
+
: "";
|
|
197
|
+
core.info(`[tuning] ${param}=${value} profile=${profileName} model=${model}${clipInfo}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if a model supports reasoningEffort.
|
|
202
|
+
* Uses the static allowlist; at runtime the SDK's models.list() could be used
|
|
203
|
+
* but that requires an authenticated client which isn't available at config time.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} model - Model name
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
export function supportsReasoningEffort(model) {
|
|
209
|
+
return MODELS_SUPPORTING_REASONING_EFFORT.has(model);
|
|
210
|
+
}
|
|
211
|
+
|
|
41
212
|
/**
|
|
42
213
|
* Run a Copilot SDK session and return the response.
|
|
43
214
|
* Handles the full lifecycle: create client → create session → send → stop.
|
|
@@ -49,11 +220,21 @@ export function buildClientOptions(githubToken) {
|
|
|
49
220
|
* @param {string[]} options.writablePaths - Paths the agent may modify
|
|
50
221
|
* @param {string} [options.githubToken] - Optional token; falls back to COPILOT_GITHUB_TOKEN env var.
|
|
51
222
|
* @param {Object} [options.tuning] - Tuning config (reasoningEffort, infiniteSessions)
|
|
223
|
+
* @param {string} [options.profileName] - Profile name for logging
|
|
52
224
|
* @returns {Promise<{content: string, tokensUsed: number}>}
|
|
53
225
|
*/
|
|
54
|
-
export async function runCopilotTask({
|
|
226
|
+
export async function runCopilotTask({
|
|
227
|
+
model,
|
|
228
|
+
systemMessage,
|
|
229
|
+
prompt,
|
|
230
|
+
writablePaths,
|
|
231
|
+
githubToken,
|
|
232
|
+
tuning,
|
|
233
|
+
profileName,
|
|
234
|
+
}) {
|
|
235
|
+
const profile = profileName || "unknown";
|
|
55
236
|
core.info(
|
|
56
|
-
`[copilot] Creating client (model=${model}, promptLen=${prompt.length}, writablePaths=${writablePaths.length}, tuning=${tuning?.reasoningEffort || "default"})`,
|
|
237
|
+
`[copilot] Creating client (model=${model}, promptLen=${prompt.length}, writablePaths=${writablePaths.length}, tuning=${tuning?.reasoningEffort || "default"}, profile=${profile})`,
|
|
57
238
|
);
|
|
58
239
|
|
|
59
240
|
const clientOptions = buildClientOptions(githubToken);
|
|
@@ -68,12 +249,32 @@ export async function runCopilotTask({ model, systemMessage, prompt, writablePat
|
|
|
68
249
|
onPermissionRequest: approveAll,
|
|
69
250
|
workingDirectory: process.cwd(),
|
|
70
251
|
};
|
|
71
|
-
|
|
72
|
-
|
|
252
|
+
|
|
253
|
+
// Only set reasoningEffort for models that support it
|
|
254
|
+
if (tuning?.reasoningEffort && tuning.reasoningEffort !== "none") {
|
|
255
|
+
if (supportsReasoningEffort(model)) {
|
|
256
|
+
sessionConfig.reasoningEffort = tuning.reasoningEffort;
|
|
257
|
+
logTuningParam("reasoningEffort", tuning.reasoningEffort, profile, model);
|
|
258
|
+
} else {
|
|
259
|
+
core.info(
|
|
260
|
+
`[copilot] Skipping reasoningEffort="${tuning.reasoningEffort}" — not supported by model "${model}". Only supported by: ${[...MODELS_SUPPORTING_REASONING_EFFORT].join(", ")}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
73
263
|
}
|
|
264
|
+
|
|
74
265
|
if (tuning?.infiniteSessions === true) {
|
|
75
266
|
sessionConfig.infiniteSessions = {};
|
|
267
|
+
logTuningParam("infiniteSessions", true, profile, model);
|
|
76
268
|
}
|
|
269
|
+
|
|
270
|
+
// Log scan/context tuning params
|
|
271
|
+
if (tuning?.featuresScan) logTuningParam("featuresScan", tuning.featuresScan, profile, model);
|
|
272
|
+
if (tuning?.sourceScan) logTuningParam("sourceScan", tuning.sourceScan, profile, model);
|
|
273
|
+
if (tuning?.sourceContent) logTuningParam("sourceContent", tuning.sourceContent, profile, model);
|
|
274
|
+
if (tuning?.issuesScan) logTuningParam("issuesScan", tuning.issuesScan, profile, model);
|
|
275
|
+
if (tuning?.documentSummary) logTuningParam("documentSummary", tuning.documentSummary, profile, model);
|
|
276
|
+
if (tuning?.discussionComments) logTuningParam("discussionComments", tuning.discussionComments, profile, model);
|
|
277
|
+
|
|
77
278
|
const session = await client.createSession(sessionConfig);
|
|
78
279
|
core.info(`[copilot] Session created: ${session.sessionId}`);
|
|
79
280
|
|
|
@@ -105,7 +306,9 @@ export async function runCopilotTask({ model, systemMessage, prompt, writablePat
|
|
|
105
306
|
totalInputTokens += input;
|
|
106
307
|
totalOutputTokens += output;
|
|
107
308
|
totalCost += cost;
|
|
108
|
-
core.info(
|
|
309
|
+
core.info(
|
|
310
|
+
`[copilot] event=${eventType}: model=${d.model} input=${input} output=${output} cacheRead=${cacheRead} cost=${cost}`,
|
|
311
|
+
);
|
|
109
312
|
} else if (eventType === "session.idle") {
|
|
110
313
|
core.info(`[copilot] event=${eventType}`);
|
|
111
314
|
} else if (eventType === "session.error") {
|
|
@@ -153,26 +356,66 @@ export function readOptionalFile(filePath, limit) {
|
|
|
153
356
|
* @param {number} [options.fileLimit=10] - Max files to return
|
|
154
357
|
* @param {number} [options.contentLimit] - Max chars per file content
|
|
155
358
|
* @param {boolean} [options.recursive=false] - Scan recursively
|
|
359
|
+
* @param {boolean} [options.sortByMtime=false] - Sort files by modification time (most recent first)
|
|
360
|
+
* @param {boolean} [options.clean=false] - Strip source noise (license headers, blank lines, linter directives)
|
|
361
|
+
* @param {boolean} [options.outline=false] - Generate structural outline when content exceeds limit
|
|
156
362
|
* @returns {Array<{name: string, content: string}>}
|
|
157
363
|
*/
|
|
158
364
|
export function scanDirectory(dirPath, extensions, options = {}) {
|
|
159
|
-
const { fileLimit = 10, contentLimit, recursive = false } = options;
|
|
365
|
+
const { fileLimit = 10, contentLimit, recursive = false, sortByMtime = false, clean = false, outline = false } = options;
|
|
160
366
|
const exts = Array.isArray(extensions) ? extensions : [extensions];
|
|
161
367
|
|
|
162
368
|
if (!existsSync(dirPath)) return [];
|
|
163
369
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
370
|
+
const allFiles = readdirSync(dirPath, recursive ? { recursive: true } : undefined).filter((f) =>
|
|
371
|
+
exts.some((ext) => String(f).endsWith(ext)),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if (sortByMtime) {
|
|
375
|
+
allFiles.sort((a, b) => {
|
|
168
376
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
core.debug(`[scanDirectory] ${join(dirPath, f)}: ${err.message}`);
|
|
173
|
-
return { name: f, content: "" };
|
|
377
|
+
return statSync(join(dirPath, String(b))).mtimeMs - statSync(join(dirPath, String(a))).mtimeMs;
|
|
378
|
+
} catch {
|
|
379
|
+
return 0;
|
|
174
380
|
}
|
|
175
381
|
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const clipped = allFiles.slice(0, fileLimit);
|
|
385
|
+
if (allFiles.length > fileLimit) {
|
|
386
|
+
core.info(
|
|
387
|
+
`[scanDirectory] Clipped ${dirPath}: ${allFiles.length} files found, returning ${fileLimit} (excess=${allFiles.length - fileLimit})`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return clipped.map((f) => {
|
|
392
|
+
const fileName = String(f);
|
|
393
|
+
try {
|
|
394
|
+
let raw = readFileSync(join(dirPath, fileName), "utf8");
|
|
395
|
+
if (clean) raw = cleanSource(raw);
|
|
396
|
+
|
|
397
|
+
let content;
|
|
398
|
+
if (outline && contentLimit && raw.length > contentLimit) {
|
|
399
|
+
const outlineText = generateOutline(raw, fileName);
|
|
400
|
+
const halfLimit = Math.floor(contentLimit / 2);
|
|
401
|
+
content = outlineText + "\n\n" + raw.substring(0, halfLimit);
|
|
402
|
+
core.info(
|
|
403
|
+
`[scanDirectory] Outlined ${fileName}: ${raw.length} chars → outline + ${halfLimit} chars`,
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
content = contentLimit ? raw.substring(0, contentLimit) : raw;
|
|
407
|
+
if (contentLimit && raw.length > contentLimit) {
|
|
408
|
+
core.info(
|
|
409
|
+
`[scanDirectory] Clipped ${fileName}: ${raw.length} chars, returning ${contentLimit} (excess=${raw.length - contentLimit})`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { name: fileName, content };
|
|
414
|
+
} catch (err) {
|
|
415
|
+
core.debug(`[scanDirectory] ${join(dirPath, fileName)}: ${err.message}`);
|
|
416
|
+
return { name: fileName, content: "" };
|
|
417
|
+
}
|
|
418
|
+
});
|
|
176
419
|
}
|
|
177
420
|
|
|
178
421
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as core from "@actions/core";
|
|
9
9
|
import * as github from "@actions/github";
|
|
10
10
|
import { loadConfig, getWritablePaths } from "./config-loader.js";
|
|
11
|
-
import { logActivity } from "./logging.js";
|
|
11
|
+
import { logActivity, generateClosingNotes } from "./logging.js";
|
|
12
12
|
import { readFileSync } from "fs";
|
|
13
13
|
|
|
14
14
|
// Task implementations
|
|
@@ -43,7 +43,7 @@ async function run() {
|
|
|
43
43
|
const issueNumber = core.getInput("issue-number");
|
|
44
44
|
const prNumber = core.getInput("pr-number");
|
|
45
45
|
const writablePathsOverride = core.getInput("writable-paths");
|
|
46
|
-
const
|
|
46
|
+
const testCommandInput = core.getInput("test-command");
|
|
47
47
|
const discussionUrl = core.getInput("discussion-url");
|
|
48
48
|
const model = core.getInput("model");
|
|
49
49
|
|
|
@@ -52,6 +52,7 @@ async function run() {
|
|
|
52
52
|
// Load config
|
|
53
53
|
const config = loadConfig(configPath);
|
|
54
54
|
const writablePaths = getWritablePaths(config, writablePathsOverride);
|
|
55
|
+
const testCommand = testCommandInput || config.testScript;
|
|
55
56
|
|
|
56
57
|
// Load instructions if provided
|
|
57
58
|
let instructions = "";
|
|
@@ -95,6 +96,35 @@ async function run() {
|
|
|
95
96
|
if (result.prNumber) core.setOutput("pr-number", String(result.prNumber));
|
|
96
97
|
if (result.tokensUsed) core.setOutput("tokens-used", String(result.tokensUsed));
|
|
97
98
|
if (result.model) core.setOutput("model", result.model);
|
|
99
|
+
if (result.action) core.setOutput("action", result.action);
|
|
100
|
+
if (result.actionArg) core.setOutput("action-arg", result.actionArg);
|
|
101
|
+
|
|
102
|
+
// Compute limits status for enriched logging
|
|
103
|
+
const limitsStatus = [
|
|
104
|
+
{ name: "transformation-budget", valueNum: 0, capacityNum: config.transformationBudget || 0, value: `0/${config.transformationBudget || 0}`, remaining: `${config.transformationBudget || 0} remaining`, status: "" },
|
|
105
|
+
{ name: "max-feature-issues", valueNum: 0, capacityNum: config.featureDevelopmentIssuesWipLimit, value: `?/${config.featureDevelopmentIssuesWipLimit}`, remaining: "?", status: "" },
|
|
106
|
+
{ name: "max-maintenance-issues", valueNum: 0, capacityNum: config.maintenanceIssuesWipLimit, value: `?/${config.maintenanceIssuesWipLimit}`, remaining: "?", status: "" },
|
|
107
|
+
{ name: "max-attempts-per-issue", valueNum: 0, capacityNum: config.attemptsPerIssue, value: `?/${config.attemptsPerIssue}`, remaining: "?", status: task === "resolve-issue" ? "" : "n/a" },
|
|
108
|
+
{ name: "max-attempts-per-branch", valueNum: 0, capacityNum: config.attemptsPerBranch, value: `?/${config.attemptsPerBranch}`, remaining: "?", status: task === "fix-code" ? "" : "n/a" },
|
|
109
|
+
{ name: "features", valueNum: 0, capacityNum: config.paths?.features?.limit || 4, value: `?/${config.paths?.features?.limit || 4}`, remaining: "?", status: ["maintain-features", "transform"].includes(task) ? "" : "n/a" },
|
|
110
|
+
{ name: "library", valueNum: 0, capacityNum: config.paths?.library?.limit || 32, value: `?/${config.paths?.library?.limit || 32}`, remaining: "?", status: task === "maintain-library" ? "" : "n/a" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// Merge task-reported limits if available
|
|
114
|
+
if (result.limitsStatus) {
|
|
115
|
+
for (const reported of result.limitsStatus) {
|
|
116
|
+
const existing = limitsStatus.find((ls) => ls.name === reported.name);
|
|
117
|
+
if (existing) Object.assign(existing, reported);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const closingNotes = result.closingNotes || generateClosingNotes(limitsStatus);
|
|
122
|
+
const profileName = config.tuning?.profileName || "unknown";
|
|
123
|
+
|
|
124
|
+
// Transformation cost: 1 for code-changing tasks, 0 otherwise
|
|
125
|
+
const COST_TASKS = ["transform", "fix-code"];
|
|
126
|
+
const isNop = result.outcome === "nop" || result.outcome === "error";
|
|
127
|
+
const transformationCost = COST_TASKS.includes(task) && !isNop ? 1 : 0;
|
|
98
128
|
|
|
99
129
|
// Log to intentïon.md (commit-if-changed excludes this on non-default branches)
|
|
100
130
|
const intentionFilepath = config.intentionBot?.intentionFilepath;
|
|
@@ -114,6 +144,13 @@ async function run() {
|
|
|
114
144
|
model: result.model || model,
|
|
115
145
|
details: result.details,
|
|
116
146
|
workflowUrl: `${process.env.GITHUB_SERVER_URL}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}`,
|
|
147
|
+
profile: profileName,
|
|
148
|
+
changes: result.changes,
|
|
149
|
+
contextNotes: result.contextNotes,
|
|
150
|
+
limitsStatus,
|
|
151
|
+
promptBudget: result.promptBudget,
|
|
152
|
+
closingNotes,
|
|
153
|
+
transformationCost,
|
|
117
154
|
});
|
|
118
155
|
}
|
|
119
156
|
|
|
@@ -27,6 +27,13 @@ import * as core from "@actions/core";
|
|
|
27
27
|
* @param {string} [options.model] - Model used
|
|
28
28
|
* @param {string} [options.details] - Additional details
|
|
29
29
|
* @param {string} [options.workflowUrl] - URL to the workflow run
|
|
30
|
+
* @param {string} [options.profile] - Active tuning profile name
|
|
31
|
+
* @param {Array} [options.changes] - List of file changes { action, file, sizeInfo }
|
|
32
|
+
* @param {string} [options.contextNotes] - English notes about task observations
|
|
33
|
+
* @param {Array} [options.limitsStatus] - Limit status entries { name, value, remaining, status }
|
|
34
|
+
* @param {Array} [options.promptBudget] - Prompt budget entries { section, size, files, notes }
|
|
35
|
+
* @param {string} [options.closingNotes] - Auto-generated limit concern notes
|
|
36
|
+
* @param {number} [options.transformationCost] - Transformation cost for this entry (0 or 1)
|
|
30
37
|
*/
|
|
31
38
|
export function logActivity({
|
|
32
39
|
filepath,
|
|
@@ -43,6 +50,13 @@ export function logActivity({
|
|
|
43
50
|
model,
|
|
44
51
|
details,
|
|
45
52
|
workflowUrl,
|
|
53
|
+
profile,
|
|
54
|
+
changes,
|
|
55
|
+
contextNotes,
|
|
56
|
+
limitsStatus,
|
|
57
|
+
promptBudget,
|
|
58
|
+
closingNotes,
|
|
59
|
+
transformationCost,
|
|
46
60
|
}) {
|
|
47
61
|
const dir = dirname(filepath);
|
|
48
62
|
if (!existsSync(dir)) {
|
|
@@ -56,6 +70,7 @@ export function logActivity({
|
|
|
56
70
|
if (prNumber) parts.push(`**PR:** #${prNumber}`);
|
|
57
71
|
if (commitUrl) parts.push(`**Commit:** [${commitUrl}](${commitUrl})`);
|
|
58
72
|
if (model) parts.push(`**Model:** ${model}`);
|
|
73
|
+
if (profile) parts.push(`**Profile:** ${profile}`);
|
|
59
74
|
if (tokensUsed) parts.push(`**Token Count:** ${tokensUsed} (in: ${inputTokens || 0}, out: ${outputTokens || 0})`);
|
|
60
75
|
if (cost) parts.push(`**Model Invocations:** ${cost}`);
|
|
61
76
|
if (durationMs) {
|
|
@@ -63,7 +78,38 @@ export function logActivity({
|
|
|
63
78
|
const mins = (durationMs / 60000).toFixed(1);
|
|
64
79
|
parts.push(`**Duration:** ${secs}s (~${mins} GitHub Actions min)`);
|
|
65
80
|
}
|
|
81
|
+
if (transformationCost != null) parts.push(`**agentic-lib transformation cost:** ${transformationCost}`);
|
|
66
82
|
if (workflowUrl) parts.push(`**Workflow:** [${workflowUrl}](${workflowUrl})`);
|
|
83
|
+
if (changes && changes.length > 0) {
|
|
84
|
+
parts.push("", "### What Changed");
|
|
85
|
+
for (const c of changes) {
|
|
86
|
+
parts.push(`- ${c.action}: \`${c.file}\`${c.sizeInfo ? ` (${c.sizeInfo})` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (contextNotes) {
|
|
90
|
+
parts.push("", "### Context Notes");
|
|
91
|
+
parts.push(contextNotes);
|
|
92
|
+
}
|
|
93
|
+
if (limitsStatus && limitsStatus.length > 0) {
|
|
94
|
+
parts.push("", "### Limits Status");
|
|
95
|
+
parts.push("| Limit | Value | Capacity | Status |");
|
|
96
|
+
parts.push("|---|---|---|---|");
|
|
97
|
+
for (const ls of limitsStatus) {
|
|
98
|
+
parts.push(`| ${ls.name} | ${ls.value} | ${ls.remaining} remaining | ${ls.status || ""} |`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (promptBudget && promptBudget.length > 0) {
|
|
102
|
+
parts.push("", "### Prompt Budget");
|
|
103
|
+
parts.push("| Section | Size | Files | Notes |");
|
|
104
|
+
parts.push("|---|---|---|---|");
|
|
105
|
+
for (const pb of promptBudget) {
|
|
106
|
+
parts.push(`| ${pb.section} | ${pb.size} chars | ${pb.files || "—"} | ${pb.notes || ""} |`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (closingNotes) {
|
|
110
|
+
parts.push("", "### Closing Notes");
|
|
111
|
+
parts.push(closingNotes);
|
|
112
|
+
}
|
|
67
113
|
if (details) {
|
|
68
114
|
parts.push("");
|
|
69
115
|
parts.push(details);
|
|
@@ -92,6 +138,35 @@ export function logActivity({
|
|
|
92
138
|
}
|
|
93
139
|
}
|
|
94
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Generate closing notes from limits status, flagging limits at or approaching capacity.
|
|
143
|
+
*
|
|
144
|
+
* @param {Array} limitsStatus - Array of { name, value, remaining, status, valueNum, capacityNum }
|
|
145
|
+
* @returns {string} Closing notes text
|
|
146
|
+
*/
|
|
147
|
+
export function generateClosingNotes(limitsStatus) {
|
|
148
|
+
if (!limitsStatus || limitsStatus.length === 0) return "";
|
|
149
|
+
|
|
150
|
+
const concerns = [];
|
|
151
|
+
let anyChecked = false;
|
|
152
|
+
for (const ls of limitsStatus) {
|
|
153
|
+
if (ls.status === "n/a") continue;
|
|
154
|
+
const used = ls.valueNum || 0;
|
|
155
|
+
const capacity = ls.capacityNum || 0;
|
|
156
|
+
if (capacity === 0) continue;
|
|
157
|
+
anyChecked = true;
|
|
158
|
+
const pct = Math.round((used / capacity) * 100);
|
|
159
|
+
if (pct >= 100) {
|
|
160
|
+
concerns.push(`${ls.name} at capacity (${ls.value}) — actions will be blocked.`);
|
|
161
|
+
} else if (pct >= 80) {
|
|
162
|
+
concerns.push(`${ls.name} approaching capacity (${ls.value}).`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!anyChecked) return "";
|
|
167
|
+
return concerns.length > 0 ? concerns.join("\n") : "All limits within normal range.";
|
|
168
|
+
}
|
|
169
|
+
|
|
95
170
|
/**
|
|
96
171
|
* Log a safety check outcome to the GitHub Actions log.
|
|
97
172
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// and provides status updates. Uses the Copilot SDK for natural conversation.
|
|
7
7
|
|
|
8
8
|
import * as core from "@actions/core";
|
|
9
|
-
import { existsSync } from "fs";
|
|
9
|
+
import { existsSync, writeFileSync } from "fs";
|
|
10
10
|
import { runCopilotTask, readOptionalFile, scanDirectory } from "../copilot.js";
|
|
11
11
|
|
|
12
12
|
const BOT_LOGINS = ["github-actions[bot]", "github-actions"];
|
|
@@ -175,6 +175,22 @@ export async function discussions(context) {
|
|
|
175
175
|
const replyBody = content.replace(/\[ACTION:\S+?\].+/, "").trim();
|
|
176
176
|
|
|
177
177
|
core.info(`Discussion bot action: ${action}, arg: ${actionArg}`);
|
|
178
|
+
|
|
179
|
+
// Write MISSION_COMPLETE.md signal when bot declares mission complete
|
|
180
|
+
if (action === "mission-complete") {
|
|
181
|
+
const signal = [
|
|
182
|
+
"# Mission Complete",
|
|
183
|
+
"",
|
|
184
|
+
`- **Timestamp:** ${new Date().toISOString()}`,
|
|
185
|
+
`- **Detected by:** discussions`,
|
|
186
|
+
`- **Reason:** ${actionArg || "Declared via discussion bot"}`,
|
|
187
|
+
"",
|
|
188
|
+
"This file was created automatically. To restart transformations, delete this file or run `npx @xn-intenton-z2a/agentic-lib init --reseed`.",
|
|
189
|
+
].join("\n");
|
|
190
|
+
writeFileSync("MISSION_COMPLETE.md", signal);
|
|
191
|
+
core.info("Mission complete signal written (MISSION_COMPLETE.md)");
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
await postReply(octokit, discussion.nodeId, replyBody);
|
|
179
195
|
|
|
180
196
|
const argSuffix = actionArg ? ` (${actionArg})` : "";
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// Reviews existing features, creates new ones from mission/library analysis,
|
|
6
6
|
// prunes completed/irrelevant features, and ensures quality.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection, extractFeatureSummary } from "../copilot.js";
|
|
10
|
+
import { checkWipLimit } from "../safety.js";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Maintain the feature set — create, update, or prune feature files.
|
|
@@ -17,10 +19,28 @@ export async function maintainFeatures(context) {
|
|
|
17
19
|
const { config, instructions, writablePaths, model, octokit, repo } = context;
|
|
18
20
|
const t = config.tuning || {};
|
|
19
21
|
|
|
22
|
+
// Check mission-complete signal
|
|
23
|
+
if (existsSync("MISSION_COMPLETE.md")) {
|
|
24
|
+
return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check maintenance WIP limit
|
|
28
|
+
const wipCheck = await checkWipLimit(octokit, repo, "maintenance", config.maintenanceIssuesWipLimit);
|
|
29
|
+
if (!wipCheck.allowed) {
|
|
30
|
+
return { outcome: "wip-limit-reached", details: `Maintenance WIP limit reached (${wipCheck.count}/${config.maintenanceIssuesWipLimit})` };
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
const mission = readOptionalFile(config.paths.mission.path);
|
|
21
34
|
const featuresPath = config.paths.features.path;
|
|
22
35
|
const featureLimit = config.paths.features.limit;
|
|
23
36
|
const features = scanDirectory(featuresPath, ".md", { fileLimit: t.featuresScan || 10 });
|
|
37
|
+
|
|
38
|
+
// Sort features: incomplete (has unchecked items) first, then by name
|
|
39
|
+
features.sort((a, b) => {
|
|
40
|
+
const aIncomplete = /- \[ \]/.test(a.content) ? 0 : 1;
|
|
41
|
+
const bIncomplete = /- \[ \]/.test(b.content) ? 0 : 1;
|
|
42
|
+
return aIncomplete - bIncomplete || a.name.localeCompare(b.name);
|
|
43
|
+
});
|
|
24
44
|
const libraryDocs = scanDirectory(config.paths.library.path, ".md", {
|
|
25
45
|
contentLimit: t.documentSummary || 1000,
|
|
26
46
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// that provides context for feature generation and issue resolution.
|
|
7
7
|
|
|
8
8
|
import * as core from "@actions/core";
|
|
9
|
+
import { existsSync } from "fs";
|
|
9
10
|
import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection } from "../copilot.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -18,6 +19,12 @@ export async function maintainLibrary(context) {
|
|
|
18
19
|
const { config, instructions, writablePaths, model } = context;
|
|
19
20
|
const t = config.tuning || {};
|
|
20
21
|
|
|
22
|
+
// Check mission-complete signal
|
|
23
|
+
if (existsSync("MISSION_COMPLETE.md")) {
|
|
24
|
+
core.info("Mission is complete — skipping library maintenance");
|
|
25
|
+
return { outcome: "nop", details: "Mission already complete (MISSION_COMPLETE.md signal)" };
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
const sources = readOptionalFile(config.paths.librarySources.path);
|
|
22
29
|
if (!sources.trim()) {
|
|
23
30
|
core.info("No sources found. Returning nop.");
|
|
@@ -35,11 +35,16 @@ async function reviewSingleIssue({ octokit, repo, config, targetIssueNumber, ins
|
|
|
35
35
|
contentLimit: t.sourceContent || 5000,
|
|
36
36
|
fileLimit: t.sourceScan || 20,
|
|
37
37
|
recursive: true,
|
|
38
|
+
sortByMtime: true,
|
|
39
|
+
clean: true,
|
|
40
|
+
outline: true,
|
|
38
41
|
});
|
|
39
42
|
const testFiles = scanDirectory(config.paths.tests.path, [".test.js", ".test.ts"], {
|
|
40
|
-
contentLimit: t.sourceContent || 5000,
|
|
43
|
+
contentLimit: t.testContent || t.sourceContent || 5000,
|
|
41
44
|
fileLimit: t.sourceScan || 20,
|
|
42
45
|
recursive: true,
|
|
46
|
+
sortByMtime: true,
|
|
47
|
+
clean: true,
|
|
43
48
|
});
|
|
44
49
|
const docsFiles = scanDirectory(config.paths.documentation?.path || "docs/", [".md"], {
|
|
45
50
|
fileLimit: t.featuresScan || 10,
|