@plainconceptsplatform/workflows 0.4.33 → 0.5.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.
Files changed (75) hide show
  1. package/README.md +88 -88
  2. package/loops/actions/agent-output.cjs +17 -17
  3. package/loops/actions/audit-close/action.yml +24 -0
  4. package/loops/actions/classify-route/action.yml +4 -3
  5. package/loops/actions/classify-route/classify-route.sh +69 -29
  6. package/loops/actions/load-issue-context/action.yml +2 -0
  7. package/loops/actions/update-changelog/action.yml +113 -0
  8. package/loops/actions/validate-merge-gate-output/action.yml +40 -0
  9. package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +34 -0
  10. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  11. package/loops/actions/validate-review-output/action.yml +35 -0
  12. package/loops/actions/validate-review-output/validate-review-output.sh +30 -0
  13. package/loops/actions/validate-triage-output/action.yml +36 -0
  14. package/loops/actions/validate-triage-output/validate-triage-output.sh +36 -0
  15. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -36
  16. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  17. package/loops/templates/agentics/agentics-checks.yml +86 -86
  18. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  19. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  20. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  21. package/loops/templates/issues/bug_report.yml +109 -109
  22. package/loops/templates/issues/feature_request.yml +75 -75
  23. package/loops/templates/opencode/opencode.ci.json +49 -47
  24. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  25. package/loops/templates/release/github-release.yml +30 -30
  26. package/loops/workflows/agent-apply-review.md +465 -373
  27. package/loops/workflows/agent-audit.md +213 -197
  28. package/loops/workflows/agent-implement.md +538 -374
  29. package/loops/workflows/agent-merge-gate.md +730 -485
  30. package/loops/workflows/agent-refine.md +609 -379
  31. package/loops/workflows/agent-release.md +258 -0
  32. package/loops/workflows/agent-triage.md +447 -0
  33. package/loops/workflows/authorize-bot-work.yml +85 -81
  34. package/loops/workflows/shared/opencode-ci.md +206 -197
  35. package/loops/workflows/shared/platform-defaults.md +19 -16
  36. package/loops/workflows/work-router.yml +862 -551
  37. package/package.json +42 -42
  38. package/dist/catalog-installation.d.ts +0 -30
  39. package/dist/catalog-installation.js +0 -352
  40. package/dist/catalog-installation.test.d.ts +0 -1
  41. package/dist/catalog-installation.test.js +0 -448
  42. package/dist/catalog-listing.d.ts +0 -13
  43. package/dist/catalog-listing.js +0 -70
  44. package/dist/catalog-listing.test.d.ts +0 -1
  45. package/dist/catalog-listing.test.js +0 -150
  46. package/dist/index.d.ts +0 -2
  47. package/dist/index.js +0 -218
  48. package/dist/index.test.d.ts +0 -1
  49. package/dist/index.test.js +0 -273
  50. package/dist/repository-inspection.d.ts +0 -23
  51. package/dist/repository-inspection.js +0 -113
  52. package/dist/repository-inspection.test.d.ts +0 -1
  53. package/dist/repository-inspection.test.js +0 -77
  54. package/dist/repository-state.d.ts +0 -18
  55. package/dist/repository-state.js +0 -77
  56. package/dist/repository-state.test.d.ts +0 -1
  57. package/dist/repository-state.test.js +0 -96
  58. package/dist/route-processing.d.ts +0 -6
  59. package/dist/route-processing.js +0 -150
  60. package/dist/route-processing.test.d.ts +0 -1
  61. package/dist/route-processing.test.js +0 -342
  62. package/dist/stack-defaults.d.ts +0 -11
  63. package/dist/stack-defaults.js +0 -104
  64. package/dist/stack-defaults.test.d.ts +0 -1
  65. package/dist/stack-defaults.test.js +0 -266
  66. package/dist/tui.d.ts +0 -25
  67. package/dist/tui.js +0 -287
  68. package/dist/tui.test.d.ts +0 -1
  69. package/dist/tui.test.js +0 -246
  70. package/dist/workflow-catalog.d.ts +0 -25
  71. package/dist/workflow-catalog.js +0 -57
  72. package/dist/workflow-catalog.test.d.ts +0 -1
  73. package/dist/workflow-catalog.test.js +0 -29
  74. package/loops/workflows/agent-direct.md +0 -363
  75. package/loops/workflows/agent-propose.md +0 -342
