Compare commits

..

14 Commits

Author SHA1 Message Date
mikkeli fb08692dde docs: shorten gdext-verify-live-docs description to 75 chars
Hermes' own skill descriptions run under 60, lenient to ~80 -
the original ran to nearly 280 and buried the trigger in an
enumerated macro list that belongs in the body, not here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-23 13:18:54 +09:00
mikkeli 19ac0ea563 Merge branch 'feat/redirect-skills-and-handbooks-dir'
Adds redirect-skills as a second content type alongside handbooks,
and splits both into their own directories (handbooks/, skills/).
First redirect-skill: gdext-verify-live-docs.
2026-08-23 11:45:54 +09:00
mikkeli 2cce76f46f feat: add redirect-skills as a second content type, split into dirs
This repo now houses two shapes of authored, version-controlled
content: long-form handbooks (retrieved via a KB) and short
redirect-skills (deployed into an agent's own skill directory).
A redirect-skill's entire job is pointing at a live or handbook
source instead of caching an answer that would go stale - the
gdext binding for Godot 4 is the first case, since it's pre-1.0
with no existing skill or MCP tool anywhere.

Neither shape is consumed directly from this repo: a handbook
isn't live until admitted to kb-engineering, a skill isn't live
until copied (never symlinked, since an agent may write back to
its own skill files) into wherever it actually runs.

Handbooks moved from the repo root into handbooks/, for symmetry
with skills/ - each content shape now owns its own directory
instead of handbooks implicitly owning the root.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-23 11:45:40 +09:00
mikkeli 8e8e1aaa6e Merge branch 'docs/repo-naming-hyphens'
Adds the repo-naming rule (hyphens, not underscores) next to
trunk naming in Branching.
2026-08-22 21:47:35 +09:00
mikkeli 54d6aebbe5 docs: add repo-naming rule (hyphens, not underscores)
New repos only, same non-retroactive caveat as trunk naming.
Underscores don't survive the trip to a domain, a Docker tag, or
most package-registry names, where hyphens do; matches the
kebab-case already used for branch descriptions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-22 21:47:17 +09:00
mikkeli 2e5f6b4db0 Merge branch 'docs/failure-handling-handbook'
Composes failure-handling.md (input contract discipline,
failure-propagation discipline) and adds a frontmatter convention
(title, collections) to both handbooks, fixing a collection-tag
drift found in the process.
2026-08-22 17:05:48 +09:00
mikkeli 57e03af2b9 docs: add frontmatter convention (title, collections) to handbooks
Each handbook now opens with a flat YAML frontmatter block so the
two ingest-time values that actually describe the document -
title and collections - live with it instead of being retyped by
hand at admission time. That drift was real, not hypothetical:
the ingest recipe documented `--collection dev-practice` while the
actual admission used `handbooks`.

`version` deliberately stays out of frontmatter - a file can't
contain the hash of the commit that writes it - and remains a
value the ingest recipe computes fresh, now scoped per-file
(git log -1 -- <file>) rather than the whole repo's HEAD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-22 17:05:23 +09:00
mikkeli 4ed06c7e4b docs: compose the failure-handling handbook
Two sections, forged the same way as git-operations.md: input
contract discipline (degenerate input as the default case,
validate/sanitize/reject as distinct responses, the
meaning-preserving line for when sanitizing is safe) and
failure-propagation discipline (never substitute a plausible
artifact for a failed operation, retry/abort/surface, one
signaling convention per boundary, the anti-pattern catalogue).

Environment/verification parity was scoped out deliberately —
different axis (confidence-establishment, not code behavior),
earns its own handbook later rather than a section here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-22 17:04:11 +09:00
mikkeli ccefd1751b README: pin --version to this repo's commit in the ingest recipe
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvxzSKi1zR395wvYXUenVE
2026-08-22 16:53:10 +09:00
mikkeli 119309debf README: add the ingest recipe, fix the halogen repo link
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvxzSKi1zR395wvYXUenVE
2026-08-22 14:44:46 +09:00
mikkeli 8dd4b207cb Merge branch 'fix/git-operations-review-findings'
Fixes a factually reversed rebase conflict-marker explanation found
during review, plus four smaller accuracy/clarity corrections in the
same document.
2026-08-22 01:06:58 +09:00
mikkeli 9aa0916d95 fix: correct conflict-marker direction and other review findings
The rebase conflict-marker explanation had HEAD and the incoming
side backwards, verified by reproducing all three merge/rebase
scenarios in a throwaway repo. Rewrote that passage around the
actual behavior instead of a merge-vs-rebase binary, since the old
"merge = HEAD is trunk" claim was also only true for one of the two
merge scenarios the doc itself describes.

Also fixes, from the same review pass:
- push-rejection handling in Branching split into its two real
  causes (protection/permission vs. plain non-fast-forward)
- the commit-body trigger reworded to match how it's actually used
  ("why worth keeping", not just subject length)
- a mislabeled checkout equivalent for unstaging (that was always
  git reset's job, not checkout's)
- the vendor-sync example no longer assumes upstream's trunk is
  named master

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-22 01:06:49 +09:00
mikkeli 7622669a4c Merge branch 'docs/git-operations-handbook'
Composes git-operations.md: branching, committing, merging vs.
rebasing, reverting and undoing, remotes and collaboration, tags and
releases.
2026-08-22 00:55:56 +09:00
mikkeli ebfb7da987 docs: compose the git-operations handbook
Branching through tags, forged collaboratively over a full session:
trunk-based branching with type/description naming, atomic commits,
rebase-to-stay-current paired with merge --no-ff to land, revert vs.
reset vs. switch/restore split by shared-vs-local history, an
origin/upstream/mirror remote-naming discipline (including a
vendor-branch pattern for reconciling borrowed-repo drift), and
SemVer tags driven by the same type taxonomy as commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EBCcjJvEvhpToahqdsLLA
2026-08-22 00:55:43 +09:00
5 changed files with 626 additions and 47 deletions
+117 -13
View File
@@ -1,32 +1,136 @@
# Handbooks
Instance-agnostic reference material for coding agents — the verbose, citable kind that belongs in
a retrieval store, not in a skill.
Instance-agnostic reference material for coding agents, in two shapes: long-form **handbooks** meant
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
- Public standards, style guides, and conventions that apply regardless of which box 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
into a model's context wholesale, so length carries no per-turn tax.
- **Handbooks: public standards, style guides, and conventions** that apply regardless of which box
or project is running — general git operations, language style guides, CERT/MISRA-style rulebooks.
Long-form and specific; nothing here is loaded into a model's context wholesale, so length carries
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
- **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
(`kb-engineering` for [halogen](https://pi5-16.local/mikkeli/halogen-workstation-roadmap)).
- **Short, prescriptive procedures achievable within a context window or two.** That's a skill, not
a handbook — it gets loaded, not retrieved. See `skills/` in the halogen-workstation-roadmap repo
for the sibling convention.
(the `engineering` corpus, for
[halogen](https://git.novoyuuparosk.org/mikkeli/halogen-workstation-roadmap)).
- **Full procedural skills that aren't redirect-shaped.** Genuine workflow knowledge — a debugging
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
for. The chunked, embedded index built from it is derived and rebuildable state — it lives beside
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
One markdown file per topic at the repo root. Each file should stand as a single admittable
document — the same shape a human vouches for at KB admission time.
**Handbooks:** one markdown file per topic under `handbooks/`. Each file should stand as a single
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
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.
-34
View File
@@ -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.)*
+131
View File
@@ -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.)*
+351
View File
@@ -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.)*
+27
View File
@@ -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.