From 46df72f008113d0ed683a6c7344b1d7309564d84 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Tue, 10 Nov 2026 08:14:35 +0100 Subject: [PATCH] S19: share workstation runner and move agent workflows to landscape --- AGENTS.md | 3 +- README.md | 70 +++++++++-- SOURCE.md | 3 + UPGRADING.md | 20 ++++ VALIDATION.md | 17 +++ create-project.py | 5 +- resources/CODING_STANDARDS.md | 2 +- resources/ci/Ci.java | 15 ++- resources/ci/CiTests.java | 53 ++++++++- resources/ci/Pipeline.java | 6 +- resources/ci/Release.java | 4 +- resources/ci/ReleaseSetup.java | 4 +- resources/ci/ReleaseTests.java | 4 +- resources/ci/Setup.java | 110 ++++++++++++++---- resources/ci/runner/WaitForDocker.java | 13 ++- resources/docs/backend-review.md | 2 +- resources/skills/README.md | 23 ---- .../skills/adr-brainstorm/ATTRIBUTION.md | 24 ---- .../skills/adr-brainstorm/LICENSE.mattpocock | 21 ---- .../skills/adr-brainstorm/LICENSE.wanjakrah | 21 ---- resources/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 ---- resources/skills/code-review/SKILL.md | 89 -------------- .../skills/code-review/agents/openai.yaml | 3 - resources/skills/grill-me/LICENSE.mattpocock | 21 ---- resources/skills/grill-me/SKILL.md | 7 -- resources/skills/grill-me/agents/openai.yaml | 5 - resources/skills/grilling/LICENSE.mattpocock | 21 ---- resources/skills/grilling/SKILL.md | 30 ----- resources/skills/grilling/agents/openai.yaml | 3 - resources/skills/upstream.json | 14 --- resources/tooling/coverage/Coverage.java | 11 +- resources/tooling/format/Format.java | 13 ++- .../standards/src/main/java/Standards.java | 8 +- templates/common/AGENTS.md | 9 +- templates/common/README.md | 6 +- templates/common/ci/runner/runner.service | 10 +- tests/test_generator.py | 15 +++ 40 files changed, 334 insertions(+), 498 deletions(-) delete mode 100644 resources/skills/README.md delete mode 100644 resources/skills/adr-brainstorm/ATTRIBUTION.md delete mode 100644 resources/skills/adr-brainstorm/LICENSE.mattpocock delete mode 100644 resources/skills/adr-brainstorm/LICENSE.wanjakrah delete mode 100644 resources/skills/adr-brainstorm/SKILL.md delete mode 100644 resources/skills/adr-brainstorm/adr-template.md delete mode 100644 resources/skills/adr-brainstorm/agents/openai.yaml delete mode 100644 resources/skills/code-review/LICENSE.mattpocock delete mode 100644 resources/skills/code-review/SKILL.md delete mode 100644 resources/skills/code-review/agents/openai.yaml delete mode 100644 resources/skills/grill-me/LICENSE.mattpocock delete mode 100644 resources/skills/grill-me/SKILL.md delete mode 100644 resources/skills/grill-me/agents/openai.yaml delete mode 100644 resources/skills/grilling/LICENSE.mattpocock delete mode 100644 resources/skills/grilling/SKILL.md delete mode 100644 resources/skills/grilling/agents/openai.yaml delete mode 100644 resources/skills/upstream.json diff --git a/AGENTS.md b/AGENTS.md index 4a69073..e2ec33b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,8 @@ - Edit resources/ as the source; generated copies in applications are consumers. - Read resources/CODING_STANDARDS.md before changing Java; project architecture and metrics apply to consumer backend modules as documented there. -- Keep upstream skills unchanged and retain resources/skills/upstream.json and licenses. +- Shared agent skills and their upstream attribution live in landscape/skills/. + Keep upstream files unchanged; update shared workflows there, not in engineering. - Keep project-specific configuration, secrets and waivers out of shared defaults. - Run the bootstrap, formatter, CI/release regression and generated-example checks. - Never move a published release tag. Document consumer-file changes in UPGRADING.md. diff --git a/README.md b/README.md index baffbc1..99502b9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Engineering -Versioned skills, Java standards, formatting, coverage checks, and the Gitea +Versioned Java standards, formatting, coverage checks, and the Gitea Actions / Maven Java 25 / npm / Docker Compose recipe. Architecture and specifications belong to [landscape](http://192.168.2.28:3000/wanjakrah/landscape). -Source attribution is in SOURCE.md; upstream skill licenses accompany each skill. +Shared agent skills are maintained in landscape under skills/. Source attribution is in SOURCE.md. ## Start a project @@ -11,7 +11,7 @@ Clone this repository and check out a released tag. Use JDK 25, Maven, Python 3, Git, Docker with Compose, and a supported Node version for a frontend. ```sh -python3 create-project.py my-project ../my-project --version v1.0.0 --components both --runner engineering +python3 create-project.py my-project ../my-project --version v2.0.0 --components both ``` Choose `backend`, `frontend`, or `both`. The command refuses existing destinations @@ -21,8 +21,11 @@ while preserving the configured build commands and container contracts. Initialize the resulting Git repository and commit its generated resources. Create the remote repository separately, configure its runner and secrets, then -push. The runner label must match an available **host** runner with the required -tools. `--runner` deliberately has no machine-specific default. +push. New projects default to the shared **host** runner label `mini-pc`. +Register it once at Gitea user scope for the owner of the projects; it then serves +existing and future repositories of that owner. `--runner` overrides the label for +another workstation. A label alone does not grant a repository access to a runner. +Do not install another runner service for every project. ## Consumer layout and configuration @@ -88,11 +91,58 @@ Manual deployment and rollback workflows require variables `DEPLOY_HOST`, a successful main-push build.yaml run. Keep build.yaml's filename when adapting the recipe. Configure Gitea access, registry access and SSH host trust explicitly. -Runner setup mechanisms are available as `configure-gitea`, `configure-docker` -and `install-runner`. Supply GITEA_SERVER_URL, CI_REPOSITORY, RUNNER_LABEL and -INSECURE_REGISTRY where applicable; the latter is only for a deliberately trusted -internal HTTP registry. Runner configuration and service templates are project -files under ci/runner/. Installation currently supports Linux x86_64 host runners. +### Shared workstation runner + +Set up the physical mini-pc once, using a consumer generated from this release: + +```sh +export GITEA_SERVER_URL=http://192.168.2.28:3000 +export RUNNER_NAME=mini-pc +export RUNNER_LABEL=mini-pc +export RUNNER_SCOPE=user +# Owner-only PAT file with permissions for the selected runner scope; never a workflow secret. +export GITEA_ADMIN_TOKEN_FILE="$HOME/.config/mini-pc-runner/admin-token" +java generated/ci/Ci.java configure-runner +java generated/ci/Ci.java install-runner +systemctl --user status mini-pc-runner +``` + +User scope uses `/api/v1/user/actions/runners/registration-token`, granting the +runner access to that user's current and future repositories. Organization and +instance scopes are optional alternatives (`RUNNER_ORGANIZATION` is required for +organization scope); instance scope requires a Gitea administrator. Repository +scope is available only for intentionally isolated runners. For this landscape, +use user scope for `wanjakrah` and the label `mini-pc`. + +The runner has one slot shared by all projects. Its identity, service and state +are based on RUNNER_NAME, independent of PROJECT_NAME: `~/.local/share/mini-pc-runner`, +`~/.config/mini-pc-runner` and `mini-pc-runner.service`. Application credentials remain +in each application's secrets; never copy deployment credentials into runner defaults. +The host runner executes trusted project code as the workstation user; use it for +repositories maintained by the trusted owner. A new project's first push needs no +new workstation registration. Ensure the workstation is awake and Docker is ready. + +For each new repository enable Actions, configure its own registry secrets and +TARGET_PLATFORM, then verify availability before pushing: + +```sh +export CI_REPOSITORY=wanjakrah/my-project +# Owner PAT with read access to the repository, user profile and user runners. +java generated/ci/Ci.java check-runner +``` + +`configure-gitea` configures the application and does not create runner registrations. +`configure-runner` obtains the one-time shared registration token; `install-runner` +installs and registers the workstation. `--prepare-only` installs without registering +or starting. Installation supports Linux x86_64. `configure-docker` uses +INSECURE_REGISTRY only for a deliberately trusted internal HTTP registry. +Runner configuration and service templates are under ci/runner/. + +When migrating a repository-scoped runner, keep the old registration and service +available until the new shared runner is online and has run a test workflow for +both an existing and a new consumer. Change all workflow labels to `mini-pc`, then +stop/disable the old service. Preserve its state outside Git for recovery. Do not +change application-specific deployment credential paths as part of this migration. ## Deployment and recovery diff --git a/SOURCE.md b/SOURCE.md index 7e5875d..007088b 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -6,3 +6,6 @@ Source commit: `bd898e92d22f6c412e671908770f4d89c8ae12bc`. Original history rema Project-specific extraction and generalization are recorded in S19. Upstream licenses and revision records accompany the resources they cover. + +On 2026-11-10, shared agent skills moved to landscape/skills/ by maintainer request. +Historical release v1.0.0 retains its original attributed snapshot unchanged. diff --git a/UPGRADING.md b/UPGRADING.md index 7419ce2..5082ad6 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,25 @@ # Engineering upgrades +## v2.0.0 + +Shared skills have moved to landscape/skills/. Upstream files and licenses are +preserved there. This is a major release because generated/skills/ is removed. +Update AGENTS.md and review companions to reference landscape's skills and retain +the project's generated/CODING_STANDARDS.md and backend-review companion. +Regeneration removes the obsolete skills automatically; never edit generated copies. + +New consumers default to the shared mini-pc runner. Existing consumers must change +all workflow runs-on labels and RUNNER_LABEL to mini-pc, add RUNNER_NAME=mini-pc and +RUNNER_SCOPE=user, and update ci/runner templates to use the shared identity. +These project-owned files are not regenerated automatically. Register the workstation +once at user scope using configure-runner, then install-runner; configure-gitea +now configures only application Actions, registry secrets and platform. Use +check-runner for each new repository before its first push. No production deployment +or migration of application deployment credentials is required. + +Java launcher contracts now use varargs and document behavior and failures. Console +messages use Java 25 IO.println. JDK 25 remains required. + ## v1.0.0 Initial release. For Agile Dancers, paths to shared Java launchers, formatting, diff --git a/VALIDATION.md b/VALIDATION.md index db18efb..6fee642 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -28,3 +28,20 @@ No production deployment was performed. Live NAS SSH connectivity, production credentials and production rollback across existing retained releases remain operational validation limits. Legacy release-state JSON remains compatible; pre-S19 retained releases use current health/settings as documented in README.md. + +## S19 review follow-up — 2026-11-10 + +- Skills relocated unchanged to landscape/skills/ with upstream revision records, + attribution and licenses. New consumers reference landscape workflows. +- Eight bootstrap/generator tests passed, including actual Maven bootstrap with + generated/ absent, all component selections and the CLI's default mini-pc label. +- Twenty-one CI tests passed, including shared runner scope/identity and preflight + rejection of missing, offline or differently labelled runners. +- Thirty-four release behavior tests passed against the actual remote shell. +- Formatter integration passed; changed Java helpers formatted with the pinned engine. +- User-scoped mini-pc runner registered and online; repository-level API runner + listings exclude inherited user runners, so preflight checks the selected scope + and verifies that the repository owner matches for user scope. + +Consumer builds and actual remote workflow results are recorded in the consuming +repositories. No production deployment is part of this follow-up. diff --git a/create-project.py b/create-project.py index 956f48e..9df369a 100644 --- a/create-project.py +++ b/create-project.py @@ -18,7 +18,7 @@ def create(destination, name, components, repository, tag, runner, landscape): if destination.exists(): raise ValueError('Destination must not exist; existing project files are never overwritten') commit = subprocess.check_output(['git', '-C', str(HOME), 'rev-parse', tag + '^{commit}'], text=True).strip() - replacements = {'@PROJECT@': name, '@RUNNER@': runner, '@LANDSCAPE@': landscape, + replacements = {'@PROJECT@': name, '@RUNNER@': runner, '@RUNNER_NAME@': runner, '@LANDSCAPE@': landscape, '@ENGINEERING@': repository.removesuffix('.git'), '@MODULES@': 'backend' if 'backend' in components else ''} try: @@ -41,6 +41,7 @@ def create(destination, name, components, repository, tag, runner, landscape): 'FORMAT_DIRECTORIES': 'backend/src', 'COMPOSE_PROJECT': name + '-production', 'BROWSER_TESTS': 'false', 'CI_DATABASE': 'false', 'TARGET_PLATFORM': 'linux/amd64', 'RUNNER_LABEL': runner, + 'RUNNER_NAME': runner, 'RUNNER_SCOPE': 'user', 'FRONTEND_TEST_COMMAND': '["npm","test"]', 'DEPENDENCY_SERVICES': '', 'REQUIRED_DEPLOY_SECRETS': ''} (destination / 'engineering.properties').write_text(''.join(k + '=' + v + '\n' for k, v in settings.items())) @@ -78,7 +79,7 @@ def main(): parser.add_argument('--components', choices=['backend', 'frontend', 'both'], default='both') parser.add_argument('--repository', default='http://192.168.2.28:3000/wanjakrah/engineering.git') parser.add_argument('--version', required=True) - parser.add_argument('--runner', required=True, help='Existing Gitea host runner label') + parser.add_argument('--runner', default='mini-pc', help='Shared Gitea host runner label (default: mini-pc)') parser.add_argument('--landscape', default='http://192.168.2.28:3000/wanjakrah/landscape') args = parser.parse_args() try: diff --git a/resources/CODING_STANDARDS.md b/resources/CODING_STANDARDS.md index aaad96e..9ec97ae 100644 --- a/resources/CODING_STANDARDS.md +++ b/resources/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/resources/ci/Ci.java b/resources/ci/Ci.java index a7b369e..bcf867a 100644 --- a/resources/ci/Ci.java +++ b/resources/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/resources/ci/CiTests.java b/resources/ci/CiTests.java index d33485f..dc44690 100644 --- a/resources/ci/CiTests.java +++ b/resources/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/resources/ci/Pipeline.java b/resources/ci/Pipeline.java index d1ac2da..bcc7a0e 100644 --- a/resources/ci/Pipeline.java +++ b/resources/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/resources/ci/Release.java b/resources/ci/Release.java index 395574c..2555ad6 100644 --- a/resources/ci/Release.java +++ b/resources/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/resources/ci/ReleaseSetup.java b/resources/ci/ReleaseSetup.java index de8fa26..bcad410 100644 --- a/resources/ci/ReleaseSetup.java +++ b/resources/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/resources/ci/ReleaseTests.java b/resources/ci/ReleaseTests.java index 0f733b2..8316030 100644 --- a/resources/ci/ReleaseTests.java +++ b/resources/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/resources/ci/Setup.java b/resources/ci/Setup.java index 0963d5b..20ca904 100644 --- a/resources/ci/Setup.java +++ b/resources/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/resources/ci/runner/WaitForDocker.java b/resources/ci/runner/WaitForDocker.java index 27c8aab..6ee9d0d 100644 --- a/resources/ci/runner/WaitForDocker.java +++ b/resources/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/resources/docs/backend-review.md b/resources/docs/backend-review.md index ff4a556..ad21580 100644 --- a/resources/docs/backend-review.md +++ b/resources/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/resources/skills/README.md b/resources/skills/README.md deleted file mode 100644 index 2e5c712..0000000 --- a/resources/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/resources/skills/adr-brainstorm/ATTRIBUTION.md b/resources/skills/adr-brainstorm/ATTRIBUTION.md deleted file mode 100644 index 1ebe60f..0000000 --- a/resources/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/resources/skills/adr-brainstorm/LICENSE.mattpocock b/resources/skills/adr-brainstorm/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/resources/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/resources/skills/adr-brainstorm/LICENSE.wanjakrah b/resources/skills/adr-brainstorm/LICENSE.wanjakrah deleted file mode 100644 index 73f2e1d..0000000 --- a/resources/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/resources/skills/adr-brainstorm/SKILL.md b/resources/skills/adr-brainstorm/SKILL.md deleted file mode 100644 index 6ef44da..0000000 --- a/resources/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/resources/skills/adr-brainstorm/adr-template.md b/resources/skills/adr-brainstorm/adr-template.md deleted file mode 100644 index 7afb962..0000000 --- a/resources/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/resources/skills/adr-brainstorm/agents/openai.yaml b/resources/skills/adr-brainstorm/agents/openai.yaml deleted file mode 100644 index 9dae6f3..0000000 --- a/resources/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/resources/skills/code-review/LICENSE.mattpocock b/resources/skills/code-review/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/resources/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/resources/skills/code-review/SKILL.md b/resources/skills/code-review/SKILL.md deleted file mode 100644 index 83183e0..0000000 --- a/resources/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/resources/skills/code-review/agents/openai.yaml b/resources/skills/code-review/agents/openai.yaml deleted file mode 100644 index 9076774..0000000 --- a/resources/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/resources/skills/grill-me/LICENSE.mattpocock b/resources/skills/grill-me/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/resources/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/resources/skills/grill-me/SKILL.md b/resources/skills/grill-me/SKILL.md deleted file mode 100644 index 3947ff9..0000000 --- a/resources/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/resources/skills/grill-me/agents/openai.yaml b/resources/skills/grill-me/agents/openai.yaml deleted file mode 100644 index 4d6fb0c..0000000 --- a/resources/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/resources/skills/grilling/LICENSE.mattpocock b/resources/skills/grilling/LICENSE.mattpocock deleted file mode 100644 index f1dd2c0..0000000 --- a/resources/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/resources/skills/grilling/SKILL.md b/resources/skills/grilling/SKILL.md deleted file mode 100644 index df69d99..0000000 --- a/resources/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/resources/skills/grilling/agents/openai.yaml b/resources/skills/grilling/agents/openai.yaml deleted file mode 100644 index ddbdb96..0000000 --- a/resources/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/resources/skills/upstream.json b/resources/skills/upstream.json deleted file mode 100644 index 67230b0..0000000 --- a/resources/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/resources/tooling/coverage/Coverage.java b/resources/tooling/coverage/Coverage.java index 86d99ed..3051bad 100644 --- a/resources/tooling/coverage/Coverage.java +++ b/resources/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/resources/tooling/format/Format.java b/resources/tooling/format/Format.java index 3413b94..f601407 100644 --- a/resources/tooling/format/Format.java +++ b/resources/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/resources/tooling/standards/src/main/java/Standards.java b/resources/tooling/standards/src/main/java/Standards.java index 213b3ad..51bbfe0 100644 --- a/resources/tooling/standards/src/main/java/Standards.java +++ b/resources/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(); diff --git a/templates/common/AGENTS.md b/templates/common/AGENTS.md index 275454d..ee2dea7 100644 --- a/templates/common/AGENTS.md +++ b/templates/common/AGENTS.md @@ -1,8 +1,11 @@ # Project guidance Read `generated/CODING_STANDARDS.md` before changing Java. -For reviews use `generated/skills/code-review/SKILL.md` with -`generated/docs/backend-review.md`. +Shared agent workflows live in landscape: @LANDSCAPE@/src/branch/main/skills/README.md. +Before a review, read landscape's skills/code-review/SKILL.md and +`generated/docs/backend-review.md`. Read any requested skill before using it. +Use grill-me only when explicitly requested, with its grilling dependency. +Keep upstream skills unchanged; change shared workflows in landscape. Specifications and ADRs: @LANDSCAPE@. Do not edit generated resources; use `mvn -N -Pregenerate-engineering validate`. -See README.md for setup and engineering upgrades. Skills are project-local. +See README.md for setup and engineering upgrades. diff --git a/templates/common/README.md b/templates/common/README.md index 7abf926..0c2fc2b 100644 --- a/templates/common/README.md +++ b/templates/common/README.md @@ -9,7 +9,9 @@ Local builds use committed generated files. Run Edit engineering.properties for module paths, images (derived from PROJECT_NAME), deployment project, required secrets and dependencies. Edit deploy/health.sh for -application readiness. Configure the workflow runner label and Gitea variables +application readiness. New projects use the shared mini-pc runner at user scope; no per-project runner +registration is needed. Verify its availability with `java generated/ci/Ci.java check-runner` +before pushing. Configure Gitea variables and secrets as described in the engineering README. Repository creation and secret installation are manual setup steps; the generator creates local files. @@ -18,3 +20,5 @@ Engineering usage and upgrades: @ENGINEERING@. Update engineering.lock.json's tag and commit together, regenerate and review the diff and that release's upgrade instructions. Project-owned files outside generated/ are never overwritten by an upgrade. + +Shared agent skills: @LANDSCAPE@/src/branch/main/skills/README.md. diff --git a/templates/common/ci/runner/runner.service b/templates/common/ci/runner/runner.service index bda8d05..12285ae 100644 --- a/templates/common/ci/runner/runner.service +++ b/templates/common/ci/runner/runner.service @@ -1,15 +1,15 @@ [Unit] -Description=@PROJECT@ Gitea Actions runner +Description=@RUNNER_NAME@ Gitea Actions runner After=network-online.target Wants=network-online.target [Service] Type=simple -WorkingDirectory=%h/.local/share/@PROJECT@-runner -EnvironmentFile=%h/.config/@PROJECT@-runner/environment -ExecStartPre=/usr/bin/env java %h/.local/share/@PROJECT@-runner/WaitForDocker.java 120 +WorkingDirectory=%h/.local/share/@RUNNER_NAME@-runner +EnvironmentFile=%h/.config/@RUNNER_NAME@-runner/environment +ExecStartPre=/usr/bin/env java %h/.local/share/@RUNNER_NAME@-runner/WaitForDocker.java 120 TimeoutStartSec=150 -ExecStart=%h/.local/bin/act_runner daemon --config %h/.local/share/@PROJECT@-runner/config.yaml +ExecStart=%h/.local/bin/act_runner daemon --config %h/.local/share/@RUNNER_NAME@-runner/config.yaml Restart=on-failure RestartSec=10 KillSignal=SIGTERM diff --git a/tests/test_generator.py b/tests/test_generator.py index 65e12e5..e0a24f6 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -35,6 +35,10 @@ class GeneratorTests(unittest.TestCase): self.assertEqual('backend' in components, (project / 'backend/pom.xml').exists()) self.assertEqual('frontend' in components, (project / 'frontend/package.json').exists()) self.assertIn('test-runner', (project / '.gitea/workflows/build.yaml').read_text()) + self.assertIn('RUNNER_SCOPE=user', (project / 'engineering.properties').read_text()) + self.assertIn('.local/share/test-runner-runner', (project / 'ci/runner/runner.service').read_text()) + self.assertIn('landscape', (project / 'AGENTS.md').read_text()) + self.assertFalse((project / 'generated/skills').exists()) shutil.rmtree(project / 'generated') result = subprocess.run(['mvn','-N','-B','-ntp','-Pregenerate-engineering','validate'], cwd=project, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180) @@ -46,6 +50,17 @@ class GeneratorTests(unittest.TestCase): subprocess.run(args, cwd=project / 'frontend', check=True, stdout=subprocess.DEVNULL) self.assertTrue((project / 'frontend/dist/index.html').exists()) + def test_cli_defaults_to_shared_mini_pc(self): + project = self.root / 'default-runner' + subprocess.run(['python3', str(self.source / 'create-project.py'), 'default-runner', str(project), + '--components', 'frontend', '--repository', str(self.source), '--version', 'v1.0.0'], + check=True, stdout=subprocess.DEVNULL) + self.assertIn('runs-on: mini-pc', (project / '.gitea/workflows/build.yaml').read_text()) + self.assertIn('RUNNER_NAME=mini-pc', (project / 'engineering.properties').read_text()) + self.assertIn('RUNNER_SCOPE=user', (project / 'engineering.properties').read_text()) + self.assertIn('.config/mini-pc-runner/environment', (project / 'ci/runner/runner.service').read_text()) + self.assertFalse((project / 'generated/skills').exists()) + def test_generator_preserves_existing_destination(self): project = self.root / 'already-exists' project.mkdir()