@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.
Files changed (61) hide show
  1. package/README.md +88 -88
  2. package/dist/action-validation.test.js +5 -2
  3. package/dist/catalog-installation.js +50 -7
  4. package/dist/catalog-installation.test.js +59 -22
  5. package/dist/catalog-listing.test.js +4 -4
  6. package/dist/index.js +48 -48
  7. package/dist/index.test.js +9 -9
  8. package/dist/repository-inspection.test.js +2 -2
  9. package/dist/route-processing.js +24 -25
  10. package/dist/route-processing.test.js +166 -233
  11. package/dist/stack-defaults.js +14 -14
  12. package/dist/stack-defaults.test.js +79 -79
  13. package/dist/tui.test.js +10 -7
  14. package/dist/workflow-catalog.d.ts +1 -1
  15. package/dist/workflow-catalog.js +1 -5
  16. package/loops/actions/add-issue-labels/action.yml +2 -2
  17. package/loops/actions/agent-output.cjs +17 -17
  18. package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
  19. package/loops/actions/audit-close/action.yml +24 -0
  20. package/loops/actions/classify-route/action.yml +8 -3
  21. package/loops/actions/classify-route/classify-route.sh +58 -38
  22. package/loops/actions/cleanup-artifacts/action.yml +38 -12
  23. package/loops/actions/identify-gate-subject/action.yml +3 -1
  24. package/loops/actions/remove-issue-labels/action.yml +4 -2
  25. package/loops/actions/update-changelog/action.yml +113 -0
  26. package/loops/actions/validate-merge-gate-output/action.yml +40 -40
  27. package/loops/actions/validate-refine-output/validate-refine-output.sh +34 -5
  28. package/loops/actions/validate-review-output/action.yml +35 -35
  29. package/loops/actions/validate-triage-output/action.yml +36 -36
  30. package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
  31. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +375 -36
  32. package/loops/scripts/compile-agent-workflows.mjs +290 -6
  33. package/loops/scripts/merge-changelog.mjs +76 -0
  34. package/loops/templates/agentics/actionlint.yaml +13 -0
  35. package/loops/templates/agentics/agentics-checks.yml +125 -8
  36. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  37. package/loops/templates/ci/app-ci-dotnet-next.yml +330 -171
  38. package/loops/templates/ci/app-ci-node-monorepo.yml +260 -178
  39. package/loops/templates/issues/bug_report.yml +109 -109
  40. package/loops/templates/issues/feature_request.yml +75 -75
  41. package/loops/templates/opencode/opencode.ci.json +49 -47
  42. package/loops/templates/opencode/opencode.ci.json.md +49 -41
  43. package/loops/templates/release/github-release.yml +30 -30
  44. package/loops/workflows/agent-apply-review.md +469 -443
  45. package/loops/workflows/agent-audit.md +213 -197
  46. package/loops/workflows/agent-implement.md +640 -414
  47. package/loops/workflows/agent-merge-gate.md +844 -584
  48. package/loops/workflows/agent-refine.md +633 -418
  49. package/loops/workflows/agent-release.md +258 -0
  50. package/loops/workflows/agent-triage.md +447 -439
  51. package/loops/workflows/authorize-bot-work.yml +85 -82
  52. package/loops/workflows/shared/opencode-ci.md +206 -197
  53. package/loops/workflows/shared/platform-defaults.md +19 -16
  54. package/loops/workflows/work-router.yml +1038 -632
  55. package/package.json +7 -8
  56. package/dist/repository-state.d.ts +0 -18
  57. package/dist/repository-state.js +0 -77
  58. package/dist/repository-state.test.d.ts +0 -1
  59. package/dist/repository-state.test.js +0 -96
  60. package/loops/workflows/agent-direct.md +0 -375
  61. 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
- const actionsDir = join(catalogSourcePath(), "actions");
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 destination = join(repositoryPath, target);
122
- const conflicts = await exists(destination) && !(await filesMatch(source, destination)) ? [target] : [];
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
- await mkdir(dirname(destination), { recursive: true });
126
- await copyFile(source, destination);
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(destination, transformed, "utf8");
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: [target], conflicts };
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n");
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-2\" }\n",
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-2\" }\n");
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-2\" }\n",
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-2\" }\n",
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-2\" }\n");
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-2\" }\n",
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-2\" }\n",
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-2\" }\n",
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-2\" }\n");
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-2\" }\n",
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-2\" }\n");
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, ["propose"])).resolves.toEqual([]);
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", "direct", "triage", "apply-review", "merge-gate", "audit", "propose"]);
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-2\" }",
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-2\" }",
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: "direct", description: "Executes a CI instruction.", file: "agent-direct.md", installed: false },
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");