@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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// telemetry.js — Mission metrics, readiness narrative, and cost tracking
|
|
4
|
+
//
|
|
5
|
+
// Phase 4: Extracted from src/actions/agentic-step/index.js so that both
|
|
6
|
+
// CLI and Actions share the same metric calculation logic.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively count TODO/FIXME comments in source files under a directory.
|
|
13
|
+
* @param {string} dir - Directory to scan
|
|
14
|
+
* @param {string[]} [extensions] - File extensions to include (default: .js, .ts, .mjs)
|
|
15
|
+
* @returns {number} Total count of TODO/FIXME occurrences
|
|
16
|
+
*/
|
|
17
|
+
export function countSourceTodos(dir, extensions = [".js", ".ts", ".mjs"]) {
|
|
18
|
+
let count = 0;
|
|
19
|
+
if (!existsSync(dir)) return 0;
|
|
20
|
+
try {
|
|
21
|
+
const entries = readdirSync(dir);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
24
|
+
const fullPath = join(dir, entry);
|
|
25
|
+
try {
|
|
26
|
+
const stat = statSync(fullPath);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
count += countSourceTodos(fullPath, extensions);
|
|
29
|
+
} else if (extensions.some((ext) => entry.endsWith(ext))) {
|
|
30
|
+
const content = readFileSync(fullPath, "utf8");
|
|
31
|
+
const matches = content.match(/\bTODO\b/gi);
|
|
32
|
+
if (matches) count += matches.length;
|
|
33
|
+
}
|
|
34
|
+
} catch { /* skip unreadable files */ }
|
|
35
|
+
}
|
|
36
|
+
} catch { /* skip unreadable dirs */ }
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build mission-complete metrics array for the intentïon.md dashboard.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} config - Parsed agentic-lib config
|
|
44
|
+
* @param {Object} result - Task result object
|
|
45
|
+
* @param {Array} _limitsStatus - Limits status array (unused but kept for signature compatibility)
|
|
46
|
+
* @param {number} cumulativeCost - Cumulative transformation cost
|
|
47
|
+
* @param {number} featureIssueCount - Number of open feature issues
|
|
48
|
+
* @param {number} maintenanceIssueCount - Number of open maintenance issues
|
|
49
|
+
* @returns {Array} Mission metrics entries
|
|
50
|
+
*/
|
|
51
|
+
export function buildMissionMetrics(config, result, _limitsStatus, cumulativeCost, featureIssueCount, maintenanceIssueCount) {
|
|
52
|
+
const openIssues = featureIssueCount + maintenanceIssueCount;
|
|
53
|
+
const budgetCap = config.transformationBudget || 0;
|
|
54
|
+
const resolvedCount = result.resolvedCount || 0;
|
|
55
|
+
const missionComplete = existsSync("MISSION_COMPLETE.md");
|
|
56
|
+
const missionFailed = existsSync("MISSION_FAILED.md");
|
|
57
|
+
const openPrs = result.openPrCount || 0;
|
|
58
|
+
|
|
59
|
+
const sourcePath = config.paths?.source?.path || "src/lib/";
|
|
60
|
+
const sourceDir = sourcePath.endsWith("/") ? sourcePath.slice(0, -1) : sourcePath;
|
|
61
|
+
const srcRoot = sourceDir.includes("/") ? sourceDir.split("/").slice(0, -1).join("/") || "src" : "src";
|
|
62
|
+
const todoCount = countSourceTodos(srcRoot);
|
|
63
|
+
|
|
64
|
+
const dedicatedTestCount = result.dedicatedTestCount ?? 0;
|
|
65
|
+
|
|
66
|
+
const thresholds = config.missionCompleteThresholds || {};
|
|
67
|
+
const minResolved = thresholds.minResolvedIssues ?? 3;
|
|
68
|
+
const minTests = thresholds.minDedicatedTests ?? 1;
|
|
69
|
+
const maxTodos = thresholds.maxSourceTodos ?? 0;
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
{ metric: "Open issues", value: String(openIssues), target: "0", status: openIssues === 0 ? "MET" : "NOT MET" },
|
|
73
|
+
{ metric: "Open PRs", value: String(openPrs), target: "0", status: openPrs === 0 ? "MET" : "NOT MET" },
|
|
74
|
+
{ metric: "Issues resolved (review or PR merge)", value: String(resolvedCount), target: `>= ${minResolved}`, status: resolvedCount >= minResolved ? "MET" : "NOT MET" },
|
|
75
|
+
{ metric: "Dedicated test files", value: String(dedicatedTestCount), target: `>= ${minTests}`, status: dedicatedTestCount >= minTests ? "MET" : "NOT MET" },
|
|
76
|
+
{ metric: "Source TODO count", value: String(todoCount), target: `<= ${maxTodos}`, status: todoCount <= maxTodos ? "MET" : "NOT MET" },
|
|
77
|
+
{ metric: "Transformation budget used", value: `${cumulativeCost}/${budgetCap}`, target: budgetCap > 0 ? `< ${budgetCap}` : "unlimited", status: budgetCap > 0 && cumulativeCost >= budgetCap ? "EXHAUSTED" : "OK" },
|
|
78
|
+
{ metric: "Cumulative transforms", value: String(cumulativeCost), target: ">= 1", status: cumulativeCost >= 1 ? "MET" : "NOT MET" },
|
|
79
|
+
{ metric: "Mission complete declared", value: missionComplete ? "YES" : "NO", target: "—", status: "—" },
|
|
80
|
+
{ metric: "Mission failed declared", value: missionFailed ? "YES" : "NO", target: "—", status: "—" },
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build mission-complete readiness narrative from metrics.
|
|
86
|
+
*
|
|
87
|
+
* @param {Array} metrics - Mission metrics from buildMissionMetrics()
|
|
88
|
+
* @returns {string} Readiness narrative
|
|
89
|
+
*/
|
|
90
|
+
export function buildMissionReadiness(metrics) {
|
|
91
|
+
const openIssues = parseInt(metrics.find((m) => m.metric === "Open issues")?.value || "0", 10);
|
|
92
|
+
const openPrs = parseInt(metrics.find((m) => m.metric === "Open PRs")?.value || "0", 10);
|
|
93
|
+
const resolved = parseInt(metrics.find((m) => m.metric === "Issues resolved (review or PR merge)")?.value || "0", 10);
|
|
94
|
+
const dedicatedTests = parseInt(metrics.find((m) => m.metric === "Dedicated test files")?.value || "0", 10);
|
|
95
|
+
const todoCount = parseInt(metrics.find((m) => m.metric === "Source TODO count")?.value || "0", 10);
|
|
96
|
+
const missionComplete = metrics.find((m) => m.metric === "Mission complete declared")?.value === "YES";
|
|
97
|
+
const missionFailed = metrics.find((m) => m.metric === "Mission failed declared")?.value === "YES";
|
|
98
|
+
|
|
99
|
+
if (missionComplete) return "Mission has been declared complete.";
|
|
100
|
+
if (missionFailed) return "Mission has been declared failed.";
|
|
101
|
+
|
|
102
|
+
const notMet = metrics.filter((m) => m.status === "NOT MET");
|
|
103
|
+
const allMet = notMet.length === 0;
|
|
104
|
+
const parts = [];
|
|
105
|
+
|
|
106
|
+
if (allMet) {
|
|
107
|
+
parts.push("Mission complete conditions ARE met.");
|
|
108
|
+
parts.push(`0 open issues, 0 open PRs, ${resolved} issue(s) resolved, ${dedicatedTests} dedicated test(s), TODOs: ${todoCount}.`);
|
|
109
|
+
} else {
|
|
110
|
+
parts.push("Mission complete conditions are NOT met.");
|
|
111
|
+
if (openIssues > 0) parts.push(`${openIssues} open issue(s) remain.`);
|
|
112
|
+
if (openPrs > 0) parts.push(`${openPrs} open PR(s) remain.`);
|
|
113
|
+
for (const m of notMet) {
|
|
114
|
+
if (m.metric !== "Open issues" && m.metric !== "Open PRs") {
|
|
115
|
+
parts.push(`${m.metric}: ${m.value} (target: ${m.target}).`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return parts.join(" ");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute transformation cost for a task execution.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} task - Task name
|
|
127
|
+
* @param {string} outcome - Task outcome
|
|
128
|
+
* @param {boolean} isInstabilityTransform - Whether this is an instability transform
|
|
129
|
+
* @returns {number} 0 or 1
|
|
130
|
+
*/
|
|
131
|
+
export function computeTransformationCost(task, outcome, isInstabilityTransform) {
|
|
132
|
+
const COST_TASKS = ["transform", "fix-code", "maintain-features", "maintain-library"];
|
|
133
|
+
const isNop = outcome === "nop" || outcome === "error";
|
|
134
|
+
return COST_TASKS.includes(task) && !isNop && !isInstabilityTransform ? 1 : 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read cumulative transformation cost from the intentïon.md activity log.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} intentionFilepath - Path to the intentïon.md file
|
|
141
|
+
* @returns {number} Cumulative cost
|
|
142
|
+
*/
|
|
143
|
+
export function readCumulativeCost(intentionFilepath) {
|
|
144
|
+
if (!intentionFilepath || !existsSync(intentionFilepath)) return 0;
|
|
145
|
+
const logContent = readFileSync(intentionFilepath, "utf8");
|
|
146
|
+
const costMatches = logContent.matchAll(/\*\*agentic-lib transformation cost:\*\* (\d+)/g);
|
|
147
|
+
return [...costMatches].reduce((sum, m) => sum + parseInt(m[1], 10), 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build limits status array for activity logging.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} params
|
|
154
|
+
* @returns {Array} Limits status entries
|
|
155
|
+
*/
|
|
156
|
+
export function buildLimitsStatus({
|
|
157
|
+
task, cumulativeCost, config, featureIssueCount, maintenanceIssueCount, featuresUsed, libraryUsed,
|
|
158
|
+
}) {
|
|
159
|
+
const budgetCap = config.transformationBudget || 0;
|
|
160
|
+
const featCap = config.paths?.features?.limit || 4;
|
|
161
|
+
const libCap = config.paths?.library?.limit || 32;
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
{ name: "transformation-budget", valueNum: cumulativeCost, capacityNum: budgetCap, value: `${cumulativeCost}/${budgetCap}`, remaining: `${Math.max(0, budgetCap - cumulativeCost)}`, status: cumulativeCost >= budgetCap && budgetCap > 0 ? "EXHAUSTED" : "" },
|
|
165
|
+
{ name: "max-feature-issues", valueNum: featureIssueCount, capacityNum: config.featureDevelopmentIssuesWipLimit, value: `${featureIssueCount}/${config.featureDevelopmentIssuesWipLimit}`, remaining: `${Math.max(0, config.featureDevelopmentIssuesWipLimit - featureIssueCount)}`, status: "" },
|
|
166
|
+
{ name: "max-maintenance-issues", valueNum: maintenanceIssueCount, capacityNum: config.maintenanceIssuesWipLimit, value: `${maintenanceIssueCount}/${config.maintenanceIssuesWipLimit}`, remaining: `${Math.max(0, config.maintenanceIssuesWipLimit - maintenanceIssueCount)}`, status: "" },
|
|
167
|
+
{ name: "max-attempts-per-issue", valueNum: 0, capacityNum: config.attemptsPerIssue, value: `?/${config.attemptsPerIssue}`, remaining: "?", status: task === "resolve-issue" ? "" : "n/a" },
|
|
168
|
+
{ name: "max-attempts-per-branch", valueNum: 0, capacityNum: config.attemptsPerBranch, value: `?/${config.attemptsPerBranch}`, remaining: "?", status: task === "fix-code" ? "" : "n/a" },
|
|
169
|
+
{ name: "features", valueNum: featuresUsed, capacityNum: featCap, value: `${featuresUsed}/${featCap}`, remaining: `${Math.max(0, featCap - featuresUsed)}`, status: ["maintain-features", "transform"].includes(task) ? "" : "n/a" },
|
|
170
|
+
{ name: "library", valueNum: libraryUsed, capacityNum: libCap, value: `${libraryUsed}/${libCap}`, remaining: `${Math.max(0, libCap - libraryUsed)}`, status: task === "maintain-library" ? "" : "n/a" },
|
|
171
|
+
];
|
|
172
|
+
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
-
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// src/copilot/tasks/fix-code.js — Fix failing tests (shared)
|
|
4
|
-
//
|
|
5
|
-
// In CLI/local mode: runs tests, feeds failures to agent.
|
|
6
|
-
// In Actions mode: can also resolve merge conflicts and PR check failures.
|
|
7
|
-
|
|
8
|
-
import { execSync } from "child_process";
|
|
9
|
-
import {
|
|
10
|
-
runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
|
|
11
|
-
extractNarrative, NARRATIVE_INSTRUCTION,
|
|
12
|
-
} from "../session.js";
|
|
13
|
-
import { defaultLogger } from "../logger.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Fix failing code — local mode (no GitHub context needed).
|
|
17
|
-
* Runs tests, feeds failure output to the agent.
|
|
18
|
-
*/
|
|
19
|
-
export async function fixCode(context) {
|
|
20
|
-
const { config, instructions, writablePaths, testCommand, model, logger = defaultLogger } = context;
|
|
21
|
-
const t = config.tuning || {};
|
|
22
|
-
const workDir = context.workingDirectory || process.cwd();
|
|
23
|
-
|
|
24
|
-
// Run tests to get failure output
|
|
25
|
-
let testOutput;
|
|
26
|
-
let testsPassed = false;
|
|
27
|
-
try {
|
|
28
|
-
testOutput = execSync(testCommand, { cwd: workDir, encoding: "utf8", timeout: 120000 });
|
|
29
|
-
testsPassed = true;
|
|
30
|
-
} catch (err) {
|
|
31
|
-
testOutput = `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (testsPassed) {
|
|
35
|
-
logger.info("Tests already pass — nothing to fix");
|
|
36
|
-
return { outcome: "nop", details: "Tests already pass" };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const mission = readOptionalFile(config.paths.mission.path);
|
|
40
|
-
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
41
|
-
fileLimit: t.sourceScan || 10,
|
|
42
|
-
contentLimit: t.sourceContent || 5000,
|
|
43
|
-
recursive: true, sortByMtime: true, clean: true,
|
|
44
|
-
}, logger);
|
|
45
|
-
|
|
46
|
-
const agentInstructions = instructions || "Fix the failing tests by modifying the source code.";
|
|
47
|
-
|
|
48
|
-
const prompt = [
|
|
49
|
-
"## Instructions", agentInstructions, "",
|
|
50
|
-
...(mission ? ["## Mission", mission, ""] : []),
|
|
51
|
-
"## Failing Test Output",
|
|
52
|
-
"```", testOutput.substring(0, 8000), "```", "",
|
|
53
|
-
`## Current Source Files (${sourceFiles.length})`,
|
|
54
|
-
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
|
|
55
|
-
formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
|
|
56
|
-
"## Constraints",
|
|
57
|
-
`- Run \`${testCommand}\` to validate your fixes`,
|
|
58
|
-
"- Make minimal changes to fix the failing tests",
|
|
59
|
-
"- Do not introduce new features — focus on making the build green",
|
|
60
|
-
].join("\n");
|
|
61
|
-
|
|
62
|
-
const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
63
|
-
model,
|
|
64
|
-
systemMessage: "You are an autonomous coding agent fixing failing tests. Analyze the error output and make minimal, targeted changes to fix it." + NARRATIVE_INSTRUCTION,
|
|
65
|
-
prompt, writablePaths, tuning: t, logger, workingDirectory: workDir,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
outcome: "fix-applied", tokensUsed, inputTokens, outputTokens, cost, model,
|
|
70
|
-
details: `Applied fix based on test failures`,
|
|
71
|
-
narrative: extractNarrative(resultContent, "Fixed failing tests."),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
-
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// src/copilot/tasks/maintain-features.js — Feature lifecycle (shared)
|
|
4
|
-
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import {
|
|
7
|
-
runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
|
|
8
|
-
extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION,
|
|
9
|
-
} from "../session.js";
|
|
10
|
-
import { defaultLogger } from "../logger.js";
|
|
11
|
-
|
|
12
|
-
export async function maintainFeatures(context) {
|
|
13
|
-
const { config, instructions, writablePaths, model, logger = defaultLogger } = context;
|
|
14
|
-
const t = config.tuning || {};
|
|
15
|
-
|
|
16
|
-
if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
|
|
17
|
-
return { outcome: "nop", details: "Mission already complete" };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const mission = readOptionalFile(config.paths.mission.path);
|
|
21
|
-
const featuresPath = config.paths.features.path;
|
|
22
|
-
const featureLimit = config.paths.features.limit;
|
|
23
|
-
const features = scanDirectory(featuresPath, ".md", { fileLimit: t.featuresScan || 10 }, logger);
|
|
24
|
-
|
|
25
|
-
features.sort((a, b) => {
|
|
26
|
-
const aInc = /- \[ \]/.test(a.content) ? 0 : 1;
|
|
27
|
-
const bInc = /- \[ \]/.test(b.content) ? 0 : 1;
|
|
28
|
-
return aInc - bInc || a.name.localeCompare(b.name);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const libraryDocs = scanDirectory(config.paths.library.path, ".md", { contentLimit: t.documentSummary || 1000 }, logger);
|
|
32
|
-
|
|
33
|
-
const prompt = [
|
|
34
|
-
"## Instructions", instructions || "Maintain the feature set by creating, updating, or pruning features.", "",
|
|
35
|
-
"## Mission", mission, "",
|
|
36
|
-
`## Current Features (${features.length}/${featureLimit} max)`,
|
|
37
|
-
...features.map((f) => `### ${f.name}\n${f.content}`), "",
|
|
38
|
-
...(libraryDocs.length > 0 ? [
|
|
39
|
-
`## Library Documents (${libraryDocs.length})`,
|
|
40
|
-
...libraryDocs.map((d) => `### ${d.name}\n${d.content}`), "",
|
|
41
|
-
] : []),
|
|
42
|
-
"## Your Task",
|
|
43
|
-
`1. Review each existing feature — delete if implemented or irrelevant.`,
|
|
44
|
-
`2. If fewer than ${featureLimit} features, create new ones aligned with the mission.`,
|
|
45
|
-
"3. Ensure each feature has clear, testable acceptance criteria.", "",
|
|
46
|
-
formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
|
|
47
|
-
"## Constraints", `- Maximum ${featureLimit} feature files`,
|
|
48
|
-
].join("\n");
|
|
49
|
-
|
|
50
|
-
const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
51
|
-
model,
|
|
52
|
-
systemMessage: "You are a feature lifecycle manager. Create, update, and prune feature specification files to keep the project focused on its mission." + NARRATIVE_INSTRUCTION,
|
|
53
|
-
prompt, writablePaths, tuning: t, logger,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
outcome: "features-maintained", tokensUsed, inputTokens, outputTokens, cost, model,
|
|
58
|
-
details: `Maintained features (${features.length} existing, limit ${featureLimit})`,
|
|
59
|
-
narrative: extractNarrative(resultContent, `Maintained ${features.length} features.`),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
-
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// src/copilot/tasks/maintain-library.js — Library management (shared)
|
|
4
|
-
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import {
|
|
7
|
-
runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
|
|
8
|
-
extractNarrative, NARRATIVE_INSTRUCTION,
|
|
9
|
-
} from "../session.js";
|
|
10
|
-
import { defaultLogger } from "../logger.js";
|
|
11
|
-
|
|
12
|
-
export async function maintainLibrary(context) {
|
|
13
|
-
const { config, instructions, writablePaths, model, logger = defaultLogger } = context;
|
|
14
|
-
const t = config.tuning || {};
|
|
15
|
-
|
|
16
|
-
if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
|
|
17
|
-
logger.info("Mission complete — skipping library maintenance");
|
|
18
|
-
return { outcome: "nop", details: "Mission already complete" };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const sourcesPath = config.paths.librarySources.path;
|
|
22
|
-
const sources = readOptionalFile(sourcesPath);
|
|
23
|
-
const mission = readOptionalFile(config.paths.mission.path);
|
|
24
|
-
const hasUrls = /https?:\/\//.test(sources);
|
|
25
|
-
|
|
26
|
-
const libraryPath = config.paths.library.path;
|
|
27
|
-
const libraryLimit = config.paths.library.limit;
|
|
28
|
-
const libraryDocs = scanDirectory(libraryPath, ".md", { contentLimit: t.documentSummary || 500 }, logger);
|
|
29
|
-
|
|
30
|
-
let prompt;
|
|
31
|
-
if (!hasUrls) {
|
|
32
|
-
prompt = [
|
|
33
|
-
"## Instructions", instructions || "Maintain the library by updating documents from sources.", "",
|
|
34
|
-
"## Mission", mission || "(no mission)", "",
|
|
35
|
-
"## Current SOURCES.md", sources || "(empty)", "",
|
|
36
|
-
"## Your Task",
|
|
37
|
-
`Populate ${sourcesPath} with 3-8 relevant reference URLs.`, "",
|
|
38
|
-
formatPathsSection(writablePaths, config.readOnlyPaths, config),
|
|
39
|
-
].join("\n");
|
|
40
|
-
} else {
|
|
41
|
-
prompt = [
|
|
42
|
-
"## Instructions", instructions || "Maintain the library by updating documents from sources.", "",
|
|
43
|
-
"## Sources", sources, "",
|
|
44
|
-
`## Current Library Documents (${libraryDocs.length}/${libraryLimit} max)`,
|
|
45
|
-
...libraryDocs.map((d) => `### ${d.name}\n${d.content}`), "",
|
|
46
|
-
"## Your Task",
|
|
47
|
-
"1. Read each URL in SOURCES.md and extract technical content.",
|
|
48
|
-
"2. Create or update library documents.", "",
|
|
49
|
-
formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
|
|
50
|
-
"## Constraints", `- Maximum ${libraryLimit} library documents`,
|
|
51
|
-
].join("\n");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
55
|
-
model,
|
|
56
|
-
systemMessage: "You are a knowledge librarian. Maintain a library of technical documents extracted from web sources." + NARRATIVE_INSTRUCTION,
|
|
57
|
-
prompt, writablePaths, tuning: t, logger,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
outcome: hasUrls ? "library-maintained" : "sources-discovered",
|
|
62
|
-
tokensUsed, inputTokens, outputTokens, cost, model,
|
|
63
|
-
details: hasUrls ? `Maintained library (${libraryDocs.length} docs)` : "Discovered sources from mission",
|
|
64
|
-
narrative: extractNarrative(resultContent, hasUrls ? "Maintained library." : "Discovered sources."),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
-
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// src/copilot/tasks/transform.js — Transform task (shared)
|
|
4
|
-
//
|
|
5
|
-
// Ported from src/actions/agentic-step/tasks/transform.js.
|
|
6
|
-
// GitHub context (octokit, issues) is optional for local CLI use.
|
|
7
|
-
|
|
8
|
-
import { existsSync } from "fs";
|
|
9
|
-
import {
|
|
10
|
-
runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection,
|
|
11
|
-
filterIssues, summariseIssue, extractFeatureSummary, extractNarrative, NARRATIVE_INSTRUCTION,
|
|
12
|
-
} from "../session.js";
|
|
13
|
-
import { defaultLogger } from "../logger.js";
|
|
14
|
-
|
|
15
|
-
export async function transform(context) {
|
|
16
|
-
const { config, instructions, writablePaths, testCommand, model, logger = defaultLogger } = context;
|
|
17
|
-
// octokit + repo are optional (absent in CLI mode)
|
|
18
|
-
const octokit = context.octokit || null;
|
|
19
|
-
const repo = context.repo || null;
|
|
20
|
-
const issueNumber = context.issueNumber || null;
|
|
21
|
-
const t = config.tuning || {};
|
|
22
|
-
|
|
23
|
-
const mission = readOptionalFile(config.paths.mission.path);
|
|
24
|
-
if (!mission) {
|
|
25
|
-
logger.warning(`No mission file found at ${config.paths.mission.path}`);
|
|
26
|
-
return { outcome: "nop", details: "No mission file found" };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (existsSync("MISSION_COMPLETE.md") && config.supervisor !== "maintenance") {
|
|
30
|
-
logger.info("Mission complete — skipping transformation");
|
|
31
|
-
return { outcome: "nop", details: "Mission already complete" };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const features = scanDirectory(config.paths.features.path, ".md", { fileLimit: t.featuresScan || 10 }, logger);
|
|
35
|
-
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
36
|
-
fileLimit: t.sourceScan || 10,
|
|
37
|
-
contentLimit: t.sourceContent || 5000,
|
|
38
|
-
recursive: true, sortByMtime: true, clean: true, outline: true,
|
|
39
|
-
}, logger);
|
|
40
|
-
const webFiles = scanDirectory(config.paths.web?.path || "src/web/", [".html", ".css", ".js"], {
|
|
41
|
-
fileLimit: t.sourceScan || 10,
|
|
42
|
-
contentLimit: t.sourceContent || 5000,
|
|
43
|
-
recursive: true, sortByMtime: true, clean: true,
|
|
44
|
-
}, logger);
|
|
45
|
-
|
|
46
|
-
// GitHub issues (optional)
|
|
47
|
-
let openIssues = [];
|
|
48
|
-
let rawIssuesCount = 0;
|
|
49
|
-
if (octokit && repo) {
|
|
50
|
-
const { data: rawIssues } = await octokit.rest.issues.listForRepo({ ...repo, state: "open", per_page: t.issuesScan || 20 });
|
|
51
|
-
rawIssuesCount = rawIssues.length;
|
|
52
|
-
openIssues = filterIssues(rawIssues, { staleDays: t.staleDays || 30 });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let targetIssue = null;
|
|
56
|
-
if (issueNumber && octokit && repo) {
|
|
57
|
-
try {
|
|
58
|
-
const { data: issue } = await octokit.rest.issues.get({ ...repo, issue_number: Number(issueNumber) });
|
|
59
|
-
targetIssue = issue;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
logger.warning(`Could not fetch target issue #${issueNumber}: ${err.message}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const agentInstructions = instructions || "Transform the repository toward its mission by identifying the next best action.";
|
|
66
|
-
|
|
67
|
-
const prompt = [
|
|
68
|
-
"## Instructions", agentInstructions, "",
|
|
69
|
-
...(targetIssue ? [
|
|
70
|
-
`## Target Issue #${targetIssue.number}: ${targetIssue.title}`,
|
|
71
|
-
targetIssue.body || "(no description)",
|
|
72
|
-
`Labels: ${targetIssue.labels.map((l) => l.name).join(", ") || "none"}`,
|
|
73
|
-
"", "**Focus your transformation on resolving this specific issue.**", "",
|
|
74
|
-
] : []),
|
|
75
|
-
"## Mission", mission, "",
|
|
76
|
-
`## Current Features (${features.length})`,
|
|
77
|
-
...features.map((f) => `### ${f.name}\n${extractFeatureSummary(f.content, f.name)}`), "",
|
|
78
|
-
`## Current Source Files (${sourceFiles.length})`,
|
|
79
|
-
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
|
|
80
|
-
...(webFiles.length > 0 ? [
|
|
81
|
-
`## Website Files (${webFiles.length})`,
|
|
82
|
-
...webFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``), "",
|
|
83
|
-
] : []),
|
|
84
|
-
...(openIssues.length > 0 ? [
|
|
85
|
-
`## Open Issues (${openIssues.length})`,
|
|
86
|
-
...openIssues.slice(0, t.issuesScan || 20).map((i) => summariseIssue(i, t.issueBodyLimit || 500)), "",
|
|
87
|
-
] : []),
|
|
88
|
-
"## Output Artifacts",
|
|
89
|
-
`Save output artifacts to \`${config.paths.examples?.path || "examples/"}\`.`, "",
|
|
90
|
-
"## Your Task",
|
|
91
|
-
"Analyze the mission, features, source code, and open issues.",
|
|
92
|
-
"Determine the single most impactful next step to transform this repository.", "Then implement that step.", "",
|
|
93
|
-
"## When NOT to make changes",
|
|
94
|
-
"If the existing code already satisfies all requirements:", "- Do NOT make cosmetic changes", "- Instead, report that the mission is satisfied", "",
|
|
95
|
-
formatPathsSection(writablePaths, config.readOnlyPaths, config), "",
|
|
96
|
-
"## Constraints", `- Run \`${testCommand}\` to validate your changes`,
|
|
97
|
-
].join("\n");
|
|
98
|
-
|
|
99
|
-
logger.info(`Transform prompt length: ${prompt.length} chars`);
|
|
100
|
-
|
|
101
|
-
const { content: resultContent, tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
102
|
-
model,
|
|
103
|
-
systemMessage: "You are an autonomous code transformation agent. Your goal is to advance the repository toward its mission by making the most impactful change possible in a single step." + NARRATIVE_INSTRUCTION,
|
|
104
|
-
prompt, writablePaths, tuning: t, logger,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const promptBudget = [
|
|
108
|
-
{ section: "mission", size: mission.length, files: "1", notes: "full" },
|
|
109
|
-
{ section: "features", size: features.reduce((s, f) => s + f.content.length, 0), files: `${features.length}`, notes: "" },
|
|
110
|
-
{ section: "source", size: sourceFiles.reduce((s, f) => s + f.content.length, 0), files: `${sourceFiles.length}`, notes: "" },
|
|
111
|
-
{ section: "issues", size: openIssues.length * 80, files: `${openIssues.length}`, notes: `${rawIssuesCount - openIssues.length} filtered` },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
outcome: "transformed", tokensUsed, inputTokens, outputTokens, cost, model,
|
|
116
|
-
details: resultContent.substring(0, 500),
|
|
117
|
-
narrative: extractNarrative(resultContent, "Transformation step completed."),
|
|
118
|
-
promptBudget,
|
|
119
|
-
};
|
|
120
|
-
}
|