From 77b7159f0a879a0db7957fae4743f9970d327b2c Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Tue, 10 Nov 2026 08:53:20 +0100 Subject: [PATCH] S19: adopt shared mini-pc runner and landscape workflows --- .gitea/workflows/build.yaml | 2 +- .gitea/workflows/deploy.yaml | 2 +- .gitea/workflows/rollback.yaml | 2 +- AGENTS.md | 23 ++-- README.md | 2 +- ci/runner/config.yaml | 2 +- ci/runner/runner.service | 10 +- docs/S19-followup-validation.md | 12 ++ docs/backend-review.md | 2 +- docs/gitea-ci.md | 70 +++++------ engineering.lock.json | 4 +- engineering.properties | 4 +- generated/CODING_STANDARDS.md | 2 +- generated/SOURCE.json | 4 +- generated/ci/Ci.java | 15 ++- generated/ci/CiTests.java | 53 ++++++++- generated/ci/Pipeline.java | 6 +- generated/ci/Release.java | 4 +- generated/ci/ReleaseSetup.java | 4 +- generated/ci/ReleaseTests.java | 4 +- generated/ci/Setup.java | 110 ++++++++++++++---- generated/ci/runner/WaitForDocker.java | 13 ++- generated/docs/backend-review.md | 2 +- generated/skills/README.md | 23 ---- .../skills/adr-brainstorm/ATTRIBUTION.md | 24 ---- .../skills/adr-brainstorm/LICENSE.mattpocock | 21 ---- .../skills/adr-brainstorm/LICENSE.wanjakrah | 21 ---- generated/skills/adr-brainstorm/SKILL.md | 103 ---------------- .../skills/adr-brainstorm/adr-template.md | 20 ---- .../skills/adr-brainstorm/agents/openai.yaml | 3 - .../skills/code-review/LICENSE.mattpocock | 21 ---- generated/skills/code-review/SKILL.md | 89 -------------- .../skills/code-review/agents/openai.yaml | 3 - generated/skills/grill-me/LICENSE.mattpocock | 21 ---- generated/skills/grill-me/SKILL.md | 7 -- generated/skills/grill-me/agents/openai.yaml | 5 - generated/skills/grilling/LICENSE.mattpocock | 21 ---- generated/skills/grilling/SKILL.md | 30 ----- generated/skills/grilling/agents/openai.yaml | 3 - generated/skills/upstream.json | 14 --- generated/tooling/coverage/Coverage.java | 11 +- generated/tooling/format/Format.java | 13 ++- .../standards/src/main/java/Standards.java | 8 +- 43 files changed, 265 insertions(+), 548 deletions(-) create mode 100644 docs/S19-followup-validation.md delete mode 100644 generated/skills/README.md delete mode 100644 generated/skills/adr-brainstorm/ATTRIBUTION.md delete mode 100644 generated/skills/adr-brainstorm/LICENSE.mattpocock delete mode 100644 generated/skills/adr-brainstorm/LICENSE.wanjakrah delete mode 100644 generated/skills/adr-brainstorm/SKILL.md delete mode 100644 generated/skills/adr-brainstorm/adr-template.md delete mode 100644 generated/skills/adr-brainstorm/agents/openai.yaml delete mode 100644 generated/skills/code-review/LICENSE.mattpocock delete mode 100644 generated/skills/code-review/SKILL.md delete mode 100644 generated/skills/code-review/agents/openai.yaml delete mode 100644 generated/skills/grill-me/LICENSE.mattpocock delete mode 100644 generated/skills/grill-me/SKILL.md delete mode 100644 generated/skills/grill-me/agents/openai.yaml delete mode 100644 generated/skills/grilling/LICENSE.mattpocock delete mode 100644 generated/skills/grilling/SKILL.md delete mode 100644 generated/skills/grilling/agents/openai.yaml delete mode 100644 generated/skills/upstream.json diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 8da7663..5beedd2 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -5,7 +5,7 @@ on: jobs: verify: - runs-on: agiledancers-mini-pc + runs-on: mini-pc env: CI: 'true' TARGET_PLATFORM: ${{ vars.TARGET_PLATFORM }} diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index ad23a78..4a34bf3 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -28,7 +28,7 @@ on: jobs: deploy: if: gitea.ref == 'refs/heads/main' - runs-on: agiledancers-mini-pc + runs-on: mini-pc env: CI: 'true' GITEA_SERVER_URL: ${{ gitea.server_url }} diff --git a/.gitea/workflows/rollback.yaml b/.gitea/workflows/rollback.yaml index bd91070..e9cd1ed 100644 --- a/.gitea/workflows/rollback.yaml +++ b/.gitea/workflows/rollback.yaml @@ -5,7 +5,7 @@ on: jobs: rollback: if: gitea.ref == 'refs/heads/main' - runs-on: agiledancers-mini-pc + runs-on: mini-pc env: CI: 'true' GITEA_SERVER_URL: ${{ gitea.server_url }} diff --git a/AGENTS.md b/AGENTS.md index 355bfd9..27d3408 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,22 +1,17 @@ -# Project skills and specifications +# Shared skills and specifications -- Project skills are stored in `generated/skills/`; specifications belong in [landscape](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/specs). -- For code reviews, read and follow `generated/skills/code-review/SKILL.md`. If no - dedicated Skill tool is available, load the file directly. -- Use `grill-me` only when the user explicitly requests it, for example - `Use grill-me to review this plan` or `Gebruik grill-me voor dit ontwerp`. -- Before using it, read `generated/skills/grill-me/SKILL.md` and its dependency - `generated/skills/grilling/SKILL.md`. If no dedicated Skill tool is available, load these - files directly and follow their instructions within the available tools. -- Installing or mentioning a skill does not start an interview. Do not apply - `grilling` automatically to ordinary development requests. -- Keep the upstream skill files unchanged. Source revision and license are - recorded in `generated/skills/upstream.json` and `generated/skills/LICENSE.mattpocock`. +- Shared agent workflows live in [landscape skills](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/skills/README.md). + Read the requested SKILL.md there before applying it. A sibling checkout is optional. +- For code reviews use landscape's skills/code-review/SKILL.md with docs/backend-review.md. +- Use grill-me only when explicitly requested, with its skills/grilling/SKILL.md dependency. + Installing or mentioning a skill starts no interview. +- Keep upstream skills unchanged; licenses and revision records live in landscape/skills/. +- Specifications and ADRs belong to landscape. Engineering owns pinned build resources. # Backend changes and reviews - Before changing backend Java, read `CODING_STANDARDS.md`. -- For reviews, use `generated/skills/code-review/SKILL.md` together with +- For reviews, use landscape's `skills/code-review/SKILL.md` together with `docs/backend-review.md`. Keep upstream skills unchanged. - Obtain maintainer agreement before adding any standards waiver; record its narrow scope and reason alongside the affected code or configuration. diff --git a/README.md b/README.md index 3bb9557..f2a0ae1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Landscape and shared engineering Architecture decisions, specifications and project onboarding are maintained in -[landscape](http://192.168.2.28:3000/wanjakrah/landscape). Reusable tooling, skills, +[landscape](http://192.168.2.28:3000/wanjakrah/landscape). Shared agent skills are maintained in landscape/skills/. Reusable tooling, standards and the project generator are maintained in [engineering](http://192.168.2.28:3000/wanjakrah/engineering). diff --git a/ci/runner/config.yaml b/ci/runner/config.yaml index 8654efa..955786c 100644 --- a/ci/runner/config.yaml +++ b/ci/runner/config.yaml @@ -6,6 +6,6 @@ runner: timeout: 2h shutdown_timeout: 2m labels: - - agiledancers-mini-pc:host + - mini-pc:host cache: enabled: false diff --git a/ci/runner/runner.service b/ci/runner/runner.service index 3007e90..a0c6841 100644 --- a/ci/runner/runner.service +++ b/ci/runner/runner.service @@ -1,15 +1,15 @@ [Unit] -Description=Agiledancers Gitea Actions runner +Description=mini-pc Gitea Actions runner After=network-online.target Wants=network-online.target [Service] Type=simple -WorkingDirectory=%h/.local/share/agiledancers-runner -EnvironmentFile=%h/.config/agiledancers-runner/environment -ExecStartPre=/usr/bin/env java %h/.local/share/agiledancers-runner/WaitForDocker.java 120 +WorkingDirectory=%h/.local/share/mini-pc-runner +EnvironmentFile=%h/.config/mini-pc-runner/environment +ExecStartPre=/usr/bin/env java %h/.local/share/mini-pc-runner/WaitForDocker.java 120 TimeoutStartSec=150 -ExecStart=%h/.local/bin/act_runner daemon --config %h/.local/share/agiledancers-runner/config.yaml +ExecStart=%h/.local/bin/act_runner daemon --config %h/.local/share/mini-pc-runner/config.yaml Restart=on-failure RestartSec=10 KillSignal=SIGTERM diff --git a/docs/S19-followup-validation.md b/docs/S19-followup-validation.md new file mode 100644 index 0000000..b2185f2 --- /dev/null +++ b/docs/S19-followup-validation.md @@ -0,0 +1,12 @@ +# S19 review follow-up — 2026-11-10 + +Engineering v2.0.0 at `46df72f008113d0ed683a6c7344b1d7309564d84` is pinned and committed. +Regeneration and bootstrap --check passed against the published tag and full commit. +Shared engineering tests passed: 8 bootstrap/generator, 21 CI and 34 release tests; +formatter integration passed. Java launcher documentation, varargs and console API updated. + +Workflows use the shared user-scoped mini-pc runner for wanjakrah. It serves current +and future repositories without per-project registration. Skills are maintained in +landscape/skills/ and project agent instructions reference those workflows. +Project deployment credentials and runtime configuration remain application-owned. +No production deployment was performed. diff --git a/docs/backend-review.md b/docs/backend-review.md index c6fb0b0..6dc4e86 100644 --- a/docs/backend-review.md +++ b/docs/backend-review.md @@ -1,7 +1,7 @@ # Agile Dancers backend review companion Use [the shared review guidance](../generated/docs/backend-review.md), -[code-review skill](../generated/skills/code-review/SKILL.md), and +[code-review skill](http://192.168.2.28:3000/wanjakrah/landscape/src/branch/main/skills/code-review/SKILL.md), and [project standards](../CODING_STANDARDS.md). The backend module is agiledancers-be. The approved Group constructor waiver diff --git a/docs/gitea-ci.md b/docs/gitea-ci.md index 65199df..bba3677 100644 --- a/docs/gitea-ci.md +++ b/docs/gitea-ci.md @@ -36,7 +36,7 @@ Gitea 1.25.4 en de registry staan op de bestaande Synology, Linux x86_64 mini-pc. Er is geen extra hardware nodig. De mini-pc moet aanstaan en wakker blijven; Docker en de runner moeten draaien. Anders blijven jobs wachten. Een host-runner voert repositorycode uit als de ontwikkelgebruiker: gebruik deze -runner alleen voor deze vertrouwde repository. +runner alleen voor de vertrouwde repositories van deze eigenaar. De runner heeft Git, Bash, JDK 25, Maven 3.9+, Node 22.22.3+/24.15.0+/26+, npm en een werkende Docker-daemon @@ -87,7 +87,7 @@ Docker-clients die images van deze interne HTTP-registry moeten ophalen. Gebruik de Docker Desktop-context of zet `DOCKER_HOST` expliciet. De installer neemt de geselecteerde socket over in de gebruikersservice. Als de socket later -wijzigt, pas dan `~/.config/agiledancers-runner/environment` aan en herstart de +wijzigt, pas dan `~/.config/mini-pc-runner/environment` aan en herstart de runner. Bij Docker Desktop blijft de Desktop-VM eveneens nodig voor builds. Runner en containers moeten de Synology en internetdownloads kunnen bereiken. @@ -109,12 +109,13 @@ Stel bij repository **Settings → Actions** in: | Secret | `REGISTRY_TOKEN` | PAT met `write:package` en `read:repository` voor imagepublicatie en controle van de huidige hoofdbranch | Het gewone jobtoken van Gitea 1.25 biedt geen packagepublicatie; gebruik een PAT. -Gebruik een repository-runnerregistratietoken, geen instancebrede registratie. +Gebruik de gedeelde user-scoped runner `mini-pc` voor `wanjakrah`; een nieuwe +repository heeft geen eigen runnerregistratie nodig. Sla tokenwaarden buiten Git op en plaats ze niet in opdrachten, screenshots of logs. Voor reproduceerbare configuratie bestaat `java generated/ci/Ci.java configure-gitea`. Maak twee bestanden buiten de repository met mode `600`: één voor een PAT met voldoende -repositorybeheerrechten (repository Actions, secrets, variabelen en runnerregistratie), +repositorybeheerrechten (repository Actions, secrets en variabelen), en één voor de publicatie-PAT. De beheer-PAT hoeft niet als workflowsecret te worden bewaard. Vul die bestanden via een lokale editor of verborgen terminalinvoer. Voer daarna uit met de werkelijke platformwaarde: @@ -127,11 +128,9 @@ export TARGET_PLATFORM=linux/amd64 # alleen als dit op de Synology is vastgeste java generated/ci/Ci.java configure-gitea ``` -Het script stelt de repository-instellingen in via de API en schrijft het -runnerregistratietoken naar -`~/.config/agiledancers-runner/registration-token` met mode `600`. Het toont geen -tokenwaarden. Voor handmatige inrichting kun je dat registratietoken uit de -repository-instellingen halen en in hetzelfde bestand plaatsen. +Het script stelt alleen de applicatie-instellingen, registrysecrets en platformwaarde +in. De gedeelde runnerregistratie wordt eenmalig beheerd vanuit engineering. +Applicatiespecifieke deploymentcredentials blijven onder ~/.config/agiledancers-runner/. De bevestigde Gitea 1.25.4-configuratie heeft geen `[actions]`-sectie: de defaults zijn negentig dagen voor artifacts en 365 dagen voor logs. Een workflow vraagt veertien dagen @@ -139,43 +138,30 @@ via `retention-days: 14`. Een kortere serverlimiet of het vroegtijdig verwijdere van workflowruns mag die periode niet inkorten. Artifactretentie geldt voor testdiagnostiek; gepubliceerde containerimages worden niet automatisch verwijderd. -## Runner installeren en starten +## Gedeelde runner gebruiken + +De fysieke mini-pc draait `mini-pc-runner.service` met label `mini-pc:host`, +geregistreerd op gebruikersniveau voor `wanjakrah`. Daardoor kunnen bestaande en +nieuwe repositories van deze eigenaar dezelfde runner gebruiken. De gedeelde +capaciteit is één job tegelijk. De workflowbestanden gebruiken `runs-on: mini-pc`. + +Voor eenmalige registratie, installatie en migratie volg je de +[engineering-instructies](http://192.168.2.28:3000/wanjakrah/engineering/src/tag/v2.0.0/README.md). +Controleer dit project met `java generated/ci/Ci.java check-runner`; gebruik daarbij +het in engineering beschreven owner-only tokenbestand voor de gedeelde setup. ```sh -export JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 -java generated/ci/Ci.java install-runner --prepare-only -# Na Dockerconfiguratie en het opslaan van het registratietoken: -java generated/ci/Ci.java install-runner -systemctl --user status agiledancers-runner -journalctl --user -u agiledancers-runner -f +systemctl --user status mini-pc-runner +journalctl --user -u mini-pc-runner -f +systemctl --user restart mini-pc-runner ``` -De installer controleert tools, installeert checksum-gecontroleerde runner -`v0.2.13`, neemt label `agiledancers-mini-pc:host` over en registreert de runner. -Configuratie en `.runner`-credentials staan in -`~/.local/share/agiledancers-runner`, de service in -`~/.config/systemd/user/agiledancers-runner.service` en de expliciete Java-, PATH- -en Docker-socketinstellingen in `~/.config/agiledancers-runner/environment`. -Een bestaande registratie wordt hergebruikt. Er worden geen secrets in de -repository geschreven. Bewaar het tokenbestand alleen zolang registratie/herstel -dat nodig heeft en behandel `.runner` als credential. - -Op de mini-pc starten Docker Desktop en de runner automatisch na grafisch -aanmelden. Bij een Docker Desktop-socket installeert de installer hiervoor -`~/.config/systemd/user/agiledancers-runner.service.d/10-agiledancers-docker-desktop.conf` -en schakelt beide services in voor `graphical-session.target`. De runner wacht -via `~/.local/share/agiledancers-runner/WaitForDocker.java` maximaal 120 seconden -tot de ingestelde Docker-engine bereikbaar is. Bij een mislukte start probeert -systemd het na tien seconden opnieuw. De runner volgt stoppen en herstarten van -Docker Desktop en stopt bij afmelden. - -User lingering staat op deze pc uit: vóór aanmelden starten deze services niet. -S4 belooft geen beschikbaarheid wanneer de mini-pc uitstaat. Herstarten/stoppen: - -```sh -systemctl --user restart agiledancers-runner -systemctl --user stop agiledancers-runner -``` +Runnerstate staat onder ~/.local/share/mini-pc-runner/ en serviceconfiguratie onder +~/.config/mini-pc-runner/. De runner start samen met Docker Desktop na grafisch +aanmelden, wacht maximaal 120 seconden op Docker en probeert een mislukte start +opnieuw. User lingering staat uit: vóór aanmelden of bij een uitgeschakelde mini-pc +is de runner niet beschikbaar. De oude agiledancers-runnerstate blijft voor herstel +bewaard; applicatiespecifieke deploymenttokens en SSH-sleutels blijven op hun eigen paden. ## Images en publicatie diff --git a/engineering.lock.json b/engineering.lock.json index 646ff93..8643026 100644 --- a/engineering.lock.json +++ b/engineering.lock.json @@ -1,5 +1,5 @@ { "repository": "http://192.168.2.28:3000/wanjakrah/engineering.git", - "tag": "v1.0.0", - "commit": "c25ce11a1bb8bf97f24d5e2b12d98680a9c573ca" + "tag": "v2.0.0", + "commit": "46df72f008113d0ed683a6c7344b1d7309564d84" } diff --git a/engineering.properties b/engineering.properties index 5e51de6..9f87ef9 100644 --- a/engineering.properties +++ b/engineering.properties @@ -16,7 +16,9 @@ TARGET_PLATFORM=linux/amd64 GITEA_SERVER_URL=http://192.168.2.28:3000 CI_REPOSITORY=wanjakrah/agiledancers INSECURE_REGISTRY=192.168.2.28:3000 -RUNNER_LABEL=agiledancers-mini-pc +RUNNER_LABEL=mini-pc +RUNNER_NAME=mini-pc +RUNNER_SCOPE=user DEPLOY_HOST=192.168.2.28 DEPLOY_USER=agiledancers-deploy DEPLOY_SSH_PORT=22 diff --git a/generated/CODING_STANDARDS.md b/generated/CODING_STANDARDS.md index aaad96e..9ec97ae 100644 --- a/generated/CODING_STANDARDS.md +++ b/generated/CODING_STANDARDS.md @@ -153,7 +153,7 @@ exactly at a blocking limit passes. repeating checks already enforced by tooling. - Keep upstream skill files unchanged. Put project-specific code-review guidance in a companion file referenced by project instructions and used alongside - `skills/code-review/SKILL.md`. + landscape's `skills/code-review/SKILL.md`. - Waivers must be explicit, narrowly scoped and carry a recorded reason alongside code or configuration. Agents need maintainer agreement before introducing one. No blanket suppressions, exclusions of difficult files or weakened thresholds diff --git a/generated/SOURCE.json b/generated/SOURCE.json index d833c0a..13b245f 100644 --- a/generated/SOURCE.json +++ b/generated/SOURCE.json @@ -1,5 +1,5 @@ { - "commit": "c25ce11a1bb8bf97f24d5e2b12d98680a9c573ca", + "commit": "46df72f008113d0ed683a6c7344b1d7309564d84", "repository": "http://192.168.2.28:3000/wanjakrah/engineering.git", - "tag": "v1.0.0" + "tag": "v2.0.0" } diff --git a/generated/ci/Ci.java b/generated/ci/Ci.java index a7b369e..bcf867a 100644 --- a/generated/ci/Ci.java +++ b/generated/ci/Ci.java @@ -3,9 +3,15 @@ import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; -/** Run from the repository root with JDK 25: java ci/Ci.java . */ +/** Run from the repository root with JDK 25: java generated/ci/Ci.java . */ public class Ci { - public static void main(String[] args) { + /** + * Runs a CI, release or workstation setup command from the consumer repository root. + * + * @param args command followed by its optional arguments; configuration comes from the project and environment + * @implNote Requires JDK 25. Failures exit nonzero with redacted diagnostics; setup commands can change local or Gitea state. + */ + public static void main(final String... args) { try { if (Runtime.version().feature() < 25) throw new IllegalArgumentException("JDK 25 or newer is required"); if (args.length == 0) throw new IllegalArgumentException(usage()); @@ -30,6 +36,8 @@ public class Ci { file, "CI_RUN_ATTEMPT=" + id + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND); } case "configure-gitea" -> Setup.configureGitea(env); + case "configure-runner" -> Setup.configureRunner(env); + case "check-runner" -> Setup.checkRunner(env); case "configure-docker" -> { if (args.length != 2) throw new IllegalArgumentException("Supply the daemon.json path"); Setup.configureDocker(Path.of(args[1]), Setup.required(env, "INSECURE_REGISTRY")); @@ -56,7 +64,8 @@ public class Ci { } static String usage() { - return "Usage: java ci/Ci.java verify|publish|cleanup|attempt|test|configure-gitea|configure-docker" + return "Usage: java generated/ci/Ci.java" + + " verify|publish|cleanup|attempt|test|configure-gitea|configure-runner|check-runner|configure-docker" + " |install-runner [--prepare-only]|deploy|rollback|release-test|configure-deployment"; } } diff --git a/generated/ci/CiTests.java b/generated/ci/CiTests.java index d33485f..dc44690 100644 --- a/generated/ci/CiTests.java +++ b/generated/ci/CiTests.java @@ -14,7 +14,7 @@ class CiTests { static void test(String name, Test test) throws Exception { test.run(); count++; - System.out.println("PASS " + name); + IO.println("PASS " + name); } static void equal(Object expected, Object actual) { @@ -32,6 +32,55 @@ class CiTests { static void run() throws Exception { count = 0; + test("shared runner scope and identity are independent of the application", () -> { + equal("mini-pc", Setup.runnerName(Map.of("PROJECT_NAME", "first"))); + equal("mini-pc", Setup.runnerName(Map.of("PROJECT_NAME", "second"))); + equal("/api/v1/user/actions/runners/registration-token", Setup.runnerRegistrationPath(Map.of())); + equal( + "/api/v1/admin/actions/runners/registration-token", + Setup.runnerRegistrationPath(Map.of("RUNNER_SCOPE", "instance"))); + equal( + "/api/v1/orgs/team/actions/runners/registration-token", + Setup.runnerRegistrationPath( + Map.of("RUNNER_SCOPE", "organization", "RUNNER_ORGANIZATION", "team"))); + equal( + "/api/v1/repos/owner/app/actions/runners/registration-token", + Setup.runnerRegistrationPath(Map.of("RUNNER_SCOPE", "repository", "CI_REPOSITORY", "owner/app"))); + failure(() -> Setup.runnerRegistrationPath(Map.of("RUNNER_SCOPE", "unknown"))); + failure(() -> Setup.runnerRegistrationPath( + Map.of("RUNNER_SCOPE", "organization", "RUNNER_ORGANIZATION", "../unsafe"))); + failure(() -> Setup.runnerName(Map.of("RUNNER_NAME", "../unsafe"))); + }); + test("runner preflight rejects unavailable offline and differently labelled runners", () -> { + equal(false, Setup.hasRunner(Map.of("runners", List.of()), "mini-pc")); + equal( + false, + Setup.hasRunner( + Map.of( + "runners", + List.of(Map.of("status", "offline", "labels", List.of(Map.of("name", "mini-pc"))))), + "mini-pc")); + equal( + false, + Setup.hasRunner( + Map.of( + "runners", + List.of(Map.of("status", "online", "labels", List.of(Map.of("name", "other"))))), + "mini-pc")); + equal( + true, + Setup.hasRunner( + Map.of( + "runners", + List.of(Map.of( + "status", + "online", + "busy", + true, + "labels", + List.of(Map.of("name", "mini-pc"))))), + "mini-pc")); + }); test("recipe supports backend-only and frontend-only projects", () -> { equal(List.of("backend"), Recipe.roles(Map.of("BACKEND_MODULE", "server"))); equal(List.of("frontend"), Recipe.roles(Map.of("FRONTEND_MODULE", "web"))); @@ -253,7 +302,7 @@ class CiTests { Pipeline.deleteTree(dir); } }); - System.out.println(count + " CI tests passed"); + IO.println(count + " CI tests passed"); } static class Fixture implements AutoCloseable { diff --git a/generated/ci/Pipeline.java b/generated/ci/Pipeline.java index d1ac2da..bcc7a0e 100644 --- a/generated/ci/Pipeline.java +++ b/generated/ci/Pipeline.java @@ -377,7 +377,7 @@ class Pipeline { void publish() throws Exception { if (!"refs/heads/main".equals(env.get("CI_REF"))) { - System.out.println("Branch is not main; no publication"); + IO.println("Branch is not main; no publication"); return; } check( @@ -425,7 +425,7 @@ class Pipeline { } for (String role : Recipe.roles(env)) { if (!commit.equals(mainHead())) { - System.out.println("Newer main exists; moving tags unchanged"); + IO.println("Newer main exists; moving tags unchanged"); return; } String image = prefix + role; @@ -462,7 +462,7 @@ class Pipeline { void ensureImage(String repository, String source) throws Exception { String ref = repository + ":" + commit; var manifest = result(false, "docker", "manifest", "inspect", "--insecure", ref); - if (manifest.code() == 0) System.out.println("Preserving existing " + ref); + if (manifest.code() == 0) IO.println("Preserving existing " + ref); else if (manifest.output().toLowerCase(Locale.ROOT).matches("(?s).*(manifest unknown|no such manifest).*")) { run("docker", "tag", source, ref); run("docker", "push", ref); diff --git a/generated/ci/Release.java b/generated/ci/Release.java index 395574c..2555ad6 100644 --- a/generated/ci/Release.java +++ b/generated/ci/Release.java @@ -75,7 +75,7 @@ class Release { remote.call("preflight", null); ReleaseState before = ReleaseState.parse(remote.call("state", null)); selected = select(before); - System.out.println("Selected " + selected.version() + " at " + selected.sha()); + IO.println("Selected " + selected.version() + " at " + selected.sha()); // Validate the recovery pair before any replacement too. if (before.current() != null) ensure(before.current()); if (mode.equals("new")) { @@ -108,7 +108,7 @@ class Release { record(outcome); remote.call("finish", null); pending = false; - System.out.println("Production " + selected.version() + " is healthy (" + selected.sha() + ")"); + IO.println("Production " + selected.version() + " is healthy (" + selected.sha() + ")"); } finally { try { record(outcome); diff --git a/generated/ci/ReleaseSetup.java b/generated/ci/ReleaseSetup.java index de8fa26..bcad410 100644 --- a/generated/ci/ReleaseSetup.java +++ b/generated/ci/ReleaseSetup.java @@ -61,7 +61,7 @@ class ReleaseSetup { env.getOrDefault("PULL_USER", env.get("CI_REPOSITORY").split("/")[0]); Pipeline.check(registryUser.matches("[a-zA-Z0-9_.-]+"), "Invalid registry user"); try (var remote = new ReleaseRemote(root, env, commands, "setup")) { - System.out.println(remote.executeScript( + IO.println(remote.executeScript( Files.readString(root.resolve("generated/deploy/provision.sh")), pull + "\n", 300, @@ -86,7 +86,7 @@ class ReleaseSetup { for (String variable : List.of("DEPLOY_HOST", "DEPLOY_USER", "DEPLOY_SSH_PORT", "DEPLOY_DIRECTORY", "DEPLOY_REGISTRY")) variable(api, admin, variable, env.get(variable)); - System.out.println( + IO.println( "Production configuration and Gitea deployment secrets/variables installed; no release was created."); } diff --git a/generated/ci/ReleaseTests.java b/generated/ci/ReleaseTests.java index 0f733b2..8316030 100644 --- a/generated/ci/ReleaseTests.java +++ b/generated/ci/ReleaseTests.java @@ -14,7 +14,7 @@ class ReleaseTests { static void test(String name, CiTests.Test test) throws Exception { test.run(); count++; - System.out.println("PASS release: " + name); + IO.println("PASS release: " + name); } static Map environment() { @@ -252,7 +252,7 @@ class ReleaseTests { apiTests(); shellTests(); ReleaseShellTests.run(); - System.out.println(count + " release behavior tests passed"); + IO.println(count + " release behavior tests passed"); } static class FakeApi extends ReleaseApi { diff --git a/generated/ci/Setup.java b/generated/ci/Setup.java index 0963d5b..20ca904 100644 --- a/generated/ci/Setup.java +++ b/generated/ci/Setup.java @@ -79,12 +79,79 @@ class Setup { exists = false; } api(server, admin, exists ? "PUT" : "POST", variable, Map.of("value", platform)); - Map response = - Json.object(api(server, admin, "POST", base + "/actions/runners/registration-token", null)); - Path directory = home().resolve(".config/" + Recipe.project(env) + "-runner"); + IO.println("Actions, project registry secrets and platform configured; using the shared runner."); + } + + static String runnerName(final Map env) { + final String name = env.getOrDefault("RUNNER_NAME", env.getOrDefault("RUNNER_LABEL", "mini-pc")); + Pipeline.check(name.matches("[a-z][a-z0-9-]*"), "Invalid runner name"); + return name; + } + + static String runnerRegistrationPath(final Map env) { + return switch (env.getOrDefault("RUNNER_SCOPE", "user")) { + case "user" -> "/api/v1/user/actions/runners/registration-token"; + case "instance" -> "/api/v1/admin/actions/runners/registration-token"; + case "organization" -> { + final String organization = required(env, "RUNNER_ORGANIZATION"); + Pipeline.check(organization.matches("[a-zA-Z0-9_-]+"), "Invalid runner organization"); + yield "/api/v1/orgs/" + organization + "/actions/runners/registration-token"; + } + case "repository" -> + "/api/v1/repos/" + required(env, "CI_REPOSITORY") + "/actions/runners/registration-token"; + default -> + throw new IllegalArgumentException("Runner scope must be user, organization, instance or repository"); + }; + } + + static void configureRunner(final Map env) throws Exception { + final String admin = secret(env, "GITEA_ADMIN_TOKEN_FILE"); + final Map response = + Json.object(api(required(env, "GITEA_SERVER_URL"), admin, "POST", runnerRegistrationPath(env), null)); + final Path directory = home().resolve(".config/" + runnerName(env) + "-runner"); privateDirectory(directory); privateWrite(directory.resolve("registration-token"), Objects.toString(response.get("token"))); - System.out.println("Actions, registry secrets and platform configured; runner token saved outside Git."); + IO.println("Shared runner registration token saved outside Git; run install-runner once on the workstation."); + } + + static void checkRunner(final Map env) throws Exception { + final String token = secret(env, "GITEA_ADMIN_TOKEN_FILE"); + final String repository = required(env, "CI_REPOSITORY"); + final String server = required(env, "GITEA_SERVER_URL"); + final Map project = Json.object(api(server, token, "GET", "/api/v1/repos/" + repository, null)); + Pipeline.check(Boolean.TRUE.equals(project.get("has_actions")), "Enable Actions for the repository first"); + if ("user".equals(env.getOrDefault("RUNNER_SCOPE", "user"))) { + final Map user = Json.object(api(server, token, "GET", "/api/v1/user", null)); + Pipeline.check( + repository.split("/", 2)[0].equalsIgnoreCase(Objects.toString(user.get("login"))), + "User-scoped runner must belong to the repository owner; use organization scope for an" + + " organization"); + } + // Gitea's repository runner list excludes inherited user/organization runners. + final String base = runnerRegistrationPath(env).replace("/registration-token", ""); + final Map response = + Json.object(api(required(env, "GITEA_SERVER_URL"), token, "GET", base, null)); + Pipeline.check( + hasRunner(response, env.getOrDefault("RUNNER_LABEL", "mini-pc")), + "No online shared runner with the configured label is available to this repository"); + IO.println("Shared runner is online and available to " + required(env, "CI_REPOSITORY")); + } + + static boolean hasRunner(final Map response, final String label) { + for (final Object entry : (List) response.getOrDefault("runners", List.of())) { + final Map runner = Json.object(entry); + if (!"online".equals(runner.get("status")) + && !"idle".equals(runner.get("status")) + && !"active".equals(runner.get("status"))) { + continue; + } + for (final Object item : (List) runner.getOrDefault("labels", List.of())) { + if (label.equals(Json.object(item).get("name"))) { + return true; + } + } + } + return false; } static void configureDocker(Path file, String registry) throws IOException { @@ -95,7 +162,7 @@ class Setup { throw new IllegalArgumentException("Existing insecure-registries must be an array; unchanged"); List registries = new ArrayList<>((List) entry); if (registries.contains(registry)) { - System.out.println("Internal registry already configured; no change"); + IO.println("Internal registry already configured; no change"); return; } registries.add(registry); @@ -114,7 +181,7 @@ class Setup { } finally { Files.deleteIfExists(temp); } - System.out.println("Internal registry added; restart/apply Docker Engine configuration to activate"); + IO.println("Internal registry added; restart/apply Docker Engine configuration to activate"); } static void installRunner(Path root, Map env, Commands commands, boolean prepareOnly) @@ -123,8 +190,8 @@ class Setup { System.getProperty("os.name").equals("Linux") && Set.of("amd64", "x86_64").contains(System.getProperty("os.arch")), "Installer targets the existing Linux x86_64 mini-PC"); - Path runnerDir = home().resolve(".local/share/" + Recipe.project(env) + "-runner"), - configDir = home().resolve(".config/" + Recipe.project(env) + "-runner"); + Path runnerDir = home().resolve(".local/share/" + runnerName(env) + "-runner"), + configDir = home().resolve(".config/" + runnerName(env) + "-runner"); Path bin = home().resolve(".local/bin"), units = home().resolve(".config/systemd/user"); for (Path path : List.of(runnerDir, configDir, bin, units)) privateDirectory(path); for (String tool : List.of("node", "java", "mvn", "docker", "git", "systemctl")) executable(env, tool); @@ -164,11 +231,14 @@ class Setup { String actual = commands.execute(List.of(runner.toString(), "--version"), root, env, null, false) .checked(); Pipeline.check(actual.contains("v0.2.13"), "Expected runner v0.2.13"); - privateWrite(runnerDir.resolve("config.yaml"), Files.readString(root.resolve("ci/runner/config.yaml"))); - Files.copy( - root.resolve("ci/runner/runner.service"), - units.resolve(Recipe.project(env) + "-runner.service"), - StandardCopyOption.REPLACE_EXISTING); + privateWrite( + runnerDir.resolve("config.yaml"), + Files.readString(root.resolve("ci/runner/config.yaml")) + .replace("@RUNNER@", env.getOrDefault("RUNNER_LABEL", "mini-pc"))); + privateWrite( + units.resolve(runnerName(env) + "-runner.service"), + Files.readString(root.resolve("ci/runner/runner.service")).replace("@RUNNER_NAME@", runnerName(env))); + Files.copy( root.resolve("generated/ci/runner/WaitForDocker.java"), runnerDir.resolve("WaitForDocker.java"), @@ -189,7 +259,7 @@ class Setup { "JAVA_HOME=" + quoted(javaHome) + "\nPATH=" + quoted(path) + "\nDOCKER_HOST=" + quoted(dockerHost) + "\n"); boolean desktop = dockerHost.endsWith("/.docker/desktop/docker.sock"); - Path overrides = units.resolve(Recipe.project(env) + "-runner.service.d"); + Path overrides = units.resolve(runnerName(env) + "-runner.service.d"); Path desktopOverride = overrides.resolve("10-docker-desktop.conf"); if (desktop) { Files.createDirectories(overrides); @@ -203,7 +273,7 @@ class Setup { commands.execute(List.of("systemctl", "--user", "daemon-reload"), root, env, null, true) .checked(); if (prepareOnly) { - System.out.println("Runner installed; registration and startup still required."); + IO.println("Runner installed; registration and startup still required."); return; } commands.execute(List.of("docker", "info"), root, env, null, false).checked(); @@ -221,9 +291,9 @@ class Setup { "--instance", required(env, "GITEA_SERVER_URL"), "--name", - required(env, "RUNNER_LABEL"), + runnerName(env), "--labels", - required(env, "RUNNER_LABEL") + ":host", + env.getOrDefault("RUNNER_LABEL", "mini-pc") + ":host", "--token", token), runnerDir, @@ -243,7 +313,7 @@ class Setup { .checked(); // Replace any previous default.target link with the desktop-login target. commands.execute( - List.of("systemctl", "--user", "disable", Recipe.project(env) + "-runner.service"), + List.of("systemctl", "--user", "disable", runnerName(env) + "-runner.service"), root, env, null, @@ -251,13 +321,13 @@ class Setup { .checked(); } commands.execute( - List.of("systemctl", "--user", "enable", "--now", Recipe.project(env) + "-runner.service"), + List.of("systemctl", "--user", "enable", "--now", runnerName(env) + "-runner.service"), root, env, null, true) .checked(); - System.out.println("Runner registered and started; keep the mini-PC and Docker available."); + IO.println("Runner registered and started; keep the mini-PC and Docker available."); } static Path executable(Map env, String name) { diff --git a/generated/ci/runner/WaitForDocker.java b/generated/ci/runner/WaitForDocker.java index 27c8aab..6ee9d0d 100644 --- a/generated/ci/runner/WaitForDocker.java +++ b/generated/ci/runner/WaitForDocker.java @@ -3,11 +3,18 @@ import java.util.concurrent.TimeUnit; /** Source-launcher startup probe; uses the service's configured DOCKER_HOST. */ class WaitForDocker { - public static void main(String[] args) throws Exception { + /** + * Waits for the configured Docker daemon before allowing the runner service to start. + * + * @param args optional timeout in seconds, from 1 through 300; defaults to 120 + * @throws Exception if the timeout is invalid or the Docker probe cannot run + * @implNote Exits nonzero if Docker does not become ready before the deadline. + */ + public static void main(final String... args) throws Exception { long seconds = args.length == 0 ? 120 : Long.parseLong(args[0]); if (seconds < 1 || seconds > 300) throw new IllegalArgumentException("Timeout must be 1–300 seconds"); long deadline = System.nanoTime() + Duration.ofSeconds(seconds).toNanos(); - System.out.println("Waiting for the configured Docker engine..."); + IO.println("Waiting for the configured Docker engine..."); while (System.nanoTime() < deadline) { Process process = new ProcessBuilder("docker", "info", "--format", "{{.ServerVersion}}") .redirectOutput(ProcessBuilder.Redirect.DISCARD) @@ -17,7 +24,7 @@ class WaitForDocker { long remaining = Math.max(1, deadline - System.nanoTime()); if (process.waitFor(Math.min(remaining, TimeUnit.SECONDS.toNanos(5)), TimeUnit.NANOSECONDS) && process.exitValue() == 0) { - System.out.println("Docker is available; starting the Gitea runner."); + IO.println("Docker is available; starting the Gitea runner."); return; } } finally { diff --git a/generated/docs/backend-review.md b/generated/docs/backend-review.md index ff4a556..ad21580 100644 --- a/generated/docs/backend-review.md +++ b/generated/docs/backend-review.md @@ -1,6 +1,6 @@ # Backend review companion -Use alongside `generated/skills/code-review/SKILL.md` and `generated/CODING_STANDARDS.md`. +Use alongside landscape's `skills/code-review/SKILL.md` and `generated/CODING_STANDARDS.md`. Review changed code for intent-revealing comments, missing public-contract Javadoc, appropriate record use, defensive copying of mutable record components, and diff --git a/generated/skills/README.md b/generated/skills/README.md deleted file mode 100644 index 2e5c712..0000000 --- a/generated/skills/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Engineering skills - -Reusable agent instructions for consuming repositories. Invoke them explicitly in the -conversation, for example: - -> Gebruik grill-me om de JSON-import en -export met mij uit te werken. - -## Installed skills - -| Skill | Purpose | -| --- | --- | -| [code-review](code-review/SKILL.md) | Review changes against documented coding standards and the originating spec. | -| [grill-me](grill-me/SKILL.md) | Entry point for reviewing a plan or design through questions. | -| [grilling](grilling/SKILL.md) | Interview instructions required by `grill-me`. | - -The skills are copied from [Matt Pocock's skills.sh listing](https://skills.sh/mattpocock/skills/grill-me) -and its [upstream repository](https://github.com/mattpocock/skills). -The exact upstream revisions and paths are recorded in [upstream.json](upstream.json). -The upstream [MIT license](LICENSE.mattpocock) is included. - -These project-local files are available to the agent from the next conversation -turn through the instructions in the consuming project's root `AGENTS.md`. They are not installed -globally. Update upstream sources in engineering, retaining licenses and revision records, then release a new engineering version. Consumers regenerate their pinned copies. diff --git a/generated/skills/adr-brainstorm/ATTRIBUTION.md b/generated/skills/adr-brainstorm/ATTRIBUTION.md deleted file mode 100644 index 1ebe60f..0000000 --- a/generated/skills/adr-brainstorm/ATTRIBUTION.md +++ /dev/null @@ -1,24 +0,0 @@ -# License and attribution - -ADR brainstorm is maintained by Wanja Krah and is distributed under the MIT -License. See [LICENSE.wanjakrah](LICENSE.wanjakrah) for the license covering -Wanja Krah's original contributions. - -The design-tree approach and related interviewing guidance in `SKILL.md` were -adapted from Matt Pocock's `grilling` skill, invoked by his `grill-me` skill. -This includes tracking dependent decisions, settling prerequisites before -asking dependent questions, investigating accessible facts, and leaving -decisions with the user. The wording and workflow have been adapted for ADR -brainstorming; this is not an unmodified copy of the upstream skill. - -Upstream source: -- Repository: https://github.com/mattpocock/skills -- Revision: `59db609eb1e18aaf59b63284d0e52255aff0c63c` -- Paths: `skills/productivity/grilling` and `skills/productivity/grill-me` -- License: MIT, copyright (c) 2026 Matt Pocock - -Matt Pocock's full copyright and license notice is preserved in -[LICENSE.mattpocock](LICENSE.mattpocock) for the adapted material. Keep both -license files with redistributed copies or substantial portions of this skill. -The two notices preserve attribution under the same MIT terms; they do not -introduce an additional license restriction or imply upstream endorsement. diff --git a/generated/skills/adr-brainstorm/LICENSE.mattpocock b/generated/skills/adr-brainstorm/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/generated/skills/adr-brainstorm/LICENSE.mattpocock +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Matt Pocock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generated/skills/adr-brainstorm/LICENSE.wanjakrah b/generated/skills/adr-brainstorm/LICENSE.wanjakrah deleted file mode 100644 index 73f2e1d..0000000 --- a/generated/skills/adr-brainstorm/LICENSE.wanjakrah +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Wanja Krah - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generated/skills/adr-brainstorm/SKILL.md b/generated/skills/adr-brainstorm/SKILL.md deleted file mode 100644 index 6ef44da..0000000 --- a/generated/skills/adr-brainstorm/SKILL.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: adr-brainstorm -description: "Brainstorm with the user about a new ADR. Use when the user wants to stress-test their thinking, or uses any combination of 'adr' and 'brainstorm' in their trigger phrase." ---- - -An ADR is an Architecture Decision Record. Such a record contains a decision on the architecture or design for the application landscape the user is maintaining. -If an ADR-folder exists, read the README.md of that folder to get a full understanding of any specific rules and properties of how the user works with ADR's that might not be listed in this Skill. - -The goal of the ADR is to provide an architectural solution or a design solution for a given context. Therefor, two to four different approaches must be worked out with their pro's and con's, from which the user should select one as their preferred solution. - -Below, the brainstorm session is mapped out in multiple steps. Each step must be completed succesfully, before continuing to the next step. Success means reaching a shared understanding and whatever the step entitles as its success criteria. - -If the user, or the relevant ADR, indicates that an ADR is to supersede another ADR, read that ADR first. - -## Explore decisions with the user - -### Follow a design tree - -Map the architectural question as a design tree: each decision may -introduce dependent decisions. Track which decisions are agreed, -open, awaiting factual investigation, or explicitly deferred. - -Ask dependent questions only after their prerequisites are settled. -Revisit earlier decisions when new evidence changes their assumptions, -and explain which dependent decisions need reconsideration. - -Keep the tree within the ADR's agreed scope. Suggest follow-up work -for independent topics rather than continually expanding the ADR. -A deferred topic must have a clear boundary and must not leave the -main decision ambiguous. - -Finish when the user has agreed on the architectural decision and its material tradeoffs. Identify remaining implementation questions as follow-up work. Do not leave questions open that could materially change the chosen approach. - -### Establish facts before asking for decisions - -Finding accessible facts is the agent's responsibility. Inspect relevant -code, configuration, specifications, ADRs, and authoritative documentation -before asking the user questions those sources can answer. - -Distinguish observed facts, assumptions, recommendations, and decisions. -Cite sources for material factual claims and explain uncertainty where -evidence is unavailable or conflicting. Ask the user for requirements, -priorities, constraints, and knowledge unavailable from accessible sources. - -Evaluate alternatives against the agreed requirements. Present credible -alternatives fairly, including drawbacks of the recommended approach. -Do not infer that an option is unsuitable merely because the current -implementation uses something else. - -### Keep the conversation collaborative - -Use short rounds, usually one main question or a few closely related -questions. Number questions when asking more than one. Do not ask every -available question at once if that would hinder discussion. - -For decision questions, normally provide a recommended answer and its -reason. Where natural, phrase the question so "yes" clearly accepts that -recommendation. Make its scope explicit; avoid combining independent -decisions into one yes/no question. - -Use open questions when discovering requirements or when a recommendation -would prematurely narrow the discussion. Allow disagreement, alternatives, -and back-and-forth exploration before seeking a decision. - -### Leave architectural decisions with the user - -The agent investigates, explains tradeoffs, challenges assumptions, and -recommends. The user selects the architectural approach and accepts its -material tradeoffs. - -Do not treat silence, a factual answer, or agreement on one decision as -approval of other decisions. Preserve earlier agreements without repeatedly -requesting confirmation unless their meaning or supporting facts change. - -At meaningful milestones, summarize the agreed decisions and remaining -questions. Before finalizing the ADR, confirm shared understanding of the -complete proposal. Keep recommendations and unresolved assumptions out of -the Decisions section until the user accepts them. - -### Guide the process through steps - -Coöperate with the user via the following steps. Communicate each step in which step you are, and what the step entails. Use the template adr-template.md as a template for the final ADR. - -The _first step_ is to get to a shared understanding of the probived context and the problem area the user is in need for a decision. Success for step one means you can describe the context in one to fifty sentences. - -The _second step_ is to get to two to four different architectural solutions or design solutions for the described context. Every solution needs a short description of one to fifty sentences and a list of one to ten pro's and one to ten con's. allow discussion of consequences when they affect the choice. Record them formally in _step four_, but don’t postpone discovering migration costs or operational burdens until after selecting an option. -Success for step two means there are more than one credible solutions with a set of relevant pro's and con's by which a user is able to choose, or not choose that solution for the next step. - -The _third step_ is to pick the best solution. Ths user must decide what solution he prefers. The choosen solution will be presented as the ADR decision. A decision may branch into a small tree of related decisions, small meaning one to five levels deep. Make the architectural decision, its scope, and its material tradeoffs unambiguous; record remaining implementation questions as follow-up work. Success of step three means there is a shared understanding of the choosen solution and what it entails. - -The _fourth step_ is to make clear what the consequences will be for the application landscape. Success of step four means there is a clear understanding of the changes that should be made to software landscape and the way futurue work should be implemented. - -The _fifth step_ is fill out the other properties of the ADR that is required by the template. Succes of step five means the ID of the ADR is unique, the title is short and comprehandible, and gives the user an overall idea of what the ADR is about. If the ADR supersedes another ADR, that is listed as well. -Set the Expiration Date of when the ADR should be reviewd. This should be a relative date, to be made absolute by the user as soon as the user moves the ADR to APPROVED. -Finally, set the status to REFINED. It's up to the user to set the Status to either APPROVED - meaning the ADR is accepted and to be applied for all future work - or REJECTED - meaning the ADR is not accepted and should not be taken in consideration. - -When updating the ADR with the result of the brainstorm, make it both easy to glanse through, as to read in full, meaning: -- stick to the template -- keep to the formatting as is custom for markdown-files -- high use of headers and sub-headers -- high use of bullit points, lists, and tables where applicable -- keep sentences short and precise where possible -- create images with ASCII-art where possible \ No newline at end of file diff --git a/generated/skills/adr-brainstorm/adr-template.md b/generated/skills/adr-brainstorm/adr-template.md deleted file mode 100644 index 7afb962..0000000 --- a/generated/skills/adr-brainstorm/adr-template.md +++ /dev/null @@ -1,20 +0,0 @@ -**ID:** \ -**Title:** \ -**Supersedes:** - \ -**Status:** NEW \ -**Expiration Date:** to be determined - -## Context -To be determined - -## Decisions -To be determined - -## Options considered -To be determined - -## Consequences -To be determined - -## References - \ No newline at end of file diff --git a/generated/skills/adr-brainstorm/agents/openai.yaml b/generated/skills/adr-brainstorm/agents/openai.yaml deleted file mode 100644 index 9dae6f3..0000000 --- a/generated/skills/adr-brainstorm/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "ADR Brainstorm" - short_description: "Brainstorm with the user on an ADR to get it from NEW to REFINED" diff --git a/generated/skills/code-review/LICENSE.mattpocock b/generated/skills/code-review/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/generated/skills/code-review/LICENSE.mattpocock +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Matt Pocock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generated/skills/code-review/SKILL.md b/generated/skills/code-review/SKILL.md deleted file mode 100644 index 83183e0..0000000 --- a/generated/skills/code-review/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: code-review -description: "Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\"." ---- - -Two-axis review of the diff between `HEAD` and a fixed point the user supplies: - -- **Standards**: does the code conform to this repo's documented coding standards? -- **Spec**: does the code faithfully implement the originating issue / spec? - -Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. Start every sub-agent with a fresh context window. - -The issue tracker should have been provided to you. If not, tell the user to run `/setup-matt-pocock-skills`. - -## Process - -### 1. Pin the fixed point - -Whatever the user said is the fixed point (a commit SHA, branch name, tag, `main`, `HEAD~5`, etc.). If they didn't specify one, ask for it. - -Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. - -Before going further, confirm the fixed point resolves (`git rev-parse `) and the diff is non-empty. A bad ref or empty diff should fail here, not inside two parallel sub-agents. - -### 2. Identify the spec source - -Look for the originating spec, in this order: - -1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in the tracker doc. -2. A path the user passed as an argument. -3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. -4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". - -### 3. Identify the standards sources - -Search the repo for every file that documents how code should be written. When `CODING_STANDARDS.md` or `CONTRIBUTING.md` exists, it must be on the list. - -On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below: a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it: - -- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell. -- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation. Like any standard here, skip anything tooling already enforces. - -Each smell reads *what it is* → *how to fix*; match it against the diff: - -- **Mysterious Name**: a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky. -- **Duplicated Code**: the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both. -- **Feature Envy**: a method that reaches into another object's data more than its own. → move the method onto the data it envies. -- **Data Clumps**: the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that. -- **Primitive Obsession**: a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type. -- **Repeated Switches**: the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share. -- **Shotgun Surgery**: one logical change forces scattered edits across many files in the diff. → gather what changes together into one module. -- **Divergent Change**: one file or module is edited for several unrelated reasons. → split so each module changes for one reason. -- **Speculative Generality**: abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows. -- **Message Chains**: long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object. -- **Middle Man**: a class or function that mostly just delegates onward. → cut it, call the real target direct. -- **Refused Bequest**: a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition. - -### 4. Spawn both sub-agents in parallel - -Issue both sub-agent calls together, in the foreground, and aggregate the reports they return. - -**Standards sub-agent prompt** should include: - -- The full diff command and commit list. -- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full (the sub-agent has no other access to it). -- The brief: "Report, per file/hunk where relevant, (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls: documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words." - -**Spec sub-agent prompt** should include: - -- The diff command and commit list. -- The path or fetched contents of the spec. -- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." - -If the spec is missing, skip the Spec sub-agent and note this in the final report. - -### 5. Aggregate - -Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings, because the two axes are deliberately separate (see _Why two axes_). - -End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes: that's the reranking the separation exists to prevent. - -## Why two axes - -A change can pass one axis and fail the other: - -- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** -- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** - -Reporting them separately stops one axis from masking the other. diff --git a/generated/skills/code-review/agents/openai.yaml b/generated/skills/code-review/agents/openai.yaml deleted file mode 100644 index 9076774..0000000 --- a/generated/skills/code-review/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "Code Review" - short_description: "Review a diff on standards and spec" diff --git a/generated/skills/grill-me/LICENSE.mattpocock b/generated/skills/grill-me/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/generated/skills/grill-me/LICENSE.mattpocock +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Matt Pocock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generated/skills/grill-me/SKILL.md b/generated/skills/grill-me/SKILL.md deleted file mode 100644 index 3947ff9..0000000 --- a/generated/skills/grill-me/SKILL.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: grill-me -description: A relentless interview to sharpen a plan or design. -disable-model-invocation: true ---- - -Call the Skill tool with "grilling". diff --git a/generated/skills/grill-me/agents/openai.yaml b/generated/skills/grill-me/agents/openai.yaml deleted file mode 100644 index 4d6fb0c..0000000 --- a/generated/skills/grill-me/agents/openai.yaml +++ /dev/null @@ -1,5 +0,0 @@ -interface: - display_name: "Grill Me" - short_description: "Sharpen a plan through interview" -policy: - allow_implicit_invocation: false diff --git a/generated/skills/grilling/LICENSE.mattpocock b/generated/skills/grilling/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/generated/skills/grilling/LICENSE.mattpocock +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Matt Pocock - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/generated/skills/grilling/SKILL.md b/generated/skills/grilling/SKILL.md deleted file mode 100644 index df69d99..0000000 --- a/generated/skills/grilling/SKILL.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: grilling -description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. ---- - -Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it. - -Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round. - -Format a round like so: - -``` -❓ **Q1** - ****: - -➡️ - ---- - -❓ **Q2** - ****: - -➡️ -``` - -Word each question so "yes" accepts your recommended answer. - -Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one. - -Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait. - -The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding. diff --git a/generated/skills/grilling/agents/openai.yaml b/generated/skills/grilling/agents/openai.yaml deleted file mode 100644 index ddbdb96..0000000 --- a/generated/skills/grilling/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "Grilling" - short_description: "Stress-test thinking a round of questions at a time" diff --git a/generated/skills/upstream.json b/generated/skills/upstream.json deleted file mode 100644 index 67230b0..0000000 --- a/generated/skills/upstream.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "repository": "https://github.com/mattpocock/skills", - "commit": "59db609eb1e18aaf59b63284d0e52255aff0c63c", - "source": "https://skills.sh/mattpocock/skills/grill-me", - "skills": { - "grill-me": "skills/productivity/grill-me", - "grilling": "skills/productivity/grilling", - "code-review": { - "path": "skills/engineering/code-review", - "commit": "a496f5513d4a4cf889bc48e189e579f4b0bdf6aa", - "source": "https://www.skills.sh/mattpocock/skills/code-review" - } - } -} diff --git a/generated/tooling/coverage/Coverage.java b/generated/tooling/coverage/Coverage.java index 86d99ed..3051bad 100644 --- a/generated/tooling/coverage/Coverage.java +++ b/generated/tooling/coverage/Coverage.java @@ -4,7 +4,13 @@ import org.w3c.dom.Element; /** Exact module-wide line gate; consumes JaCoCo XML without rounding percentages. */ public class Coverage { - public static void main(String[] args) throws Exception { + /** + * Gates a JaCoCo XML report at 80 percent module line coverage without rounding. + * + * @param args the report path as the sole argument + * @throws Exception if the report cannot be read, has no line counter, or fails the coverage gate + */ + public static void main(final String... args) throws Exception { final var factory = DocumentBuilderFactory.newInstance(); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); @@ -18,8 +24,7 @@ public class Coverage { final long covered = Long.parseLong(counter.getAttribute("covered")); final long missed = Long.parseLong(counter.getAttribute("missed")); final long total = covered + missed; - System.out.println( - "Backend line coverage: " + covered + "/" + total + " (" + 100.0 * covered / total + "%)"); + IO.println("Backend line coverage: " + covered + "/" + total + " (" + 100.0 * covered / total + "%)"); if (total == 0 || 5 * covered < 4 * total) throw new IllegalStateException("Backend line coverage must reach 80%: " + args[0]); return; diff --git a/generated/tooling/format/Format.java b/generated/tooling/format/Format.java index 3413b94..f601407 100644 --- a/generated/tooling/format/Format.java +++ b/generated/tooling/format/Format.java @@ -9,7 +9,13 @@ import org.openrewrite.text.*; /** Source-launcher bootstrap: no project-specific artifact is resolved or installed. */ public class Format { - public static void main(String[] args) throws Exception { + /** + * Checks or atomically formats configured handwritten Java sources with the pinned formatter. + * + * @param args consumer repository root followed by either {@code check} or {@code apply} + * @throws Exception if configuration, parsing or formatting fails, or check mode finds differences + */ + public static void main(final String... args) throws Exception { Path root = Path.of(args[0]).toAbsolutePath().normalize(); boolean check = switch (args[1]) { case "check" -> true; @@ -64,7 +70,7 @@ public class Format { .getChangeset() .getAllResults(); for (Result result : results) { - System.out.println((check ? "Needs formatting: " : "Formatted: ") + IO.println((check ? "Needs formatting: " : "Formatted: ") + result.getAfter().getSourcePath()); } if (check && !results.isEmpty()) { @@ -96,8 +102,7 @@ public class Format { try (var output = Files.newOutputStream(cacheFile)) { cache.store(output, "Successfully formatted content only"); } - System.out.println( - "Palantir " + args[1] + ": " + sources.size() + " Java files, " + results.size() + " differences"); + IO.println("Palantir " + args[1] + ": " + sources.size() + " Java files, " + results.size() + " differences"); } static String digest(String value) throws Exception { diff --git a/generated/tooling/standards/src/main/java/Standards.java b/generated/tooling/standards/src/main/java/Standards.java index 213b3ad..51bbfe0 100644 --- a/generated/tooling/standards/src/main/java/Standards.java +++ b/generated/tooling/standards/src/main/java/Standards.java @@ -28,7 +28,13 @@ public class Standards { static final List waivers = new ArrayList<>(); - public static void main(String[] args) throws Exception { + /** + * Checks configured backend sources and writes standards reports without changing maintained code. + * + * @param args the consumer repository root as the sole argument + * @throws Exception if configuration, resolution or parsing fails, or a blocking violation is found + */ + public static void main(final String... args) throws Exception { findings.clear(); codeLines.clear(); types.clear();