@intentius/chant-lexicon-gitlab 0.1.12 → 0.1.14

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 (82) hide show
  1. package/README.md +4 -0
  2. package/dist/integrity.json +3 -2
  3. package/dist/manifest.json +1 -1
  4. package/dist/skills/chant-gitlab-migrate.md +117 -0
  5. package/package.json +11 -4
  6. package/src/import/generator.ts +20 -2
  7. package/src/migrate/from-github/actions/index.ts +27 -0
  8. package/src/migrate/from-github/actions/registry.ts +112 -0
  9. package/src/migrate/from-github/actions/tier-1.test.ts +128 -0
  10. package/src/migrate/from-github/actions/tier-1.ts +325 -0
  11. package/src/migrate/from-github/actions/tier-2-3.test.ts +144 -0
  12. package/src/migrate/from-github/actions/tier-2.ts +296 -0
  13. package/src/migrate/from-github/actions/tier-3.ts +124 -0
  14. package/src/migrate/from-github/composites/patterns.ts +167 -0
  15. package/src/migrate/from-github/composites/rewriter.test.ts +98 -0
  16. package/src/migrate/from-github/composites/rewriter.ts +29 -0
  17. package/src/migrate/from-github/diagnostics.ts +45 -0
  18. package/src/migrate/from-github/emit-ts.test.ts +49 -0
  19. package/src/migrate/from-github/emit-yaml.ts +128 -0
  20. package/src/migrate/from-github/expressions.test.ts +124 -0
  21. package/src/migrate/from-github/expressions.ts +302 -0
  22. package/src/migrate/from-github/fixtures/README.md +27 -0
  23. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected-report.json +15 -0
  24. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/expected.gitlab-ci.yml +13 -0
  25. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-checkout/input.yml +7 -0
  26. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected-report.json +20 -0
  27. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/expected.gitlab-ci.yml +20 -0
  28. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-node/input.yml +12 -0
  29. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected-report.json +20 -0
  30. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/expected.gitlab-ci.yml +17 -0
  31. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/actions-setup-python/input.yml +12 -0
  32. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected-report.json +24 -0
  33. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/expected.gitlab-ci.yml +20 -0
  34. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/docker-build-push/input.yml +16 -0
  35. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected-report.json +24 -0
  36. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/expected.gitlab-ci.yml +27 -0
  37. package/src/migrate/from-github/fixtures/marketplace-actions/tier-1/upload-download-artifact/input.yml +20 -0
  38. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected-report.json +24 -0
  39. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/expected.gitlab-ci.yml +15 -0
  40. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/codecov-action/input.yml +13 -0
  41. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected-report.json +20 -0
  42. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/expected.gitlab-ci.yml +17 -0
  43. package/src/migrate/from-github/fixtures/marketplace-actions/tier-2/setup-bun/input.yml +11 -0
  44. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected-report.json +21 -0
  45. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/expected.gitlab-ci.yml +15 -0
  46. package/src/migrate/from-github/fixtures/marketplace-actions/tier-3/paths-filter/input.yml +11 -0
  47. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected-report.json +20 -0
  48. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/expected.gitlab-ci.yml +16 -0
  49. package/src/migrate/from-github/fixtures/syntax-mapping/01-triggers/input.yml +12 -0
  50. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected-report.json +13 -0
  51. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/expected.gitlab-ci.yml +31 -0
  52. package/src/migrate/from-github/fixtures/syntax-mapping/02-stages-needs/input.yml +16 -0
  53. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected-report.json +13 -0
  54. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/expected.gitlab-ci.yml +20 -0
  55. package/src/migrate/from-github/fixtures/syntax-mapping/03-matrix/input.yml +10 -0
  56. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected-report.json +13 -0
  57. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/expected.gitlab-ci.yml +18 -0
  58. package/src/migrate/from-github/fixtures/syntax-mapping/04-env-secrets/input.yml +11 -0
  59. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected-report.json +13 -0
  60. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/expected.gitlab-ci.yml +24 -0
  61. package/src/migrate/from-github/fixtures/syntax-mapping/05-conditional/input.yml +12 -0
  62. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected-report.json +13 -0
  63. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/expected.gitlab-ci.yml +18 -0
  64. package/src/migrate/from-github/fixtures/syntax-mapping/06-services/input.yml +13 -0
  65. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected-report.json +20 -0
  66. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/expected.gitlab-ci.yml +17 -0
  67. package/src/migrate/from-github/fixtures/syntax-mapping/07-job-control/input.yml +13 -0
  68. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected-report.json +13 -0
  69. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/expected.gitlab-ci.yml +14 -0
  70. package/src/migrate/from-github/fixtures/syntax-mapping/08-workflow-name/input.yml +7 -0
  71. package/src/migrate/from-github/fixtures.test.ts +92 -0
  72. package/src/migrate/from-github/index.ts +128 -0
  73. package/src/migrate/from-github/provenance.ts +68 -0
  74. package/src/migrate/from-github/rules.ts +82 -0
  75. package/src/migrate/from-github/stages.test.ts +99 -0
  76. package/src/migrate/from-github/stages.ts +177 -0
  77. package/src/migrate/from-github/transformer.test.ts +278 -0
  78. package/src/migrate/from-github/transformer.ts +719 -0
  79. package/src/migrate.mcp.test.ts +69 -0
  80. package/src/plugin.test.ts +7 -3
  81. package/src/plugin.ts +105 -1
  82. package/src/skills/chant-gitlab-migrate.md +117 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Smoke test for the gitlab MCP `migrate` tool. Exercises the tool's
