@xn-intenton-z2a/agentic-lib 7.2.20 → 7.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/agentic-lib-bot.yml +6 -0
- package/.github/workflows/agentic-lib-init.yml +8 -1
- package/.github/workflows/agentic-lib-schedule.yml +7 -1
- package/.github/workflows/agentic-lib-test.yml +12 -0
- package/.github/workflows/agentic-lib-update.yml +7 -1
- package/.github/workflows/agentic-lib-workflow.yml +32 -6
- package/bin/agentic-lib.js +54 -50
- package/package.json +1 -1
- package/src/actions/agentic-step/config-loader.js +4 -3
- package/src/actions/agentic-step/copilot.js +45 -527
- package/src/actions/agentic-step/tools.js +19 -134
- package/src/copilot/sdk.js +5 -3
- 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/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;
|
|
@@ -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
|
-
}
|