Contributing
Adding a tool
Section: Adding a tool-
Put it in
tools/if it is generic,profiles/<codename>/tools/if it encodes a vendor protocol or one silicon block. Scope it honestly — over-claiming portability is worse than scoping narrowly. -
Give it the four-field header. All four are required and enforced:
#!/bin/bash# SPDX-License-Identifier: MIT# scope: generic | soc:<soc> | device:<codename># needs: BOOTED | FASTBOOT | FROZEN | INITRAMFS | on-device | any | -# env: PHONE, TK_AGENT, ... (or `-`)# exits: 0 ok · 1 failed · 75 lock · 76 wrong state# One line saying what it does.porthole tools lintlists any gaps;make testfails on them. -
Never hardcode an IP, username, slot letter or package name. Shell:
. tools/ph-lib.sh. Python:import porthole. -
If it deliberately induces a reset, put a timeout on every ssh — otherwise it wedges the device lock for everyone else.
-
Non-trivial logic leaves one runnable check behind.
Adding a CLI verb
Section: Adding a CLI verbOne file. lib/porthole_cmd_<name>.py exporting a SPEC dict — see
docs/ARCHITECTURE.md for the shape, and any existing porthole_cmd_*.py for a
worked example. It is discovered at startup; there is no list to update, and
shell completion is generated from it automatically.
Adding a device
Section: Adding a deviceporthole new-device <codename>Fill in device.env as you learn. Every blank is a question you now know to
ask; checklist.md is the order to answer them in.
If the framework got something wrong for your device — a key that does not fit, an assumption that does not hold — that is the most valuable bug report this project can get. It has only ever been proven against one device.
Adding to the brain
Section: Adding to the brainA finding is not a trap. A trap warns about territory (“do not do X”); a finding closes a question (“X is already answered”). Findings live in brain/findings/, carry a refutes: line naming the theories they kill, and rank above everything else in search – because an answer that exists outranks a warning about the ground around it.
porthole brain new <id> --severity finding --refutes "the theory it kills"A note earns its place if it would have saved someone a session.
- One idea per note. If the title needs an “and”, it is two notes.
- Cite the evidence. A trap without a source is folklore.
- Describe the symptom, not just the cause. People search by symptom.
- Prefer
scope: generic, honestly. If it only applied to one device, scope it there. - Link with
[[note-id]]. porthole brain reindexafterwards.
Documentation
Section: DocumentationThe site is generated, never hand-edited:
cd site-app && npm ci # install the locked static-site toolchain oncecd .. && make docs # stage generated Markdown for Starlightnpm run build --prefix site-appmake docs-serve # preview at http://127.0.0.1:8000/porthole/Its CLI reference comes from the command registry, its tool catalogue from the
tool headers, its profile keys from profiles/_template/device.env, and its
knowledge base from brain/. Edit those, not the site.
site-src/, generated content, and site-app/dist/ are gitignored. A committed copy would
silently shadow the generated one and let the published docs drift from the
code.
Publishing is opt-in. CI builds the documentation on every push and pull request, but deploys to GitHub Pages only when a repository variable says to:
Settings → Secrets and variables → Actions → Variables →
PUBLISH_DOCS=true
That default exists because GitHub Pages on a private repository needs a plan that includes it. Attempting to deploy without one puts a permanent red cross on a workflow that is otherwise doing its job, which trains people to ignore CI.
Tests
Section: TestsAll of these run with no device attached:
make ci # every job GitHub runs, plus the python floormake check # lint + tests on your interpretermake ci is CI: every job in .github/workflows/ci.yml runs one of these
targets and nothing else, and tests/test_tools.py fails if a job ever grows
its own copy of the steps or names a target make ci does not reach. Green here
is green on GitHub. The individual jobs, if you want one on its own:
make test # suites, brain lint, shell lib, device mutexmake lint # shellcheck + python syntaxmake smoke # fresh clone, bare PATH, empty HOME -- tests/ci-local.shmake floor # the suite on python 3.8 in a container (needs podman)Run make ci before you claim something passes. make check compiles with
your interpreter, and if yours is newer than the declared floor it will accept
syntax CI rejects — a multi-line expression inside an f-string is PEP 701, legal
on 3.12+ and a syntax error below it. That exact thing compiled clean locally on
3.14 and broke every CI job.
The floor is declared in three places and a test asserts they agree:
bin/porthole, PY_FLOOR in the Makefile, and the CI matrix.
python3 tests/test_config.py # config resolution, legacy aliasesbash tests/test_shell_lib.sh # the same, plus shell/python agreementpython3 tests/test_cli.py # CLI verbs, JSON, exit codesbash tools/ph-device-test.sh # the device mutexporthole doctor --tools # every tool has a headerThe legacy-alias tests in the first two are the ones to be careful with. Each row corresponds to a command line printed in real documentation; breaking one breaks somebody’s muscle memory silently.
Commits
Section: CommitsAuthor and committer are the human. One logical change per commit, and the body explains why.
Contributions written with or without AI are both welcome. Two rules:
-
If you are contributing from a fork, sign off every commit (
git commit -s, orgit rebase --signoff <base>). The sign-off is your Developer Certificate of Origin: you are certifying you have the right to give us this code. CI fails a fork’s pull request commit whose author has no matchingSigned-off-by:. An AI assistant never adds one.On a branch of this repository it is not required, and CI does not ask. Only someone with write access can open one, so the certificate would be us asking ourselves about our own work. What certifies those is a maintainer reading the diff and merging, recorded by GitHub — a person looking at the diff, which is the thing a trailer never was. Patches we send upstream still carry a real sign-off from their human author at submission time;
porthole aportsenforces that one separately. -
If an AI assistant helped, say so with an
Assisted-by:trailer (for exampleAssisted-by: Claude), orGenerated-by:when it wrote nearly all of it. Nothing requires the trailer: a commit written without AI needs nothing.AI.md, at the top of the repository, says how this project itself uses AI.
Banned on commits, pull request bodies and issue bodies alike, because it
attributes the work wrongly: a Co-Authored-By:, Co-developed-by: or
Signed-off-by: naming an AI, a Claude-Session: line,
a session URL, a generated-with line. lib/porthole_trailers.py holds the one
pattern list; the commit hook rejects a message with it (it never rewrites
one), and the Commit check CI job fails a pull request whose body or
log matches it. make trailers runs the log check locally, and
python3 lib/porthole_trailers.py --dco origin/main..HEAD checks a range’s
sign-offs by hand — what you want before sending a series upstream.
A cherry-picked commit keeps its original author — git cherry-pick -x. On a
community port a lot of the early device tree is someone else’s work, and
getting this wrong is both rude and a licensing problem.
Reviewing and merging
Section: Reviewing and mergingMost of the work here is opened by an assistant, so the review is where a human enters the loop — there is no trailer standing in for one, and no script to run before merging. Reviewing the diff and pressing merge IS this project’s provenance record.
Three commands, and none of them needs a clone:
gh pr diff <N> # the whole diff, in the terminalgh pr diff <N> --patch | git apply --check # does it still apply cleanly?gh pr merge <N> --squash --delete-branch # when you are happyTo review commit by commit rather than as one diff — which is the point of
keeping them separate — open the pull request’s Commits tab and click each
one. GitHub shows that commit alone, with its own message and its own
comment threads. gh pr view <N> --web opens it.
A pull request body here states which claims were verified by execution and
which were only read (state-what-you-verified). Read that first: it tells
you which parts of the diff have evidence behind them and which are the
author’s belief, and those are the parts worth your attention.
The branch protection
Section: The branch protection.github/main-ruleset.json is the ruleset main carries: a pull request is
required, CI passed must be green, and force-pushes and deletion are
blocked. Organization admins bypass it, so a hotfix is never locked out.
It deliberately requires zero approving reviews. GitHub will not let you approve a pull request you opened, so a required-review rule would block every branch a solo maintainer pushes — a gate that cannot be satisfied is the thing this project just finished removing. The merge button is the gate; the ruleset is there to stop an accident, not to stand in for reading the diff.
Apply or update it with:
gh api -X POST repos/porthole-dev/<repo>/rulesets \ --input .github/main-ruleset.json # first timegh api -X PUT repos/porthole-dev/<repo>/rulesets/<id> \ --input .github/main-ruleset.json # to change it latergh api repos/porthole-dev/<repo>/rulesets # what is in force nowThe organization is on the free plan, which has no organization-wide rulesets, so this is applied per repository.
