diff --git a/SUMMARY.md b/SUMMARY.md
index f5f2d06..c1a6f9e 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -10,6 +10,7 @@
- [Documentation Guidelines](documentation.md)
- [Meetings](meetings/README.md)
- [Guidelines](meetings/guidelines.md)
+ - [Notes Tool](meetings/notes/index.md)
---
diff --git a/meetings/notes/index.md b/meetings/notes/index.md
new file mode 100644
index 0000000..c5a6a45
--- /dev/null
+++ b/meetings/notes/index.md
@@ -0,0 +1,187 @@
+# Process Meeting Notes
+
+Paste Google Docs Markdown export on the left; cleaned Markdown for GitHub Discussions appears on the right.
+
+
+
+
+
+
diff --git a/meetings/notes/name-map.json b/meetings/notes/name-map.json
new file mode 100644
index 0000000..a7b5dbe
--- /dev/null
+++ b/meetings/notes/name-map.json
@@ -0,0 +1,25 @@
+{
+ "people": [
+ {
+ "github": "calebmcquaid",
+ "names": [
+ "Caleb"
+ ]
+ },
+ {
+ "github": "cassidyjames",
+ "emails": ["cassidyjames@roost.tools"],
+ "names": ["Cassidy James Blaede", "Cassidy"]
+ },
+ {
+ "github": "juanmrad",
+ "emails": ["juan@roost.tools"],
+ "names": ["Juan Mrad", "Juan"]
+ },
+ {
+ "github": "julietshen",
+ "emails": ["juliet@roost.tools"],
+ "names": ["Juliet Shen", "Juliet"]
+ }
+ ]
+}
diff --git a/meetings/notes/process-notes.mjs b/meetings/notes/process-notes.mjs
new file mode 100644
index 0000000..cdef4cf
--- /dev/null
+++ b/meetings/notes/process-notes.mjs
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+/**
+ * Process ROOST meeting notes from Google Docs Markdown export to GitHub Discussions format.
+ *
+ * Usage:
+ * node process-notes.mjs notes.md # from file
+ * cat notes.md | node process-notes.mjs # from stdin
+ *
+ * Transformation logic lives in transform.js. Name mapping lives in name-map.json.
+ */
+import { readFileSync, existsSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+import { loadNameMap, processNotes } from './transform.js';
+
+const configPath = join(dirname(fileURLToPath(import.meta.url)), 'name-map.json');
+
+let emailToGithub = {}, nameToGithub = {};
+if (existsSync(configPath)) {
+ ({ emailToGithub, nameToGithub } = loadNameMap(JSON.parse(readFileSync(configPath, 'utf8'))));
+} else {
+ process.stderr.write(`Warning: ${configPath} not found; name mapping disabled\n`);
+}
+
+let text;
+if (process.argv[2]) {
+ text = readFileSync(process.argv[2], 'utf8');
+} else {
+ const chunks = [];
+ for await (const chunk of process.stdin) chunks.push(chunk);
+ text = Buffer.concat(chunks).toString('utf8');
+}
+
+process.stdout.write(processNotes(text, emailToGithub, nameToGithub));
diff --git a/meetings/notes/transform.js b/meetings/notes/transform.js
new file mode 100644
index 0000000..8324b18
--- /dev/null
+++ b/meetings/notes/transform.js
@@ -0,0 +1,68 @@
+/**
+ * Pure transformation logic for processing ROOST meeting notes.
+ * Used by both process-notes.mjs (CLI) and index.html (web app).
+ */
+
+export function loadNameMap(data) {
+ const emailToGithub = {}, nameToGithub = {};
+ for (const person of data.people ?? []) {
+ for (const email of person.emails ?? [])
+ emailToGithub[email.toLowerCase()] = person.github;
+ for (const name of person.names ?? [])
+ nameToGithub[name.toLowerCase()] = person.github;
+ }
+ return { emailToGithub, nameToGithub };
+}
+
+function escapeRegex(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export function processNotes(text, emailToGithub, nameToGithub) {
+ let inActions = false;
+ const lines = text.split('\n').map(line => {
+ line = line.replace(/\s+$/, '');
+
+ if (/^## Actions\b/.test(line)) inActions = true;
+ else if (/^## /.test(line)) inActions = false;
+
+ // Issue/PR links: [\#NNN](https://github.com/ORG/REPO/issues/NNN) → #NNN
+ line = line.replace(
+ /\[\\?#(\d+)\]\(https:\/\/github\.com\/[^)]+\/(?:issues|pull)\/\d+\/?\)/g,
+ '#$1'
+ );
+
+ // [Name](mailto:email) → @github-username
+ line = line.replace(/\[([^\]]+)\]\(mailto:([^)]+)\)/g, (match, _name, email) => {
+ const github = emailToGithub[email.toLowerCase()];
+ return github ? `@${github}` : match;
+ });
+
+ // Unescape Google Docs artifacts: \! \( \) \[ \]
+ line = line.replace(/\\([!()\[\]])/g, '$1');
+
+ // * bullets → - bullets (preserving indentation)
+ line = line.replace(/^(\s*)\* /g, '$1- ');
+
+ // Actions section: ensure all list items are checklists
+ if (inActions) line = line.replace(/^(\s*)-\s+(?!\[)/, '$1- [ ] ');
+
+ return line;
+ });
+
+ let result = lines.join('\n');
+
+ // Replace bare names → @-mentions, longest names first to avoid partial matches
+ for (const [name, github] of Object.entries(nameToGithub)
+ .sort(([a], [b]) => b.length - a.length)) {
+ result = result.replace(
+ new RegExp('(?