docs: add Gitea workflows, issue/PR templates, README and licence notice
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:
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Licence: not yet chosen
|
||||||
|
|
||||||
|
Blocker has **no licence yet**. This is deliberate, and it is recorded as ADR-17
|
||||||
|
rather than left as an oversight.
|
||||||
|
|
||||||
|
## Why nothing was applied
|
||||||
|
|
||||||
|
Applying a licence is a legal commitment, and it is the owner's to make. CLAUDE.md
|
||||||
|
section 0 rule 3 requires stopping to ask before decisions of that kind — and the
|
||||||
|
temptation to "just pick MIT" is exactly the kind of well-meant overreach that rule
|
||||||
|
exists to prevent.
|
||||||
|
|
||||||
|
There is a second reason specific to this project, and it is not a formality.
|
||||||
|
|
||||||
|
**The licence choice and the UI toolkit choice interact.** ADR-6 leaves the desktop
|
||||||
|
toolkit undecided between Slint and egui. Slint is GPL-3.0 or commercial; egui is
|
||||||
|
MIT or Apache-2.0. Choosing a permissive licence and then Slint would force the
|
||||||
|
project GPL, and discovering that after building against Slint would mean either
|
||||||
|
relicensing under copyleft or paying for a commercial licence — so the licence is
|
||||||
|
genuinely upstream of the toolkit decision, not beside it.
|
||||||
|
|
||||||
|
Until both are settled, the project stays unlicensed and unpublished.
|
||||||
|
|
||||||
|
## What this means in practice
|
||||||
|
|
||||||
|
- The code is not yet published anywhere. The repository is on a self-hosted Gitea.
|
||||||
|
- Nothing here is released, distributed, or offered to third parties.
|
||||||
|
- No licence header is asserted in any source file, because asserting one would be
|
||||||
|
asserting something untrue.
|
||||||
|
|
||||||
|
## What is needed to resolve this
|
||||||
|
|
||||||
|
1. **A licence choice.** The realistic candidates for a project intended to be
|
||||||
|
published: MIT, Apache-2.0, AGPL-3.0, or a dual licence.
|
||||||
|
2. **The UI toolkit decision** (ADR-6), which follows from it — or is decided on
|
||||||
|
measurement first, with the licence chosen to accommodate it.
|
||||||
|
|
||||||
|
## If you want to publish before this is settled
|
||||||
|
|
||||||
|
Don't. An unlicensed repository is, by default, not open source and not usable by
|
||||||
|
anyone else, and publishing it that way is a mess to unpick.
|
||||||
|
|
||||||
|
If you are in a hurry, the fastest honest path is: pick MIT or Apache-2.0 (both
|
||||||
|
permissive, both compatible with egui), then decide the toolkit with the licence
|
||||||
|
constraint known. That is a suggestion, not a decision — it is the owner's call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Recorded in `docs/DECISIONS.md` as ADR-17. Remove this file when a licence is
|
||||||
|
chosen, and add `LICENSE` with the actual terms.*
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Blocker
|
||||||
|
|
||||||
|
A cross-platform app and website blocker for **Android, Windows and Linux**.
|
||||||
|
|
||||||
|
One shared Rust core owns every decision. Platform code observes the OS and
|
||||||
|
enforces what the core decides. That is what makes the same schedule behave
|
||||||
|
identically on all three, and it is testable rather than aspirational — the golden
|
||||||
|
decision tables in `tests/` are the parity contract, so a platform disagreeing
|
||||||
|
with the core fails the build instead of producing a bug report.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**M0 — repository bootstrap.** The core engine exists and is tested; nothing
|
||||||
|
enforces anything yet. No platform has a working build.
|
||||||
|
|
||||||
|
| Milestone | State |
|
||||||
|
|---|---|
|
||||||
|
| M0 repo bootstrap | in progress |
|
||||||
|
| M1 core engine | engine, lock timers and safety guarantees written and tested; storage has no I/O yet |
|
||||||
|
| M2-Linux | not started — daemon is a placeholder |
|
||||||
|
| M3-Android | not started |
|
||||||
|
| M4 conditions | not started |
|
||||||
|
| M5 websites | not started |
|
||||||
|
| M6 Strict Mode + friction | not started |
|
||||||
|
| M7 statistics, settings, releases | not started |
|
||||||
|
|
||||||
|
Milestone order is Linux-first rather than Android-first. See ADR-15.
|
||||||
|
|
||||||
|
## What it will do
|
||||||
|
|
||||||
|
- Schedules of apps, websites and keywords, with time windows, usage limits,
|
||||||
|
launch counts, Wi-Fi and location conditions
|
||||||
|
- Blocklist and allowlist modes, with strictest-wins when schedules overlap
|
||||||
|
- QuickBlock: block a chosen set for a chosen duration
|
||||||
|
- Strict Mode and a configurable friction ladder, so turning blocking off takes
|
||||||
|
deliberate effort in a weak moment while remaining always possible
|
||||||
|
- Local usage statistics
|
||||||
|
|
||||||
|
## What it will not do
|
||||||
|
|
||||||
|
This is a friction tool, not a wall. A user with root, ADB, safe mode, a factory
|
||||||
|
reset, or desktop admin rights can always remove it, and that is intended. Factory
|
||||||
|
reset, safe mode and OS recovery are never interfered with. The goal is to outlast
|
||||||
|
an impulse, not to defeat a determined decision.
|
||||||
|
|
||||||
|
There is also a hardcoded list of things that can never be blocked — the phone and
|
||||||
|
dialler, emergency calls and alerts, and on desktop the OS shell and the Settings
|
||||||
|
basics needed to keep the machine usable. There is no setting that changes it.
|
||||||
|
|
||||||
|
See [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) for all eight platform limits,
|
||||||
|
stated plainly, plus the shortfalls in the current code.
|
||||||
|
|
||||||
|
## Build and test
|
||||||
|
|
||||||
|
Requires a stable Rust toolchain. The toolchain is pinned in `rust-toolchain.toml`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/ci.sh # everything, stops at the first failure
|
||||||
|
scripts/ci.sh --keep-going # everything, summarises all failures
|
||||||
|
cargo test --workspace # tests only
|
||||||
|
cargo build --workspace # build only
|
||||||
|
scripts/size-check.sh # the 5 MB stripped daemon gate
|
||||||
|
```
|
||||||
|
|
||||||
|
Build artifacts go to `$HOME/.blocker-target`, outside the repository, so `target/`
|
||||||
|
never shows up in `git status` and can be deleted in one command. Override with
|
||||||
|
`CARGO_TARGET_DIR`.
|
||||||
|
|
||||||
|
Two checks skip on a machine without the relevant toolchain — Android without an
|
||||||
|
SDK, the secret scan without gitleaks. Both **skip loudly** and are listed in the
|
||||||
|
summary as not-passes. A skip is never reported as a pass.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
core/ the decision engine — pure, no OS calls, no dependencies
|
||||||
|
tests/ golden decision tables, and the engine purity check
|
||||||
|
desktop/daemon/ long-lived observer and enforcer (5 MB budget)
|
||||||
|
desktop/ui/ block-screen window, launched on demand
|
||||||
|
desktop/platform-linux/ ForegroundWatcher backends, enforcement
|
||||||
|
desktop/platform-windows/ same, Windows
|
||||||
|
android/ not started
|
||||||
|
docs/ architecture, ADRs, limitations, permissions, perf, testing
|
||||||
|
scripts/ ci.sh, size-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Document | What it is for |
|
||||||
|
|---|---|
|
||||||
|
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | The purity contract, `next_wakeup`, and why the engine has no dependencies |
|
||||||
|
| [`docs/DECISIONS.md`](docs/DECISIONS.md) | 17 ADRs, including the two still awaiting a decision |
|
||||||
|
| [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md) | What the OS will not let us do, and what is not built yet |
|
||||||
|
| [`docs/PERMISSIONS.md`](docs/PERMISSIONS.md) | Every permission, and what degrades without it |
|
||||||
|
| [`docs/PERF.md`](docs/PERF.md) | Section 5 budgets, and the measurements taken so far |
|
||||||
|
| [`docs/TESTING.md`](docs/TESTING.md) | The golden-table contract and the manual checklists |
|
||||||
|
| [`CLAUDE.md`](CLAUDE.md) | The project specification. It governs. |
|
||||||
|
|
||||||
|
## Performance budgets
|
||||||
|
|
||||||
|
| Metric | Target |
|
||||||
|
|---|---|
|
||||||
|
| Idle CPU | ~0%, no wakeups without events |
|
||||||
|
| Idle RAM (daemon) | ≤ 15 MB RSS |
|
||||||
|
| Block latency | ≤ 150 ms |
|
||||||
|
| Daemon binary | ≤ 5 MB stripped |
|
||||||
|
| APK size | ≤ 10 MB |
|
||||||
|
|
||||||
|
Currently measured: **0.279 MB** stripped daemon — a placeholder that prints two
|
||||||
|
lines. The meaningful number comes in M2-Linux. Enforced in CI from the first
|
||||||
|
commit by `scripts/size-check.sh`.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
**Not yet chosen.** See ADR-17. Choosing a licence is the owner's decision, and it
|
||||||
|
interacts with the UI toolkit choice in ADR-6 (Slint is GPL-3.0 or commercial), so
|
||||||
|
it is not something to settle by accident.
|
||||||
|
|
||||||
|
## A note on origin
|
||||||
|
|
||||||
|
Blocker is a functional reimplementation of the behaviour of existing app blockers.
|
||||||
|
It reuses no third-party name, logo, icon, copy, or screenshot — only behaviour.
|
||||||
Reference in New Issue
Block a user