@plainconceptsplatform/workflows 0.4.34 → 0.6.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 +88 -88
- package/dist/action-validation.test.js +5 -2
- package/dist/catalog-installation.js +50 -7
- package/dist/catalog-installation.test.js +59 -22
- package/dist/catalog-listing.test.js +4 -4
- package/dist/index.js +48 -48
- package/dist/index.test.js +9 -9
- package/dist/repository-inspection.test.js +2 -2
- package/dist/route-processing.js +24 -25
- package/dist/route-processing.test.js +166 -233
- package/dist/stack-defaults.js +14 -14
- package/dist/stack-defaults.test.js +79 -79
- package/dist/tui.test.js +10 -7
- package/dist/workflow-catalog.d.ts +1 -1
- package/dist/workflow-catalog.js +1 -5
- package/loops/actions/add-issue-labels/action.yml +2 -2
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +8 -3
- package/loops/actions/classify-route/classify-route.sh +58 -38
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -40
- package/loops/actions/validate-refine-output/validate-refine-output.sh +34 -5
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +375 -36
- package/loops/scripts/compile-agent-workflows.mjs +290 -6
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +125 -8
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -178
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +49 -47
- package/loops/templates/opencode/opencode.ci.json.md +49 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +469 -443
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +640 -414
- package/loops/workflows/agent-merge-gate.md +844 -584
- package/loops/workflows/agent-refine.md +633 -418
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -439
- package/loops/workflows/authorize-bot-work.yml +85 -82
- package/loops/workflows/shared/opencode-ci.md +206 -197
- package/loops/workflows/shared/platform-defaults.md +19 -16
- package/loops/workflows/work-router.yml +1038 -632
- package/package.json +7 -8
- package/dist/repository-state.d.ts +0 -18
- package/dist/repository-state.js +0 -77
- package/dist/repository-state.test.d.ts +0 -1
- package/dist/repository-state.test.js +0 -96
- package/loops/workflows/agent-direct.md +0 -375
- package/loops/workflows/agent-propose.md +0 -342
package/README.md
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
# Workflows CLI
|
|
2
|
-
|
|
3
|
-
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
|
-
|
|
5
|
-
## Quick start
|
|
6
|
-
|
|
7
|
-
The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx @plainconceptsplatform/workflows
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
|
|
14
|
-
|
|
15
|
-
## Install
|
|
16
|
-
|
|
17
|
-
Before installing workflows, install and configure [`PlainConceptsPlatform/agent-harness`](https://github.com/PlainConceptsPlatform/agent-harness) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
|
|
18
|
-
|
|
19
|
-
For non-interactive use (advanced):
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx @plainconceptsplatform/workflows@latest init
|
|
23
|
-
npx @plainconceptsplatform/workflows@latest add
|
|
24
|
-
npx @plainconceptsplatform/workflows@latest update
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
For a project-local development dependency:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pnpm add -D @plainconceptsplatform/workflows
|
|
31
|
-
pnpm exec workflows # launch interactive TUI
|
|
32
|
-
pnpm exec workflows init
|
|
33
|
-
pnpm exec workflows add
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
|
-
|
|
38
|
-
`add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
workflows add # mandatory files only, no worker .md files
|
|
42
|
-
workflows add implement refine direct # those route workers plus mandatory files
|
|
43
|
-
workflows add --template agentics-checks # named template only (no mandatory files)
|
|
44
|
-
workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
|
|
48
|
-
|
|
49
|
-
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
## List and search
|
|
54
|
-
|
|
55
|
-
List all available workflows, routes, and templates with install status:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
|
|
62
|
-
|
|
63
|
-
Search by name or description:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Manual installation
|
|
70
|
-
|
|
71
|
-
The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
72
|
-
|
|
73
|
-
- `loops/actions/` maps to `.github/actions/`
|
|
74
|
-
- `loops/workflows/` maps to `.github/workflows/`
|
|
75
|
-
- `loops/scripts/` maps to `scripts/`
|
|
76
|
-
|
|
77
|
-
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
78
|
-
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
79
|
-
|
|
80
|
-
## Compile
|
|
81
|
-
|
|
82
|
-
Consumer repositories generate and commit `*.lock.yml` files:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
node scripts/compile-agent-workflows.mjs --force
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Do not commit generated locks to this package source repository.
|
|
1
|
+
# Workflows CLI
|
|
2
|
+
|
|
3
|
+
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @plainconceptsplatform/workflows
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Before installing workflows, install and configure [`PlainConceptsPlatform/agent-harness`](https://github.com/PlainConceptsPlatform/agent-harness) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
|
|
18
|
+
|
|
19
|
+
For non-interactive use (advanced):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @plainconceptsplatform/workflows@latest init
|
|
23
|
+
npx @plainconceptsplatform/workflows@latest add
|
|
24
|
+
npx @plainconceptsplatform/workflows@latest update
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a project-local development dependency:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add -D @plainconceptsplatform/workflows
|
|
31
|
+
pnpm exec workflows # launch interactive TUI
|
|
32
|
+
pnpm exec workflows init
|
|
33
|
+
pnpm exec workflows add
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
|
+
|
|
38
|
+
`add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
workflows add # mandatory files only, no worker .md files
|
|
42
|
+
workflows add implement refine direct # those route workers plus mandatory files
|
|
43
|
+
workflows add --template agentics-checks # named template only (no mandatory files)
|
|
44
|
+
workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
|
|
48
|
+
|
|
49
|
+
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
50
|
+
|
|
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
|
+
|
|
53
|
+
## List and search
|
|
54
|
+
|
|
55
|
+
List all available workflows, routes, and templates with install status:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
|
|
62
|
+
|
|
63
|
+
Search by name or description:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Manual installation
|
|
70
|
+
|
|
71
|
+
The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
72
|
+
|
|
73
|
+
- `loops/actions/` maps to `.github/actions/`
|
|
74
|
+
- `loops/workflows/` maps to `.github/workflows/`
|
|
75
|
+
- `loops/scripts/` maps to `scripts/`
|
|
76
|
+
|
|
77
|
+
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
78
|
+
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
79
|
+
|
|
80
|
+
## Compile
|
|
81
|
+
|
|
82
|
+
Consumer repositories generate and commit `*.lock.yml` files:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
node scripts/compile-agent-workflows.mjs --force
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Do not commit generated locks to this package source repository.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { describe, expect, it } from "vitest";
|
|
4
5
|
import { parse } from "yaml";
|
|
5
|
-
import { catalogSourcePath } from "./catalog-installation.js";
|
|
6
6
|
async function collectActionYmlFiles(actionsDir) {
|
|
7
7
|
const entries = await readdir(actionsDir, { withFileTypes: true });
|
|
8
8
|
const results = [];
|
|
@@ -18,7 +18,10 @@ async function collectActionYmlFiles(actionsDir) {
|
|
|
18
18
|
return results;
|
|
19
19
|
}
|
|
20
20
|
async function loadManifests() {
|
|
21
|
-
|
|
21
|
+
// catalogSourcePath() resolves against the published package layout, where loops/ ships
|
|
22
|
+
// next to the compiled module. In this repository loops/ lives at the root, two levels
|
|
23
|
+
// above this test, so resolve it from the source tree instead.
|
|
24
|
+
const actionsDir = join(fileURLToPath(new URL("../../loops", import.meta.url)), "actions");
|
|
22
25
|
const files = await collectActionYmlFiles(actionsDir);
|
|
23
26
|
return Promise.all(files.map(async (file) => {
|
|
24
27
|
const content = await readFile(file, "utf8");
|
|
@@ -83,7 +83,29 @@ function mergeWorkerEnv(packageContent, consumerContent) {
|
|
|
83
83
|
if (endpoint !== undefined) {
|
|
84
84
|
result = result.replace(/^ OPENAI_BASE_URL: .+$/m, ` OPENAI_BASE_URL: ${endpoint}`);
|
|
85
85
|
}
|
|
86
|
-
return result;
|
|
86
|
+
return preserveRunnerPool(result, consumerContent);
|
|
87
|
+
}
|
|
88
|
+
// Which self-hosted pool a worker's agent jobs run on. gh-aw cannot share runs-on through
|
|
89
|
+
// an import, so every worker names it, and it belongs with the env values rather than with
|
|
90
|
+
// the prompt: a forced update that rewrote it would move a repository's agents onto another
|
|
91
|
+
// pool, and one of ours has two machines in it. ubuntu-latest is excluded, being GitHub's
|
|
92
|
+
// own runner, used by the deterministic jobs everywhere and chosen by nobody.
|
|
93
|
+
function runnerPools(content) {
|
|
94
|
+
const found = new Set();
|
|
95
|
+
for (const match of content.matchAll(/^\s*runs-on(?:-slim)?: (\S+)\s*$/gm)) {
|
|
96
|
+
if (match[1] !== "ubuntu-latest")
|
|
97
|
+
found.add(match[1]);
|
|
98
|
+
}
|
|
99
|
+
return [...found];
|
|
100
|
+
}
|
|
101
|
+
function preserveRunnerPool(packageContent, consumerContent) {
|
|
102
|
+
const mine = runnerPools(consumerContent);
|
|
103
|
+
const theirs = runnerPools(packageContent);
|
|
104
|
+
// Only an unambiguous swap. A consumer naming several pools has drifted rather than
|
|
105
|
+
// decided, and guessing which it meant is worse than leaving the package's.
|
|
106
|
+
if (mine.length !== 1 || theirs.length !== 1 || mine[0] === theirs[0])
|
|
107
|
+
return packageContent;
|
|
108
|
+
return packageContent.replace(/^(\s*runs-on(?:-slim)?: )(\S+)(\s*)$/gm, (line, prefix, pool, tail) => pool === "ubuntu-latest" ? line : `${prefix}${mine[0]}${tail}`);
|
|
87
109
|
}
|
|
88
110
|
function workerEnvValues(content) {
|
|
89
111
|
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/m.exec(content)?.[1];
|
|
@@ -113,21 +135,42 @@ function transformOpencodeFiles(files, inspection) {
|
|
|
113
135
|
}
|
|
114
136
|
return result;
|
|
115
137
|
}
|
|
138
|
+
// Templates that need a companion config beside the workflow file. actionlint only
|
|
139
|
+
// knows GitHub-hosted runner labels, so every workflow naming a self-hosted label is an
|
|
140
|
+
// error without this file; it belongs wherever the lint workflow that reads it lives.
|
|
141
|
+
const templateCompanions = {
|
|
142
|
+
"agentics-checks": [{ source: "templates/agentics/actionlint.yaml", target: ".github/actionlint.yaml" }],
|
|
143
|
+
};
|
|
116
144
|
export async function installTemplate(repositoryPath, template, options = {}) {
|
|
117
145
|
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
118
146
|
const meta = catalogTemplateMeta(template);
|
|
119
147
|
const source = join(sourcePath, "templates", meta.directory, meta.file);
|
|
120
148
|
const target = meta.target;
|
|
121
|
-
const
|
|
122
|
-
const
|
|
149
|
+
const companions = templateCompanions[template] ?? [];
|
|
150
|
+
const destinations = [
|
|
151
|
+
{ source, target, companion: false },
|
|
152
|
+
...companions.map((companion) => ({
|
|
153
|
+
source: join(sourcePath, companion.source),
|
|
154
|
+
target: companion.target,
|
|
155
|
+
companion: true,
|
|
156
|
+
})),
|
|
157
|
+
];
|
|
158
|
+
const conflicts = (await Promise.all(destinations.map(async (entry) => await exists(join(repositoryPath, entry.target)) && await exists(entry.source) && !(await filesMatch(entry.source, join(repositoryPath, entry.target)))
|
|
159
|
+
? entry.target
|
|
160
|
+
: undefined))).filter((file) => file !== undefined);
|
|
123
161
|
if (conflicts.length > 0 && !options.force)
|
|
124
162
|
return { installed: [], conflicts };
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
for (const entry of destinations) {
|
|
164
|
+
if (!(await exists(entry.source)))
|
|
165
|
+
continue; // companion files may not exist for every template version
|
|
166
|
+
const destination = join(repositoryPath, entry.target);
|
|
167
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
168
|
+
await copyFile(entry.source, destination);
|
|
169
|
+
}
|
|
127
170
|
if (options.inspection !== undefined && template === "opencode.ci.json") {
|
|
128
171
|
const baseContent = await readFile(source, "utf8");
|
|
129
172
|
const transformed = generateOpencodeConfig(baseContent, options.inspection);
|
|
130
|
-
await writeFile(
|
|
173
|
+
await writeFile(join(repositoryPath, target), transformed, "utf8");
|
|
131
174
|
}
|
|
132
175
|
try {
|
|
133
176
|
await runCompileIfAvailable(repositoryPath);
|
|
@@ -135,7 +178,7 @@ export async function installTemplate(repositoryPath, template, options = {}) {
|
|
|
135
178
|
catch {
|
|
136
179
|
// compile failure is non-fatal
|
|
137
180
|
}
|
|
138
|
-
return { installed:
|
|
181
|
+
return { installed: (await Promise.all(destinations.map(async (entry) => await exists(entry.source) ? entry.target : undefined))).filter((file) => file !== undefined), conflicts };
|
|
139
182
|
}
|
|
140
183
|
export async function installMandatoryFiles(repositoryPath, options = {}) {
|
|
141
184
|
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
@@ -29,7 +29,7 @@ describe("catalog installation", () => {
|
|
|
29
29
|
"workflows/shared/defaults.md": "defaults\n",
|
|
30
30
|
"workflows/work-router.yml": "name: Router\n",
|
|
31
31
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
32
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
32
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
33
33
|
});
|
|
34
34
|
const repositoryPath = await createDirectory({});
|
|
35
35
|
const result = await installCatalog(repositoryPath, { sourcePath, selectedRoutes: [] });
|
|
@@ -42,7 +42,6 @@ describe("catalog installation", () => {
|
|
|
42
42
|
expect(result.installed).not.toContain(".github/workflows/agent-refine.md");
|
|
43
43
|
expect(result.installed).not.toContain(".github/workflows/agent-implement.md");
|
|
44
44
|
expect(result.installed).not.toContain(".github/workflows/agent-audit.md");
|
|
45
|
-
expect(result.installed).not.toContain(".github/workflows/agent-propose.md");
|
|
46
45
|
});
|
|
47
46
|
it("installs package-owned loops files including mandatory opencode.ci.json and compile script", async () => {
|
|
48
47
|
const sourcePath = await createDirectory({
|
|
@@ -51,7 +50,7 @@ describe("catalog installation", () => {
|
|
|
51
50
|
"workflows/shared/defaults.md": "defaults\n",
|
|
52
51
|
"scripts/compile-agent-workflows.mjs": "console.log('compile');\n",
|
|
53
52
|
"scripts/compile.mjs": "console.log('old compile');\n",
|
|
54
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
53
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
55
54
|
"workflows/agent-check.lock.yml": "generated\n",
|
|
56
55
|
"actions/actions-lock.json": "generated\n",
|
|
57
56
|
});
|
|
@@ -75,7 +74,7 @@ describe("catalog installation", () => {
|
|
|
75
74
|
"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
75
|
"workflows/agent-check.md": `---\n${header}# Check\n`,
|
|
77
76
|
"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-
|
|
77
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
79
78
|
"templates/agentics/agentics-checks.yml": `${templateHeader}name: Agentics checks\n`,
|
|
80
79
|
});
|
|
81
80
|
const repositoryPath = await createDirectory({});
|
|
@@ -89,7 +88,7 @@ describe("catalog installation", () => {
|
|
|
89
88
|
"actions/check/action.yml": "name: Check\n",
|
|
90
89
|
"workflows/agent-check.md": "# Check\n",
|
|
91
90
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
92
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
91
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
93
92
|
});
|
|
94
93
|
const repositoryPath = await createDirectory({ ".github/workflows/agent-check.md": "# Check\n" });
|
|
95
94
|
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({ conflicts: [] });
|
|
@@ -99,7 +98,7 @@ describe("catalog installation", () => {
|
|
|
99
98
|
"actions/check/action.yml": "name: Check\n",
|
|
100
99
|
"workflows/agent-check.md": "# Check\n",
|
|
101
100
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
102
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
101
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
103
102
|
});
|
|
104
103
|
const repositoryPath = await createDirectory({
|
|
105
104
|
".github/workflows/shared/repo-config.md": "legacy consumer config\n",
|
|
@@ -120,7 +119,7 @@ describe("catalog installation", () => {
|
|
|
120
119
|
"actions/check/action.yml": "package action\n",
|
|
121
120
|
"workflows/agent-check.md": "package workflow\n",
|
|
122
121
|
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
123
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
122
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
124
123
|
});
|
|
125
124
|
const repositoryPath = await createDirectory({
|
|
126
125
|
".github/actions/check/action.yml": "consumer action\n",
|
|
@@ -152,7 +151,7 @@ describe("catalog installation", () => {
|
|
|
152
151
|
"actions/check/action.yml": "name: Check\n",
|
|
153
152
|
"workflows/agent-check.md": "# Check\n",
|
|
154
153
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
155
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
154
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
156
155
|
"templates/agentics/agentics-checks.yml": "name: Agentics checks\n",
|
|
157
156
|
"templates/ci/app-ci-node-monorepo.yml": "name: Node CI\n",
|
|
158
157
|
"templates/release/github-release.yml": "name: Publish GitHub release\n",
|
|
@@ -178,30 +177,30 @@ describe("catalog installation", () => {
|
|
|
178
177
|
"actions/check/action.yml": "name: Check\n",
|
|
179
178
|
"workflows/agent-check.md": "# Check\n",
|
|
180
179
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
181
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
180
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
182
181
|
});
|
|
183
182
|
const repositoryPath = await createDirectory({});
|
|
184
183
|
const result = await installCatalog(repositoryPath, { sourcePath });
|
|
185
184
|
expect(result.installed).toContain("opencode.ci.json");
|
|
186
185
|
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-
|
|
186
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-3\" }\n");
|
|
188
187
|
await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
|
|
189
188
|
});
|
|
190
189
|
it("installMandatoryFiles installs opencode.ci.json and compile script without catalog files", async () => {
|
|
191
190
|
const sourcePath = await createDirectory({
|
|
192
191
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
193
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
192
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
194
193
|
});
|
|
195
194
|
const repositoryPath = await createDirectory({});
|
|
196
195
|
const result = await installMandatoryFiles(repositoryPath, { sourcePath });
|
|
197
196
|
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-
|
|
197
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-3\" }\n");
|
|
199
198
|
await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
|
|
200
199
|
});
|
|
201
200
|
it("installMandatoryFiles reports conflicts on differing files", async () => {
|
|
202
201
|
const sourcePath = await createDirectory({
|
|
203
202
|
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
204
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
203
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
205
204
|
});
|
|
206
205
|
const repositoryPath = await createDirectory({
|
|
207
206
|
"opencode.ci.json": "{ \"model\": \"consumer\" }\n",
|
|
@@ -215,7 +214,7 @@ describe("catalog installation", () => {
|
|
|
215
214
|
it("installMandatoryFiles overwrites with force", async () => {
|
|
216
215
|
const sourcePath = await createDirectory({
|
|
217
216
|
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
218
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
217
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
219
218
|
});
|
|
220
219
|
const repositoryPath = await createDirectory({
|
|
221
220
|
"opencode.ci.json": "{ \"model\": \"consumer\" }\n",
|
|
@@ -224,14 +223,14 @@ describe("catalog installation", () => {
|
|
|
224
223
|
await expect(installMandatoryFiles(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
|
|
225
224
|
installed: ["opencode.ci.json", "scripts/compile-agent-workflows.mjs"],
|
|
226
225
|
});
|
|
227
|
-
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-
|
|
226
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-3\" }\n");
|
|
228
227
|
});
|
|
229
228
|
it("installMandatoryFiles does not install catalog workflow files", async () => {
|
|
230
229
|
const sourcePath = await createDirectory({
|
|
231
230
|
"actions/check/action.yml": "name: Check\n",
|
|
232
231
|
"workflows/agent-check.md": "# Check\n",
|
|
233
232
|
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
234
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
233
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
235
234
|
});
|
|
236
235
|
const repositoryPath = await createDirectory({});
|
|
237
236
|
await installMandatoryFiles(repositoryPath, { sourcePath });
|
|
@@ -242,7 +241,7 @@ describe("catalog installation", () => {
|
|
|
242
241
|
"actions/check/action.yml": "name: Check\n",
|
|
243
242
|
"workflows/agent-check.md": "# Check\n",
|
|
244
243
|
"scripts/compile-agent-workflows.mjs": "same compile script\n",
|
|
245
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
244
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
246
245
|
});
|
|
247
246
|
const repositoryPath = await createDirectory({});
|
|
248
247
|
const result = await installCatalog(repositoryPath, { sourcePath });
|
|
@@ -313,6 +312,44 @@ describe("catalog installation", () => {
|
|
|
313
312
|
await installCatalog(repositoryPath, { force: true, sourcePath });
|
|
314
313
|
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
314
|
});
|
|
315
|
+
// gh-aw cannot share runs-on through an import, so each worker names its own pool and a
|
|
316
|
+
// forced update must leave it alone. Rewriting it moves a repository's agents onto another
|
|
317
|
+
// pool, and one of ours has two machines in it.
|
|
318
|
+
it("keeps the consumer's runner pool through a forced update", async () => {
|
|
319
|
+
const sourcePath = await createDirectory({
|
|
320
|
+
"actions/check/action.yml": "name: Check\n",
|
|
321
|
+
"workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"package verify\"\n---\njobs:\n agent:\n runs-on: agents-arc\n deterministic:\n runs-on: ubuntu-latest\nruns-on: agents-arc\nruns-on-slim: agents-arc\n",
|
|
322
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
323
|
+
"templates/opencode/opencode.ci.json": "{}\n",
|
|
324
|
+
});
|
|
325
|
+
const repositoryPath = await createDirectory({
|
|
326
|
+
".github/workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"consumer verify\"\n---\njobs:\n agent:\n runs-on: OwnPool\n deterministic:\n runs-on: ubuntu-latest\nruns-on: OwnPool\nruns-on-slim: OwnPool\n",
|
|
327
|
+
});
|
|
328
|
+
await installCatalog(repositoryPath, { force: true, sourcePath });
|
|
329
|
+
const written = await readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8");
|
|
330
|
+
expect(written).toContain("runs-on: OwnPool");
|
|
331
|
+
expect(written).not.toContain("agents-arc");
|
|
332
|
+
// GitHub's own runner is not a per-repository choice and stays as the package has it.
|
|
333
|
+
expect(written).toContain("runs-on: ubuntu-latest");
|
|
334
|
+
// And the env value is still preserved, which this shares a code path with.
|
|
335
|
+
expect(written).toContain('VERIFY_COMMANDS: "consumer verify"');
|
|
336
|
+
});
|
|
337
|
+
// A consumer naming two pools has drifted rather than decided. Guessing which it meant
|
|
338
|
+
// would be worse than leaving the package's, so it is left.
|
|
339
|
+
it("leaves the package pool when the consumer names more than one", async () => {
|
|
340
|
+
const sourcePath = await createDirectory({
|
|
341
|
+
"actions/check/action.yml": "name: Check\n",
|
|
342
|
+
"workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"package verify\"\n---\njobs:\n agent:\n runs-on: agents-arc\n deterministic:\n runs-on: ubuntu-latest\nruns-on: agents-arc\nruns-on-slim: agents-arc\n",
|
|
343
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
344
|
+
"templates/opencode/opencode.ci.json": "{}\n",
|
|
345
|
+
});
|
|
346
|
+
const repositoryPath = await createDirectory({
|
|
347
|
+
".github/workflows/agent-check.md": "---\nenv:\n VERIFY_COMMANDS: \"consumer verify\"\n---\njobs:\n agent:\n runs-on: OwnPool\n other:\n runs-on: SomethingElse\n",
|
|
348
|
+
});
|
|
349
|
+
await installCatalog(repositoryPath, { force: true, sourcePath });
|
|
350
|
+
const written = await readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8");
|
|
351
|
+
expect(written).toContain("runs-on: agents-arc");
|
|
352
|
+
});
|
|
316
353
|
it("applies staged generated locks with managed sources", async () => {
|
|
317
354
|
const sourcePath = await createDirectory({
|
|
318
355
|
"actions/check/action.yml": "package action\n",
|
|
@@ -333,14 +370,14 @@ describe("catalog installation", () => {
|
|
|
333
370
|
});
|
|
334
371
|
it("installs the opencode.ci.json template to the repository root", async () => {
|
|
335
372
|
const sourcePath = await createDirectory({
|
|
336
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
373
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
337
374
|
});
|
|
338
375
|
const repositoryPath = await createDirectory({});
|
|
339
376
|
await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
|
|
340
377
|
installed: ["opencode.ci.json"],
|
|
341
378
|
conflicts: [],
|
|
342
379
|
});
|
|
343
|
-
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-
|
|
380
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-3\" }\n");
|
|
344
381
|
});
|
|
345
382
|
it("installs the .NET and Next.js CI template as app-ci.yml", async () => {
|
|
346
383
|
const sourcePath = await createDirectory({
|
|
@@ -355,7 +392,7 @@ describe("catalog installation", () => {
|
|
|
355
392
|
});
|
|
356
393
|
it("requires force to replace the opencode.ci.json template", async () => {
|
|
357
394
|
const sourcePath = await createDirectory({
|
|
358
|
-
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
395
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }\n",
|
|
359
396
|
});
|
|
360
397
|
const repositoryPath = await createDirectory({
|
|
361
398
|
"opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
|
|
@@ -367,7 +404,7 @@ describe("catalog installation", () => {
|
|
|
367
404
|
await expect(installTemplate(repositoryPath, "opencode.ci.json", { force: true, sourcePath })).resolves.toMatchObject({
|
|
368
405
|
installed: ["opencode.ci.json"],
|
|
369
406
|
});
|
|
370
|
-
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-
|
|
407
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-3\" }\n");
|
|
371
408
|
});
|
|
372
409
|
it("requires force to replace a selected template", async () => {
|
|
373
410
|
const sourcePath = await createDirectory({
|
|
@@ -429,7 +466,7 @@ describe("route lifecycle", () => {
|
|
|
429
466
|
});
|
|
430
467
|
it("ignores routes that are not installed", async () => {
|
|
431
468
|
const repositoryPath = await createDirectory({});
|
|
432
|
-
await expect(removeRouteFiles(repositoryPath, ["
|
|
469
|
+
await expect(removeRouteFiles(repositoryPath, ["audit"])).resolves.toEqual([]);
|
|
433
470
|
});
|
|
434
471
|
});
|
|
435
472
|
async function createDirectory(files) {
|
|
@@ -15,7 +15,7 @@ describe("catalog listing", () => {
|
|
|
15
15
|
expect(entries).toHaveLength(workflowRoutes.length + catalogTemplates.length);
|
|
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
|
-
expect(routeNames).toEqual(["refine", "implement", "
|
|
18
|
+
expect(routeNames).toEqual(["refine", "implement", "triage", "apply-review", "merge-gate", "audit"]);
|
|
19
19
|
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "feature-request", "github-release", "opencode.ci.json", "visual-evidence"]);
|
|
20
20
|
});
|
|
21
21
|
it("reports all entries as not installed in an empty repository", async () => {
|
|
@@ -56,7 +56,7 @@ describe("catalog listing", () => {
|
|
|
56
56
|
});
|
|
57
57
|
it("marks the opencode.ci.json template as installed when the file exists at repository root", async () => {
|
|
58
58
|
const repositoryPath = await createRepository({
|
|
59
|
-
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
59
|
+
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }",
|
|
60
60
|
});
|
|
61
61
|
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
62
62
|
const opencodeEntry = entries.find((entry) => entry.name === "opencode.ci.json");
|
|
@@ -68,7 +68,7 @@ describe("catalog listing", () => {
|
|
|
68
68
|
".github/workflows/agent-refine.md": "# Refine",
|
|
69
69
|
".github/workflows/agent-audit.md": "# Audit",
|
|
70
70
|
".github/workflows/app-ci-dotnet-next.yml": "name: CI",
|
|
71
|
-
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-
|
|
71
|
+
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-3\" }",
|
|
72
72
|
});
|
|
73
73
|
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
74
74
|
expect(entries.filter((entry) => entry.installed).map((entry) => entry.name))
|
|
@@ -106,7 +106,7 @@ describe("searchCatalog", () => {
|
|
|
106
106
|
});
|
|
107
107
|
it("returns both routes and templates when the query matches both kinds", () => {
|
|
108
108
|
const entries = [
|
|
109
|
-
{ kind: "route", name: "
|
|
109
|
+
{ kind: "route", name: "audit", description: "Executes a CI instruction.", file: "agent-audit.md", installed: false },
|
|
110
110
|
{ kind: "template", name: "app-ci-dotnet-next", description: "App CI pipeline.", file: "app-ci-dotnet-next.yml", installed: true },
|
|
111
111
|
];
|
|
112
112
|
const results = searchCatalog(entries, "CI");
|