@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,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xn-intenton-z2a/agentic-step",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GitHub Action wrapping the Copilot SDK for autonomous code evolution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "vitest --run",
|
|
9
|
+
"build": "echo 'No build step required'"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@actions/core": "^3.0.0",
|
|
13
|
+
"@actions/github": "^9.0.0",
|
|
14
|
+
"@github/copilot-sdk": "0.1.29",
|
|
15
|
+
"js-yaml": "^4.1.0",
|
|
16
|
+
"smol-toml": "^1.6.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"vitest": "^4.0.18"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24.0.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "GPL-3.0",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/xn-intenton-z2a/agentic-lib"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// safety.js — WIP limits, attempt tracking, and path enforcement
|
|
4
|
+
//
|
|
5
|
+
// Provides safety checks that prevent the agentic system from:
|
|
6
|
+
// - Exceeding WIP (work-in-progress) limits on issues
|
|
7
|
+
// - Retrying failed branches/issues beyond configured limits
|
|
8
|
+
// - Writing to paths outside the allowed set
|
|
9
|
+
|
|
10
|
+
import { logSafetyCheck } from "./logging.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if the number of open issues with a given label is below the WIP limit.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('@actions/github').GitHub} octokit - GitHub API client
|
|
16
|
+
* @param {Object} repo - { owner, repo } identifying the repository
|
|
17
|
+
* @param {string} label - Issue label to count (e.g. 'feature', 'maintenance')
|
|
18
|
+
* @param {number} limit - Maximum allowed open issues
|
|
19
|
+
* @returns {Promise<{allowed: boolean, count: number}>}
|
|
20
|
+
*/
|
|
21
|
+
export async function checkWipLimit(octokit, repo, label, limit) {
|
|
22
|
+
const { data: issues } = await octokit.rest.issues.listForRepo({
|
|
23
|
+
...repo,
|
|
24
|
+
state: "open",
|
|
25
|
+
labels: label,
|
|
26
|
+
per_page: limit + 1,
|
|
27
|
+
});
|
|
28
|
+
const count = issues.length;
|
|
29
|
+
const allowed = count < limit;
|
|
30
|
+
logSafetyCheck("wip-limit", allowed, { label, count, limit });
|
|
31
|
+
return { allowed, count };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Count how many branches exist for a given issue (by naming convention).
|
|
36
|
+
*
|
|
37
|
+
* @param {import('@actions/github').GitHub} octokit - GitHub API client
|
|
38
|
+
* @param {Object} repo - { owner, repo } identifying the repository
|
|
39
|
+
* @param {string|number} issueNumber - The issue number
|
|
40
|
+
* @param {string} branchPrefix - Branch naming prefix (e.g. 'agentic-lib-issue-')
|
|
41
|
+
* @returns {Promise<number>} Number of branches matching the pattern
|
|
42
|
+
*/
|
|
43
|
+
export async function countBranchAttempts(octokit, repo, issueNumber, branchPrefix) {
|
|
44
|
+
const pattern = `${branchPrefix}${issueNumber}`;
|
|
45
|
+
const { data: refs } = await octokit.rest.git.listMatchingRefs({
|
|
46
|
+
...repo,
|
|
47
|
+
ref: `heads/${pattern}`,
|
|
48
|
+
});
|
|
49
|
+
return refs.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if the number of attempts for an issue is below the limit.
|
|
54
|
+
*
|
|
55
|
+
* @param {import('@actions/github').GitHub} octokit - GitHub API client
|
|
56
|
+
* @param {Object} repo - { owner, repo }
|
|
57
|
+
* @param {string|number} issueNumber - The issue number
|
|
58
|
+
* @param {string} branchPrefix - Branch naming prefix
|
|
59
|
+
* @param {number} maxAttempts - Maximum allowed attempts
|
|
60
|
+
* @returns {Promise<{allowed: boolean, attempts: number}>}
|
|
61
|
+
*/
|
|
62
|
+
export async function checkAttemptLimit(octokit, repo, issueNumber, branchPrefix, maxAttempts) {
|
|
63
|
+
const attempts = await countBranchAttempts(octokit, repo, issueNumber, branchPrefix);
|
|
64
|
+
const allowed = attempts < maxAttempts;
|
|
65
|
+
logSafetyCheck("attempt-limit", allowed, { issueNumber, attempts, maxAttempts });
|
|
66
|
+
return { allowed, attempts };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate that a target path is within the allowed writable paths.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} targetPath - The path being written to
|
|
73
|
+
* @param {string[]} writablePaths - Allowed writable paths
|
|
74
|
+
* @returns {boolean} True if the path is allowed
|
|
75
|
+
*/
|
|
76
|
+
export function isPathWritable(targetPath, writablePaths) {
|
|
77
|
+
const writable = writablePaths.some((allowed) => {
|
|
78
|
+
if (targetPath === allowed) return true;
|
|
79
|
+
if (allowed.endsWith("/") && targetPath.startsWith(allowed)) return true;
|
|
80
|
+
if (targetPath.startsWith(allowed + "/")) return true;
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
83
|
+
logSafetyCheck("path-writable", writable, { targetPath });
|
|
84
|
+
return writable;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if an issue is already resolved (closed).
|
|
89
|
+
*
|
|
90
|
+
* @param {import('@actions/github').GitHub} octokit - GitHub API client
|
|
91
|
+
* @param {Object} repo - { owner, repo }
|
|
92
|
+
* @param {string|number} issueNumber - The issue number
|
|
93
|
+
* @returns {Promise<boolean>} True if the issue is closed
|
|
94
|
+
*/
|
|
95
|
+
export async function isIssueResolved(octokit, repo, issueNumber) {
|
|
96
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
97
|
+
...repo,
|
|
98
|
+
issue_number: Number(issueNumber),
|
|
99
|
+
});
|
|
100
|
+
const resolved = issue.state === "closed";
|
|
101
|
+
logSafetyCheck("issue-resolved", !resolved, { issueNumber, state: issue.state });
|
|
102
|
+
return resolved;
|
|
103
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/discussions.js — GitHub Discussions bot
|
|
4
|
+
//
|
|
5
|
+
// Responds to GitHub Discussions, creates features, seeds repositories,
|
|
6
|
+
// and provides status updates. Uses the Copilot SDK for natural conversation.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { runCopilotTask, readOptionalFile, scanDirectory } from "../copilot.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Respond to a GitHub Discussion using the Copilot SDK.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} context - Task context from index.js
|
|
16
|
+
* @returns {Promise<Object>} Result with outcome, action, tokensUsed, model
|
|
17
|
+
*/
|
|
18
|
+
export async function discussions(context) {
|
|
19
|
+
const { octokit, config, instructions, model, discussionUrl } = context;
|
|
20
|
+
|
|
21
|
+
if (!discussionUrl) {
|
|
22
|
+
throw new Error("discussions task requires discussion-url input");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Parse discussion URL and fetch content via GraphQL
|
|
26
|
+
const urlMatch = discussionUrl.match(/github\.com\/([^/]+)\/([^/]+)\/discussions\/(\d+)/);
|
|
27
|
+
let discussionTitle = "";
|
|
28
|
+
let discussionBody = "";
|
|
29
|
+
let discussionComments = [];
|
|
30
|
+
|
|
31
|
+
if (urlMatch) {
|
|
32
|
+
const [, urlOwner, urlRepo, discussionNumber] = urlMatch;
|
|
33
|
+
try {
|
|
34
|
+
const query = `query {
|
|
35
|
+
repository(owner: "${urlOwner}", name: "${urlRepo}") {
|
|
36
|
+
discussion(number: ${discussionNumber}) {
|
|
37
|
+
title
|
|
38
|
+
body
|
|
39
|
+
comments(last: 10) {
|
|
40
|
+
nodes {
|
|
41
|
+
body
|
|
42
|
+
author { login }
|
|
43
|
+
createdAt
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}`;
|
|
49
|
+
const result = await octokit.graphql(query);
|
|
50
|
+
const discussion = result.repository.discussion;
|
|
51
|
+
discussionTitle = discussion.title || "";
|
|
52
|
+
discussionBody = discussion.body || "";
|
|
53
|
+
discussionComments = discussion.comments.nodes || [];
|
|
54
|
+
core.info(
|
|
55
|
+
`Fetched discussion #${discussionNumber}: "${discussionTitle}" (${discussionComments.length} comments)`,
|
|
56
|
+
);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
core.warning(`Failed to fetch discussion content via GraphQL: ${err.message}. Falling back to URL-only.`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
core.warning(`Could not parse discussion URL: ${discussionUrl}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const mission = readOptionalFile(config.paths.mission.path);
|
|
65
|
+
const contributing = readOptionalFile(config.paths.contributing.path, 1000);
|
|
66
|
+
|
|
67
|
+
const featuresPath = config.paths.features.path;
|
|
68
|
+
let featureNames = [];
|
|
69
|
+
if (existsSync(featuresPath)) {
|
|
70
|
+
featureNames = scanDirectory(featuresPath, ".md").map((f) => f.name.replace(".md", ""));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const intentionPath = config.intentionBot.intentionFilepath;
|
|
74
|
+
const recentActivity = readOptionalFile(intentionPath).split("\n").slice(-20).join("\n");
|
|
75
|
+
|
|
76
|
+
const agentInstructions = instructions || "Respond to the GitHub Discussion as the repository bot.";
|
|
77
|
+
|
|
78
|
+
const prompt = [
|
|
79
|
+
"## Instructions",
|
|
80
|
+
agentInstructions,
|
|
81
|
+
"",
|
|
82
|
+
"## Discussion",
|
|
83
|
+
`URL: ${discussionUrl}`,
|
|
84
|
+
discussionTitle ? `### ${discussionTitle}` : "",
|
|
85
|
+
discussionBody || "(no body)",
|
|
86
|
+
discussionComments.length > 0 ? `### Comments (${discussionComments.length})` : "",
|
|
87
|
+
...discussionComments.map((c) => `**${c.author?.login || "unknown"}** (${c.createdAt}):\n${c.body}`),
|
|
88
|
+
"",
|
|
89
|
+
"## Context",
|
|
90
|
+
`### Mission\n${mission}`,
|
|
91
|
+
contributing ? `### Contributing\n${contributing}` : "",
|
|
92
|
+
`### Current Features\n${featureNames.join(", ") || "none"}`,
|
|
93
|
+
recentActivity ? `### Recent Activity\n${recentActivity}` : "",
|
|
94
|
+
"",
|
|
95
|
+
"## Available Actions",
|
|
96
|
+
"Respond with one of these action tags in your response:",
|
|
97
|
+
"- `[ACTION:seed-repository]` — Reset the sandbox to initial state",
|
|
98
|
+
"- `[ACTION:create-feature] <name>` — Create a new feature",
|
|
99
|
+
"- `[ACTION:update-feature] <name>` — Update an existing feature",
|
|
100
|
+
"- `[ACTION:delete-feature] <name>` — Delete a feature that is no longer needed",
|
|
101
|
+
"- `[ACTION:create-issue] <title>` — Create a new issue",
|
|
102
|
+
"- `[ACTION:nop]` — No action needed, just respond conversationally",
|
|
103
|
+
"- `[ACTION:mission-complete]` — Declare the current mission complete",
|
|
104
|
+
"- `[ACTION:stop]` — Halt automation",
|
|
105
|
+
"",
|
|
106
|
+
"Include exactly one action tag. The rest of your response is the discussion reply.",
|
|
107
|
+
"",
|
|
108
|
+
"## Mission Protection",
|
|
109
|
+
"If the user requests something that contradicts or would undermine the mission,",
|
|
110
|
+
"you MUST push back. Explain why the request conflicts with the mission and suggest",
|
|
111
|
+
"an alternative that aligns with it. Use `[ACTION:nop]` in this case.",
|
|
112
|
+
"The mission is the non-negotiable foundation of this repository.",
|
|
113
|
+
].join("\n");
|
|
114
|
+
|
|
115
|
+
const { content, tokensUsed } = await runCopilotTask({
|
|
116
|
+
model,
|
|
117
|
+
systemMessage:
|
|
118
|
+
"You are a repository bot that responds to GitHub Discussions. You are self-aware — you refer to yourself as the repository. Be helpful, adaptive, and proactive about suggesting features. You can update and delete features proactively when they are outdated or completed. You MUST protect the mission: if a user requests something that contradicts the mission, push back politely and suggest an aligned alternative.",
|
|
119
|
+
prompt,
|
|
120
|
+
writablePaths: [],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Parse action from response
|
|
124
|
+
const actionMatch = content.match(/\[ACTION:(\S+?)\](.+)?/);
|
|
125
|
+
const action = actionMatch ? actionMatch[1] : "nop";
|
|
126
|
+
const actionArg = actionMatch && actionMatch[2] ? actionMatch[2].trim() : "";
|
|
127
|
+
const replyBody = content.replace(/\[ACTION:\S+?\].+/, "").trim();
|
|
128
|
+
|
|
129
|
+
core.info(`Discussion bot action: ${action}, arg: ${actionArg}`);
|
|
130
|
+
|
|
131
|
+
const argSuffix = actionArg ? ` (${actionArg})` : "";
|
|
132
|
+
return {
|
|
133
|
+
outcome: `discussion-${action}`,
|
|
134
|
+
tokensUsed,
|
|
135
|
+
model,
|
|
136
|
+
details: `Action: ${action}${argSuffix}\nReply: ${replyBody.substring(0, 200)}`,
|
|
137
|
+
action,
|
|
138
|
+
actionArg,
|
|
139
|
+
replyBody,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/enhance-issue.js — Add testable acceptance criteria to issues
|
|
4
|
+
//
|
|
5
|
+
// Takes an issue and enhances it with clear, testable acceptance criteria
|
|
6
|
+
// so that the resolve-issue task can implement it effectively.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { isIssueResolved } from "../safety.js";
|
|
10
|
+
import { runCopilotTask, readOptionalFile, scanDirectory } from "../copilot.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enhance a GitHub issue with testable acceptance criteria.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} context - Task context from index.js
|
|
16
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
17
|
+
*/
|
|
18
|
+
export async function enhanceIssue(context) {
|
|
19
|
+
const { octokit, repo, config, issueNumber, instructions, model } = context;
|
|
20
|
+
|
|
21
|
+
if (!issueNumber) {
|
|
22
|
+
throw new Error("enhance-issue task requires issue-number input");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (await isIssueResolved(octokit, repo, issueNumber)) {
|
|
26
|
+
return { outcome: "nop", details: "Issue already resolved" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
30
|
+
...repo,
|
|
31
|
+
issue_number: Number(issueNumber),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (issue.labels.some((l) => l.name === "ready")) {
|
|
35
|
+
return { outcome: "nop", details: "Issue already has ready label" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const contributing = readOptionalFile(config.paths.contributing.path);
|
|
39
|
+
const features = scanDirectory(config.paths.features.path, ".md", { contentLimit: 500 });
|
|
40
|
+
|
|
41
|
+
const agentInstructions = instructions || "Enhance this issue with clear, testable acceptance criteria.";
|
|
42
|
+
|
|
43
|
+
const prompt = [
|
|
44
|
+
"## Instructions",
|
|
45
|
+
agentInstructions,
|
|
46
|
+
"",
|
|
47
|
+
`## Issue #${issueNumber}: ${issue.title}`,
|
|
48
|
+
issue.body || "(no description)",
|
|
49
|
+
"",
|
|
50
|
+
contributing ? `## Contributing Guidelines\n${contributing.substring(0, 1000)}` : "",
|
|
51
|
+
features.length > 0 ? `## Related Features\n${features.map((f) => f.content).join("\n---\n")}` : "",
|
|
52
|
+
"",
|
|
53
|
+
"## Your Task",
|
|
54
|
+
"Write an enhanced version of this issue body that includes:",
|
|
55
|
+
"1. Clear problem statement or feature description",
|
|
56
|
+
"2. Testable acceptance criteria (Given/When/Then or checkbox format)",
|
|
57
|
+
"3. Implementation hints if applicable",
|
|
58
|
+
"",
|
|
59
|
+
"Output ONLY the new issue body text, no markdown code fences.",
|
|
60
|
+
].join("\n");
|
|
61
|
+
|
|
62
|
+
const { content: enhancedBody, tokensUsed } = await runCopilotTask({
|
|
63
|
+
model,
|
|
64
|
+
systemMessage: "You are a requirements analyst. Enhance GitHub issues with clear, testable acceptance criteria.",
|
|
65
|
+
prompt,
|
|
66
|
+
writablePaths: [],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (enhancedBody.trim()) {
|
|
70
|
+
await octokit.rest.issues.update({
|
|
71
|
+
...repo,
|
|
72
|
+
issue_number: Number(issueNumber),
|
|
73
|
+
body: enhancedBody.trim(),
|
|
74
|
+
});
|
|
75
|
+
await octokit.rest.issues.addLabels({
|
|
76
|
+
...repo,
|
|
77
|
+
issue_number: Number(issueNumber),
|
|
78
|
+
labels: ["ready"],
|
|
79
|
+
});
|
|
80
|
+
await octokit.rest.issues.createComment({
|
|
81
|
+
...repo,
|
|
82
|
+
issue_number: Number(issueNumber),
|
|
83
|
+
body: [
|
|
84
|
+
"**Automated Enhancement:** This issue has been enhanced with testable acceptance criteria.",
|
|
85
|
+
"",
|
|
86
|
+
`**Task:** enhance-issue`,
|
|
87
|
+
`**Model:** ${model}`,
|
|
88
|
+
`**Features referenced:** ${features.length}`,
|
|
89
|
+
"",
|
|
90
|
+
"The issue body has been updated. The `ready` label has been added to indicate it is ready for implementation.",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
});
|
|
93
|
+
core.info(`Issue #${issueNumber} enhanced and labeled 'ready'`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
outcome: "issue-enhanced",
|
|
98
|
+
tokensUsed,
|
|
99
|
+
model,
|
|
100
|
+
details: `Enhanced issue #${issueNumber} with acceptance criteria`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/fix-code.js — Fix failing tests on a PR
|
|
4
|
+
//
|
|
5
|
+
// Given a PR number with failing tests, analyzes the test output,
|
|
6
|
+
// generates fixes using the Copilot SDK, and pushes a commit.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { runCopilotTask, formatPathsSection } from "../copilot.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fix failing code on a pull request.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} context - Task context from index.js
|
|
15
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
|
+
*/
|
|
17
|
+
export async function fixCode(context) {
|
|
18
|
+
const { octokit, repo, config, prNumber, instructions, writablePaths, testCommand, model } = context;
|
|
19
|
+
|
|
20
|
+
if (!prNumber) {
|
|
21
|
+
throw new Error("fix-code task requires pr-number input");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fetch the PR and check runs
|
|
25
|
+
const { data: pr } = await octokit.rest.pulls.get({ ...repo, pull_number: Number(prNumber) });
|
|
26
|
+
const { data: checkRuns } = await octokit.rest.checks.listForRef({ ...repo, ref: pr.head.sha, per_page: 10 });
|
|
27
|
+
|
|
28
|
+
const failedChecks = checkRuns.check_runs.filter((cr) => cr.conclusion === "failure");
|
|
29
|
+
if (failedChecks.length === 0) {
|
|
30
|
+
core.info(`PR #${prNumber} has no failing checks. Returning nop.`);
|
|
31
|
+
return { outcome: "nop", details: "No failing checks found" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const failureDetails = failedChecks.map((cr) => `**${cr.name}:** ${cr.output?.summary || "Failed"}`).join("\n");
|
|
35
|
+
const agentInstructions = instructions || "Fix the failing tests by modifying the source code.";
|
|
36
|
+
const readOnlyPaths = config.readOnlyPaths;
|
|
37
|
+
|
|
38
|
+
const prompt = [
|
|
39
|
+
"## Instructions",
|
|
40
|
+
agentInstructions,
|
|
41
|
+
"",
|
|
42
|
+
`## Pull Request #${prNumber}: ${pr.title}`,
|
|
43
|
+
"",
|
|
44
|
+
pr.body || "(no description)",
|
|
45
|
+
"",
|
|
46
|
+
"## Failing Checks",
|
|
47
|
+
failureDetails,
|
|
48
|
+
"",
|
|
49
|
+
formatPathsSection(writablePaths, readOnlyPaths),
|
|
50
|
+
"",
|
|
51
|
+
"## Constraints",
|
|
52
|
+
`- Run \`${testCommand}\` to validate your fixes`,
|
|
53
|
+
"- Make minimal changes to fix the failing tests",
|
|
54
|
+
].join("\n");
|
|
55
|
+
|
|
56
|
+
const { tokensUsed } = await runCopilotTask({
|
|
57
|
+
model,
|
|
58
|
+
systemMessage: `You are an autonomous coding agent fixing failing tests on PR #${prNumber}. Make minimal, targeted changes to fix the test failures.`,
|
|
59
|
+
prompt,
|
|
60
|
+
writablePaths,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
core.info(`Copilot SDK fix response received (${tokensUsed} tokens)`);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
outcome: "fix-applied",
|
|
67
|
+
tokensUsed,
|
|
68
|
+
model,
|
|
69
|
+
details: `Applied fix for ${failedChecks.length} failing check(s) on PR #${prNumber}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/maintain-features.js — Feature lifecycle management
|
|
4
|
+
//
|
|
5
|
+
// Reviews existing features, creates new ones from mission/library analysis,
|
|
6
|
+
// prunes completed/irrelevant features, and ensures quality.
|
|
7
|
+
|
|
8
|
+
import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection } from "../copilot.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maintain the feature set — create, update, or prune feature files.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} context - Task context from index.js
|
|
14
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
15
|
+
*/
|
|
16
|
+
export async function maintainFeatures(context) {
|
|
17
|
+
const { config, instructions, writablePaths, model, octokit, repo } = context;
|
|
18
|
+
|
|
19
|
+
const mission = readOptionalFile(config.paths.mission.path);
|
|
20
|
+
const featuresPath = config.paths.features.path;
|
|
21
|
+
const featureLimit = config.paths.features.limit;
|
|
22
|
+
const features = scanDirectory(featuresPath, ".md");
|
|
23
|
+
const libraryDocs = scanDirectory(config.paths.library.path, ".md", {
|
|
24
|
+
contentLimit: 1000,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { data: closedIssues } = await octokit.rest.issues.listForRepo({
|
|
28
|
+
...repo,
|
|
29
|
+
state: "closed",
|
|
30
|
+
per_page: 20,
|
|
31
|
+
sort: "updated",
|
|
32
|
+
direction: "desc",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const agentInstructions = instructions || "Maintain the feature set by creating, updating, or pruning features.";
|
|
36
|
+
|
|
37
|
+
const prompt = [
|
|
38
|
+
"## Instructions",
|
|
39
|
+
agentInstructions,
|
|
40
|
+
"",
|
|
41
|
+
"## Mission",
|
|
42
|
+
mission,
|
|
43
|
+
"",
|
|
44
|
+
`## Current Features (${features.length}/${featureLimit} max)`,
|
|
45
|
+
...features.map((f) => `### ${f.name}\n${f.content}`),
|
|
46
|
+
"",
|
|
47
|
+
libraryDocs.length > 0 ? `## Library Documents (${libraryDocs.length})` : "",
|
|
48
|
+
...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
|
|
49
|
+
"",
|
|
50
|
+
`## Recently Closed Issues (${closedIssues.length})`,
|
|
51
|
+
...closedIssues.slice(0, 10).map((i) => `- #${i.number}: ${i.title}`),
|
|
52
|
+
"",
|
|
53
|
+
"## Your Task",
|
|
54
|
+
`1. Review each existing feature — if it is already implemented or irrelevant, delete it.`,
|
|
55
|
+
`2. If there are fewer than ${featureLimit} features, create new features aligned with the mission.`,
|
|
56
|
+
"3. Ensure each feature has clear, testable acceptance criteria.",
|
|
57
|
+
"",
|
|
58
|
+
formatPathsSection(writablePaths, config.readOnlyPaths),
|
|
59
|
+
"",
|
|
60
|
+
"## Constraints",
|
|
61
|
+
`- Maximum ${featureLimit} feature files`,
|
|
62
|
+
"- Feature files must be markdown with a descriptive filename (e.g. HTTP_SERVER.md)",
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
const { tokensUsed } = await runCopilotTask({
|
|
66
|
+
model,
|
|
67
|
+
systemMessage:
|
|
68
|
+
"You are a feature lifecycle manager. Create, update, and prune feature specification files to keep the project focused on its mission.",
|
|
69
|
+
prompt,
|
|
70
|
+
writablePaths,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
outcome: "features-maintained",
|
|
75
|
+
tokensUsed,
|
|
76
|
+
model,
|
|
77
|
+
details: `Maintained features (${features.length} existing, limit ${featureLimit})`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
+
// tasks/maintain-library.js — Library and knowledge management
|
|
4
|
+
//
|
|
5
|
+
// Crawls SOURCES.md, updates library documents, maintains the knowledge base
|
|
6
|
+
// that provides context for feature generation and issue resolution.
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import { runCopilotTask, readOptionalFile, scanDirectory, formatPathsSection } from "../copilot.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maintain the library of knowledge documents from source URLs.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} context - Task context from index.js
|
|
15
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
16
|
+
*/
|
|
17
|
+
export async function maintainLibrary(context) {
|
|
18
|
+
const { config, instructions, writablePaths, model } = context;
|
|
19
|
+
|
|
20
|
+
const sources = readOptionalFile(config.paths.librarySources.path);
|
|
21
|
+
if (!sources.trim()) {
|
|
22
|
+
core.info("No sources found. Returning nop.");
|
|
23
|
+
return { outcome: "nop", details: "No SOURCES.md or empty" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const libraryPath = config.paths.library.path;
|
|
27
|
+
const libraryLimit = config.paths.library.limit;
|
|
28
|
+
const libraryDocs = scanDirectory(libraryPath, ".md", { contentLimit: 500 });
|
|
29
|
+
|
|
30
|
+
const agentInstructions = instructions || "Maintain the library by updating documents from sources.";
|
|
31
|
+
|
|
32
|
+
const prompt = [
|
|
33
|
+
"## Instructions",
|
|
34
|
+
agentInstructions,
|
|
35
|
+
"",
|
|
36
|
+
"## Sources",
|
|
37
|
+
sources,
|
|
38
|
+
"",
|
|
39
|
+
`## Current Library Documents (${libraryDocs.length}/${libraryLimit} max)`,
|
|
40
|
+
...libraryDocs.map((d) => `### ${d.name}\n${d.content}`),
|
|
41
|
+
"",
|
|
42
|
+
"## Your Task",
|
|
43
|
+
"1. Read each URL in SOURCES.md and extract technical content.",
|
|
44
|
+
"2. Create or update library documents based on the source content.",
|
|
45
|
+
"3. Remove library documents that no longer have corresponding sources.",
|
|
46
|
+
"",
|
|
47
|
+
formatPathsSection(writablePaths, config.readOnlyPaths),
|
|
48
|
+
"",
|
|
49
|
+
"## Constraints",
|
|
50
|
+
`- Maximum ${libraryLimit} library documents`,
|
|
51
|
+
].join("\n");
|
|
52
|
+
|
|
53
|
+
const { tokensUsed } = await runCopilotTask({
|
|
54
|
+
model,
|
|
55
|
+
systemMessage:
|
|
56
|
+
"You are a knowledge librarian. Maintain a library of technical documents extracted from web sources.",
|
|
57
|
+
prompt,
|
|
58
|
+
writablePaths,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
outcome: "library-maintained",
|
|
63
|
+
tokensUsed,
|
|
64
|
+
model,
|
|
65
|
+
details: `Maintained library (${libraryDocs.length} docs, limit ${libraryLimit})`,
|
|
66
|
+
};
|
|
67
|
+
}
|