Vibecode Savio
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Savio, centralize manually imported feedback, tag it, and connect it to feature decisions. The hard boundary is crm and support integrations, customer revenue context, workflow, and collaboration, plus participant network, synthesis workflow, and collaboration.
You are building a lean indie version of Savio. 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 ===== # Savio · indie build A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. 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 | | --- | --- | --- | | Ingestion | Manual import and per-source connectors, both first class | connectors break and a tool that only works when they work is not a tool | | Extraction | Requests identified within a conversation with the quote kept | a request is a sentence inside a longer exchange | | Deduplication | Proposed clustering with a person confirming | automatic merging of distinct requests loses the distinction permanently | | Attribution | Every request linked to an account with its own attributes | who asked is the signal; how many asked is the noise | | Runtime | TypeScript with PostgreSQL | conversations, requests and accounts are relational | ## 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 server and the import workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Conversations, requests and account attribution with real relationships. - Get it: Docker or your package manager. - Verify: psql connects and full text search returns a result - [ ] **Five hundred real support conversations** · free with an existing system - Why: Extraction quality, clustering and the difference between adjacent and identical requests only appear at volume and in real language. - Get it: Export from your support system, including the threads that contain no request at all, which are most of them. - Verify: Five hundred conversations are imported including many with no request - [ ] **Account data with segments and value** · free - Why: The whole premise is that who asked matters more than how many, which requires knowing something about the accounts beyond their names. - Get it: Export accounts with plan, seats and value from wherever that lives, and decide how it stays current. - Verify: Accounts import with segment and value attributes - [ ] **Your segments, decided before reporting** · free - Why: The reporting is built around segments, and choosing them after seeing the data means choosing whichever segmentation supports the conclusion you already had. - Get it: Decide the segments that matter to you and write down why, before generating any theme reports. - Verify: A written note names the segments and the reasoning ## 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: - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. - CRM and support integrations, customer revenue context, workflow, and collaboration - large participant panel - recording and transcription infrastructure - advanced research repository - enterprise governance If one of those is essential to you, that is the reason to keep paying for Savio, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Savio 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 feedback pulled from support conversations and attached to the accounts it came from to replace Savio. 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. ### Stack (fixed, do not substitute) - Ingestion: Manual import and per-source connectors, both first class · connectors break and a tool that only works when they work is not a tool - Extraction: Requests identified within a conversation with the quote kept · a request is a sentence inside a longer exchange - Deduplication: Proposed clustering with a person confirming · automatic merging of distinct requests loses the distinction permanently - Attribution: Every request linked to an account with its own attributes · who asked is the signal; how many asked is the noise - Runtime: TypeScript with PostgreSQL · conversations, requests and accounts are relational ### Data model (create this before Phase 1) - accounts(id, external_ref, name, plan, seats, value_minor, health, attributes_json) - people(id, account_id, email_hash, name, role) - conversations(id, source, external_id, person_id, subject, body, received_at, imported_at) - requests(id, conversation_id, quote, char_start, char_end, extracted_at, confirmed_by) - themes(id, title, description, created_at, merged_into_id) · the deduplicated grouping - request_themes(request_id, theme_id, confidence, confirmed_by, at) - decisions(theme_id, outcome, note, decided_by, at, communicated_at) · what happened and whether anyone was told - A request keeps the quote and its position in the conversation, because a paraphrased request loses the specificity that made it actionable. ### Phase 1 · Getting conversations in, both ways Build: Import conversations from a support system and by manual paste, storing the original text and its source. Handle a connector breaking without blocking manual entry. Deduplicate on the source's own identifiers. Trap: Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. Done when: Conversations import from a connector and by manual paste with equal fidelity. A broken connector does not prevent manual import. Re-importing does not duplicate, deduplicated on the source's identifiers. The original conversation text is stored rather than a summary. Do not build yet: No extraction, themes or attribution. ### Phase 2 · Finding the request inside the conversation Build: Extract candidate requests from conversations with their quote and position, for a person to confirm. Handle a conversation containing several distinct requests, and one containing none. Trap: Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. Done when: A conversation containing several distinct requests produces several. A conversation containing none produces none rather than a forced extraction. Every request keeps its quote and position and the quote resolves to the stored text. Extraction is proposed and confirmed by a person rather than applied automatically. Do not build yet: No themes, attribution or reporting yet. ### Phase 3 · The same request, twenty wordings Build: Cluster requests into themes by proposal, with a person confirming each assignment. Make unmerging possible. Keep every original request visible under its theme rather than replacing it with a summary. Trap: Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. Done when: Similar requests are proposed for the same theme and a person confirms. Every original request stays visible under its theme with its quote. Unmerging restores a request to its own theme. Two adjacent but distinct requests can be kept separate, tested with a crafted pair. Do not build yet: No attribution or decisions yet. ### Phase 4 · Who asked, not how many Build: Link every request to the account it came from, with that account's attributes. Report themes by account segment, value and count together. Never rank by count alone. Trap: Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. Done when: Every request links to an account and its attributes. Themes report count, segment breakdown and account value together. No ordering by count alone is available, verified by looking for one. A request from an unidentified person is marked as unattributed rather than being counted as an account. Do not build yet: No decisions or reporting back yet. ### Phase 5 · Closing the loop Build: Record a decision per theme with an outcome and a note. Produce the list of people to tell when a theme is delivered or declined. Track whether they were told. Trap: Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. Done when: A theme decision produces the list of people who requested it, derived from the linked conversations. Whether they were told is tracked and unreported deliveries are surfaced. A declined theme also produces a list, since declining silently is worse than declining. Export contains conversations, requests, themes, attribution and decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the support system. This reads conversations and never becomes the place they are handled. - Public roadmaps and voting, which is a different product for a different audience with different integrity problems. - Sentiment scoring, which adds a number that looks meaningful and drives decisions it cannot support. - Automatic prioritisation, which is a judgement about strategy rather than a computation over feedback. ### README must contain - That manual import is a first-class path, because connectors break while feedback keeps arriving - That requests are extracted at sentence level with the quote kept, since a thread often contains one relevant sentence - That clustering is proposed and confirmed, and originals stay visible under a theme - That themes report segment and account value beside the count, and no count-only ordering exists - That a decision produces the list of people who asked, and whether they were told is tracked ===== AGENTS.md ===== # Agent instructions · Savio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Manual import and per-source connectors, both first class, Requests identified within a conversation with the quote kept, Proposed clustering with a person confirming, Every request linked to an account with its own attributes, TypeScript with PostgreSQL. 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 - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ===== BUILD_PLAN.md ===== # Build plan · Savio A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Getting conversations in, both ways Feedback arrives from a connector or by paste, equally well. ### Steps 1. Make manual import first class Connectors break and feedback keeps arriving. Files: `src/import.ts` 2. Deduplicate on source identifiers Not on content. The same message reimported is the common case. Files: `src/dedupe.ts` 3. Store the original text A summary loses the sentence that made the request actionable. Files: `src/conversations.ts` ### Done when - [ ] Connector and manual import work equally - [ ] A broken connector does not block manual entry - [ ] Re-importing does not duplicate - [ ] Original text is stored, not a summary ### Watch out - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. ## Phase 2 · Finding the request inside the conversation A support thread yields the specific sentence that is a feature request. ### Steps 1. Extract at sentence level with position The request is usually one sentence in a longer exchange. Files: `src/extract.ts` 2. Allow zero or several per conversation Forcing one per thread is where the specificity goes. Files: `src/extract.ts` 3. Require human confirmation Extraction quality varies and a wrong request pollutes a theme. Files: `src/review.ts` ### Done when - [ ] Several requests can come from one conversation - [ ] A conversation with none produces none - [ ] Quotes resolve to the stored text - [ ] Extraction is confirmed by a person ### Watch out - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. ## Phase 3 · The same request, twenty wordings Duplicate requests group without losing distinct ones. ### Steps 1. Propose clustering, confirm by hand Adjacent but distinct asks merge wrongly and irreversibly otherwise. Files: `src/themes.ts` 2. Keep originals visible The theme summary is not a substitute for what someone actually said. Files: `src/themes.ts` 3. Support unmerging You will merge some wrongly and notice a month later. Files: `src/themes.ts`, `tests/unmerge.test.ts` ### Done when - [ ] Similar requests are proposed, not merged - [ ] Originals stay visible under a theme - [ ] Unmerging restores a request - [ ] Adjacent distinct requests stay separate ### Watch out - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. ## Phase 4 · Who asked, not how many A theme shows the accounts behind it, not just a count. ### Steps 1. Link requests to accounts Who asked is the signal. How many is the noise. Files: `src/attribution.ts` 2. Report segment and value beside count Forty trial users and three large customers are different findings. Files: `src/report.ts` 3. Provide no count-only ordering Its absence is the design. A bare count looks objective and is not. Files: `tests/no-count-ranking.test.ts` ### Done when - [ ] Every request links to an account - [ ] Themes show count, segment and value together - [ ] No count-only ordering exists - [ ] Unattributed requests are marked as such ### Watch out - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ## Not in this build - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. ## After v1, if you want it - Linking a theme to the work that delivered it, which is what makes the loop closeable automatically - Requests from sales calls and churn interviews, which are the highest value and least captured - A view of what a specific account has asked for, which is the question account managers actually have ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Customer conversations, which contain personal data and often frustration. DATABASE_URL=postgres://localhost:5432/feedback # Optional · secret. Read only. Absent means manual import, which is a supported path rather than a fallback. SUPPORT_API_KEY=a read-only key # Required. Runs over customer conversations, which is a reason to keep it local. EXTRACTION_MODEL=local # Required. Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. COUNT_ONLY_RANKING=never # Required. For extraction and clustering both. Wrong merges are not recoverable. REQUIRE_HUMAN_CONFIRMATION=always # Required. Customer conversations are personal data with a stated retention. CONVERSATION_RETENTION_DAYS=1095
You are building a lean indie version of Savio. 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 ===== # Savio · indie build A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. 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 | | --- | --- | --- | | Ingestion | Manual import and per-source connectors, both first class | connectors break and a tool that only works when they work is not a tool | | Extraction | Requests identified within a conversation with the quote kept | a request is a sentence inside a longer exchange | | Deduplication | Proposed clustering with a person confirming | automatic merging of distinct requests loses the distinction permanently | | Attribution | Every request linked to an account with its own attributes | who asked is the signal; how many asked is the noise | | Runtime | TypeScript with PostgreSQL | conversations, requests and accounts are relational | ## 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 server and the import workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Conversations, requests and account attribution with real relationships. - Get it: Docker or your package manager. - Verify: psql connects and full text search returns a result - [ ] **Five hundred real support conversations** · free with an existing system - Why: Extraction quality, clustering and the difference between adjacent and identical requests only appear at volume and in real language. - Get it: Export from your support system, including the threads that contain no request at all, which are most of them. - Verify: Five hundred conversations are imported including many with no request - [ ] **Account data with segments and value** · free - Why: The whole premise is that who asked matters more than how many, which requires knowing something about the accounts beyond their names. - Get it: Export accounts with plan, seats and value from wherever that lives, and decide how it stays current. - Verify: Accounts import with segment and value attributes - [ ] **Your segments, decided before reporting** · free - Why: The reporting is built around segments, and choosing them after seeing the data means choosing whichever segmentation supports the conclusion you already had. - Get it: Decide the segments that matter to you and write down why, before generating any theme reports. - Verify: A written note names the segments and the reasoning ## 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: - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. - CRM and support integrations, customer revenue context, workflow, and collaboration - large participant panel - recording and transcription infrastructure - advanced research repository - enterprise governance If one of those is essential to you, that is the reason to keep paying for Savio, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Savio 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 feedback pulled from support conversations and attached to the accounts it came from to replace Savio. 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. ### Stack (fixed, do not substitute) - Ingestion: Manual import and per-source connectors, both first class · connectors break and a tool that only works when they work is not a tool - Extraction: Requests identified within a conversation with the quote kept · a request is a sentence inside a longer exchange - Deduplication: Proposed clustering with a person confirming · automatic merging of distinct requests loses the distinction permanently - Attribution: Every request linked to an account with its own attributes · who asked is the signal; how many asked is the noise - Runtime: TypeScript with PostgreSQL · conversations, requests and accounts are relational ### Data model (create this before Phase 1) - accounts(id, external_ref, name, plan, seats, value_minor, health, attributes_json) - people(id, account_id, email_hash, name, role) - conversations(id, source, external_id, person_id, subject, body, received_at, imported_at) - requests(id, conversation_id, quote, char_start, char_end, extracted_at, confirmed_by) - themes(id, title, description, created_at, merged_into_id) · the deduplicated grouping - request_themes(request_id, theme_id, confidence, confirmed_by, at) - decisions(theme_id, outcome, note, decided_by, at, communicated_at) · what happened and whether anyone was told - A request keeps the quote and its position in the conversation, because a paraphrased request loses the specificity that made it actionable. ### Phase 1 · Getting conversations in, both ways Build: Import conversations from a support system and by manual paste, storing the original text and its source. Handle a connector breaking without blocking manual entry. Deduplicate on the source's own identifiers. Trap: Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. Done when: Conversations import from a connector and by manual paste with equal fidelity. A broken connector does not prevent manual import. Re-importing does not duplicate, deduplicated on the source's identifiers. The original conversation text is stored rather than a summary. Do not build yet: No extraction, themes or attribution. ### Phase 2 · Finding the request inside the conversation Build: Extract candidate requests from conversations with their quote and position, for a person to confirm. Handle a conversation containing several distinct requests, and one containing none. Trap: Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. Done when: A conversation containing several distinct requests produces several. A conversation containing none produces none rather than a forced extraction. Every request keeps its quote and position and the quote resolves to the stored text. Extraction is proposed and confirmed by a person rather than applied automatically. Do not build yet: No themes, attribution or reporting yet. ### Phase 3 · The same request, twenty wordings Build: Cluster requests into themes by proposal, with a person confirming each assignment. Make unmerging possible. Keep every original request visible under its theme rather than replacing it with a summary. Trap: Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. Done when: Similar requests are proposed for the same theme and a person confirms. Every original request stays visible under its theme with its quote. Unmerging restores a request to its own theme. Two adjacent but distinct requests can be kept separate, tested with a crafted pair. Do not build yet: No attribution or decisions yet. ### Phase 4 · Who asked, not how many Build: Link every request to the account it came from, with that account's attributes. Report themes by account segment, value and count together. Never rank by count alone. Trap: Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. Done when: Every request links to an account and its attributes. Themes report count, segment breakdown and account value together. No ordering by count alone is available, verified by looking for one. A request from an unidentified person is marked as unattributed rather than being counted as an account. Do not build yet: No decisions or reporting back yet. ### Phase 5 · Closing the loop Build: Record a decision per theme with an outcome and a note. Produce the list of people to tell when a theme is delivered or declined. Track whether they were told. Trap: Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. Done when: A theme decision produces the list of people who requested it, derived from the linked conversations. Whether they were told is tracked and unreported deliveries are surfaced. A declined theme also produces a list, since declining silently is worse than declining. Export contains conversations, requests, themes, attribution and decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the support system. This reads conversations and never becomes the place they are handled. - Public roadmaps and voting, which is a different product for a different audience with different integrity problems. - Sentiment scoring, which adds a number that looks meaningful and drives decisions it cannot support. - Automatic prioritisation, which is a judgement about strategy rather than a computation over feedback. ### README must contain - That manual import is a first-class path, because connectors break while feedback keeps arriving - That requests are extracted at sentence level with the quote kept, since a thread often contains one relevant sentence - That clustering is proposed and confirmed, and originals stay visible under a theme - That themes report segment and account value beside the count, and no count-only ordering exists - That a decision produces the list of people who asked, and whether they were told is tracked ===== AGENTS.md ===== # Agent instructions · Savio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Manual import and per-source connectors, both first class, Requests identified within a conversation with the quote kept, Proposed clustering with a person confirming, Every request linked to an account with its own attributes, TypeScript with PostgreSQL. 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 - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ===== BUILD_PLAN.md ===== # Build plan · Savio A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Getting conversations in, both ways Feedback arrives from a connector or by paste, equally well. ### Steps 1. Make manual import first class Connectors break and feedback keeps arriving. Files: `src/import.ts` 2. Deduplicate on source identifiers Not on content. The same message reimported is the common case. Files: `src/dedupe.ts` 3. Store the original text A summary loses the sentence that made the request actionable. Files: `src/conversations.ts` ### Done when - [ ] Connector and manual import work equally - [ ] A broken connector does not block manual entry - [ ] Re-importing does not duplicate - [ ] Original text is stored, not a summary ### Watch out - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. ## Phase 2 · Finding the request inside the conversation A support thread yields the specific sentence that is a feature request. ### Steps 1. Extract at sentence level with position The request is usually one sentence in a longer exchange. Files: `src/extract.ts` 2. Allow zero or several per conversation Forcing one per thread is where the specificity goes. Files: `src/extract.ts` 3. Require human confirmation Extraction quality varies and a wrong request pollutes a theme. Files: `src/review.ts` ### Done when - [ ] Several requests can come from one conversation - [ ] A conversation with none produces none - [ ] Quotes resolve to the stored text - [ ] Extraction is confirmed by a person ### Watch out - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. ## Phase 3 · The same request, twenty wordings Duplicate requests group without losing distinct ones. ### Steps 1. Propose clustering, confirm by hand Adjacent but distinct asks merge wrongly and irreversibly otherwise. Files: `src/themes.ts` 2. Keep originals visible The theme summary is not a substitute for what someone actually said. Files: `src/themes.ts` 3. Support unmerging You will merge some wrongly and notice a month later. Files: `src/themes.ts`, `tests/unmerge.test.ts` ### Done when - [ ] Similar requests are proposed, not merged - [ ] Originals stay visible under a theme - [ ] Unmerging restores a request - [ ] Adjacent distinct requests stay separate ### Watch out - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. ## Phase 4 · Who asked, not how many A theme shows the accounts behind it, not just a count. ### Steps 1. Link requests to accounts Who asked is the signal. How many is the noise. Files: `src/attribution.ts` 2. Report segment and value beside count Forty trial users and three large customers are different findings. Files: `src/report.ts` 3. Provide no count-only ordering Its absence is the design. A bare count looks objective and is not. Files: `tests/no-count-ranking.test.ts` ### Done when - [ ] Every request links to an account - [ ] Themes show count, segment and value together - [ ] No count-only ordering exists - [ ] Unattributed requests are marked as such ### Watch out - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ## Not in this build - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. ## After v1, if you want it - Linking a theme to the work that delivered it, which is what makes the loop closeable automatically - Requests from sales calls and churn interviews, which are the highest value and least captured - A view of what a specific account has asked for, which is the question account managers actually have ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Customer conversations, which contain personal data and often frustration. DATABASE_URL=postgres://localhost:5432/feedback # Optional · secret. Read only. Absent means manual import, which is a supported path rather than a fallback. SUPPORT_API_KEY=a read-only key # Required. Runs over customer conversations, which is a reason to keep it local. EXTRACTION_MODEL=local # Required. Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. COUNT_ONLY_RANKING=never # Required. For extraction and clustering both. Wrong merges are not recoverable. REQUIRE_HUMAN_CONFIRMATION=always # Required. Customer conversations are personal data with a stated retention. CONVERSATION_RETENTION_DAYS=1095
You are building a production product version of Savio. 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 ===== # Savio · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Savio, centralize manually imported feedback, tag it, and connect it to feature decisions. The hard boundary is crm and support integrations, customer revenue context, workflow, and collaboration, plus participant network, synthesis workflow, and collaboration. ## Product outcome A feedback record that knows which accounts asked for what, keeps the sentence someone actually wrote, and produces the list of people to tell when something ships. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - public HTTPS deployment - Resend API key - optional video-call links ## Explicit non-goals for v1 - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. - CRM and support integrations, customer revenue context, workflow, and collaboration - large participant panel - recording and transcription infrastructure - advanced research repository - enterprise governance ## Success criteria - Manual import is confirmed to work with the connector disabled - Quotes are confirmed to resolve to the stored conversation text - Unmerging a theme is confirmed to restore the request - No count-only ordering is confirmed to exist - A decision is confirmed to produce the list of requesters ===== BRIEF.md ===== # Build brief · Savio 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 feedback pulled from support conversations and attached to the accounts it came from to replace Savio. 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. ### Stack (fixed, do not substitute) - Ingestion: Manual import and per-source connectors, both first class · connectors break and a tool that only works when they work is not a tool - Extraction: Requests identified within a conversation with the quote kept · a request is a sentence inside a longer exchange - Deduplication: Proposed clustering with a person confirming · automatic merging of distinct requests loses the distinction permanently - Attribution: Every request linked to an account with its own attributes · who asked is the signal; how many asked is the noise - Runtime: TypeScript with PostgreSQL · conversations, requests and accounts are relational ### Data model (create this before Phase 1) - accounts(id, external_ref, name, plan, seats, value_minor, health, attributes_json) - people(id, account_id, email_hash, name, role) - conversations(id, source, external_id, person_id, subject, body, received_at, imported_at) - requests(id, conversation_id, quote, char_start, char_end, extracted_at, confirmed_by) - themes(id, title, description, created_at, merged_into_id) · the deduplicated grouping - request_themes(request_id, theme_id, confidence, confirmed_by, at) - decisions(theme_id, outcome, note, decided_by, at, communicated_at) · what happened and whether anyone was told - A request keeps the quote and its position in the conversation, because a paraphrased request loses the specificity that made it actionable. ### Phase 1 · Getting conversations in, both ways Build: Import conversations from a support system and by manual paste, storing the original text and its source. Handle a connector breaking without blocking manual entry. Deduplicate on the source's own identifiers. Trap: Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. Done when: Conversations import from a connector and by manual paste with equal fidelity. A broken connector does not prevent manual import. Re-importing does not duplicate, deduplicated on the source's identifiers. The original conversation text is stored rather than a summary. Do not build yet: No extraction, themes or attribution. ### Phase 2 · Finding the request inside the conversation Build: Extract candidate requests from conversations with their quote and position, for a person to confirm. Handle a conversation containing several distinct requests, and one containing none. Trap: Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. Done when: A conversation containing several distinct requests produces several. A conversation containing none produces none rather than a forced extraction. Every request keeps its quote and position and the quote resolves to the stored text. Extraction is proposed and confirmed by a person rather than applied automatically. Do not build yet: No themes, attribution or reporting yet. ### Phase 3 · The same request, twenty wordings Build: Cluster requests into themes by proposal, with a person confirming each assignment. Make unmerging possible. Keep every original request visible under its theme rather than replacing it with a summary. Trap: Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. Done when: Similar requests are proposed for the same theme and a person confirms. Every original request stays visible under its theme with its quote. Unmerging restores a request to its own theme. Two adjacent but distinct requests can be kept separate, tested with a crafted pair. Do not build yet: No attribution or decisions yet. ### Phase 4 · Who asked, not how many Build: Link every request to the account it came from, with that account's attributes. Report themes by account segment, value and count together. Never rank by count alone. Trap: Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. Done when: Every request links to an account and its attributes. Themes report count, segment breakdown and account value together. No ordering by count alone is available, verified by looking for one. A request from an unidentified person is marked as unattributed rather than being counted as an account. Do not build yet: No decisions or reporting back yet. ### Phase 5 · Closing the loop Build: Record a decision per theme with an outcome and a note. Produce the list of people to tell when a theme is delivered or declined. Track whether they were told. Trap: Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. Done when: A theme decision produces the list of people who requested it, derived from the linked conversations. Whether they were told is tracked and unreported deliveries are surfaced. A declined theme also produces a list, since declining silently is worse than declining. Export contains conversations, requests, themes, attribution and decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the support system. This reads conversations and never becomes the place they are handled. - Public roadmaps and voting, which is a different product for a different audience with different integrity problems. - Sentiment scoring, which adds a number that looks meaningful and drives decisions it cannot support. - Automatic prioritisation, which is a judgement about strategy rather than a computation over feedback. ### README must contain - That manual import is a first-class path, because connectors break while feedback keeps arriving - That requests are extracted at sentence level with the quote kept, since a thread often contains one relevant sentence - That clustering is proposed and confirmed, and originals stay visible under a theme - That themes report segment and account value beside the count, and no count-only ordering exists - That a decision produces the list of people who asked, and whether they were told is tracked ===== ARCHITECTURE.md ===== # Architecture · Savio ## Stack | Part | Choice | Why | | --- | --- | --- | | Ingestion | Manual import and per-source connectors, both first class | connectors break and a tool that only works when they work is not a tool | | Extraction | Requests identified within a conversation with the quote kept | a request is a sentence inside a longer exchange | | Deduplication | Proposed clustering with a person confirming | automatic merging of distinct requests loses the distinction permanently | | Attribution | Every request linked to an account with its own attributes | who asked is the signal; how many asked is the noise | | Runtime | TypeScript with PostgreSQL | conversations, requests and accounts are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Ingestion | Connector and manual import equally. | Any source, provided manual import stays first class. | | Extraction | Sentence-level requests with quotes. | Any extractor, provided a person confirms and quotes are kept. | | Themes | Proposed clustering with reversible merges. | Any clustering, provided originals stay visible and unmerging works. | | Attribution | Account linkage and segment reporting. | Not swappable for count ranking, which looks objective and is not. | | Loop | Deriving who to tell and tracking whether they were. | Any implementation, provided the list comes from the requests. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Customer conversations, which contain personal data and often frustration. - `SUPPORT_API_KEY` · optional, secret · Read only. Absent means manual import, which is a supported path rather than a fallback. - `EXTRACTION_MODEL` · required · Runs over customer conversations, which is a reason to keep it local. - `COUNT_ONLY_RANKING` · required · Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. - `REQUIRE_HUMAN_CONFIRMATION` · required · For extraction and clustering both. Wrong merges are not recoverable. - `CONVERSATION_RETENTION_DAYS` · required · Customer conversations are personal data with a stated retention. ## 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 · Savio product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Manual import and per-source connectors, both first class, Requests identified within a conversation with the quote kept, Proposed clustering with a person confirming, Every request linked to an account with its own attributes, TypeScript with PostgreSQL. - 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 - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. - Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. ===== MILESTONES.md ===== # Delivery milestones · Savio Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Getting conversations in, both ways Feedback arrives from a connector or by paste, equally well. ### Steps 1. Make manual import first class Connectors break and feedback keeps arriving. Files: `src/import.ts` 2. Deduplicate on source identifiers Not on content. The same message reimported is the common case. Files: `src/dedupe.ts` 3. Store the original text A summary loses the sentence that made the request actionable. Files: `src/conversations.ts` ### Done when - [ ] Connector and manual import work equally - [ ] A broken connector does not block manual entry - [ ] Re-importing does not duplicate - [ ] Original text is stored, not a summary ### Watch out - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. ## M2 · Finding the request inside the conversation A support thread yields the specific sentence that is a feature request. ### Steps 1. Extract at sentence level with position The request is usually one sentence in a longer exchange. Files: `src/extract.ts` 2. Allow zero or several per conversation Forcing one per thread is where the specificity goes. Files: `src/extract.ts` 3. Require human confirmation Extraction quality varies and a wrong request pollutes a theme. Files: `src/review.ts` ### Done when - [ ] Several requests can come from one conversation - [ ] A conversation with none produces none - [ ] Quotes resolve to the stored text - [ ] Extraction is confirmed by a person ### Watch out - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. ## M3 · The same request, twenty wordings Duplicate requests group without losing distinct ones. ### Steps 1. Propose clustering, confirm by hand Adjacent but distinct asks merge wrongly and irreversibly otherwise. Files: `src/themes.ts` 2. Keep originals visible The theme summary is not a substitute for what someone actually said. Files: `src/themes.ts` 3. Support unmerging You will merge some wrongly and notice a month later. Files: `src/themes.ts`, `tests/unmerge.test.ts` ### Done when - [ ] Similar requests are proposed, not merged - [ ] Originals stay visible under a theme - [ ] Unmerging restores a request - [ ] Adjacent distinct requests stay separate ### Watch out - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. ## M4 · Who asked, not how many A theme shows the accounts behind it, not just a count. ### Steps 1. Link requests to accounts Who asked is the signal. How many is the noise. Files: `src/attribution.ts` 2. Report segment and value beside count Forty trial users and three large customers are different findings. Files: `src/report.ts` 3. Provide no count-only ordering Its absence is the design. A bare count looks objective and is not. Files: `tests/no-count-ranking.test.ts` ### Done when - [ ] Every request links to an account - [ ] Themes show count, segment and value together - [ ] No count-only ordering exists - [ ] Unattributed requests are marked as such ### Watch out - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ## M5 · Closing the loop (production only) When something ships, the people who asked for it find out. ### Steps 1. Derive the tell list from requests Not from a mailing list. The people who asked are recorded. Files: `src/loop.ts` 2. Track whether they were told Unreported deliveries are the normal failure and are invisible. Files: `src/decisions.ts` 3. Produce a list for declines too Declining silently is worse than declining. Files: `src/loop.ts` ### Done when - [ ] A decision produces the list of requesters - [ ] Communication is tracked and gaps surfaced - [ ] Declines also produce a list - [ ] Export contains the full chain ### Watch out - Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. ===== OPERATIONS.md ===== # Operations · Savio ## Backup Conversations are customer data and requests represent analysis work. Back up both, apply the stated retention to backups as well, and remember these contain identified people describing problems. ## Restore Restore and confirm theme merges and their confirmations survived, since regenerating clustering without them recreates every merge decision your team already made and reverses the ones they rejected. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Unreported deliveries is the number that matters. It only grows, nobody notices, and it is the single most common failure of any feedback process. ## Incident checklist A theme prioritised on volume that turned out to be trial users is the failure this build is organised against, and the segment breakdown is what makes it visible before the decision rather than after the quarter. 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 - [ ] Manual import is confirmed to work with the connector disabled - [ ] Quotes are confirmed to resolve to the stored conversation text - [ ] Unmerging a theme is confirmed to restore the request - [ ] No count-only ordering is confirmed to exist - [ ] A decision is confirmed to produce the list of requesters ## Launch constraint Do not market omitted Savio 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 · secret. Customer conversations, which contain personal data and often frustration. DATABASE_URL=postgres://localhost:5432/feedback # Optional · secret. Read only. Absent means manual import, which is a supported path rather than a fallback. SUPPORT_API_KEY=a read-only key # Required. Runs over customer conversations, which is a reason to keep it local. EXTRACTION_MODEL=local # Required. Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. COUNT_ONLY_RANKING=never # Required. For extraction and clustering both. Wrong merges are not recoverable. REQUIRE_HUMAN_CONFIRMATION=always # Required. Customer conversations are personal data with a stated retention. CONVERSATION_RETENTION_DAYS=1095
# Savio · indie build A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. 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 | | --- | --- | --- | | Ingestion | Manual import and per-source connectors, both first class | connectors break and a tool that only works when they work is not a tool | | Extraction | Requests identified within a conversation with the quote kept | a request is a sentence inside a longer exchange | | Deduplication | Proposed clustering with a person confirming | automatic merging of distinct requests loses the distinction permanently | | Attribution | Every request linked to an account with its own attributes | who asked is the signal; how many asked is the noise | | Runtime | TypeScript with PostgreSQL | conversations, requests and accounts are relational | ## 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 server and the import workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Conversations, requests and account attribution with real relationships. - Get it: Docker or your package manager. - Verify: psql connects and full text search returns a result - [ ] **Five hundred real support conversations** · free with an existing system - Why: Extraction quality, clustering and the difference between adjacent and identical requests only appear at volume and in real language. - Get it: Export from your support system, including the threads that contain no request at all, which are most of them. - Verify: Five hundred conversations are imported including many with no request - [ ] **Account data with segments and value** · free - Why: The whole premise is that who asked matters more than how many, which requires knowing something about the accounts beyond their names. - Get it: Export accounts with plan, seats and value from wherever that lives, and decide how it stays current. - Verify: Accounts import with segment and value attributes - [ ] **Your segments, decided before reporting** · free - Why: The reporting is built around segments, and choosing them after seeing the data means choosing whichever segmentation supports the conclusion you already had. - Get it: Decide the segments that matter to you and write down why, before generating any theme reports. - Verify: A written note names the segments and the reasoning ## 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: - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. - CRM and support integrations, customer revenue context, workflow, and collaboration - large participant panel - recording and transcription infrastructure - advanced research repository - enterprise governance If one of those is essential to you, that is the reason to keep paying for Savio, and the README should say so rather than pretend.
# Build brief · Savio 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 feedback pulled from support conversations and attached to the accounts it came from to replace Savio. 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. ### Stack (fixed, do not substitute) - Ingestion: Manual import and per-source connectors, both first class · connectors break and a tool that only works when they work is not a tool - Extraction: Requests identified within a conversation with the quote kept · a request is a sentence inside a longer exchange - Deduplication: Proposed clustering with a person confirming · automatic merging of distinct requests loses the distinction permanently - Attribution: Every request linked to an account with its own attributes · who asked is the signal; how many asked is the noise - Runtime: TypeScript with PostgreSQL · conversations, requests and accounts are relational ### Data model (create this before Phase 1) - accounts(id, external_ref, name, plan, seats, value_minor, health, attributes_json) - people(id, account_id, email_hash, name, role) - conversations(id, source, external_id, person_id, subject, body, received_at, imported_at) - requests(id, conversation_id, quote, char_start, char_end, extracted_at, confirmed_by) - themes(id, title, description, created_at, merged_into_id) · the deduplicated grouping - request_themes(request_id, theme_id, confidence, confirmed_by, at) - decisions(theme_id, outcome, note, decided_by, at, communicated_at) · what happened and whether anyone was told - A request keeps the quote and its position in the conversation, because a paraphrased request loses the specificity that made it actionable. ### Phase 1 · Getting conversations in, both ways Build: Import conversations from a support system and by manual paste, storing the original text and its source. Handle a connector breaking without blocking manual entry. Deduplicate on the source's own identifiers. Trap: Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. Done when: Conversations import from a connector and by manual paste with equal fidelity. A broken connector does not prevent manual import. Re-importing does not duplicate, deduplicated on the source's identifiers. The original conversation text is stored rather than a summary. Do not build yet: No extraction, themes or attribution. ### Phase 2 · Finding the request inside the conversation Build: Extract candidate requests from conversations with their quote and position, for a person to confirm. Handle a conversation containing several distinct requests, and one containing none. Trap: Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. Done when: A conversation containing several distinct requests produces several. A conversation containing none produces none rather than a forced extraction. Every request keeps its quote and position and the quote resolves to the stored text. Extraction is proposed and confirmed by a person rather than applied automatically. Do not build yet: No themes, attribution or reporting yet. ### Phase 3 · The same request, twenty wordings Build: Cluster requests into themes by proposal, with a person confirming each assignment. Make unmerging possible. Keep every original request visible under its theme rather than replacing it with a summary. Trap: Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. Done when: Similar requests are proposed for the same theme and a person confirms. Every original request stays visible under its theme with its quote. Unmerging restores a request to its own theme. Two adjacent but distinct requests can be kept separate, tested with a crafted pair. Do not build yet: No attribution or decisions yet. ### Phase 4 · Who asked, not how many Build: Link every request to the account it came from, with that account's attributes. Report themes by account segment, value and count together. Never rank by count alone. Trap: Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. Done when: Every request links to an account and its attributes. Themes report count, segment breakdown and account value together. No ordering by count alone is available, verified by looking for one. A request from an unidentified person is marked as unattributed rather than being counted as an account. Do not build yet: No decisions or reporting back yet. ### Phase 5 · Closing the loop Build: Record a decision per theme with an outcome and a note. Produce the list of people to tell when a theme is delivered or declined. Track whether they were told. Trap: Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. Done when: A theme decision produces the list of people who requested it, derived from the linked conversations. Whether they were told is tracked and unreported deliveries are surfaced. A declined theme also produces a list, since declining silently is worse than declining. Export contains conversations, requests, themes, attribution and decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the support system. This reads conversations and never becomes the place they are handled. - Public roadmaps and voting, which is a different product for a different audience with different integrity problems. - Sentiment scoring, which adds a number that looks meaningful and drives decisions it cannot support. - Automatic prioritisation, which is a judgement about strategy rather than a computation over feedback. ### README must contain - That manual import is a first-class path, because connectors break while feedback keeps arriving - That requests are extracted at sentence level with the quote kept, since a thread often contains one relevant sentence - That clustering is proposed and confirmed, and originals stay visible under a theme - That themes report segment and account value beside the count, and no count-only ordering exists - That a decision produces the list of people who asked, and whether they were told is tracked
# Agent instructions · Savio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Manual import and per-source connectors, both first class, Requests identified within a conversation with the quote kept, Proposed clustering with a person confirming, Every request linked to an account with its own attributes, TypeScript with PostgreSQL. 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 - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering.
# Build plan · Savio A feedback aggregation tool where the value is not counting requests but knowing who asked: the same request from forty trial users and from your three largest customers are different signals, and a tool that only counts cannot tell them apart. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Getting conversations in, both ways Feedback arrives from a connector or by paste, equally well. ### Steps 1. Make manual import first class Connectors break and feedback keeps arriving. Files: `src/import.ts` 2. Deduplicate on source identifiers Not on content. The same message reimported is the common case. Files: `src/dedupe.ts` 3. Store the original text A summary loses the sentence that made the request actionable. Files: `src/conversations.ts` ### Done when - [ ] Connector and manual import work equally - [ ] A broken connector does not block manual entry - [ ] Re-importing does not duplicate - [ ] Original text is stored, not a summary ### Watch out - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. ## Phase 2 · Finding the request inside the conversation A support thread yields the specific sentence that is a feature request. ### Steps 1. Extract at sentence level with position The request is usually one sentence in a longer exchange. Files: `src/extract.ts` 2. Allow zero or several per conversation Forcing one per thread is where the specificity goes. Files: `src/extract.ts` 3. Require human confirmation Extraction quality varies and a wrong request pollutes a theme. Files: `src/review.ts` ### Done when - [ ] Several requests can come from one conversation - [ ] A conversation with none produces none - [ ] Quotes resolve to the stored text - [ ] Extraction is confirmed by a person ### Watch out - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. ## Phase 3 · The same request, twenty wordings Duplicate requests group without losing distinct ones. ### Steps 1. Propose clustering, confirm by hand Adjacent but distinct asks merge wrongly and irreversibly otherwise. Files: `src/themes.ts` 2. Keep originals visible The theme summary is not a substitute for what someone actually said. Files: `src/themes.ts` 3. Support unmerging You will merge some wrongly and notice a month later. Files: `src/themes.ts`, `tests/unmerge.test.ts` ### Done when - [ ] Similar requests are proposed, not merged - [ ] Originals stay visible under a theme - [ ] Unmerging restores a request - [ ] Adjacent distinct requests stay separate ### Watch out - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. ## Phase 4 · Who asked, not how many A theme shows the accounts behind it, not just a count. ### Steps 1. Link requests to accounts Who asked is the signal. How many is the noise. Files: `src/attribution.ts` 2. Report segment and value beside count Forty trial users and three large customers are different findings. Files: `src/report.ts` 3. Provide no count-only ordering Its absence is the design. A bare count looks objective and is not. Files: `tests/no-count-ranking.test.ts` ### Done when - [ ] Every request links to an account - [ ] Themes show count, segment and value together - [ ] No count-only ordering exists - [ ] Unattributed requests are marked as such ### Watch out - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ## Not in this build - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. ## After v1, if you want it - Linking a theme to the work that delivered it, which is what makes the loop closeable automatically - Requests from sales calls and churn interviews, which are the highest value and least captured - A view of what a specific account has asked for, which is the question account managers actually have
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Customer conversations, which contain personal data and often frustration. DATABASE_URL=postgres://localhost:5432/feedback # Optional · secret. Read only. Absent means manual import, which is a supported path rather than a fallback. SUPPORT_API_KEY=a read-only key # Required. Runs over customer conversations, which is a reason to keep it local. EXTRACTION_MODEL=local # Required. Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. COUNT_ONLY_RANKING=never # Required. For extraction and clustering both. Wrong merges are not recoverable. REQUIRE_HUMAN_CONFIRMATION=always # Required. Customer conversations are personal data with a stated retention. CONVERSATION_RETENTION_DAYS=1095
# Savio · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Savio, centralize manually imported feedback, tag it, and connect it to feature decisions. The hard boundary is crm and support integrations, customer revenue context, workflow, and collaboration, plus participant network, synthesis workflow, and collaboration. ## Product outcome A feedback record that knows which accounts asked for what, keeps the sentence someone actually wrote, and produces the list of people to tell when something ships. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - public HTTPS deployment - Resend API key - optional video-call links ## Explicit non-goals for v1 - Being the support system. - Public roadmaps and voting. - Sentiment scoring. - Automatic prioritisation. - CRM and support integrations, customer revenue context, workflow, and collaboration - large participant panel - recording and transcription infrastructure - advanced research repository - enterprise governance ## Success criteria - Manual import is confirmed to work with the connector disabled - Quotes are confirmed to resolve to the stored conversation text - Unmerging a theme is confirmed to restore the request - No count-only ordering is confirmed to exist - A decision is confirmed to produce the list of requesters
# Build brief · Savio 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 feedback pulled from support conversations and attached to the accounts it came from to replace Savio. 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. ### Stack (fixed, do not substitute) - Ingestion: Manual import and per-source connectors, both first class · connectors break and a tool that only works when they work is not a tool - Extraction: Requests identified within a conversation with the quote kept · a request is a sentence inside a longer exchange - Deduplication: Proposed clustering with a person confirming · automatic merging of distinct requests loses the distinction permanently - Attribution: Every request linked to an account with its own attributes · who asked is the signal; how many asked is the noise - Runtime: TypeScript with PostgreSQL · conversations, requests and accounts are relational ### Data model (create this before Phase 1) - accounts(id, external_ref, name, plan, seats, value_minor, health, attributes_json) - people(id, account_id, email_hash, name, role) - conversations(id, source, external_id, person_id, subject, body, received_at, imported_at) - requests(id, conversation_id, quote, char_start, char_end, extracted_at, confirmed_by) - themes(id, title, description, created_at, merged_into_id) · the deduplicated grouping - request_themes(request_id, theme_id, confidence, confirmed_by, at) - decisions(theme_id, outcome, note, decided_by, at, communicated_at) · what happened and whether anyone was told - A request keeps the quote and its position in the conversation, because a paraphrased request loses the specificity that made it actionable. ### Phase 1 · Getting conversations in, both ways Build: Import conversations from a support system and by manual paste, storing the original text and its source. Handle a connector breaking without blocking manual entry. Deduplicate on the source's own identifiers. Trap: Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. Done when: Conversations import from a connector and by manual paste with equal fidelity. A broken connector does not prevent manual import. Re-importing does not duplicate, deduplicated on the source's identifiers. The original conversation text is stored rather than a summary. Do not build yet: No extraction, themes or attribution. ### Phase 2 · Finding the request inside the conversation Build: Extract candidate requests from conversations with their quote and position, for a person to confirm. Handle a conversation containing several distinct requests, and one containing none. Trap: Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. Done when: A conversation containing several distinct requests produces several. A conversation containing none produces none rather than a forced extraction. Every request keeps its quote and position and the quote resolves to the stored text. Extraction is proposed and confirmed by a person rather than applied automatically. Do not build yet: No themes, attribution or reporting yet. ### Phase 3 · The same request, twenty wordings Build: Cluster requests into themes by proposal, with a person confirming each assignment. Make unmerging possible. Keep every original request visible under its theme rather than replacing it with a summary. Trap: Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. Done when: Similar requests are proposed for the same theme and a person confirms. Every original request stays visible under its theme with its quote. Unmerging restores a request to its own theme. Two adjacent but distinct requests can be kept separate, tested with a crafted pair. Do not build yet: No attribution or decisions yet. ### Phase 4 · Who asked, not how many Build: Link every request to the account it came from, with that account's attributes. Report themes by account segment, value and count together. Never rank by count alone. Trap: Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. Done when: Every request links to an account and its attributes. Themes report count, segment breakdown and account value together. No ordering by count alone is available, verified by looking for one. A request from an unidentified person is marked as unattributed rather than being counted as an account. Do not build yet: No decisions or reporting back yet. ### Phase 5 · Closing the loop Build: Record a decision per theme with an outcome and a note. Produce the list of people to tell when a theme is delivered or declined. Track whether they were told. Trap: Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened. Done when: A theme decision produces the list of people who requested it, derived from the linked conversations. Whether they were told is tracked and unreported deliveries are surfaced. A declined theme also produces a list, since declining silently is worse than declining. Export contains conversations, requests, themes, attribution and decisions. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the support system. This reads conversations and never becomes the place they are handled. - Public roadmaps and voting, which is a different product for a different audience with different integrity problems. - Sentiment scoring, which adds a number that looks meaningful and drives decisions it cannot support. - Automatic prioritisation, which is a judgement about strategy rather than a computation over feedback. ### README must contain - That manual import is a first-class path, because connectors break while feedback keeps arriving - That requests are extracted at sentence level with the quote kept, since a thread often contains one relevant sentence - That clustering is proposed and confirmed, and originals stay visible under a theme - That themes report segment and account value beside the count, and no count-only ordering exists - That a decision produces the list of people who asked, and whether they were told is tracked
# Architecture · Savio ## Stack | Part | Choice | Why | | --- | --- | --- | | Ingestion | Manual import and per-source connectors, both first class | connectors break and a tool that only works when they work is not a tool | | Extraction | Requests identified within a conversation with the quote kept | a request is a sentence inside a longer exchange | | Deduplication | Proposed clustering with a person confirming | automatic merging of distinct requests loses the distinction permanently | | Attribution | Every request linked to an account with its own attributes | who asked is the signal; how many asked is the noise | | Runtime | TypeScript with PostgreSQL | conversations, requests and accounts are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Ingestion | Connector and manual import equally. | Any source, provided manual import stays first class. | | Extraction | Sentence-level requests with quotes. | Any extractor, provided a person confirms and quotes are kept. | | Themes | Proposed clustering with reversible merges. | Any clustering, provided originals stay visible and unmerging works. | | Attribution | Account linkage and segment reporting. | Not swappable for count ranking, which looks objective and is not. | | Loop | Deriving who to tell and tracking whether they were. | Any implementation, provided the list comes from the requests. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Customer conversations, which contain personal data and often frustration. - `SUPPORT_API_KEY` · optional, secret · Read only. Absent means manual import, which is a supported path rather than a fallback. - `EXTRACTION_MODEL` · required · Runs over customer conversations, which is a reason to keep it local. - `COUNT_ONLY_RANKING` · required · Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. - `REQUIRE_HUMAN_CONFIRMATION` · required · For extraction and clustering both. Wrong merges are not recoverable. - `CONVERSATION_RETENTION_DAYS` · required · Customer conversations are personal data with a stated retention. ## 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 · Savio product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Manual import and per-source connectors, both first class, Requests identified within a conversation with the quote kept, Proposed clustering with a person confirming, Every request linked to an account with its own attributes, TypeScript with PostgreSQL. - 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 - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. - Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened.
# Delivery milestones · Savio Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Getting conversations in, both ways Feedback arrives from a connector or by paste, equally well. ### Steps 1. Make manual import first class Connectors break and feedback keeps arriving. Files: `src/import.ts` 2. Deduplicate on source identifiers Not on content. The same message reimported is the common case. Files: `src/dedupe.ts` 3. Store the original text A summary loses the sentence that made the request actionable. Files: `src/conversations.ts` ### Done when - [ ] Connector and manual import work equally - [ ] A broken connector does not block manual entry - [ ] Re-importing does not duplicate - [ ] Original text is stored, not a summary ### Watch out - Building only around a connector means the tool stops working entirely when the support system changes its API, and the feedback keeps arriving in the meantime. Make manual import a first-class path from the start rather than a fallback nobody tested. ## M2 · Finding the request inside the conversation A support thread yields the specific sentence that is a feature request. ### Steps 1. Extract at sentence level with position The request is usually one sentence in a longer exchange. Files: `src/extract.ts` 2. Allow zero or several per conversation Forcing one per thread is where the specificity goes. Files: `src/extract.ts` 3. Require human confirmation Extraction quality varies and a wrong request pollutes a theme. Files: `src/review.ts` ### Done when - [ ] Several requests can come from one conversation - [ ] A conversation with none produces none - [ ] Quotes resolve to the stored text - [ ] Extraction is confirmed by a person ### Watch out - Treating a whole support conversation as one feature request loses the specificity that made it useful, since a thread about a billing problem often contains one sentence that is the actual request. Extract at sentence level with the quote kept, and let a conversation produce several requests or none. ## M3 · The same request, twenty wordings Duplicate requests group without losing distinct ones. ### Steps 1. Propose clustering, confirm by hand Adjacent but distinct asks merge wrongly and irreversibly otherwise. Files: `src/themes.ts` 2. Keep originals visible The theme summary is not a substitute for what someone actually said. Files: `src/themes.ts` 3. Support unmerging You will merge some wrongly and notice a month later. Files: `src/themes.ts`, `tests/unmerge.test.ts` ### Done when - [ ] Similar requests are proposed, not merged - [ ] Originals stay visible under a theme - [ ] Unmerging restores a request - [ ] Adjacent distinct requests stay separate ### Watch out - Automatically clustering requests merges two adjacent but distinct asks into one theme, and once the individual requests are represented only by the theme's summary the distinction is gone and cannot be recovered. Propose, confirm, keep the originals, and allow unmerging. ## M4 · Who asked, not how many A theme shows the accounts behind it, not just a count. ### Steps 1. Link requests to accounts Who asked is the signal. How many is the noise. Files: `src/attribution.ts` 2. Report segment and value beside count Forty trial users and three large customers are different findings. Files: `src/report.ts` 3. Provide no count-only ordering Its absence is the design. A bare count looks objective and is not. Files: `tests/no-count-ranking.test.ts` ### Done when - [ ] Every request links to an account - [ ] Themes show count, segment and value together - [ ] No count-only ordering exists - [ ] Unattributed requests are marked as such ### Watch out - Ranking themes by request count hands the roadmap to whoever contacts support most, which is frequently trial users and rarely your largest customers, and the ranking looks entirely objective. Show the segment breakdown and the account value beside the count, and refuse to provide a bare count ordering. ## M5 · Closing the loop (production only) When something ships, the people who asked for it find out. ### Steps 1. Derive the tell list from requests Not from a mailing list. The people who asked are recorded. Files: `src/loop.ts` 2. Track whether they were told Unreported deliveries are the normal failure and are invisible. Files: `src/decisions.ts` 3. Produce a list for declines too Declining silently is worse than declining. Files: `src/loop.ts` ### Done when - [ ] A decision produces the list of requesters - [ ] Communication is tracked and gaps surfaced - [ ] Declines also produce a list - [ ] Export contains the full chain ### Watch out - Recording that a theme was delivered without producing the list of people who asked means nobody is told, and the customer who raised it eighteen months ago never learns, which is the most common failure of every feedback process. Generate the list from the requests and track whether the communication happened.
# Operations · Savio ## Backup Conversations are customer data and requests represent analysis work. Back up both, apply the stated retention to backups as well, and remember these contain identified people describing problems. ## Restore Restore and confirm theme merges and their confirmations survived, since regenerating clustering without them recreates every merge decision your team already made and reverses the ones they rejected. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Unreported deliveries is the number that matters. It only grows, nobody notices, and it is the single most common failure of any feedback process. ## Incident checklist A theme prioritised on volume that turned out to be trial users is the failure this build is organised against, and the segment breakdown is what makes it visible before the decision rather than after the quarter. 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 - [ ] Manual import is confirmed to work with the connector disabled - [ ] Quotes are confirmed to resolve to the stored conversation text - [ ] Unmerging a theme is confirmed to restore the request - [ ] No count-only ordering is confirmed to exist - [ ] A decision is confirmed to produce the list of requesters ## Launch constraint Do not market omitted Savio 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 · secret. Customer conversations, which contain personal data and often frustration. DATABASE_URL=postgres://localhost:5432/feedback # Optional · secret. Read only. Absent means manual import, which is a supported path rather than a fallback. SUPPORT_API_KEY=a read-only key # Required. Runs over customer conversations, which is a reason to keep it local. EXTRACTION_MODEL=local # Required. Present as a constant. A bare count looks objective and hands the roadmap to whoever writes in most. COUNT_ONLY_RANKING=never # Required. For extraction and clustering both. Wrong merges are not recoverable. REQUIRE_HUMAN_CONFIRMATION=always # Required. Customer conversations are personal data with a stated retention. CONVERSATION_RETENTION_DAYS=1095
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Savio because people pay either for access to participants or for a research repository that keeps evidence usable across an organization. The recurring cost buys recruitment, consent, scheduling, media storage, transcription, tagging consistency, notifications, search, and retention, not just the visible interface.
xCRM and support integrations, customer revenue context, workflow, and collaboration
xlarge participant panel
xrecording and transcription infrastructure
xadvanced research repository
xenterprise governance
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Savio pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| essential | $39/workspace | $32.50/workspace | 1 paid user included; unlimited free users; 1 product; 1 roadmap. |
| professional | $79/workspace | $65.83/workspace | 1 paid user included; unlimited free users; 1 product; 5 roadmaps. |
| business | $249/workspace | $207.50/workspace | 1 paid user included; unlimited free users; unlimited products and roadmaps. |
| enterprise | custom | — | Custom users, onboarding, invoicing, security and service terms. |
free tierno free tier
billingMonthly + annual; annual billing gives 2 months free. A 14-day trial requires no card.
hidden costsOnly owner/admin/manager roles are paid; extra paid users cost $23, $39 or $49/month each depending on tier, while contributors, viewers and submitters are free.
verified 2026-08-12 · source ↗
Vibecode Savio
Kinda. The core of Savio is buildable in a weekend with the prompt on this page, but there are real gaps: CRM and support integrations, customer revenue context, workflow, and collaboration, large participant panel. Read the honest list above before committing.
How much does Savio cost?
Savio costs about $39/month (Essential, checked 2026-08-12), which is $468 per year.
What do I lose by replacing Savio?
Honestly: CRM and support integrations, customer revenue context, workflow, and collaboration; large participant panel; recording and transcription infrastructure; advanced research repository; enterprise governance. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Savio?
Yes: Quackback (A central feedback inbox with tags, duplicate merging, roadmap decisions and issue-tracker sync.) The prompt is for when you want it exactly your way.