| name | requests-from-meetings |
| description | Parse meeting recordings into precise feature-request documents under ./requests-from-meetings. Use when the user invokes /requests-from-meetings, asks to parse a meeting or meeting backlog, asks what was requested or shown in a meeting, or mentions meeting recordings, transcripts, or feature requests from meetings. |
Requests from meetings
We record every feature request presented in meetings with the utmost precision — so that months later, someone who wasn't there can understand exactly what was asked, by whom, while looking at what. The output lives in ./requests-from-meetings, one folder per meeting, each with a beautifully readable index.html.
Requests are records, not commitments. Nothing in these documents implies we will build what was asked. Shaping work from requests is a separate, future skill. Every document states this plainly.
This skill is self-improving: when a recipe here proves wrong or a better one emerges (a different scene threshold, a better crop, a sharper document structure), update this file in the same PR as the meeting that taught the lesson.
First run in a repository
Meeting records live in the project, not in this skill. The first time this skill runs in a repository, create requests-from-meetings/ and copy the shared files out of the installed skill:
mkdir -p requests-from-meetings
cp -R .claude/skills/requests-from-meetings/requests-from-meetings/. requests-from-meetings/
That lands verify.py, assets/style.css, a starter stakeholders.md, and the rollup index.html — which reads well with zero meetings parsed. Add /requests-from-meetings/config.local.json to the repo's .gitignore, and set the project's name where the rollup's placeholder copy asks for it.
Per-user config
Each user keeps their meetings in their own folder. The path lives in requests-from-meetings/config.local.json (gitignored):
{ "meetingsFolder": "/Users/you/Dropbox/meeting-recordings" }
If the file is missing, ask the user for their meetings folder before doing anything else, then write it. Every worktree needs its own copy — verify.py reads it, and without it the quote gate cannot run.
Project principles
A repo can also keep requests-from-meetings/project-principles.md: standing rulings that bind every parse run in that repo — a house doctrine the skill itself stays free of. Read it right after the per-user config, before doing anything else. It is where a repo declares what this file cannot know generically: the meetings' source language (the code every Whisper decode's -l takes), its shipping mechanics (how a worktree is provisioned, how branches are named, which repo gates a meeting PR runs), and any rulings its own meetings have taught.
Invocation
- No arguments → diff the user's meetings folder against the parsed manifests (see Discovering meetings) and answer with
/goal copy the user can paste to parse everything pending. Do not start parsing. The goal copy must: list the pending meetings by date and title, instruct loading this skill for the entire doctrine, mandate one PR per meeting with lanes run in parallel, and define done as "every listed meeting parsed, merged, and linked from the rollup; a fresh no-arg /requests-from-meetings reports nothing pending." Stay under /goal's 4,000-character limit.
- With a specific meeting (or instructions) → parse that meeting following the doctrine below.
Discovering meetings
The Gemini notes define meetings. In the user's folder, a file matching <Title> - <YYYY_MM_DD> <time> - Notes by Gemini*.md is a meeting; its recordings share the same <Title> - <date> <time> prefix (... - Recording.mp4, ... - Recording 2.mp4, …), and each part's Whisper transcript sits beside its recording under the same name with a .vtt extension. Other files in the folder (spreadsheets, PDFs) are shared documents referenced across meetings.
A meeting's repo folder is requests-from-meetings/YYYY-MM-DD-title-slug/ (e.g. 2026-07-27-software-review). A meeting is parsed when that folder's meeting.json records the notes file with a matching sha256 and "status": "parsed". Everything else is pending.
The doctrine
Wide net, labeled entries
Capture three kinds of entries, each labeled with its explicitness:
- Explicit request — someone asked for something in so many words.
- Observed pain point — friction that surfaced (a workaround, a complaint, a manual step nobody would choose) without being voiced as a request.
- Current workflow to replicate — a demonstrated way of working that clearly implies what the product would need to support. When we shape solutions later, the demonstrated workflow is often more precise than the voiced ask.
Every entry is tied to timestamps on the meeting clock. Record who asked, in what context, and what was on screen. Precision beats brevity; completeness beats tidiness — when in doubt, record it.
A label has to be provable from the quotes the entry carries. Explicit request needs a quote in which someone voices the ask — not an entry title phrased as one. Current workflow to replicate needs demonstrated or described practice of today, in the indicative; a subjunctive or conditional (se tivesse, poderia, ia ser — "if we had", "it could", "it would be") is someone imagining a future, and imagining is not a workflow. What passes neither test is an observed pain point, or context inside another entry.
Two transcripts
Every meeting is read through two transcripts, and each answers a question the other cannot.
Whisper carries the words. Every recording part is transcribed with whisper-cli (whisper.cpp) using ggml-large-v3, in the meeting's own language, and the VTT it writes is the verbatim source for every quote from that part:
ffmpeg -hide_banner -loglevel error -i "<part>.mp4" -ar 16000 -ac 1 "<part>.wav"
whisper-cli -m ~/.cache/whisper-cpp/ggml-large-v3.bin -l <language> -ovtt -of "<part>" -f "<part>.wav"
The model is pinned: ggml-large-v3.bin has to hash to 64d182b440b98d5203c4f9bd541544d84c605196c4f7b845dfa11fb23594d1e2, and a decode by anything else is not this transcript. The VTT lands beside its recording in the meetings folder as <part>.vtt, and meeting.json records it by filename and sha256 — like the recordings themselves, transcripts are never committed. A part's cue times are native to that part's clock: cue 00:01:06 is sixty-six seconds into that file, whatever the meeting timeline calls that moment.
A degenerate decode never ships. On long audio, large-v3's default decode sometimes falls into a loop and emits one cue hundreds of times, destroying everything after it — and the wreckage reads as a plausible transcript to anyone skimming the top. Decode with the defaults, then look: more than five identical cues in a row is the tell, and verify.py refuses the whole meeting when it finds one. The remedy is to re-decode that part behind voice-activity detection:
whisper-cli -m ~/.cache/whisper-cpp/ggml-large-v3.bin -l <language> -mc 0 --vad \
-vm ~/.cache/whisper-cpp/ggml-silero-v5.1.2.bin -ovtt -of "<part>" -f "<part>.wav"
The silero model is pinned too, at 29940d98d42b91fbd05ce489f3ecf7c72f0a42f027e4875919a28fb4c04ea2cf, and meeting.json records which decode shipped for each part so a reader knows what produced the words.
The Gemini notes carry the people and the clock. They are what makes a meeting a meeting, and they are the only source that says who spoke — Whisper hears words, not speakers. A quote's wording is Whisper's; its attribution comes from laying the Whisper text over the notes' speaker turns at the same moment, and verify.py holds a cited speaker to having a turn there. The notes also fix the timeline that every entry, quote, caption, and frame filename on the page is written in.
A part the notes never reached still gets read. Its Whisper transcript gives the words, but nothing gives the names, so attribution has to be argued from the conversation itself — who is driving the shared screen, who is asked and who answers, whose sentence the notes pick up when they finally start — and Reading this record says exactly what the argument rested on. A part that sits before the meeting clock has no timeline time to cite, so its quotes and its frames name the part and the time inside it: Part 1 · 00:01:06.
Language
Documents are written in English prose. Preserve verbatim quotes in the meeting's own language wherever the speaker's exact wording matters — which is most requests — followed by an English translation. Never paraphrase away the original ask.
Machine transcription mangles domain vocabulary (in one food-plant meeting series: QC → si, CCP → KCPI, batch → bet, yield → wield, spec sheet → specet). Reproduce the mangling in the verbatim quote and resolve the intended term in the translation with square brackets; the Reading this record note states the convention once, so no quote has to explain itself, and names the mangles the meeting actually produced — or says plainly that it produced none worth flagging, which is an answer and not a gap.
A verbatim never welds two stretches of speech together silently. In the notes, backchannel splits a single sentence across turns and sometimes across a timestamp heading; in a Whisper transcript the join that matters is a pause, because a cue break inside unbroken speech is the decoder segmenting rather than the speaker stopping. Mark either kind of join with …, or […] when the elision is long.
The English translation renders the joined sentence whole: the verbatim carries the fidelity, the translation carries the meaning, and a trailing … in a translation marks only a sentence the speaker never finished. The Reading this record note states this.
Verify every quote mechanically
The check is committed: requests-from-meetings/verify.py. Run it on the finished document, and a clean run is the ship gate:
python3 requests-from-meetings/verify.py 2026-08-05-software-review
It re-hashes every transcript against meeting.json, then walks every p.verbatim, splits it on … and […], and holds each fragment to the rules below. Each quote is checked against whichever transcript covers the moment it cites: a recording part with a Whisper transcript answers for its own quotes, and everything else answers to the notes. Exit 0 is clean, 1 is failures, 2 is a setup problem (no config, a missing transcript, a transcript whose sha256 has drifted, or a Whisper decode that looped). Paste its summary line into the PR body.
A verbatim fragment lies within one speaker turn. Any join — across a backchannel (Sim., certo?), across the same speaker's own interrupted turns, across a block heading — is marked with …, or […] when the elision is long. Never silently stitch: what stands between two … has to be one person's words, in one turn, exactly as the transcript has them.
That rule is why the script builds the notes as a stream of turns and never as one flat blob. Flatten it and every stitch becomes a substring, so the check passes while the document ships a sentence nobody said in one breath — which is exactly how six of these documents shipped before the script existed.
In a Whisper transcript the unit is a run of unbroken speech. Cues that follow one another with no audible gap between them are one stretch, and a fragment may cross them freely; a pause between cues is a boundary and needs its marker exactly as a turn boundary does. The script builds each part's VTT as runs for the same reason it builds the notes as turns.
The rest of what the script holds you to:
- The cite is where the quote starts. In the notes, a claimed timestamp resolves to the block whose window contains it (header time ≤ claimed < next header); the first fragment must sit in that block, and each later fragment must run forward from the one before it. In a Whisper transcript it resolves to a cue instead, and the cite has to land within a minute of it — part offsets are calibrated to a second or two, and a minute is the same forgiveness a block window gives. Off-by-one-block cites read as perfectly plausible and are caught here.
- A cite in part time names its part.
Part 1 · 00:01:06 cites a part that sits before the meeting clock. Every other cite is meeting time, and naming a part that is on the clock fails: the page keeps one clock.
- A speaker is a speaker the notes heard. A quote lifted from a Whisper transcript is credited to someone the notes have speaking within a minute of that moment. Where the notes never reached, nothing mechanical stands behind the name and the record's own account of the attribution is what carries it.
- Casing and quote characters are the transcript's. Do not sentence-case a fragment that starts mid-sentence. Normalization folds HTML entities, markdown backslash escapes (
300\., p\*\*\*\*), typographic quotes and dashes, and whitespace — on both sides, and nothing else.
- No brackets inside a verbatim except
[…] and [unclear]. A mangled term stays mangled where it was spoken and is resolved in brackets in the translation.
- Every quote carries a translation.
The script also warns — without failing the run — about multi-word source-language lang spans in prose (any lang that is not English) that match no single turn. Quote them properly or reword them.
Audit before shipping
verify.py proves that every quote the record ships is real. It cannot see what the record missed, what its prose claims, or what a caption asserts about pixels. A finished record gets a full-precision audit over exactly that gap, and it is not shippable until the audit's findings are fixed in the same PR. One record shipped its first pass with a clean verifier, every gate green and the whole document read end to end — and the audit still found two entries the wide net missed, an entry whose closing exchange shipped an outcome a later exchange had already superseded, five cross-meeting citations whose targets said something else, an initialism expanded past its evidence, and a figure caption its own frame contradicts.
Five passes, each on a layer no script reaches:
- Blind completeness sweep. A reader with fresh context builds its own timestamped inventory of all three entry kinds straight from the transcripts, before opening the finished record, then diffs both ways: entries the record misses, and sentences the record asserts that neither transcript carries — chronology reversed, a hedge hardened into a cause, an outcome a later exchange superseded, a label the quotes do not prove. Building the inventory first is the whole point. A reader who starts from the record only confirms it, and confirmation finds nothing.
- Sibling-claim verification. Every sentence that cites another meeting's entry is read against what that entry actually says. The anchor resolving is necessary and nowhere near sufficient: the speaker, the number, and the direction of the claim all have to match the target. Five citations shipped whose targets named a different speaker, a different horizon, or the opposite of the citing sentence.
- Calibration re-derivation. Re-derive the timeline offset from the recording's pixels, independently of the note already in
meeting.json, and extract a counterfactual frame showing the rival offset failing. Arithmetic proposes and pixels decide — but a confirming frame is only evidence if the alternative was actually disproven.
- Ink re-render. Draw every figure's ink onto its own frame with PIL and read the composites as images, as Frames that ship requires. Draw
.ink-ring as a rounded rectangle — border-radius: 8px in assets/style.css is what the page renders — because an ellipse composite makes a precisely-placed ring look like it clips the corners of what it encloses, and false-flags it.
- The shipped-evidence rule. The record asserts only what a transcript, a shipped frame, or a shipped asset carries. A fact read off a frame deliberately not shipped — cropped for privacy — is not citable: resolve to what the shipped evidence supports, the way one record leaves an initialism unexpanded because the meeting never says what it stands for. A withheld frame's contents may inform a hedge in
stakeholders.md; the meeting page cites only evidence a reader can open.
Two layers of this audit are already mechanical, and verify.py runs them: the entry count agreeing across the header meta, the section heading, meeting.json, and the rollup, and every relative link and in-document anchor resolving. It cannot judge the five above — spend the audit there.
Stakeholders
requests-from-meetings/stakeholders.md describes everyone who appears in any meeting. Before finalizing a meeting document, every speaker in its notes must have an entry there. When the notes surface an unknown speaker — or an unresolved reference that matters (a nickname, an unnamed "her assistant" who owns a workflow) — ask the user about them before finalizing (during a goal, use questionnaire questions), then add what you learned to stakeholders.md. Record uncertainty honestly ("as far as we know…").
A speaker label is not proof of who spoke. Diarization gives one label per microphone, so when people walk into a participant's room mid-call, everything any of them says inherits that participant's name. Watch for the tell — the labelled speaker being referred to in the third person by whoever answers them — and, when a document is built on such notes, state the conflation and the moment it starts in the Reading this record note, keep quote attributions exactly as the notes have them, and qualify an entry's "who" only where the conversation itself gives evidence.
Parsing a meeting
1. Transcribe every part
Run every recording part through Whisper as Two transcripts sets out, check each decode for the loop tell, and record the VTTs in meeting.json. This comes first because everything downstream reads from these files — the inventory, the timeline, and every quote the document ships.
2. Transcript pass
Read both transcripts end to end, the notes for who is speaking and the VTTs for what they said. Build a timestamped inventory of every candidate entry (all three kinds), with the surrounding conversational context and the exact quotes worth preserving. The notes have ### **HH:MM:SS** blocks (~1/minute) and bold speaker names; a VTT has cue windows a second or two wide and no names at all, so walk them side by side and carry the names across.
3. Map the recording timeline
The notes' timeline runs continuously across recording parts, in filename order: … - Recording.mp4 first, then … - Recording 2.mp4, 3, … Probe each part's duration:
ffprobe -v error -show_entries format=duration -of csv=p=0 "<part>.mp4"
Cumulative durations are a hypothesis, not the answer. Never assume the notes' clock starts at Recording.mp4: a meeting whose recording was stopped and restarted in its first minutes leaves an orphan first part the notes never cover, and summing durations then puts every frame late by that part's length — consistently enough to look right and be wrong.
Anchor on the notes' own session markers instead. Gemini writes a session-end marker at each break, in the notes' own language — A sessão foi encerrada após HH:MM:SS (and A transcrição foi encerrada após …) in a Portuguese-locale meeting; the equivalent phrasing elsewhere — and that time is the end of a recording part, to the second. Match each marker to the part whose duration lands on it, then work backwards: a part's offset is its end marker minus its duration; the notes resume a few seconds into the next part. If the durations only line up when a part is left out, that part sits outside the notes' clock — record its offset as negative and say so in meeting.json.
Anchors lie in wait, too. In one meeting a session-end marker exactly equalled the second part's duration by coincidence, which made a wrong mapping arithmetically perfect and self-consistent. Arithmetic proposes; pixels decide.
The Whisper cues give a second, independent alignment. Take a distinctive sentence the notes place at a known block, find the same words in a part's VTT, and the difference between the two times is that part's offset, to the second. Where the markers and the cues agree, the mapping is settled; where they disagree, the mapping is wrong and the pixels say which way.
A part outside the notes' clock is still part of the meeting, and its Whisper transcript covers it whether the notes did or not. Read that transcript, watch the part — sample frames across it with ffmpeg — and record what it says and shows. Never write that a part "contains no requests" without looking: one 78-second part dismissed that way had a shared workbook on screen the whole time, and a 75-second part written off as silent turned out to hold the only statement of the rule behind one of its meeting's headline requests.
Then confirm against pixels, twice:
- Content: extract a frame at a moment the notes make visually unambiguous ("agora vou abrir o calendário…", a URL being dictated) and read it.
- A clock on the shared screen: a Windows taskbar or macOS menu bar in the share gives wall-clock minutes. Crop it tiny and upscale (
crop=90:30:1355:815,scale=iw*6:ih*6:flags=neighbor) and read it at the first and last second of each part — that alone settles whether two parts are contiguous or overlapping.
Beware the block spacing: Gemini's headers are nominally one a minute but can run two minutes apart, and a block's dialogue spreads across the whole gap. A quote near the end of a block happened near the next header's time, so locate frames by content and treat block timestamps as the start of the window that contains a quote — say so in the document rather than implying second-level precision.
Record the final offsets in meeting.json, and give every meeting that has video a calibration note — under that exact key — naming the method and the points it was confirmed at. Some meetings have no video — the document then works from the notes alone, with no Whisper transcript to hold it, and says so.
4. Find what the screen showed
Two complementary passes:
Scene-change detection over each part (~60× realtime) lists every moment the shared content changed — tab switches, app navigation, document scrolls:
ffmpeg -hide_banner -loglevel error -i "<part>.mp4" \
-vf "select='gt(scene,0.2)',metadata=print:file=scenes.txt" -fps_mode vfr -f null -
Transcript-guided extraction: for each request-bearing passage, extract frames at its timestamps — and around deictic words (aqui, isso, esse aqui, ó, "here", "this one") pull frames within ±2–4 s, because the words alone don't say what the cursor was pointing at:
ffmpeg -hide_banner -loglevel error -ss <seconds-within-part> -i "<part>.mp4" -frames:v 1 -q:v 2 out.jpg
Read extracted frames as images to resolve what was actually shown. Discipline matters: read in small batches, skip near-duplicates, and target reading well under ~150 frames per meeting so your context stays healthy. Meeting videos are often letterboxed (screen share left, camera tiles right) — when the share region is stable, crop for legibility (e.g. -vf "crop=1440:1080:0:0"), but verify the region per meeting.
5. Frames that ship
Embed a frame in the document only when it genuinely helps understand an entry — the screen being discussed, the spreadsheet cell being pointed at. Name shipped frames by timeline position: frames/HH-MM-SS.jpg inside the meeting folder, and frames/part-N-HH-MM-SS.jpg for a frame from a part that sits before the meeting clock, the same way its quotes are cited. Crop below the browser chrome — tab strip, address bar, bookmarks bar — before shipping: the chrome carries the presenter's bookmarks and live document URLs, which have no business in the permanent record. The one exception is a frame whose address bar is the evidence (a caption pointing at a query parameter); crop that frame below the bookmarks bar instead and say in the caption why the URL is there. Annotate with the record's own visual language — accent #c2410c (the stylesheet's --ink), ~2.5px strokes, circles/boxes/arrows with small label chips — drawn as absolutely-positioned SVG/CSS overlays in the HTML, never burned into the image.
Verify the ink by rendering it, not by re-reading it. Before shipping, draw every figure's ink coordinates — rings, chips, badges — onto its own frame with a small PIL script and read the composites as images: badge n has to land on what the caption's "(n)" names, and each ring has to enclose exactly what the caption claims, nothing more. .ink-badge is centre-anchored (margin: -10px 0 0 -10px), so its left/top is the dot's centre; rings and chips are corner-anchored. A caption can be perfectly true while every pointer sits one row off, and proofreading the HTML cannot catch it — that happened.
6. Shared documents
Files shown or referenced in meetings that live at the folder root (spreadsheets, PDFs) are copied to requests-from-meetings/assets/ with kebab-case names, recorded in assets/manifest.json (original, file, sha256, firstSeen). Copy any that aren't there yet; reference them from entries. Videos are never committed.
The document (index.html)
Self-contained and beautiful: links only the shared stylesheet ../assets/style.css, no external fonts/scripts/CDNs, system font stack. Structure:
-
Header — meeting title, date, duration, participants (from stakeholders.md), source files, and the standing disclaimer: records of what was asked, not commitments to build.
-
Overview — the meeting's narrative arc in a few paragraphs: what happened, what was demoed, what themes emerged.
-
Reading this record — a <section class="section"> under that exact heading, sitting between the overview and the entries. Its checklist is fixed, and every document answers all five points, including the ones whose answer is "none":
- The verbatim is machine-transcribed and mangles the domain's vocabulary — name the mangles this meeting produced, or say plainly that it produced none worth flagging, and state that a mangled term stays as spoken in the verbatim and is resolved in brackets in the translation.
- What
… and […] mark inside a verbatim.
- That block timestamps are the start of a window rather than second-precision, and whether this document interpolates any timestamps of its own.
- Whether the notes conflate speakers and from which moment — or an explicit "no conflation observed".
- For a recording with parts the notes never cover, what those parts hold — and, where the document quotes them, how each quote's speaker was established, since no speaker scaffold reaches there.
Left to invention, each document writes some of this note and drops the rest, and a reader cannot tell a missing item from a non-existent problem. Write one short paragraph per point, each opening with its claim in bold — and the checklist above is the list, never a previous document's note.
-
Entries — the heart. Each entry: a slugged id, its label (explicit request / observed pain point / current workflow to replicate), a clear title, who asked, timestamps, the story in English prose, verbatim quotes with translations, annotated frames with captions, and links to shared assets and related entries (same meeting or cross-meeting). A reference to another entry is a link, always: <a href="#other-entry-id"> inside the meeting, a relative URL like ../2026-08-03-software-review/index.html#other-entry-id across meetings. Prose that says "as in the recipe tree entry above" without one leaves the reader to search.
Markup
Every meeting page uses these exact shapes — they are what assets/style.css styles. Study the stylesheet before diverging from them.
<div class="page">
<header class="doc-header">
<p class="eyebrow"><a href="../index.html">Requests from meetings</a> · Software Review</p>
<h1>Software Review — July 27, 2026</h1>
<dl class="meta">
<div><dt>Date</dt><dd>Monday, July 27, 2026</dd></div>
<div><dt>Duration</dt><dd>39:28</dd></div>
<div><dt>Participants</dt><dd>Marina Duarte · Alex Chen</dd></div>
Entries12
SourceSoftware Review - 2026_07_27 10_17 PDT (4 recording parts)
These are records, not commitments. …
Overview
…
Reading this record
The machine transcription mangles the domain's vocabulary. …
Entries · 12
Explicit request
Entry title
Marina Duarte
00:05:12
Reviewing the weekly orders spreadsheet
The story in English.
Marina Duarte 00:05:18
“…”
“…”
Read down this column first
1
00:00:40
Caption in English. (1) What the badge marks.
Open full size
XLSX 2026 Planning Calendar
…
The three label variants are label-request (explicit request), label-pain (observed pain point), and label-workflow (current workflow to replicate). The ink overlay's SVG viewBox matches the image's pixel dimensions; rings, chips, and badges are percentage-positioned so they stay aligned at every width.
meeting.json
{
"title": "Software Review",
"date": "2026-07-27",
"sourcePrefix": "Software Review - 2026_07_27 10_17 PDT",
"status": "parsed",
"parsedAt": "2026-08-07",
"sources": [
{ "file": "….md", "role": "transcript", "sha256": "…" },
{ "file": "… Recording.mp4", "role": "recording", "bytes": 10670309, "durationSeconds": 80.25, "timelineOffsetSeconds": 0
The notes' sha256 is what makes the pending diff exact — and every transcript's sha256 is what verify.py re-checks before it trusts a single quote. A whisper source names the recording it transcribes, the model that produced it, and which decode shipped: default, or vad for a part whose first decode looped. Recordings are listed with size and duration, never hashed or committed, and every recording of a meeting that has video carries its calibration note.
Rollup
requests-from-meetings/index.html links every parsed meeting (date, title, entry count, one-line summary) plus stakeholders.md. Each meeting PR adds its entry. It must read well with zero meetings parsed. Rebase conflicts there resolve keep-both.
Shipping
One PR per meeting, built in an isolated worktree. PR title like "Record feature requests from the 2026-07-27 Software Review meeting", body written for an external reader, with the verifier's summary line in it. Gates: a clean verify.py run, a clean audit with its findings fixed (see Audit before shipping), and whatever checks the consuming repo runs on every PR. project-principles.md is where a repo pins the rest of its shipping mechanics — how a worktree is provisioned, how branches are named, and which of the repo's own definition-of-done rules apply to content-only PRs.