Compare commits
14 Commits
0ede23fdfe
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fb08692dde | |||
| 19ac0ea563 | |||
| 2cce76f46f | |||
| 8e8e1aaa6e | |||
| 54d6aebbe5 | |||
| 2e5f6b4db0 | |||
| 57e03af2b9 | |||
| 4ed06c7e4b | |||
| ccefd1751b | |||
| 119309debf | |||
| 8dd4b207cb | |||
| 9aa0916d95 | |||
| 7622669a4c | |||
| ebfb7da987 |
@@ -1,32 +1,136 @@
|
|||||||
# Handbooks
|
# Handbooks
|
||||||
|
|
||||||
Instance-agnostic reference material for coding agents — the verbose, citable kind that belongs in
|
Instance-agnostic reference material for coding agents, in two shapes: long-form **handbooks** meant
|
||||||
a retrieval store, not in a skill.
|
for a retrieval store, and short **redirect-skills** meant to be deployed into an agent's own skill
|
||||||
|
directory. Both are authored and version-controlled here; neither is consumed directly *from* here —
|
||||||
|
a handbook isn't live until admitted to a KB, a skill isn't live until deployed to wherever it
|
||||||
|
actually runs.
|
||||||
|
|
||||||
## What goes here
|
## What goes here
|
||||||
|
|
||||||
- Public standards, style guides, and conventions that apply regardless of which box or project is
|
- **Handbooks: public standards, style guides, and conventions** that apply regardless of which box
|
||||||
running — general git operations, language style guides, CERT/MISRA-style rulebooks.
|
or project is running — general git operations, language style guides, CERT/MISRA-style rulebooks.
|
||||||
- Long-form and specific. A handbook can be as detailed as it needs to be; nothing here is loaded
|
Long-form and specific; nothing here is loaded into a model's context wholesale, so length carries
|
||||||
into a model's context wholesale, so length carries no per-turn tax.
|
no per-turn tax.
|
||||||
|
- **Redirect-skills: short, prescriptive, and specifically pointer-shaped** — their entire job is
|
||||||
|
telling an agent to verify against a live or handbook source rather than trust its own recall,
|
||||||
|
for domains that churn too fast for a stored answer to stay correct (`gdext-verify-live-docs` is
|
||||||
|
the first: gdext is pre-1.0, so the skill points at `docs.rs/gdext` and the `godot-rust` book
|
||||||
|
instead of encoding any API surface as text). **Not** a home for full procedural skills that encode
|
||||||
|
real workflow knowledge of their own (a debugging methodology, a review checklist) — those still
|
||||||
|
belong wherever they're consumed, authored there directly.
|
||||||
|
|
||||||
## What does not go here
|
## What does not go here
|
||||||
|
|
||||||
- **Anything specific to one machine or one project.** A local Gitea's branch-naming rule, this
|
- **Anything specific to one machine or one project.** A local Gitea's branch-naming rule, this
|
||||||
box's PR checklist — that's instance-bound and belongs in that project's own KB corpus instead
|
box's PR checklist — that's instance-bound and belongs in that project's own KB corpus instead
|
||||||
(`kb-engineering` for [halogen](https://pi5-16.local/mikkeli/halogen-workstation-roadmap)).
|
(the `engineering` corpus, for
|
||||||
- **Short, prescriptive procedures achievable within a context window or two.** That's a skill, not
|
[halogen](https://git.novoyuuparosk.org/mikkeli/halogen-workstation-roadmap)).
|
||||||
a handbook — it gets loaded, not retrieved. See `skills/` in the halogen-workstation-roadmap repo
|
- **Full procedural skills that aren't redirect-shaped.** Genuine workflow knowledge — a debugging
|
||||||
for the sibling convention.
|
methodology, a review checklist, anything that *is* the content rather than pointing at it — still
|
||||||
|
belongs where it's consumed: the shared, git-tracked `skills/` at halogen-workstation-roadmap's
|
||||||
|
root (OpenCode, Codex, Claude Code, via `--link`), or authored directly by an agent into its own
|
||||||
|
runtime skill directory (hermes' per-profile home isn't git-tracked at all).
|
||||||
- **The retrieval store itself.** This repo holds the source-of-truth markdown a human has vouched
|
- **The retrieval store itself.** This repo holds the source-of-truth markdown a human has vouched
|
||||||
for. The chunked, embedded index built from it is derived and rebuildable state — it lives beside
|
for. The chunked, embedded index built from it is derived and rebuildable state — it lives beside
|
||||||
whichever KB is consuming it, never committed here.
|
whichever KB is consuming it, never committed here.
|
||||||
|
- **The deployed copy of a skill.** This repo holds the authored source; the running copy lives
|
||||||
|
wherever the consuming agent actually reads skills from, deployed by copy (never symlink — an
|
||||||
|
agent may write back to its own skill files, which a symlink would corrupt at the source).
|
||||||
|
Deployment tooling lives with the consumer, not here.
|
||||||
|
|
||||||
## Format
|
## Format
|
||||||
|
|
||||||
One markdown file per topic at the repo root. Each file should stand as a single admittable
|
**Handbooks:** one markdown file per topic under `handbooks/`. Each file should stand as a single
|
||||||
document — the same shape a human vouches for at KB admission time.
|
admittable document — the same shape a human vouches for at KB admission time. (Redirect-skills live
|
||||||
|
under `skills/` instead, with their own format — see below.)
|
||||||
|
|
||||||
|
Each file opens with a small flat YAML frontmatter block — `title` and `collections` only, no nested
|
||||||
|
structure, so it stays greppable without a YAML dependency (same reasoning `index.py` itself gives
|
||||||
|
for hand-parsing `corpora.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: Git operations
|
||||||
|
collections: [handbooks, dev-practice]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
`title` and `collections` are the two ingest-time values that are actually about the document, so
|
||||||
|
they live with it instead of being retyped by hand at admission time — that's what let `--collection
|
||||||
|
dev-practice` and an actual admission of `handbooks` drift apart on 2026-08-22, silently, until it was
|
||||||
|
noticed by inspection rather than by anything catching it.
|
||||||
|
|
||||||
|
`version` is deliberately **not** a frontmatter field — a file can't contain the hash of the commit
|
||||||
|
that writes it, the same self-reference problem any `__COMMIT__` build stamp runs into. It stays a
|
||||||
|
value computed at admission time, same as before, just scoped to the file rather than the whole repo.
|
||||||
|
|
||||||
|
## Ingesting a handbook
|
||||||
|
|
||||||
|
Markdown needs no conversion and no extraction verifier — admit it straight from this repo, reading
|
||||||
|
`title`/`collections` from the frontmatter instead of retyping them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
F=~/dev/handbooks/handbooks/git-operations.md
|
||||||
|
TITLE=$(sed -n '/^---$/,/^---$/{/^title:/s/^title:[[:space:]]*//p}' "$F")
|
||||||
|
COLLECTIONS=$(sed -n '/^---$/,/^---$/{/^collections:/s/^collections:[[:space:]]*//p}' "$F" \
|
||||||
|
| tr -d '[]' | tr ',' '\n' | sed 's/^ *//;s/ *$//')
|
||||||
|
VERSION=$(git -C ~/dev/handbooks log -1 --format=%h -- "$F")
|
||||||
|
|
||||||
|
index.py admit "$F" --corpus engineering --vouch mikkeli \
|
||||||
|
--title "$TITLE" --version "$VERSION" \
|
||||||
|
$(for c in $COLLECTIONS; do printf -- '--collection %s ' "$c"; done)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠ **`--version` is the last commit that touched *this file*, not the repo's `HEAD`.** A whole-repo
|
||||||
|
`HEAD` pin would bump every handbook's recorded version on any unrelated edit, even one that never
|
||||||
|
touched this document — the per-file form is what actually answers "which revision does this chunk
|
||||||
|
reflect."
|
||||||
|
|
||||||
|
⚠ **Re-admitting replaces a document outright, with no version history kept.** The `--version` pin is
|
||||||
|
the only record of which revision a chunk reflects, which is also why editing a file here does not by
|
||||||
|
itself update the corpus — re-admission is deliberate, a fresh claim about a specific version of a
|
||||||
|
document, not something that should happen automatically on every commit.
|
||||||
|
|
||||||
|
## Redirect-skills
|
||||||
|
|
||||||
|
Live under `skills/<name>/SKILL.md` — the same `skills/<name>/SKILL.md` shape every consuming agent
|
||||||
|
already expects, so a skill here copies straight across with no reshaping. Frontmatter is `name` +
|
||||||
|
`description` (the Agent Skills standard, not this repo's `title`/`collections`): `description` is
|
||||||
|
what a consuming agent's own trigger-matching reads to decide relevance, so it needs to name the
|
||||||
|
concrete situation the skill applies to, not just summarize the body.
|
||||||
|
|
||||||
|
A redirect-skill's body should contain no cacheable domain knowledge — only what to check, where, and
|
||||||
|
under what condition to suspect staleness over a code error. If a skill here starts accumulating API
|
||||||
|
details "for next time," that's the sign it's stopped being a redirect and started being the exact
|
||||||
|
stale cache it exists to avoid.
|
||||||
|
|
||||||
|
**Deploying one is out of scope for this repo** — copy the file into wherever the consuming agent
|
||||||
|
reads skills from, never symlink (an agent may write back to its own skill files over time, which a
|
||||||
|
symlink would corrupt at the vendor source). The deployment tooling itself lives with the consumer.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Started 2026-08-21. First handbook: general git operations.
|
Started 2026-08-21. First handbook, `git-operations.md`, composed 2026-08-21 – 2026-08-22: branching,
|
||||||
|
committing, merging vs. rebasing, reverting and undoing, remotes and collaboration, tags and releases.
|
||||||
|
Confirmed ingestible 2026-08-22 — 30 chunks, heading paths intact, retrieved correctly.
|
||||||
|
|
||||||
|
Second handbook, `failure-handling.md`, composed 2026-08-22: input contract discipline, failure-
|
||||||
|
propagation discipline. Environment/verification parity was scoped out of it deliberately — a
|
||||||
|
different axis (how confidence is established, not how code behaves), queued as a handbook of its
|
||||||
|
own rather than folded in.
|
||||||
|
|
||||||
|
Frontmatter (`title`, `collections`) added to both 2026-08-22, after `--collection dev-practice` in
|
||||||
|
this file's own recipe was found to have drifted from `handbooks`, the value actually used at
|
||||||
|
admission — the two ingest-time values that describe the document now live with it instead of being
|
||||||
|
retyped by hand.
|
||||||
|
|
||||||
|
Scope widened 2026-08-22, superseding the same-day note above: this repo now also houses
|
||||||
|
**redirect-skills** (`skills/<name>/SKILL.md`) — short, pointer-shaped skills whose job is verifying
|
||||||
|
against a live or handbook source rather than caching an answer that would go stale. First one:
|
||||||
|
`gdext-verify-live-docs`, for `gdext` (Rust bindings for Godot 4, pre-1.0, no existing skill or MCP
|
||||||
|
tool anywhere). Full procedural skills — genuine workflow knowledge, not a pointer — still don't
|
||||||
|
belong here; they're authored where they're consumed, same as before.
|
||||||
|
|
||||||
|
Handbooks moved from the repo root into `handbooks/` the same day, for symmetry with `skills/` —
|
||||||
|
each content shape gets its own directory rather than handbooks implicitly owning the root.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# Git operations
|
|
||||||
|
|
||||||
> Draft outline — 2026-08-21. General practice, not tied to any one project's Gitea/GitHub setup.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
General git operations a coding agent should get right without being told each time: branching,
|
|
||||||
committing, merging vs. rebasing, and how to recover from the common ways each goes wrong. Anything
|
|
||||||
specific to one project's hosting setup (branch protection, PR template, CI triggers) belongs in
|
|
||||||
that project's own KB corpus instead — see this repo's README.
|
|
||||||
|
|
||||||
## Outline
|
|
||||||
|
|
||||||
- Branching
|
|
||||||
- naming conventions and when a new branch is warranted vs. not
|
|
||||||
- long-lived vs. short-lived branches
|
|
||||||
- Committing
|
|
||||||
- what makes a commit atomic; when to split or squash
|
|
||||||
- message conventions and what a good message actually needs to say
|
|
||||||
- Merging vs. rebasing
|
|
||||||
- when each is appropriate, and the history each produces
|
|
||||||
- conflict resolution — reading a conflict marker correctly, not guessing
|
|
||||||
- Reverting and undoing
|
|
||||||
- revert vs. reset vs. checkout — what each does to shared history
|
|
||||||
- recovering from a bad rebase or reset (reflog)
|
|
||||||
- Remotes and collaboration
|
|
||||||
- fetch vs. pull, and why the distinction matters under contention
|
|
||||||
- force-push: when it's ever appropriate, and how to do it without destroying someone else's work
|
|
||||||
- Tags and releases
|
|
||||||
- lightweight vs. annotated tags
|
|
||||||
- what a tag should mean once it's pushed
|
|
||||||
|
|
||||||
*(Fill in as it's composed. Each section should end up citable on its own — a chunk boundary, not
|
|
||||||
just a heading.)*
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
title: Failure handling
|
||||||
|
collections: [handbooks, dev-practice]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Failure handling
|
||||||
|
|
||||||
|
> Composed 2026-08-22. General practice, not tied to any one project or language.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
General discipline for how code should behave when it encounters conditions it can't cleanly
|
||||||
|
complete against — malformed input, an unreachable dependency, an assumption that turns out false.
|
||||||
|
Two sections: what a function's contract should say about the input space before it's written, and
|
||||||
|
what happens once something has actually gone wrong.
|
||||||
|
|
||||||
|
Excluded on purpose: language-specific exception syntax, logging infrastructure choices,
|
||||||
|
testing-framework mechanics, and environment/verification parity ("tested" vs. "tested where it
|
||||||
|
actually runs") — that last one is close enough to earn a future handbook of its own rather than a
|
||||||
|
section here.
|
||||||
|
|
||||||
|
## Input contract discipline
|
||||||
|
|
||||||
|
**Degenerate input is the default case to design for, not an edge case bolted on after.** Empty
|
||||||
|
collections, duplicate entries, out-of-range values, malformed or wrong-typed data, oversized
|
||||||
|
payloads, and concurrently-modified reads are not rare — they're what actually arrives once code
|
||||||
|
leaves a demo. Design for that space first, and treat "obviously fine" input as the special case, not
|
||||||
|
the other way around.
|
||||||
|
|
||||||
|
**Validate, sanitize, and reject are three different responses — not one blurred behavior.**
|
||||||
|
|
||||||
|
- **Validate** — check conformance against the contract, making no change to the input. Proceed only
|
||||||
|
if it holds.
|
||||||
|
- **Sanitize** — transform nonconforming input into conforming input. The transformed value is what
|
||||||
|
proceeds, treated exactly as if the caller had sent it that way to begin with.
|
||||||
|
- **Reject** — refuse to proceed, surfacing why (see Failure-propagation discipline for what
|
||||||
|
"surfacing why" should actually look like).
|
||||||
|
|
||||||
|
**Sanitize only what's meaning-preserving; reject anything that requires guessing intent.**
|
||||||
|
Sanitizing has a real cost: the caller's actual input is never seen again, only its cleaned-up
|
||||||
|
substitute. That's fine when the correction is unambiguous — trimming incidental whitespace,
|
||||||
|
normalizing Unicode form, case-folding under an already-documented case-insensitive contract. It's
|
||||||
|
not fine when the correction requires guessing what the caller meant — clamping an out-of-range
|
||||||
|
number to its nearest valid bound, silently truncating an oversized value, defaulting a missing
|
||||||
|
required field. The test: *would the caller be surprised to learn what actually happened to their
|
||||||
|
input?* If yes, reject instead of sanitizing — finding out via an error immediately is strictly
|
||||||
|
better than finding out weeks later that a value was quietly substituted for the one actually sent.
|
||||||
|
Silent sanitization of an out-of-range value and a guard that returns success without checking are
|
||||||
|
the same failure shape: something couldn't proceed correctly, and produced a plausible-looking result
|
||||||
|
instead of saying so.
|
||||||
|
|
||||||
|
**Validate at trust boundaries; trust your own internals.** Check input where it crosses from outside
|
||||||
|
your control into it — user input, an external API response, a file read from disk, a message from
|
||||||
|
another service. Code on the inside of that boundary, already known to satisfy its own contract (by
|
||||||
|
type, by test, by the caller being code under the same control), doesn't need to re-validate what its
|
||||||
|
caller already guaranteed — that's redundant defense that doubles maintenance cost without doubling
|
||||||
|
safety. The boundary is where trust actually changes; that's the only place validation earns what it
|
||||||
|
costs.
|
||||||
|
|
||||||
|
**Before writing the implementation, write the contract's answer for each category.** A function's
|
||||||
|
contract should state, explicitly, what happens for: empty input, a duplicate where uniqueness was
|
||||||
|
assumed, a value outside the expected range, a type mismatch, an oversized input, and a stale or
|
||||||
|
concurrently-modified read. Leave any of those unanswered, and the implementation will invent an
|
||||||
|
answer under latitude — usually the same way, by quietly doing something plausible instead of
|
||||||
|
refusing. Writing the answer down first is what closes that gap; discovering it during implementation,
|
||||||
|
or worse in production, is what leaves it open.
|
||||||
|
|
||||||
|
## Failure-propagation discipline
|
||||||
|
|
||||||
|
**Never substitute a plausible-looking artifact for a failed operation.** When something can't
|
||||||
|
complete its contract, the correct output is a signal that it didn't — not a value that resembles
|
||||||
|
success closely enough to pass unnoticed. A blank result where real output should be, a guard that
|
||||||
|
reports "fine" because it couldn't read what it was checking, a stale cached value returned as if it
|
||||||
|
were fresh: each of these is a failure wearing success's clothes, and the cost is always paid later,
|
||||||
|
by whoever trusted the output.
|
||||||
|
|
||||||
|
**A check that can't verify must refuse — and "couldn't verify" must be a different state than
|
||||||
|
"verified, and it's fine."** Collapsing both into one boolean is the mechanism behind the failure
|
||||||
|
above: a function that returns `true` for both "the input passed validation" and "validation itself
|
||||||
|
couldn't run" cannot be trusted by anything downstream, because the caller has no way to tell which
|
||||||
|
one happened. Whatever the check returns, its failure-to-run and its pass result need to be
|
||||||
|
distinguishable — a third state, a different return type, an exception path, anything that doesn't
|
||||||
|
require the caller to assume good faith on the checker's behalf.
|
||||||
|
|
||||||
|
**Retry, abort, and surface are three different responses to a failure, and picking the wrong one is
|
||||||
|
a fail-loudly violation on its own.**
|
||||||
|
|
||||||
|
- **Retry** only for failures that are plausibly transient and where repeating the operation doesn't
|
||||||
|
change its meaning (a network read, not a payment). Bound it — a fixed number of attempts or a
|
||||||
|
timeout, never unconditional — and don't let a bounded retry become an unbounded one by accident (a
|
||||||
|
retry nested inside another retry, a caller that itself retries a function that already retries
|
||||||
|
internally).
|
||||||
|
- **Abort** when the failure indicates a broken invariant or a state that's unsafe to continue from —
|
||||||
|
retrying or asking would just compound whatever's already wrong.
|
||||||
|
- **Surface** — to a caller, a human, or an error return — when the system doesn't have enough
|
||||||
|
information to pick correctly on its own. This is usually the right default when nothing above
|
||||||
|
clearly applies: asking is cheaper than guessing wrong.
|
||||||
|
|
||||||
|
**A retry that eventually succeeds is still a failure that happened, and needs to be recorded as one,
|
||||||
|
not discarded because the second attempt worked.** The caller downstream can get the successful
|
||||||
|
result — that's the point of retrying — but "the first attempt failed" is real information: it's how
|
||||||
|
a dependency's slow slide toward actually breaking becomes visible before it breaks outright, instead
|
||||||
|
of only being visible in the moment it finally does. A retry policy that only speaks up once retries
|
||||||
|
are exhausted throws that signal away for free.
|
||||||
|
|
||||||
|
**Pick one failure-signaling convention per boundary, and don't mix it with another.** Exceptions, a
|
||||||
|
`Result`-shaped return, a sentinel value, an error code — any of these work, but only if every caller
|
||||||
|
at that boundary can rely on which one is in play. Mixing them within the same boundary is its own
|
||||||
|
failure mode: a caller checking a sentinel value for a function that actually raises, or catching an
|
||||||
|
exception type a function never throws, proceeds past the real failure exactly as if it hadn't
|
||||||
|
checked at all — the check existed, and did nothing.
|
||||||
|
|
||||||
|
**Anti-patterns, all variations on the same shape:**
|
||||||
|
|
||||||
|
- A swallowed exception (`except: pass`, an empty `catch` block) — the failure existed for one
|
||||||
|
instruction and then didn't.
|
||||||
|
- `continue` on a caught exception inside a loop — one bad item disappears with no record it was ever
|
||||||
|
there, and the loop reports success for everything it actually attempted.
|
||||||
|
- A fallback default that isn't marked as one — returning `0`, `null`, an empty collection, or a
|
||||||
|
stale cached value in place of a failed real result, with nothing distinguishing it from a genuine
|
||||||
|
`0`, `null`, empty collection, or fresh value.
|
||||||
|
- A comment explaining away a shortcut instead of the code being fixed — `# TODO: handle this
|
||||||
|
properly` sitting next to the exact silent failure it describes, left in place.
|
||||||
|
- An unbounded retry — functionally identical to abort, except it costs resources first and never
|
||||||
|
actually informs anyone that it gave up.
|
||||||
|
|
||||||
|
The same discipline shows up in test coverage: a suite that only exercises the happy path hasn't
|
||||||
|
tested the code, it's tested the narrative about the code.
|
||||||
|
|
||||||
|
*(Handbook composed 2026-08-22. Both sections above stand as citable units — a chunk boundary, not
|
||||||
|
just a heading.)*
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
---
|
||||||
|
title: Git operations
|
||||||
|
collections: [handbooks, dev-practice]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git operations
|
||||||
|
|
||||||
|
> Composed 2026-08-21 – 2026-08-22. General practice, not tied to any one project's Gitea/GitHub
|
||||||
|
> setup.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
General git operations a coding agent should get right without being told each time: branching,
|
||||||
|
committing, merging vs. rebasing, and how to recover from the common ways each goes wrong. Anything
|
||||||
|
specific to one project's hosting setup (branch protection, PR template, CI triggers) belongs in
|
||||||
|
that project's own KB corpus instead — see this repo's README.
|
||||||
|
|
||||||
|
## Branching
|
||||||
|
|
||||||
|
**Model: trunk-based (GitHub flow).** One long-lived integration branch carries the project's
|
||||||
|
history; everything else is a short-lived branch cut from it and merged straight back in. No
|
||||||
|
`develop`, no `release/*`, no `hotfix/*` — those exist to support scheduled, multi-version releases,
|
||||||
|
which most agent-driven work doesn't need. A project that genuinely needs staged releases is a
|
||||||
|
deviation from this default, and belongs in that project's own KB corpus, not here.
|
||||||
|
|
||||||
|
**Trunk name: `master`**, by convention across these instances — not `main`. This is a default for
|
||||||
|
*new* repos, not a mandate to rename an existing one: if a repo already uses `main` (or anything
|
||||||
|
else), work with what's there. Never rename a repo's default branch as an incidental part of
|
||||||
|
unrelated work.
|
||||||
|
|
||||||
|
**Repo name: hyphens, not underscores** (`my-project`, not `my_project`) — for new repos, same
|
||||||
|
caveat as trunk naming: don't rename an existing one over this.
|
||||||
|
|
||||||
|
**Naming scheme: `type/short-description`.** A type prefix from the taxonomy below, then a
|
||||||
|
kebab-case description — `feat/branch-naming-rules`, `fix/null-check-on-login`. No ticket ID in the
|
||||||
|
name: a branch name should say what the change does, and if a tracker ticket exists, that reference
|
||||||
|
belongs in the commit body or the PR, not the branch name. No author namespace either — branch names
|
||||||
|
describe the change, not who's making it; `git log`/`git blame` already answer "who."
|
||||||
|
|
||||||
|
**Type taxonomy — mirrors Conventional Commits**, so branch prefixes and commit-message prefixes
|
||||||
|
share one vocabulary:
|
||||||
|
|
||||||
|
- `feat` — new capability
|
||||||
|
- `fix` — bug fix
|
||||||
|
- `docs` — documentation only
|
||||||
|
- `refactor` — restructuring with no behavior change
|
||||||
|
- `test` — tests only
|
||||||
|
- `chore` — maintenance (deps, tooling, config) with no product-facing effect
|
||||||
|
- `ci` — CI/CD pipeline changes
|
||||||
|
|
||||||
|
The one ambiguous split is `refactor` vs. `chore`: if the change touches source that ships, it's
|
||||||
|
`refactor`; if it touches only the surrounding machinery (build config, lint rules, dependency
|
||||||
|
bumps), it's `chore`.
|
||||||
|
|
||||||
|
**When a branch is not warranted.** Trivial, low-risk changes — a typo fix, a corrected newline, a
|
||||||
|
single-line doc or config edit — can be committed straight to trunk: `git commit`, then `git push`.
|
||||||
|
Don't check branch-protection status first; just attempt the push. A rejection has two different
|
||||||
|
causes, and they call for different responses:
|
||||||
|
|
||||||
|
- **Protected branch, or no direct push permission at all** (increasingly the default for an agent
|
||||||
|
identity) — this *is* the signal to fall back to branching normally (`git switch -c fix/...` —
|
||||||
|
`git checkout -b` is equivalent — then push, open a PR) and carry on as if a branch had been used
|
||||||
|
from the start.
|
||||||
|
- **A plain non-fast-forward rejection** — trunk moved since your last fetch, nothing to do with
|
||||||
|
permission. `git fetch && git rebase origin/master`, then push again, is enough on its own;
|
||||||
|
branching is still a safe fallback if a second collision seems likely, just not the only correct
|
||||||
|
response to this one.
|
||||||
|
|
||||||
|
Anything past the trivial bar — any actual logic or behavior change, however small — branches from
|
||||||
|
the outset regardless of which rejection would apply; don't wait for a push rejection to decide.
|
||||||
|
|
||||||
|
**Branch lifetime.** No fixed day limit — the right threshold varies too much by project pace to be
|
||||||
|
universal — but watch for the smell: a branch that's drifted far enough from trunk that merging it
|
||||||
|
is no longer a small diff, that's grown hard to review as one unit, or that's now blocking someone
|
||||||
|
else's work. When any of those show up, don't just keep adding commits to it — merge what's ready,
|
||||||
|
split the remainder into its own branch, or rebase onto current trunk before continuing.
|
||||||
|
|
||||||
|
## Committing
|
||||||
|
|
||||||
|
**Merges preserve the branch's commits as-is** (see Merging vs. rebasing) — there is no squash-merge
|
||||||
|
collapsing everything into one commit at the end. That makes the commits on a branch, not just the
|
||||||
|
diff they sum to, part of what a reviewer reads and what lands permanently on trunk. Commit
|
||||||
|
discipline is therefore not optional cleanup; it's the actual shape of the history.
|
||||||
|
|
||||||
|
**Atomic commits.** One logical change per commit — the smallest unit that still makes sense on its
|
||||||
|
own if someone had to revert, cherry-pick, or `git bisect` to just that commit. A commit that mixes
|
||||||
|
an unrelated formatting pass into a bug fix, or bundles two unrelated fixes because they happened to
|
||||||
|
touch the same file, isn't atomic even if it's small. Size isn't the test; independence is — could
|
||||||
|
this commit be reverted alone without dragging along something unrelated?
|
||||||
|
|
||||||
|
**Split and clean up before merge, not after.** Since a branch's commits ride straight onto trunk,
|
||||||
|
in-progress noise (`wip`, `fix typo`, `actually fix it`) should never reach it. Use
|
||||||
|
`git commit --fixup=<sha>` while working, then `git rebase -i --autosquash` before pushing for review
|
||||||
|
to fold those into the commit they actually belong to. If a single commit turns out to bundle two
|
||||||
|
unrelated changes, split it with `git reset` (or interactive rebase + `git add -p`) before merge —
|
||||||
|
don't leave that untangling for whoever reads the history later.
|
||||||
|
|
||||||
|
**Message format: a type prefix is recommended, not enforced.** Same taxonomy as branch names —
|
||||||
|
`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `ci:` — because reusing one vocabulary
|
||||||
|
beats maintaining two. But don't force a change into a type that doesn't fit; a plain descriptive
|
||||||
|
subject with no prefix is better than a mislabeled one. Subject line in imperative mood — "Reject
|
||||||
|
expired tokens", not "Rejected expired tokens" or "Rejects expired tokens" — as if finishing the
|
||||||
|
sentence "If applied, this commit will...".
|
||||||
|
|
||||||
|
**Body: add one whenever there's a "why" worth keeping**, not as a mandatory second paragraph on
|
||||||
|
every commit. A subject that would have to run long to say what's needed is one trigger; just as
|
||||||
|
common is a subject that's already a clean, short label but leaves the motivation unstated — the
|
||||||
|
root cause behind a fix, why an approach was chosen over an obvious alternative. Either case gets a
|
||||||
|
body; its job is to carry what the diff itself can't, not to re-narrate the diff line by line.
|
||||||
|
|
||||||
|
## Merging vs. rebasing
|
||||||
|
|
||||||
|
**Two different jobs, not competing techniques for the same one.** Rebase is for keeping a branch
|
||||||
|
current while it's still being worked on; merge is for the one-time act of landing a finished branch
|
||||||
|
back onto trunk. A fast-forward integration would still keep every individual commit, but it would
|
||||||
|
lose the durable marker `--no-ff` provides: the merge commit itself, which is what makes a unit of
|
||||||
|
work walkable later via `git log --first-parent` or revertible as a whole via `git revert -m 1`
|
||||||
|
(see Branching, Committing). Using merge to catch a branch up mid-flight, in the other direction,
|
||||||
|
buries trunk's incoming commits as extra noise inside the branch's own history instead.
|
||||||
|
|
||||||
|
**Staying current while in progress: rebase onto trunk.**
|
||||||
|
|
||||||
|
```
|
||||||
|
git fetch origin
|
||||||
|
git rebase origin/master
|
||||||
|
# resolve conflicts if any
|
||||||
|
git push --force-with-lease
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebase rewrites the branch's commits (new SHAs), which is why the follow-up push needs
|
||||||
|
`--force-with-lease`, never plain `--force` (see Remotes and collaboration). This is safe as long as
|
||||||
|
the branch is still exclusively yours. **The moment a branch is shared** — someone else has fetched
|
||||||
|
it and built on top — rebasing it out from under them corrupts their work; merge trunk into the
|
||||||
|
branch instead in that case (`git merge origin/master`), and accept the extra merge-commit noise as
|
||||||
|
the price of not rewriting history someone else is depending on.
|
||||||
|
|
||||||
|
**Landing on trunk: merge --no-ff.**
|
||||||
|
|
||||||
|
```
|
||||||
|
git switch master # checkout: git checkout master
|
||||||
|
git merge --no-ff branch-name
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces a merge commit and keeps the branch's own commits intact and visible as one unit in
|
||||||
|
trunk's history — the payoff for the atomic, preserved-as-is commits Committing asks for: the merge
|
||||||
|
commit is what ties them back to a single piece of work.
|
||||||
|
|
||||||
|
**Reading a conflict, not guessing.** A conflict marker block:
|
||||||
|
|
||||||
|
```
|
||||||
|
<<<<<<< HEAD
|
||||||
|
(current branch's version)
|
||||||
|
=======
|
||||||
|
(incoming version)
|
||||||
|
>>>>>>> branch-name
|
||||||
|
```
|
||||||
|
|
||||||
|
**`HEAD` always means whatever's currently checked out — never assume, check.** `<<<<<<< HEAD` is the
|
||||||
|
state you have checked out right now; the bottom half (`>>>>>>> ...`) is whatever you told git to
|
||||||
|
bring in. That produces three different results across the operations this handbook actually uses,
|
||||||
|
verified directly rather than assumed:
|
||||||
|
|
||||||
|
- **Staying current** (`git rebase origin/master` on your feature branch): rebase rebuilds trunk
|
||||||
|
forward and replays your commits onto it one at a time, so `HEAD` during a conflict is *trunk's*
|
||||||
|
content — even though you're conceptually "on" your feature branch — and the bottom half is your
|
||||||
|
original commit being replayed.
|
||||||
|
- **Catching up a shared branch** (`git merge origin/master` while on the feature branch): `HEAD` is
|
||||||
|
your feature branch's content, and the bottom half is trunk's incoming change — the opposite
|
||||||
|
polarity from the rebase case above, despite both being "sync with trunk."
|
||||||
|
- **Landing on trunk** (`git switch master && git merge --no-ff branch-name`): `HEAD` is trunk again,
|
||||||
|
and the bottom half is the branch being merged in.
|
||||||
|
|
||||||
|
There's no single "merge vs. rebase" rule that covers all three — rebase always puts the target
|
||||||
|
you're rebasing *onto* in `HEAD`; merge puts whatever you had checked out *before* running the
|
||||||
|
command, which is trunk in the landing case and the feature branch in the catch-up case. Run
|
||||||
|
`git status` before resolving — it names both sides explicitly ("rebasing branch X on Y", or "On
|
||||||
|
branch X ... You are merging Y") — and resolve by reading what each side actually contains, never by
|
||||||
|
reflexively picking one. If a resolution goes wrong partway through, `git merge --abort` or
|
||||||
|
`git rebase --abort` returns to the pre-conflict state cleanly — cheaper than untangling a bad
|
||||||
|
resolution by hand.
|
||||||
|
|
||||||
|
## Reverting and undoing
|
||||||
|
|
||||||
|
Three tools, three different scopes — the dividing line is the same one from Merging vs. rebasing:
|
||||||
|
**has this history already been shared?**
|
||||||
|
|
||||||
|
**`git revert` — safe for anything already pushed or shared.** Creates a new commit that undoes an
|
||||||
|
earlier one, without rewriting anything. History stays intact and honest: the mistake and its
|
||||||
|
correction are both visible. This is the only undo tool that's safe on trunk, or on any branch
|
||||||
|
someone else might have fetched.
|
||||||
|
|
||||||
|
```
|
||||||
|
git revert <sha> # undo a single commit
|
||||||
|
git revert -m 1 <merge-sha> # undo an entire merged unit of work (see Merging vs. rebasing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`git reset` — local, unpushed history only.** Moves the branch pointer, optionally taking the
|
||||||
|
working tree and index with it. It rewrites history the same way rebase does, so resetting anything
|
||||||
|
already shared is the same mistake as rebasing shared history — don't.
|
||||||
|
|
||||||
|
- `--soft <sha>` — move the pointer, keep changes staged. Useful for folding recent commits back
|
||||||
|
into the index to re-commit them differently.
|
||||||
|
- `--mixed <sha>` (the default) — move the pointer, unstage changes, leave them in the working tree.
|
||||||
|
- `--hard <sha>` — move the pointer and discard changes entirely. Destructive to anything not
|
||||||
|
committed elsewhere — check `git status` first, same standing rule as any other history-discarding
|
||||||
|
command.
|
||||||
|
|
||||||
|
**`git switch` / `git restore` — day-to-day, not history-rewriting.** `switch` changes which branch
|
||||||
|
`HEAD` points to; `restore` discards or unstages changes to specific files. Neither touches commit
|
||||||
|
history at all. Three of these four map onto the older, overloaded `git checkout`, which remains
|
||||||
|
valid on any git a project might be pinned to — but unstaging was never one of `checkout`'s jobs;
|
||||||
|
that was always `git reset`'s, from well before `restore` existed. The equivalents below are for
|
||||||
|
reference, not a second command set to also learn.
|
||||||
|
|
||||||
|
```
|
||||||
|
git switch -c fix/typo-readme # create + switch to a new branch (checkout: git checkout -b fix/typo-readme)
|
||||||
|
git switch master # switch back (checkout: git checkout master)
|
||||||
|
git restore --staged file.txt # unstage a file (pre-restore: git reset file.txt)
|
||||||
|
git restore file.txt # discard working-tree changes (checkout: git checkout -- file.txt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recovering from a bad rebase or reset: the reflog.** Every move of `HEAD` — including ones a
|
||||||
|
rebase or `reset --hard` just threw away — is recorded locally in the reflog, independent of branch
|
||||||
|
history.
|
||||||
|
|
||||||
|
```
|
||||||
|
git reflog # find the entry from just before the mistake
|
||||||
|
git reset --hard HEAD@{n} # or the sha shown — back to that state
|
||||||
|
```
|
||||||
|
|
||||||
|
The reflog is local only — it never leaves the machine, isn't fetched or pushed, and isn't a
|
||||||
|
substitute for backing up work by pushing it somewhere. It also expires (unreachable entries after
|
||||||
|
30 days, reachable ones after 90, by default) — a safety net for the last few minutes' mistake, not
|
||||||
|
a long-term archive.
|
||||||
|
|
||||||
|
## Remotes and collaboration
|
||||||
|
|
||||||
|
**Remote names are role signals, not just labels — and the role holds regardless of which host a
|
||||||
|
given repo happens to point them at.** A repo can carry up to three:
|
||||||
|
|
||||||
|
- **`origin` — the canonical remote.** Every repo has one; it's where actual work happens and where
|
||||||
|
every rule from the sections above (protected trunk, `--no-ff` merges, rebase-before-merge) applies
|
||||||
|
at full strength. On a repo you authored, it's where the repo was created. On a borrowed repo (next
|
||||||
|
bullet), it's still the canonical one — the moment you start working on someone else's project,
|
||||||
|
your own copy of it is what you treat as home.
|
||||||
|
- **`upstream` — present only on borrowed repos: the external project a repo was cloned from**
|
||||||
|
(someone else's public or limited-access repo you don't control). Fetch-only, by rule: pull from it
|
||||||
|
to track their changes when you want to, never push to it. This isn't a fork-and-contribute-back
|
||||||
|
workflow; it's personal reuse, so there's no write access to lose and no PR cycle back to it.
|
||||||
|
- **`mirror` — present only on repos you additionally publish out: a second remote that receives a
|
||||||
|
copy of canonical work.** Push-only, by rule: push to it after `origin` already has the change,
|
||||||
|
never pull from it or treat it as a source of truth. It's a passive broadcast copy, not a place
|
||||||
|
where independent work happens — if it ever diverges from `origin`, that's a process bug, not a
|
||||||
|
second line of development to reconcile.
|
||||||
|
|
||||||
|
The pattern across all three: **the name states the relationship, and the relationship dictates which
|
||||||
|
of fetch/pull/push are ever legitimate on that remote** — `origin` gets all three, `upstream` gets
|
||||||
|
fetch only, `mirror` gets push only. The name alone doesn't enforce this (nothing stops a literal
|
||||||
|
`git push upstream`) — it's a discipline rule to hold yourself to, the same way `--force-with-lease`
|
||||||
|
below is a habit, not something git makes you use.
|
||||||
|
|
||||||
|
**Fetch vs. pull.** `git fetch` downloads a remote's state into local `refs/remotes/*` without
|
||||||
|
touching your working branch — nothing about your checkout changes. `git pull` is `fetch` plus an
|
||||||
|
automatic merge or rebase into your current branch, applied immediately, with the exact behavior
|
||||||
|
depending on `pull.rebase` config you may not have checked. Under contention — someone else has
|
||||||
|
pushed to the branch you're on, or a rebase is already in flight — that automatic step is the
|
||||||
|
problem: `fetch` lets you look at what actually changed (`git log origin/master..master`, `git diff`)
|
||||||
|
and integrate deliberately; `pull` commits you to an integration right away, often producing a merge
|
||||||
|
commit you didn't ask for on what should have been a clean rebase. Default to `fetch` followed by a
|
||||||
|
deliberate rebase or merge (see Merging vs. rebasing) over `pull`.
|
||||||
|
|
||||||
|
**Force-push discipline.** Only ever `--force-with-lease`, never plain `--force` (see Merging vs.
|
||||||
|
rebasing) — restated here because it's fundamentally a remotes concern: `--force-with-lease` refuses
|
||||||
|
the push if the remote has moved since your last fetch (someone else got there first), where plain
|
||||||
|
`--force` overwrites blind, without checking. Force-pushing is legitimate on a branch that's still
|
||||||
|
exclusively yours (rebasing your own in-progress feature branch), and on your own `mirror` remote
|
||||||
|
(which never holds independent history to lose — it's a broadcast target, so overwriting it just
|
||||||
|
re-syncs it to `origin`). Force-pushing `origin`'s trunk, or any branch someone else has fetched, is
|
||||||
|
never legitimate, whichever flag is used.
|
||||||
|
|
||||||
|
**Reconciling upstream changes on borrowed repos.** A borrowed repo has two moving targets — your
|
||||||
|
local changes and `upstream`'s own progress — and the real risk is letting them collide unprepared,
|
||||||
|
not the merge conflict itself (Merging vs. rebasing already covers that mechanically).
|
||||||
|
|
||||||
|
- **A dedicated `vendor` branch mirrors `upstream` exactly and never carries local commits.** It only
|
||||||
|
ever fast-forwards:
|
||||||
|
```
|
||||||
|
git switch vendor
|
||||||
|
git fetch upstream
|
||||||
|
git merge --ff-only upstream/master # or whatever upstream calls its own trunk — not yours to rename
|
||||||
|
```
|
||||||
|
If the fast-forward fails, upstream's history moved in a way it shouldn't have (rare, but possible
|
||||||
|
on a repo you don't control) — stop and look, don't force it. `vendor` is the answer to "what did
|
||||||
|
upstream actually look like at this sync point," kept separate from your own diff against it.
|
||||||
|
- **Local changes live on trunk, merged in from `vendor` — never the reverse.** After updating
|
||||||
|
`vendor`, merge it into trunk the normal way (see Merging vs. rebasing); trunk gains upstream's new
|
||||||
|
commits, and any conflict surfaces exactly at that merge, isolated from the rest of your work.
|
||||||
|
- **Inspect before merging, not after conflict markers appear.** `git log master..vendor` for what
|
||||||
|
upstream actually changed; `git diff vendor...master` for your own outstanding delta since the last
|
||||||
|
sync. Knowing which of your local changes touch files upstream also changed tells you where a real
|
||||||
|
collision is possible before you're staring at raw markers — a sync that touches unrelated files
|
||||||
|
needs no special care.
|
||||||
|
- **Tag before a sync, beyond what the reflog gives you.** This is a recurring operation, not a
|
||||||
|
one-off — `git tag -a pre-sync-2026-08-22 -m "before upstream sync"` before attempting it (see Tags
|
||||||
|
and releases for why always annotated). The reflog (Reverting and undoing) is a safety net for the
|
||||||
|
next few minutes; a sync worth revisiting days later deserves a marker that doesn't expire.
|
||||||
|
- **No fixed sync cadence — same drift smell as Branch lifetime.** The longer local work diverges
|
||||||
|
from upstream unreconciled, the more a sync eventually costs. Sync opportunistically rather than
|
||||||
|
let it compound.
|
||||||
|
- **When a conflict does appear, there's no universal winner — read what each hunk is actually for**
|
||||||
|
(see Merging vs. rebasing's conflict-marker rule). One question worth asking first: is upstream's
|
||||||
|
changed hunk the reason your local patch exists — a bug it also touches, a function it also
|
||||||
|
modifies — or just adjacent code your patch never depended on? The first case usually means the
|
||||||
|
patch needs rethinking against the new code, not mechanical preservation; the second usually means
|
||||||
|
taking upstream's version outright is safe. That doesn't replace judgment — it just says which
|
||||||
|
conflicts are worth slowing down for.
|
||||||
|
|
||||||
|
## Tags and releases
|
||||||
|
|
||||||
|
**Always annotated, no exceptions** — `git tag -a v1.2.0 -m "Release 1.2.0"`, never plain
|
||||||
|
`git tag v1.2.0`. One command shape to hold to, including for a disposable personal marker like the
|
||||||
|
pre-sync tags in Remotes and collaboration: annotated tags carry a tagger, date and message, which is
|
||||||
|
exactly the context worth having on anything you might read back later. Lightweight tags exist mainly
|
||||||
|
as a cheap scripted alias to a commit, not for anything a human or agent is meant to read.
|
||||||
|
|
||||||
|
**Version scheme: SemVer, `vMAJOR.MINOR.PATCH`**, with the bump driven by the same type taxonomy
|
||||||
|
already adopted for branches and commits (Branching, Committing):
|
||||||
|
|
||||||
|
- `feat` merged since the last tag → bump MINOR
|
||||||
|
- `fix`, `chore`, `docs`, `refactor`, `test`, `ci` merged since the last tag → bump PATCH
|
||||||
|
- a documented breaking change → bump MAJOR, regardless of which type introduced it
|
||||||
|
|
||||||
|
This is the payoff for adopting Conventional-Commits-shaped types earlier: the version number becomes
|
||||||
|
a fact derived by walking merged type prefixes since the last tag, not a judgment call made fresh at
|
||||||
|
release time.
|
||||||
|
|
||||||
|
**A tag marks a release, not a merge.** Every `--no-ff` merge already marks an integration point in
|
||||||
|
trunk's history (Merging vs. rebasing); a tag is a coarser, deliberate boundary on top of that — "this
|
||||||
|
state was released," not "this feature landed." Tagging every merge would just duplicate what the
|
||||||
|
merge commits already record. Tags don't travel with an ordinary `git push` — push one explicitly
|
||||||
|
(`git push origin v1.2.0`, or `git push --tags` for all of them) once the release point is decided.
|
||||||
|
|
||||||
|
**Once pushed, a tag is immutable — the same rule as pushed commits, applied to tags.** Never
|
||||||
|
re-point an existing tag name at a different commit and re-push it (`git tag -f` + `git push --force`)
|
||||||
|
once it's left your machine: a tag someone else may have already checked out, built from, or cited in
|
||||||
|
a release note has to keep meaning what it meant when they read it. If a release was tagged wrong, tag
|
||||||
|
the corrected state under a new name (`v1.2.1`, never a rewritten `v1.2.0`) — the same "add a
|
||||||
|
correction, don't rewrite" instinct Reverting and undoing already establishes for commits.
|
||||||
|
|
||||||
|
*(Handbook composed 2026-08-22. Each section above stands as a citable unit — a chunk boundary, not
|
||||||
|
just a heading.)*
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: gdext-verify-live-docs
|
||||||
|
description: Verify gdext (Godot Rust bindings) API calls against live docs, not recall.
|
||||||
|
---
|
||||||
|
|
||||||
|
# gdext: verify against live docs, don't trust recall
|
||||||
|
|
||||||
|
`gdext` is pre-1.0 and under active development — breaking changes between versions are routine, not
|
||||||
|
exceptional. Training-data recall of its API is likely to be stale in exactly the way that looks
|
||||||
|
correct: a plausible macro invocation or trait bound that compiled against an earlier version and
|
||||||
|
silently doesn't against the one actually in use.
|
||||||
|
|
||||||
|
**Before writing gdext-specific code (a `#[derive(GodotClass)]`, `#[godot_api]` block, signal
|
||||||
|
registration, an exported property, or any other gdext macro/trait), check the version actually
|
||||||
|
pinned in the project's `Cargo.toml`, then verify the API against:**
|
||||||
|
|
||||||
|
- `https://docs.rs/gdext` — pin the query to the pinned version, not `/latest`, if the two differ.
|
||||||
|
- `https://godot-rust.github.io/book` — the official book; tutorials and patterns change with the
|
||||||
|
crate, so prefer the version-matched section over a remembered pattern.
|
||||||
|
|
||||||
|
If a build fails on a gdext API call that looks like it should work, the first hypothesis is a
|
||||||
|
version mismatch between what's remembered and what's pinned — check the docs at the pinned version
|
||||||
|
before assuming the code is wrong.
|
||||||
|
|
||||||
|
Do not treat this skill as a place to cache gdext API details for next time. Anything gdext-specific
|
||||||
|
written here would go stale the same way recall does — the whole point is redirecting to a source
|
||||||
|
that stays current on its own.
|
||||||
Reference in New Issue
Block a user