Vibecode Tactiq
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Tactiq, capture browser meeting captions, clean the transcript, and generate notes on demand. The hard boundary is browser integration polish, multilingual handling, sharing, and automations, plus capture reliability, integrations, and collaboration.
You are building a lean indie version of Tactiq. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Tactiq · indie build A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension observing the caption region of the meeting page | the platform already produces captions and reading them needs no audio access at all | | Runtime | TypeScript with a local application and SQLite | transcripts of colleagues' conversations are better kept off a server | | Fallback | whisper.cpp for imported recordings | when captions were off or unavailable, a recording you were entitled to make is the other route | | Speakers | Labels from the platform where it provides them, corrected by hand | platform captions often attribute speakers, and where they do not, guessing is worse than asking | | Notes | Generated on demand with segment citations | a summary stating something nobody said is worse than no summary | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Your recording consent position** · free to read; an hour of advice if this is for work - Why: The rules apply to the recording rather than the mechanism, and several jurisdictions require every participant to agree. Capturing invisibly does not change that. - Get it: Identify where participants typically join from, take the strictest applicable rule, and record what you concluded and when. - Verify: A written note names the jurisdictions and the rule applied - [ ] **A meeting platform with live captions you use** · free with an existing account - Why: The extension reads that platform's caption region specifically, and every platform renders them differently. - Get it: Whichever you actually meet on. Turn captions on and observe how the region updates before writing any code. - Verify: Captions appear and you can identify their elements in the page - [ ] **Three real meetings you may capture** · free - Why: Caption revision, interleaved speakers and platform quirks only appear in real conversation with several people. - Get it: Ask colleagues, explain what you are testing, and disclose as you would in normal use. - Verify: Three captured meetings exist with disclosure records - [ ] **whisper.cpp with a model** (optional) · free, but transcription takes real machine time - Why: Phase 5 only, for recordings where captions were unavailable. - Get it: Build whisper.cpp and download a model sized for your machine. - Verify: It transcribes a short recording with word timings ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. - browser integration polish, multilingual handling, sharing, and automations - calendar auto-join - reliable speaker diarization - mobile capture - team search and sharing If one of those is essential to you, that is the reason to keep paying for Tactiq, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Tactiq The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me meeting notes from the captions already on your screen, with no bot in the room to replace Tactiq. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Capturing without a bot is technically easier and ethically harder. A bot in the participant list is visible and can be objected to; an extension reading the captions on your screen is invisible to everyone else in the call, and they have no way to know it is happening or to say no. That does not make it acceptable by default. Recording consent rules apply to the recording rather than to the mechanism, and several jurisdictions require every participant to agree. So say at the start of the meeting that you are capturing the transcript, make that prompt part of starting a capture rather than something you remember, and stop if anyone objects. The convenience of an invisible capture is exactly why the disclosure has to be deliberate. ### Stack (fixed, do not substitute) - Capture: A browser extension observing the caption region of the meeting page · the platform already produces captions and reading them needs no audio access at all - Runtime: TypeScript with a local application and SQLite · transcripts of colleagues' conversations are better kept off a server - Fallback: whisper.cpp for imported recordings · when captions were off or unavailable, a recording you were entitled to make is the other route - Speakers: Labels from the platform where it provides them, corrected by hand · platform captions often attribute speakers, and where they do not, guessing is worse than asking - Notes: Generated on demand with segment citations · a summary stating something nobody said is worse than no summary ### Data model (create this before Phase 1) - meetings(id, platform, title, started_at, ended_at, disclosed_at, capture_method) · capture_method is captions or recording - lines(id, meeting_id, ordinal, at_ms, speaker, text, source) · source is platform_caption or transcription - segments(id, meeting_id, start_ms, end_ms, speaker, text) · cleaned and merged from lines - notes(id, meeting_id, kind, body, cited_segment_ids_json, validated, generated_at) - corrections(id, meeting_id, segment_id, field, before, after, at) · speaker and text fixes, kept so a regeneration uses them - A meeting has a disclosure timestamp or it does not, and one without is visibly marked as undisclosed rather than treated the same. ### Phase 1 · Reading captions as they appear Build: An extension observing the meeting page's caption region, capturing each line with its time and its speaker attribution where the platform gives one. Handle the caption area rewriting itself as a sentence is refined, which platforms do constantly. Trap: Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. Done when: A real meeting's captions are captured as lines with times and speakers. Revised captions are stored once in their final form rather than as several partial versions, verified by reading a captured transcript. A speaker change is reflected in the attribution. The extension survives the meeting page being reloaded mid-call. Do not build yet: No disclosure prompt, cleaning or notes. ### Phase 2 · Disclosure as part of starting Build: Make starting a capture present a prompt to disclose, with suggested wording to paste into the chat or say aloud, and record the disclosure time. Allow proceeding without it but mark the meeting as undisclosed everywhere it appears. Trap: A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. Done when: Starting a capture presents the disclosure prompt every time rather than once. The disclosure time is recorded when confirmed. A meeting captured without disclosure is marked as such in the list and on its notes. There is no setting that permanently suppresses the prompt. Do not build yet: No cleaning or notes yet. ### Phase 3 · Cleaning a caption transcript Build: Merge consecutive lines from one speaker into segments, remove duplicated overlaps that caption revision produces, and normalise the obvious recogniser artefacts. Keep the raw lines so cleaning can be improved without recapturing. Trap: Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. Done when: Consecutive lines from one speaker merge into readable segments. Overlapping text from caption revision appears once. Raw lines are retained and cleaning can be re-run to produce different segments. A transcript with two speakers interleaving is segmented without mixing their words. Do not build yet: No notes or import yet. ### Phase 4 · Notes that cite the transcript Build: Generate a summary, decisions and actions from numbered segments, requiring a citation per line. Validate every citation exists and reject the whole output otherwise. Attribute an action only where a segment names an owner. Trap: Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. Done when: Every note line cites a segment that resolves to real text. A crafted output citing a nonexistent segment is rejected, tested against the validator directly. Clicking a citation shows the segment and its time. An action with no owner named in its cited segment is left unassigned. Do not build yet: No import or search yet. ### Phase 5 · Recordings, when captions were not available Build: Import an audio or video recording and transcribe it locally with word timings. Require the same disclosure record as a caption capture. Feed the result into the same cleaning and notes pipeline. Trap: Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. Done when: An imported recording transcribes with word timings and flows into the same pipeline. Importing requires a disclosure record and refuses without one. Notes generated from an import cite segments the same way. Transcription runs locally, verified with networking disabled. Do not build yet: Search and retention come last. ### Phase 6 · Search, retention and sharing carefully Build: Full text search across transcripts with date and speaker filters. Retention deleting transcripts past a period, with the disclosure record kept longer. Sharing a transcript requires a deliberate action and records who it went to. Trap: Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. Done when: Search across meetings returns the moment. Retention deletes transcripts past their period and the disclosure record survives. Sharing prompts about the other participants and records the recipient. A deletion request removes a meeting's transcript, segments and notes together. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable capture across every meeting platform, since each renders captions differently and changes without notice, which is the maintenance the hosted product absorbs. - Multilingual caption handling, where platform quality varies enormously and a bad transcript produces confidently wrong notes. - Calendar integration that joins meetings automatically, which is the convenience feature and also the one that captures meetings you did not think about. - Speaker diarisation for imports where the platform gave no attribution, which is unreliable enough that manual correction is the honest answer. ### README must contain - That capturing without a bot is invisible to other participants, which makes disclosure more important rather than less - That the disclosure prompt is part of starting a capture and cannot be permanently suppressed - That caption revision is deduplicated and raw lines are kept so cleaning can improve - That every note line cites a segment and an uncited output is rejected - That sharing a transcript prompts about the other participants ===== AGENTS.md ===== # Agent instructions · Tactiq indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension observing the caption region of the meeting page, TypeScript with a local application and SQLite, whisper.cpp for imported recordings, Labels from the platform where it provides them, corrected by hand, Generated on demand with segment citations. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ===== BUILD_PLAN.md ===== # Build plan · Tactiq A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading captions as they appear Live captions from a real meeting arrive in your store as they are spoken. ### Steps 1. Observe the caption region Watching for changes rather than polling, and identifying the caption elements the platform uses. Files: `extension/observe.ts` 2. Store final text per caption element Once it stops changing. Every intermediate version is the naive failure. Files: `extension/dedupe.ts` 3. Survive a page reload Meetings get reloaded. Losing the transcript at that point loses the meeting. Files: `extension/persist.ts` ### Done when - [ ] Real captions capture with times and speakers - [ ] Revised captions store once in final form - [ ] Speaker changes are reflected - [ ] A mid-call page reload does not lose the transcript ### Watch out - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. ## Phase 2 · Disclosure as part of starting You cannot start a capture without being prompted to tell the room. ### Steps 1. Put the prompt in the start flow Every capture, not a one-time preference. This is the whole control. Files: `extension/start.ts` 2. Offer wording to use One sentence to paste in the chat. Making it easy is what makes it happen. Files: `config/disclosure.md` 3. Mark undisclosed meetings In the list and on any output. Not identical to a disclosed one. Files: `src/meetings.ts` ### Done when - [ ] The prompt appears on every capture start - [ ] Disclosure time is recorded when confirmed - [ ] An undisclosed meeting is visibly marked - [ ] No setting permanently suppresses the prompt ### Watch out - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. ## Phase 3 · Cleaning a caption transcript The transcript reads as speech rather than as caption fragments. ### Steps 1. Merge by speaker and proximity Consecutive lines from one speaker within a short gap become one segment. Files: `src/clean.ts` 2. Remove revision overlaps Caption revision leaves duplicated fragments. Detect and drop them. Files: `src/clean.ts` 3. Keep raw lines So a better cleaner improves your existing meetings rather than only future ones. Files: `src/segments.ts` ### Done when - [ ] Consecutive lines merge into readable segments - [ ] Revision overlaps appear once - [ ] Re-running cleaning produces updated segments from raw lines - [ ] Interleaved speakers are segmented without mixing ### Watch out - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. ## Phase 4 · Notes that cite the transcript Every line of the summary points at a moment in the meeting. ### Steps 1. Pass numbered segments and require citations Structured output, so parsing is not a regular expression. Files: `src/notes.ts` 2. Validate before showing A pure function, unit tested with a crafted bad citation. Files: `src/validate.ts`, `src/validate.test.ts` 3. Require named owners In the cited segment, or the action stays unassigned. Files: `src/actions.ts` ### Done when - [ ] Every note line cites a real segment - [ ] A crafted bad citation is rejected - [ ] Clicking a citation shows the segment and time - [ ] An unnamed action stays unassigned ### Watch out - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. ## Phase 5 · Recordings, when captions were not available A meeting without captions can still be processed, from a recording you were entitled to make. ### Steps 1. Transcribe imports locally With word timings, using the same segment model as captions. Files: `src/import.ts` 2. Require the disclosure record Refuse the import without one. A recording is a stronger artefact, not a weaker one. Files: `src/import.ts` 3. Share the downstream pipeline One cleaning and notes path for both sources. Files: `src/segments.ts` ### Done when - [ ] An import transcribes and flows into the same pipeline - [ ] Import without a disclosure record is refused - [ ] Notes from an import cite segments identically - [ ] Transcription works with networking disabled ### Watch out - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ## Not in this build - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. ## After v1, if you want it - A pre-meeting note to invitees, which is better practice than disclosing at the start - Cross-meeting search for a topic, which is where an archive starts paying - Exporting notes into wherever your team already writes things down ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Transcripts and notes. Contains colleagues' words, so treat backups accordingly. DB_PATH=~/.local/share/meetings/meetings.db # Required. Suggested wording offered at capture start. Configuration so it fits your context. DISCLOSURE_TEXT=./config/disclosure.md # Required. How long transcripts are kept, per your written policy. TRANSCRIPT_RETENTION_DAYS=180 # Required. Longer than the transcript, because it is the evidence the capture was disclosed. DISCLOSURE_RETENTION_DAYS=1825 # Required. Which model generates notes. Local means transcripts never leave the machine. NOTES_MODEL=local
You are building a lean indie version of Tactiq. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Tactiq · indie build A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension observing the caption region of the meeting page | the platform already produces captions and reading them needs no audio access at all | | Runtime | TypeScript with a local application and SQLite | transcripts of colleagues' conversations are better kept off a server | | Fallback | whisper.cpp for imported recordings | when captions were off or unavailable, a recording you were entitled to make is the other route | | Speakers | Labels from the platform where it provides them, corrected by hand | platform captions often attribute speakers, and where they do not, guessing is worse than asking | | Notes | Generated on demand with segment citations | a summary stating something nobody said is worse than no summary | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Your recording consent position** · free to read; an hour of advice if this is for work - Why: The rules apply to the recording rather than the mechanism, and several jurisdictions require every participant to agree. Capturing invisibly does not change that. - Get it: Identify where participants typically join from, take the strictest applicable rule, and record what you concluded and when. - Verify: A written note names the jurisdictions and the rule applied - [ ] **A meeting platform with live captions you use** · free with an existing account - Why: The extension reads that platform's caption region specifically, and every platform renders them differently. - Get it: Whichever you actually meet on. Turn captions on and observe how the region updates before writing any code. - Verify: Captions appear and you can identify their elements in the page - [ ] **Three real meetings you may capture** · free - Why: Caption revision, interleaved speakers and platform quirks only appear in real conversation with several people. - Get it: Ask colleagues, explain what you are testing, and disclose as you would in normal use. - Verify: Three captured meetings exist with disclosure records - [ ] **whisper.cpp with a model** (optional) · free, but transcription takes real machine time - Why: Phase 5 only, for recordings where captions were unavailable. - Get it: Build whisper.cpp and download a model sized for your machine. - Verify: It transcribes a short recording with word timings ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. - browser integration polish, multilingual handling, sharing, and automations - calendar auto-join - reliable speaker diarization - mobile capture - team search and sharing If one of those is essential to you, that is the reason to keep paying for Tactiq, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Tactiq The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me meeting notes from the captions already on your screen, with no bot in the room to replace Tactiq. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Capturing without a bot is technically easier and ethically harder. A bot in the participant list is visible and can be objected to; an extension reading the captions on your screen is invisible to everyone else in the call, and they have no way to know it is happening or to say no. That does not make it acceptable by default. Recording consent rules apply to the recording rather than to the mechanism, and several jurisdictions require every participant to agree. So say at the start of the meeting that you are capturing the transcript, make that prompt part of starting a capture rather than something you remember, and stop if anyone objects. The convenience of an invisible capture is exactly why the disclosure has to be deliberate. ### Stack (fixed, do not substitute) - Capture: A browser extension observing the caption region of the meeting page · the platform already produces captions and reading them needs no audio access at all - Runtime: TypeScript with a local application and SQLite · transcripts of colleagues' conversations are better kept off a server - Fallback: whisper.cpp for imported recordings · when captions were off or unavailable, a recording you were entitled to make is the other route - Speakers: Labels from the platform where it provides them, corrected by hand · platform captions often attribute speakers, and where they do not, guessing is worse than asking - Notes: Generated on demand with segment citations · a summary stating something nobody said is worse than no summary ### Data model (create this before Phase 1) - meetings(id, platform, title, started_at, ended_at, disclosed_at, capture_method) · capture_method is captions or recording - lines(id, meeting_id, ordinal, at_ms, speaker, text, source) · source is platform_caption or transcription - segments(id, meeting_id, start_ms, end_ms, speaker, text) · cleaned and merged from lines - notes(id, meeting_id, kind, body, cited_segment_ids_json, validated, generated_at) - corrections(id, meeting_id, segment_id, field, before, after, at) · speaker and text fixes, kept so a regeneration uses them - A meeting has a disclosure timestamp or it does not, and one without is visibly marked as undisclosed rather than treated the same. ### Phase 1 · Reading captions as they appear Build: An extension observing the meeting page's caption region, capturing each line with its time and its speaker attribution where the platform gives one. Handle the caption area rewriting itself as a sentence is refined, which platforms do constantly. Trap: Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. Done when: A real meeting's captions are captured as lines with times and speakers. Revised captions are stored once in their final form rather than as several partial versions, verified by reading a captured transcript. A speaker change is reflected in the attribution. The extension survives the meeting page being reloaded mid-call. Do not build yet: No disclosure prompt, cleaning or notes. ### Phase 2 · Disclosure as part of starting Build: Make starting a capture present a prompt to disclose, with suggested wording to paste into the chat or say aloud, and record the disclosure time. Allow proceeding without it but mark the meeting as undisclosed everywhere it appears. Trap: A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. Done when: Starting a capture presents the disclosure prompt every time rather than once. The disclosure time is recorded when confirmed. A meeting captured without disclosure is marked as such in the list and on its notes. There is no setting that permanently suppresses the prompt. Do not build yet: No cleaning or notes yet. ### Phase 3 · Cleaning a caption transcript Build: Merge consecutive lines from one speaker into segments, remove duplicated overlaps that caption revision produces, and normalise the obvious recogniser artefacts. Keep the raw lines so cleaning can be improved without recapturing. Trap: Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. Done when: Consecutive lines from one speaker merge into readable segments. Overlapping text from caption revision appears once. Raw lines are retained and cleaning can be re-run to produce different segments. A transcript with two speakers interleaving is segmented without mixing their words. Do not build yet: No notes or import yet. ### Phase 4 · Notes that cite the transcript Build: Generate a summary, decisions and actions from numbered segments, requiring a citation per line. Validate every citation exists and reject the whole output otherwise. Attribute an action only where a segment names an owner. Trap: Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. Done when: Every note line cites a segment that resolves to real text. A crafted output citing a nonexistent segment is rejected, tested against the validator directly. Clicking a citation shows the segment and its time. An action with no owner named in its cited segment is left unassigned. Do not build yet: No import or search yet. ### Phase 5 · Recordings, when captions were not available Build: Import an audio or video recording and transcribe it locally with word timings. Require the same disclosure record as a caption capture. Feed the result into the same cleaning and notes pipeline. Trap: Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. Done when: An imported recording transcribes with word timings and flows into the same pipeline. Importing requires a disclosure record and refuses without one. Notes generated from an import cite segments the same way. Transcription runs locally, verified with networking disabled. Do not build yet: Search and retention come last. ### Phase 6 · Search, retention and sharing carefully Build: Full text search across transcripts with date and speaker filters. Retention deleting transcripts past a period, with the disclosure record kept longer. Sharing a transcript requires a deliberate action and records who it went to. Trap: Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. Done when: Search across meetings returns the moment. Retention deletes transcripts past their period and the disclosure record survives. Sharing prompts about the other participants and records the recipient. A deletion request removes a meeting's transcript, segments and notes together. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable capture across every meeting platform, since each renders captions differently and changes without notice, which is the maintenance the hosted product absorbs. - Multilingual caption handling, where platform quality varies enormously and a bad transcript produces confidently wrong notes. - Calendar integration that joins meetings automatically, which is the convenience feature and also the one that captures meetings you did not think about. - Speaker diarisation for imports where the platform gave no attribution, which is unreliable enough that manual correction is the honest answer. ### README must contain - That capturing without a bot is invisible to other participants, which makes disclosure more important rather than less - That the disclosure prompt is part of starting a capture and cannot be permanently suppressed - That caption revision is deduplicated and raw lines are kept so cleaning can improve - That every note line cites a segment and an uncited output is rejected - That sharing a transcript prompts about the other participants ===== AGENTS.md ===== # Agent instructions · Tactiq indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension observing the caption region of the meeting page, TypeScript with a local application and SQLite, whisper.cpp for imported recordings, Labels from the platform where it provides them, corrected by hand, Generated on demand with segment citations. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ===== BUILD_PLAN.md ===== # Build plan · Tactiq A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading captions as they appear Live captions from a real meeting arrive in your store as they are spoken. ### Steps 1. Observe the caption region Watching for changes rather than polling, and identifying the caption elements the platform uses. Files: `extension/observe.ts` 2. Store final text per caption element Once it stops changing. Every intermediate version is the naive failure. Files: `extension/dedupe.ts` 3. Survive a page reload Meetings get reloaded. Losing the transcript at that point loses the meeting. Files: `extension/persist.ts` ### Done when - [ ] Real captions capture with times and speakers - [ ] Revised captions store once in final form - [ ] Speaker changes are reflected - [ ] A mid-call page reload does not lose the transcript ### Watch out - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. ## Phase 2 · Disclosure as part of starting You cannot start a capture without being prompted to tell the room. ### Steps 1. Put the prompt in the start flow Every capture, not a one-time preference. This is the whole control. Files: `extension/start.ts` 2. Offer wording to use One sentence to paste in the chat. Making it easy is what makes it happen. Files: `config/disclosure.md` 3. Mark undisclosed meetings In the list and on any output. Not identical to a disclosed one. Files: `src/meetings.ts` ### Done when - [ ] The prompt appears on every capture start - [ ] Disclosure time is recorded when confirmed - [ ] An undisclosed meeting is visibly marked - [ ] No setting permanently suppresses the prompt ### Watch out - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. ## Phase 3 · Cleaning a caption transcript The transcript reads as speech rather than as caption fragments. ### Steps 1. Merge by speaker and proximity Consecutive lines from one speaker within a short gap become one segment. Files: `src/clean.ts` 2. Remove revision overlaps Caption revision leaves duplicated fragments. Detect and drop them. Files: `src/clean.ts` 3. Keep raw lines So a better cleaner improves your existing meetings rather than only future ones. Files: `src/segments.ts` ### Done when - [ ] Consecutive lines merge into readable segments - [ ] Revision overlaps appear once - [ ] Re-running cleaning produces updated segments from raw lines - [ ] Interleaved speakers are segmented without mixing ### Watch out - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. ## Phase 4 · Notes that cite the transcript Every line of the summary points at a moment in the meeting. ### Steps 1. Pass numbered segments and require citations Structured output, so parsing is not a regular expression. Files: `src/notes.ts` 2. Validate before showing A pure function, unit tested with a crafted bad citation. Files: `src/validate.ts`, `src/validate.test.ts` 3. Require named owners In the cited segment, or the action stays unassigned. Files: `src/actions.ts` ### Done when - [ ] Every note line cites a real segment - [ ] A crafted bad citation is rejected - [ ] Clicking a citation shows the segment and time - [ ] An unnamed action stays unassigned ### Watch out - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. ## Phase 5 · Recordings, when captions were not available A meeting without captions can still be processed, from a recording you were entitled to make. ### Steps 1. Transcribe imports locally With word timings, using the same segment model as captions. Files: `src/import.ts` 2. Require the disclosure record Refuse the import without one. A recording is a stronger artefact, not a weaker one. Files: `src/import.ts` 3. Share the downstream pipeline One cleaning and notes path for both sources. Files: `src/segments.ts` ### Done when - [ ] An import transcribes and flows into the same pipeline - [ ] Import without a disclosure record is refused - [ ] Notes from an import cite segments identically - [ ] Transcription works with networking disabled ### Watch out - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ## Not in this build - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. ## After v1, if you want it - A pre-meeting note to invitees, which is better practice than disclosing at the start - Cross-meeting search for a topic, which is where an archive starts paying - Exporting notes into wherever your team already writes things down ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Transcripts and notes. Contains colleagues' words, so treat backups accordingly. DB_PATH=~/.local/share/meetings/meetings.db # Required. Suggested wording offered at capture start. Configuration so it fits your context. DISCLOSURE_TEXT=./config/disclosure.md # Required. How long transcripts are kept, per your written policy. TRANSCRIPT_RETENTION_DAYS=180 # Required. Longer than the transcript, because it is the evidence the capture was disclosed. DISCLOSURE_RETENTION_DAYS=1825 # Required. Which model generates notes. Local means transcripts never leave the machine. NOTES_MODEL=local
You are building a production product version of Tactiq. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Tactiq · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Tactiq, capture browser meeting captions, clean the transcript, and generate notes on demand. The hard boundary is browser integration polish, multilingual handling, sharing, and automations, plus capture reliability, integrations, and collaboration. ## Product outcome Meeting notes taken from captions that were already on screen, where the room was told, every claim can be played back, and sharing someone else's words is a deliberate act. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop microphone access - whisper.cpp model files - optional OpenAI or Anthropic API key - local storage ## Explicit non-goals for v1 - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. - browser integration polish, multilingual handling, sharing, and automations - calendar auto-join - reliable speaker diarization - mobile capture - team search and sharing ## Success criteria - Revised captions are confirmed to store once in final form - The disclosure prompt is confirmed to appear on every capture - A crafted bad citation is rejected by the validator - An import without a disclosure record is confirmed refused - Retention is confirmed to keep the disclosure record ===== BRIEF.md ===== # Build brief · Tactiq The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me meeting notes from the captions already on your screen, with no bot in the room to replace Tactiq. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Capturing without a bot is technically easier and ethically harder. A bot in the participant list is visible and can be objected to; an extension reading the captions on your screen is invisible to everyone else in the call, and they have no way to know it is happening or to say no. That does not make it acceptable by default. Recording consent rules apply to the recording rather than to the mechanism, and several jurisdictions require every participant to agree. So say at the start of the meeting that you are capturing the transcript, make that prompt part of starting a capture rather than something you remember, and stop if anyone objects. The convenience of an invisible capture is exactly why the disclosure has to be deliberate. ### Stack (fixed, do not substitute) - Capture: A browser extension observing the caption region of the meeting page · the platform already produces captions and reading them needs no audio access at all - Runtime: TypeScript with a local application and SQLite · transcripts of colleagues' conversations are better kept off a server - Fallback: whisper.cpp for imported recordings · when captions were off or unavailable, a recording you were entitled to make is the other route - Speakers: Labels from the platform where it provides them, corrected by hand · platform captions often attribute speakers, and where they do not, guessing is worse than asking - Notes: Generated on demand with segment citations · a summary stating something nobody said is worse than no summary ### Data model (create this before Phase 1) - meetings(id, platform, title, started_at, ended_at, disclosed_at, capture_method) · capture_method is captions or recording - lines(id, meeting_id, ordinal, at_ms, speaker, text, source) · source is platform_caption or transcription - segments(id, meeting_id, start_ms, end_ms, speaker, text) · cleaned and merged from lines - notes(id, meeting_id, kind, body, cited_segment_ids_json, validated, generated_at) - corrections(id, meeting_id, segment_id, field, before, after, at) · speaker and text fixes, kept so a regeneration uses them - A meeting has a disclosure timestamp or it does not, and one without is visibly marked as undisclosed rather than treated the same. ### Phase 1 · Reading captions as they appear Build: An extension observing the meeting page's caption region, capturing each line with its time and its speaker attribution where the platform gives one. Handle the caption area rewriting itself as a sentence is refined, which platforms do constantly. Trap: Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. Done when: A real meeting's captions are captured as lines with times and speakers. Revised captions are stored once in their final form rather than as several partial versions, verified by reading a captured transcript. A speaker change is reflected in the attribution. The extension survives the meeting page being reloaded mid-call. Do not build yet: No disclosure prompt, cleaning or notes. ### Phase 2 · Disclosure as part of starting Build: Make starting a capture present a prompt to disclose, with suggested wording to paste into the chat or say aloud, and record the disclosure time. Allow proceeding without it but mark the meeting as undisclosed everywhere it appears. Trap: A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. Done when: Starting a capture presents the disclosure prompt every time rather than once. The disclosure time is recorded when confirmed. A meeting captured without disclosure is marked as such in the list and on its notes. There is no setting that permanently suppresses the prompt. Do not build yet: No cleaning or notes yet. ### Phase 3 · Cleaning a caption transcript Build: Merge consecutive lines from one speaker into segments, remove duplicated overlaps that caption revision produces, and normalise the obvious recogniser artefacts. Keep the raw lines so cleaning can be improved without recapturing. Trap: Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. Done when: Consecutive lines from one speaker merge into readable segments. Overlapping text from caption revision appears once. Raw lines are retained and cleaning can be re-run to produce different segments. A transcript with two speakers interleaving is segmented without mixing their words. Do not build yet: No notes or import yet. ### Phase 4 · Notes that cite the transcript Build: Generate a summary, decisions and actions from numbered segments, requiring a citation per line. Validate every citation exists and reject the whole output otherwise. Attribute an action only where a segment names an owner. Trap: Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. Done when: Every note line cites a segment that resolves to real text. A crafted output citing a nonexistent segment is rejected, tested against the validator directly. Clicking a citation shows the segment and its time. An action with no owner named in its cited segment is left unassigned. Do not build yet: No import or search yet. ### Phase 5 · Recordings, when captions were not available Build: Import an audio or video recording and transcribe it locally with word timings. Require the same disclosure record as a caption capture. Feed the result into the same cleaning and notes pipeline. Trap: Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. Done when: An imported recording transcribes with word timings and flows into the same pipeline. Importing requires a disclosure record and refuses without one. Notes generated from an import cite segments the same way. Transcription runs locally, verified with networking disabled. Do not build yet: Search and retention come last. ### Phase 6 · Search, retention and sharing carefully Build: Full text search across transcripts with date and speaker filters. Retention deleting transcripts past a period, with the disclosure record kept longer. Sharing a transcript requires a deliberate action and records who it went to. Trap: Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. Done when: Search across meetings returns the moment. Retention deletes transcripts past their period and the disclosure record survives. Sharing prompts about the other participants and records the recipient. A deletion request removes a meeting's transcript, segments and notes together. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable capture across every meeting platform, since each renders captions differently and changes without notice, which is the maintenance the hosted product absorbs. - Multilingual caption handling, where platform quality varies enormously and a bad transcript produces confidently wrong notes. - Calendar integration that joins meetings automatically, which is the convenience feature and also the one that captures meetings you did not think about. - Speaker diarisation for imports where the platform gave no attribution, which is unreliable enough that manual correction is the honest answer. ### README must contain - That capturing without a bot is invisible to other participants, which makes disclosure more important rather than less - That the disclosure prompt is part of starting a capture and cannot be permanently suppressed - That caption revision is deduplicated and raw lines are kept so cleaning can improve - That every note line cites a segment and an uncited output is rejected - That sharing a transcript prompts about the other participants ===== ARCHITECTURE.md ===== # Architecture · Tactiq ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension observing the caption region of the meeting page | the platform already produces captions and reading them needs no audio access at all | | Runtime | TypeScript with a local application and SQLite | transcripts of colleagues' conversations are better kept off a server | | Fallback | whisper.cpp for imported recordings | when captions were off or unavailable, a recording you were entitled to make is the other route | | Speakers | Labels from the platform where it provides them, corrected by hand | platform captions often attribute speakers, and where they do not, guessing is worse than asking | | Notes | Generated on demand with segment citations | a summary stating something nobody said is worse than no summary | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Reading captions and deduplicating revisions. | Platform specific and the part most likely to break. Keeping raw lines is what limits the damage. | | Disclosure | The prompt and the record. | Not swappable for a setting. Invisible capture is why this has to be in the flow. | | Cleaning | Turning caption fragments into segments. | Any implementation, provided raw lines survive so it can be re-run. | | Notes | Generation with validated citations. | Any model. Validation matters more here because caption transcripts are noisier. | | Retention | Deleting transcripts and keeping evidence. | Any scheduler, provided the disclosure record outlives the transcript. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Transcripts and notes. Contains colleagues' words, so treat backups accordingly. - `DISCLOSURE_TEXT` · required · Suggested wording offered at capture start. Configuration so it fits your context. - `TRANSCRIPT_RETENTION_DAYS` · required · How long transcripts are kept, per your written policy. - `DISCLOSURE_RETENTION_DAYS` · required · Longer than the transcript, because it is the evidence the capture was disclosed. - `NOTES_MODEL` · required · Which model generates notes. Local means transcripts never leave the machine. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Tactiq product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A browser extension observing the caption region of the meeting page, TypeScript with a local application and SQLite, whisper.cpp for imported recordings, Labels from the platform where it provides them, corrected by hand, Generated on demand with segment citations. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. - Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. ===== MILESTONES.md ===== # Delivery milestones · Tactiq Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading captions as they appear Live captions from a real meeting arrive in your store as they are spoken. ### Steps 1. Observe the caption region Watching for changes rather than polling, and identifying the caption elements the platform uses. Files: `extension/observe.ts` 2. Store final text per caption element Once it stops changing. Every intermediate version is the naive failure. Files: `extension/dedupe.ts` 3. Survive a page reload Meetings get reloaded. Losing the transcript at that point loses the meeting. Files: `extension/persist.ts` ### Done when - [ ] Real captions capture with times and speakers - [ ] Revised captions store once in final form - [ ] Speaker changes are reflected - [ ] A mid-call page reload does not lose the transcript ### Watch out - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. ## M2 · Disclosure as part of starting You cannot start a capture without being prompted to tell the room. ### Steps 1. Put the prompt in the start flow Every capture, not a one-time preference. This is the whole control. Files: `extension/start.ts` 2. Offer wording to use One sentence to paste in the chat. Making it easy is what makes it happen. Files: `config/disclosure.md` 3. Mark undisclosed meetings In the list and on any output. Not identical to a disclosed one. Files: `src/meetings.ts` ### Done when - [ ] The prompt appears on every capture start - [ ] Disclosure time is recorded when confirmed - [ ] An undisclosed meeting is visibly marked - [ ] No setting permanently suppresses the prompt ### Watch out - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. ## M3 · Cleaning a caption transcript The transcript reads as speech rather than as caption fragments. ### Steps 1. Merge by speaker and proximity Consecutive lines from one speaker within a short gap become one segment. Files: `src/clean.ts` 2. Remove revision overlaps Caption revision leaves duplicated fragments. Detect and drop them. Files: `src/clean.ts` 3. Keep raw lines So a better cleaner improves your existing meetings rather than only future ones. Files: `src/segments.ts` ### Done when - [ ] Consecutive lines merge into readable segments - [ ] Revision overlaps appear once - [ ] Re-running cleaning produces updated segments from raw lines - [ ] Interleaved speakers are segmented without mixing ### Watch out - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. ## M4 · Notes that cite the transcript Every line of the summary points at a moment in the meeting. ### Steps 1. Pass numbered segments and require citations Structured output, so parsing is not a regular expression. Files: `src/notes.ts` 2. Validate before showing A pure function, unit tested with a crafted bad citation. Files: `src/validate.ts`, `src/validate.test.ts` 3. Require named owners In the cited segment, or the action stays unassigned. Files: `src/actions.ts` ### Done when - [ ] Every note line cites a real segment - [ ] A crafted bad citation is rejected - [ ] Clicking a citation shows the segment and time - [ ] An unnamed action stays unassigned ### Watch out - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. ## M5 · Recordings, when captions were not available A meeting without captions can still be processed, from a recording you were entitled to make. ### Steps 1. Transcribe imports locally With word timings, using the same segment model as captions. Files: `src/import.ts` 2. Require the disclosure record Refuse the import without one. A recording is a stronger artefact, not a weaker one. Files: `src/import.ts` 3. Share the downstream pipeline One cleaning and notes path for both sources. Files: `src/segments.ts` ### Done when - [ ] An import transcribes and flows into the same pipeline - [ ] Import without a disclosure record is refused - [ ] Notes from an import cite segments identically - [ ] Transcription works with networking disabled ### Watch out - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ## M6 · Search, retention and sharing carefully (production only) You can find things across meetings, and you delete what you should. ### Steps 1. Index transcripts with timings So a result links to a moment rather than a meeting. Files: `src/search.ts` 2. Retain per artefact Transcript, notes and disclosure record have different periods, the last being longest. Files: `src/retention.ts` 3. Make sharing deliberate and recorded With a prompt about the other participants. It is their words too. Files: `src/share.ts` ### Done when - [ ] Search returns the meeting and moment - [ ] Retention deletes transcripts and keeps disclosure records - [ ] Sharing prompts about participants and records the recipient - [ ] A deletion removes transcript, segments and notes together ### Watch out - Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. ===== OPERATIONS.md ===== # Operations · Tactiq ## Backup Transcripts contain colleagues' words and disclosure records are the evidence you were entitled to capture them. Back up both, encrypted, and apply retention rather than keeping everything. ## Restore Restore and confirm disclosure records survived and that retention resumes. A restore that reinstates expired transcripts has recreated data you were required to delete. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Capture failure rate per platform and citation validation failures. Platforms change their caption markup without notice, and a silent capture failure means a meeting you thought you had. ## Incident checklist If capture stops producing lines, the platform changed its markup, which is the recurring maintenance. If notes state something nobody said, the validator missed a case and that is where the fix belongs. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Revised captions are confirmed to store once in final form - [ ] The disclosure prompt is confirmed to appear on every capture - [ ] A crafted bad citation is rejected by the validator - [ ] An import without a disclosure record is confirmed refused - [ ] Retention is confirmed to keep the disclosure record ## Launch constraint Do not market omitted Tactiq capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Transcripts and notes. Contains colleagues' words, so treat backups accordingly. DB_PATH=~/.local/share/meetings/meetings.db # Required. Suggested wording offered at capture start. Configuration so it fits your context. DISCLOSURE_TEXT=./config/disclosure.md # Required. How long transcripts are kept, per your written policy. TRANSCRIPT_RETENTION_DAYS=180 # Required. Longer than the transcript, because it is the evidence the capture was disclosed. DISCLOSURE_RETENTION_DAYS=1825 # Required. Which model generates notes. Local means transcripts never leave the machine. NOTES_MODEL=local
# Tactiq · indie build A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension observing the caption region of the meeting page | the platform already produces captions and reading them needs no audio access at all | | Runtime | TypeScript with a local application and SQLite | transcripts of colleagues' conversations are better kept off a server | | Fallback | whisper.cpp for imported recordings | when captions were off or unavailable, a recording you were entitled to make is the other route | | Speakers | Labels from the platform where it provides them, corrected by hand | platform captions often attribute speakers, and where they do not, guessing is worse than asking | | Notes | Generated on demand with segment citations | a summary stating something nobody said is worse than no summary | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Your recording consent position** · free to read; an hour of advice if this is for work - Why: The rules apply to the recording rather than the mechanism, and several jurisdictions require every participant to agree. Capturing invisibly does not change that. - Get it: Identify where participants typically join from, take the strictest applicable rule, and record what you concluded and when. - Verify: A written note names the jurisdictions and the rule applied - [ ] **A meeting platform with live captions you use** · free with an existing account - Why: The extension reads that platform's caption region specifically, and every platform renders them differently. - Get it: Whichever you actually meet on. Turn captions on and observe how the region updates before writing any code. - Verify: Captions appear and you can identify their elements in the page - [ ] **Three real meetings you may capture** · free - Why: Caption revision, interleaved speakers and platform quirks only appear in real conversation with several people. - Get it: Ask colleagues, explain what you are testing, and disclose as you would in normal use. - Verify: Three captured meetings exist with disclosure records - [ ] **whisper.cpp with a model** (optional) · free, but transcription takes real machine time - Why: Phase 5 only, for recordings where captions were unavailable. - Get it: Build whisper.cpp and download a model sized for your machine. - Verify: It transcribes a short recording with word timings ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. - browser integration polish, multilingual handling, sharing, and automations - calendar auto-join - reliable speaker diarization - mobile capture - team search and sharing If one of those is essential to you, that is the reason to keep paying for Tactiq, and the README should say so rather than pretend.
# Build brief · Tactiq The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me meeting notes from the captions already on your screen, with no bot in the room to replace Tactiq. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Capturing without a bot is technically easier and ethically harder. A bot in the participant list is visible and can be objected to; an extension reading the captions on your screen is invisible to everyone else in the call, and they have no way to know it is happening or to say no. That does not make it acceptable by default. Recording consent rules apply to the recording rather than to the mechanism, and several jurisdictions require every participant to agree. So say at the start of the meeting that you are capturing the transcript, make that prompt part of starting a capture rather than something you remember, and stop if anyone objects. The convenience of an invisible capture is exactly why the disclosure has to be deliberate. ### Stack (fixed, do not substitute) - Capture: A browser extension observing the caption region of the meeting page · the platform already produces captions and reading them needs no audio access at all - Runtime: TypeScript with a local application and SQLite · transcripts of colleagues' conversations are better kept off a server - Fallback: whisper.cpp for imported recordings · when captions were off or unavailable, a recording you were entitled to make is the other route - Speakers: Labels from the platform where it provides them, corrected by hand · platform captions often attribute speakers, and where they do not, guessing is worse than asking - Notes: Generated on demand with segment citations · a summary stating something nobody said is worse than no summary ### Data model (create this before Phase 1) - meetings(id, platform, title, started_at, ended_at, disclosed_at, capture_method) · capture_method is captions or recording - lines(id, meeting_id, ordinal, at_ms, speaker, text, source) · source is platform_caption or transcription - segments(id, meeting_id, start_ms, end_ms, speaker, text) · cleaned and merged from lines - notes(id, meeting_id, kind, body, cited_segment_ids_json, validated, generated_at) - corrections(id, meeting_id, segment_id, field, before, after, at) · speaker and text fixes, kept so a regeneration uses them - A meeting has a disclosure timestamp or it does not, and one without is visibly marked as undisclosed rather than treated the same. ### Phase 1 · Reading captions as they appear Build: An extension observing the meeting page's caption region, capturing each line with its time and its speaker attribution where the platform gives one. Handle the caption area rewriting itself as a sentence is refined, which platforms do constantly. Trap: Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. Done when: A real meeting's captions are captured as lines with times and speakers. Revised captions are stored once in their final form rather than as several partial versions, verified by reading a captured transcript. A speaker change is reflected in the attribution. The extension survives the meeting page being reloaded mid-call. Do not build yet: No disclosure prompt, cleaning or notes. ### Phase 2 · Disclosure as part of starting Build: Make starting a capture present a prompt to disclose, with suggested wording to paste into the chat or say aloud, and record the disclosure time. Allow proceeding without it but mark the meeting as undisclosed everywhere it appears. Trap: A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. Done when: Starting a capture presents the disclosure prompt every time rather than once. The disclosure time is recorded when confirmed. A meeting captured without disclosure is marked as such in the list and on its notes. There is no setting that permanently suppresses the prompt. Do not build yet: No cleaning or notes yet. ### Phase 3 · Cleaning a caption transcript Build: Merge consecutive lines from one speaker into segments, remove duplicated overlaps that caption revision produces, and normalise the obvious recogniser artefacts. Keep the raw lines so cleaning can be improved without recapturing. Trap: Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. Done when: Consecutive lines from one speaker merge into readable segments. Overlapping text from caption revision appears once. Raw lines are retained and cleaning can be re-run to produce different segments. A transcript with two speakers interleaving is segmented without mixing their words. Do not build yet: No notes or import yet. ### Phase 4 · Notes that cite the transcript Build: Generate a summary, decisions and actions from numbered segments, requiring a citation per line. Validate every citation exists and reject the whole output otherwise. Attribute an action only where a segment names an owner. Trap: Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. Done when: Every note line cites a segment that resolves to real text. A crafted output citing a nonexistent segment is rejected, tested against the validator directly. Clicking a citation shows the segment and its time. An action with no owner named in its cited segment is left unassigned. Do not build yet: No import or search yet. ### Phase 5 · Recordings, when captions were not available Build: Import an audio or video recording and transcribe it locally with word timings. Require the same disclosure record as a caption capture. Feed the result into the same cleaning and notes pipeline. Trap: Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. Done when: An imported recording transcribes with word timings and flows into the same pipeline. Importing requires a disclosure record and refuses without one. Notes generated from an import cite segments the same way. Transcription runs locally, verified with networking disabled. Do not build yet: Search and retention come last. ### Phase 6 · Search, retention and sharing carefully Build: Full text search across transcripts with date and speaker filters. Retention deleting transcripts past a period, with the disclosure record kept longer. Sharing a transcript requires a deliberate action and records who it went to. Trap: Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. Done when: Search across meetings returns the moment. Retention deletes transcripts past their period and the disclosure record survives. Sharing prompts about the other participants and records the recipient. A deletion request removes a meeting's transcript, segments and notes together. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable capture across every meeting platform, since each renders captions differently and changes without notice, which is the maintenance the hosted product absorbs. - Multilingual caption handling, where platform quality varies enormously and a bad transcript produces confidently wrong notes. - Calendar integration that joins meetings automatically, which is the convenience feature and also the one that captures meetings you did not think about. - Speaker diarisation for imports where the platform gave no attribution, which is unreliable enough that manual correction is the honest answer. ### README must contain - That capturing without a bot is invisible to other participants, which makes disclosure more important rather than less - That the disclosure prompt is part of starting a capture and cannot be permanently suppressed - That caption revision is deduplicated and raw lines are kept so cleaning can improve - That every note line cites a segment and an uncited output is rejected - That sharing a transcript prompts about the other participants
# Agent instructions · Tactiq indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension observing the caption region of the meeting page, TypeScript with a local application and SQLite, whisper.cpp for imported recordings, Labels from the platform where it provides them, corrected by hand, Generated on demand with segment citations. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one.
# Build plan · Tactiq A browser extension that reads the meeting platform's own live captions, a local store that cleans and speaker-labels the transcript, notes generated on demand with every claim citing a moment, and an import path for recordings when captions were not available. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading captions as they appear Live captions from a real meeting arrive in your store as they are spoken. ### Steps 1. Observe the caption region Watching for changes rather than polling, and identifying the caption elements the platform uses. Files: `extension/observe.ts` 2. Store final text per caption element Once it stops changing. Every intermediate version is the naive failure. Files: `extension/dedupe.ts` 3. Survive a page reload Meetings get reloaded. Losing the transcript at that point loses the meeting. Files: `extension/persist.ts` ### Done when - [ ] Real captions capture with times and speakers - [ ] Revised captions store once in final form - [ ] Speaker changes are reflected - [ ] A mid-call page reload does not lose the transcript ### Watch out - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. ## Phase 2 · Disclosure as part of starting You cannot start a capture without being prompted to tell the room. ### Steps 1. Put the prompt in the start flow Every capture, not a one-time preference. This is the whole control. Files: `extension/start.ts` 2. Offer wording to use One sentence to paste in the chat. Making it easy is what makes it happen. Files: `config/disclosure.md` 3. Mark undisclosed meetings In the list and on any output. Not identical to a disclosed one. Files: `src/meetings.ts` ### Done when - [ ] The prompt appears on every capture start - [ ] Disclosure time is recorded when confirmed - [ ] An undisclosed meeting is visibly marked - [ ] No setting permanently suppresses the prompt ### Watch out - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. ## Phase 3 · Cleaning a caption transcript The transcript reads as speech rather than as caption fragments. ### Steps 1. Merge by speaker and proximity Consecutive lines from one speaker within a short gap become one segment. Files: `src/clean.ts` 2. Remove revision overlaps Caption revision leaves duplicated fragments. Detect and drop them. Files: `src/clean.ts` 3. Keep raw lines So a better cleaner improves your existing meetings rather than only future ones. Files: `src/segments.ts` ### Done when - [ ] Consecutive lines merge into readable segments - [ ] Revision overlaps appear once - [ ] Re-running cleaning produces updated segments from raw lines - [ ] Interleaved speakers are segmented without mixing ### Watch out - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. ## Phase 4 · Notes that cite the transcript Every line of the summary points at a moment in the meeting. ### Steps 1. Pass numbered segments and require citations Structured output, so parsing is not a regular expression. Files: `src/notes.ts` 2. Validate before showing A pure function, unit tested with a crafted bad citation. Files: `src/validate.ts`, `src/validate.test.ts` 3. Require named owners In the cited segment, or the action stays unassigned. Files: `src/actions.ts` ### Done when - [ ] Every note line cites a real segment - [ ] A crafted bad citation is rejected - [ ] Clicking a citation shows the segment and time - [ ] An unnamed action stays unassigned ### Watch out - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. ## Phase 5 · Recordings, when captions were not available A meeting without captions can still be processed, from a recording you were entitled to make. ### Steps 1. Transcribe imports locally With word timings, using the same segment model as captions. Files: `src/import.ts` 2. Require the disclosure record Refuse the import without one. A recording is a stronger artefact, not a weaker one. Files: `src/import.ts` 3. Share the downstream pipeline One cleaning and notes path for both sources. Files: `src/segments.ts` ### Done when - [ ] An import transcribes and flows into the same pipeline - [ ] Import without a disclosure record is refused - [ ] Notes from an import cite segments identically - [ ] Transcription works with networking disabled ### Watch out - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ## Not in this build - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. ## After v1, if you want it - A pre-meeting note to invitees, which is better practice than disclosing at the start - Cross-meeting search for a topic, which is where an archive starts paying - Exporting notes into wherever your team already writes things down
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Transcripts and notes. Contains colleagues' words, so treat backups accordingly. DB_PATH=~/.local/share/meetings/meetings.db # Required. Suggested wording offered at capture start. Configuration so it fits your context. DISCLOSURE_TEXT=./config/disclosure.md # Required. How long transcripts are kept, per your written policy. TRANSCRIPT_RETENTION_DAYS=180 # Required. Longer than the transcript, because it is the evidence the capture was disclosed. DISCLOSURE_RETENTION_DAYS=1825 # Required. Which model generates notes. Local means transcripts never leave the machine. NOTES_MODEL=local
# Tactiq · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Tactiq, capture browser meeting captions, clean the transcript, and generate notes on demand. The hard boundary is browser integration polish, multilingual handling, sharing, and automations, plus capture reliability, integrations, and collaboration. ## Product outcome Meeting notes taken from captions that were already on screen, where the room was told, every claim can be played back, and sharing someone else's words is a deliberate act. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop microphone access - whisper.cpp model files - optional OpenAI or Anthropic API key - local storage ## Explicit non-goals for v1 - Reliable capture across every meeting platform. - Multilingual caption handling. - Calendar integration that joins automatically. - Speaker diarisation for unattributed imports. - browser integration polish, multilingual handling, sharing, and automations - calendar auto-join - reliable speaker diarization - mobile capture - team search and sharing ## Success criteria - Revised captions are confirmed to store once in final form - The disclosure prompt is confirmed to appear on every capture - A crafted bad citation is rejected by the validator - An import without a disclosure record is confirmed refused - Retention is confirmed to keep the disclosure record
# Build brief · Tactiq The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me meeting notes from the captions already on your screen, with no bot in the room to replace Tactiq. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Capturing without a bot is technically easier and ethically harder. A bot in the participant list is visible and can be objected to; an extension reading the captions on your screen is invisible to everyone else in the call, and they have no way to know it is happening or to say no. That does not make it acceptable by default. Recording consent rules apply to the recording rather than to the mechanism, and several jurisdictions require every participant to agree. So say at the start of the meeting that you are capturing the transcript, make that prompt part of starting a capture rather than something you remember, and stop if anyone objects. The convenience of an invisible capture is exactly why the disclosure has to be deliberate. ### Stack (fixed, do not substitute) - Capture: A browser extension observing the caption region of the meeting page · the platform already produces captions and reading them needs no audio access at all - Runtime: TypeScript with a local application and SQLite · transcripts of colleagues' conversations are better kept off a server - Fallback: whisper.cpp for imported recordings · when captions were off or unavailable, a recording you were entitled to make is the other route - Speakers: Labels from the platform where it provides them, corrected by hand · platform captions often attribute speakers, and where they do not, guessing is worse than asking - Notes: Generated on demand with segment citations · a summary stating something nobody said is worse than no summary ### Data model (create this before Phase 1) - meetings(id, platform, title, started_at, ended_at, disclosed_at, capture_method) · capture_method is captions or recording - lines(id, meeting_id, ordinal, at_ms, speaker, text, source) · source is platform_caption or transcription - segments(id, meeting_id, start_ms, end_ms, speaker, text) · cleaned and merged from lines - notes(id, meeting_id, kind, body, cited_segment_ids_json, validated, generated_at) - corrections(id, meeting_id, segment_id, field, before, after, at) · speaker and text fixes, kept so a regeneration uses them - A meeting has a disclosure timestamp or it does not, and one without is visibly marked as undisclosed rather than treated the same. ### Phase 1 · Reading captions as they appear Build: An extension observing the meeting page's caption region, capturing each line with its time and its speaker attribution where the platform gives one. Handle the caption area rewriting itself as a sentence is refined, which platforms do constantly. Trap: Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. Done when: A real meeting's captions are captured as lines with times and speakers. Revised captions are stored once in their final form rather than as several partial versions, verified by reading a captured transcript. A speaker change is reflected in the attribution. The extension survives the meeting page being reloaded mid-call. Do not build yet: No disclosure prompt, cleaning or notes. ### Phase 2 · Disclosure as part of starting Build: Make starting a capture present a prompt to disclose, with suggested wording to paste into the chat or say aloud, and record the disclosure time. Allow proceeding without it but mark the meeting as undisclosed everywhere it appears. Trap: A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. Done when: Starting a capture presents the disclosure prompt every time rather than once. The disclosure time is recorded when confirmed. A meeting captured without disclosure is marked as such in the list and on its notes. There is no setting that permanently suppresses the prompt. Do not build yet: No cleaning or notes yet. ### Phase 3 · Cleaning a caption transcript Build: Merge consecutive lines from one speaker into segments, remove duplicated overlaps that caption revision produces, and normalise the obvious recogniser artefacts. Keep the raw lines so cleaning can be improved without recapturing. Trap: Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. Done when: Consecutive lines from one speaker merge into readable segments. Overlapping text from caption revision appears once. Raw lines are retained and cleaning can be re-run to produce different segments. A transcript with two speakers interleaving is segmented without mixing their words. Do not build yet: No notes or import yet. ### Phase 4 · Notes that cite the transcript Build: Generate a summary, decisions and actions from numbered segments, requiring a citation per line. Validate every citation exists and reject the whole output otherwise. Attribute an action only where a segment names an owner. Trap: Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. Done when: Every note line cites a segment that resolves to real text. A crafted output citing a nonexistent segment is rejected, tested against the validator directly. Clicking a citation shows the segment and its time. An action with no owner named in its cited segment is left unassigned. Do not build yet: No import or search yet. ### Phase 5 · Recordings, when captions were not available Build: Import an audio or video recording and transcribe it locally with word timings. Require the same disclosure record as a caption capture. Feed the result into the same cleaning and notes pipeline. Trap: Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. Done when: An imported recording transcribes with word timings and flows into the same pipeline. Importing requires a disclosure record and refuses without one. Notes generated from an import cite segments the same way. Transcription runs locally, verified with networking disabled. Do not build yet: Search and retention come last. ### Phase 6 · Search, retention and sharing carefully Build: Full text search across transcripts with date and speaker filters. Retention deleting transcripts past a period, with the disclosure record kept longer. Sharing a transcript requires a deliberate action and records who it went to. Trap: Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document. Done when: Search across meetings returns the moment. Retention deletes transcripts past their period and the disclosure record survives. Sharing prompts about the other participants and records the recipient. A deletion request removes a meeting's transcript, segments and notes together. Do not build yet: Nothing after this. ### Out of scope (and why) - Reliable capture across every meeting platform, since each renders captions differently and changes without notice, which is the maintenance the hosted product absorbs. - Multilingual caption handling, where platform quality varies enormously and a bad transcript produces confidently wrong notes. - Calendar integration that joins meetings automatically, which is the convenience feature and also the one that captures meetings you did not think about. - Speaker diarisation for imports where the platform gave no attribution, which is unreliable enough that manual correction is the honest answer. ### README must contain - That capturing without a bot is invisible to other participants, which makes disclosure more important rather than less - That the disclosure prompt is part of starting a capture and cannot be permanently suppressed - That caption revision is deduplicated and raw lines are kept so cleaning can improve - That every note line cites a segment and an uncited output is rejected - That sharing a transcript prompts about the other participants
# Architecture · Tactiq ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension observing the caption region of the meeting page | the platform already produces captions and reading them needs no audio access at all | | Runtime | TypeScript with a local application and SQLite | transcripts of colleagues' conversations are better kept off a server | | Fallback | whisper.cpp for imported recordings | when captions were off or unavailable, a recording you were entitled to make is the other route | | Speakers | Labels from the platform where it provides them, corrected by hand | platform captions often attribute speakers, and where they do not, guessing is worse than asking | | Notes | Generated on demand with segment citations | a summary stating something nobody said is worse than no summary | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Reading captions and deduplicating revisions. | Platform specific and the part most likely to break. Keeping raw lines is what limits the damage. | | Disclosure | The prompt and the record. | Not swappable for a setting. Invisible capture is why this has to be in the flow. | | Cleaning | Turning caption fragments into segments. | Any implementation, provided raw lines survive so it can be re-run. | | Notes | Generation with validated citations. | Any model. Validation matters more here because caption transcripts are noisier. | | Retention | Deleting transcripts and keeping evidence. | Any scheduler, provided the disclosure record outlives the transcript. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Transcripts and notes. Contains colleagues' words, so treat backups accordingly. - `DISCLOSURE_TEXT` · required · Suggested wording offered at capture start. Configuration so it fits your context. - `TRANSCRIPT_RETENTION_DAYS` · required · How long transcripts are kept, per your written policy. - `DISCLOSURE_RETENTION_DAYS` · required · Longer than the transcript, because it is the evidence the capture was disclosed. - `NOTES_MODEL` · required · Which model generates notes. Local means transcripts never leave the machine. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Tactiq product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A browser extension observing the caption region of the meeting page, TypeScript with a local application and SQLite, whisper.cpp for imported recordings, Labels from the platform where it provides them, corrected by hand, Generated on demand with segment citations. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. - Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document.
# Delivery milestones · Tactiq Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading captions as they appear Live captions from a real meeting arrive in your store as they are spoken. ### Steps 1. Observe the caption region Watching for changes rather than polling, and identifying the caption elements the platform uses. Files: `extension/observe.ts` 2. Store final text per caption element Once it stops changing. Every intermediate version is the naive failure. Files: `extension/dedupe.ts` 3. Survive a page reload Meetings get reloaded. Losing the transcript at that point loses the meeting. Files: `extension/persist.ts` ### Done when - [ ] Real captions capture with times and speakers - [ ] Revised captions store once in final form - [ ] Speaker changes are reflected - [ ] A mid-call page reload does not lose the transcript ### Watch out - Meeting platforms revise a caption line in place as the speech recogniser improves its guess, so naive capture records every intermediate version and produces a transcript full of half-sentences. Track each caption element and store its final text when it stops changing rather than every update. ## M2 · Disclosure as part of starting You cannot start a capture without being prompted to tell the room. ### Steps 1. Put the prompt in the start flow Every capture, not a one-time preference. This is the whole control. Files: `extension/start.ts` 2. Offer wording to use One sentence to paste in the chat. Making it easy is what makes it happen. Files: `config/disclosure.md` 3. Mark undisclosed meetings In the list and on any output. Not identical to a disclosed one. Files: `src/meetings.ts` ### Done when - [ ] The prompt appears on every capture start - [ ] Disclosure time is recorded when confirmed - [ ] An undisclosed meeting is visibly marked - [ ] No setting permanently suppresses the prompt ### Watch out - A disclosure setting that defaults to off, or a reminder you can dismiss permanently, becomes a capture nobody knows about within a week. Put the prompt in the start flow, record what happened, and make an undisclosed meeting visibly different rather than identical to a disclosed one. ## M3 · Cleaning a caption transcript The transcript reads as speech rather than as caption fragments. ### Steps 1. Merge by speaker and proximity Consecutive lines from one speaker within a short gap become one segment. Files: `src/clean.ts` 2. Remove revision overlaps Caption revision leaves duplicated fragments. Detect and drop them. Files: `src/clean.ts` 3. Keep raw lines So a better cleaner improves your existing meetings rather than only future ones. Files: `src/segments.ts` ### Done when - [ ] Consecutive lines merge into readable segments - [ ] Revision overlaps appear once - [ ] Re-running cleaning produces updated segments from raw lines - [ ] Interleaved speakers are segmented without mixing ### Watch out - Cleaning in place destroys the raw capture, and caption cleaning rules always need improving, at which point the meetings you already have cannot benefit. Keep the raw lines and derive segments, so a better cleaner can be re-run over everything you have. ## M4 · Notes that cite the transcript Every line of the summary points at a moment in the meeting. ### Steps 1. Pass numbered segments and require citations Structured output, so parsing is not a regular expression. Files: `src/notes.ts` 2. Validate before showing A pure function, unit tested with a crafted bad citation. Files: `src/validate.ts`, `src/validate.test.ts` 3. Require named owners In the cited segment, or the action stays unassigned. Files: `src/actions.ts` ### Done when - [ ] Every note line cites a real segment - [ ] A crafted bad citation is rejected - [ ] Clicking a citation shows the segment and time - [ ] An unnamed action stays unassigned ### Watch out - Caption transcripts are noisier than audio transcription, with dropped words and misrecognitions, which makes a model more likely to smooth over a gap by inventing what fits. Requiring citations and validating them matters more here than with a clean transcript, not less. ## M5 · Recordings, when captions were not available A meeting without captions can still be processed, from a recording you were entitled to make. ### Steps 1. Transcribe imports locally With word timings, using the same segment model as captions. Files: `src/import.ts` 2. Require the disclosure record Refuse the import without one. A recording is a stronger artefact, not a weaker one. Files: `src/import.ts` 3. Share the downstream pipeline One cleaning and notes path for both sources. Files: `src/segments.ts` ### Done when - [ ] An import transcribes and flows into the same pipeline - [ ] Import without a disclosure record is refused - [ ] Notes from an import cite segments identically - [ ] Transcription works with networking disabled ### Watch out - Treating an imported recording as needing less disclosure than a live capture is backwards: a recording is a stronger artefact than a transcript and the consent question is the same or greater. Require the same record, and refuse to process an import without one. ## M6 · Search, retention and sharing carefully (production only) You can find things across meetings, and you delete what you should. ### Steps 1. Index transcripts with timings So a result links to a moment rather than a meeting. Files: `src/search.ts` 2. Retain per artefact Transcript, notes and disclosure record have different periods, the last being longest. Files: `src/retention.ts` 3. Make sharing deliberate and recorded With a prompt about the other participants. It is their words too. Files: `src/share.ts` ### Done when - [ ] Search returns the meeting and moment - [ ] Retention deletes transcripts and keeps disclosure records - [ ] Sharing prompts about participants and records the recipient - [ ] A deletion removes transcript, segments and notes together ### Watch out - Sharing a transcript containing what colleagues said, without their knowledge, is a second disclosure question separate from the capture. Make sharing deliberate, record it, and prompt about who is in the transcript rather than treating it as your own document.
# Operations · Tactiq ## Backup Transcripts contain colleagues' words and disclosure records are the evidence you were entitled to capture them. Back up both, encrypted, and apply retention rather than keeping everything. ## Restore Restore and confirm disclosure records survived and that retention resumes. A restore that reinstates expired transcripts has recreated data you were required to delete. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Capture failure rate per platform and citation validation failures. Platforms change their caption markup without notice, and a silent capture failure means a meeting you thought you had. ## Incident checklist If capture stops producing lines, the platform changed its markup, which is the recurring maintenance. If notes state something nobody said, the validator missed a case and that is where the fix belongs. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Revised captions are confirmed to store once in final form - [ ] The disclosure prompt is confirmed to appear on every capture - [ ] A crafted bad citation is rejected by the validator - [ ] An import without a disclosure record is confirmed refused - [ ] Retention is confirmed to keep the disclosure record ## Launch constraint Do not market omitted Tactiq capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Transcripts and notes. Contains colleagues' words, so treat backups accordingly. DB_PATH=~/.local/share/meetings/meetings.db # Required. Suggested wording offered at capture start. Configuration so it fits your context. DISCLOSURE_TEXT=./config/disclosure.md # Required. How long transcripts are kept, per your written policy. TRANSCRIPT_RETENTION_DAYS=180 # Required. Longer than the transcript, because it is the evidence the capture was disclosed. DISCLOSURE_RETENTION_DAYS=1825 # Required. Which model generates notes. Local means transcripts never leave the machine. NOTES_MODEL=local
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Tactiq because a meeting tool must capture every call without surprising anyone, then make the result searchable and shareable across a team. The recurring cost buys audio permissions, model updates, calendar APIs, storage, speaker correction, and sync, not just the visible interface.
xbrowser integration polish, multilingual handling, sharing, and automations
xcalendar auto-join
xreliable speaker diarization
xmobile capture
xteam search and sharing
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Tactiq →· no votes, no pay-to-list · just what's real
Tactiq pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 10 transcripts per month and 5 AI credits per month. |
| pro | $12/user | $8/user | Unlimited transcripts and 10 AI credits per month. |
| team | $20/user | $16.67/user | Unlimited transcripts and unlimited AI credits. |
| business | $40/user | $29.16/user | Unlimited transcripts and AI credits, plus retention controls, AI agents, customer success, and SSO. |
| enterprise | custom | — | Custom deployment for roughly 200+ users; public price and numeric usage limits are not stated. |
free tier10 transcripts per month and 5 AI credits per month.
billingmonthly + annual
hidden costsBusiness is positioned for 20+ users and Enterprise for 200+, but the public page does not state contractual seat minimums.
verified 2026-08-14 · source ↗
Vibecode Tactiq
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Tactiq replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Tactiq cost?
Tactiq costs about $12/month (Pro, checked 2026-07-31), which is $144 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Tactiq?
Honestly: browser integration polish, multilingual handling, sharing, and automations; calendar auto-join; reliable speaker diarization; mobile capture; team search and sharing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Tactiq?
Yes: OpenWhispr (Cross-platform meeting capture, transcript cleanup and AI notes without a browser extension or minute meter.) Muesli (Skips browser captions and records the actual call instead, then cleans it into notes on your Mac.) Meeting Transcriber (Auto-detects common meeting apps on Mac, transcribes locally and writes structured notes.) All 3 curated free alternatives are at vibecodeit.com/tactiq/alternatives. The prompt is for when you want it exactly your way.