cj-meetings
Skill1 starsChanged 32 days agoMITView on GitHub
Interactive meeting notes skill for preparing, running, and documenting meetings with automatic participant research and live transcription from Microsoft Teams live captions, plus on-demand screenshots of the Teams meeting window (codeword `sc`). Use when the user asks for meeting notes, meeting minutes, meeting prep, "møtenotater", "møtereferat", wants to take notes during a meeting, transcribe a meeting, says "start møte", or wants to document what happened in a meeting. Also handles recap of an already-recorded meeting from a Teams/OneDrive recording URL. For generic calendar lookup, use cj-owa-tools instead.
In this repository19 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cj-abSkill
- cj-ambitionSkill
- cj-automateSkill
- cj-cloudflare-workerSkill
- cj-code-reviewSkill
- cj-dailySkill
- cj-notesSkill
- cj-nowSkill
- cj-owa-toolsSkill
- cj-thingsSkill
- cj-todoSkill
- cj-uxloopSkill
- cj-video-visionSkill
- cj-voice-dnaSkill
- repo-deepSkill
- repo-depsSkill
- repo-groomSkill
- repo-healthSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
23 rules. About 0 are common, 17 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈17
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 12
Language: All user-facing output, and the notes themselves, follow the language the user invoked the skill in.
- Very specific: line 20
If ~/.config/cj-skills/cj-meetings.md exists, read it before acting.
- Distinctive: line 52
If calendar access fails, fall back to manual meeting input.
- Distinctive: line 89
Ask: "Is this meeting in your Outlook calendar?" Yes → Step 3.
- Very specific: line 114
Every profile coming back empty means suspect the filter: look at the raw output before falling back to manual input.
- Distinctive: line 143
Ask which meeting to take notes for.
- Distinctive: line 179
Keep the results in context: the live phase uses them to resolve references.
- Very specific: line 199
Ask where to save, offering $MEETING_NOTES_DIR (or the current directory) as the default.
- Distinctive: line 249
Start it as soon as the notes file exists, before the meeting begins.
- Very specific: line 256
Run it as a background command with the longest timeout the harness allows (2 hours in Claude Code).
- Distinctive: line 276
Only the user can turn captions on.
- Very specific: line 326
If the meeting was simply silent, restart with a larger --stop-after-blind / --stop-after-frozen (or 0 to disable).
- Distinctive: line 338
Read the tail of the transcript to ground them: who said what, exact figures, names and dates the user only half-typed.
- Distinctive: line 359
Read the file each time rather than holding it in context.
- Very specific: line 381
When the user types sc (alone, or leading a note), screenshot the meeting window right away, without asking.
- Distinctive: line 399
Read the preview, write up what it shows, and put the embed line under the topic it belongs to.
- Distinctive: line 405
If the cj-voice-dna skill is installed, load it before the first write to disk and keep applying it to every append and to all of Phase 6.
- Distinctive: line 418
Keep attributions where relevant
- Distinctive: line 432
Every other phase is unchanged.
- Distinctive: line 438
When the user says the meeting is over ("møtet er ferdig", or equivalent):
- Distinctive: line 442
Stop capture so the transcript is final, then read it in full:
- Distinctive: line 523
Context: keep the participant research active all meeting to resolve pronouns and company mentions.
- Distinctive: line 527
Ambiguity: ask a clarifying question when a note is ambiguous.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Meeting notes (cj-meetings)
Prepare, run, and document a meeting: fetch it from Outlook with owa-cal,
research the attendees, and write clean markdown notes, grounded in a live
transcript captured from Microsoft Teams live captions.
Language. All user-facing output, and the notes themselves, follow the language the user invoked the skill in. If that is ambiguous, use Norwegian (Bokmål) without asking, unless the overlay names another default, and keep it for the session. The prompts quoted below are examples; say them in the user's language. Code and logs stay in English.
Local overlay
If ~/.config/cj-skills/cj-meetings.md exists, read it before acting. It holds this user's own profiles, paths and conventions, and it wins over the defaults here.
Requirements
- owa-piggy + owa-tools (
owa-cal,owa-graph,owa-vids). Check and install commands are in the## Requirementssection of the cj-owa-tools skill; profile resolution, auth and troubleshooting follow that skill too. - macOS, for live capture and
sc.teams-captions.pyreads Teams through the macOS Accessibility API (viactypes, stdlib only, runs on the system/usr/bin/python3) and screenshots withscreencaptureandsips. It needs Accessibility permission for the terminal running the agent, plus Screen Recording forsc(both in System Settings → Privacy & Security). On other platforms, run the meeting on chat notes alone. - Recap mode:
ffmpeg(forowa-vids get) andwhisper-cpp.
Paths. <skill dir> is the directory holding this SKILL.md (it differs
between Claude, Codex and Copilot installs); the bundled scripts are
<skill dir>/teams-captions.py and <skill dir>/facilitator-recap.py.
MEETING_NOTES_DIR, when set, is the default folder for new meeting notes;
otherwise the default is the current working directory.
Phases
- Calendar lookup: the user's meetings for the next 7 days via
owa-cal - Meeting selection: the user picks one
- Participant research: attendees from Graph, externals researched on the web
- File setup: the notes file, optionally from a template
- Live meeting: Teams captions mirrored to a transcript file while the user takes notes in chat; the agent refines and appends them
- Wrap-up: end timestamp, summary and action items from the user's notes, the transcript, and the Teams Facilitator notes when the meeting had them
If calendar access fails, fall back to manual meeting input. For a meeting that is already over and was recorded, use Recap mode instead of the live phases.
Recap mode (recorded meeting)
Trigger: the user pastes a Teams/OneDrive recording URL, or asks to summarize a recording.
owa-vids info "<recording-url>" --profile <alias> --pretty
owa-vids get "<recording-url>" --profile <alias> --audio-only --out meeting.m4a
whisper-cpp meeting.m4aowa-vids writes binary output, so it runs on one explicit --profile, never
-A. Infer the profile per cj-owa-tools; the recording URL tells you the
tenant. If the recording has slides or screen-share worth reading and the
cj-video-vision skill is installed, run it on the full video download instead.
Feed the transcript into Phase 6 exactly as a live transcript, resolving speaker
references from the attendee list (owa-graph GET "me/events/<id>" if the
meeting is still in the calendar, otherwise ask). Confirm the summary with the
user before finalizing.
Phase 1: Get calendar data
Step 1: The user's email domain
Take the domain of the user's email from session context to classify attendees as internal or external. Ask if it is unavailable.
Step 2: Is the meeting in Outlook?
Ask: "Is this meeting in your Outlook calendar?" Yes → Step 3. No (ad-hoc, external invite, informal) → Manual fallback. When the opening message already answers it (a pasted title and time, "I have a meeting with X in 10 minutes"), go straight to Manual fallback.
Step 3: Fetch meetings
Compute today through today + 7 days and pull every active profile in one call, without asking which calendar:
owa-cal -A events --from <YYYY-MM-DD> --to <YYYY-MM-DD> --limit 50-A labels each meeting with the profile it belongs to, so a meeting on an
unexpected profile still shows up. The output is
{"results": [{"profile", "ok", "data"}]} with the events in data (there
is no events key); with a single --profile it is the bare events array. Each
event has id, subject, start, end, categories, location, showAs,
isAllDay, type, seriesMasterId.
owa-cal -A events --from <d> --to <d> | jq -c '.results[] | {profile, e: [.data[]? | {id, subject, start, end, location}]}'Every profile coming back empty means suspect the filter: look at the raw
output before falling back to manual input. A []? on a wrong key silently
returns nothing.
Attendees come later, from owa-graph (Phase 3).
Step 4: Filter
Drop events where:
isAllDayis true (blockers, out-of-office)showAsisFreeorTentativecategoriescontainsIGNORE- the subject is a personal block ("Focus time", "Lunch", "[Calendar Blocker] ...")
Manual fallback
Tell the user calendar data could not be fetched, then ask for: title, date and time (from–to), location (physical / Teams / other), attendees (name and email).
Phase 2: Meeting selection
Show the filtered meetings as a numbered list with date and time, subject, and location:
- Project meeting with Acme Inc – Monday 27 March 10:00–11:00, Teams
- Q2 status – Tuesday 28 March 14:00–15:00, Room 4
Ask which meeting to take notes for; accept a number or a title.
Phase 3: Participant research
Step 1: Attendees from Graph
For a meeting picked from Outlook, fetch the event with the id from owa-cal:
owa-graph GET "me/events/<event_id>" --audience outlookTake every attendees[] entry of type required or optional (skip
resource): emailAddress.name, emailAddress.address. If the call fails or
the list is empty, ask the user to paste names and emails.
Step 2: Classify
Internal = same email domain as the user; external = any other domain, or flagged external by the user. Ask about attendees without an email. Show the list so the user can confirm or correct it.
Step 3: Choose whom to research
Offer: externals, everyone, no one, or a custom pick (multi-select).
Research
For each selected person, search the web for current role and employer, a brief
professional background, a LinkedIn profile, and what their employer does (size,
relevant context). Start with "Full Name" company/domain; for a generic email
domain (gmail, outlook, hotmail) search "Full Name" LinkedIn. On an ambiguous
match, ask the user for a LinkedIn URL or more context.
Keep the results in context: the live phase uses them to resolve references. Summarize for the user:
Ola Nordmann (external) – Project lead, Acme Inc. Acme is a 50-person consultancy focused on digital transformation. Kari Hansen (internal) – Developer, your team.
Phase 4: File setup
Template
List <skill dir>/templates/*.md. If any exist, ask whether to use one (or
blank). For a chosen template, fill its frontmatter (e.g. customer, project,
date auto-filled from the meeting, meeting type) and ask for fields that cannot
be auto-filled. templates/standard.md is a Norwegian example; add a .md file
there to create a new template.
Location
Ask where to save, offering $MEETING_NOTES_DIR (or the current directory) as
the default. In a vault with per-customer folders (02-work/<client>/, see
cj-notes' question playbook), suggest the folder matching the meeting's
profile and client. Filename: YYYY-MM-DD-<title>.md, the title lowercased with
hyphens and no special characters.
The live transcript goes in a second file beside it, same stem plus
.transcript.md (2026-03-27-project-acme.transcript.md).
teams-captions.py owns that file and rewrites it on every poll; notes go only
in the notes file.
Create the file
Apply Writing style before this first write. Start the file with (headings in the user's language):
# [Meeting title]
**Date:** YYYY-MM-DD
**Time:** HH:MM – HH:MM
**Location:** [Location]
**Attendees:**
- Name (role/company) – internal/external
---
## Attendee background
[Brief summaries from the research phase]
---
## Notes
[Appended during the meeting]With a template, follow its structure instead, keeping the attendee info and research.
Phase 5: Live meeting
The meeting yields two things in parallel: a transcript from Teams live captions, and the user's own notes typed in chat. Both feed Phase 6.
Step 1: Start capture in the background
Start it as soon as the notes file exists, before the meeting begins. It waits for Teams and the caption panel on its own.
<skill dir>/teams-captions.py --out "<notes-stem>.transcript.md"Run it as a background command with the longest timeout the harness allows
(2 hours in Claude Code). When the harness stops it at its time limit, restart
it at once with the same --out and say so in one line; a meeting over 2 hours
needs one restart per 2 hours. Restarting with the same --out always resumes
from the existing file and never truncates it, so that is the fix whenever
capture stops mid-meeting.
It polls every 5 s (--interval) and exits by itself once the caption panel
has been gone for 12 polls (--stop-after-missing), so leaving the meeting
stops it. If the user never turns captions on, stop it with
pkill -TERM -f teams-captions.py.
Append to the notes file:
**Meeting started:** YYYY-MM-DD HH:MMStep 2: Have the user turn on captions
Only the user can turn captions on. Ask them to:
- ... More → Language and speech → Turn on live captions
- Pop the caption panel out into its own window and leave the meeting and
caption windows unminimized. (Capture also reads a docked panel;
scuses the popped-out window to find the meeting window.) - Set Language and speech → Spoken language to the language the meeting is actually in.
Give the menu labels in the language of the user's Teams UI, which is often
English even when the conversation is Norwegian. A Norwegian UI reads
... Mer → Språk og tale → Slå på live teksting. For a non-English
Teams UI, pass the caption panel's localised accessibility name with --anchor
(default Live Captions).
Ask the spoken language for every meeting. Teams keeps whatever was set last, and speech captioned in the wrong language comes out as gibberish. When the attendee list makes the language obvious, say which you expect and let the user correct it. If the transcript tail turns to nonsense mid-meeting (someone switched language), tell the user in one line to change Spoken language, and note the garbled window in the notes so no one trusts it later.
Step 3: Check the panel is readable
A diagnostic only; capture is already running.
<skill dir>/teams-captions.py --checkOK: caption panel found, N line(s) currently visible→ done.Nmay be0until someone speaks, or while Teams wakes its accessibility tree; leave capture running.NOT FOUND→ it lists the open Teams windows; captions are off or the--anchordoes not match. Have the user redo Step 2.no Accessibility permission→ the user grants it to the terminal (one-time).
Then tell the user capture is running, that typing sc screenshots the
meeting window into the notes, and that they should still type the notes that
matter: the transcript is raw material, their judgement is the record.
When capture stops on its own
Each exit is a meeting minute being lost, so act on it the moment it arrives:
| Exit | Message | Cause | Tell the user, then restart with the same --out |
| 2 | panel found but reading empty | Accessibility tree went cold: 3 min of empty reads with the panel open | close and re-pop the captions panel |
| 3 | panel frozen | Same lines for 3 min: a minimized Teams window stops updating | restore the meeting and caption windows |
If the meeting was simply silent, restart with a larger --stop-after-blind /
--stop-after-frozen (or 0 to disable). When the user announces a break,
restart right away with --stop-after-blind 240 (20 min) before it trips, and
say so in one line.
During the meeting
The user's messages are raw notes, questions, or instructions ("add an action item", "note that we agreed on X").
Raw notes:
- Clean them up: fix typos, expand abbreviations, add structure.
- Read the tail of the transcript to ground them: who said what, exact figures, names and dates the user only half-typed.
- Resolve ambiguous references with the participant research (one external woman in the meeting + "she says we must deliver faster" → attribute it to her by name).
- Append under the notes heading, grouped by topic with
###subheadings.
Questions:
- Answer from the participant research, with extra web lookups as needed. Add Q&A to the notes only when asked.
- Investigations go to a background subagent, so note-taking never stops. A question needing more than one quick read (code, a tenant, telemetry, a work item) is handed to a background subagent: a cheaper model for a bounded lookup, the strongest model for root-cause work across sources. Give it the established facts and ask for read-only work with evidence. Keep taking notes while it runs, and add its answer to the notes when it reports back.
Instructions: carry them out in the right section of the file.
Reading the transcript
Read the file each time rather than holding it in context; it grows all meeting. Lines look like:
**11:52 Hansen, Kari:** This is a test of speaking a long sentence.- In a hybrid meeting the room is one speaker. Everyone physically present
is captioned under the room device's name; only remote participants get their
own. Ask the user who said what when it matters. The user appears under their
directory display name, often
Last, First. - Captions are evidence, never quotable text. Live ASR mangles names, numbers, acronyms and words from a second language. Where the transcript and the user disagree, the user wins.
- The file is rewritten each poll, because Teams revises caption lines after showing them. Read it; leave writing to the script.
- A stale-looking tail is normal. The panel clears on its own timer, regardless of whether people go quiet. Judge capture health by the file's mtime; real stalls end capture loudly (see the exit table).
Screenshots: the sc codeword
When the user types sc (alone, or leading a note), screenshot the meeting
window right away, without asking. It captures only the meeting window, on
whichever display, silently and without stealing focus:
<skill dir>/teams-captions.py --shot "<notes-file>.md"The script picks the location: inside an Obsidian vault it follows the vault's
attachment setting (.obsidian/app.json) and embeds with a wikilink; elsewhere
the image lands beside the notes with a relative markdown link. It prints:
png: <saved image>
preview: <downscaled copy, read this one>
embed: <line to paste into the notes>Read the preview, write up what it shows, and put the embed line under the topic it belongs to. Pair it with the transcript tail for what was said around it. Without Screen Recording permission macOS returns only the wallpaper.
Writing style
If the cj-voice-dna skill is installed, load it before the first write to disk and keep applying it to every append and to all of Phase 6.
Punctuate with a spaced en dash (–), a comma, a colon or parentheses, and
check the text for em dashes (U+2014) before writing it, since the em dash is
the most obvious tell of machine-written text. The rule holds even when
cj-voice-dna fails to load.
- Past tense for what was said and decided ("Ola presented...", "It was decided that...")
- Decisions: Decision: ...
- Action items: Action: [who], [what], due [date]
- Material taken only from captions and not confirmed by the user: mark it
(ASR, unconfirmed) - Keep attributions where relevant
Norwegian notes use the headings of templates/standard.md (**Dato:**,
**Tid:**, **Sted:**, **Deltakere:** with intern/ekstern,
## Deltakerbakgrunn, ## Møtenotater, ## Oppsummering, ## Beslutninger,
## Aksjonspunkter with # | Ansvarlig | Oppgave | Frist, ## Neste steg)
and these markers: **Møtet startet:**, **Møtet avsluttet:**,
**Beslutning:**, **Aksjon:** [Hvem], [Hva], frist [Frist],
(ASR, ikke bekreftet).
Without captions
The user forgot, the tenant blocks them, the meeting is on a phone, or the machine is not a Mac: say so once and run the meeting on chat notes alone. Every other phase is unchanged.
Phase 6: Wrap-up
When the user says the meeting is over ("møtet er ferdig", or equivalent):
0. Gather the sources
Stop capture so the transcript is final, then read it in full:
pkill -INT -f teams-captions.pyThen fetch the Facilitator notes, if the meeting ran the Teams Facilitator
agent. Facilitator writes a Loop page, <subject> <date>.loop, to the
organizer's OneDrive and shares it with attendees, so the profile that attended
can read it:
<skill dir>/facilitator-recap.py --profile <alias> "<subject>" <YYYY-MM-DD>It prints a snapshot and a replay of the edit ops, each line tagged with its
author: [AI-generated] is Facilitator, a name is a human edit. Read both;
where they differ, the snapshot is the reconciled one. On "no ... in
sharedWithMe" (no Facilitator, or not shared with that profile), ask in one line
whether the user wants to paste the recap from Loop; otherwise continue with two
sources.
Rank the sources user notes > Facilitator notes > captions. The transcript supplies coverage (topics touched, figures, names, commitments the user did not type); Facilitator heard the whole meeting, so it covers minutes before capture started and any stalled or garbled stretch; the user's notes supply what mattered and win every disagreement. Both generated sources are evidence: never paste a caption line or Facilitator text into the summary verbatim.
1. End timestamp
**Meeting ended:** YYYY-MM-DD HH:MM2. Summary and action items
The longest stretch of generated prose: apply Writing style and sweep for em dashes (U+2014) before writing. Append (headings in the user's language):
---
## Summary
[2-5 bullets: key topics and outcomes]
## Decisions
[Decisions made, if any]
## Action items
| # | Owner | Task | Due |
|---|-------|------|-----|
| 1 | Ola | ... | ... |
## Next steps
[Agreed follow-ups, next meeting, etc.]3. Review with the user
Present the summary and ask whether it is right and complete. List what came only from the transcript ("From the transcript I included: [X]. Correct?") and only from Facilitator ("From Facilitator I included: [Y]. Correct?") so the user can confirm or drop it. Refine, then confirm the final file is saved.
4. Delete the transcript
Once the user has answered the review, delete the transcript file without asking and say so in one line: it is long, verbatim and full of chatter, and the notes are the record. Keep it only if the user asked for that before wrap-up.
Standing rules
- Notes file: edit or append after each batch of notes rather than
rewriting the whole file. The transcript file belongs to
teams-captions.py. - Context: keep the participant research active all meeting to resolve pronouns and company mentions.
- Timestamps: the user's local time (Europe/Oslo unless the overlay says otherwise).
- Tone: professional, approachable, short sentences.
- Ambiguity: ask a clarifying question when a note is ambiguous; resolve it silently when context makes it obvious.
teams-captions.py reference
Mirrors the Teams caption panel into a markdown file. Read-only towards Teams: it never clicks, types or steals focus, and needs no API, recording or audio pipeline.
teams-captions.py --check # is the caption panel readable?
teams-captions.py --out transcript.md # mirror it, 5 s polls, until captions close
teams-captions.py --out transcript.md # same file again = resume, never truncate
teams-captions.py --out t.md --interval 10 # slower polling
teams-captions.py --out t.md --stop-after-blind 0 # disable the cold-tree watchdog
teams-captions.py --out t.md --stop-after-frozen 0 # disable the frozen-panel watchdog
teams-captions.py --out t.md --anchor <name> # non-English Teams UI
teams-captions.py --shot notes.md # screenshot the meeting window, exit
teams-captions.py --demo # self-check, no Teams neededIt is stdlib-only by design (the Accessibility API is plain C, reached with
ctypes); keep it free of pip/uv dependencies. The merge, the
two-static-text entry shape, the resume, and the three watchdogs each handle a
Teams behaviour observed in live meetings; the reasons are in the script's
docstrings, --demo covers each one, and simplifications are marked
ponytail:. Read those before changing any of it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

