@plainconceptsplatform/workflows 0.4.34 → 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 (73) 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 +1 -3
  5. package/loops/actions/classify-route/classify-route.sh +40 -35
  6. package/loops/actions/update-changelog/action.yml +113 -0
  7. package/loops/actions/validate-merge-gate-output/action.yml +40 -40
  8. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  9. package/loops/actions/validate-review-output/action.yml +35 -35
  10. package/loops/actions/validate-triage-output/action.yml +36 -36
  11. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +91 -36
  12. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  13. package/loops/templates/agentics/agentics-checks.yml +86 -86
  14. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  15. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  16. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  17. package/loops/templates/issues/bug_report.yml +109 -109
  18. package/loops/templates/issues/feature_request.yml +75 -75
  19. package/loops/templates/opencode/opencode.ci.json +49 -47
  20. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  21. package/loops/templates/release/github-release.yml +30 -30
  22. package/loops/workflows/agent-apply-review.md +465 -443
  23. package/loops/workflows/agent-audit.md +213 -197
  24. package/loops/workflows/agent-implement.md +538 -414
  25. package/loops/workflows/agent-merge-gate.md +730 -584
  26. package/loops/workflows/agent-refine.md +609 -418
  27. package/loops/workflows/agent-release.md +258 -0
  28. package/loops/workflows/agent-triage.md +447 -439
  29. package/loops/workflows/authorize-bot-work.yml +85 -82
  30. package/loops/workflows/shared/opencode-ci.md +206 -197
  31. package/loops/workflows/shared/platform-defaults.md +19 -16
  32. package/loops/workflows/work-router.yml +862 -632
  33. package/package.json +7 -8
  34. package/dist/action-validation.test.d.ts +0 -1
  35. package/dist/action-validation.test.js +0 -84
  36. package/dist/catalog-installation.d.ts +0 -30
  37. package/dist/catalog-installation.js +0 -379
  38. package/dist/catalog-installation.test.d.ts +0 -1
  39. package/dist/catalog-installation.test.js +0 -448
  40. package/dist/catalog-listing.d.ts +0 -13
  41. package/dist/catalog-listing.js +0 -70
  42. package/dist/catalog-listing.test.d.ts +0 -1
  43. package/dist/catalog-listing.test.js +0 -150
  44. package/dist/index.d.ts +0 -2
  45. package/dist/index.js +0 -218
  46. package/dist/index.test.d.ts +0 -1
  47. package/dist/index.test.js +0 -273
  48. package/dist/repository-inspection.d.ts +0 -23
  49. package/dist/repository-inspection.js +0 -113
  50. package/dist/repository-inspection.test.d.ts +0 -1
  51. package/dist/repository-inspection.test.js +0 -77
  52. package/dist/repository-state.d.ts +0 -18
  53. package/dist/repository-state.js +0 -77
  54. package/dist/repository-state.test.d.ts +0 -1
  55. package/dist/repository-state.test.js +0 -96
  56. package/dist/route-processing.d.ts +0 -6
  57. package/dist/route-processing.js +0 -150
  58. package/dist/route-processing.test.d.ts +0 -1
  59. package/dist/route-processing.test.js +0 -350
  60. package/dist/stack-defaults.d.ts +0 -11
  61. package/dist/stack-defaults.js +0 -104
  62. package/dist/stack-defaults.test.d.ts +0 -1
  63. package/dist/stack-defaults.test.js +0 -266
  64. package/dist/tui.d.ts +0 -25
  65. package/dist/tui.js +0 -287
  66. package/dist/tui.test.d.ts +0 -1
  67. package/dist/tui.test.js +0 -246
  68. package/dist/workflow-catalog.d.ts +0 -25
  69. package/dist/workflow-catalog.js +0 -59
  70. package/dist/workflow-catalog.test.d.ts +0 -1
  71. package/dist/workflow-catalog.test.js +0 -29
  72. package/loops/workflows/agent-direct.md +0 -375
  73. package/loops/workflows/agent-propose.md +0 -342