3
+ * handler directly (no JSON-RPC harness needed — the tool contract is
4
+ * a plain async function from inputSchema params to a result object).
5
+ */
6
+
7
+ import { describe, test, expect } from "vitest";
8
+ import { gitlabPlugin } from "./plugin";
9
+
10
+ describe("gitlab MCP migrate tool", () => {
11
+ test("registered alongside the diff tool", () => {
12
+ const tools = gitlabPlugin.mcpTools?.() ?? [];
13
+ const names = tools.map((t) => t.name);
14
+ // The MCP server applies ${plugin.name}: namespacing at registration
15
+ // time. Diff is pre-prefixed by createDiffTool; migrate is plain.
16
+ expect(names).toContain("gitlab:diff");
17
+ expect(names).toContain("migrate");
18
+ });
19
+
20
+ test("handler translates a trivial workflow", async () => {
21
+ const tools = gitlabPlugin.mcpTools?.() ?? [];
22
+ const migrate = tools.find((t) => t.name === "migrate");
23
+ expect(migrate).toBeDefined();
24
+ const result = await migrate!.handler({
25
+ content: `on: push
26
+ jobs:
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - run: echo hello
31
+ `,
32
+ }) as { output: string; diagnostics: unknown[]; provenance: unknown[]; stages: string[] };
33
+
34
+ expect(result.output).toContain("stages:");
35
+ expect(result.output).toContain("build:");
36
+ expect(Array.isArray(result.diagnostics)).toBe(true);
37
+ expect(Array.isArray(result.provenance)).toBe(true);
38
+ expect(result.stages).toContain("build");
39
+ });
40
+
41
+ test("respects useComposites toggle", async () => {
42
+ const tools = gitlabPlugin.mcpTools?.() ?? [];
43
+ const migrate = tools.find((t) => t.name === "migrate")!;
44
+ const nodePipelineWorkflow = `on: push
45
+ jobs:
46
+ build:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - uses: actions/setup-node@v4
51
+ with: { node-version: '20' }
52
+ - run: npm ci
53
+ - run: npm run build
54
+ test:
55
+ runs-on: ubuntu-latest
56
+ needs: build
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-node@v4
60
+ with: { node-version: '20' }
61
+ - run: npm ci
62
+ - run: npm test
63
+ `;
64
+ const without = await migrate.handler({ content: nodePipelineWorkflow, emit: "ts" }) as { output: string };
65
+ const withC = await migrate.handler({ content: nodePipelineWorkflow, emit: "ts", useComposites: true }) as { output: string };
66
+ expect(without.output).not.toContain("NodePipeline(");
67
+ expect(withC.output).toContain("NodePipeline(");
68
+ });
69
+ });
@@ -214,9 +214,13 @@ describe("gitlabPlugin", () => {
214
214
 
215
215
  test("returns MCP tools", () => {
216
216
  const tools = gitlabPlugin.mcpTools!();
217
- expect(tools).toHaveLength(1);
218
- expect(tools[0].name).toBe("gitlab:diff");
219
- expect(typeof tools[0].handler).toBe("function");
217
+ expect(tools.length).toBeGreaterThanOrEqual(2);
218
+ const names = tools.map((t) => t.name);
219
+ expect(names).toContain("gitlab:diff");
220
+ expect(names).toContain("migrate");
221
+ for (const t of tools) {
222
+ expect(typeof t.handler).toBe("function");
223
+ }
220
224
  });
221
225
 
222
226
  test("returns MCP resources", () => {
package/src/plugin.ts CHANGED
@@ -252,7 +252,68 @@ export const test = new Job({
252
252
  },
253
253
 
254
254
  mcpTools() {
255
- return [createDiffTool(gitlabSerializer, "Compare current build output against previous output for GitLab CI", "gitlab")];
255
+ return [
256
+ createDiffTool(gitlabSerializer, "Compare current build output against previous output for GitLab CI", "gitlab"),
257
+ {
258
+ name: "migrate",
259
+ description: "Translate a GitHub Actions workflow YAML into a GitLab CI/CD pipeline. Returns the rendered output plus diagnostic + provenance arrays.",
260
+ inputSchema: {
261
+ type: "object" as const,
262
+ properties: {
263
+ content: { type: "string", description: "Raw .github/workflows/*.yml content" },
264
+ emit: { type: "string", enum: ["yaml", "ts"], description: "Output format (default: yaml)" },
265
+ useComposites: { type: "boolean", description: "Recognise composite patterns and emit NodePipeline/NodeCI calls" },
266
+ strict: { type: "boolean", description: "Escalate needs-review diagnostics to errors" },
267
+ },
268
+ required: ["content"],
269
+ },
270
+ async handler(params: Record<string, unknown>): Promise<unknown> {
271
+ const { transform } = await import("./migrate/from-github/index");
272
+ const result = await transform(params.content as string, {
273
+ emit: (params.emit as "yaml" | "ts" | undefined) ?? "yaml",
274
+ useComposites: !!params.useComposites,
275
+ strict: !!params.strict,
276
+ sourceFile: "<mcp-input>",
277
+ });
278
+ return {
279
+ output: result.output,
280
+ diagnostics: result.diagnostics,
281
+ provenance: result.provenance,
282
+ stages: result.stages,
283
+ };
284
+ },
285
+ },
286
+ ];
287
+ },
288
+
289
+ migrationSource(from: string) {
290
+ if (from !== "github") return undefined;
291
+ return {
292
+ detect(content: string): boolean {
293
+ // Avoid bringing the migrate code into the import graph until needed
294
+ if (!/^\s*jobs\s*:/m.test(content)) return false;
295
+ return /^\s*on\s*:/m.test(content) || /^\s*runs-on\s*:/m.test(content);
296
+ },
297
+ async transform(content: string, opts) {
298
+ const { transform } = await import("./migrate/from-github/index");
299
+ const result = await transform(content, {
300
+ emit: opts.emit,
301
+ useComposites: opts.useComposites,
302
+ sourceFile: opts.sourceFile,
303
+ strict: opts.strict,
304
+ });
305
+ // The composites rewriter (when enabled) replaces several Job
306
+ // resources with a single Composite resource — stages: in the
307
+ // top-level YAML output is now stale for that path. The yaml
308
+ // emitter reads metadata.stages directly; no special handling
309
+ // needed at the call site.
310
+ return {
311
+ output: result.output,
312
+ provenance: result.provenance as unknown as Array<Record<string, unknown>>,
313
+ diagnostics: result.diagnostics as unknown as Array<Record<string, unknown>>,
314
+ };
315
+ },
316
+ };
256
317
  },
257
318
 
258
319
  mcpResources() {
@@ -400,6 +461,49 @@ curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \\
400
461
  },
401
462
  ],
402
463
  },
464
+ {
465
+ file: "chant-gitlab-migrate.md",
466
+ name: "chant-gitlab-migrate",
467
+ description: "Translate GitHub Actions workflows into GitLab CI/CD pipelines via chant migrate",
468
+ triggers: [
469
+ { type: "file-pattern", value: "**/.github/workflows/*.yml" },
470
+ { type: "file-pattern", value: "**/.github/workflows/*.yaml" },
471
+ { type: "context", value: "migrate from github actions" },
472
+ { type: "context", value: "github actions to gitlab" },
473
+ { type: "context", value: "convert workflow" },
474
+ ],
475
+ preConditions: [
476
+ "chant CLI is installed (chant --version succeeds)",
477
+ "@intentius/chant-lexicon-gitlab is installed",
478
+ ],
479
+ postConditions: [
480
+ "Translated .gitlab-ci.yml or .ts source on disk",
481
+ "Migration report visible to the user (Markdown + SARIF if --report)",
482
+ ],
483
+ parameters: [],
484
+ examples: [
485
+ {
486
+ title: "Translate a single workflow file",
487
+ description: "Migrate .github/workflows/ci.yml into .gitlab-ci.yml with a SARIF report",
488
+ input: "Migrate this GitHub workflow to GitLab CI",
489
+ output: `npx chant migrate .github/workflows/ci.yml \\
490
+ --output .gitlab-ci.yml \\
491
+ --report migration.sarif`,
492
+ },
493
+ {
494
+ title: "Translate to chant TypeScript",
495
+ description: "Produce typed chant source instead of YAML so the user can maintain the pipeline in chant going forward",
496
+ input: "I want to maintain this in chant — produce TypeScript",
497
+ output: `npx chant migrate .github/workflows/ci.yml --emit ts --output src/pipeline.ts`,
498
+ },
499
+ {
500
+ title: "Recognise and emit composites",
501
+ description: "Collapse a 2-job NodePipeline-shaped workflow into a single NodePipeline() call",
502
+ input: "Use composites for the upgrade",
503
+ output: `npx chant migrate .github/workflows/ci.yml --emit ts --use-composites --output src/pipeline.ts`,
504
+ },
505
+ ],
506
+ },
403
507
  {
404
508
  file: "chant-gitlab-patterns.md",
405
509
  name: "chant-gitlab-patterns",
@@ -0,0 +1,117 @@
1
+ ---
2
+ skill: chant-gitlab-migrate
3
+ description: Translate GitHub Actions workflows into GitLab CI/CD pipelines via chant migrate
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Chant GitHub Actions → GitLab CI/CD Migration
8
+
9
+ ## When to invoke this skill
10
+
11
+ The user wants to translate a `.github/workflows/*.yml` workflow into a `.gitlab-ci.yml` pipeline. Trigger phrases include:
12
+
13
+ - "migrate this GitHub workflow to GitLab"
14
+ - "convert .github/workflows/ci.yml to GitLab CI"
15
+ - pasting a GitHub workflow and asking "how would this look in GitLab"
16
+ - evaluating GitLab CI/CD from a GitHub Actions background
17
+ - "what's the GitLab equivalent of <action>"
18
+
19
+ This skill is the operational glue around `chant migrate`. The translation logic lives in `@intentius/chant-lexicon-gitlab`; this skill knows how to invoke it, surface the report, and suggest GitLab-only upgrade moments.
20
+
21
+ ## Distinction from the upstream GitLab skill
22
+
23
+ The upstream `gitlab-org/ci-cd/github-actions-to-gitlab-ci` Agent Skill (MIT) translates workflows by direct LLM prompting — stateless, freehand each run. `chant migrate` ports the same translation rules into a typed compiler so the output is reproducible, testable, and re-runnable. The skills are complementary:
24
+
25
+ - Use the upstream skill for one-shot read-and-respond ("here's my YAML, paste the answer").
26
+ - Use `chant migrate` when the translation has to survive evolution of the source workflow.
27
+
28
+ ## Step 1: Detect
29
+
30
+ Confirm the input is a GitHub Actions workflow. Heuristic: top-level `jobs:` plus either `on:` or per-job `runs-on:`. If you're given a path, read the file. If pasted inline, work from the paste.
31
+
32
+ ## Step 2: Dry-run migrate
33
+
34
+ Run with no `--strict`, no `--validate` first so the user sees the full translation including any NeedsReview items:
35
+
36
+ ```bash
37
+ npx chant migrate path/to/workflow.yml --output /tmp/proposed.gitlab-ci.yml --report /tmp/migration.sarif
38
+ ```
39
+
40
+ Or for inline content, pipe via a temp file:
41
+
42
+ ```bash
43
+ TMPF=$(mktemp --suffix=.yml) && cat > "$TMPF" <<'EOF'
44
+ <paste workflow here>
45
+ EOF
46
+ npx chant migrate "$TMPF" --output /tmp/proposed.gitlab-ci.yml --report /tmp/migration.sarif
47
+ ```
48
+
49
+ The Markdown summary always prints to stderr. The SARIF v2.1.0 report goes to `--report`.
50
+
51
+ ## Step 3: Review NeedsReview items
52
+
53
+ The transformer surfaces categorised provenance. The categories are:
54
+
55
+ | Category | Severity (default) | Meaning |
56
+ |---|---|---|
57
+ | literal | (none, no diagnostic) | Direct key rename (env → variables) |
58
+ | rewrite | (none, no diagnostic) | Expression substitution (github.ref → $CI_COMMIT_REF_NAME) |
59
+ | synthesis | (none, no diagnostic) | Emitted construct with no GH original (inferred stage) |
60
+ | action-map tier 3 | warning | Marketplace action mapping that requires manual review |
61
+ | skipped | info | Intentionally dropped (actions/checkout) |
62
+ | needs-review | warning | No clean translation — user must decide |
63
+
64
+ Common needs-review rules and the right response:
65
+
66
+ - **MIG-PERMISSIONS-001**: GitHub `permissions:` has no per-job equivalent. Configure CI/CD token access at project level (Settings > CI/CD > Token Access).
67
+ - **MIG-ON-SCHEDULE**: GitLab cron schedules live in the UI under CI/CD > Schedules, not in YAML. The workflow rule (`$CI_PIPELINE_SOURCE == "schedule"`) is emitted; the schedule itself needs UI setup.
68
+ - **MIG-ON-DISPATCH**: `workflow_dispatch` inputs require `spec:inputs` (GitLab 17+) with defaults on every input (auto-triggered pipelines can't prompt).
69
+ - **MIG-NEEDS-OUTPUTS-001**: GitHub job outputs require the `artifacts:reports:dotenv` pattern in GitLab. Manual rewire needed.
70
+ - **MIG-ACTION-UNKNOWN**: A marketplace action has no registered mapping. Either replace with an inline script, or add an `--action-mapping <file>` extension (future flag).
71
+
72
+ ## Step 4: Validate (optional)
73
+
74
+ If the user has `glci` or `glab` installed locally, run validation:
75
+
76
+ ```bash
77
+ npx chant migrate path/to/workflow.yml --output .gitlab-ci.yml --validate
78
+ ```
79
+
80
+ The CLI picks `glci` (offline, no auth) first; `glab ci lint` second. If neither is on PATH, `--validate` warns and skips. Pair with `--strict` to make validation failures hard.
81
+
82
+ ## Step 5: Decide the emit mode
83
+
84
+ Two modes serve different intents:
85
+
86
+ - `--emit yaml` (default): produces `.gitlab-ci.yml` directly. Right when the user wants the YAML and is done with chant going forward.
87
+ - `--emit ts`: produces chant TypeScript source the user owns. Right when the user wants to maintain the pipeline in chant — they can edit the typed source and rebuild via `chant build` to refresh the YAML.
88
+
89
+ When in doubt, do `--emit yaml` first to validate the translation, then offer `--emit ts` as the long-term ownership option.
90
+
91
+ ## Step 6: Suggest the upgrade moments
92
+
93
+ After surfacing the literal translation, opportunistically suggest GitLab-native features that improve on the GitHub original. Only suggest when they actually apply:
94
+
95
+ - `--use-composites` recognises Node-shaped pipelines and emits `NodePipeline({...})` or `NodeCI({...})` instead of raw `Job` constructors. The output is 5-10x shorter and easier to maintain.
96
+ - DAG `needs:` removes stage barriers — jobs run as soon as deps complete (already handled by the transformer for explicit `needs:`).
97
+ - `rules:changes:` enables monorepo path-based job filtering — no GitHub equivalent.
98
+ - `resource_group:` serialises deploys to the same environment (covered when GH `concurrency.group:` is translated).
99
+ - `include:` shares config across projects and workflows.
100
+ - GitLab CI templates (`Auto-DevOps`, `Security/SAST`, `Terraform/Base`) — no GitHub Actions equivalent.
101
+
102
+ ## Self-check rubric
103
+
104
+ Before reporting "done" to the user, verify:
105
+
106
+ - [ ] Was `actions/checkout` removed (GitLab clones automatically)?
107
+ - [ ] Was `runs-on:` translated to `image:` (Linux runners) or `tags:` (self-hosted)?
108
+ - [ ] Did every `uses:` step either map cleanly or get flagged with NeedsReview?
109
+ - [ ] Did `if:` conditions translate to `rules:if:` (NOT a string substitution; semantics differ)?
110
+ - [ ] Were `permissions:` items documented as manual project-level setup?
111
+ - [ ] Was the migration report (Markdown + SARIF) surfaced to the user?
112
+ - [ ] Were `NeedsReview` items called out explicitly?
113
+ - [ ] Did I suggest `--use-composites` if Node patterns were detected?
114
+
115
+ ## Inspired by
116
+
117
+ `gitlab-org/ci-cd/github-actions-to-gitlab-ci` — MIT. The trigger-phrase patterns, the four-category report shape, and the "suggest GitLab improvements" step are direct ports. The difference: this skill *calls* a compiler instead of *being* one.