1 Commits
v1.0.0 ... main

40 changed files with 334 additions and 498 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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.

View File

@@ -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@': '<module>backend</module>' 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:

View File

@@ -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

View File

@@ -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 <command>. */
/** Run from the repository root with JDK 25: java generated/ci/Ci.java <command>. */
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"
+ " <path>|install-runner [--prepare-only]|deploy|rollback|release-test|configure-deployment";
}
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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.");
}

View File

@@ -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<String, String> 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 {

View File

@@ -79,12 +79,79 @@ class Setup {
exists = false;
}
api(server, admin, exists ? "PUT" : "POST", variable, Map.of("value", platform));
Map<String, Object> 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<String, String> 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<String, String> 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<String, String> env) throws Exception {
final String admin = secret(env, "GITEA_ADMIN_TOKEN_FILE");
final Map<String, Object> 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<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> response, final String label) {
for (final Object entry : (List<?>) response.getOrDefault("runners", List.of())) {
final Map<String, Object> 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<Object> 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<String, String> 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<String, String> env, String name) {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -1,20 +0,0 @@
**ID:** <insert ADR followed by a unique number> \
**Title:** <insert short and descriptive 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
<insert a list of references consulted, along with a timestamp of when it was consulted>

View File

@@ -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"

View File

@@ -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.

View File

@@ -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 <fixed-point>...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log <fixed-point>..HEAD --oneline`.
Before going further, confirm the fixed point resolves (`git rev-parse <fixed-point>`) 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.

View File

@@ -1,3 +0,0 @@
interface:
display_name: "Code Review"
short_description: "Review a diff on standards and spec"

View File

@@ -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.

View File

@@ -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".

View File

@@ -1,5 +0,0 @@
interface:
display_name: "Grill Me"
short_description: "Sharpen a plan through interview"
policy:
allow_implicit_invocation: false

View File

@@ -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.

View File

@@ -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** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
➡️ <your recommended answer>
---
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
➡️ <your recommended answer>
```
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.

View File

@@ -1,3 +0,0 @@
interface:
display_name: "Grilling"
short_description: "Stress-test thinking a round of questions at a time"

View File

@@ -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"
}
}
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -28,7 +28,13 @@ public class Standards {
static final List<Waiver> 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();

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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()