Skip to content

Commit af1e8ef

Browse files
dpsideriusclaude
andauthored
chore: add mise toolchain, agent docs, and CI hardening (#37)
Bring the project scaffolding up to standard so both humans and agents have a single, verifiable entry point. Toolchain (.mise.toml): - Pin Node to 22.20.0 — the exact version packaging/assemble.sh bundles into the .deb, so CI now tests the runtime that actually ships to the Pi. It was testing on Node 20. - Pin zizmor and actionlint so the workflow audits are runnable locally. - 13 tasks delegating to the existing npm scripts, plus `ci` and `ci-watch`. CI: - Route the gate through `mise run ci` so local and CI cannot drift. - Add least-privilege `permissions: contents: read`, a cancel-in-progress concurrency group, `workflow_dispatch`, and `persist-credentials: false`. - Add a zizmor job auditing the workflows on every run. - release.yml: set `persist-credentials: false`; nothing there pushes with git, so the credential need not survive into the docker build step. Coverage: - Wire @vitest/coverage-v8 with per-metric floors (50/48/40/50), scoped to src/plot and src/grbl — the framework-free core. src/ui and src/transport need a DOM and a live socket, so including them would only buy a floor low enough to be meaningless. Verified the floor actually fails the build. Agent + contributor docs: - AGENTS.md as the canonical agent config, CLAUDE.md as a symlink to it. It leads with the machine-safety rules — never interrupt a running plot (there is no resume), no limit switches, inverted Z, identity axis mapping, and the open-the-port-exactly-once constraint behind the CH340 wedge. - docs/agents/issue-tracker.md, and the four missing triage labels created on the remote. Fixes found on the way: - README had an empty code block where the rollback instructions belonged. - README claimed React 18; the project is on 19. - dependabot.yml had no cooldown, so a compromised release could be proposed the day it lands. Now 7 days on both ecosystems. zizmor reports no findings and actionlint is clean, with no suppressions added. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bca16a8 commit af1e8ef

13 files changed

Lines changed: 606 additions & 29 deletions

File tree

.github/dependabot.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ updates:
66
schedule:
77
interval: weekly
88
open-pull-requests-limit: 10
9+
# Wait a week before proposing a new release: a compromised or broken
10+
# version is usually caught and yanked within days, and nothing here is
11+
# urgent enough to want a same-day bump.
12+
cooldown:
13+
default-days: 7
914
groups:
1015
# Batch routine dev-dependency bumps into one PR to cut noise.
1116
dev-dependencies:
@@ -16,3 +21,5 @@ updates:
1621
directory: "/"
1722
schedule:
1823
interval: weekly
24+
cooldown:
25+
default-days: 7

.github/workflows/ci.yml

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
11
name: CI
22

3-
# When to run: on every push to main and on every pull request.
3+
# When to run: on every push to main, on every pull request, and on demand.
44
on:
55
push:
66
branches: [main]
77
pull_request:
8+
workflow_dispatch:
9+
10+
# Least privilege: this workflow only ever reads the repo.
11+
permissions:
12+
contents: read
13+
14+
# A new push to the same branch cancels the previous, still-running check.
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
818

919
jobs:
1020
check:
21+
name: Lint, typecheck, test, build
1122
runs-on: ubuntu-latest
1223
steps:
13-
# 1. Get your code onto the runner.
24+
# 1. Get the code onto the runner.
1425
# Actions are pinned to a full commit SHA (org policy); comment tracks the version.
1526
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27+
with:
28+
# No checks here push anything, so don't leave a credential in .git/config.
29+
persist-credentials: false
1630

17-
# 2. Install Node 20 and cache npm downloads for faster runs.
18-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
31+
# 2. Install the toolchain from .mise.toml — Node is pinned there to the
32+
# same version packaging/assemble.sh bundles into the .deb, so CI tests
33+
# the runtime that actually ships.
34+
- uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
1935
with:
20-
node-version: 20
21-
cache: npm
36+
install: true
37+
cache: true
38+
39+
# 3. Cache npm downloads across runs, keyed on the lockfile.
40+
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
41+
with:
42+
path: ~/.npm
43+
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
44+
restore-keys: ${{ runner.os }}-npm-
45+
46+
# 4. Install from the lockfile (clean, reproducible).
47+
- run: mise run install
2248

23-
# 3. Install dependencies from the lockfile (clean, reproducible).
24-
- run: npm ci
49+
# 5. The gate. Identical to `mise run ci` locally — format check,
50+
# both typechecks, tests with the coverage floor, and the GUI build.
51+
- run: mise run ci
2552

26-
# 4. The same checks you run locally — if any fail, the build goes red.
27-
- run: npm run typecheck
28-
- run: npm run typecheck:node
29-
- run: npm test
30-
- run: npx prettier --check "src/**/*.{ts,tsx}" "gateway/**/*.ts"
53+
zizmor:
54+
name: Zizmor (GitHub Actions audit)
55+
runs-on: ubuntu-latest
56+
permissions:
57+
contents: read
58+
steps:
59+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
60+
with:
61+
persist-credentials: false
62+
- name: Audit GitHub Actions workflows (zizmor)
63+
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
64+
with:
65+
advanced-security: false

.github/workflows/release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ jobs:
2222
steps:
2323
# Actions pinned to a full commit SHA (org policy); comment tracks the version.
2424
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25+
with:
26+
# Nothing here pushes with git — the release upload below authenticates
27+
# with an explicit GH_TOKEN — so don't leave a credential in .git/config
28+
# where the docker build step could pick it up.
29+
persist-credentials: false
2530

2631
# The package version comes from package.json (nfpm ${SEMVER}); keep the tag
2732
# in lockstep so the Release, the .deb, and the in-app version banner agree.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dist
33
dist-gateway
44
build
55
dist-deb
6+
coverage
67
*.local
78
.DS_Store
89
gateway/.plotter-state.json

.mise.toml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# mise: https://mise.jdx.dev/
2+
#
3+
# Single source of truth for the toolchain and the task list. CI runs the same
4+
# `mise run ci` gate (see .github/workflows/ci.yml), so local and CI cannot drift.
5+
#
6+
# Node is pinned to the exact version packaging/assemble.sh bundles into the
7+
# .deb, so what CI tests is the runtime that actually ships to the Pi. If you
8+
# bump NODE_VERSION there, bump it here too.
9+
10+
[tools]
11+
node = "22.20.0"
12+
zizmor = "1.29.0" # GitHub Actions security audit
13+
actionlint = "1.7.12" # GitHub Actions schema + expression lint
14+
15+
[tasks.install]
16+
description = "Install dependencies from the lockfile"
17+
run = "npm ci"
18+
19+
[tasks.dev]
20+
description = "Vite dev server for UI work on :5173"
21+
run = "npm run dev"
22+
23+
[tasks.gateway]
24+
description = "Run the plotter gateway daemon on :8717"
25+
run = "npm run gateway"
26+
27+
[tasks.build]
28+
description = "Typecheck + build the GUI into dist/"
29+
run = "npm run build"
30+
31+
[tasks.test]
32+
description = "Run the unit tests, enforcing the coverage floor"
33+
run = "npm test"
34+
35+
[tasks.typecheck]
36+
description = "Type-check the browser sources"
37+
run = "npm run typecheck"
38+
39+
[tasks.typecheck-node]
40+
description = "Type-check the gateway sources"
41+
run = "npm run typecheck:node"
42+
43+
[tasks.format]
44+
description = "Format with Prettier"
45+
run = "npm run format"
46+
47+
[tasks.format-check]
48+
description = "Check formatting without rewriting files"
49+
run = "npm run format:check"
50+
51+
[tasks.audit]
52+
description = "Security-audit the workflows + dependabot config (zizmor)"
53+
# The token enables the online audits (impostor-commit, known-vulnerable-actions).
54+
# Without one zizmor falls back to offline and those two simply do not run.
55+
run = "GH_TOKEN=$(gh auth token 2>/dev/null) zizmor --collect=all --strict-collection ."
56+
57+
[tasks.lint-actions]
58+
description = "Lint the workflows for schema + expression errors (actionlint)"
59+
run = "actionlint"
60+
61+
[tasks.ci]
62+
description = "Full CI gate — exactly what CI runs"
63+
depends = ["format-check", "typecheck", "typecheck-node", "test", "build"]
64+
65+
[tasks.ci-watch]
66+
description = "Watch GitHub Actions CI for the current branch; exits non-zero on failure"
67+
run = """
68+
sleep 5
69+
remote_url=$(git remote get-url upstream 2>/dev/null || git remote get-url origin)
70+
repo=$(echo "$remote_url" | sed -E 's|^.*github\\.com[:/]||; s|\\.git$||')
71+
run_id=$(gh run list --repo "$repo" --branch "$(git branch --show-current)" --limit 1 --json databaseId --jq '.[0].databaseId')
72+
if [ -z "$run_id" ]; then echo "No run found for current branch on $repo" >&2; exit 1; fi
73+
gh run watch --repo "$repo" --exit-status "$run_id"
74+
"""

AGENTS.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# AGENTS.md
2+
3+
This file (`AGENTS.md`) is the canonical agent configuration. `CLAUDE.md` is a symlink to this file.
4+
5+
Browser-based control app for a GRBL-style pen plotter (a UUNA TEK 3.0 with an A0
6+
bed). A long-running gateway daemon owns the serial port and streams plots
7+
autonomously; the browser is a thin WebSocket client. See [README.md](README.md)
8+
for what it does and how to run it.
9+
10+
## Safety — this code drives a physical machine
11+
12+
These are not style preferences. Getting them wrong wastes a sheet of paper, or
13+
drives the gantry into the frame.
14+
15+
**Never interrupt a running plot.** A plot is a one-shot physical job and there is
16+
**no resume** — a client cannot re-attach to a plot it did not start. While the
17+
machine is moving (the state file's mtime is within ~3 s), do not:
18+
19+
- `systemctl restart plotter-gateway`, run `deploy.sh`, or install an update — restarting the daemon aborts the plot.
20+
- Open a WebSocket to the gateway. When no client holds control, the next client to connect inherits it.
21+
- Suggest pressing **Plot**. `streamProgram` has no in-progress guard, so a second program interleaves into the running queue.
22+
23+
To check progress without touching the plot, read
24+
`/var/lib/penplotter271/.plotter-state.json` over SSH: `wpos` is the live work
25+
position, `z: 0` is pen-down (drawing), `z: 2` is pen-up (travel). That connects
26+
no client and transfers no control. Defer every fix and restart until the plot
27+
finishes.
28+
29+
**No limit switches** (`$22=0`, homing disabled). There is no `$H`. The operator
30+
sets work zero by hand at the paper's top-left corner each session. After any
31+
power cycle the restored origin can be ~1 cm off — and if it is wrong, nothing
32+
stops the machine.
33+
34+
**Machine conventions**, baked into the G-code generator:
35+
36+
- **Inverted Z:** `Z+` moves the pen **down**. Pen-down Z is positive (default `3`), pen-up is `0`.
37+
- **Origin = the paper's top-left corner**, and the SVG→G-code mapping is **identity — no Y flip**. Machine `+Y` runs physically *down* the page. Drawing fills the `+X`/`+Y` quadrant.
38+
- **The daemon opens the serial port exactly once.** Repeated reopen wedges the macOS CH340 driver (errno 22) and only a physical replug recovers it. Never add a reopen path.
39+
40+
## Architecture
41+
42+
The GRBL engine depends only on a `Transport` interface — never on Web Serial, the
43+
DOM, or React — so the same engine runs on the Pi behind a Node serial adapter.
44+
Keep that seam intact.
45+
46+
```
47+
src/grbl/ Portable GRBL protocol engine: streaming, status, alarms (no UI deps)
48+
src/transport/ The seam — Transport interface + the browser's WebSocket client
49+
src/gateway/ Shared WebSocket protocol (commands, snapshot, forwarded events)
50+
src/plot/ Pure pipeline: SVG/PNG → polylines → placement → G-code
51+
src/ui/ React app (the only DOM-aware layer)
52+
gateway/ Raspberry Pi / dev daemon
53+
```
54+
55+
`src/plot/` and `src/grbl/` are the pure, unit-tested core and the only code the
56+
coverage floor measures. New logic belongs there rather than in `src/ui/` wherever
57+
that is a real choice.
58+
59+
## Development commands
60+
61+
Use `mise`. It pins the toolchain — including Node `22.20.0`, the same version
62+
`packaging/assemble.sh` bundles into the `.deb` — and CI runs the same tasks, so
63+
local and CI cannot drift.
64+
65+
```bash
66+
mise install # install the pinned toolchain
67+
mise run install # npm ci
68+
mise run ci # the full gate: format-check, both typechecks, test, build
69+
```
70+
71+
| Task | What it does |
72+
| --- | --- |
73+
| `mise run dev` | Vite dev server for UI work on :5173 |
74+
| `mise run gateway` | Run the plotter gateway daemon on :8717 |
75+
| `mise run build` | Typecheck + build the GUI into `dist/` |
76+
| `mise run test` | Unit tests, enforcing the coverage floor |
77+
| `mise run typecheck` | Type-check the browser sources |
78+
| `mise run typecheck-node` | Type-check the gateway sources |
79+
| `mise run format` / `format-check` | Prettier write / check |
80+
| `mise run audit` | Security-audit the workflows + dependabot config (zizmor) |
81+
| `mise run lint-actions` | Lint the workflows (actionlint) |
82+
| `mise run ci-watch` | Watch the GitHub Actions run for the current branch |
83+
84+
Coverage is measured over `src/plot` and `src/grbl` only, with per-metric floors in
85+
`vite.config.ts`. Raise them as coverage improves; never lower one to make CI pass.
86+
87+
## Spec-driven changes
88+
89+
Non-trivial work goes through OpenSpec: proposals and tasks under
90+
`openspec/changes/`, capability specs under `openspec/specs/`, completed changes in
91+
`openspec/changes/archive/`. Use the `/opsx:*` skills (`propose`, `apply`, `archive`,
92+
`sync`, `explore`). There are currently no active changes.
93+
94+
Hardware-dependent tasks are **not** done when the code typechecks. Several
95+
position-restore bugs passed review and failed on the actual Pi. Leave hardware
96+
verification tasks unchecked until the operator confirms them on the machine.
97+
98+
## Agent skills
99+
100+
### Git remote
101+
102+
GitHub, via the `gh` CLI. The repo is `LAB271/labs-pen-plotter`.
103+
104+
**The maintainer handles all git operations themselves.** Do not commit, push, or
105+
open PRs unless explicitly asked in that instance.
106+
107+
### Issue tracker
108+
109+
GitHub Issues, via `gh`. See [`docs/agents/issue-tracker.md`](docs/agents/issue-tracker.md).
110+
111+
### Triage labels
112+
113+
`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See
114+
[`docs/agents/issue-tracker.md`](docs/agents/issue-tracker.md).
115+
116+
### Commits
117+
118+
[Conventional Commits](https://conventionalcommits.org/) — see
119+
[CONTRIBUTING.md](CONTRIBUTING.md). Explain *why*, not *what*: this codebase carries
120+
unusually detailed inline rationale and that is deliberate.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

0 commit comments

Comments
 (0)