@xn-intenton-z2a/agentic-lib 7.1.6
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/LICENSE +674 -0
- package/README.md +323 -0
- package/bin/agentic-lib.js +765 -0
- package/package.json +102 -0
- package/src/actions/agentic-step/action.yml +58 -0
- package/src/actions/agentic-step/config-loader.js +153 -0
- package/src/actions/agentic-step/copilot.js +170 -0
- package/src/actions/agentic-step/index.js +118 -0
- package/src/actions/agentic-step/logging.js +88 -0
- package/src/actions/agentic-step/package-lock.json +1891 -0
- package/src/actions/agentic-step/package.json +29 -0
- package/src/actions/agentic-step/safety.js +103 -0
- package/src/actions/agentic-step/tasks/discussions.js +141 -0
- package/src/actions/agentic-step/tasks/enhance-issue.js +102 -0
- package/src/actions/agentic-step/tasks/fix-code.js +71 -0
- package/src/actions/agentic-step/tasks/maintain-features.js +79 -0
- package/src/actions/agentic-step/tasks/maintain-library.js +67 -0
- package/src/actions/agentic-step/tasks/resolve-issue.js +98 -0
- package/src/actions/agentic-step/tasks/review-issue.js +121 -0
- package/src/actions/agentic-step/tasks/transform.js +213 -0
- package/src/actions/agentic-step/tools.js +142 -0
- package/src/actions/commit-if-changed/action.yml +39 -0
- package/src/actions/setup-npmrc/action.yml +38 -0
- package/src/agents/agent-apply-fix.md +13 -0
- package/src/agents/agent-discussion-bot.md +35 -0
- package/src/agents/agent-issue-resolution.md +13 -0
- package/src/agents/agent-maintain-features.md +29 -0
- package/src/agents/agent-maintain-library.md +31 -0
- package/src/agents/agent-ready-issue.md +13 -0
- package/src/agents/agent-review-issue.md +2 -0
- package/src/agents/agentic-lib.yml +68 -0
- package/src/scripts/accept-release.sh +29 -0
- package/src/scripts/activate-schedule.sh +41 -0
- package/src/scripts/clean.sh +21 -0
- package/src/scripts/generate-library-index.js +143 -0
- package/src/scripts/initialise.sh +39 -0
- package/src/scripts/md-to-html.js +77 -0
- package/src/scripts/update.sh +19 -0
- package/src/seeds/test.yml +33 -0
- package/src/seeds/zero-MISSION.md +7 -0
- package/src/seeds/zero-README.md +14 -0
- package/src/seeds/zero-agentic-lib.toml +32 -0
- package/src/seeds/zero-main.js +15 -0
- package/src/seeds/zero-main.test.js +11 -0
- package/src/seeds/zero-package.json +26 -0
- package/src/workflows/agent-discussions-bot.yml +78 -0
- package/src/workflows/agent-flow-fix-code.yml +98 -0
- package/src/workflows/agent-flow-maintain.yml +114 -0
- package/src/workflows/agent-flow-review.yml +99 -0
- package/src/workflows/agent-flow-transform.yml +82 -0
- package/src/workflows/agent-supervisor.yml +85 -0
- package/src/workflows/ci-automerge.yml +544 -0
- package/src/workflows/ci-init.yml +63 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/resolve-issue.js — Issue → code → PR
|
|
4
|
+
//
|
|
5
|
+
// Given an issue number, reads the issue, generates code using the Copilot SDK,
|
|
6
|
+
// validates with tests, and creates a PR.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { checkAttemptLimit, checkWipLimit, isIssueResolved } from "../safety.js";
|
|
10
|
+
import { runCopilotTask, readOptionalFile, formatPathsSection } from "../copilot.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a GitHub issue by generating code and creating a PR.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} context - Task context from index.js
|
|
16
|
+
* @returns {Promise<Object>} Result with outcome, prNumber, tokensUsed, model
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveIssue(context) {
|
|
19
|
+
const { octokit, repo, config, issueNumber, instructions, writablePaths, testCommand, model } = context;
|
|
20
|
+
|
|
21
|
+
if (!issueNumber) {
|
|
22
|
+
throw new Error("resolve-issue task requires issue-number input");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Safety: check if issue is already resolved
|
|
26
|
+
if (await isIssueResolved(octokit, repo, issueNumber)) {
|
|
27
|
+
core.info(`Issue #${issueNumber} is already closed. Returning nop.`);
|
|
28
|
+
return { outcome: "nop", details: "Issue already resolved" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Safety: check attempt limits
|
|
32
|
+
const branchPrefix = "agentic-lib-issue-";
|
|
33
|
+
const { allowed, attempts } = await checkAttemptLimit(
|
|
34
|
+
octokit,
|
|
35
|
+
repo,
|
|
36
|
+
issueNumber,
|
|
37
|
+
branchPrefix,
|
|
38
|
+
config.attemptsPerIssue,
|
|
39
|
+
);
|
|
40
|
+
if (!allowed) {
|
|
41
|
+
core.warning(`Issue #${issueNumber} has exceeded attempt limit (${attempts}/${config.attemptsPerIssue})`);
|
|
42
|
+
return { outcome: "attempt-limit-exceeded", details: `${attempts} attempts exhausted` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Safety: check WIP limits
|
|
46
|
+
const wipCheck = await checkWipLimit(octokit, repo, "in-progress", config.featureDevelopmentIssuesWipLimit);
|
|
47
|
+
if (!wipCheck.allowed) {
|
|
48
|
+
core.info(`WIP limit reached (${wipCheck.count}/${config.featureDevelopmentIssuesWipLimit}). Returning nop.`);
|
|
49
|
+
return { outcome: "wip-limit-reached", details: `${wipCheck.count} issues in progress` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch the issue and comments
|
|
53
|
+
const [{ data: issue }, { data: comments }] = await Promise.all([
|
|
54
|
+
octokit.rest.issues.get({ ...repo, issue_number: Number(issueNumber) }),
|
|
55
|
+
octokit.rest.issues.listComments({ ...repo, issue_number: Number(issueNumber), per_page: 10 }),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const contributing = readOptionalFile(config.paths.contributing.path);
|
|
59
|
+
const agentInstructions = instructions || "Resolve the GitHub issue by writing code that satisfies the requirements.";
|
|
60
|
+
const readOnlyPaths = config.readOnlyPaths;
|
|
61
|
+
|
|
62
|
+
const prompt = [
|
|
63
|
+
"## Instructions",
|
|
64
|
+
agentInstructions,
|
|
65
|
+
"",
|
|
66
|
+
"## Issue",
|
|
67
|
+
`#${issueNumber}: ${issue.title}`,
|
|
68
|
+
"",
|
|
69
|
+
issue.body || "(no description)",
|
|
70
|
+
"",
|
|
71
|
+
comments.length > 0 ? "## Issue Comments" : "",
|
|
72
|
+
...comments.map((c) => `**${c.user.login}:** ${c.body}`),
|
|
73
|
+
"",
|
|
74
|
+
formatPathsSection(writablePaths, readOnlyPaths),
|
|
75
|
+
"",
|
|
76
|
+
"## Constraints",
|
|
77
|
+
`- Run \`${testCommand}\` to validate your changes`,
|
|
78
|
+
contributing ? `\n## Contributing Guidelines\n${contributing}` : "",
|
|
79
|
+
].join("\n");
|
|
80
|
+
|
|
81
|
+
const { content: resultContent, tokensUsed } = await runCopilotTask({
|
|
82
|
+
model,
|
|
83
|
+
systemMessage: `You are an autonomous coding agent resolving GitHub issue #${issueNumber}. Write clean, tested code. Only modify files listed under "Writable" paths. Read-only paths are for context only.`,
|
|
84
|
+
prompt,
|
|
85
|
+
writablePaths,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
core.info(`Copilot SDK response received (${tokensUsed} tokens)`);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
outcome: "code-generated",
|
|
92
|
+
prNumber: null,
|
|
93
|
+
tokensUsed,
|
|
94
|
+
model,
|
|
95
|
+
commitUrl: null,
|
|
96
|
+
details: `Generated code for issue #${issueNumber}: ${resultContent.substring(0, 200)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/review-issue.js — Review issues and close resolved ones
|
|
4
|
+
//
|
|
5
|
+
// Checks open issues against the current codebase to determine
|
|
6
|
+
// if they have been resolved, and closes them if so.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { runCopilotTask, scanDirectory } from "../copilot.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Review open issues and close those that have been resolved.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} context - Task context from index.js
|
|
15
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
|
+
*/
|
|
17
|
+
export async function reviewIssue(context) {
|
|
18
|
+
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
19
|
+
|
|
20
|
+
// If no specific issue, review the oldest open automated issue
|
|
21
|
+
let targetIssueNumber = issueNumber;
|
|
22
|
+
if (!targetIssueNumber) {
|
|
23
|
+
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
24
|
+
...repo,
|
|
25
|
+
state: "open",
|
|
26
|
+
labels: "automated",
|
|
27
|
+
per_page: 1,
|
|
28
|
+
sort: "created",
|
|
29
|
+
direction: "asc",
|
|
30
|
+
});
|
|
31
|
+
if (openIssues.length === 0) {
|
|
32
|
+
return { outcome: "nop", details: "No open automated issues to review" };
|
|
33
|
+
}
|
|
34
|
+
targetIssueNumber = openIssues[0].number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
38
|
+
...repo,
|
|
39
|
+
issue_number: Number(targetIssueNumber),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (issue.state === "closed") {
|
|
43
|
+
return { outcome: "nop", details: `Issue #${targetIssueNumber} is already closed` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
47
|
+
contentLimit: 2000,
|
|
48
|
+
recursive: true,
|
|
49
|
+
});
|
|
50
|
+
const testFiles = scanDirectory(config.paths.tests.path, [".test.js", ".test.ts"], {
|
|
51
|
+
contentLimit: 2000,
|
|
52
|
+
recursive: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const agentInstructions = instructions || "Review whether this issue has been resolved by the current codebase.";
|
|
56
|
+
|
|
57
|
+
const prompt = [
|
|
58
|
+
"## Instructions",
|
|
59
|
+
agentInstructions,
|
|
60
|
+
"",
|
|
61
|
+
`## Issue #${targetIssueNumber}: ${issue.title}`,
|
|
62
|
+
issue.body || "(no description)",
|
|
63
|
+
"",
|
|
64
|
+
`## Current Source (${sourceFiles.length} files)`,
|
|
65
|
+
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
66
|
+
"",
|
|
67
|
+
`## Current Tests (${testFiles.length} files)`,
|
|
68
|
+
...testFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
69
|
+
"",
|
|
70
|
+
"## Your Task",
|
|
71
|
+
"Determine if this issue has been resolved by the current code.",
|
|
72
|
+
"Respond with exactly one of:",
|
|
73
|
+
'- "RESOLVED: <reason>" if the issue is satisfied by the current code',
|
|
74
|
+
'- "OPEN: <reason>" if the issue is not yet resolved',
|
|
75
|
+
].join("\n");
|
|
76
|
+
|
|
77
|
+
const { content: verdict, tokensUsed } = await runCopilotTask({
|
|
78
|
+
model,
|
|
79
|
+
systemMessage: "You are a code reviewer determining if GitHub issues have been resolved.",
|
|
80
|
+
prompt,
|
|
81
|
+
writablePaths: [],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (verdict.toUpperCase().startsWith("RESOLVED")) {
|
|
85
|
+
await octokit.rest.issues.createComment({
|
|
86
|
+
...repo,
|
|
87
|
+
issue_number: Number(targetIssueNumber),
|
|
88
|
+
body: [
|
|
89
|
+
"**Automated Review Result:** This issue has been resolved by the current codebase.",
|
|
90
|
+
"",
|
|
91
|
+
`**Task:** review-issue`,
|
|
92
|
+
`**Model:** ${model}`,
|
|
93
|
+
`**Source files reviewed:** ${sourceFiles.length}`,
|
|
94
|
+
`**Test files reviewed:** ${testFiles.length}`,
|
|
95
|
+
"",
|
|
96
|
+
verdict,
|
|
97
|
+
].join("\n"),
|
|
98
|
+
});
|
|
99
|
+
await octokit.rest.issues.update({
|
|
100
|
+
...repo,
|
|
101
|
+
issue_number: Number(targetIssueNumber),
|
|
102
|
+
state: "closed",
|
|
103
|
+
});
|
|
104
|
+
core.info(`Issue #${targetIssueNumber} closed as resolved`);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
outcome: "issue-closed",
|
|
108
|
+
tokensUsed,
|
|
109
|
+
model,
|
|
110
|
+
details: `Closed issue #${targetIssueNumber}: ${verdict.substring(0, 200)}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
core.info(`Issue #${targetIssueNumber} still open: ${verdict.substring(0, 100)}`);
|
|
115
|
+
return {
|
|
116
|
+
outcome: "issue-still-open",
|
|
117
|
+
tokensUsed,
|
|
118
|
+
model,
|
|
119
|
+
details: `Issue #${targetIssueNumber} remains open: ${verdict.substring(0, 200)}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/transform.js — Full mission → features → issues → code pipeline
|
|
4
|
+
//
|
|
5
|
+
// Reads the mission, analyzes the current state, identifies what to build next,
|
|
6
|
+
// and either creates features, issues, or code.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection } from "../copilot.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run the full transformation pipeline from mission to code.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} context - Task context from index.js
|
|
15
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
|
+
*/
|
|
17
|
+
export async function transform(context) {
|
|
18
|
+
const { config, instructions, writablePaths, testCommand, model, octokit, repo } = context;
|
|
19
|
+
|
|
20
|
+
// Read mission (required)
|
|
21
|
+
const mission = readOptionalFile(config.paths.mission.path);
|
|
22
|
+
if (!mission) {
|
|
23
|
+
core.warning(`No mission file found at ${config.paths.mission.path}`);
|
|
24
|
+
return { outcome: "nop", details: "No mission file found" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const features = scanDirectory(config.paths.features.path, ".md");
|
|
28
|
+
const sourceFiles = scanDirectory(config.paths.source.path, [".js", ".ts"], {
|
|
29
|
+
contentLimit: 2000,
|
|
30
|
+
recursive: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { data: openIssues } = await octokit.rest.issues.listForRepo({
|
|
34
|
+
...repo,
|
|
35
|
+
state: "open",
|
|
36
|
+
per_page: 20,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const agentInstructions =
|
|
40
|
+
instructions || "Transform the repository toward its mission by identifying the next best action.";
|
|
41
|
+
const readOnlyPaths = config.readOnlyPaths;
|
|
42
|
+
|
|
43
|
+
// TDD mode: split into test-first + implementation phases
|
|
44
|
+
if (config.tdd === true) {
|
|
45
|
+
return await transformTdd({
|
|
46
|
+
config,
|
|
47
|
+
instructions: agentInstructions,
|
|
48
|
+
writablePaths,
|
|
49
|
+
readOnlyPaths,
|
|
50
|
+
testCommand,
|
|
51
|
+
model,
|
|
52
|
+
mission,
|
|
53
|
+
features,
|
|
54
|
+
sourceFiles,
|
|
55
|
+
openIssues,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const prompt = [
|
|
60
|
+
"## Instructions",
|
|
61
|
+
agentInstructions,
|
|
62
|
+
"",
|
|
63
|
+
"## Mission",
|
|
64
|
+
mission,
|
|
65
|
+
"",
|
|
66
|
+
`## Current Features (${features.length})`,
|
|
67
|
+
...features.map((f) => `### ${f.name}\n${f.content.substring(0, 500)}`),
|
|
68
|
+
"",
|
|
69
|
+
`## Current Source Files (${sourceFiles.length})`,
|
|
70
|
+
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
71
|
+
"",
|
|
72
|
+
`## Open Issues (${openIssues.length})`,
|
|
73
|
+
...openIssues.slice(0, 10).map((i) => `- #${i.number}: ${i.title}`),
|
|
74
|
+
"",
|
|
75
|
+
"## Your Task",
|
|
76
|
+
"Analyze the mission, features, source code, and open issues.",
|
|
77
|
+
"Determine the single most impactful next step to transform this repository.",
|
|
78
|
+
"Then implement that step.",
|
|
79
|
+
"",
|
|
80
|
+
formatPathsSection(writablePaths, readOnlyPaths),
|
|
81
|
+
"",
|
|
82
|
+
"## Constraints",
|
|
83
|
+
`- Run \`${testCommand}\` to validate your changes`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
|
|
86
|
+
core.info(`Transform prompt length: ${prompt.length} chars`);
|
|
87
|
+
|
|
88
|
+
const { content: resultContent, tokensUsed } = await runCopilotTask({
|
|
89
|
+
model,
|
|
90
|
+
systemMessage:
|
|
91
|
+
"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.",
|
|
92
|
+
prompt,
|
|
93
|
+
writablePaths,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
core.info(`Transformation step completed (${tokensUsed} tokens)`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
outcome: "transformed",
|
|
100
|
+
tokensUsed,
|
|
101
|
+
model,
|
|
102
|
+
details: resultContent.substring(0, 500),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* TDD-mode transformation: Phase 1 creates a failing test, Phase 2 writes implementation.
|
|
108
|
+
*/
|
|
109
|
+
async function transformTdd({
|
|
110
|
+
config: _config,
|
|
111
|
+
instructions,
|
|
112
|
+
writablePaths,
|
|
113
|
+
readOnlyPaths,
|
|
114
|
+
testCommand,
|
|
115
|
+
model,
|
|
116
|
+
mission,
|
|
117
|
+
features,
|
|
118
|
+
sourceFiles,
|
|
119
|
+
openIssues,
|
|
120
|
+
}) {
|
|
121
|
+
let totalTokens = 0;
|
|
122
|
+
|
|
123
|
+
// Phase 1: Create a failing test
|
|
124
|
+
core.info("TDD Phase 1: Creating failing test");
|
|
125
|
+
|
|
126
|
+
const testPrompt = [
|
|
127
|
+
"## Instructions",
|
|
128
|
+
instructions,
|
|
129
|
+
"",
|
|
130
|
+
"## Mode: TDD Phase 1 — Write Failing Test",
|
|
131
|
+
"You are in TDD mode. In this phase, you must ONLY write a test.",
|
|
132
|
+
"The test should capture the next feature requirement based on the mission and current state.",
|
|
133
|
+
"The test MUST fail against the current codebase (it tests something not yet implemented).",
|
|
134
|
+
"Do NOT write any implementation code in this phase.",
|
|
135
|
+
"",
|
|
136
|
+
"## Mission",
|
|
137
|
+
mission,
|
|
138
|
+
"",
|
|
139
|
+
`## Current Features (${features.length})`,
|
|
140
|
+
...features.map((f) => `### ${f.name}\n${f.content.substring(0, 500)}`),
|
|
141
|
+
"",
|
|
142
|
+
`## Current Source Files (${sourceFiles.length})`,
|
|
143
|
+
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
144
|
+
"",
|
|
145
|
+
`## Open Issues (${openIssues.length})`,
|
|
146
|
+
...openIssues.slice(0, 10).map((i) => `- #${i.number}: ${i.title}`),
|
|
147
|
+
"",
|
|
148
|
+
formatPathsSection(writablePaths, readOnlyPaths),
|
|
149
|
+
"",
|
|
150
|
+
"## Constraints",
|
|
151
|
+
"- Write ONLY test code in this phase",
|
|
152
|
+
"- The test must fail when run (it tests unimplemented functionality)",
|
|
153
|
+
`- Run \`${testCommand}\` to confirm the test fails`,
|
|
154
|
+
].join("\n");
|
|
155
|
+
|
|
156
|
+
const phase1 = await runCopilotTask({
|
|
157
|
+
model,
|
|
158
|
+
systemMessage:
|
|
159
|
+
"You are a TDD agent. In this phase, write ONLY a failing test that captures the next feature requirement. Do not write implementation code.",
|
|
160
|
+
prompt: testPrompt,
|
|
161
|
+
writablePaths,
|
|
162
|
+
});
|
|
163
|
+
totalTokens += phase1.tokensUsed;
|
|
164
|
+
const testResult = phase1.content;
|
|
165
|
+
|
|
166
|
+
core.info(`TDD Phase 1 completed (${totalTokens} tokens): test created`);
|
|
167
|
+
|
|
168
|
+
// Phase 2: Write implementation to make the test pass
|
|
169
|
+
core.info("TDD Phase 2: Writing implementation");
|
|
170
|
+
|
|
171
|
+
const implPrompt = [
|
|
172
|
+
"## Instructions",
|
|
173
|
+
instructions,
|
|
174
|
+
"",
|
|
175
|
+
"## Mode: TDD Phase 2 — Write Implementation",
|
|
176
|
+
"A failing test has been written in Phase 1. Your job is to write the MINIMUM implementation",
|
|
177
|
+
"code needed to make the test pass. Do not modify the test.",
|
|
178
|
+
"",
|
|
179
|
+
"## What was done in Phase 1",
|
|
180
|
+
testResult.substring(0, 1000),
|
|
181
|
+
"",
|
|
182
|
+
"## Mission",
|
|
183
|
+
mission,
|
|
184
|
+
"",
|
|
185
|
+
`## Current Source Files (${sourceFiles.length})`,
|
|
186
|
+
...sourceFiles.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
187
|
+
"",
|
|
188
|
+
formatPathsSection(writablePaths, readOnlyPaths),
|
|
189
|
+
"",
|
|
190
|
+
"## Constraints",
|
|
191
|
+
"- Write implementation code to make the failing test pass",
|
|
192
|
+
"- Do NOT modify the test file created in Phase 1",
|
|
193
|
+
`- Run \`${testCommand}\` to confirm all tests pass`,
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
196
|
+
const phase2 = await runCopilotTask({
|
|
197
|
+
model,
|
|
198
|
+
systemMessage:
|
|
199
|
+
"You are a TDD agent. A failing test was written in Phase 1. Write the minimum implementation to make it pass. Do not modify the test.",
|
|
200
|
+
prompt: implPrompt,
|
|
201
|
+
writablePaths,
|
|
202
|
+
});
|
|
203
|
+
totalTokens += phase2.tokensUsed;
|
|
204
|
+
|
|
205
|
+
core.info(`TDD Phase 2 completed (total ${totalTokens} tokens)`);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
outcome: "transformed-tdd",
|
|
209
|
+
tokensUsed: totalTokens,
|
|
210
|
+
model,
|
|
211
|
+
details: `TDD transformation: Phase 1 (failing test) + Phase 2 (implementation). ${testResult.substring(0, 200)}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tools.js — Shared tool definitions for agentic-step task handlers
|
|
4
|
+
//
|
|
5
|
+
// Defines file I/O and shell tools using the Copilot SDK's defineTool().
|
|
6
|
+
// The agent has no built-in filesystem access — these tools give it the
|
|
7
|
+
// ability to read, write, list files, and run commands.
|
|
8
|
+
|
|
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
|
+
import * as core from "@actions/core";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the standard set of agent tools.
|
|
18
|
+
*
|
|
19
|
+
* @param {string[]} writablePaths - Paths the agent is allowed to write to
|
|
20
|
+
* @returns {import('@github/copilot-sdk').Tool[]} Array of tools for createSession()
|
|
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) && !isPathWritable(path, 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
|
+
});
|
|
140
|
+
|
|
141
|
+
return [readFile, writeFile, listFiles, runCommand];
|
|
142
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
# .github/agentic-lib/actions/commit-if-changed/action.yml
|
|
4
|
+
#
|
|
5
|
+
# Composite action: Commit and push if there are staged changes.
|
|
6
|
+
# Replaces the 8-line block repeated across 6 agent workflows.
|
|
7
|
+
|
|
8
|
+
name: "Commit if changed"
|
|
9
|
+
description: "Stage all changes, commit if any exist, and push"
|
|
10
|
+
|
|
11
|
+
inputs:
|
|
12
|
+
commit-message:
|
|
13
|
+
description: "Commit message"
|
|
14
|
+
required: true
|
|
15
|
+
push-ref:
|
|
16
|
+
description: "Branch ref to push to (default: current ref_name)"
|
|
17
|
+
required: false
|
|
18
|
+
default: ""
|
|
19
|
+
|
|
20
|
+
runs:
|
|
21
|
+
using: "composite"
|
|
22
|
+
steps:
|
|
23
|
+
- name: Commit and push if changed
|
|
24
|
+
shell: bash
|
|
25
|
+
run: |
|
|
26
|
+
git config --local user.email 'action@github.com'
|
|
27
|
+
git config --local user.name 'GitHub Actions[bot]'
|
|
28
|
+
git add -A
|
|
29
|
+
if git diff --cached --quiet; then
|
|
30
|
+
echo "No changes to commit"
|
|
31
|
+
else
|
|
32
|
+
git commit -m "${{ inputs.commit-message }}"
|
|
33
|
+
REF="${{ inputs.push-ref }}"
|
|
34
|
+
if [ -n "$REF" ]; then
|
|
35
|
+
git push origin "$REF"
|
|
36
|
+
else
|
|
37
|
+
git push
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
# .github/agentic-lib/actions/setup-npmrc/action.yml
|
|
4
|
+
#
|
|
5
|
+
# Composite action: Set up .npmrc for GitHub Packages authentication.
|
|
6
|
+
# Replaces the 7-line block repeated across 11+ workflows.
|
|
7
|
+
|
|
8
|
+
name: "Setup .npmrc"
|
|
9
|
+
description: "Configure .npmrc for GitHub Packages authentication"
|
|
10
|
+
|
|
11
|
+
inputs:
|
|
12
|
+
npm-auth-organisation:
|
|
13
|
+
description: "npm scope (e.g. @xn-intenton-z2a). If empty, skip setup."
|
|
14
|
+
required: false
|
|
15
|
+
default: ""
|
|
16
|
+
token:
|
|
17
|
+
description: "GitHub token for npm authentication"
|
|
18
|
+
required: true
|
|
19
|
+
target:
|
|
20
|
+
description: 'Where to write .npmrc: "home" (~/.npmrc) or "project" (.npmrc)'
|
|
21
|
+
required: false
|
|
22
|
+
default: "project"
|
|
23
|
+
|
|
24
|
+
runs:
|
|
25
|
+
using: "composite"
|
|
26
|
+
steps:
|
|
27
|
+
- name: Set up .npmrc
|
|
28
|
+
if: ${{ inputs.npm-auth-organisation != '' }}
|
|
29
|
+
shell: bash
|
|
30
|
+
run: |
|
|
31
|
+
if [ "${{ inputs.target }}" = "home" ]; then
|
|
32
|
+
NPMRC="$HOME/.npmrc"
|
|
33
|
+
else
|
|
34
|
+
NPMRC=".npmrc"
|
|
35
|
+
fi
|
|
36
|
+
echo "${{ inputs.npm-auth-organisation }}:registry=https://npm.pkg.github.com" >> "$NPMRC"
|
|
37
|
+
echo "//npm.pkg.github.com/:_authToken=${{ inputs.token }}" >> "$NPMRC"
|
|
38
|
+
echo "always-auth=true" >> "$NPMRC"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are providing the entire new content of source files, test files, documentation files,
|
|
2
|
+
and other necessary files with all necessary changes applied to resolve a possible build or test
|
|
3
|
+
problem. Focus on high-impact, functional fixes that address core issues rather than superficial
|
|
4
|
+
changes or excessive debugging or validation. If the problem is in an area of the code with little
|
|
5
|
+
value you may re-implement it or remove it.
|
|
6
|
+
|
|
7
|
+
Apply the contributing guidelines to your response, and when suggesting enhancements, consider the tone
|
|
8
|
+
and direction of the contributing guidelines. Prioritize changes that deliver substantial user value
|
|
9
|
+
and maintain the integrity of the codebase's primary purpose.
|
|
10
|
+
|
|
11
|
+
You may complete the implementation of a feature and/or bring the code output in line with the README
|
|
12
|
+
or other documentation. Do as much as you can all at once so that the build runs (even with nothing
|
|
13
|
+
to build) and the tests pass and the main at least doesn't output an error.
|