@@ -1,448 +0,0 @@
1
- import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
- import { constants } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { afterEach, describe, expect, it } from "vitest";
6
- import { catalogSourcePath, ensurePreCommitHook, installCatalog, installedRoutes, installMandatoryFiles, installTemplate, removeRouteFiles } from "./catalog-installation.js";
7
- const temporaryDirectories = [];
8
- afterEach(async () => {
9
- await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
10
- });
11
- describe("catalog installation", () => {
12
- it("resolves loops beside built package files", async () => {
13
- const packageDirectory = await createDirectory({
14
- "dist/catalog-installation.js": "export {};\n",
15
- "loops/workflows/agent-check.md": "# Check\n",
16
- });
17
- expect(catalogSourcePath(join(packageDirectory, "dist", "catalog-installation.js"))).toBe(join(packageDirectory, "loops"));
18
- });
19
- it("installCatalog with empty selectedRoutes installs mandatory and infrastructure files but no worker .md files", async () => {
20
- const sourcePath = await createDirectory({
21
- "actions/check/action.yml": "name: Check\n",
22
- "workflows/agent-refine.md": "# Refine\n",
23
- "workflows/agent-implement.md": "# Implement\n",
24
- "workflows/agent-direct.md": "# Direct\n",
25
- "workflows/agent-audit.md": "# Audit\n",
26
- "workflows/agent-propose.md": "# Propose\n",
27
- "workflows/agent-apply-review.md": "# Apply Review\n",
28
- "workflows/agent-merge-gate.md": "# Merge Gate\n",
29
- "workflows/shared/defaults.md": "defaults\n",
30
- "workflows/work-router.yml": "name: Router\n",
31
- "scripts/compile-agent-workflows.mjs": "compile\n",
32
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
33
- });
34
- const repositoryPath = await createDirectory({});
35
- const result = await installCatalog(repositoryPath, { sourcePath, selectedRoutes: [] });
36
- expect(result.installed).toContain("opencode.ci.json");
37
- expect(result.installed).toContain("scripts/compile-agent-workflows.mjs");
38
- expect(result.installed).toContain(".github/actions/check/action.yml");
39
- expect(result.installed.some((f) => f.endsWith("shared/defaults.md"))).toBe(true);
40
- expect(result.installed.some((f) => f.endsWith("work-router.yml"))).toBe(true);
41
- // No worker .md files
42
- expect(result.installed).not.toContain(".github/workflows/agent-refine.md");
43
- expect(result.installed).not.toContain(".github/workflows/agent-implement.md");
44
- expect(result.installed).not.toContain(".github/workflows/agent-audit.md");
45
- expect(result.installed).not.toContain(".github/workflows/agent-propose.md");
46
- });
47
- it("installs package-owned loops files including mandatory opencode.ci.json and compile script", async () => {
48
- const sourcePath = await createDirectory({
49
- "actions/check/action.yml": "name: Check\n",
50
- "workflows/agent-check.md": "# Check\n",
51
- "workflows/shared/defaults.md": "defaults\n",
52
- "scripts/compile-agent-workflows.mjs": "console.log('compile');\n",
53
- "scripts/compile.mjs": "console.log('old compile');\n",
54
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
55
- "workflows/agent-check.lock.yml": "generated\n",
56
- "actions/actions-lock.json": "generated\n",
57
- });
58
- const repositoryPath = await createDirectory({});
59
- await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
60
- installed: [
61
- ".github/actions/check/action.yml",
62
- ".github/workflows/agent-check.md",
63
- ".github/workflows/shared/defaults.md",
64
- "opencode.ci.json",
65
- "scripts/compile-agent-workflows.mjs",
66
- "scripts/compile.mjs",
67
- ],
68
- conflicts: [],
69
- });
70
- });
71
- it("copies ownership headers for base workflows and selected templates", async () => {
72
- const header = "# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-check.md. Update with `workflows update --force`; consumer edits may be overwritten.\n";
73
- const templateHeader = "# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.\n";
74
- const sourcePath = await createDirectory({
75
- "actions/check/action.yml": "# Managed by @plainconceptsplatform/workflows. Source: loops/actions/check/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.\nname: Check\n",
76
- "workflows/agent-check.md": `---\n${header}# Check\n`,
77
- "scripts/compile-agent-workflows.mjs": "// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile-agent-workflows.mjs. Update with `workflows update --force`; consumer edits may be overwritten.\n",
78
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
79
- "templates/agentics/agentics-checks.yml": `${templateHeader}name: Agentics checks\n`,
80
- });
81
- const repositoryPath = await createDirectory({});
82
- await installCatalog(repositoryPath, { sourcePath });
83
- await installTemplate(repositoryPath, "agentics-checks", { sourcePath });
84
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).resolves.toMatch(new RegExp(`^---\\n${escapeRegularExpression(header)}`));
85
- await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).resolves.toMatch(new RegExp(`^${escapeRegularExpression(templateHeader)}`));
86
- });
87
- it("does not report identical managed files as conflicts", async () => {
88
- const sourcePath = await createDirectory({
89
- "actions/check/action.yml": "name: Check\n",
90
- "workflows/agent-check.md": "# Check\n",
91
- "scripts/compile-agent-workflows.mjs": "compile\n",
92
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
93
- });
94
- const repositoryPath = await createDirectory({ ".github/workflows/agent-check.md": "# Check\n" });
95
- await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({ conflicts: [] });
96
- });
97
- it("does not manage legacy repository configuration files", async () => {
98
- const sourcePath = await createDirectory({
99
- "actions/check/action.yml": "name: Check\n",
100
- "workflows/agent-check.md": "# Check\n",
101
- "scripts/compile-agent-workflows.mjs": "compile\n",
102
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
103
- });
104
- const repositoryPath = await createDirectory({
105
- ".github/workflows/shared/repo-config.md": "legacy consumer config\n",
106
- });
107
- await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({
108
- installed: [
109
- ".github/actions/check/action.yml",
110
- ".github/workflows/agent-check.md",
111
- "opencode.ci.json",
112
- "scripts/compile-agent-workflows.mjs",
113
- ],
114
- conflicts: [],
115
- });
116
- await expect(readFile(join(repositoryPath, ".github/workflows/shared/repo-config.md"), "utf8")).resolves.toBe("legacy consumer config\n");
117
- });
118
- it("requires force for different managed files", async () => {
119
- const sourcePath = await createDirectory({
120
- "actions/check/action.yml": "package action\n",
121
- "workflows/agent-check.md": "package workflow\n",
122
- "scripts/compile-agent-workflows.mjs": "package script\n",
123
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
124
- });
125
- const repositoryPath = await createDirectory({
126
- ".github/actions/check/action.yml": "consumer action\n",
127
- ".github/workflows/agent-check.md": "consumer workflow\n",
128
- "scripts/compile-agent-workflows.mjs": "consumer script\n",
129
- "opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
130
- });
131
- await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
132
- installed: [],
133
- conflicts: [
134
- ".github/actions/check/action.yml",
135
- ".github/workflows/agent-check.md",
136
- "opencode.ci.json",
137
- "scripts/compile-agent-workflows.mjs",
138
- ],
139
- });
140
- await expect(installCatalog(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
141
- conflicts: [
142
- ".github/actions/check/action.yml",
143
- ".github/workflows/agent-check.md",
144
- "opencode.ci.json",
145
- "scripts/compile-agent-workflows.mjs",
146
- ],
147
- });
148
- await expect(readFile(join(repositoryPath, ".github/actions/check/action.yml"), "utf8")).resolves.toBe("package action\n");
149
- });
150
- it("installs templates only when explicitly selected", async () => {
151
- const sourcePath = await createDirectory({
152
- "actions/check/action.yml": "name: Check\n",
153
- "workflows/agent-check.md": "# Check\n",
154
- "scripts/compile-agent-workflows.mjs": "compile\n",
155
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
156
- "templates/agentics/agentics-checks.yml": "name: Agentics checks\n",
157
- "templates/ci/app-ci-node-monorepo.yml": "name: Node CI\n",
158
- "templates/release/github-release.yml": "name: Publish GitHub release\n",
159
- });
160
- const repositoryPath = await createDirectory({});
161
- await installCatalog(repositoryPath, { sourcePath });
162
- await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).rejects.toThrow();
163
- await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
164
- installed: [".github/workflows/agentics-checks.yml"],
165
- conflicts: [],
166
- });
167
- await expect(installTemplate(repositoryPath, "app-ci-node-monorepo", { sourcePath })).resolves.toEqual({
168
- installed: [".github/workflows/app-ci-node-monorepo.yml"],
169
- conflicts: [],
170
- });
171
- await expect(installTemplate(repositoryPath, "github-release", { sourcePath })).resolves.toEqual({
172
- installed: [".github/workflows/github-release.yml"],
173
- conflicts: [],
174
- });
175
- });
176
- it("installCatalog installs mandatory opencode.ci.json and compile script alongside catalog files", async () => {
177
- const sourcePath = await createDirectory({
178
- "actions/check/action.yml": "name: Check\n",
179
- "workflows/agent-check.md": "# Check\n",
180
- "scripts/compile-agent-workflows.mjs": "compile\n",
181
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
182
- });
183
- const repositoryPath = await createDirectory({});
184
- const result = await installCatalog(repositoryPath, { sourcePath });
185
- expect(result.installed).toContain("opencode.ci.json");
186
- expect(result.installed).toContain("scripts/compile-agent-workflows.mjs");
187
- await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
188
- await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
189
- });
190
- it("installMandatoryFiles installs opencode.ci.json and compile script without catalog files", async () => {
191
- const sourcePath = await createDirectory({
192
- "scripts/compile-agent-workflows.mjs": "compile\n",
193
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
194
- });
195
- const repositoryPath = await createDirectory({});
196
- const result = await installMandatoryFiles(repositoryPath, { sourcePath });
197
- expect(result.installed).toEqual(["opencode.ci.json", "scripts/compile-agent-workflows.mjs"]);
198
- await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
199
- await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
200
- });
201
- it("installMandatoryFiles reports conflicts on differing files", async () => {
202
- const sourcePath = await createDirectory({
203
- "scripts/compile-agent-workflows.mjs": "package script\n",
204
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
205
- });
206
- const repositoryPath = await createDirectory({
207
- "opencode.ci.json": "{ \"model\": \"consumer\" }\n",
208
- "scripts/compile-agent-workflows.mjs": "consumer script\n",
209
- });
210
- await expect(installMandatoryFiles(repositoryPath, { sourcePath })).resolves.toEqual({
211
- installed: [],
212
- conflicts: ["opencode.ci.json", "scripts/compile-agent-workflows.mjs"],
213
- });
214
- });
215
- it("installMandatoryFiles overwrites with force", async () => {
216
- const sourcePath = await createDirectory({
217
- "scripts/compile-agent-workflows.mjs": "package script\n",
218
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
219
- });
220
- const repositoryPath = await createDirectory({
221
- "opencode.ci.json": "{ \"model\": \"consumer\" }\n",
222
- "scripts/compile-agent-workflows.mjs": "consumer script\n",
223
- });
224
- await expect(installMandatoryFiles(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
225
- installed: ["opencode.ci.json", "scripts/compile-agent-workflows.mjs"],
226
- });
227
- await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
228
- });
229
- it("installMandatoryFiles does not install catalog workflow files", async () => {
230
- const sourcePath = await createDirectory({
231
- "actions/check/action.yml": "name: Check\n",
232
- "workflows/agent-check.md": "# Check\n",
233
- "scripts/compile-agent-workflows.mjs": "compile\n",
234
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
235
- });
236
- const repositoryPath = await createDirectory({});
237
- await installMandatoryFiles(repositoryPath, { sourcePath });
238
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).rejects.toThrow();
239
- });
240
- it("installCatalog deduplicates compile script in both scripts/ and mandatory files", async () => {
241
- const sourcePath = await createDirectory({
242
- "actions/check/action.yml": "name: Check\n",
243
- "workflows/agent-check.md": "# Check\n",
244
- "scripts/compile-agent-workflows.mjs": "same compile script\n",
245
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
246
- });
247
- const repositoryPath = await createDirectory({});
248
- const result = await installCatalog(repositoryPath, { sourcePath });
249
- const compileEntries = result.installed.filter((path) => path === "scripts/compile-agent-workflows.mjs");
250
- expect(compileEntries).toHaveLength(1);
251
- });
252
- it("creates a Husky pre-commit hook when the consumer has none", async () => {
253
- const repositoryPath = await createDirectory({});
254
- await ensurePreCommitHook(repositoryPath);
255
- await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
256
- .resolves.toBe("if git diff --cached --name-only -- .github | grep -q .; then\n node scripts/compile-agent-workflows.mjs\n git add -- .github/workflows/*.lock.yml\n [ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\nfi\n");
257
- });
258
- it("keeps existing pre-commit commands and appends the compiler once", async () => {
259
- const repositoryPath = await createDirectory({ ".husky/pre-commit": "pnpm lint\n" });
260
- await ensurePreCommitHook(repositoryPath);
261
- await ensurePreCommitHook(repositoryPath);
262
- await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
263
- .resolves.toBe("pnpm lint\nif git diff --cached --name-only -- .github | grep -q .; then\n node scripts/compile-agent-workflows.mjs\n git add -- .github/workflows/*.lock.yml\n [ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\nfi\n");
264
- });
265
- it("upgrades an existing compiler hook to stage generated locks", async () => {
266
- const repositoryPath = await createDirectory({ ".husky/pre-commit": "node scripts/compile-agent-workflows.mjs\n" });
267
- await ensurePreCommitHook(repositoryPath);
268
- await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
269
- .resolves.toContain("if git diff --cached --name-only -- .github | grep -q .; then");
270
- });
271
- it("repairs a malformed compiler hook prefixed with pnpm exec", async () => {
272
- const repositoryPath = await createDirectory({
273
- ".husky/pre-commit": "pnpm exec if git diff --cached --name-only -- .github | grep -q .; then\n node scripts/compile-agent-workflows.mjs\n git add -- .github/workflows/*.lock.yml\n [ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\nfi\n",
274
- });
275
- await ensurePreCommitHook(repositoryPath);
276
- await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
277
- .resolves.not.toContain("pnpm exec if");
278
- });
279
- it("leaves consumer files untouched when staged workflow compilation fails", async () => {
280
- const sourcePath = await createDirectory({
281
- "actions/check/action.yml": "package action\n",
282
- "workflows/agent-check.md": "package workflow\n",
283
- "scripts/compile-agent-workflows.mjs": "compile\n",
284
- "templates/opencode/opencode.ci.json": "{ \"model\": \"package\" }\n",
285
- });
286
- const repositoryPath = await createDirectory({
287
- ".github/actions/check/action.yml": "consumer action\n",
288
- ".github/workflows/agent-check.md": "consumer workflow\n",
289
- "opencode.ci.json": "{ \"model\": \"consumer\" }\n",
290
- "scripts/compile-agent-workflows.mjs": "consumer compiler\n",
291
- });
292
- await expect(installCatalog(repositoryPath, {
293
- force: true,
294
- sourcePath,
295
- compile: async () => { throw new Error("compile failed"); },
296
- })).rejects.toThrow("compile failed");
297
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8"))
298
- .resolves.toBe("consumer workflow\n");
299
- await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8"))
300
- .resolves.toBe("consumer compiler\n");
301
- await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8")).rejects.toThrow();
302
- });
303
- it("preserves consumer-specific worker environment values during a forced update", async () => {
304
- const sourcePath = await createDirectory({
305
- "actions/check/action.yml": "name: Check\n",
306
- "workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"package verify\"\n REPO_RULES: \"package rules\"\n---\nengine:\n env:\n OPENAI_BASE_URL: https://forge.plainconcepts.com/v1\n",
307
- "scripts/compile-agent-workflows.mjs": "compile\n",
308
- "templates/opencode/opencode.ci.json": "{}\n",
309
- });
310
- const repositoryPath = await createDirectory({
311
- ".github/workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"consumer verify\"\n REPO_RULES: \"consumer rules\"\n---\nengine:\n env:\n OPENAI_BASE_URL: https://consumer.example/v1\n",
312
- });
313
- await installCatalog(repositoryPath, { force: true, sourcePath });
314
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).resolves.toContain(" REPO_RULES: \"consumer rules\"\n---\nengine:\n env:\n OPENAI_BASE_URL: https://consumer.example/v1\n");
315
- });
316
- it("applies staged generated locks with managed sources", async () => {
317
- const sourcePath = await createDirectory({
318
- "actions/check/action.yml": "package action\n",
319
- "workflows/agent-check.md": "package workflow\n",
320
- "scripts/compile-agent-workflows.mjs": "compile\n",
321
- "templates/opencode/opencode.ci.json": "{ \"model\": \"package\" }\n",
322
- });
323
- const repositoryPath = await createDirectory({});
324
- await installCatalog(repositoryPath, {
325
- sourcePath,
326
- compile: async (stagingPath) => {
327
- await expect(access(join(stagingPath, ".git"), constants.F_OK)).resolves.toBeUndefined();
328
- await writeFile(join(stagingPath, ".github", "workflows", "agent-check.lock.yml"), "opencode run --log-level ERROR\n", "utf8");
329
- },
330
- });
331
- await expect(readFile(join(repositoryPath, ".github", "workflows", "agent-check.lock.yml"), "utf8"))
332
- .resolves.toBe("opencode run --log-level ERROR\n");
333
- });
334
- it("installs the opencode.ci.json template to the repository root", async () => {
335
- const sourcePath = await createDirectory({
336
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
337
- });
338
- const repositoryPath = await createDirectory({});
339
- await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
340
- installed: ["opencode.ci.json"],
341
- conflicts: [],
342
- });
343
- await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
344
- });
345
- it("installs the .NET and Next.js CI template as app-ci.yml", async () => {
346
- const sourcePath = await createDirectory({
347
- "templates/ci/app-ci-dotnet-next.yml": "name: App: CI\n",
348
- });
349
- const repositoryPath = await createDirectory({});
350
- await expect(installTemplate(repositoryPath, "app-ci-dotnet-next", { sourcePath })).resolves.toEqual({
351
- installed: [".github/workflows/app-ci.yml"],
352
- conflicts: [],
353
- });
354
- await expect(readFile(join(repositoryPath, ".github/workflows/app-ci.yml"), "utf8")).resolves.toBe("name: App: CI\n");
355
- });
356
- it("requires force to replace the opencode.ci.json template", async () => {
357
- const sourcePath = await createDirectory({
358
- "templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
359
- });
360
- const repositoryPath = await createDirectory({
361
- "opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
362
- });
363
- await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
364
- installed: [],
365
- conflicts: ["opencode.ci.json"],
366
- });
367
- await expect(installTemplate(repositoryPath, "opencode.ci.json", { force: true, sourcePath })).resolves.toMatchObject({
368
- installed: ["opencode.ci.json"],
369
- });
370
- await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
371
- });
372
- it("requires force to replace a selected template", async () => {
373
- const sourcePath = await createDirectory({
374
- "templates/agentics/agentics-checks.yml": "package template\n",
375
- });
376
- const repositoryPath = await createDirectory({
377
- ".github/workflows/agentics-checks.yml": "consumer template\n",
378
- });
379
- await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
380
- installed: [],
381
- conflicts: [".github/workflows/agentics-checks.yml"],
382
- });
383
- await expect(installTemplate(repositoryPath, "agentics-checks", { force: true, sourcePath })).resolves.toMatchObject({
384
- installed: [".github/workflows/agentics-checks.yml"],
385
- });
386
- });
387
- it("installs issue templates to .github/ISSUE_TEMPLATE/", async () => {
388
- const sourcePath = await createDirectory({
389
- "templates/issues/bug_report.yml": "name: Bug report\n",
390
- "templates/issues/feature_request.yml": "name: Feature request\n",
391
- });
392
- const repositoryPath = await createDirectory({});
393
- await expect(installTemplate(repositoryPath, "bug-report", { sourcePath })).resolves.toEqual({
394
- installed: [".github/ISSUE_TEMPLATE/bug_report.yml"],
395
- conflicts: [],
396
- });
397
- await expect(installTemplate(repositoryPath, "feature-request", { sourcePath })).resolves.toEqual({
398
- installed: [".github/ISSUE_TEMPLATE/feature_request.yml"],
399
- conflicts: [],
400
- });
401
- await expect(readFile(join(repositoryPath, ".github/ISSUE_TEMPLATE/bug_report.yml"), "utf8")).resolves.toBe("name: Bug report\n");
402
- await expect(readFile(join(repositoryPath, ".github/ISSUE_TEMPLATE/feature_request.yml"), "utf8")).resolves.toBe("name: Feature request\n");
403
- });
404
- });
405
- describe("route lifecycle", () => {
406
- it("detects installed route workers in workflowRoutes order", async () => {
407
- const repositoryPath = await createDirectory({
408
- ".github/workflows/agent-implement.md": "# Implement\n",
409
- ".github/workflows/agent-refine.md": "# Refine\n",
410
- });
411
- await expect(installedRoutes(repositoryPath)).resolves.toEqual(["refine", "implement"]);
412
- });
413
- it("returns an empty list when no route workers are installed", async () => {
414
- const repositoryPath = await createDirectory({});
415
- await expect(installedRoutes(repositoryPath)).resolves.toEqual([]);
416
- });
417
- it("removes a route worker and its generated lock, leaving other workers", async () => {
418
- const repositoryPath = await createDirectory({
419
- ".github/workflows/agent-refine.md": "# Refine\n",
420
- ".github/workflows/agent-refine.lock.yml": "generated\n",
421
- ".github/workflows/agent-implement.md": "# Implement\n",
422
- });
423
- await expect(removeRouteFiles(repositoryPath, ["refine"])).resolves.toEqual([
424
- ".github/workflows/agent-refine.lock.yml",
425
- ".github/workflows/agent-refine.md",
426
- ]);
427
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-refine.md"), "utf8")).rejects.toThrow();
428
- await expect(readFile(join(repositoryPath, ".github/workflows/agent-implement.md"), "utf8")).resolves.toBe("# Implement\n");
429
- });
430
- it("ignores routes that are not installed", async () => {
431
- const repositoryPath = await createDirectory({});
432
- await expect(removeRouteFiles(repositoryPath, ["propose"])).resolves.toEqual([]);
433
- });
434
- });
435
- async function createDirectory(files) {
436
- const directory = await mkdtemp(join(tmpdir(), "workflows-"));
437
- temporaryDirectories.push(directory);
438
- await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
439
- const path = join(directory, relativePath);
440
- const { mkdir } = await import("node:fs/promises");
441
- await mkdir(dirname(path), { recursive: true });
442
- await writeFile(path, content, "utf8");
443
- }));
444
- return directory;
445
- }
446
- function escapeRegularExpression(value) {
447
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
448
- }
@@ -1,13 +0,0 @@
1
- export interface CatalogEntry {
2
- readonly kind: "route" | "template";
3
- readonly name: string;
4
- readonly description: string;
5
- readonly file: string;
6
- readonly installed: boolean;
7
- }
8
- export interface ListOptions {
9
- readonly installedPath?: string;
10
- }
11
- export declare function listCatalog(options?: ListOptions): Promise<readonly CatalogEntry[]>;
12
- export declare function searchCatalog(entries: readonly CatalogEntry[], query: string): readonly CatalogEntry[];
13
- export declare function formatCatalog(entries: readonly CatalogEntry[]): string;
@@ -1,70 +0,0 @@
1
- import { access, constants } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { catalogTemplates, workflowRoutes } from "./workflow-catalog.js";
4
- export async function listCatalog(options = {}) {
5
- const basePath = options.installedPath ?? process.cwd();
6
- const routes = await Promise.all(workflowRoutes.map(async (route) => ({
7
- kind: "route",
8
- name: route.name,
9
- description: route.description,
10
- file: route.worker,
11
- installed: await isFileInstalled(basePath, route.worker),
12
- })));
13
- const templates = await Promise.all(catalogTemplates.map(async (template) => ({
14
- kind: "template",
15
- name: template.name,
16
- description: template.description,
17
- file: template.file,
18
- installed: await isFileInstalled(basePath, template.file, template.target),
19
- })));
20
- return [...routes, ...templates];
21
- }
22
- export function searchCatalog(entries, query) {
23
- const normalized = query.toLowerCase().trim();
24
- if (normalized === "")
25
- return [];
26
- return entries.filter((entry) => entry.name.toLowerCase().includes(normalized) ||
27
- entry.description.toLowerCase().includes(normalized));
28
- }
29
- export function formatCatalog(entries) {
30
- if (entries.length === 0)
31
- return "No workflows matched the search query.";
32
- const routes = entries.filter((entry) => entry.kind === "route");
33
- const templates = entries.filter((entry) => entry.kind === "template");
34
- const lines = [];
35
- if (routes.length > 0) {
36
- lines.push("Workflows:");
37
- for (const entry of routes) {
38
- lines.push(formatEntry(entry));
39
- }
40
- }
41
- if (templates.length > 0) {
42
- if (lines.length > 0)
43
- lines.push("");
44
- lines.push("Templates:");
45
- for (const entry of templates) {
46
- lines.push(formatEntry(entry));
47
- }
48
- }
49
- return lines.join("\n");
50
- }
51
- function formatEntry(entry) {
52
- const mark = entry.installed ? "[x]" : "[ ]";
53
- return ` ${mark} ${entry.name} — ${entry.description}`;
54
- }
55
- async function isFileInstalled(basePath, workerOrTemplateFile, explicitTarget) {
56
- const installedPath = templateInstallPath(basePath, workerOrTemplateFile, explicitTarget);
57
- try {
58
- await access(installedPath, constants.F_OK);
59
- return true;
60
- }
61
- catch {
62
- return false;
63
- }
64
- }
65
- function templateInstallPath(basePath, file, explicitTarget) {
66
- if (explicitTarget !== undefined)
67
- return join(basePath, ...explicitTarget.split("/"));
68
- const isRootTemplate = file.endsWith(".json");
69
- return isRootTemplate ? join(basePath, file) : join(basePath, ".github", "workflows", file);
70
- }
@@ -1 +0,0 @@
1
- export {};