@plainconceptsplatform/workflows 0.3.2 → 0.4.1
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/README.md +1 -1
- package/dist/catalog-installation.d.ts +1 -0
- package/dist/catalog-installation.js +108 -38
- package/dist/catalog-installation.test.js +60 -1
- package/dist/catalog-listing.test.js +1 -1
- package/dist/index.test.js +3 -2
- package/dist/repository-inspection.js +27 -5
- package/dist/repository-inspection.test.js +1 -1
- package/dist/route-processing.js +33 -4
- package/dist/route-processing.test.js +7 -9
- package/dist/stack-defaults.js +4 -5
- package/dist/stack-defaults.test.js +7 -7
- package/dist/workflow-catalog.d.ts +1 -1
- package/dist/workflow-catalog.js +2 -0
- package/dist/workflow-catalog.test.js +1 -1
- package/loops/scripts/compile-agent-workflows.mjs +20 -6
- package/loops/templates/opencode/opencode.ci.json +2 -1
- package/loops/templates/release/github-release.yml +30 -0
- package/loops/workflows/agent-apply-review.md +0 -1
- package/loops/workflows/agent-audit.md +0 -1
- package/loops/workflows/agent-direct.md +0 -1
- package/loops/workflows/agent-implement.md +0 -1
- package/loops/workflows/agent-merge-gate.md +0 -1
- package/loops/workflows/agent-propose.md +0 -1
- package/loops/workflows/agent-refine.md +0 -1
- package/loops/workflows/shared/platform-defaults.md +2 -0
- package/loops/workflows/work-router.yml +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Route names: refine, implement, direct, apply-review, merge-gate, audit, propose
|
|
|
48
48
|
|
|
49
49
|
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
50
50
|
|
|
51
|
-
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`,
|
|
51
|
+
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, `app-ci-node-monorepo`, and `github-release`. CI templates are stack-specific copies, not a combined template. `github-release` publishes generated release notes when a `v*` tag is pushed. Edit their top-level `env:` values for repository paths, package names, and commands.
|
|
52
52
|
|
|
53
53
|
## List and search
|
|
54
54
|
|
|
@@ -9,6 +9,7 @@ export interface CatalogInstallOptions {
|
|
|
9
9
|
readonly sourcePath?: string;
|
|
10
10
|
readonly selectedRoutes?: readonly RouteName[];
|
|
11
11
|
readonly inspection?: RepositoryInspection;
|
|
12
|
+
readonly compile?: (repositoryPath: string) => Promise<void>;
|
|
12
13
|
}
|
|
13
14
|
interface CatalogFile {
|
|
14
15
|
readonly source: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { access, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, copyFile, cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -33,13 +33,6 @@ export async function installCatalog(repositoryPath, options = {}) {
|
|
|
33
33
|
const fileName = file.target.split("/").pop() ?? "";
|
|
34
34
|
return !excluded.has(fileName);
|
|
35
35
|
});
|
|
36
|
-
const managedFiles = filtered.filter((file) => file.managed);
|
|
37
|
-
const conflicts = (await Promise.all(managedFiles.map(async (file) => {
|
|
38
|
-
const destination = join(repositoryPath, file.target);
|
|
39
|
-
return await exists(destination) && !(await filesMatch(file.source, destination)) ? file.target : undefined;
|
|
40
|
-
}))).filter((file) => file !== undefined);
|
|
41
|
-
if (conflicts.length > 0 && !options.force)
|
|
42
|
-
return { installed: [], conflicts };
|
|
43
36
|
const fileContents = new Map();
|
|
44
37
|
for (const file of filtered) {
|
|
45
38
|
fileContents.set(file.target, await readFile(file.source, "utf8"));
|
|
@@ -50,21 +43,14 @@ export async function installCatalog(repositoryPath, options = {}) {
|
|
|
50
43
|
processedContents = injectStackIntoWorkers(processedContents, defaults);
|
|
51
44
|
processedContents = transformOpencodeFiles(processedContents, options.inspection);
|
|
52
45
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await ensurePreCommitHook(repositoryPath);
|
|
62
|
-
try {
|
|
63
|
-
await runCompileIfAvailable(repositoryPath);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// compile failure is non-fatal
|
|
67
|
-
}
|
|
46
|
+
const updates = [...processedContents.entries()]
|
|
47
|
+
.filter(([target]) => filtered.find((file) => file.target === target)?.managed ?? true)
|
|
48
|
+
.map(([target, content]) => ({ target, content }));
|
|
49
|
+
const conflicts = await conflictingTargets(repositoryPath, updates);
|
|
50
|
+
if (conflicts.length > 0 && !options.force)
|
|
51
|
+
return { installed: [], conflicts };
|
|
52
|
+
const stagedLocks = await validateStagedCatalog(repositoryPath, updates, options.compile);
|
|
53
|
+
await applyTransaction(repositoryPath, [...updates, ...stagedLocks, await preCommitHookUpdate(repositoryPath)]);
|
|
68
54
|
return { installed: [...processedContents.keys()].sort(), conflicts };
|
|
69
55
|
}
|
|
70
56
|
function injectStackIntoWorkers(files, defaults) {
|
|
@@ -121,28 +107,30 @@ export async function installMandatoryFiles(repositoryPath, options = {}) {
|
|
|
121
107
|
}))).filter((file) => file !== undefined);
|
|
122
108
|
if (conflicts.length > 0 && !options.force)
|
|
123
109
|
return { installed: [], conflicts };
|
|
124
|
-
await Promise.all(files.map(async (file) => {
|
|
125
|
-
|
|
126
|
-
await mkdir(dirname(destination), { recursive: true });
|
|
127
|
-
await copyFile(file.source, destination);
|
|
128
|
-
}));
|
|
110
|
+
const updates = await Promise.all(files.map(async (file) => ({ target: file.target, content: await readFile(file.source, "utf8") })));
|
|
111
|
+
await applyTransaction(repositoryPath, [...updates, await preCommitHookUpdate(repositoryPath)]);
|
|
129
112
|
return { installed: files.map((file) => file.target), conflicts };
|
|
130
113
|
}
|
|
131
114
|
export function isTemplateName(value) {
|
|
132
115
|
return templateNames.includes(value);
|
|
133
116
|
}
|
|
134
117
|
export async function ensurePreCommitHook(repositoryPath) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
const update = await preCommitHookUpdate(repositoryPath);
|
|
119
|
+
await applyTransaction(repositoryPath, [update]);
|
|
120
|
+
}
|
|
121
|
+
async function preCommitHookUpdate(repositoryPath) {
|
|
122
|
+
const target = ".husky/pre-commit";
|
|
123
|
+
const hookPath = join(repositoryPath, target);
|
|
139
124
|
const compileLine = "node scripts/compile-agent-workflows.mjs";
|
|
125
|
+
if (!await exists(hookPath)) {
|
|
126
|
+
return { target, content: `${compileLine}\n` };
|
|
127
|
+
}
|
|
128
|
+
const content = await readFile(hookPath, "utf8");
|
|
140
129
|
if (content.includes("compile-agent-workflows"))
|
|
141
|
-
return;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await writeFile(hookPath, newContent, "utf8");
|
|
130
|
+
return { target, content };
|
|
131
|
+
return { target, content: content.endsWith("\n") || content === ""
|
|
132
|
+
? `${content}${compileLine}\n`
|
|
133
|
+
: `${content}\n${compileLine}\n` };
|
|
146
134
|
}
|
|
147
135
|
export async function runCompileIfAvailable(repositoryPath) {
|
|
148
136
|
const script = join(repositoryPath, "scripts", "compile-agent-workflows.mjs");
|
|
@@ -150,11 +138,93 @@ export async function runCompileIfAvailable(repositoryPath) {
|
|
|
150
138
|
await execFileAsync("node", [script, "--force"], { cwd: repositoryPath });
|
|
151
139
|
}
|
|
152
140
|
}
|
|
141
|
+
async function conflictingTargets(repositoryPath, updates) {
|
|
142
|
+
const conflicts = (await Promise.all(updates.map(async ({ target, content }) => {
|
|
143
|
+
const destination = join(repositoryPath, target);
|
|
144
|
+
if (!await exists(destination))
|
|
145
|
+
return undefined;
|
|
146
|
+
return (await readFile(destination, "utf8")) === content ? undefined : target;
|
|
147
|
+
}))).filter((target) => target !== undefined);
|
|
148
|
+
return [...new Set(conflicts)].sort();
|
|
149
|
+
}
|
|
150
|
+
async function validateStagedCatalog(repositoryPath, updates, compileOverride) {
|
|
151
|
+
const temporaryRoot = join(repositoryPath, ".opencode", ".tmp");
|
|
152
|
+
await mkdir(temporaryRoot, { recursive: true });
|
|
153
|
+
const stagingPath = await mkdtemp(join(temporaryRoot, "workflows-"));
|
|
154
|
+
try {
|
|
155
|
+
await copyCompilationInputs(repositoryPath, stagingPath);
|
|
156
|
+
await writeUpdates(stagingPath, updates);
|
|
157
|
+
const compiler = compileOverride ?? await packageCompiler(stagingPath);
|
|
158
|
+
if (compiler === undefined)
|
|
159
|
+
return [];
|
|
160
|
+
await compiler(stagingPath);
|
|
161
|
+
return await generatedFiles(stagingPath);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await rm(stagingPath, { force: true, recursive: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function copyCompilationInputs(repositoryPath, stagingPath) {
|
|
168
|
+
const githubPath = join(repositoryPath, ".github");
|
|
169
|
+
if (await exists(githubPath))
|
|
170
|
+
await cp(githubPath, join(stagingPath, ".github"), { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
async function packageCompiler(repositoryPath) {
|
|
173
|
+
const scriptPath = join(repositoryPath, "scripts", "compile-agent-workflows.mjs");
|
|
174
|
+
if (!await exists(scriptPath))
|
|
175
|
+
return undefined;
|
|
176
|
+
const content = await readFile(scriptPath, "utf8");
|
|
177
|
+
return content.includes("gh aw compile") ? runCompileIfAvailable : undefined;
|
|
178
|
+
}
|
|
179
|
+
async function generatedFiles(repositoryPath) {
|
|
180
|
+
const workflowPath = join(repositoryPath, ".github", "workflows");
|
|
181
|
+
const updates = [];
|
|
182
|
+
if (await exists(workflowPath)) {
|
|
183
|
+
for (const file of await filesIn(workflowPath)) {
|
|
184
|
+
if (!file.endsWith(".lock.yml"))
|
|
185
|
+
continue;
|
|
186
|
+
updates.push({ target: `.github/workflows/${file.replaceAll("\\", "/")}`, content: await readFile(join(workflowPath, file), "utf8") });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const actionsLock = join(repositoryPath, ".github", "actions", "actions-lock.json");
|
|
190
|
+
if (await exists(actionsLock)) {
|
|
191
|
+
updates.push({ target: ".github/actions/actions-lock.json", content: await readFile(actionsLock, "utf8") });
|
|
192
|
+
}
|
|
193
|
+
return updates.sort((left, right) => left.target.localeCompare(right.target));
|
|
194
|
+
}
|
|
195
|
+
async function applyTransaction(repositoryPath, updates) {
|
|
196
|
+
const uniqueUpdates = [...new Map(updates.map((update) => [update.target, update])).values()];
|
|
197
|
+
const rollback = await Promise.all(uniqueUpdates.map(async ({ target }) => {
|
|
198
|
+
const path = join(repositoryPath, target);
|
|
199
|
+
return { target, existed: await exists(path), content: await exists(path) ? await readFile(path, "utf8") : undefined };
|
|
200
|
+
}));
|
|
201
|
+
try {
|
|
202
|
+
await writeUpdates(repositoryPath, uniqueUpdates);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
await Promise.all(rollback.map(async ({ target, existed, content }) => {
|
|
206
|
+
if (existed && content !== undefined) {
|
|
207
|
+
await writeUpdates(repositoryPath, [{ target, content }]);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await rm(join(repositoryPath, target), { force: true });
|
|
211
|
+
}
|
|
212
|
+
}));
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function writeUpdates(repositoryPath, updates) {
|
|
217
|
+
await Promise.all(updates.map(async ({ target, content }) => {
|
|
218
|
+
const destination = join(repositoryPath, target);
|
|
219
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
220
|
+
await writeFile(destination, content, "utf8");
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
153
223
|
function catalogTemplateMeta(template) {
|
|
154
224
|
const entry = catalogTemplates.find((item) => item.name === template);
|
|
155
225
|
if (entry === undefined)
|
|
156
226
|
throw new Error(`Unknown template: ${template}`);
|
|
157
|
-
const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : "agentics";
|
|
227
|
+
const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : template === "github-release" ? "release" : "agentics";
|
|
158
228
|
const isWorkflow = entry.file.endsWith(".yml");
|
|
159
229
|
const target = template === "app-ci-dotnet-next"
|
|
160
230
|
? ".github/workflows/app-ci.yml"
|
|
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { catalogSourcePath, installCatalog, installMandatoryFiles, installTemplate } from "./catalog-installation.js";
|
|
5
|
+
import { catalogSourcePath, ensurePreCommitHook, installCatalog, installMandatoryFiles, installTemplate } from "./catalog-installation.js";
|
|
6
6
|
const temporaryDirectories = [];
|
|
7
7
|
afterEach(async () => {
|
|
8
8
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
@@ -154,6 +154,7 @@ describe("catalog installation", () => {
|
|
|
154
154
|
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
155
155
|
"templates/agentics/agentics-checks.yml": "name: Agentics checks\n",
|
|
156
156
|
"templates/ci/app-ci-node-monorepo.yml": "name: Node CI\n",
|
|
157
|
+
"templates/release/github-release.yml": "name: Publish GitHub release\n",
|
|
157
158
|
});
|
|
158
159
|
const repositoryPath = await createDirectory({});
|
|
159
160
|
await installCatalog(repositoryPath, { sourcePath });
|
|
@@ -166,6 +167,10 @@ describe("catalog installation", () => {
|
|
|
166
167
|
installed: [".github/workflows/app-ci-node-monorepo.yml"],
|
|
167
168
|
conflicts: [],
|
|
168
169
|
});
|
|
170
|
+
await expect(installTemplate(repositoryPath, "github-release", { sourcePath })).resolves.toEqual({
|
|
171
|
+
installed: [".github/workflows/github-release.yml"],
|
|
172
|
+
conflicts: [],
|
|
173
|
+
});
|
|
169
174
|
});
|
|
170
175
|
it("installCatalog installs mandatory opencode.ci.json and compile script alongside catalog files", async () => {
|
|
171
176
|
const sourcePath = await createDirectory({
|
|
@@ -243,6 +248,60 @@ describe("catalog installation", () => {
|
|
|
243
248
|
const compileEntries = result.installed.filter((path) => path === "scripts/compile-agent-workflows.mjs");
|
|
244
249
|
expect(compileEntries).toHaveLength(1);
|
|
245
250
|
});
|
|
251
|
+
it("creates a Husky pre-commit hook when the consumer has none", async () => {
|
|
252
|
+
const repositoryPath = await createDirectory({});
|
|
253
|
+
await ensurePreCommitHook(repositoryPath);
|
|
254
|
+
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
255
|
+
.resolves.toBe("node scripts/compile-agent-workflows.mjs\n");
|
|
256
|
+
});
|
|
257
|
+
it("keeps existing pre-commit commands and appends the compiler once", async () => {
|
|
258
|
+
const repositoryPath = await createDirectory({ ".husky/pre-commit": "pnpm lint\n" });
|
|
259
|
+
await ensurePreCommitHook(repositoryPath);
|
|
260
|
+
await ensurePreCommitHook(repositoryPath);
|
|
261
|
+
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
262
|
+
.resolves.toBe("pnpm lint\nnode scripts/compile-agent-workflows.mjs\n");
|
|
263
|
+
});
|
|
264
|
+
it("leaves consumer files untouched when staged workflow compilation fails", async () => {
|
|
265
|
+
const sourcePath = await createDirectory({
|
|
266
|
+
"actions/check/action.yml": "package action\n",
|
|
267
|
+
"workflows/agent-check.md": "package workflow\n",
|
|
268
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
269
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"package\" }\n",
|
|
270
|
+
});
|
|
271
|
+
const repositoryPath = await createDirectory({
|
|
272
|
+
".github/actions/check/action.yml": "consumer action\n",
|
|
273
|
+
".github/workflows/agent-check.md": "consumer workflow\n",
|
|
274
|
+
"opencode.ci.json": "{ \"model\": \"consumer\" }\n",
|
|
275
|
+
"scripts/compile-agent-workflows.mjs": "consumer compiler\n",
|
|
276
|
+
});
|
|
277
|
+
await expect(installCatalog(repositoryPath, {
|
|
278
|
+
force: true,
|
|
279
|
+
sourcePath,
|
|
280
|
+
compile: async () => { throw new Error("compile failed"); },
|
|
281
|
+
})).rejects.toThrow("compile failed");
|
|
282
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8"))
|
|
283
|
+
.resolves.toBe("consumer workflow\n");
|
|
284
|
+
await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8"))
|
|
285
|
+
.resolves.toBe("consumer compiler\n");
|
|
286
|
+
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8")).rejects.toThrow();
|
|
287
|
+
});
|
|
288
|
+
it("applies staged generated locks with managed sources", async () => {
|
|
289
|
+
const sourcePath = await createDirectory({
|
|
290
|
+
"actions/check/action.yml": "package action\n",
|
|
291
|
+
"workflows/agent-check.md": "package workflow\n",
|
|
292
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
293
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"package\" }\n",
|
|
294
|
+
});
|
|
295
|
+
const repositoryPath = await createDirectory({});
|
|
296
|
+
await installCatalog(repositoryPath, {
|
|
297
|
+
sourcePath,
|
|
298
|
+
compile: async (stagingPath) => {
|
|
299
|
+
await writeFile(join(stagingPath, ".github", "workflows", "agent-check.lock.yml"), "opencode run --log-level ERROR\n", "utf8");
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
await expect(readFile(join(repositoryPath, ".github", "workflows", "agent-check.lock.yml"), "utf8"))
|
|
303
|
+
.resolves.toBe("opencode run --log-level ERROR\n");
|
|
304
|
+
});
|
|
246
305
|
it("installs the opencode.ci.json template to the repository root", async () => {
|
|
247
306
|
const sourcePath = await createDirectory({
|
|
248
307
|
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
@@ -16,7 +16,7 @@ describe("catalog listing", () => {
|
|
|
16
16
|
const routeNames = entries.filter((entry) => entry.kind === "route").map((entry) => entry.name);
|
|
17
17
|
const templateNames = entries.filter((entry) => entry.kind === "template").map((entry) => entry.name);
|
|
18
18
|
expect(routeNames).toEqual(["refine", "implement", "direct", "apply-review", "merge-gate", "audit", "propose"]);
|
|
19
|
-
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"]);
|
|
19
|
+
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "github-release", "opencode.ci.json"]);
|
|
20
20
|
});
|
|
21
21
|
it("reports all entries as not installed in an empty repository", async () => {
|
|
22
22
|
const repositoryPath = await createRepository({});
|
package/dist/index.test.js
CHANGED
|
@@ -61,12 +61,13 @@ describe("workflows CLI", () => {
|
|
|
61
61
|
expect(output).toContain("implement");
|
|
62
62
|
expect(output).toContain("agentics-checks");
|
|
63
63
|
expect(output).toContain("app-ci-dotnet-next");
|
|
64
|
+
expect(output).toContain("github-release");
|
|
64
65
|
// None installed: all [ ]
|
|
65
66
|
const installedCount = (output.match(/\[x\]/g) ?? []).length;
|
|
66
67
|
expect(installedCount).toBe(0);
|
|
67
|
-
// 7 routes +
|
|
68
|
+
// 7 routes + 6 templates = 13 entries
|
|
68
69
|
const uninstalledCount = (output.match(/\[ \]/g) ?? []).length;
|
|
69
|
-
expect(uninstalledCount).toBe(
|
|
70
|
+
expect(uninstalledCount).toBe(13);
|
|
70
71
|
log.mockRestore();
|
|
71
72
|
});
|
|
72
73
|
it("marks installed workflows with [x]", async () => {
|
|
@@ -46,11 +46,33 @@ export function parseVisibility(value) {
|
|
|
46
46
|
return value === "public" || value === "private" ? value : undefined;
|
|
47
47
|
}
|
|
48
48
|
async function findSolutionFiles(repositoryPath) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const results = [];
|
|
50
|
+
await scanForSlnx(repositoryPath, "", results, 0);
|
|
51
|
+
return results.sort();
|
|
52
|
+
}
|
|
53
|
+
const MAX_DEPTH = 5;
|
|
54
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".next", "dist", "out", "build", ".turbo", ".cache"]);
|
|
55
|
+
async function scanForSlnx(root, relativePath, results, depth) {
|
|
56
|
+
if (depth >= MAX_DEPTH)
|
|
57
|
+
return;
|
|
58
|
+
const currentPath = join(root, relativePath);
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (SKIP_DIRS.has(entry.name))
|
|
69
|
+
continue;
|
|
70
|
+
await scanForSlnx(root, join(relativePath, entry.name), results, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
else if (entry.isFile() && entry.name.endsWith(".slnx")) {
|
|
73
|
+
results.push(relativePath === "" ? entry.name : join(relativePath, entry.name));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
54
76
|
}
|
|
55
77
|
async function pathExists(path) {
|
|
56
78
|
try {
|
package/dist/route-processing.js
CHANGED
|
@@ -72,17 +72,46 @@ export function processRoutes(files, selectedRoutes) {
|
|
|
72
72
|
if (routerKey !== undefined) {
|
|
73
73
|
result.set(routerKey, stripRouteFromRouter(result.get(routerKey), route));
|
|
74
74
|
}
|
|
75
|
-
const classifierKey = findFileKey(result, "classify-route.sh");
|
|
76
|
-
if (classifierKey !== undefined) {
|
|
77
|
-
result.set(classifierKey, stripRouteFromClassifier(result.get(classifierKey), route));
|
|
78
|
-
}
|
|
79
75
|
const matrixKey = findFileKey(result, "verify-route-matrix.sh");
|
|
80
76
|
if (matrixKey !== undefined) {
|
|
81
77
|
result.set(matrixKey, addRouteExclusion(result.get(matrixKey), route));
|
|
82
78
|
}
|
|
83
79
|
}
|
|
80
|
+
const matrixKey = findFileKey(result, "verify-route-matrix.sh");
|
|
81
|
+
if (matrixKey !== undefined) {
|
|
82
|
+
result.set(matrixKey, createRouteMatrix(selectedRoutes));
|
|
83
|
+
}
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
|
+
function createRouteMatrix(selectedRoutes) {
|
|
87
|
+
const selected = selectedRoutes.join(" ");
|
|
88
|
+
const excluded = routeNames.filter((route) => !selectedRoutes.includes(route)).join(" ");
|
|
89
|
+
return `#!/usr/bin/env bash
|
|
90
|
+
set -euo pipefail
|
|
91
|
+
|
|
92
|
+
HERE="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
93
|
+
ROUTER_YML="\${HERE}/../../workflows/work-router.yml"
|
|
94
|
+
CLASSIFIER="\${HERE}/../classify-route/classify-route.sh"
|
|
95
|
+
|
|
96
|
+
bash -n "$CLASSIFIER"
|
|
97
|
+
|
|
98
|
+
for route in ${selected}; do
|
|
99
|
+
grep -q "route == '$route'" "$ROUTER_YML" || {
|
|
100
|
+
echo "FAIL: selected route '$route' has no router job" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
}
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
for route in ${excluded}; do
|
|
106
|
+
if grep -q "route == '$route'" "$ROUTER_YML"; then
|
|
107
|
+
echo "FAIL: excluded route '$route' remains in router" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
echo "Route matrix: selected routes valid"
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
86
115
|
export function excludedWorkerFiles(selectedRoutes) {
|
|
87
116
|
return new Set(workflowRoutes
|
|
88
117
|
.filter((route) => !selectedRoutes.includes(route.name))
|
|
@@ -259,11 +259,10 @@ describe("processRoutes", () => {
|
|
|
259
259
|
expect(router).not.toContain("- propose");
|
|
260
260
|
expect(router).not.toContain("- refine");
|
|
261
261
|
const classifier = result.get(".github/actions/classify-route/classify-route.sh");
|
|
262
|
-
expect(classifier).
|
|
263
|
-
expect(classifier).not.toContain("readonly AUDIT_CRON");
|
|
262
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
264
263
|
const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
|
|
265
|
-
expect(matrix).toContain("
|
|
266
|
-
expect(matrix).toContain("
|
|
264
|
+
expect(matrix).toContain("Route matrix: selected routes valid");
|
|
265
|
+
expect(matrix).toContain("for route in refine implement direct apply-review merge-gate audit propose");
|
|
267
266
|
});
|
|
268
267
|
it("strips propose from all three files when unselected", () => {
|
|
269
268
|
const files = new Map([
|
|
@@ -278,10 +277,10 @@ describe("processRoutes", () => {
|
|
|
278
277
|
expect(router).not.toContain('cron: "29 7 * * *"');
|
|
279
278
|
expect(router).not.toMatch(/^\s+- propose$/m);
|
|
280
279
|
const classifier = result.get(".github/actions/classify-route/classify-route.sh");
|
|
281
|
-
expect(classifier).
|
|
282
|
-
expect(classifier).not.toContain('"$PROPOSE_CRON")');
|
|
280
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
283
281
|
const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
|
|
284
|
-
expect(matrix).toContain("
|
|
282
|
+
expect(matrix).toContain("for route in refine implement direct apply-review merge-gate audit");
|
|
283
|
+
expect(matrix).toContain("for route in propose");
|
|
285
284
|
});
|
|
286
285
|
it("strips audit from all three files when unselected", () => {
|
|
287
286
|
const files = new Map([
|
|
@@ -295,8 +294,7 @@ describe("processRoutes", () => {
|
|
|
295
294
|
expect(router).not.toContain("call-audit");
|
|
296
295
|
expect(router).not.toContain('cron: "17 1 * * 1"');
|
|
297
296
|
const classifier = result.get("classify-route.sh");
|
|
298
|
-
expect(classifier).
|
|
299
|
-
expect(classifier).not.toContain('"$AUDIT_CRON")');
|
|
297
|
+
expect(classifier).toBe(CLASSIFIER_SH);
|
|
300
298
|
});
|
|
301
299
|
it("processes multiple excluded routes at once", () => {
|
|
302
300
|
const files = new Map([
|
package/dist/stack-defaults.js
CHANGED
|
@@ -20,8 +20,6 @@ export function generateStackDefaults(inspection) {
|
|
|
20
20
|
}
|
|
21
21
|
export function injectStackEnv(content, defaults) {
|
|
22
22
|
let result = content;
|
|
23
|
-
if (defaults.verifyCommands === "pnpm verify")
|
|
24
|
-
return result;
|
|
25
23
|
if (result.includes("VERIFY_COMMANDS:")) {
|
|
26
24
|
result = result.replace(/ VERIFY_COMMANDS: ".*"/, ` VERIFY_COMMANDS: "${defaults.verifyCommands}"`);
|
|
27
25
|
}
|
|
@@ -33,6 +31,7 @@ export function injectStackEnv(content, defaults) {
|
|
|
33
31
|
export function generateOpencodeCi(baseContent, inspection) {
|
|
34
32
|
let result = baseContent;
|
|
35
33
|
if (inspection.stackHints.solutionFiles.length > 0) {
|
|
34
|
+
const solutionPath = inspection.stackHints.solutionFiles[0];
|
|
36
35
|
const nugetSteps = ` - name: Cache NuGet packages
|
|
37
36
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
38
37
|
with:
|
|
@@ -40,8 +39,8 @@ export function generateOpencodeCi(baseContent, inspection) {
|
|
|
40
39
|
key: nuget-\${{ runner.os }}-\${{ hashFiles('**/*.slnx', '**/Directory.Packages.props') }}
|
|
41
40
|
restore-keys: nuget-\${{ runner.os }}-
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
run: dotnet restore
|
|
42
|
+
- name: Restore .NET dependencies
|
|
43
|
+
run: dotnet restore ${solutionPath}
|
|
45
44
|
`;
|
|
46
45
|
result = insertBeforeMarker(result, nugetSteps, " - name: Install workspace dependencies");
|
|
47
46
|
}
|
|
@@ -49,7 +48,7 @@ export function generateOpencodeCi(baseContent, inspection) {
|
|
|
49
48
|
const openspecStep = ` - name: Install OpenSpec CLI
|
|
50
49
|
run: |
|
|
51
50
|
set -euo pipefail
|
|
52
|
-
npm install -g @openspec
|
|
51
|
+
npm install -g "@fission-ai/openspec@1.8.0"
|
|
53
52
|
openspec --version
|
|
54
53
|
`;
|
|
55
54
|
result = insertBeforeMarker(result, openspecStep, " - name: Install workspace dependencies");
|
|
@@ -76,7 +76,7 @@ description: test
|
|
|
76
76
|
const result = injectStackEnv(content, defaults);
|
|
77
77
|
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
78
|
});
|
|
79
|
-
it("
|
|
79
|
+
it("injects pnpm verification when no .slnx is present", () => {
|
|
80
80
|
const content = `---
|
|
81
81
|
env:
|
|
82
82
|
REPO_RULES: "some rules"
|
|
@@ -85,7 +85,7 @@ env:
|
|
|
85
85
|
pnpmLockfile: true,
|
|
86
86
|
}));
|
|
87
87
|
const result = injectStackEnv(content, defaults);
|
|
88
|
-
expect(result).
|
|
88
|
+
expect(result).toContain('VERIFY_COMMANDS: "pnpm verify"');
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
const OPENCODE_CI_MD = `---
|
|
@@ -113,20 +113,20 @@ pre-agent-steps:
|
|
|
113
113
|
jq -e . "$FRAGMENT" > /dev/null
|
|
114
114
|
---`;
|
|
115
115
|
describe("generateOpencodeCi", () => {
|
|
116
|
-
it("adds NuGet cache and
|
|
116
|
+
it("adds NuGet cache and restores detected solution when .slnx is found", () => {
|
|
117
117
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
-
solutionFiles: ["
|
|
118
|
+
solutionFiles: ["apps/api/Numa.slnx"],
|
|
119
119
|
}));
|
|
120
120
|
expect(result).toContain("Cache NuGet packages");
|
|
121
121
|
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
-
expect(result).toContain("dotnet restore");
|
|
122
|
+
expect(result).toContain("dotnet restore apps/api/Numa.slnx");
|
|
123
123
|
});
|
|
124
|
-
it("adds OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
124
|
+
it("adds the pinned OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
125
125
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
126
126
|
openSpec: true,
|
|
127
127
|
}));
|
|
128
128
|
expect(result).toContain("Install OpenSpec CLI");
|
|
129
|
-
expect(result).toContain("@openspec
|
|
129
|
+
expect(result).toContain("@fission-ai/openspec@1.8.0");
|
|
130
130
|
});
|
|
131
131
|
it("adds both NuGet and OpenSpec steps when both are detected", () => {
|
|
132
132
|
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
@@ -14,7 +14,7 @@ export interface MandatoryFile {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const mandatoryFiles: readonly MandatoryFile[];
|
|
16
16
|
export declare const generatedConsumerTargets: readonly [".github/workflows/agent-*.lock.yml", ".github/aw/actions-lock.json"];
|
|
17
|
-
export declare const templateNames: readonly ["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"];
|
|
17
|
+
export declare const templateNames: readonly ["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "github-release", "opencode.ci.json"];
|
|
18
18
|
export type TemplateName = (typeof templateNames)[number];
|
|
19
19
|
export interface CatalogTemplate {
|
|
20
20
|
readonly name: TemplateName;
|
package/dist/workflow-catalog.js
CHANGED
|
@@ -38,6 +38,7 @@ export const templateNames = [
|
|
|
38
38
|
"agentics-maintenance",
|
|
39
39
|
"app-ci-dotnet-next",
|
|
40
40
|
"app-ci-node-monorepo",
|
|
41
|
+
"github-release",
|
|
41
42
|
"opencode.ci.json",
|
|
42
43
|
];
|
|
43
44
|
export const catalogTemplates = [
|
|
@@ -45,5 +46,6 @@ export const catalogTemplates = [
|
|
|
45
46
|
{ name: "agentics-maintenance", file: "agentics-maintenance.yml", description: "Agentic maintenance: scheduled daily maintenance workflow for keeping workflows and actions up to date." },
|
|
46
47
|
{ name: "app-ci-dotnet-next", file: "app-ci-dotnet-next.yml", description: "App CI pipeline for a .NET + Next.js monorepo: build, test, and lint on PRs and schedule." },
|
|
47
48
|
{ name: "app-ci-node-monorepo", file: "app-ci-node-monorepo.yml", description: "App CI pipeline for a Node monorepo: build, test, and lint on PRs and schedule." },
|
|
49
|
+
{ name: "github-release", file: "github-release.yml", description: "Publishes or updates a GitHub Release with generated notes whenever a v* tag is pushed." },
|
|
48
50
|
{ name: "opencode.ci.json", file: "opencode.ci.json", description: "Standalone OpenCode CI config: plainconcepts provider, GLM model registration, ci-workflow-agent, and LSP defaults for consumer repositories." },
|
|
49
51
|
];
|
|
@@ -17,7 +17,7 @@ describe("workflow catalog", () => {
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
it("lists supported optional templates", () => {
|
|
20
|
-
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"]);
|
|
20
|
+
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "github-release", "opencode.ci.json"]);
|
|
21
21
|
});
|
|
22
22
|
it("gives every catalog template a non-empty description and file", () => {
|
|
23
23
|
expect(catalogTemplates.map((template) => template.name)).toEqual([...templateNames]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile-agent-workflows.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
|
|
5
6
|
const workflowDirectory = existsSync("loops/workflows") ? "loops/workflows" : ".github/workflows";
|
|
6
7
|
|
|
@@ -21,9 +22,22 @@ const compile = spawnSync(resolveGhPath(), ["aw", "compile", "--strict", "--dir"
|
|
|
21
22
|
shell: false,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
25
|
+
if (compile.error?.code === "ENOENT" || compile.status === null) {
|
|
25
26
|
process.stderr.write("Could not run `gh aw compile`. Install githubnext/gh-aw first.\n");
|
|
26
27
|
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
process.exit(compile.status ?? 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (compile.status !== 0) process.exit(compile.status ?? 1);
|
|
31
|
+
|
|
32
|
+
for (const file of readdirSync(workflowDirectory)) {
|
|
33
|
+
if (!file.endsWith(".lock.yml")) continue;
|
|
34
|
+
|
|
35
|
+
const path = join(workflowDirectory, file);
|
|
36
|
+
const content = readFileSync(path, "utf8");
|
|
37
|
+
const patched = content
|
|
38
|
+
.replaceAll("opencode run --print-logs --log-level DEBUG", "opencode run --log-level ERROR")
|
|
39
|
+
.replaceAll("opencode run --print-logs --log-level ERROR", "opencode run --log-level ERROR")
|
|
40
|
+
.replaceAll("--log-level DEBUG", "--log-level ERROR");
|
|
41
|
+
|
|
42
|
+
if (patched !== content) writeFileSync(path, patched);
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/release/github-release.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Publish GitHub release
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Publish release
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
steps:
|
|
18
|
+
- name: Create or update GitHub release
|
|
19
|
+
env:
|
|
20
|
+
GH_TOKEN: ${{ github.token }}
|
|
21
|
+
TAG: ${{ github.ref_name }}
|
|
22
|
+
shell: bash
|
|
23
|
+
run: |
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
27
|
+
gh release edit "$TAG" --title "$TAG" --generate-notes
|
|
28
|
+
else
|
|
29
|
+
gh release create "$TAG" --title "$TAG" --generate-notes
|
|
30
|
+
fi
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-apply-review.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Apply only actionable outstanding reviewer feedback to the selected bot pull request. Make minimal changes that address each comment. Preserve architecture and do not weaken tests. Run full verification after changes."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
WORKING_LABEL: bot-working
|
|
7
6
|
REVIEW_LABEL: review
|
|
8
7
|
REVIEW_MARKER: "<!-- agent-apply-review -->"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-audit.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Read-only repository audit. Report only reproducible, actionable defects with evidence. Look for: architectural layer violations, missing tests, security gaps, performance issues, and documentation drift. Do not modify files, commit, push, or run write operations."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
AUDIT_MARKER: "<!-- agent-audit -->"
|
|
7
6
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
8
7
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-direct.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Execute the selected issue's latest human instruction exactly as asked. Follow repository documentation and existing patterns. Keep scope to the requested outcome."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
WORKING_LABEL: bot-working
|
|
7
6
|
REVIEW_LABEL: review
|
|
8
7
|
DIRECT_LABEL: direct
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-implement.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Implement only the selected issue. Follow repository documentation and existing conventions. Do not weaken tests, lower coverage thresholds, or bypass checks. Run the project's full verification suite before creating a pull request."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
IMPLEMENT_LABEL: implement
|
|
7
6
|
WORKING_LABEL: bot-working
|
|
8
7
|
REVIEW_LABEL: review
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-merge-gate.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Make a risk-based merge decision for the selected bot pull request. Merge only when CI is green and no risk indicators are present. Flag security, schema, auth, or calculation changes for human review. Do not merge protected file changes."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
WORKING_LABEL: bot-working
|
|
7
6
|
IMPLEMENT_LABEL: implement
|
|
8
7
|
REVIEW_LABEL: review
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-propose.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Propose one focused product candidate from repository evidence and curated feature radar. Respect documented product goals and architecture boundaries. Do not propose features that conflict with the project's stated scope."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
PROPOSED_LABEL: proposed
|
|
7
6
|
IMPLEMENT_LABEL: implement
|
|
8
7
|
PROPOSE_MARKER: "<!-- agent-propose -->"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-refine.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
4
|
REPO_RULES: "Refine only the selected issue into a grounded, implementation-ready user story. Read repository documentation for domain context. Write acceptance criteria that match existing patterns. Do not implement code."
|
|
5
|
-
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
6
5
|
REFINE_LABEL: refine
|
|
7
6
|
REFINED_LABEL: refined
|
|
8
7
|
WORKING_LABEL: bot-working
|
|
@@ -39,7 +39,7 @@ on:
|
|
|
39
39
|
pull_request_target:
|
|
40
40
|
types: [opened, synchronize, reopened]
|
|
41
41
|
workflow_run:
|
|
42
|
-
workflows: ["App: CI"]
|
|
42
|
+
workflows: ["App: CI", "CI"]
|
|
43
43
|
types: [completed]
|
|
44
44
|
branches:
|
|
45
45
|
- "fix/*"
|
|
@@ -142,7 +142,10 @@ jobs:
|
|
|
142
142
|
;;
|
|
143
143
|
esac
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
# Platform bot owns Safe Outputs. Its label changes must be able to
|
|
146
|
+
# start the follow-on worker, even though GitHub does not report it
|
|
147
|
+
# as a repository collaborator.
|
|
148
|
+
if [ "$ACTOR" = "github-actions[bot]" ] || [ "$ACTOR" = "platform-devbox[bot]" ]; then
|
|
146
149
|
echo "trusted=true" >> "$GITHUB_OUTPUT"
|
|
147
150
|
exit 0
|
|
148
151
|
fi
|