@@ -1,350 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { addRouteExclusion, excludedWorkerFiles, processRoutes, stripRouteFromClassifier, stripRouteFromRouter, } from "./route-processing.js";
3
- import { routeNames } from "./workflow-catalog.js";
4
- const ROUTER_YAML = `# header
5
- name: "All Work Router"
6
-
7
- on:
8
- schedule:
9
- - cron: "17 1 * * 1"
10
- - cron: "43 3 * * *"
11
- - cron: "0 6 * * *"
12
- - cron: "*/5 * * * *"
13
- - cron: "0 */2 * * *"
14
- - cron: "29 7 * * *"
15
-
16
- workflow_dispatch:
17
- inputs:
18
- operation:
19
- description: "Operation to run"
20
- required: true
21
- type: choice
22
- options:
23
- - refine
24
- - implement
25
- - direct
26
- - triage
27
- - apply-review
28
- - merge-gate
29
- - audit
30
- - propose
31
- - audit-close
32
- - cleanup-artifacts
33
- - reconcile-bot-pr-runs
34
- - stale-recovery
35
- - validate
36
-
37
- jobs:
38
- call-refine:
39
- needs: [classify, authorize]
40
- if: needs.classify.outputs.route == 'refine' && needs.authorize.outputs.trusted == 'true'
41
- uses: ./.github/workflows/agent-refine.lock.yml
42
- secrets:
43
- OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
44
-
45
- call-implement:
46
- needs: [classify, authorize]
47
- if: needs.classify.outputs.route == 'implement' && needs.authorize.outputs.trusted == 'true'
48
- uses: ./.github/workflows/agent-implement.lock.yml
49
- secrets:
50
- OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
51
-
52
- call-triage:
53
- needs: [classify, authorize]
54
- if: needs.classify.outputs.route == 'triage' && needs.authorize.outputs.is_outside_collaborator == 'true'
55
- uses: ./.github/workflows/agent-triage.lock.yml
56
- secrets:
57
- OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
58
-
59
- call-audit:
60
- needs: classify
61
- if: needs.classify.outputs.route == 'audit'
62
- uses: ./.github/workflows/agent-audit.lock.yml
63
- secrets:
64
- OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
65
-
66
- call-propose:
67
- needs: classify
68
- if: needs.classify.outputs.route == 'propose'
69
- uses: ./.github/workflows/agent-propose.lock.yml
70
- secrets:
71
- OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
72
-
73
- audit-close:
74
- needs: classify
75
- if: needs.classify.outputs.route == 'audit-close'
76
- runs-on: ubuntu-latest
77
- `;
78
- const CLASSIFIER_SH = `#!/usr/bin/env bash
79
- set -euo pipefail
80
-
81
- readonly AUDIT_CRON="17 1 * * 1"
82
- readonly AUDIT_CLOSE_CRON="43 3 * * *"
83
- readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
84
- readonly RECONCILE_BOT_PR_RUNS_CRON="*/5 * * * *"
85
- readonly STALE_RECOVERY_CRON="0 */2 * * *"
86
- readonly PROPOSE_CRON="29 7 * * *"
87
-
88
- classify_route() {
89
- local route="none" error=""
90
-
91
- case "\${EVENT:-}" in
92
- schedule)
93
- trigger_kind="scheduled"
94
- case "\${SCHEDULE:-}" in
95
- "\$AUDIT_CRON") route="audit" ;;
96
- "\$AUDIT_CLOSE_CRON") route="audit-close" ;;
97
- "\$CLEANUP_ARTIFACTS_CRON") route="cleanup-artifacts" ;;
98
- "\$RECONCILE_BOT_PR_RUNS_CRON") route="reconcile-bot-pr-runs" ;;
99
- "\$STALE_RECOVERY_CRON") route="stale-recovery" ;;
100
- "\$PROPOSE_CRON") route="propose" ;;
101
- *) error="no route for cron '\${SCHEDULE:-}'" ;;
102
- esac
103
- ;;
104
-
105
- workflow_dispatch)
106
- trigger_kind="manual"
107
- case "\${OPERATION:-}" in
108
- refine | implement | direct)
109
- route="\${OPERATION}"
110
- ;;
111
- apply-review)
112
- route="apply-review"
113
- ;;
114
- merge-gate)
115
- route="merge-gate"
116
- ;;
117
- audit | propose)
118
- route="\${OPERATION}"
119
- trigger_kind="\${INPUT_TRIGGER_KIND:-manual}"
120
- ;;
121
- audit-close | cleanup-artifacts | reconcile-bot-pr-runs | stale-recovery | validate)
122
- route="\${OPERATION}"
123
- ;;
124
- *)
125
- error="unknown operation '\${OPERATION:-}'"
126
- ;;
127
- esac
128
- ;;
129
- esac
130
-
131
- cat <<EOF
132
- route=\${route}
133
- error=\${error}
134
- EOF
135
- }
136
-
137
- if [ "\${BASH_SOURCE[0]}" = "\$0" ]; then
138
- classify_route
139
- fi
140
- `;
141
- const MATRIX_SH = `#!/usr/bin/env bash
142
- set -euo pipefail
143
-
144
- echo "── Router wiring ─────────────────────────────────────────────────────────"
145
- for route in refine implement direct triage apply-review merge-gate audit propose bot-approve \\
146
- audit-close cleanup-artifacts reconcile-bot-pr-runs stale-recovery validate; do
147
- if grep -q "route == '\${route}'" "\$ROUTER_YML"; then
148
- PASS=\$((PASS + 1))
149
- else
150
- FAIL=\$((FAIL + 1))
151
- echo "FAIL: work-router.yml has no job for route '\${route}'" >&2
152
- fi
153
- done
154
-
155
- while read -r operation; do
156
- if grep -q "route == '\${operation}'" "\$ROUTER_YML"; then
157
- PASS=\$((PASS + 1))
158
- else
159
- FAIL=\$((FAIL + 1))
160
- echo "FAIL: dispatch operation '\${operation}' has no job in work-router.yml" >&2
161
- fi
162
- done < <(sed -n '/^ operation:/,/^ issue-number:/p' "\$ROUTER_YML" | sed -n 's/^ - //p')
163
-
164
- echo
165
- if [ "\$FAIL" -eq 0 ]; then
166
- echo "Route matrix: \${PASS} passed"
167
- else
168
- echo "Route matrix: \${PASS} passed, \${FAIL} FAILED" >&2
169
- fi
170
-
171
- exit \$((FAIL > 0))
172
- `;
173
- describe("stripRouteFromRouter", () => {
174
- it("removes the propose cron entry", () => {
175
- const result = stripRouteFromRouter(ROUTER_YAML, "propose");
176
- expect(result).not.toContain('cron: "29 7 * * *"');
177
- expect(result).toContain('cron: "17 1 * * 1"');
178
- });
179
- it("removes the call-propose job block", () => {
180
- const result = stripRouteFromRouter(ROUTER_YAML, "propose");
181
- expect(result).not.toContain("call-propose");
182
- expect(result).toContain("call-refine");
183
- expect(result).toContain("call-audit");
184
- });
185
- it("removes propose from the dispatch options", () => {
186
- const result = stripRouteFromRouter(ROUTER_YAML, "propose");
187
- expect(result).not.toMatch(/^\s+- propose$/m);
188
- expect(result).toMatch(/^\s+- refine$/m);
189
- expect(result).toMatch(/^\s+- audit$/m);
190
- });
191
- it("removes the audit cron entry", () => {
192
- const result = stripRouteFromRouter(ROUTER_YAML, "audit");
193
- expect(result).not.toContain('cron: "17 1 * * 1"');
194
- expect(result).toContain('cron: "29 7 * * *"');
195
- });
196
- it("removes the call-audit job block", () => {
197
- const result = stripRouteFromRouter(ROUTER_YAML, "audit");
198
- expect(result).not.toContain("call-audit");
199
- expect(result).toContain("call-refine");
200
- });
201
- it("preserves the audit-close job when removing audit", () => {
202
- const result = stripRouteFromRouter(ROUTER_YAML, "audit");
203
- expect(result).toContain("audit-close");
204
- });
205
- it("does not modify the yaml when stripping a route that has no cron", () => {
206
- const yamlWithoutCron = ROUTER_YAML.replace(/ - cron: "17 1 \* \* 1"\n/, "");
207
- const result = stripRouteFromRouter(yamlWithoutCron, "audit");
208
- expect(result).not.toContain("call-audit");
209
- });
210
- });
211
- describe("stripRouteFromClassifier", () => {
212
- it("removes the PROPOSE_CRON constant", () => {
213
- const result = stripRouteFromClassifier(CLASSIFIER_SH, "propose");
214
- expect(result).not.toContain('readonly PROPOSE_CRON');
215
- expect(result).toContain('readonly AUDIT_CRON');
216
- });
217
- it("removes the propose schedule case", () => {
218
- const result = stripRouteFromClassifier(CLASSIFIER_SH, "propose");
219
- expect(result).not.toContain('"$PROPOSE_CRON") route="propose"');
220
- expect(result).toContain('"$AUDIT_CRON") route="audit"');
221
- });
222
- it("removes the AUDIT_CRON constant", () => {
223
- const result = stripRouteFromClassifier(CLASSIFIER_SH, "audit");
224
- expect(result).not.toContain('readonly AUDIT_CRON');
225
- expect(result).toContain('readonly PROPOSE_CRON');
226
- });
227
- it("removes the audit schedule case", () => {
228
- const result = stripRouteFromClassifier(CLASSIFIER_SH, "audit");
229
- expect(result).not.toContain('"$AUDIT_CRON") route="audit"');
230
- expect(result).toContain('"$PROPOSE_CRON") route="propose"');
231
- });
232
- it("removes propose from the dispatch case union", () => {
233
- const result = stripRouteFromClassifier(CLASSIFIER_SH, "propose");
234
- expect(result).not.toContain("audit | propose)");
235
- expect(result).toContain("audit)");
236
- });
237
- });
238
- describe("addRouteExclusion", () => {
239
- it("adds an exclusion assertion for the route", () => {
240
- const result = addRouteExclusion(MATRIX_SH, "propose");
241
- expect(result).toContain("excluded route 'propose'");
242
- expect(result).toContain("propose correctly excluded from work-router.yml");
243
- });
244
- it("does not add the exclusion twice", () => {
245
- const once = addRouteExclusion(MATRIX_SH, "propose");
246
- const twice = addRouteExclusion(once, "propose");
247
- const matchCount = (twice.match(/excluded route 'propose'/g) ?? []).length;
248
- expect(matchCount).toBe(1);
249
- });
250
- });
251
- describe("processRoutes", () => {
252
- it("returns the same map when all routes are selected", () => {
253
- const files = new Map([
254
- ["work-router.yml", ROUTER_YAML],
255
- ["classify-route.sh", CLASSIFIER_SH],
256
- ["verify-route-matrix.sh", MATRIX_SH],
257
- ]);
258
- const result = processRoutes(files, [...routeNames]);
259
- expect(result).toBe(files);
260
- });
261
- it("strips every worker route when no routes are selected", () => {
262
- const files = new Map([
263
- [".github/workflows/work-router.yml", ROUTER_YAML],
264
- [".github/actions/classify-route/classify-route.sh", CLASSIFIER_SH],
265
- [".github/actions/verify-route-matrix/verify-route-matrix.sh", MATRIX_SH],
266
- ]);
267
- const result = processRoutes(files, []);
268
- const router = result.get(".github/workflows/work-router.yml");
269
- expect(router).not.toContain("call-propose");
270
- expect(router).not.toContain("call-audit");
271
- expect(router).not.toContain("- propose");
272
- expect(router).not.toContain("- refine");
273
- const classifier = result.get(".github/actions/classify-route/classify-route.sh");
274
- expect(classifier).toBe(CLASSIFIER_SH);
275
- const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
276
- expect(matrix).toContain("Route matrix: selected routes valid");
277
- expect(matrix).toContain("for route in refine implement direct triage apply-review merge-gate audit propose");
278
- });
279
- it("strips propose from all three files when unselected", () => {
280
- const files = new Map([
281
- [".github/workflows/work-router.yml", ROUTER_YAML],
282
- [".github/actions/classify-route/classify-route.sh", CLASSIFIER_SH],
283
- [".github/actions/verify-route-matrix/verify-route-matrix.sh", MATRIX_SH],
284
- ]);
285
- const selectedRoutes = routeNames.filter((r) => r !== "propose");
286
- const result = processRoutes(files, selectedRoutes);
287
- const router = result.get(".github/workflows/work-router.yml");
288
- expect(router).not.toContain("call-propose");
289
- expect(router).not.toContain('cron: "29 7 * * *"');
290
- expect(router).not.toMatch(/^\s+- propose$/m);
291
- const classifier = result.get(".github/actions/classify-route/classify-route.sh");
292
- expect(classifier).toBe(CLASSIFIER_SH);
293
- const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
294
- expect(matrix).toContain("for route in refine implement direct triage apply-review merge-gate audit");
295
- expect(matrix).toContain("for route in propose");
296
- });
297
- it("strips audit from all three files when unselected", () => {
298
- const files = new Map([
299
- ["work-router.yml", ROUTER_YAML],
300
- ["classify-route.sh", CLASSIFIER_SH],
301
- ["verify-route-matrix.sh", MATRIX_SH],
302
- ]);
303
- const selectedRoutes = routeNames.filter((r) => r !== "audit");
304
- const result = processRoutes(files, selectedRoutes);
305
- const router = result.get("work-router.yml");
306
- expect(router).not.toContain("call-audit");
307
- expect(router).not.toContain('cron: "17 1 * * 1"');
308
- const classifier = result.get("classify-route.sh");
309
- expect(classifier).toBe(CLASSIFIER_SH);
310
- });
311
- it("processes multiple excluded routes at once", () => {
312
- const files = new Map([
313
- ["work-router.yml", ROUTER_YAML],
314
- ["classify-route.sh", CLASSIFIER_SH],
315
- ["verify-route-matrix.sh", MATRIX_SH],
316
- ]);
317
- const selectedRoutes = ["refine", "implement"];
318
- const result = processRoutes(files, selectedRoutes);
319
- const router = result.get("work-router.yml");
320
- expect(router).toContain("call-refine");
321
- expect(router).toContain("call-implement");
322
- expect(router).not.toContain("call-audit");
323
- expect(router).not.toContain("call-propose");
324
- });
325
- });
326
- describe("excludedWorkerFiles", () => {
327
- it("returns worker files for unselected routes", () => {
328
- const selectedRoutes = ["refine", "implement"];
329
- const excluded = excludedWorkerFiles(selectedRoutes);
330
- expect(excluded.has("agent-refine.md")).toBe(false);
331
- expect(excluded.has("agent-implement.md")).toBe(false);
332
- expect(excluded.has("agent-audit.md")).toBe(true);
333
- expect(excluded.has("agent-propose.md")).toBe(true);
334
- });
335
- it("returns an empty set when all routes are selected", () => {
336
- const excluded = excludedWorkerFiles([...routeNames]);
337
- expect(excluded.size).toBe(0);
338
- });
339
- it("returns all worker files when no routes are selected", () => {
340
- const excluded = excludedWorkerFiles([]);
341
- expect(excluded.size).toBe(8);
342
- expect(excluded.has("agent-refine.md")).toBe(true);
343
- expect(excluded.has("agent-implement.md")).toBe(true);
344
- expect(excluded.has("agent-direct.md")).toBe(true);
345
- expect(excluded.has("agent-apply-review.md")).toBe(true);
346
- expect(excluded.has("agent-merge-gate.md")).toBe(true);
347
- expect(excluded.has("agent-audit.md")).toBe(true);
348
- expect(excluded.has("agent-propose.md")).toBe(true);
349
- });
350
- });
@@ -1,11 +0,0 @@
1
- import type { RepositoryInspection } from "./repository-inspection.js";
2
- export interface StackDefaults {
3
- readonly verifyCommands: string;
4
- readonly repoRulesBase: string;
5
- readonly hasDotnet: boolean;
6
- readonly hasNodeOnly: boolean;
7
- }
8
- export declare function generateStackDefaults(inspection: RepositoryInspection): StackDefaults;
9
- export declare function injectStackEnv(content: string, defaults: StackDefaults): string;
10
- export declare function generateOpencodeCi(baseContent: string, inspection: RepositoryInspection): string;
11
- export declare function generateOpencodeConfig(baseContent: string, inspection: RepositoryInspection): string;
@@ -1,104 +0,0 @@
1
- export function generateStackDefaults(inspection) {
2
- const hasDotnet = inspection.stackHints.solutionFiles.length > 0;
3
- const hasNode = inspection.stackHints.pnpmLockfile;
4
- const hasNodeOnly = hasNode && !hasDotnet;
5
- let verifyCommands;
6
- let repoRulesBase;
7
- if (hasDotnet && hasNode) {
8
- verifyCommands = ".NET: dotnet restore && dotnet build -c Release && dotnet test | Web: pnpm lint && pnpm test && pnpm build";
9
- repoRulesBase = "Full-stack .NET + React/Next.js repository. Follow Clean Architecture layering: API → Application → Domain. Infrastructure implements Application ports. Do not reference EF Core or ASP.NET from Application. Frontend communicates exclusively via HTTP endpoints. Run both .NET and frontend verification.";
10
- }
11
- else if (hasDotnet) {
12
- verifyCommands = "dotnet restore && dotnet build -c Release --no-restore && dotnet test";
13
- repoRulesBase = ".NET repository using Clean Architecture. Follow layering: API → Application → Domain. Infrastructure implements Application ports. Do not reference EF Core or ASP.NET from Application.";
14
- }
15
- else {
16
- verifyCommands = "pnpm verify";
17
- repoRulesBase = "Node.js repository. Follow existing project conventions and import boundaries.";
18
- }
19
- return { verifyCommands, repoRulesBase, hasDotnet, hasNodeOnly };
20
- }
21
- export function injectStackEnv(content, defaults) {
22
- let result = content;
23
- if (result.includes("VERIFY_COMMANDS:")) {
24
- result = result.replace(/ VERIFY_COMMANDS: ".*"/, ` VERIFY_COMMANDS: "${defaults.verifyCommands}"`);
25
- }
26
- else if (/^env:\n/m.test(result)) {
27
- result = result.replace(/^env:\n/m, `env:\n VERIFY_COMMANDS: "${defaults.verifyCommands}"\n`);
28
- }
29
- return result;
30
- }
31
- export function generateOpencodeCi(baseContent, inspection) {
32
- let result = baseContent;
33
- if (inspection.stackHints.solutionFiles.length > 0) {
34
- const solutionPath = inspection.stackHints.solutionFiles[0].replaceAll("\\", "/");
35
- const nugetSteps = ` - name: Cache NuGet packages
36
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
37
- with:
38
- path: ~/.nuget/packages
39
- key: nuget-\${{ runner.os }}-\${{ hashFiles('**/*.slnx', '**/Directory.Packages.props') }}
40
- restore-keys: nuget-\${{ runner.os }}-
41
-
42
- - name: Restore .NET dependencies
43
- run: dotnet restore ${solutionPath}
44
- `;
45
- result = insertBeforeMarker(result, nugetSteps, " - name: Install workspace dependencies");
46
- }
47
- if (inspection.stackHints.openSpec) {
48
- const openspecStep = ` - name: Install OpenSpec CLI
49
- run: |
50
- set -euo pipefail
51
- npm install -g "@fission-ai/openspec@1.8.0"
52
- openspec --version
53
- `;
54
- result = insertBeforeMarker(result, openspecStep, " - name: Install workspace dependencies");
55
- }
56
- if (inspection.stackHints.packageJson && !result.includes("--legacy-peer-deps")) {
57
- result = result.replace(` if ! npm install --prefix .opencode; then\n echo "No plugin deps, skipping."\n exit 0\n fi`, ` if ! npm install --prefix .opencode; then\n echo "::warning::Strict npm install failed on a peer conflict. Retrying with --legacy-peer-deps; check .opencode/package.json."\n npm install --prefix .opencode --legacy-peer-deps\n fi`);
58
- }
59
- return result;
60
- }
61
- function insertBeforeMarker(content, steps, marker) {
62
- if (content.includes(marker)) {
63
- return content.replace(marker, steps + marker);
64
- }
65
- return content + "\n" + steps;
66
- }
67
- export function generateOpencodeConfig(baseContent, inspection) {
68
- try {
69
- const config = JSON.parse(baseContent);
70
- const hasDotnet = inspection.stackHints.solutionFiles.length > 0;
71
- if (!hasDotnet && config.lsp !== undefined) {
72
- delete config.lsp;
73
- }
74
- else if (hasDotnet && config.lsp === undefined) {
75
- config.lsp = {
76
- csharp: { disabled: true },
77
- fsharp: { disabled: true },
78
- razor: { disabled: true },
79
- };
80
- }
81
- const agent = config.agent;
82
- if (agent !== undefined) {
83
- const agentEntry = agent["ci-workflow-agent"];
84
- if (agentEntry !== undefined) {
85
- let prompt = agentEntry.prompt ?? "";
86
- if (hasDotnet) {
87
- if (!prompt.includes(".NET guardrails")) {
88
- prompt += "\n\n# .NET guardrails\nFollow Clean Architecture layering: API → Application → Domain. Infrastructure implements Application ports. Application must not reference EF Core or ASP.NET. Use Central Package Management (Directory.Packages.props). Build in Release mode for CI.";
89
- }
90
- }
91
- else {
92
- if (!prompt.includes("Node/React rules")) {
93
- prompt += "\n\n# Node/React rules\nFollow Feature-Sliced Design import boundaries. Use pnpm, never npm or yarn. All user-facing text must be i18n messages. TypeScript strict mode.";
94
- }
95
- }
96
- agentEntry.prompt = prompt;
97
- }
98
- }
99
- return JSON.stringify(config, null, 2) + "\n";
100
- }
101
- catch {
102
- return baseContent;
103
- }
104
- }
@@ -1 +0,0 @@
1
- export {};