docs: add Gitea workflows, issue/PR templates, README and licence notice
CI / ci (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled

ci.yml calls `scripts/ci.sh` rather than restating the checks: a workflow that
reimplements them becomes a second definition of "done", and the two drift.

Android runs as a separate job. It fails when the SDK is absent while ci.sh
skips it — a developer should not be blocked by a missing toolchain, but a
silently skipped Android build in CI means regressions ship unnoticed. Every
action is pinned to an exact version.

LICENSE.md records that no licence is chosen yet, and why it is not a formality:
the choice is upstream of the UI toolkit decision, since Slint is GPL-3.0.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:56:13 +03:00
parent 5073c7321d
commit af8b28f6c6
8 changed files with 784 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
name: Bug report
description: Something behaves differently from how CLAUDE.md section 1 describes it
title: ""
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Two failure modes matter most here, and they need different fixes.
- **A block that should have fired but did not.**
- **A block that fired but should not have.**
Please pick which one you are reporting in the first question.
- type: dropdown
id: mode
attributes:
label: Which kind of failure is this?
description: The two modes below need completely different investigations.
options:
- A blocked app was not blocked
- A block fired when it should not have
- Enforcement stopped working entirely
- The app crashed or would not start
- Something else
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Linux (KDE Plasma / Wayland)
- Linux (KDE Plasma / X11)
- Linux (other compositor)
- Windows
- Android
- More than one
- Not yet implemented on any
- type: input
id: version
attributes:
label: Version or commit
description: Output of `blocker-daemon --version`, or the commit SHA for a build from source.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: |
Concrete steps, starting from a known state. "Open Discord during Focus" is
reproducible; "it blocks sometimes" is not.
placeholder: |
1. Start a schedule named "Focus", 09:00-17:00, targeting Discord
2. Focus Discord
3. Nothing happens
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What does CLAUDE.md section 1 say should happen? Quote the requirement if you can.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behaviour
description: What happened instead, including any error output.
validations:
required: true
- type: textarea
id: backend
attributes:
label: Which enforcement backend is active?
description: |
The troubleshooting screen shows this. If you cannot find it, say so — that is
itself useful information, because it suggests the daemon started with no
backend and cannot block anything.
placeholder: "kwin: KWin script loaded successfully"
- type: textarea
id: logs
attributes:
label: Daemon log output
description: |
From `journalctl --user -u blocker -n 100`.
Redact anything private. Do not paste API tokens, keys, or credentials.
render: shell
- type: checkboxes
id: checks
attributes:
label: Before submitting
options:
- label: I searched existing issues for this.
- label: I removed secrets and personal paths from anything I pasted above.
- label: I am not asking to block ADB, safe mode, factory reset, or OS recovery.
description: |
Those are never blocked, by design (CLAUDE.md section 6.7 and ADR-11). Such a
request will be closed with a link to the ADR rather than implemented.
+102
View File
@@ -0,0 +1,102 @@
name: Platform limitation
description: A case where the OS will not permit what CLAUDE.md asks for
title: ""
labels: ["limitation"]
body:
- type: markdown
attributes:
value: |
CLAUDE.md section 6 lists the hard limits this project is known to have. This
template exists so a newly discovered one is recorded rather than quietly worked
around.
Section 0 rule 2: never fake a feature. If something cannot be enforced on a
platform, we implement the closest honest approximation and document the gap.
This form is how that documentation starts.
- type: dropdown
id: existing
attributes:
label: Is this already documented?
description: |
If it matches a numbered limitation in CLAUDE.md section 6, please comment on
that discussion instead of opening a new issue.
options:
- "No, this is new"
- "Yes, but our handling of it is wrong or incomplete"
- "Not sure"
- type: input
id: section
attributes:
label: Which section 6 limitation does this relate to?
description: "A number 1-8, or 'new'."
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Linux (KDE Plasma / Wayland)
- Linux (KDE Plasma / X11)
- Linux (wlroots compositor)
- Linux (GNOME)
- Windows
- Android
- More than one
- type: textarea
id: reality
attributes:
label: What does the OS refuse to do?
description: |
The OS-level fact. Not "we could not figure it out" — what the platform makes
impossible or requires privileges we do not have.
placeholder: |
Wayland has no portable API for the focused window. Each compositor exposes its
own, and none is standard.
validations:
required: true
- type: textarea
id: workaround
attributes:
label: What do we do instead?
description: |
The closest honest approximation. If the answer is "nothing", say that. An
unimplemented limitation is a valid and common answer.
placeholder: "Process-based detection, which cannot show a block screen."
validations:
required: true
- type: textarea
id: privileges
attributes:
label: What privileges would a real fix need?
description: |
Root, CAP_SYS_ADMIN, an accessibility service, a browser extension, a Windows
service install? This determines whether the limitation is permanent or a
matter of permission design.
validations:
required: true
- type: checkboxes
id: docs
attributes:
label: Documentation
options:
- label: This gap is documented in docs/LIMITATIONS.md
description: |
Required. An undocumented limitation is worse than an unimplemented one,
because the docs will imply a capability that does not exist.
- type: checkboxes
id: notblocked
attributes:
label: Confirm
options:
- label: |
This is not a request to block ADB, root access, safe mode, factory reset, or
OS recovery. Those are never blocked, by design (section 6.7, ADR-11).
+69
View File
@@ -0,0 +1,69 @@
name: Question
description: An open decision for the project owner, or something blocking work
title: ""
labels: ["question"]
body:
- type: markdown
attributes:
value: |
CLAUDE.md section 10.3: any open question becomes an issue labelled
"question" instead of being buried in a chat thread where it gets lost.
Most questions here are ones the agent cannot answer alone — section 0 rule 3
requires stopping to ask before adding an unlisted dependency, changing the
architecture, or choosing between two non-obvious OS mechanisms.
- type: textarea
id: question
attributes:
label: The question
description: One question. If there are several, they need separate issues.
validations:
required: true
- type: dropdown
id: kind
attributes:
label: What kind of decision is this?
options:
- Dependency to add (section 0 rule 3)
- Architecture change (section 0 rule 3)
- Choice between OS mechanisms (section 0 rule 3)
- Licensing
- Scope or priority
- Something else
- type: input
id: blocks
attributes:
label: What does this block?
description: Which milestone, and what specifically cannot proceed.
placeholder: "M2-Linux: the KWin backend cannot be written until the block-window approach is chosen."
validations:
required: true
- type: textarea
id: options
attributes:
label: Options and trade-offs
description: |
What are the realistic choices, and what does each cost? Include binary size,
licence, and maintenance burden where they apply.
validations:
required: true
- type: textarea
id: recommendation
attributes:
label: Recommendation, if you have one
description: |
Optional. A default the agent has been proceeding with, so it can keep working
while you decide. Say here if you would rather it stop and wait.
- type: textarea
id: research
attributes:
label: Supporting research
description: |
Measurements, documentation links, or a comparison. Links are more useful than
conclusions.
+88
View File
@@ -0,0 +1,88 @@
<!--
Pull request template for Blocker.
CLAUDE.md section 10.2 requires each PR to state: what changed, which section 1
requirement it covers, test evidence, and perf numbers if it touches the hot path.
Test evidence means pasted output. A claim that tests pass is not evidence; the
output is. Section 10.6: if a check fails, say so in the PR — do not weaken a test
to make it pass.
-->
## What changed
<!-- What this PR does, in a paragraph. Not a file list — the diff is the file list. -->
Refs #
## Which requirement this covers
<!--
Quote the relevant part of CLAUDE.md section 1. If this covers none, say so plainly
and explain why the change exists anyway — refactors and CI changes legitimately
cover no user-facing requirement, but that should be visible rather than assumed.
-->
Section 1:
## Test evidence
<!--
Paste the actual output of `scripts/ci.sh`. Not "all tests pass" — the output.
Note any check that SKIPPED, and why. A skip is not a pass, and saying so here is
part of the evidence.
```
$ scripts/ci.sh
...
```
## Performance
<!--
Required if this touches the hot path — the evaluator, the event loop, the block
screen, or anything that runs on every focus change (section 9).
Otherwise write "not affected" and say why.
-->
Measured against section 5:
| Metric | Target | Before | After |
|---|---|---|---|
| | | | |
Command used:
```
## Checklist
- [ ] Each commit builds and passes tests on its own (section 10.2)
- [ ] Commits are conventional (`feat:`, `fix:`, `perf:`, `refactor:`, `test:`, `docs:`, `ci:`)
- [ ] `scripts/ci.sh` passes, or every skip is explained above
- [ ] New golden decision-table cases are **added**, and behaviour changes are
matched by a case change in the same commit (the add-before-change rule)
- [ ] Every fallible OS or I/O call is handled — no `unwrap()`/`expect()` outside
tests (section 0 rule 5)
- [ ] Perf budget re-measured if the hot path was touched (section 9)
- [ ] Any platform that cannot comply has its gap documented in `docs/LIMITATIONS.md`
- [ ] No credentials, keystores, `.env` files, or local databases in the diff
- [ ] No secret, API token, SSH key, or private path appears in the diff
- [ ] No dependency added without sign-off (section 0 rule 3) and an ADR entry
## Notes for the reviewer
<!--
Anything you would flag yourself: a place you are unsure about, a shortcut taken, a
decision that needs a second opinion.
If a check fails, say so here. A failing check stated plainly is fine. A failing
check hidden by a weakened test is not, and section 10.6 prohibits it.
-->
## Do not merge this yourself
The agent does not merge its own PR (section 10.2). Squash or rebase merge, keep
history linear. Tag the milestone when it lands.
+119
View File
@@ -0,0 +1,119 @@
# Gitea Actions setup
**The owner has not yet confirmed whether a runner is registered on this Gitea.**
Until one is, `scripts/ci.sh` is the only thing gating merges, and every check must
be run locally before opening a PR.
The workflows in this directory are written for **act_runner**, Gitea's
act-compatible runner, which executes jobs in Docker. They are also close enough to
GitHub Actions syntax that a GitHub-style runner would work with the labels
adjusted.
---
## Labels these workflows expect
| File | `runs-on` | Needs |
|---|---|---|
| `ci.yml` → `ci` | `ubuntu-latest` | Rust toolchain, no Android SDK |
| `ci.yml` → `android` | `android-build` | JDK 17 + Android SDK 35 |
If your runner is registered under different labels, change `runs-on` here. Both
must be changed together — a mismatch shows up as a job that queues forever, with
no error message, which is the worst failure mode of a mislabelled runner.
---
## Registering act_runner
On the runner host, with Docker present:
```sh
# 1. Generate a registration token, in Gitea's UI:
# Site Administration → Actions → Runners → Create new runner
# Copy the token.
# 2. Register. --instance is the URL Gitea is reachable at *from the runner*.
act_runner register \
--instance https://git.valiq.vu \
--token "<REGISTRATION_TOKEN>" \
--name blocker-ci \
--labels ubuntu-latest:docker://node:20-bookworm \
--labels android-build:docker://ghcr.io/cirruslabs/android-sdk:34
# 3. Start it.
./act_runner run --server http://localhost:8080
```
Each `--labels` entry is `name:docker://image`. The runner runs the job inside that
container, so the image must contain everything the job needs:
- `ubuntu-latest` — needs `bash`, `git`, `curl`, and Rust. The workflow installs
Rust via `dtolnay/rust-toolchain`, so a Rust-free base image is fine.
- `android-build` — needs JDK 17 and the Android SDK, hence the Cirrus Labs image.
### Running act_runner as a service
```ini
# /etc/systemd/system/act_runner.service
[Unit]
Description=Gitea Actions runner
After=docker.service
Requires=docker.service
[Service]
Type=simple
User=git
WorkingDirectory=/opt/act_runner
ExecStart=/opt/act_runner/act_runner run --server http://localhost:3000 --config /opt/act_runner/config.yml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
---
## What the runner still needs from the owner
CLAUDE.md section 10.4 says to ask whether the runner has the Android SDK. **Not
yet answered.** The two behaviours are deliberately different until it is:
- `scripts/ci.sh` **skips** Android when no SDK is found, and prints SKIP with the
reason. A developer should not be blocked from running checks by a missing
toolchain.
- The `android` job in CI **fails** when the SDK is missing. A silently skipped
Android build in CI means Android regressions ship unnoticed.
Same check, opposite defaults, because the contexts are opposite. If you would
rather the CI job also skip, say so and it will be changed — but the failure mode
of the current setting is "you find out from a user", which is worse.
---
## Why the workflow calls scripts/ci.sh
`ci.yml` runs `bash scripts/ci.sh` rather than repeating the fmt/clippy/test/size
steps inline.
The reason is drift. A workflow that reimplements the checks is a second
definition of "done". The two diverge — someone adds a check locally, forgets CI,
and green ticks mean nothing. One script, two callers, no drift.
The workflow keeps separate steps for checkout, toolchain install, caching, and
artifact upload, because those are genuinely different between a runner and a
laptop.
---
## Action pinning
Every action is pinned to an exact version (`@v4.1.7`, not `@v4`, and never `@main`).
A floating tag means upstream can change what runs in your CI without a review, a
tag can be repointed, and `@main` can change arbitrarily. On a self-hosted runner
that is a supply chain problem: the code that decides whether a change is safe to
merge is code someone else controls.
Bumping a pin is its own commit, so the diff shows what changed.
+125
View File
@@ -0,0 +1,125 @@
name: CI
# Section 10.4 requires every PR to pass these checks. This workflow does not
# reimplement them: it runs `scripts/ci.sh`, which is the single source of truth
# for what must pass. Reimplementing the steps here is how CI and local checks
# drift apart, and then one of them is lying to you.
on:
push:
branches: [main]
pull_request:
# Superseded PR runs are cancelled rather than left to finish and report on a
# commit that no longer exists.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
# Matches the act_runner label in .gitea/workflows/README.md. Change both
# together if the runner is registered under a different label.
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out
uses: actions/checkout@v4.1.7
# Pinned to an exact version, never a floating tag. A floating tag lets
# upstream change what runs in your CI without review, which is a supply
# chain problem, not a convenience.
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# rust-toolchain.toml pins the channel and components, so this action
# installs exactly what the project asks for and CI matches local.
- name: Cache cargo
uses: Swatinem/rust-cache@v2.7.5
- name: Cache cargo registry
uses: actions/cache@v4.1.2
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- name: Run all checks
# scripts/ci.sh runs: fmt --check, clippy -D warnings, cargo test, the 5 MB
# size gate, Android lint (skipped loudly without an SDK), and gitleaks.
#
# A skip inside ci.sh prints SKIP with the reason and is listed in the
# summary as "NOT a pass", so an unverified Android build can never be
# mistaken for a verified one.
run: bash scripts/ci.sh
- name: Upload the release daemon
# Section 10.5 requires releases to carry SHA-256 checksums. Producing
# the checksum here means the release pipeline never has to trust an
# artifact it did not build itself.
if: always()
run: |
set -euo pipefail
BIN="${CARGO_TARGET_DIR:-$HOME/.blocker-target}/release/blocker-daemon"
mkdir -p dist
if [ -f "$BIN" ]; then
cp "$BIN" dist/blocker-daemon
sha256sum dist/blocker-daemon > dist/blocker-daemon.sha256
else
echo "release daemon not built; nothing to upload"
fi
- uses: actions/upload-artifact@v4.4.3
if: always()
with:
name: blocker-daemon-linux
path: dist/
if-no-files-found: warn
# A separate job, rather than a step, for two reasons: it needs a different
# runner label (a runner with the Android SDK), and its failure should be
# legible on its own rather than buried in the middle of the CI output.
#
# Note the asymmetry with ci.sh, which is deliberate. ci.sh SKIPS Android when
# no SDK is present, because a developer running checks locally should not be
# blocked by a missing toolchain. This job FAILS when the SDK is absent,
# because a silently skipped Android build in CI means Android regressions ship
# unnoticed. Same check, opposite defaults, for opposite contexts.
android:
runs-on: android-build
timeout-minutes: 60
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/setup-java@v4.4.0
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v3.2.2
- name: Accept SDK licences
run: yes | sdkmanager --licenses > /dev/null 2>&1 || true
- name: Cache Gradle
uses: actions/cache@v4.1.2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle-
# Sideload flavour only. There is deliberately no Play Store signing path
# in CI: sections 6.8 and 4.1 record that the strongest friction
# (Stubborn level, Device Admin uninstall block) is hostile to Play Store
# policy and is intended for sideload or F-Droid distribution.
- name: Lint and assemble release
working-directory: android
run: ./gradlew lint assembleRelease