Specious Coda Bishop

Customer Success · Implementation · People Ops · Operations

Drawn to where things aren't working and I find the structural reason why.

About Me

I'm an operations professional with over a decade of experience across people operations, strategic HR, community management, and technical support. The through-line across all of it is the same: I'm drawn to the places where things aren't quite working, and I find the structural reason why.

Most of my career has lived at the intersection of people and systems — which means I'm comfortable working at the human layer and the process layer simultaneously, and I tend to notice when a problem that looks like one is actually the other.

I'm also technically curious. When something interests me I go properly into it — which is how I've ended up with working knowledge of network analysis, log review, and vulnerability scanning alongside the operations background. I write tech and games coverage for Phandroid, Android Central, and Mobile Nations for the same reason: I like understanding how things work well enough to explain them clearly.

Outside of work I run LoreBySpec, a YouTube channel doing narrative and lore analysis of the Kingdom Hearts series — which is where the KH Lore Checker in this portfolio came from.

Orientation Generalist by design

A decade across HR, operations, support, and community work isn't a winding path — it's a consistent pattern of being useful wherever the system needs thinking through.

Approach Build to understand

The tools in this portfolio exist because I build things to learn them properly. Every decision, dead end, and pivot is logged — not polished after the fact, but documented as it happened.

Strength Technically curious, people fluent

I've spent years making technical information legible to non-technical audiences — in support roles, in writing, and in leadership work. The interesting problems are almost always at that boundary.

Principle Costs and constraints first

Every project here has a "Constraints" section as prominent as the wins. I document what doesn't work, what the trade-offs are, and when a design decision is a compromise.

Work Experience

Fractional Chief of Staff
Spec Solutions · Jan 2023 – Present
Customer Support Operations & Service Optimisation
  • Built and optimised customer support operations for a startup, implementing a ticket allocation system that reduced response times by 30% and increased customer satisfaction. → see how
  • Developed a comprehensive knowledge base and implemented structured training for support teams, improving self-service resolution rates.
  • Provided technical support for users and investigated app bugs, undocumented issues, and usability concerns, collaborating with developers to implement fixes.
  • Created internal documentation and process guides to improve knowledge sharing across the team as a Tech Support & QA Consultant for Joocie (via Consultancy) — Remote since Jan 2024.
International Business Expansion & Compliance
  • Led expansion projects for businesses moving overseas, navigating HR compliance, Employer of Record (EOR) solutions, and market entry strategies.
  • Secured trusted global hiring solutions and scouted operational locations to support international growth.
Internal Communications & Leadership Coaching
  • Provided leadership coaching and team development strategies to improve internal communications and alignment.
  • Implemented team-building initiatives that improved retention and cross-functional collaboration.
Community & Engagement Operations
  • Designed and implemented engagement programmes that increased onboarding efficiency and strengthened networking within startup communities.
  • Planned and executed virtual and in-person events to improve team culture and industry collaboration.
Contributing Tech Writer & Content Strategist
Phandroid · Jan 2018 – Present
  • Spearheaded content strategy and execution for a leading tech publication, driving audience growth and establishing Phandroid as a trusted resource for Android and consumer tech insights.
  • Led the development of high-impact content — from feature articles to in-depth product reviews — resulting in increased site traffic and user engagement.
  • Collaborated with cross-functional teams, aligning content production with business goals and ensuring editorial excellence.
  • Drove SEO optimisation strategies that increased content visibility and sustained long-term audience growth.
  • Delivered data-driven insights to inform content strategies, leveraging analytics to optimise performance and readership retention.
Executive Assistant / Chief of Staff
Connection Engine · May 2025 – Jul 2025 · Remote
Team Coordination & Support (30%)
  • Served as organisational glue for the Mission Circle — scheduling, maintaining rhythms and reminders, and documenting key decisions.
  • Managed communication flows and followed up on action items.
Project Management (30%)
  • Maintained the Coda-based project management system, tracked milestones, and identified potential bottlenecks.
  • Helped define and document emergent processes supporting the co-creative system.
Community Management (30%)
  • Helped cultivate the early beta launch community and support implementation of the storytelling cycle.
  • Assisted with community events and story capture.
Systems & Tools Administration (10%)
  • Maintained the CRM system, optimised workflows, and supported implementation of new tools as needed.
Community Manager
The Nook · Oct 2020 – Dec 2024 · Remote

Owner and operator of The Nook, a Discord community supporting Twitch streamers through their affiliate journey. Helped over 10 streamers reach affiliate status across a 100+ member community.

  • Managed community growth, moderation, and engagement, driving active content creation and participation.
  • Provided mentorship and strategy to streamers, maintaining a high success rate for affiliate achievement.
  • Developed and enforced community rules, ensuring a safe, welcoming, and productive space for creators.
  • Facilitated regular check-ins, feedback sessions, and networking opportunities to help creators refine their strategies.
  • Curated resources on Twitch growth — technical tips, branding strategies, and content advice.
  • Coordinated events, giveaways, and collaborations to boost visibility and community engagement.
Customer Support Hero (Technical)
Stamped · May 2020 – Jul 2020
  • Led technical support for enterprise clients, resolving complex account-related issues and improving client satisfaction by 30%. → see how
  • Partnered with development teams to support clients building custom integrations and optimising API usage.
  • Drove cross-functional collaboration to enhance troubleshooting processes and accelerate issue resolution.
  • Managed sensitive customer data with a high level of confidentiality, maintaining compliance while resolving complex technical requests.
  • Delivered technical training and resources to customers, empowering them to maximise the value of Stamped's product suite.
Editor & Operations Assistant
Can I Play That? · Aug 2018 – Feb 2020
  • Provided executive support to the CEO, driving daily operations and content strategy for a platform focused on accessibility in gaming.
  • Managed strategic partnerships with developers, PR teams, and industry professionals to secure collaborations and maintain a continuous content pipeline.
  • Led the planning and execution of the Can I Play That? Awards in 2019/2020 — from concept to delivery, including designing trophies, managing communications with recipients, and overseeing all logistics.
  • Coordinated the nomination process, collaborating with industry professionals to evaluate and select the most inclusive and innovative games of the year.
  • Managed financial operations including Patreon fundraising, ensuring the platform's stability and sustainability.
  • Grew the site's audience by 30% over two years through content editing and cross-functional collaboration.
  • Acted as liaison between the CEO, content creators, and external partners to align strategic goals and maintain operational standards.
Community Manager
Irish Tech Community · Aug 2016 – Sep 2018 · Dublin, Ireland
  • Redesigned and scaled ITC's Slack community structure — formalised roles (Owner, Admins, Mods) and responsibilities, empowering members to self-govern their areas of expertise.
  • Created comprehensive operational documentation for community management, role assignments, and onboarding, ensuring consistency as the community grew.
  • Founded the ITC Jobs Board — job postings grew by 300% over two years and the board attracted acquisition interest.
  • Increased Slack membership by 250% in two years through targeted outreach and a culture of inclusivity and knowledge-sharing.
  • Ensured a safe, welcoming environment for all members — from CEOs to interns — facilitating transparent communication and conflict resolution.
  • Developed cross-level engagement strategies ensuring everyone could participate, share ideas, and access resources.
  • Created events and initiatives that expanded members' networks and visibility, positioning ITC as the go-to hub for Irish tech talent.
Developer Advocate
Samebug · Jul 2017 – Sep 2017 · Remote

Bridged the gap between the product team and the developer community — providing expert guidance on debugging and issue resolution, facilitating technical discussions, and gathering feedback to inform product development.

  • Conducted workshops, webinars, and created content to educate developers on best practices, debugging tools, and platform features.
  • Built relationships with the developer community, ensuring feedback was heard and used to drive product improvements.
  • Provided direct platform support, helping developers troubleshoot and optimise their workflows.
  • Advocated for developer needs internally, ensuring Samebug's solutions continued to evolve in line with real challenges.
Project Manager & Research Assistant
Learnovate Centre, Trinity College Dublin · Apr 2016 – Apr 2017 · Dublin, Ireland
  • Led the €3M EU-funded DEVELOP project, collaborating with international partners to deliver research outcomes on time and in alignment with long-term objectives. → see how
  • Drove Agile methodologies to enhance team productivity and adapt to evolving project needs.
  • Managed communication across European stakeholders, providing clear updates and resolving challenges to maintain momentum.
  • Mentored new employees through onboarding and led informal team-building to strengthen workplace culture.
  • Spearheaded cross-functional collaboration, eliminating departmental silos and improving information flow.
  • Revamped internal communications, establishing a culture of feedback, transparency, and problem-solving.
  • Identified and eliminated bottlenecks, improving team alignment and resource allocation.
Customer Support Specialist
CoderDojo · Sep 2015 – Jan 2016 · Dublin, Ireland
  • Provided front-line support to CoderDojo community members — mentors, volunteers, and dojo organisers — via Zendesk.
  • Managed incoming tickets, ensuring issues were categorised, prioritised, and escalated appropriately.
  • Collaborated with the team to identify recurring issues and improve the knowledge base, creating clear support documentation for organisers and participants.
  • Delivered troubleshooting guidance on technical issues, helping volunteers with setup, software installation, and platform concerns.
  • Tracked and analysed support trends, suggesting process improvements that increased operational efficiency.
  • Assisted in onboarding new dojo organisers and volunteers, ensuring smooth introductions to tools and resources.
Product Analyst
Donnerwood Media · Jan 2015 – Apr 2015 · Dublin, Ireland
  • Conducted in-depth research on consumer demographics and purchasing behaviour to support data-driven decisions for Meez's freemium model.
  • Analysed customer behaviour data using SQL, identifying key metrics related to churn, lifetime value (LTV), and user retention.
  • Supported user segmentation strategies to tailor content and offers, increasing engagement and conversion rates.
  • Created detailed reports and presentations for senior stakeholders, summarising findings and recommending changes to reduce churn.
  • Tracked the effectiveness of promotional campaigns, leveraging data to optimise customer experience and improve the freemium offering.
  • Applied freemium economics principles to evaluate conversion from free to paying users while balancing retention and growth.
Chief of People (Operations & Communications)
Leaguepedia · 2012 – 2014 · Remote
  • Supported CEO Matt Gunnin in scaling Leaguepedia from a small group to a 100+ member organisation across editorial, video content, and operations.
  • Led cross-functional collaboration between editorial, video production, and operations teams as the organisation expanded rapidly.
  • Managed end-to-end hiring, developing frameworks to attract talent while cultivating a culture of transparency, collaboration, and agility.
  • Built operational frameworks to streamline workflows, establish SOPs, and ensure consistency in content delivery.
  • Championed continuous improvement through feedback loops, performance metrics, and internal reporting systems.
  • Spearheaded onboarding and training programmes, ensuring smooth integration and accelerating new hires' path to productivity.
  • Developed a high-performance, people-centric culture that fostered accountability, recognition, and engagement during rapid scaling.
Customer Support Specialist
Vodafone · Jun 2011 – Sep 2011 · Dublin, Ireland
  • Delivered exceptional customer service across multiple Vodafone stores in South Dublin, earning recognition for consistently exceeding customer expectations.
  • Fostered trust and loyalty with customers, contributing to improved store satisfaction ratings.
  • Supported customers with sales and technical queries — mobile plans, devices, and troubleshooting — consistently achieving high conversion rates.
  • Resolved complex customer issues with empathy and a solutions-focused approach, improving satisfaction scores and repeat business.
  • Trained and mentored new team members on best practices, elevating service quality across locations.
  • Stepped into various stores to boost morale, support teams, and ensure each location met its customer service targets.

Education

Level 3 Certificate — Human Resources Management and Services
CIPD Qualifications
Bachelor of Arts — Psychology, Psychoanalysis and Film
Dublin Business School
Higher National Diploma — Critical Research
Dún Laoghaire Institute of Art, Design and Technology

Certifications

  • Foundations of Cybersecurity
  • Play It Safe: Manage Security Risks
  • Tools of the Trade: Linux and SQL
  • Assets, Threats, and Vulnerabilities
  • ChatGPT for Everyone

Skills

Project & Operations Management
Global Project Management Vendor & Partner Management Agile & Scrum Methodologies EU-funded Project Reporting Process Optimisation Workflow Design Research & Policy Implementation
HR, People & Compliance
HR Compliance Employee Well-Being Leadership Coaching Conflict Resolution Team Building & Retention Employer of Record (EOR) Onboarding & Documentation Stakeholder Engagement
Technical & AI
HTML / CSS / JavaScript Claude API (Anthropic) Prompt Engineering Client-side API Integration CORS & Browser Fetch Patterns GitHub & GitHub Pages AI Workflow Integration
Communication & Content
Tech Journalism Copywriting Content Strategy Community Management Sponsorship & Partnerships API & Technical Documentation
Cybersecurity — In Training
Network Analysis Vulnerability Scanning SIEM Log Review Incident Response Fundamentals Security Posture Assessment Google Cybersecurity Certificate

Case Studies

The Work Experience section lists outcomes. These case studies show the work behind them applied to real past work.

Diagnosing a Widget That Failed Four Different Ways

Stamped.io · Technical Support · 2020

A recurring but genuinely tricky issue: the reviews widget would load its shell — the title, the star framework — but sit empty with no actual reviews inside. The frustrating part was that it never looked the same twice. Some clients saw a plain white box. Some saw a squashed outline like a collapsed table. Some saw the full shell with nothing inside. Four different symptoms, same underlying failure class.

I worked through it in layers rather than assuming a single cause each time. A white box was sometimes just white-on-white text styling, the first thing I checked. A squashed box usually meant the client's own container CSS was overriding the widget's base config. Sometimes only part of the embed block had been copied, so the piece that actually called the data was missing entirely. The hardest cases were when everything was placed and styled correctly and it was still empty. That's where I went into Postman to manually check the API handshake and token authentication. Often the connection was fine, and the real problem was the client's own review data: if their export didn't cleanly separate title, star rating, comment, and verified purchase status, the widget had no way to know what went where.

I couldn't restructure a client's data for them, so the resolution was diagnosing exactly which fields were malformed, then walking them through sanitising and reformatting it before re-upload. Not elegant, but it got real reviews showing instead of an empty box.

Wins
  • Reduced an ambiguous, frequently-escalated issue to a four-branch diagnostic that didn't require engineering involvement.
  • Postman became a standard verification step for ruling out data-sync issues before assuming a display bug.
Constraints
  • No ability to fix malformed client data directly — every resolution depended on the client successfully following sanitisation guidance, which wasn't always fast or reliable.
  • The diagnostic logic lived in my head; it was never written down at the time, which meant every new teammate had to rediscover it.

Turning a Gmail Inbox Into an Actual Support System

Spec Solutions · Customer Support Operations

Support was running out of a Gmail inbox and an unconfigured FreeScout instance. No tags, no SLA tracking, no ticket ownership: people just grabbed whatever was at the top of the queue and hoped nothing got missed. In practice that meant tickets falling through entirely, follow-ups forgotten, and a slow, inconsistent reply rate. Ownership was the core issue: people dipped in and out without anyone directly responsible for a given ticket.

The first step was making the tools we already had actually work: configuring close-time tracking, follow-up flags, macros for common responses, and forwarding rules to route different issue types correctly. But the bigger discovery was that a huge proportion of tickets were the same question in different words: where is my item. That's not a support problem, it's an information problem.

So rather than just answering that question faster, I worked on removing the need to ask it. We rebuilt the customer-facing platform so item status was tracked and visible stage by stage, and I built a chatbot, trained on documentation and common questions I'd written, to catch repetitive queries before they became tickets at all. We tested it as a standalone FAQ tool first, then moved it into Intercom once it was working properly.

Wins
  • Cut response times by approximately 30% and lifted CSAT.
  • Eliminated a whole category of ticket by building self-serve status tracking, rather than just getting faster at answering the same question.
  • A documentation-trained chatbot meaningfully reduced inbound ticket volume.
Constraints
  • The fix required a genuine platform migration (Gmail and FreeScout to Intercom), which is disruptive and takes real adoption time.
  • Self-serve tools only catch the questions you've already seen written down; novel issues still need a human.

Keeping a €3M Project Aligned Across Borders, Without Authority

Learnovate Centre, Trinity College Dublin · Project Management · 2016–2017

One partner organisation's work package had effectively gone to zero output, despite continuing to be paid, because of internal reshuffling on their end and the work simply not being prioritised. That package covered marketing and dissemination of the project's findings, which meant it was quietly putting visible deliverables at risk. Separately, a recurring friction point ran through the whole project: development teams consistently underestimated how long it actually takes to build substantial systems, which created constant pressure from stakeholder priorities and a real risk of scope creep becoming the default state rather than the exception.

I had no formal authority over any partner organisation, so the lever available to me was visibility, not enforcement. I ran regular check-ins directly with each work package lead, and sent a weekly email to every partner laying out exactly where the project stood and where the blockers were. That weekly update mattered more than it sounds: it created a shared, visible record that surfaced underperformance on its own, without anyone needing to be confronted directly about it. For the stalled work package specifically, I intervened directly to help realign what had fallen behind and get the marketing and dissemination output moving again before it became a funding or deliverable problem.

Wins
  • A weekly visibility ritual surfaced underperformance naturally, without confrontation, and kept every partner accountable to the same shared picture.
  • Direct intervention recovered a fully stalled work package before it became a deliverable or funding risk.
  • A regular check-in rhythm with work package leads caught scope creep and timeline friction early enough to manage rather than firefight.
Constraints
  • No formal authority over partner organisations: the only real lever was visibility and relationship, not enforcement.
  • The root cause of the stalled work package (the partner's internal reshuffling) was entirely outside Learnovate's control. The intervention managed the symptom, not the underlying cause.

Rebuilding a Denim Brand, Top to Tail

Customer Experience & Operations · Early-stage made-to-order denim brand

Client anonymised; reference available on request.

I came to the company as an unhappy customer. The brand was in the worst of what the team called the great jean disaster — orders not landing, customers in the dark, a small founding team underwater, and a reputation taking on water with it. I'd been on the receiving end of it. But instead of just charging back and leaving a bad review, I reached out to the founder, who was visibly overwhelmed, and asked whether he wanted help.

A short while later I came aboard to rebuild the customer experience top to tail. Almost everything that followed was an exercise in rebuilding trust — with customers, and inside the team.

The diagnosis. Before touching anything, the founder and I worked through what we actually wanted the customer experience to be — not just "fewer angry emails," but the standard of care a customer should feel. Then I audited what was breaking it. Support was running out of an under-configured FreeScout instance and a Gmail setup with no real safeguards: no reliable way to catch unresolved problems before they aged out, no escalation path, nothing to stop things falling through. But the deeper finding was that support volume was a symptom. The single largest driver of inbound wasn't a support-process failure — it was customers having no visibility into where their order was. People weren't angry that the jeans were taking time; they were angry because they couldn't see anything. That reframing — inbound as a visibility problem, not a staffing problem — shaped everything after it.

What I did:

  • Rebuilt the support system with real safeguards. Reconfigured the FreeScout/Gmail setup into something that could actually hold a queue: tooling to surface unresolved and aging tickets, a defined escalation path, and the structure to stop problems disappearing silently. The goal was a system where nothing fell through, not just faster replies.
  • Solved inbound at the source — production transparency via RFID. The highest-leverage fix wasn't in support at all. I led a redesign of the customer-facing platform so customers could see exactly where their order was in production — and to make that real, we redesigned the factory floor around an RFID method that tracked each pattern piece and stage as it moved through the build. On the floor it gave tailors genuine accountability over where every piece was. Customer-side, it removed the anxiety generating most of the tickets. Inbound dropped significantly as a direct result. This is the piece I'm proudest of: a support fix that reached into the physical production process, because that's where the actual problem lived.
  • Deflected repeat questions with an FAQ chatbot. On top of the tracker, I built a prototype chatbot — later spun out as its own portfolio project — running through Intercom, pulling from internal FAQs to answer common questions. For logged-in users it could draw on their own data to answer order-specific questions. Together with the tracker, this took another layer of repetitive load off the queue so human attention went to the problems that actually needed a person.
  • Got the brand right where it mattered most — to backers. For the founder kits going out to early backers, I worked on what the packaging and welcome note should be so they embodied what we wanted the brand to stand for. The audience was backers and investors; at a moment when trust was the whole game, the physical artifact in their hands had to carry the brand, not undercut it.
  • Ran the EOR evaluation and selection for international expansion. As expansion approached, the team was about to become genuinely international — members across the UK, Canada, and the EU, with production in Türkiye. That created overlapping obligations: cross-border legal liability, right-to-work requirements across jurisdictions, and the problem of running payroll cleanly when there was no legal entity in the places people lived. My read was that an EOR was the right structure to absorb that complexity. So I ran the selection: set the criteria, evaluated options, and chose Oyster — not the cheapest, but the most responsive and hands-on, and the best fit for how we wanted to operate. We reached contract. The ink was effectively about to dry when the expansion funding was pulled and the plan stopped. This is an evaluation-and-selection story carried through to contract, not a multi-year operation — and I frame it that way.
  • Argued against outsourcing support — to protect the voice we'd just built. We debated whether to hand support to an agency. I argued against it. We'd only just nailed down our voice-and-tone standard, and outsourcing to an agent with a few hours a week on the contract meant betting our reputation — already fragile — on someone with little reason to care enough to protect that tone. I agreed the founder's time was better spent elsewhere; my position was that the answer was someone in-house we could properly mould, not an outsourced contract. A deliberate trade of short-term convenience for brand integrity at exactly the moment integrity mattered most.
  • Navigated cross-border duty-of-care obligations. Part of the people side was working through overlapping obligations under unfamiliar cross-border employment law. The founder and I worked through what proper support and facilitation required, and I brought in fractional CTO consultations to review the tech stack and help steer technical direction. It reinforced why getting compliant employment structures in place mattered: people obligations and business needs aren't separable across jurisdictions.
Wins
  • Inbound fell significantly once the production tracker and FAQ bot were live — because the largest ticket driver (order-status anxiety) was removed at the source rather than absorbed by support.
  • The factory floor gained real accountability through RFID stage-tracking, benefiting on-site staff as much as customers.
  • Support gained a system that held — safeguards, escalation path, nothing falling silently through.
  • The EOR selection reached contract with the right partner chosen for the company's specific multi-jurisdiction needs.
Constraints
  • The expansion funding was pulled, so the EOR was selected and contracted but never operated at scale — this is selection-and-scoping experience, not years of platform operation.
  • Self-serve deflection only removes the questions you've already anticipated and documented; novel issues still need a person, by design.
  • A production-tracking redesign depends on shop-floor adoption to stay accurate — the RFID method worked because the floor process was redesigned around it, not bolted on top.

Projects

Digital Spec — Portfolio Front Door Live

June 2026 – Present

The front door to this portfolio. Rather than dropping every visitor into the same wall of work, Digital Spec asks what brought them — hiring for a specific role, curious about the person, or just browsing — and curates a route accordingly. It's a branching, visual-novel-style guide built as an overlay on the static site. It has no AI behind it: every path is pre-programmed, so there is nothing to hallucinate. The content it surfaces isn't hardcoded either — each route runs a query over a tagged index of the whole portfolio, so it pulls the right case studies and projects by their tags and stays current as work is added.

Design decision No AI, by design

Pre-programmed branching, not an LLM. Zero hallucination risk, and nothing it can say that Spec didn't write. The constraint is the feature: every word is accountable.

Design decision Curates by tags, not lists

Routes query a tagged index of the whole portfolio rather than hardcoded item lists, so the right work surfaces automatically and no lane has to be hand-maintained.

Wins
  • Sorts visitors by intent, not topic — A hiring manager, a curious peer, and a casual browser each get a different, relevant route in a single tap.
  • Data-driven curation — Routes pull from a tagged index, so the right work surfaces automatically and nothing is hand-maintained per lane.
  • Transparent by construction — It states it's a pre-programmed guide with no AI and no hallucination risk, up front.
  • Additive and robust — Built as an overlay, so the full portfolio works untouched beneath it, and it collapses to a docked tab rather than trapping anyone.
  • Accessible — Keyboard-navigable, focus-managed, and reduced-motion aware.
Constraints
  • Tappable-only by design — It can't answer a free-typed question, because it has no live brain — it guides, it doesn't converse.
  • Index is the source of truth — A route is only as good as the tags beneath it: a mis-tagged item surfaces in the wrong lane.
  • Curation order needs a human eye — The query surfaces matching items, but "lead with the strongest" is an editorial call, tuned by hand via lane sort and pin/exclude.
// roadmap
  1. Editorial tuning per lane — Lead each lane with its strongest proof and cap counts, via the lane sort and the per-item pin/exclude overrides.
  2. New lanes as content grows — Add routes as new content types land (e.g. the implementation playbooks), without touching the conversation structure.
  3. Returning-visitor touch — A light "here's what's new since you last looked" for repeat visits, session-aware.

HTML/CSS/JS Client-side only No backend Pre-programmed (no AI) Tag-driven content

Live on this site — look for the Digital Spec tab.
// devlog running build notes — decisions, pivots, wins, and constraints
2026-06-26

Issue Fixed Creator-lore lane was surfacing writing and editorial work history (Phandroid, Can I Play That) alongside a TikTok sponsorship case study and the denim brand turnaround — none of which are what someone asking "show me the tools Spec built for the channel" came for. Root cause: content-creation is a shared domain across all of Spec's writing and media work, not just the YouTube/Kingdom Hearts tools. Fixed by adding audienceExclude: ["creator-lore"] to the four non-tool items. The lane now returns only the five LoreBySpec and KH tools.

2026-06-26

Decision audienceExclude is the right mechanism here rather than narrowing the lane's domain list or creating a separate sub-domain. Narrowing the domain would hide these items from every lane, which is wrong — the Phandroid and editorial work belong in ops and writing lanes. Exclusion keeps each item's tagging truthful and lets the lane be specific without polluting anything else.

2026-06-26

Issue Fixed "Show me how you were built" was using a hardcoded anchor string — fragile as the portfolio grows. Separately, Digital Spec sat at slot 8 of 8 in the builder-technical lane; any new project at the same score tier pushes it off the list. Fixed both: the deeplink now uses a new find-project action that looks up by item id in the portfolio index at runtime. Digital Spec is now pinned to builder-technical via audiencePin in the index so it always appears regardless of how many other projects score the same.

2026-06-21

Issue Fixed Panel was still jumping to the bottom even after the 320ms deferral — scrollBottom() scrolls to scrollHeight regardless of where new content starts. Replaced with scrollToElement(bubble): calculates the bubble's position relative to the scroll container and sets scrollTop so the bubble lands at the top of the panel. Added preventScroll: true to all focus() calls so keyboard focus doesn't fight the scroll position. Applies to all node transitions and lane results.

2026-06-21

Note Audited the portfolio copy for AI-generated writing. The recent build added a fair amount of LLM-flavoured prose, so I ran a read-only pass against the voice-and-tone guide and wrote the findings up in AI-PROSE-AUDIT.md — 28 flagged passages across five sections, worst in the front-door lane blurbs and this project's own wins and constraints. The pattern that kept recurring was the balanced "X, not Y" phrasing used as a default rhythm, plus filler words like "genuinely" and "robust" and a few tidy uplift closers. The audit recommends only; it changed no copy.

2026-06-21

Done Acted on the safe half of the audit — a deletion-only cleanup, no rewriting. Removed filler intensifiers ("genuinely" in three places, a "Crucially" opener on this project's description), two uplift closers, and a duplicated "interesting problems" line in the About cards. The front-door config and this dev log were both in scope: the kh-lore node lost a "genuinely", and the "Charm for those who lean in" sign-off that used to close the "Never trap a busy visitor" decision below came out here. The "X, not Y" and "by design" flags were left alone — those need a real voice edit, not a mechanical cut, so they're staying until they can be reworked by hand.

2026-06-21

Issue Fixed The scroll-jump bug that was fixed for lane results was still present across all other conversation paths — every node transition (hiring routes, curious, nosy, contact, etc.) rendered the bubble and options together then scrolled to the bottom, pushing the new question text above the fold. Fixed by applying the same 320ms deferred pattern to goToNode: bubble renders and scrolls into view first; options appear after a short pause.

2026-06-21

Issue Fixed Devlog was showing all 11 recent entries visible at once instead of just the most recent. Fixed: only the latest entry is now visible on load; all older entries collapse under the toggle. Updated toggle count to reflect the full history now in the overflow.

2026-06-21

Issue Fixed Lane results were jumping straight to the first bucket of cards, skipping the blurb entirely. Root cause: scrollBottom() fired after all cards were rendered, sending the viewport past the blurb. Fixed by rendering the blurb and scrolling to it first, then dropping the cards in 320ms later — matching the natural rhythm of the conversation.

2026-06-21

Done Lane results redesigned: flat card list replaced with a framing blurb (Digital Spec sets context before anything shows) followed by items grouped into labelled sections by type — Work, Case Study, Project, Document. Empty buckets are skipped. Denim brand now routes to the lane-specific section anchor via sectionsByLane. Kingdom Hearts path got an expanded synopsis node before the lane.

2026-06-21

Done Expanded the Kingdom Hearts curiosity path. Previously "What's with all the Kingdom Hearts?" dropped straight into the creator-lore lane with no context. Now it routes through a new node that introduces LoreBySpec — what it is, why Spec built it, the podcast, the sub count — before branching: see the tools built for the channel, or just curious. The just-curious branch gets a warm dead-end with a restart or skip option.

2026-06-21

Issue Fixed No way to go back or start over mid-conversation. Once you chose a path, you were committed. Added a quiet "↺ Start over" button that appears below the options from the second node onwards, and in lane result footers alongside the Back button.

2026-06-21

Issue Fixed Ctrl+R resumed the conversation at the last node — the overlay remembered which branch you were on across page reloads via sessionStorage. Unintuitive: a refresh should feel like a fresh start. Removed node caching entirely; collapse state is still remembered so the docked tab stays docked, but the conversation always restarts from the opening.

2026-06-21

Issue Fixed "The thing Spec's proudest of building" deeplink was landing mid-case-study (an internal section anchor) rather than at the title. The config was referencing a sub-section ID. Updated to use the top-level entry anchor so the route lands at the heading.

2026-06-21

Issue Fixed Project was sitting at the very bottom of the Projects tab — so the deeplink scrolled visually past every other project on the way down. Moved to the top of the section so the route lands cleanly.

2026-06-20

Issue Fixed "Show me how you were built" was routing to the builder-technical lane — a curated list of AI tools — rather than this project. Root cause: this entry didn't exist in the portfolio index when that node was written, so the lane was the fallback. Added the index entry and updated the node to deeplink directly here.

2026-06-20

Pivot Dismiss-on-click-out replaced with collapse/expand. v1 removed the overlay on dismiss — a visitor navigating away and back lost their place entirely. Refactored: clicking outside, pressing Esc, or hitting the minimise button collapses the overlay to a docked "Ask Digital Spec" tab (bottom-right). It persists through the session and re-expands at the same node. Position preserved, no re-showing on page restore.

2026-06-20

Done Language refresh. Naming rules applied across all nodes: Digital Spec = the guide; Spec = the human. "Spec Lite" (early working name) scrubbed entirely. All option labels rewritten in the visitor's voice — "I'm hiring" not "Hiring manager", "I'll poke around myself" not "Browse everything".

2026-06-20 — live

Done v1 live. The portfolio's front door — a branching guide that routes visitors by what they came for. Built as an overlay on the static site; reads the tagged portfolio index for content, so it curates by tags rather than hardcoded lists. Because it carries this dev log, it surfaces in its own builder lane — the thing explaining the portfolio is a documented piece of it.

2026-06-20

Decision Posture-first, not content-first. The opening sorts by mindset — hiring, curious, nosy — not by topic. A visitor knows which they are instantly; leading with a job-category question forces a hiring frame on someone who's just browsing. Each posture gets its own register.

2026-06-20

Decision Digital Spec announces it's a construct, up front. No pretending to be a live assistant. The honesty is the pitch: the first thing a visitor learns is that this was built deliberately and runs on pre-set logic. For a portfolio whose value is transparency, a fake "live AI" would undercut the thing it's selling.

2026-06-20

Decision No AI, by design. It would have been easy to wire an LLM in. Pre-programmed branching means zero hallucination risk and nothing it can say that Spec didn't write. Every word is accountable.

2026-06-20

Decision Decoupled structure from content. The dialogue lives in front-door-config.json; the content comes from the tagged portfolio index via lane queries. Lanes are queries over facet tags, never hardcoded item lists — so adding a project surfaces it automatically, and the conversation can change without touching content.

2026-06-20

Decision Tappable replies only — no text box. An open input promises a brain that isn't there; someone types a real question and hits a wall. Tappable choices keep the illusion honest and render as the visitor's own messages.

2026-06-20

Decision Never trap a busy visitor. A persistent skip, and the overlay collapses to a docked tab rather than vanishing — a recruiter with four minutes can dismiss it in one tap and still retrieve it.

2026-06-20

Decision Instant responses, no fake typing delays. Personality goes in the words, not artificial latency — especially for a recruiter-adjacent audience.

2026-06-20

Note Graceful degradation. If a lane has no matching content yet, the node says so and routes to the full site rather than showing an empty box. The front door could ship before every lane was full.

2026-06-20

Decision Accessibility from the start. Full keyboard navigation, focus trapped while open and restored on collapse, Esc to collapse, reduced-motion respected. A guide some visitors can't operate isn't a guide.

2026-06-20

Note Origin. This started as a slightly cynical proof of concept — how cheaply you can fake bespoke curation with the right wiring. The honest inversion became the point: a constructed guide that's loud about being constructed, backed by a real tagged index, is more transparent than most "handcrafted" portfolios.

Role Fit Watcher Live (private)

June 2026 – Present

A scheduled agent that checks ~24 companies' job boards every morning and tells me only the roles worth my time: at a company I've already vetted, a fit against my own role criteria, and remote-eligible from Germany. It started as a tracker spreadsheet I was checking by hand and turning into a daily chore. The build replaced the chore.

Each morning at 09:00 CET it pulls each company's hiring feed straight from the source (Ashby, Greenhouse, Workable, Teamtailor), filters every open posting on three gates — company, role, location — judges the survivors against my fit matrix, drops anything I've already seen, and posts a short digest. The point of difference is that the morning run applies judgment, so a posting isn't matched on keywords alone — it's read against criteria like "clean handoff", "no sales pressure", and "async load", and tagged accordingly.

Design decision Read the source, don't scrape

Every company feed is a public ATS endpoint that returns structured JSON or a readable careers page. No screen-scraping, no aggregator. The data is the same data the company publishes, which means titles, locations and salary come through clean and the tool doesn't break when a page restyles.

Design decision Judgment at the edge, not keyword match

The deterministic half (fetch, filter, dedup) is cheap and runs first. The judgment half — does this role actually fit, and is it ✅ or 🟡 — is done by the model against a written matrix. A title like "Customer Success Manager" passes the keyword gate but gets tagged 🟡 with the reason stated: post-sale CS can carry revenue pressure. The reasoning is part of the output, not hidden.

Wins
  • Three-gate filter — Every posting clears company (green/yellow only, never a ruled-out red), role (matched to my fit matrix), and location (must be reachable remote from Germany) before it reaches me. Most postings die at the location gate, which is the point.
  • Reasoned fit tags — Each surfaced role is tagged ✅ Strong or 🟡 Interesting, and the 🟡 ones state why they're 🟡 and not ✅ — which specific criterion is partial or at-risk. That's the difference between a list and a recommendation.
  • Only shows what's new — A state file remembers every role already reported, so the digest is changes-since-yesterday, not the same roles re-listed daily.
  • Catches stale tracker data — On the first run it surfaced a Germany-based Implementation role at Remote that my hand-kept tracker had missed, and showed that two roles the tracker still listed at another company were already gone. Live source beats a spreadsheet last touched in May.
  • Honest coverage — Companies without a clean public feed aren't silently dropped. They appear in a "manual check" list with a link, so I know exactly what the automation does and doesn't cover.
Constraints
  • Not every company has a feed — ~24 of the watchlist run on a queryable ATS. The rest (Comeet, custom careers pages, JavaScript-only boards) can't be polled cleanly and stay on the manual-check list. Coverage is good, not total, and the tool says so.
  • "Remote" lies — Job posts routinely say "Remote" and mean "Remote, US". The location filter is deliberately strict: anything that doesn't explicitly allow Germany/EU/EMEA gets held in a "verify" bucket rather than waved through. That occasionally parks a real role for a manual glance — the trade chosen on purpose to keep false positives down.
  • Runs while the app is open — It's a scheduled agent inside Claude/Cowork, not a hosted service. If the app is closed at 09:00 it runs on next launch. Fine for a personal tool; a server cron would remove the dependency.
  • No fully unattended fetch path yet — Scheduled runs abort before reaching the feeds: web_fetch only accepts URLs that appeared literally in a message, but the scheduler passes templated ones (…/<token>), so they never gain provenance. Pasting the literal URLs into chat unblocks a session by hand. Shell/curl is ruled out by the content-fetch policy, so the durable fix is either the scheduler injecting expanded (de-templated) URLs or a dedicated ATS connector (see devlog, 2026-06-24 and 2026-06-26).
  • Ashby feeds come back partial — Ashby's public posting API always embeds full job descriptions, so a board's response runs to 100 KB+ and is truncated when web_fetch saves it for parsing — only the first ~5–6 postings survive. Greenhouse exposes content=false and reads complete; Ashby has no such switch. Six boards (several of them target employers) are partial until a compact-fetch path lands, so they're glanced manually for now.
  • Judgment costs a run — The fit reasoning is done by the model each morning, which is what makes it accurate but also means the quality of a tag is the quality of that run's read, not a fixed rule.
// roadmap
  1. Browser-rendered boards — Add Claude-in-Chrome for the JavaScript-only and login-gated boards (Workable apply pages, HiBob's Comeet) so the manual-check list shrinks toward zero.
  2. Salary + seniority gating — Several feeds expose compensation and a seniority signal already. Add a floor and an "entry/mid only" hard filter so over-senior roles never reach the digest.
  3. Apply-state writeback — Push "applied / rejected / interviewing" back into the tracker from the digest, so the watcher and the outreach log stay one system instead of two.
  4. Self-healing tokens — When a feed goes empty for N days, auto-recheck the company's careers page for a moved ATS, rather than quietly monitoring a dead endpoint.

Scheduled agent (Claude / Cowork) Public ATS APIs — Ashby · Greenhouse · Workable · Teamtailor web_fetch + Grep parsing Markdown / CSV / JSON state No backend No scraping

No public demo — runs as a private daily automation. Build files, config, and this devlog available on request.

// devlog running build notes — decisions, pivots, wins, and constraints
2026-06-26 — completed run: 8 new roles surfaced

Done First full end-to-end pass since the block. Location-paired keyword greps over the saved payloads (Greenhouse puts location before title with a metadata gap — pair within ~500 chars), then role/location/language filters, dedup vs Seen.json, fit tags, and persistence to Matches Log.csv / Seen.json / Run Log.csv. 8 new roles (1 ✅ strong, 6 🟡 stretch, 1 🔎 verify): Remote's HR Process & System Specialist (✅, EMEA); four Remote lifecycle/onboarding/care roles (🟡); PostHog Technical Account Manager (🟡, warm contact); GitLab Senior CSM DACH (🟡, carries quota, Germany); Figma Customer Enablement Manager (🔎, Paris — verify remote). Feed health: 16 read full, 6 partial, 3 skipped — no feed silently returned "nothing".

2026-06-26 — new limitation: Ashby feeds truncate on save

Issue Ashby's posting API always embeds full job descriptions, so each response is 100 KB+ and gets truncated when saved to the overflow file — only the first ~5–6 postings per feed survive. Greenhouse (content=false) is compact and reads completely. Net: 6 Ashby feeds (ashby, posthog, deel, oyster, zapier, n8n) came back partial this run, including the flagship ashby board (warm contact). The fix is a parser that pages the response or strips descriptions before saving; until then they're treated as partial and glanced manually.

2026-06-26 — manual unblock (workaround, not a fix)

Note Spec pasted the literal Ashby + Greenhouse feed URLs into chat. Provenance is conferred per-conversation, so every subsequent web_fetch in the session then worked — which is what made today's pass possible. It's a manual workaround that has to be repeated each session, not a durable fix.

2026-06-26 — shell-curl fix marked CLOSED

Decision The 06-24 plan's option #1 (curl the feeds from the sandbox shell behind a domain allowlist) is closed: the content-fetch policy forbids retrieving URLs via shell/curl/python whenever web_fetch is blocked — allowlist or not. Re-confirmed there's no off-the-shelf Ashby/Greenhouse public-board connector in the registry either. The durable fix narrows to two routes: have the scheduler inject the expanded (de-templated) feed URLs so they enter provenance, or build a dedicated connector.

2026-06-26 — root cause pinned: templated URLs never gain provenance

Constraint The exact mechanism is now nailed down. The scheduled task passes templated URLs (…/job-board/<token>), but web_fetch only accepts URLs that appeared literally in a user message — so a templated URL never lands in the provenance set during an unattended run, and every feed is unreachable. The 06-24 constraint wasn't transient; it's structural and will recur every scheduled run until a durable route lands.

2026-06-26 — scheduled run aborted at canary (3rd identical block)

Failure Pre-flight fetch of the Remote (remotecom) canary failed with URL not in provenance set; confirmed on a second (Ashby) feed. Per the canary rule, the run aborted rather than grinding all feeds, and posted a "feeds unreachable" digest. No state advanced. Third identical abort (06-24, earlier 06-26, scheduled 06-26).

2026-06-24 — run failed: feeds unreachable, 0 roles

Failure The morning run gathered 0 roles — a tooling block, not an empty market, so the day is logged as "unknown", not "nothing out there". A guardrail held: last_run advanced but nothing was written to Seen.json or Matches Log.csv, so a failed fetch was never recorded as a clean empty result.

2026-06-24 — constraint: no unattended path from config to the feeds

Constraint This is structural, not a flaky feed. Three independent paths to live role data are each closed for a different reason. web_fetch enforces a provenance allowlist — it only retrieves a URL that has already appeared in a message or a prior fetch result, so the agent can't introduce its own feed URLs from Config.md (the one thing a poller must do). The sandbox shell can't compensate: no outbound network by design (even example.com returned HTTP 000). The hosted ATS boards can't compensate either — they're JavaScript shells and web_fetch doesn't execute JS, so only the raw JSON API carries the data, and that's the blocked path. The watcher's own logic is fine: once a Greenhouse URL happened to be in the provenance set, web_fetch returned full live JSON and the parser handled it. The gap is structural — getting our own URLs admitted — which is why it disables the core premise rather than dropping one company.

2026-06-24 — decision: no search-snippet fallback

Decision Search snippets were deliberately not used to fill the gap. The governing rule is "a missed role is cheaper than a wrong one" — cached snippets are stale and partial (they surfaced Ashby Americas roles the location filter would drop anyway) and risk false positives and re-reporting closed roles. Skipping them was a choice, not an oversight.

2026-06-24 — fix path chosen (not yet applied)

Decision Ranked, easiest first: (1) grant the sandbox shell network access to the ATS hosts so the watcher curls the JSON directly — most robust, bypasses both blockers; (2) a per-domain provenance allowlist for web_fetch; (3) a dedicated Ashby/Greenhouse jobs connector — cleanest long-term, structured output; (4) a headless render for the JS-only boards, only if still needed. Plan: do #1 now to restore coverage, build toward #3 as the durable solution. Tagged a decision, not a fix — none of this is applied yet.

2026-06-24 — decision: guardrails so a blocked run fails loudly

Decision Going in: a pre-flight canary feed at run start — if a known-good feed fails, abort and post a "feeds unreachable" digest instead of a silent "nothing new"; count only HTTP 200 + parsed JSON as "checked" so a fetch error never advances a feed's seen-state; add a per-feed last_success to Seen.json and surface anything not polled in N days under "manual check needed"; and stop trusting last_run as proof of coverage.

2026-06-23 — expanded to the yellow tier

Done Added the full yellow watchlist. Probed ATS tokens in batches: 13 confirmed on Greenhouse (Mercury, GitLab, Datadog, Cloudflare, Dropbox, Airtable, Discord, Figma, Intercom, Wikimedia, HubSpot, Bitwarden, Customer.io) plus Tyk on Workable. Ten more (GitHub, Atlassian, 1Password, Headspace, Circle, Basecamp, DuckDuckGo, Whereby, Cronofy, Spotted Zebra) have no clean public feed and went to the manual list. Scanning the new feeds, the filter immediately did its job: the yellow boards are mostly US-remote, engineering, and sales — all correctly dropped — but it caught one real fit, Bitwarden's Customer Success Specialist (EMEA), tagged 🟡 for reactive-volume risk. Logged so it won't repeat.

2026-06-23 — added fit reasoning to the output

Done Every role now carries a one-line reason, and 🟡 tags must say why they're 🟡 and not ✅. This was a deliberate move away from a bare match list. The morning run pulls the relevant matrix note when it helps explain the call.

2026-06-23 — first live test run

Note Ran the whole procedure end to end against today's feeds. Result was more current than my own tracker: Ashby's IC/IS roles weren't in the live feed (the tracker still listed them), and Remote surfaced a Germany-based Payroll Implementation Country Lead plus an EMEA Implementation Specialist — both ✅. Confirmed dedup works: the three roles already reported were skipped on the second pass.

2026-06-23 — resolved the ATS tokens

Done Mapped each company to its hiring platform. Blind Greenhouse token guesses mostly failed; the pattern is that modern remote-first startups sit on Ashby (Deel, Zapier, n8n, Oyster, Improbable, Help Scout all resolved there). Used web search to identify the non-Ashby ones: Tyk and Adaptavist on Workable, Puzzel on Teamtailor, HiBob on Comeet, Peak PEO on no ATS at all (≈18 people).

2026-06-23 — Workable fought back

Issue Workable's public API timed out repeatedly through the fetch tool, and its apply page is JavaScript-rendered, so a plain fetch returns an empty shell. Teamtailor, by contrast, renders its careers page as clean readable text — Puzzel reads fine with no API needed.

Decision Try the Workable API once, and on timeout fall the company through to manual-check rather than hang the run. One slow ATS shouldn't stall the whole morning.

2026-06-23 — the parsing problem

Issue Two constraints nearly sank the approach. First, the sandbox has no outbound network, so the obvious "write a Python script that curls the feeds" doesn't run there. Second, the fetch tool overflows large feed responses to a file instead of returning them inline, and those files are single JSON lines too long to read normally.

2026-06-23 — the parsing fix

Fixed Parse the saved overflow file with Grep (-o), which handles arbitrarily long lines and pulls title / location / jobUrl straight out. That turned a blocker into the standard parse path — fetch overflows to a file, Grep extracts the fields, the run filters from there.

2026-06-23 — feasibility proven

Done Confirmed the core question before building anything: can I get clean machine-readable role data from these specific companies? Yes. Ashby (/posting-api/job-board/<token>) and Greenhouse (/v1/boards/<token>/jobs) both return structured JSON with exactly the fields the filter needs — including locations like "Remote - European Union", which is what the Germany gate keys on. Empty responses correctly mean "no open roles" (matched a tracker note that said as much). No scraping required.

2026-06-23 — started from the spreadsheet

Note Began with my existing Role Matrix (fit criteria per role type) and Company Tracker (green/yellow/red, with culture and remote-policy notes already done by hand). The watcher is built to sit on top of that work, not replace it: the matrix defines what a fit is, the tracker defines which companies count, and the agent does the daily checking I was doing manually.

Role-Fit Evaluator — Application Decision Matrix Internal Tool

June 2026 – Present

A decision system for one question: when a role catches my eye but didn't surface through the job-matching tool, is it worth the hours of curating a CV for it? It scores any job description against a fixed profile across seven weighted dimensions, sitting behind a dealbreaker gate that can veto a high score outright. The output is a verdict — Strong, Stretch, Weak, or No-go — with the transferable bridges to lead with, the real gaps, and an effort estimate for the CV. Built as a five-tab spreadsheet engine plus a profile baseline and a runbook, so the same job is judged the same way every time.

Design decision Gate before score

The dealbreaker check runs first. Work authorisation, location, salary floor, seniority: any fail caps the result at No-go however well the role scores. Eligibility is a precondition, handled as a gate rather than a weighted factor a strong average could dilute.

Design decision Honesty by construction

The verdict logic cannot return a yes when a mandatory requirement is missing. A must-have score of zero caps the verdict at Weak even at an 85% match, so a strong transferable case never papers over a hard screen-out.

Wins
  • Seven weighted dimensions — must-have coverage, core overlap, transferable bridge, domain fit, seniority, motivation, portfolio evidence — each weighted and scored 0–3 against published anchors, so the same role scored twice lands in the same place.
  • Dealbreaker gate with override — eligibility is checked before any scoring and overrides the band. A 90% role I can't legally take still reads No-go.
  • The transferable bridge is earned — that dimension only scores high when the link between an adjacent strength and a job requirement can be stated in one sentence. A vague "it's all transferable" scores 0–1.
  • Profile as single source — skills, tools, domains, and a project-to-evidence map live in one baseline file every assessment reads from, so results stay consistent as roles vary.
  • Running tracker — each assessed role logs to a tracker tab with score, verdict, and outcome, so patterns across applications surface over time.
Constraints
  • Inputs are self-reported — the dealbreakers (salary floor, work authorisation, seniority range) are filled by hand. The gate is only as honest as those entries.
  • Scoring needs a reader — a human, or Claude, still reads the posting and assigns the 0–3 scores. The engine calculates and enforces the logic; it does not parse the job description on its own.
  • The profile drifts — as skills grow, the baseline has to be updated by hand or assessments lag behind reality.
  • Not hosted — an internal toolkit (a spreadsheet and two documents), run in conversation, rather than a public web tool.
// roadmap
  1. Auto-scoring pass — hand the posting and the profile to Claude and have it propose the seven scores with a one-line justification each, leaving me to adjust rather than score from a blank sheet.
  2. JD parsing — separate must-haves from preferred-haves out of a pasted posting automatically, so the must-have gate is populated rather than read by eye.
  3. CV-tailoring handoff — on a Strong or Stretch verdict, generate the tailored summary and the gap-framing line straight from the matched strengths and bridges.
  4. Weight tuning from outcomes — once the tracker holds enough logged results, revisit the dimension weights against which applications actually converted.

Excel / openpyxl Weighted scoring matrix Dealbreaker gate Profile-driven AI-assisted scoring Internal tool

Internal toolkit — not hosted. Spreadsheet engine + profile baseline + runbook.

// devlog running build notes — decisions, pivots, wins, and constraints
2026-06-24 — live test against a real CSE posting

Done Live test against a real Customer Success Engineer posting. Scored 88.7% → Strong. Must-have coverage and seniority both came in at 2, not 3 — the missing formal CSM/TAM title and the lack of in-production tenure pulled them down — which is the signal that the matrix discriminates rather than rubber-stamping a target-company role.

2026-06-24 — kept must-have=0 as a hard cap

Note Decision: kept the must-have=0 rule as a hard cap rather than a heavy weight. A weight alone let a strong transferable case float a screen-out role up into Stretch, which misrepresents how an ATS reads it. The cap holds the verdict at Weak regardless of the rest of the score.

2026-06-23 — verdict logic tested across eight scenarios

Issue Fixed Verdict logic tested across eight scenarios — gate fail, must-have=0 at high fit, and each band boundary at 0.72 / 0.55 / 0.40. The first pass treated must-have=0 like any other low score and let an 85% role read Strong. Rewrote the verdict formula with two overrides (gate fail → No-go, must-have=0 → capped at Weak) and re-ran: all eight resolved correctly.

2026-06-23 — built the five-tab engine

Done Built the five-tab engine — Scorecard, Tracker, Profile, Rubric, and the gate. The weighted total recalculates clean with zero formula errors. Input cells are colour-coded so the ones to change on each run are obvious.

2026-06-23 — chose a weighted scorecard with a gate

Note Chose a weighted scorecard with a dealbreaker gate over a flat yes/no or a single relevance percentage. The established approach for "is this worth applying to" is a decision matrix with anchored scoring; the separate gate carries eligibility, which a weighted average would otherwise wash out.

2026-06-23 — built the profile baseline from the README

Note Built the profile baseline from the portfolio repo README — skills, tools, domains, and a project-to-evidence map. Left salary, work authorisation, and seniority as confirm-me placeholders, since those drive the gate and only I can fill them honestly.

2026-06-23 — couldn't delete the scratch build files

Issue Couldn't delete the scratch build and test files from the working folder — sandbox permissions blocked the removal. Minor: they sit outside the three deliverables and don't affect the toolkit.

Error Log Interpreter Live

May 2026 – Present

Paste a browser console error, a Vite build error, or a stack trace — and get back a plain English breakdown aimed at developers who are still learning to read output. The tool does more than translate: it distinguishes between where an error surfaces and where the actual problem originated, which are often in completely different places. A TypeError inside a React renderer usually means something went wrong several steps earlier. A build failure citing a missing module often means a misconfigured import path, not a missing package. Getting that distinction right is the first real debugging skill.

Wins
  • Root cause framing — The "Real Problem" section explicitly addresses the error-surface vs error-origin gap. Beginners consistently debug the wrong location because the error message points there; this tool corrects that instinct.
  • Five structured sections — What it means, where the real problem is, where to look, what to try, and an error handling coaching section. Each one solves a different part of the beginner's confusion.
  • Coaching built in — The final section doesn't just explain what went wrong. It tells you where error handling should have been placed, why that layer matters, and what principle the error illustrates. Code hygiene is taught through the error, not separately from it.
  • Context input improves accuracy — An optional second panel lets you add framework, what you were doing, and any relevant code. Root cause analysis with context is significantly more targeted than without.
  • Entry-level first — The output is written for someone who is still learning. No jargon without an explanation, no "just check your code" non-answers. Every section is specific to the actual error provided.
Constraints
  • No memory of the codebase — The tool only sees what's pasted. Without the surrounding code, some root cause analysis is necessarily probabilistic — it gives the most likely candidates, not a guaranteed answer.
  • Error quality varies — Minified stack traces and cryptic build tool output are harder to interpret than clear runtime errors. The tool will do its best but flags when the error itself lacks information.
  • Requires an API key — No shared backend, so users need an Anthropic account. One-time setup, but it's friction a hosted version would remove.
// roadmap
  1. Senior/advanced mode toggle — The current output is calibrated for beginners. A mode switch would change the register: shorter explanations, deeper technical detail, skipping the hand-holding and going straight to the likely culprit and fix. Same five sections, different depth per level.
  2. Framework-aware analysis — Detect whether the error is a React, Vue, Next.js, or Vite-specific pattern and tailor the "Where To Look" section accordingly. React hydration errors and Vite import resolution failures have known patterns; naming them explicitly saves a lot of hunting.
  3. CI/build log mode — Extend the input to accept multi-line build logs, not just single error messages. CI output is full of noise; extracting the signal (the actual failing step) and explaining it is a different skill from reading a browser console error.

HTML/CSS/JS Claude API (Anthropic) Client-side only No backend

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-28 — live

Done v1.0 live. Both tools (Error Interpreter and Debug Helper) deployed and linked from the portfolio. The error-surface vs error-origin distinction is the core value proposition — tested against a range of real errors (TypeError, ReferenceError, Vite import failures, CORS errors) and the root cause framing holds up well across all of them.

2026-05-28

Decision Added a contextual callout to the "Real Problem" section in the rendered output — a small amber-bordered note that reads "Where an error appears is often not where the bug lives." It's only four words of UI but it primes the reader to hold the root cause section differently. Without the framing, people skim the section as if it's just more explanation of the error.

2026-05-28

Decision Set max_tokens to 1,400. Five detailed sections averaging 200–250 tokens each covers the output without leaving room for padding. Tested at 900 (too thin for the coaching section), 1,200 (borderline), 1,400 (consistently complete). Output tokens cost 5× more than input on Haiku — kept the cap tight.

2026-05-28

Decision Five sections, not three. First instinct was three (explain, diagnose, fix) but the coaching section earns its weight — it's what separates a translation tool from a teaching tool. The "Where To Look" and "What To Try" split also matters: one answers WHERE to investigate, the other answers HOW. Merging them into a single "next steps" list collapses the distinction between understanding and action.

2026-05-28

Decision Color-coded result sections: plain-English explanation in calm blue (informational), root cause in amber (needs attention), where to look in bright amber (investigative), what to try in green (action), coaching in purple (growth). Each color signals a different kind of reading — you scan the results differently knowing which section you're in.

2026-05-28

Decision Warm amber/ember color palette — not red. The tool is about errors but the experience should feel constructive, not alarming. Red signals danger and tends to put people on the defensive. Amber signals "pay attention here" without the connotation that something is catastrophically wrong. Important when the audience is beginners who already feel anxious about errors.

2026-05-28

Decision Monospace textarea for the error input, serif for context. Errors should look like errors — monospace signals "this is technical output, paste it as-is." The context panel is where you write prose, so serif is appropriate there. Small UX detail that reinforces the mental model of each panel.

2026-05-28 — core design

Decision The error-surface vs error-origin gap is the tool's core value. Every beginner debugging tutorial says "read the error" — but few explain that the error location and the error cause are structurally different things. A React TypeError at render time almost never means the component is broken; it means something upstream returned the wrong shape. Building that instinct early saves hours of debugging time later. The "Real Problem" section is the reason this tool exists.


Debug Helper Live

May 2026 – Present

A structured debugging coach for when you're stuck — whether you have an error message or not. Most debugging tools assume you have something to debug against. The harder case is when your code runs, produces no output, and gives you nothing to go on. This tool has three modes: I have an error (want a plan, not just an explanation), code runs but does the wrong thing, and total silence — nothing happens at all. The silent mode is built specifically around the most common causes of hidden failures: swallowed catch blocks, unhandled promise rejections, missing await, and event handlers that never attached. It coaches the debugging process, not just the answer.

Wins
  • Covers the silent failure case — The hardest debugging situation for beginners isn't an error they don't understand. It's code that runs quietly and does nothing. The silent mode specifically addresses the common causes: a catch block that swallows the error, an async function that resolves to a Promise object instead of the value, an event listener attached to the wrong element.
  • Teaches process, not just answers — Every debugging plan section explains WHY you're doing each step, not just what. A developer who understands why you add a console.log before the data fetch (to confirm the fetch even ran) will know where to add logging on the next bug without being told.
  • Three modes, one tool — A single text input handles three fundamentally different debugging situations. The mode selector changes both the placeholder text and the framing passed to the model — the debugging plan for "wrong output" is structurally different from the plan for "complete silence."
  • Logging guidance is explicit — The "Where To Add Logging" section gives the actual console.log syntax to use, not just "add some logging." Specific beats generic for beginners who aren't sure where to start.
Constraints
  • Context-limited plans — The debugging plan is only as good as the description. Vague input ("it doesn't work") produces vague plans. The placeholder text models a useful level of detail, but the quality ceiling is the quality of the description.
  • No execution — The tool can't run the code, check the network, or inspect live state. It reasons from description, not from observation. A real debugger with breakpoints will always be more precise for an active session.
  • Requires an API key — No shared backend, same friction as the other tools in the portfolio.
// roadmap
  1. Pair with Error Interpreter — Add a "Take this to the Error Interpreter →" link when the situation involves a specific error message. The two tools are complementary: Interpreter explains what an error means, Debug Helper generates the plan for fixing it. Right now they're two separate tabs; a handoff link closes the loop.
  2. Framework-aware plans — Detect common framework patterns in the pasted code (React hooks, async/await, fetch calls, Express routes) and tailor the debugging plan to that context. The logging guidance for a React state bug is different from the guidance for a Node async bug — naming the right tools (React DevTools, Node inspector) matters.
  3. Step-by-step interactive mode — Rather than delivering the full plan upfront, walk through one step at a time. After each step, ask: did that reveal anything? Then adapt. More conversation, less prescription — better for genuinely stuck cases where the problem is unknown.

HTML/CSS/JS Claude API (Anthropic) Client-side only No backend

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-28 — live

Done v1.0 live. Three-mode structure holds up well across test cases. The silent mode in particular produces genuinely useful output — the swallowed catch and missing await explanations are exactly what beginners need to hear and rarely do. The cool blue palette reads as methodical and calm, which is the right register for a debugging tool (contrast with the error interpreter's amber — different emotional tone for a different use case).

2026-05-28

Decision Set max_tokens to 1,500. The debugging plan section alone needs 400–500 tokens to be genuinely useful at 5–7 numbered steps with explanations. Testing at 1,200 showed the "What This Teaches" section getting truncated. 1,500 gives consistent full output across all three modes.

2026-05-28

Decision The "Where To Add Logging" section specifically mandates the actual log syntax — console.log('label:', variable) — not just "add logging here." Beginners often know they should add logs but don't know where to add them or what to log. Making the suggestion concrete (before the fetch, after the assignment, inside the if branch) is the difference between useful and vague.

2026-05-28

Decision Mode selector as full-width styled cards, not a model-bar-style compact row. Three modes is a meaningful choice with a paragraph of explanation each — it deserves more visual weight than a compact radio toggle. The card style also lets each option carry a subtitle explaining when to use it, which reduces the guessing.

2026-05-28

Note Silent failures are a genuine beginner trap. When code produces no output and no error, beginners often assume the code didn't run. The actual common causes — a catch block that swallows the error without logging it, an async function called without await, an event listener attached to a null element — are not intuitive. The silent mode was designed specifically to surface these, because no other debugging tool explains the absence of output rather than the presence of an error.

2026-05-28

Decision Cool slate/blue palette — deliberately different from the Error Interpreter's amber. Two tools in the same portfolio sharing a color scheme would blur the distinction. More importantly, the emotional register should differ: amber for "here's what's wrong" (attention-drawing), blue for "here's how to find it" (methodical, calm). Color communicates the relationship to the problem.

2026-05-28 — design rationale

Decision Structured as a companion to the Error Log Interpreter, not a competitor. The Interpreter answers "what does this mean?" — this tool answers "how do I approach finding and fixing it?" Same audience (beginners), different moment in the debugging process. A developer will often need both: one to understand the error, one to plan the fix. Keeping them as two separate tools with clear roles is cleaner than one multi-purpose tool that tries to do everything.


PostHog CS Health Intelligence Complete

May 2026

A research-backed prototype targeting the gap between signal collection and prioritised action in CS. PostHog's tooling (Vitally, CDP, Zapier) covers monitoring well — the morning question it doesn't answer is who needs me today, why, and what do I actually say? The prototype is a structured JSON account model and a Python script that reads it and produces a ranked morning action list: URGENT first, then WATCH, then OK. Each flagged account shows the specific signal that triggered it, a plain-English reason it matters, and a concrete suggested action. No composite health score. No frontend. The complexity lives in the design decisions, not the code.

Design decision No composite score

Composite scores hide the shape of a problem. An account with strong usage and a failed payment looks fine on a health score. It isn't. Signals are surfaced individually — what triggered it and why — so the shape of the risk is visible.

Design decision Two customer types, two threshold sets

Self-serve silence at 30 days is expected. Sales-assisted silence at 21 days is a WATCH flag. This distinction runs through every signal in the script — not just noted in a comment.

Wins
  • Leading indicators weighted over lagging ones — Days since last contact, usage trend, and renewal proximity fire before a problem is visible. The "never be surprised" success condition in the PostHog brief requires seeing things before they become conversations.
  • Failed billing always ranks first within URGENT — Explicitly scored with a boost so a billing failure on a $67k account surfaces above multi-signal lower-ARR accounts that may already have churned. One line in the code, one paragraph in the devlog explaining why.
  • Expansion signals as a separate track — A self-serve account adopting Session Replay with 40% usage growth is a conversation starter, not an emergency. It appears in the OK section with a note rather than bumping the account into WATCH. Positive signals matter but they're not urgent.
  • Every field, threshold, and ranking decision is documented — The devlog covers the data model field by field, the threshold choices and their reasoning, and one output format decision that changed during the build (URGENT signals now always render first within an account).
  • Under 130 lines of Python with no external dependencies — Complexity lives in the design decisions. The code itself should be readable by someone who doesn't write Python.
Constraints
  • No live data integration — The account model is manually maintained JSON. In practice this data would flow from Vitally, Salesforce, and PostHog's CDP. The prototype demonstrates the logic layer; the plumbing would depend on what data the team has already structured.
  • Slack channel activity is tracked but not scoredslack_channel_active is in the account model because Slack silence is a meaningful signal for PostHog's ICP. It's not wired into threshold logic because it would need per-account baseline data to fire meaningfully — a channel that's always quiet is different from one that went quiet.
  • Open unresolved issues are tracked but not ranked — The count is visible in the raw JSON for context but doesn't trigger a flag on its own. Issue severity matters more than count, and that's not something the current data model captures.

Python JSON No dependencies PostHog

View on GitHub →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-22 — expansion signals as a separate track

Decision Panda Analytics has no risk flags — usage is up 40% week-over-week and they just adopted Session Replay. That's an expansion signal, not an urgency flag. Moving them into WATCH would be wrong; making them invisible would be a missed conversation. The account model includes an expansion_signal field — a string or null. In the OK section, any account with a value gets an arrow and the signal description. Expansion signals are conversation starters. They live in their own track so the triage logic stays clean.

2026-05-22 — output format: why text, why three tiers

Decision The output is plain terminal text. Not JSON, not a dashboard. The morning list is meant to be read, not processed — a CSM should be able to scan it and know their first three actions in under two minutes. Three tiers (URGENT / WATCH / OK) map to three behavioural responses. A numeric health score doesn't. One format decision changed during the build: initially signals rendered in evaluation order, which put a WATCH above three URGENT signals for Skyline DevOps. Fixed with a one-line sort in format_account — URGENT signals always render first within an account.

2026-05-22 — threshold choices and one ranking decision

Decision Self-serve contact threshold is 45/60 days; sales-assisted is 21/30. Self-serve usage decline fires after 2/4 weeks; sales-assisted after 1/3. The shorter tolerances for sales-assisted reflect the stronger expectation of active use that comes with a managed relationship. Within the URGENT tier, failed billing gets a scoring boost (+50) so it always floats above accounts flagged only for renewal timing. The alternative was pure signal-count ranking — which would let a multi-signal $19k self-serve account that's probably already churning outrank a billing failure on a $67k sales-assisted account with renewal in 45 days. That felt wrong.

2026-05-22 — building the account data model

Note Went through each field explicitly. arr and arr_tier are separate — the numeric ARR is used for ranking, the tier string is used for display. usage_weeks_declining is distinct from usage_trend because the threshold checks need a duration, not just a direction. slack_channel_active is included because Slack silence is a meaningful signal for PostHog's ICP, even though it's not currently wired into threshold logic. expansion_signal is a nullable string, not a boolean — the description is what matters. Fields excluded: champion_name, products_in_use, tenure — all relevant context, none of them change the script's output.

2026-05-22 — scope constraint, logged deliberately

Decision The prototype is scoped to what can be fully explained and defended in an interview without notes. Every function, every field, every threshold has a reason that can be stated clearly in plain English. If a future iteration warrants more complexity — live API integration, a proper frontend, multi-CSM views — the devlog will record why that complexity was added and what problem it solved. Complexity without documented rationale is technical debt.

2026-05-22 — deciding what not to build

Decision It would be easy to build a full health dashboard — React frontend, live PostHog API integration, Stripe webhooks. That would duplicate Vitally, which PostHog already pays for. It also wouldn't be defensible in an interview — the question would shift from "why these decisions?" to "explain your framework choices." Over-engineering is a CS anti-pattern too: a CSM who ships something their team can't understand is creating a liability. The prototype is a JSON model and a short Python script. The complexity lives in the design decisions, not the code.

2026-05-22 — no composite health score

Decision PostHog's stack uses Vitally's composite score — that's right for a team-wide tool. But composite scores have a failure mode: they hide the shape of a problem. A customer with strong usage, an active Slack channel, and a health score of 78 might have an unanswered renewal conversation from 3 weeks ago. The composite score looks fine. The account is at risk. The prototype surfaces signals individually — each with a plain-English reason and a suggested action. Not a number.

2026-05-22 — two customer types, two baselines

Note A self-serve customer being quiet for 30 days is expected — they chose not to talk to anyone. A sales-assisted customer being quiet for 14 days after a personal onboarding is a flag. The account model includes a customer_type field and all signal thresholds adjust accordingly. This is one of the few places the code makes an explicit behavioural claim — and it's here in the devlog so it's defensible.

2026-05-22 — understanding the existing stack

Note PostHog's CS automation stack from their handbook: Vitally (composite health scores, synced from Salesforce, PostHog usage, BuildBetter, Stripe), PostHog CDP (usage milestone alerts piped to Vitally/Slack), BuildBetter (call analysis, auto-syncs feature requests), Zapier (renewal reminders, billing events), Pylon (Slack Connect channel management), Salesforce (CRM backbone). The stack covers monitoring. The gap is in prioritisation and action — a CSM opening Vitally to find 12 indicators and 3 Zapier alerts still has to decide where to start. That's where this project lives.

2026-05-22 — reading the brief properly

Note The brief contains two distinct problems: the signal problem (leading indicators, never be surprised) and the relationship problem (self-serve vs sales-assisted, Slack-first ICP, different engagement models for each). It also contains an explicit invitation: "If you want to build automations to help you, go for it." At PostHog, where engineers build their own tooling and the culture values autonomy, that line is a signal about what kind of person they're hiring. This project is built from the perspective of a CSM designing the automation layer they'd actually want.

PostHog CSM Copilot In Progress

May 2026 – Present

A Customer Success copilot built specifically for the PostHog CS role — managing 25–40 accounts across a $20k–$100k+ ARR range. Paste any incoming signal (Slack, email, ticket, usage note), add customer context, and choose a mode: Triage + Next Action, Draft Response, Escalation Strategy, or Health Assessment. The tool is encoded with PostHog's product suite, billing model, and the sales-known vs. self-served customer distinction — so outputs are calibrated to the actual relationship dynamics, not generic CS advice.

Focus Cognitive load reduction

A 35-account book is never one thing at once. The tool is built around the "spinning plates" reality — triaging priority across relationships, not just tickets.

Wins
  • Two customer types, two registers — Sales-known and self-served customers need fundamentally different handling. The prompt encodes this — different tone, different risk model, different recommended actions.
  • ARR-aware prioritisation — A billing question from a $100k+ enterprise reads differently than the same question from a $20k self-serve customer. Priority reasoning is tier-calibrated, not just severity-based.
  • PostHog-specific encoding — Product suite, credit billing model, renewal moments, silent churn patterns — the prompt reflects actual domain knowledge, not a generic SaaS CS template.
  • Customer integrity preserved — Drafts are calibrated to feel personal, not templated. The tool produces responses that reference what matters to this customer type — not copy-paste boilerplate that erodes trust over time.
Constraints
  • Copy-paste friction is real — Every signal requires manual copy-paste and context entry. For a 35-account book, that overhead adds up fast. V1 proves the prompt engineering works. Whether paste-and-go is the right interaction model for this workflow is an open question — and the honest answer is probably no.
  • No live data — Health assessments and triage work from what you paste in, not from live usage metrics, Stripe signals, or Slack activity. A real CS automation layer would need API integrations and a backend — both out of scope for a client-side portfolio tool.
// roadmap
  1. Batch signal queue — paste the morning's inbox as numbered items, get a priority-ordered queue with a recommended action per signal. The "spinning plates" problem at scale: which plates are wobbling right now?
  2. Account snapshot cards — lightweight account profiles (ARR, products, champion, renewal date, last contact) that persist in localStorage. Populate once, reference on every signal. Reduces context-entry friction to near zero.
  3. Red flag digest — input 5–10 account summaries, get back a ranked list of which accounts need proactive attention this week. Proactive CSM work, not just reactive triage. The implementation of "never be surprised when a customer leaves."
  4. Technical first-line debug mode — paste an error, stack trace, or technical complaint; get a structured diagnostic walkthrough before deciding whether to escalate. Encode common PostHog failure patterns: SDK ingestion issues, event schema mismatches, feature flag targeting edge cases, session replay blocked by CSPs, Data Pipelines auth failures. The CSM is first responder, not just a pass-through to support.
  5. Feedback router — a dedicated mode for when the signal is customer feedback rather than a problem. Takes a pain point or feature request and produces: a one-paragraph internal product signal (what the customer actually needs, not the feature they asked for), a recommended internal recipient (product, docs, support), and a brief acknowledgement to send back to the customer. Implements the "own their feedback" responsibility explicitly.
  6. Proactive outreach planner — three playbooks triggered by account state rather than incoming signals: new self-serve at 30/60/90 days (activation timing), approaching renewal (value review conversation), champion has recently changed (relationship reset). Shifts the tool from reactive to proactive — the difference between a CSM who feels like a friend and one who only appears when something breaks.
  7. Renewal prep mode — commercial-specific mode. Input ARR, tenure, products adopted, usage trend, and open issues. Output: value story (what this customer has gotten from PostHog this year), anticipated objections and how to address them, renewal structure recommendation (flat/expansion/multi-year), and what success looks like for the call. Demonstrates the commercial layer of the role, not just the support layer.
  8. QBR / EBR prep assistant — for accounts at $50k+ ARR, quarterly business reviews are standard. Input account data and get a structured agenda: usage highlights, success metrics framing, product roadmap discussion points, suggested next 90-day goals. Separate from renewal prep — QBRs are relationship investments, not closing conversations.
  9. V2 architecture decision: copilot or dashboard? — the current build is a paste-and-go copilot. That's appropriate for V1 proof of concept, but the real friction in a 35-account book isn't generating outputs — it's having to manually copy-paste context for every signal. V2 should seriously evaluate: persistent account profiles (localStorage minimum, backend if viable), live integration hooks (PostHog API for usage data, Slack for message volume, Stripe for billing signals), and a lightweight dashboard view showing account health at a glance without needing to trigger a prompt. V1 and V2 will coexist — the evolution is the artefact, not just the outcome.

HTML/CSS/JS Claude API (Anthropic) Client-side No backend PostHog

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-22 — is this actually useful?

Issue V1 works as a proof of concept but the interaction model has a real problem: it requires copy-pasting every signal individually. For a CSM managing 35 accounts who's already cognitively loaded, adding friction at the point of highest demand is counterproductive. The tool was designed to reduce cognitive load and it may be adding it instead. The paste-and-go mechanic made sense for a single-tool demo, but it doesn't map to how a CSM's day actually flows. This is the core question V2 has to answer before building more features on top of the same interaction model.

2026-05-22 — version preservation as a principle

Decision When V2 is built, V1 stays. The file doesn't get overwritten — it gets succeeded. The evolution from paste-and-go copilot to something with persistent state (or a full dashboard) is the actual story, and that story only exists if both versions are visible. Every build should be treated as a dated artefact: posthog-cs-copilot.html is V1, V2 gets its own file and its own devlog. What worked, what didn't, what changed — that's the devlog's job to document. The portfolio is the record, not just the outcome.

2026-05-22 — tool shipped, devlog open

Done V1 of the PostHog CSM Copilot is live. Four modes: Triage + Next Action, Draft Response, Escalation Strategy, Health Assessment. Customer context panel with type, ARR tier, tenure, and product checkboxes. Deep PostHog encoding in the system prompt — product suite, billing model, red flag list, customer type distinction. Roadmap established.

2026-05-22 — the copy-paste problem in detail

Issue The friction point is real: a CSM opens their morning Slack, has 8 messages across 6 accounts, and to use this tool they need to copy each message, switch tabs, paste, fill in context, hit analyse, read the output, switch back. That's 8 context switches before 9am. The tool was conceived as cognitive load reduction but the interaction adds its own load. The question isn't whether the AI output is good — it is — it's whether the delivery mechanism gets in the way of the work it's supposed to accelerate. A dashboard that watches for signals and surfaces them without manual paste would be structurally better for this workflow.

2026-05-22 — copilot vs. dashboard: the architecture fork

Pivot Two fundamentally different tools could serve this role. Copilot (current): you bring it a signal, it tells you what to do. Dashboard: it watches your accounts and surfaces signals to you. The copilot is buildable client-side with no backend. The dashboard requires integrations — PostHog's API, Stripe webhooks, Slack message volume — which means a backend, authentication, and ongoing maintenance. V1 proves the prompt engineering and the domain knowledge are sound. V2 needs to decide which architecture actually fits the workflow before adding features. Building more features on a fundamentally friction-heavy interaction model doesn't fix the model.

2026-05-22 — feedback routing is a missing mode

Note The brief explicitly calls out "owning their feedback and making sure it gets to the wider PostHog team." That's a distinct workflow from triage. A customer mentions a pain point in passing — not a ticket, not a complaint, just a friction they mention while asking about something else. The current tool categorises this as a "Feature Request" signal type but produces no output for the internal side: what do you tell the product team, how do you frame it, who should it go to? Feedback routing is an internal communication task, not a customer response task. It needs its own mode.

2026-05-22 — technical first-line as a mindset, not a mode

Note The brief says the CSM is "the first person to dig into customer issues, often solving them yourself rather than immediately passing to support." The current escalation mode starts from the assumption that escalation is likely — it helps you decide how to escalate, not whether you need to. A first-line debugging mode flips this: given a PostHog error or technical complaint, work through a structured diagnostic first. Common patterns worth encoding: SDK not initialising (autocapture vs. manual event setup), events ingesting but not appearing in dashboards (ingestion lag, property schema mismatch, team data isolation), feature flags not resolving (targeting rule logic, local evaluation vs. remote), session replay gaps (CSP headers blocking the script, sampling rate, rage click thresholds). Solving these before escalating is faster for the customer and builds trust faster than routing.

2026-05-22 — never be surprised as an architectural principle

Decision "Your aim is to never be surprised when a customer tells us they are leaving." That's not a workflow tip — it's an architectural requirement. The red flag list handles the reactive case (the customer signals something). But the proactive case — an account that's been quiet, whose usage has drifted, whose champion hasn't been seen for three weeks — requires scanning across all accounts on a schedule, not waiting for a signal to arrive. The red flag digest (roadmap item 3) is the implementation of this principle. The devlog should name the principle explicitly so it's clear what that roadmap item is actually solving.

2026-05-22 — proactive vs. reactive balance

Note The current tool is almost entirely reactive: something arrives, you process it. But the brief describes a proactive role — building relationships, watching health data, acting early. A good CSM probably runs 70% reactive and 30% proactive at minimum, and the proactive side is what separates great from average. Proactive outreach that adds value (sharing a relevant PostHog feature update, noting that their funnel conversion has improved since they implemented the flag they asked about) builds the relationship differently than "just checking in" messages. The tool has no playbook for this yet.

2026-05-22 — self-serve first-touch timing

Note Self-served customers who've never spoken to anyone at PostHog represent a specific timing problem. Too early (day 2, before they've done anything meaningful) and it feels like surveillance. Too late (day 45, after they've already decided it's not working) and you missed the window. Best practice in PLG CS suggests 14–21 days is the sweet spot: enough time to have done something, not so long that frustration has set in. But the trigger should be activation-based, not time-based — reaching out when they've completed a meaningful action (first dashboard created, first flag deployed) lands better than a calendar-triggered message. The tool has no playbook for this specific flow.

2026-05-22 — leading vs. lagging churn indicators

Note The red flag list currently mixes leading and lagging indicators without distinguishing them. Lagging: "evaluating alternatives", billing frustration, explicit complaints — the customer is already partway out the door. Leading: usage drop, contact silence, low feature adoption despite high ARR — the customer hasn't decided to leave but the trajectory points there. The tool's health assessment treats all signals the same. A more sophisticated health model weights leading indicators higher for early intervention, because by the time the lagging indicators appear the window for intervention is narrower. Worth encoding this distinction in V2's health scoring.

2026-05-22 — what "build automations" means at this ARR range

Note The brief explicitly invites automation: "If you want to build automations to help you, go for it." At $20k–$100k ARR, the automation stack that would make the biggest difference: (1) PostHog's own API for usage data — query event volumes, feature adoption rates, and DAU per customer on a schedule; (2) Stripe webhooks for billing signals — overages, upcoming renewals, failed payments; (3) Slack message frequency monitoring per customer channel. These three data sources cover the three dimensions of customer health (product, commercial, relationship) and none of them require complex infrastructure. This tool is the first layer — prompt engineering and domain knowledge. The next layer is piping real data into those prompts automatically.

2026-05-22 — what this tool is actually for

Decision The PostHog CSM role covers 25–40 paying accounts simultaneously. The challenge isn't that any individual account is hard — it's that you have 35 plates spinning at once and you need to know which ones are wobbling. This tool is about cognitive load reduction, not just triage. The output isn't just "here's a draft reply" — it's "here's what this signal means, how urgent it is relative to everything else on your plate, and exactly what to do next."

2026-05-22 — why not extend support-triage

Decision Support triage is about ticket severity — what urgency level and what's a good first response. CSM work is a different layer. The CSM owns the commercial relationship, not just the technical issue. A billing question from a $100k enterprise customer is a different signal to the same question from a $20k self-serve account — same words, completely different stakes and required response. A generic triage tool can't make that distinction without bespoke encoding. Separate tool, separate prompt architecture.

2026-05-22 — the two customer types are load-bearing

Decision Sales-known and self-served customers need different handling at every layer. Sales-known: has an established relationship, expects warmth and memory of past context, reads absence of contact as neglect. Self-served: has never spoken to a human at PostHog, high silent churn risk, first contact must feel like a knowledgeable friend not an account manager chasing a renewal. Getting the tone wrong with a self-served customer — especially on first contact — can permanently poison the relationship. This distinction is encoded into every mode's output logic.

2026-05-22 — four modes, one surface

Decision A CSM's day involves several distinct cognitive tasks: reading incoming signals and prioritising them, drafting responses, deciding when and how to escalate, and periodically reviewing account health. These share the same input (customer context + a signal) but produce completely different outputs. Rather than four separate tools, one surface with mode tabs keeps the context panel persistent and reduces the "start over" friction. The mode selector changes the system prompt; everything else stays the same.

2026-05-22 — ARR-tier-aware priority reasoning

Decision Priority isn't just severity of the issue — it's severity × relationship weight × commercial stakes. The same billing question rates differently at $20k vs. $100k+ ARR. The same technical issue rates differently at 1 month tenure vs. 2 years — a new customer hitting a bug is more likely to churn than an established one. Encoding this into the triage prompt means the CSM gets recommendations calibrated to the actual account, not just the ticket surface.

2026-05-22 — PostHog product suite encoding

Note The base prompt encodes PostHog's six core products with a one-liner on each: Product Analytics, Session Replay, Feature Flags, A/B Testing, Error Monitoring, Data Pipelines. Error Monitoring is flagged as newer and still growing adoption — relevant because customers using it are higher engagement signals, and bugs/limitations there carry more frustration because the product is still maturing. This context lets the model give product-specific recommendations rather than generic SaaS advice.

2026-05-22 — credit billing as a distinct risk surface

Note PostHog's billing model is credit-based. Customers buy event/session bundles upfront. This means usage spikes create billing surprises — a customer's usage doubles and suddenly they're out of credits and getting throttled, or they get an unexpected overage invoice. This is a friction pattern specific to PostHog and not common to SaaS broadly. Encoding it means the model recognises "we're running low on credits" signals as commercial moments requiring a specific kind of response (renewal conversation, usage review), not just billing support.

2026-05-22 — red flags as institutional knowledge

Decision The red flag list in the prompt isn't a generic churn signal list — it's a set of specific behavioural patterns that precede churn in CSM work: usage drop thresholds, silence windows, specific language patterns, renewal proximity combined with any negative signal. These are things an experienced CSM develops instinctively over years. Encoding them explicitly means the tool can surface them even when the CSM is on their 20th message of the day and their pattern-matching is depleted. The goal is to be the fresh pair of eyes that never gets tired.

2026-05-22 — customer integrity as a design constraint

Note "Maintaining customer integrity" was the explicit brief: responses must feel personal and genuine, not like they came from a template engine. The draft prompt explicitly instructs the model to avoid openers like "Hope you're well!", to calibrate warmth to customer type, and to never use placeholders. This is a quality floor, not a stylistic preference. A CSM who sends template-y responses to 35 accounts will eventually lose relationships because customers notice — and the damage accumulates silently before they say anything.

2026-05-22 — escalation levels as a decision tree

Decision The escalation mode offers five levels: Solo, Support Engineering, Product Team, Manager/Leadership, Executive. This isn't just taxonomy — each level implies a different internal action and a different external message. Getting the escalation level wrong in either direction is costly: under-escalating a commercial risk leaves a $100k account unprotected; over-escalating a technical question burns internal capital and signals poor judgement. The mode produces both an internal handoff draft and a customer-facing holding message, because both need to happen simultaneously and both need to be calibrated.

2026-05-22 — health assessment as weekly discipline

Note The health mode is designed for periodic account reviews, not reactive triage. The output includes a Green/Yellow/Red score, a key signals breakdown (positive and concerning), renewal and expansion risk, and 30-day recommended plays. The plays are intentionally specific enough to schedule — not "check in with the customer" but "book a usage review call to address the Session Replay adoption gap from last month". Vague action items don't get done. Specific ones do.

2026-05-22 — what this tool can't do

Issue Real customer health monitoring would pull live data: product usage metrics, Stripe billing data, Slack message frequency, ticket open rates. This tool works entirely on what the CSM pastes in. That's a genuine constraint — a health assessment is only as good as the signals the CSM provides. It's a copilot, not a dashboard. The roadmap includes account snapshot cards (localStorage persistence) to reduce context entry friction, and a batch red flag digest to do proactive portfolio scanning — but real data integration would need a backend.

2026-05-22 — multi-threading as an unbuilt layer

Note The job description calls out multi-threading explicitly: knowing key people at each company, managing escalations across stakeholders. The current tool handles single-signal, single-account responses. Multi-threading intelligence — "you have the technical champion but not the business buyer, and renewal is in 60 days" — requires persistent account state across contacts. That's account snapshot cards in v2. For now, the context notes field is the escape hatch: the CSM can paste relationship context manually and the model will factor it in.

2026-05-22 — model selection reasoning

Decision Haiku 4.5 is default. For the majority of signals — a Slack message, a ticket, a quick health check — Haiku is fast and sufficient. The nuance in the output comes primarily from the prompt engineering and the customer context, not model capability. Sonnet is recommended for complex escalation strategy decisions or health assessments where you're weighing multiple signals against each other. Same cost confirmation pattern as the other portfolio tools: no Sonnet request fires without the user seeing a price and acknowledging it.

FAQ Chatbot Live

May 2026 – Present

Paste a company's FAQ or internal knowledge base, paste a customer message, and get an answer grounded entirely in those docs — nothing made up, nothing hallucinated. Built to show how a lightweight AI support chatbot actually works: no vector database, no embeddings pipeline, no backend. Just a system prompt, a context window, and a model that knows to say "I don't have that" when the docs don't cover it.

The goal is a tool a client could realistically drop into their website with minimal setup — and a devlog that walks through every decision, tradeoff, and constraint so someone reading it understands not just what was built, but why it works and where it breaks.

Wins
  • Paste anything — Works with any plain text: Notion exports, Confluence pages, Google Docs, Zendesk article dumps, or a hand-written FAQ. No formatting required. If you can copy it, it works.
  • Claude does the search — No vector database, no embeddings, no semantic search infrastructure. The knowledge base goes into Claude's context window and the model finds the relevant answer natively — including paraphrased questions that wouldn't match keyword search.
  • Grounded by design — The system prompt explicitly instructs Claude: only answer from the provided docs. If the answer isn't there, say so. This is the critical design choice — without it, the model will helpfully hallucinate a plausible-sounding answer.
  • Sub-cent answers — A small FAQ (5K chars) costs ~$0.001 per query on Haiku. A large one (60K chars) runs ~$0.01. Still cheaper than a human response. Cost shown live after each query so there are no surprises.
  • Drop-in ready — One HTML file. Add an API key and paste your docs. No install, no setup, no infrastructure. In a real deployment, you'd swap the client-side API key for a backend proxy — but the logic is identical.
  • Shows the real pattern — KB-in-context is the core of most RAG-lite chatbots — even expensive ones. This demo strips away the infrastructure to show the concept clearly: the prompt engineering, the grounding rule, and where the limits actually are.

HTML/CSS/JS Claude API (Anthropic) Client-side only GitHub Pages Prompt engineering

Open Tool →
// roadmap
  1. Prompt caching — The single biggest cost lever that stays close to the current architecture. Requires adding a thin backend (a serverless function) to hold the KB server-side and mark it as a cacheable prefix in Anthropic's API. Cache hits cost 10% of normal input rate — a 60K char KB drops from ~$0.012 to ~$0.0012 per query on cached calls. Same model, same prompt logic, just stops re-reading the full document on every single request. Right first step once query volume makes it worth the minimal infrastructure overhead.
  2. KB distillation — A one-time client-side preprocessing step: Claude reads your full docs and produces a compressed, Q&A-optimised summary stored alongside the original in localStorage. Smaller compressed KB means cheaper queries with no backend required. Trade-off: compression loses some detail, so this suits broad FAQ coverage more than precise technical docs where exact wording matters. No infrastructure cost — just one extra API call upfront.
  3. RAG pipeline — The right answer for document sets too large for context or for high-volume deployments where per-query cost needs to be pushed as low as possible. Chunk the docs, generate embeddings once (cheap — text-embedding models cost a fraction of chat models), store in a vector database, retrieve only the two or three relevant chunks per query. Meaningful infrastructure step up: embedding model, vector store, retrieval logic. Makes sense when the KB outgrows what context-window retrieval can handle cleanly.
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-20 — roadmap: the re-reading problem

Note The core inefficiency in the current design: every single query re-sends the full knowledge base. The KB doesn't change between queries, but the model re-reads all of it every time. For a 60K char KB that's ~15,000 input tokens on every call — whether the question needs one sentence of docs or all of them. This is the thing the roadmap is built around fixing, in order of complexity and infrastructure cost.

2026-05-20 — roadmap step 1: prompt caching

Decision Anthropic's prompt caching feature lets you mark a prefix of the context (e.g. the system prompt including the full KB) as cacheable. First call is a cache miss — costs 125% of normal input rate. Subsequent calls within 5 minutes are cache hits — cost 10% of normal input rate. For a 60K char KB, that flips the input cost from ~$0.012 per query to ~$0.0012 on cache hits. 10× reduction on KB token cost for free, effectively. Constraint: requires a backend. Prompt caching only works on server-side API calls — you can't send the cache-control headers from a browser. So this step means adding a thin proxy: one serverless function that holds the KB, manages the cache headers, and forwards the user's question. At low volume, serverless costs near nothing. This is the right first step once query volume justifies it.

2026-05-20 — roadmap step 2: KB distillation

Decision An option that stays within the current client-side architecture: a one-time preprocessing step where Claude reads the full KB and produces a compressed, Q&A-optimised version. Paste your 60K char policy docs, click Compress — Claude distills them down to the 15K chars of actual answerable facts. Store the compressed version in localStorage. Every subsequent query uses the smaller, cheaper version. Cost: one API call upfront (~$0.013 for a 60K char doc), then ongoing savings on every query. No backend required. The honest trade-off: compression is lossy. A distilled KB won't answer every edge case the original would — it suits a broad FAQ better than a technical manual where exact wording matters. Useful as an intermediate step before adding proper infrastructure.

2026-05-20 — roadmap step 3: RAG

Note Retrieval-Augmented Generation is the full answer to the re-reading problem: chunk the docs, embed each chunk once using a cheap embedding model (text-embedding-3-small costs ~$0.02/MTok — a 100-page doc embeds for under a cent), store the vectors, and at query time retrieve only the 2–3 most relevant chunks to send alongside the question. Queries go from sending 15,000 tokens to sending ~500. Cost per query drops by ~96% compared to the current full-KB approach. The infrastructure price: you need an embedding model, a vector store, and retrieval logic. That's a real architectural step up. It makes sense when the KB is too large for context-window retrieval to work cleanly, or when you're handling enough volume that even prompt-cached queries add up. Not the right first step — but the right answer at scale.

2026-05-09

Done v0.3 live. Debounced KB save + beforeunload safety net. API cost unchanged. Browser overhead reduced significantly on large KBs.

2026-05-09 — does this affect API cost? No.

Note To be direct: debouncing has zero effect on Anthropic API spend. localStorage is entirely client-side — Anthropic never sees it. The only things that drive API cost are KB size (input tokens per query), conversation history length (accumulates across turns), and how often someone hits Ask. None of those change. What debouncing does affect is browser resource usage: localStorage.setItem is synchronous and serialises the full KB string on the main thread on every call. With a 60K char KB and 300 keystrokes in an editing session, the old approach meant ~18MB of data serialised to disk in a few minutes — wasteful and jank-inducing on older hardware or mobile. Debouncing turns those ~300 disk writes into roughly one. Right pattern, wrong reason to expect cost savings.

2026-05-09 — debouncing localStorage writes

Decision Switched from saving on every keystroke to a debounced save: the write only fires 800ms after the user stops typing. A beforeunload listener acts as a safety net — if the tab closes before the debounce timer fires, the save happens immediately on the way out. The "saved locally" badge still appears as soon as the user types (it's not delayed), so the UX is unchanged.

2026-05-09

Done v0.2 live. KB persists via localStorage with saved/clear indicator. Multi-turn conversation thread with per-exchange cost and copy buttons. Session total cost shown in conversation header. New Chat button to reset. QIGO note in KB panel. All constraints documented above rather than hidden.

2026-05-09

Decision Added a "quality in, quality out" note at the bottom of the KB panel. It's subtle — small, italic, monospace — but it sets the right expectation before anyone hits Ask. Vague docs produce vague answers. A one-line FAQ that says "we have a refund policy" without explaining what it is will produce an answer that says "we have a refund policy" without explaining what it is. The tool is only as good as what you feed it.

2026-05-09 — hiccup: KB changes mid-conversation

Note Nothing stops someone from editing the KB while a conversation is in progress. If they do, subsequent answers draw from the updated KB, but earlier answers in the same thread used the old one. The conversation thread could silently contain contradictions. The tool doesn't warn about this. For a demo, it's acceptable. For anything real, the KB should be locked for the duration of a session, or the UI should force a new chat when the KB changes.

2026-05-09 — cost impact of conversation history

Issue Conversation history accumulates input tokens on every turn. The KB (~15K tokens for a 60K char doc) dominates cost early on — each exchange adds maybe 100–400 tokens of conversation context, which is 1–3% extra per turn. By exchange 10, the history might add 2,000–4,000 tokens, roughly a 15–25% cost increase per call compared to turn 1. The KB itself doesn't change; it's the conversation that grows. Capped at sending the last 20 messages (10 exchanges) to the API to prevent runaway costs on very long sessions. The full visual history is always shown — only what gets sent to the API is trimmed.

2026-05-09 — multi-turn conversation history

Decision Added a conversationHistory array that accumulates each Q&A pair. Every call to the API now includes the full exchange history alongside the system prompt, so follow-up questions work: "Can I return it?" → "How long will that take?" carries context correctly. A "New Chat" button resets the history and wipes the conversation thread. The question textarea clears after each submission so the next question is always a fresh input.

2026-05-09 — hiccup: localStorage is readable plain text

Issue The stored KB sits in plain text in the browser's localStorage, visible to anyone who opens DevTools → Application → Local Storage. Any JavaScript running on the page can also read it. For a public FAQ this is irrelevant. For a company storing internal pricing, HR policies, or anything sensitive — don't use client-side storage. This is another reason the right production architecture keeps the KB server-side, behind authentication, with the client only ever receiving the answer.

2026-05-09 — hiccup: shared devices and cross-device gaps

Issue localStorage is browser-and-device-specific. Two people using the same browser on the same machine share the same stored KB — one person's company docs will silently appear for the next user. Conversely, someone who sets up the KB on their desktop opens a blank KB on their laptop. For a single-user demo tool this is acceptable friction. For any kind of team deployment it's a problem that only a server-side KB solves. Also worth noting: "clear browsing data" wipes localStorage with no warning. Users who clear their browser routinely will lose the saved KB and may not immediately realise why.

2026-05-09 — hiccup: stale knowledge base

Issue localStorage doesn't know your docs changed. A company updates its refund policy in January; the stored KB still says December's policy. The tool gives confidently correct answers based on outdated information. There's no version check, no sync, no expiry. For this demo tool, the mitigation is user discipline — clear and re-paste when docs change. In a production deployment, the KB would live server-side and be updated there; client-side storage wouldn't be in the picture at all.

2026-05-09 — cost impact of KB permanence

Issue Permanence is a silent cost amplifier. Every query re-sends the full KB regardless of how long it's been sitting in storage. A user who pastes a 60K char KB (~15K tokens) and walks away for a month is still paying ~$0.012 per query in input tokens every time they return — and they might not remember how large their KB is. At low volume this is trivial. At 1,000 queries/month against a large KB, input costs alone run ~$12. It's still cheap, but it's no longer invisible. Production deployments with a backend could cache the system prompt using Anthropic's prompt caching feature, which reduces repeated KB costs by up to 90% — but that requires a server.

2026-05-09 — tackling permanence: KB persistence

Decision Saved the knowledge base to localStorage on every keystroke. It survives page refreshes, tab closes, and browser restarts on the same device — so you set up your docs once and they're there next session. A "saved locally" badge appears in the KB panel header, with a clear link if you want to wipe it. 60K chars = ~60KB, well inside localStorage's ~5MB per-origin limit.

2026-05-08

Done v0.1 live. Two inputs (KB + question), one output (answer), cost shown per query. Character counters with soft warnings at 20K chars for the KB and 500 chars for the question. Hard truncation at 60K / 1K with an injected note so the model knows input was cut.

2026-05-08

Issue Constraints: Cost scales linearly with KB size — not a problem at small scale, matters at volume. No conversation history: each question is independent, so follow-up questions don't carry context from the previous answer. API key is client-side, so it's visible in the browser's network tab — fine for a demo, not for production. No logging or analytics out of the box. Can't handle non-text content (images, tables in PDFs, charts). Beyond ~60K chars, a proper RAG pipeline is the right answer.

2026-05-08 — wins & losses

Done Wins: Zero infrastructure for the demo. Works with any plain text KB — no preprocessing required. Claude's semantic understanding handles paraphrased questions that would defeat keyword search. Grounded by design: explicit fallback when the docs don't cover something. Cheap enough to run as a real tool, not just a demo. One file, shareable as a direct link.

2026-05-08

Note Production path for a real client deployment: add a thin backend (a single serverless function works) that holds the API key, rate-limits requests, and optionally caches responses for common questions. The KB can be stored server-side so clients never see it. The system prompt, grounding rule, and model call are identical to this demo — the only thing that changes is where the API key lives.

2026-05-08 — constraint: context window ceiling

Issue The 60K char hard limit in this tool is a practical ceiling, not a model limit. Claude can technically handle much more. But past ~60K chars, you're paying meaningfully per query and the user experience of pasting 100K chars into a textarea is poor. For larger knowledge bases — multi-section product docs, extensive policy manuals — the right answer is a proper RAG pipeline: chunk the docs, embed them, and retrieve only the relevant chunks per query. This tool demonstrates the concept; that's the production version.

2026-05-08 — constraint: cost scales with KB size

Issue Every query re-sends the full knowledge base. A 5K char FAQ costs ~$0.001 per query. A 60K char KB costs ~$0.01. Still cheap in absolute terms, but if you're handling 10,000 queries a month against a large KB, you're looking at ~$100/month in input tokens alone — before output. For high-volume use, caching common answers or splitting the KB by topic would reduce this significantly.

2026-05-08

Tried Considered adding streaming output so the answer appears word-by-word. FAQ answers are 2–4 sentences and Haiku responds in under 2 seconds. Streaming adds code complexity and browser compatibility surface area for a response that's already fast enough to feel instant. Dropped.

2026-05-08

Decision Model: Claude Haiku 4.5. Max output: 600 tokens. FAQ answers are 2–4 sentences — even a generous response rarely hits 200 tokens. 600 gives headroom without waste. Haiku handles this pattern easily; Sonnet is overkill and 10× more expensive per query.

2026-05-08

Decision The system prompt contains one critical rule: if the question cannot be answered from the knowledge base, respond with a specific "I don't have that in our documentation" message. Without this, Claude will produce a confident, helpful, entirely fabricated answer. The grounding constraint is the whole point of the tool — it's what makes it trustworthy rather than just fluent.

2026-05-08

Note Why this works: Claude Haiku has a 200K token context window — roughly 150,000 words, or about 250 pages of text. Most company FAQ docs are well under 10,000 words. The entire knowledge base fits comfortably, and Claude's comprehension across that context is strong enough for straightforward Q&A. This is sometimes called "long-context retrieval" — using the model's context window as the retrieval mechanism instead of a separate search system.

2026-05-08

Decision The knowledge base goes directly into Claude's context window as part of the system prompt. No vector database, no embedding model, no similarity search. Claude reads the entire KB and finds the relevant section itself — the same way a human would skim a help doc.

2026-05-08 — initial build

Decision Same architecture as the other tools: single HTML file, no backend, GitHub Pages. The pattern works — a static file with a client-side API call is shareable as a direct link with zero setup. No reason to introduce a server unless we need one.

LoreBySpec Creator Tool Live

May 2026 – Present

A two-mode content assistant built for my YouTube channel LoreBySpec. Toggle between Video Ideas — paste a topic or theme and get five titled angles with one-sentence hooks — and Comment Reply — paste a viewer comment and get a drafted reply in a warm, knowledgeable, lore-nerd voice. Both modes call the Anthropic API directly from the browser. One tool, one key, two workflows.

Wins
  • Two tools, one file — Video Ideas and Comment Reply share an API key bar and shell without needing separate pages, build steps, or navigation. The toggle is the only UI decision needed.
  • No proxy layer — Uses Anthropic's direct browser access header to call the API natively. No Cloudflare Worker, no relay, no CORS gymnastics.
  • Structured idea cards — Video Ideas responses are parsed into discrete cards (title + hook) rather than a wall of text. Scannable and visually distinct.
  • Prompt-engineered voice — The Comment Reply system prompt bans sycophantic openers ("great question!") by name, and anchors the persona specifically as LoreBySpec rather than a generic assistant.
  • Zero storage, zero friction — Session-only API key, no login, no account, no database. Paste key, write prompt, get output.
Constraints
  • Requires your own API key — no shared backend means users need an Anthropic account. One-time setup, but it's real friction for anyone who isn't already in the API ecosystem.
  • Jina description fetch is best-effort — YouTube is heavily JavaScript-rendered, so Jina may return partial content or time out on some videos. The tool degrades gracefully (title-only, or no video context) rather than blocking, but a dedicated YouTube Data API integration would be more reliable.
  • No per-idea copy buttons — individual idea titles and hooks aren't individually copyable yet. Worth adding if this becomes a daily-use workflow tool rather than a brainstorm aid.

HTML/CSS/JS Claude API (Anthropic) Tool Use / Structured Output Haiku 4.5 YouTube oEmbed Jina Reader Direct browser API access Client-side only GitHub Pages

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-09

Done v2.0 live. Structured output via tool use for Video Ideas. YouTube URL input with oEmbed + Jina context for Comment Reply. Stage-by-stage error handling with specific user-facing messages at every failure point. Non-fatal fetches degrade gracefully rather than blocking.

2026-05-09

Note Removed "no session history" from constraints after reviewing whether it was actually a limitation. It isn't — each interaction is self-contained (video + comment → reply, topic → ideas). There's nothing to compare across sessions and nothing that would improve with persistence. It was a reflexive constraint, not a real one.

2026-05-09

Decision Added a stage log panel that shows each fetch and API step as it runs, with per-stage ✓/✗ states. Replaces silent failures with visible progress — you can see exactly where a fetch degraded and why, rather than getting an unexpected result with no explanation.

2026-05-09

Decision Added a YouTube URL field to Comment Reply. Fetches video title via YouTube's oEmbed endpoint (free, no key, CORS-enabled) and description excerpt via Jina Reader. Both are non-fatal: oEmbed failure = no video context at all, Jina failure = title-only context. The system prompt forks based on what was retrieved. A "video context used" tag appears on the reply panel so it's clear when the context was applied.

2026-05-09

Pivot Replaced the Video Ideas text parser with Claude's tool use API. Defining a JSON schema and forcing tool_choice: {type: "tool"} means the model returns structured data regardless of how it phrases the response. No more numbered-list parsing, no fallback needed. Every field is validated individually with a specific error message if missing.

2026-05-09 — v2 iteration

Issue Two constraints from v1 were worth fixing immediately: the text parser for Video Ideas was brittle against model formatting changes, and Comment Reply had no knowledge of which video the comment was on — making it generic lore-nerd rather than video-specific.

2026-05-09

Done v1.0 live. Video Ideas renders five idea cards (title in gold, hook in italic). Comment Reply renders a bordered panel with a copy button. Parser falls back to raw pre-formatted text if the model deviates from expected structure. API key validation strips non-ASCII before any header is set — same fix as the Lore Checker.

2026-05-09

Decision Stored the last reply text in a lastReplyText module-level variable rather than encoding it into the onclick attribute via JSON.stringify. The attribute approach works but gets fragile with long replies or special characters; a variable is clean and safe.

2026-05-09

Decision Comment Reply system prompt explicitly bans sycophantic openers by name ("great question", "love this", "so glad you asked"). Without this, Claude defaults to them. Named the persona as LoreBySpec rather than "a YouTube creator" to anchor the voice more specifically.

2026-05-09

Decision Video Ideas prompt specifies exactly 5 ideas in a strict numbered format: N. [Title]\nHook: [sentence]. The renderer parses this into cards. Chose structured plain text over JSON because the output is simple enough and the formatting instruction is clear — JSON mode adds a layer for no gain here.

2026-05-09

Decision Fixed model at Haiku 4.5 — no model selector. Both tasks (brainstorming titles, drafting short replies) are well within Haiku's capability. Adding a Sonnet toggle adds UI complexity without a meaningful quality difference for 2–5 sentence outputs.

2026-05-09

Decision Used anthropic-dangerous-direct-browser-access: true to call the Anthropic API directly from the browser, skipping the Cloudflare Worker proxy used in the Lore Checker. This tool doesn't need to fetch external URLs (no Jina, no wiki pages), so there's no reason to route through a relay. Simpler and one fewer moving part.

2026-05-09

Decision Combined both modes (Video Ideas, Comment Reply) into a single file rather than two separate tools. They're lightweight, share the same API key, and both serve the same pre-production workflow. A toggle is cheaper than a navigation layer.

2026-05-09 — initial build

Decision Same single-file, no-backend pattern as the Lore Checker — static HTML, GitHub Pages, bring-your-own API key. Both tools are for the same channel so they share visual DNA; different enough in layout to feel like their own thing.


PregVoice Research & Planning

Feb 2025 – Present

A layered accessibility project tackling one specific, overlooked gap: blind and visually impaired people cannot independently read a pregnancy test. The result is a deeply private moment that requires asking someone else — stripping dignity from an already vulnerable experience. PregVoice works through the problem in phases, from smartphone-first solutions requiring no new hardware, to researching the internals of existing digital tests, to evaluating custom hardware if no off-the-shelf path is viable.

Focus Privacy by default

A person shouldn't need to hand their phone to someone else, or broadcast a health result, to know if they're pregnant. Every design decision is filtered through: does this preserve independence?

Focus Smartphone first

The most accessible solution uses hardware people already own. Phase 1 explores camera-based result recognition with on-device processing — no cloud upload, no new purchases.

Focus Open & collaborative

This can't be built well without input from blind and low-vision users, hardware engineers, and medical device specialists. The devlog is the starting point for that conversation.

research Digital test internals

Clearblue Digital tests already have LCD displays and microcontrollers inside. Researching whether adding audio output is a hardware addition, a firmware change, or a partnership conversation.

concept Haptic fallback

Audio isn't always possible — public spaces, shared housing, no headphones available. Designing a haptic result language (e.g. pulse patterns) as a non-audio, non-visual fallback.

Constraints
  • Medical-adjacent — Anything that "reads" a medical test result sits close to regulated territory. The tool reads the visual output — it doesn't perform the test. That distinction shapes every technical decision.

Computer Vision On-device ML Web Speech API Haptics Hardware Research Accessibility

Read the Research →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-09 — revised output hierarchy: tones primary, haptic secondary, voice opt-in

Decision Primary audio output is now simple tones: 1 bleep = positive, 2 bleeps = negative, 3 bleeps = inconclusive, 4 rapid bleeps = error. Count-based, no linguistic content, no emotional valence, universal across languages. Phone speakers are far more consistent across device tiers than haptic motors — a budget phone from 2018 produces a clear bleep, but may not reliably differentiate long from short vibrations. Haptic is a genuine and important secondary output for silent environments and hearing impaired users, but device variability makes it unreliable as a universal primary. Voice remains available as an explicit opt-in with clinical phrasing — never the default, never auto-playing.

2026-05-09 — RNIB finding reframed: linguistic valence, not audio itself

Pivot The RNIB's audio rejection was specifically about voice output — "pregnant" or "not pregnant" spoken aloud. The problem is words, not sound. Words carry emotional weight regardless of tone or inflection. A bleep carries none. Additionally: not all pregnancy results are good or wanted. A positive result can be devastating depending entirely on context. Any audio format using language presumes something about the emotional register of the moment. Tones do not. This reframes the RNIB finding: not "no audio" — "no words."

2026-05-09 — patent and regulatory landscape confirmed

Note SPD/Clearblue holds no patent on audio output from a pregnancy test — a gap in their portfolio, and not a blocker for a partnership conversation. Third-party patent WO2014158850 ("Diagnostic Test Device with Audible Feedback") covers audio output on lateral flow readers and requires freedom-to-operate analysis before any audio hardware is commercialised. FDA regulatory path confirmed: Class II, new 510(k) required for audio addition, 12–24 month timeline, $250K–$1M cost. Manufacturer entry point: Clearblue Innovation Centre, Bedford, UK. The conversation needs a working prototype, market size data, and a regulatory summary. We do not yet have all three.

2026-05-09 — RNIB finding: users rejected audio for emotional valence reasons

Pivot The RNIB's 2020 accessible pregnancy test prototype — the most thorough user research in this space — found blind users did not want audio output. Any tone or voice sounds either positive or negative, and users did not want a machine to telegraph the emotional outcome before they were ready. RNIB used tactile output. APT (James Dyson Award 2022) independently reached the same conclusion and used haptic. This directly challenges our audio-first design. Revised position: haptic is a first-class output modality, not a fallback. The design must support haptic-only as a complete, uncompromised experience. Community research now the immediate priority before any UX decisions are locked.

2026-05-09 — teardown complete: MCU confirmed, buzzer pins native to the chip

Done Clearblue Digital MCU confirmed as Holtek HT48C06 / HT48R065B — off-the-shelf 8-bit OTP parts with public datasheets, not custom ASICs. Critical finding: the HT48C06 has dedicated buzzer drive pins (PB0/PB1) and a programmable frequency divider built into the silicon. Connecting a piezo element requires no external driver. The hardware to make the stick beep already exists inside it. The reason it makes no sound is a product decision, not a hardware limitation. Enclosure moulding tolerances are the actual constraint — any audio addition requires a new enclosure SKU, not a modification to the existing one. OTP firmware is unreadable; any partner would need to rewrite sensing logic from scratch.

2026-05-08 — framework: PWA for zero barrier, zero cost

Decision Progressive Web App for Phase 1: no app store, no install required, shareable as a URL, GitHub Pages hosting (free), TensorFlow.js for on-device inference (no API cost), Web Speech API for audio (no API cost), service worker for offline use. Running cost is near-zero — there is no server, no database, nothing to pay for monthly. This is not a compromise: an accessibility tool that eventually needs revenue is one that has failed its primary goal.

2026-05-08 — multilingual: clinical phrasing + professional translation

Decision "The test shows a positive result" rather than "You are pregnant" — clinical framing is less culturally variable, easier to keep grammatically neutral across gendered languages, and more defensible from a regulatory standpoint. Priority languages: English, Spanish, Arabic, French, Portuguese, Hindi. All result strings professionally translated with medical language awareness — no string ships to production on machine translation alone. RTL layout (Arabic, Urdu, Hebrew) is a layout requirement, not a post-launch fix.

2026-05-08 — ethics: open source as the trust mechanism

Decision Privacy policies are words. Code is evidence. The codebase is public from day one so any developer can verify that the app does exactly what it claims — no image retention, no analytics, no data leaving the device. Equity testing (model performance across all test brands, lighting, device quality tiers) is a release requirement. The tool is free, permanently. No premium tier, no advertising, no data brokering.

2026-05-08 — audio privacy: tap-to-hear model

Decision Auto-speaking a result on the phone speaker is never acceptable. A shared bathroom, a public toilet, a partner in the next room — any of these makes unsolicited audio a privacy breach. Default: haptic pulse signals result is ready, audio only on tap. Two optional modes: earphone-only (auto-speaks when headphones detected, silent on speaker) and delay timer (10–60s pause before the haptic fires, letting the user move somewhere private). The auto-speak-on-speaker mode does not exist in the architecture.

2026-05-08 — risk assessment: false result is primary concern

Issue A false positive or false negative in this context carries real psychological and medical consequences. The model ships with a confidence threshold — below it, the result is "inconclusive, re-check recommended", never a confident wrong answer. Language is always "the test shows" not "you are." Reliability is a release gate, not a metric to improve post-launch.

2026-05-08 — what's next

Decision Three parallel workstreams: (1) Build a smartphone prototype using ML Kit for on-device line detection — validate whether recognition is reliable enough across different test brands and lighting conditions. (2) Tear down a Clearblue Digital test and document the PCB — identify chipset, output pins, and whether audio addition is feasible without redesign. (3) Reach out to blind and low-vision communities for lived experience input before locking any UX decisions.

2026-05-08 — layer 3: custom hardware path

Note If the smartphone path has reliability limits and manufacturer partnerships aren't viable, a standalone reader accessory becomes the fallback. Concept: a small clip-on device that holds the test in a fixed position, uses a colour sensor (TCS34725 or similar) to read line intensity, and outputs audio + haptic. Powered by coin cell. Single-use pairing with standard test formats. Estimated BOM: under £5 at volume.

2026-05-08 — haptic language concept

Decision Audio output has real constraints: public spaces, shared housing, headphone availability, hearing impairments among the target group. Designing a haptic fallback: single long pulse = negative, two short pulses = positive, repeating triple pulse = inconclusive / error. Simple enough to learn once, distinctive enough not to misread. Mirrors how screen readers handle notification priority.

2026-05-08 — creative constraint: test timing

Note Most line-based tests are valid to read within a 3–10 minute window. Faint lines fade. A reader that takes 30 seconds to boot or requires multiple repositioning attempts isn't usable in practice. Speed and first-attempt reliability are non-negotiable. Digital tests hold their result for ~6 minutes, which gives more working time.

2026-05-08 — constraint: medical device regulation

Issue Anything classified as part of a diagnostic process falls under FDA (US) or MDR (EU) medical device regulation. Critical distinction: a tool that reads the visual output of an already-completed test is an accessibility aid, not a diagnostic device. The test itself is the regulated product; the reader is not. This distinction must be maintained in product design and language throughout.

2026-05-08 — layer 2: digital test internals

Note Clearblue Digital tests contain a microcontroller, LCD panel, and battery — all to display two words. The electronic reading stick re-uses the same lateral flow strip underneath but processes the optical result internally. Key question: does the MCU expose any output pins? If it does, adding a piezo buzzer or BLE chip is a hardware addition, not a redesign. Filed as a research thread.

2026-05-08

Pivot On-device ML is the answer. Apple's Vision framework and Google's ML Kit both run image classification on-device with no data leaving the phone. For line detection (one line vs. two, or a digital LCD reading "Pregnant") the model complexity is low — well within what a phone can handle offline. Privacy preserved by default.

2026-05-08 — privacy constraint: cloud vision

Issue Cloud vision APIs (Google Vision, Claude vision) would send an image of the test to a remote server. That's a photo tied to a health event — possibly the most sensitive moment in a person's reproductive life. Sending it to any third-party server, even one with strong privacy policies, is an unacceptable default for this use case.

2026-05-08 — layer 1: smartphone camera

Decision Phase 1 targets the most accessible hardware: the smartphone everyone already owns. Approach: point camera at test → recognise the result → speak it aloud. No new hardware, no app purchase required. Works with standard pregnancy test formats (line tests and digital). This is the fastest path to impact.

2026-05-08

Note Clearblue Digital tests (and similar) already show "Pregnant" / "Not Pregnant" as text on an LCD screen. This is the closest existing accessibility accommodation — high-contrast text is easier to read for low-vision users with magnification tools. But it produces zero audio, zero haptic output. The hardware exists; the output modalities don't.

2026-05-08 — existing tools audit

Tried Checked what already exists. Be My Eyes and Aira connect blind users to sighted volunteers or AI descriptions via camera. Technically usable — but both require broadcasting a live video feed of a pregnancy test to a third party (volunteer or company server). Privacy cost is too high for a result this personal. Not a viable general solution.

2026-05-08 — problem framing

Decision The problem is narrow and specific: a blind pregnant person taking a home pregnancy test has no independent way to read the result. Every current path requires involving another person. That's not an edge case — it's a structural gap. Starting from that constraint rather than "make pregnancy tests accessible" keeps the scope tractable.


Technical Writing Portfolio Portfolio Sample

May 2026

Four reference-quality playbook documents across ops and HR systems, written for different audiences to show range as well as depth. Covers API troubleshooting, SQL data audits, EOR payroll processing, and tiered escalation logic.

What it demonstrates:

  • Technical depth for practitioner audiences — written to be used under real ticket pressure, not filed in a wiki
  • Cross-audience translation — same domain knowledge, completely different register (e.g. technical SQL guide vs. non-technical EOR explainer)
  • Process design embedded in documentation — the escalation playbook doesn't describe a process, it is the process
  • Scope discipline — each document is bounded precisely; knowing what not to include is a writing skill

Technical Writing API Troubleshooting SQL EOR / Payroll Support Operations Process Documentation Zendesk Slack

Read the documents →

SaaS Integration Failure Diagnostic Guide Portfolio Sample

May 2026

Internal support enablement document written to demonstrate technical writing capability in a SaaS/HRIS integration context. Designed to equip Tier 1 and Tier 2 support agents with structured diagnostic workflows for webhook delivery failures, API authentication breakdowns, and downstream HRIS sync errors — the failure modes that are most commonly misdiagnosed or escalated unnecessarily.

What it demonstrates:

  • Translating complex integration failure patterns into structured, agent-usable triage flows
  • Writing for audience: agents with no API background, not developers
  • Knowing what to surface (silent webhook failures, sync token expiry, field mapping edge cases) and how to sequence it
  • Building tools that reduce escalation rate, not just document for documentation's sake

Technical Writing SaaS Integration Webhooks API Authentication Support Enablement Zendesk Jira

Read the document →

Cat API Documentation Portfolio Sample

May 2026

Professional API reference documentation for The Cat API's images search endpoint, written as a portfolio sample for a technical writing role. Covers authentication, all query parameters, response schema, request and response examples, error states, and pagination behaviour — structured to Stripe/Twilio documentation standards.

What it demonstrates:

  • Precise parameter documentation — types, defaults, valid values, and the edge cases that matter (e.g. pagination disabled when order is RANDOM)
  • Authenticated vs unauthenticated distinction surfaced where it's actually relevant, not buried in a footnote
  • Error states written for resolution, not just enumeration — each entry tells the reader what to check
  • Clean docs-as-code formatting: tables, code blocks, and a copy-pasteable curl example

Technical Writing API Documentation REST APIs Docs as Code

Read the document →

Voice & Tone Guide Portfolio Sample

May 2026

Internal contributor guide defining voice, tone, and writing standards for technical documentation teams — written as a portfolio piece demonstrating documentation leadership and the ability to set standards, not just follow them. Covers voice characteristics, how tone shifts by context and audience state, common mistakes, and a contributor checklist.

What it demonstrates:

  • Documentation leadership — writing the guide, not just using one
  • Principled voice definition with specific, actionable rules rather than vague aspirations
  • Tone adaptation frameworks: what changes based on context, what stays constant
  • Common failure modes named explicitly — the kind of pattern recognition that comes from editing a lot of documentation

Technical Writing Documentation Standards Style Guide

Read the document →

Voice Adaptation Showcase Portfolio Sample

May 2026

The same content — a short how-to guide for setting up two-factor authentication on a fictional app — written three times, each version calibrated to a different company's documented voice principles: Apple, Mailchimp, and PostHog. Each section includes the source guide URL, the specific principles applied, the content itself, and a reflection on what was hardest to adapt.

What it demonstrates:

  • Voice adaptation as a research discipline — reading a style guide, identifying the applicable principles, and making deliberate choices at the sentence level
  • Range: from Apple's precise, user-empathetic register to PostHog's direct, developer-first terseness
  • Tone judgment at the security-critical moment (backup codes) — each version handles it differently and for documented reasons
  • Self-awareness about native register — Spec's Apple-influenced defaults are named explicitly so the departures from them read as intentional

Technical Writing Voice & Tone Style Guide Research

Read the document →

Support Ticket Triage Tool Live

May 2026 – Present

Paste a customer message, error log, or support thread — and Claude returns a category, an urgency level (colour-coded Low to Critical), and a suggested first response ready to copy and send. One input, one button. Built to directly demonstrate the kind of tool you'd build or configure in a support or implementation role.

Wins
  • No backend — Shareable as a direct link, runs in any browser with just an API key. Zero setup friction for whoever you send it to.
  • Costs pennies — Built on Claude Haiku (~0.2¢ per triage). Viable as a real tool, not just a portfolio demo.
  • Paste anything — Customer messages, error logs, stack traces. Structured input triages cleanly, and the suggested response adapts to what it received.
  • Prompt is tunable — Categories, urgency signals, and response tone can be adjusted for any product in minutes. That's the implementation judgment this demonstrates.
// roadmap
  1. Bulk mode — paste a batch of tickets (newline-separated or numbered), run them sequentially through the same triage pipeline, and get a full results table. One API call per ticket keeps the cost model identical (~0.2¢ each) and the output clean. No queue management, no backend, just a loop.
  2. CSV export — after a bulk run, export the full triage results as a CSV: ticket text, category, urgency, suggested response. Pure client-side via the Blob API — nothing leaves the browser. Paste in, triage, download. Drop it straight into a spreadsheet or ticket system.

HTML/CSS/JS Claude API (Anthropic) Client-side No backend

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-21 — live + roadmap

Done Badge updated from "In Progress" to "Live" — tool is fully functional and available. Roadmap block added to the project card covering the two most useful next builds: bulk mode and CSV export.

2026-05-21

Decision Bulk mode design: accept a batch of tickets (newline-separated or numbered), run them through the existing triage pipeline one at a time in a loop. One API call per ticket — same ~0.2¢ cost model, no change to the prompt, no queue infrastructure. The output becomes a table rather than three panels. Same architecture, bigger input.

2026-05-21

Decision CSV export design: pure client-side via the Blob API. After a bulk run, serialise each row (ticket text, category, urgency, suggested response) into a CSV string, wrap in a Blob, create an object URL, trigger a download. No server, no data leaves the browser. The file is generated and consumed entirely in the tab — consistent with the no-backend constraint and no new privacy surface.

2026-05-09 — done

Done Confirmation modal live. Pricing displayed uses the current rates ($3.00/M input, $15.00/M output, dated May 2026) so it's transparent about when those figures were last updated. No request fires on Sonnet without acknowledgement. Haiku requests are unaffected — no friction for the common case.

2026-05-09

Note The confirmation covers both manual and auto-switched Sonnet use — there's no meaningful distinction from a cost perspective. What matters is that the user sees a number, reads a price, and chooses to proceed. Implemented as a Promise-based overlay: triage() awaits confirmSonnetCost(), which resolves true on confirm and false on cancel, so the async flow is clean and the request is never partially started.

2026-05-09

Decision Add a pre-flight confirmation modal that fires whenever a Sonnet request is about to be sent — whether the user manually selected Sonnet or the tool auto-switched. The modal shows the exact character count at click time, recalculates the cost fresh from the current message length, and requires an explicit "Send anyway" before the request fires. Cancel returns to the tool with no side effects.

2026-05-09

Issue Follow-up from Rainbow: the live cost estimate in the model bar is useful, but it doesn't force the user to acknowledge the cost before they commit. A user could auto-switch to Sonnet, not notice the estimate update, and send a large request without realising the price difference. The model bar display is passive — it updates, but it doesn't interrupt.

2026-05-09 — model switcher done

Done Model bar live: Haiku selected by default with a "recommended" badge, Sonnet available for longer or higher-stakes tickets. Truncation bug fixed — total sent is always ≤ hardLimit. Cost estimates update on every keystroke. The lore checker already had this pattern; applying it here was straightforward once the right constants were in place.

2026-05-09

Note Cost estimates update live as you type. Formula: input tokens estimated as 250 (prompt overhead) + ceil(messageLength / 4), output fixed at 300 tokens. At empty the display reads ~0.20¢ (Haiku) and ~0.60¢ (Sonnet). A 5,000-char message costs about ~0.28¢ on Haiku; the same at 10,000 chars on Sonnet is about ~1.05¢. The estimates are approximate — actual tokenisation varies — but they give the user a real-time sense of relative cost.

2026-05-09

Note Auto-switch logic: if the user types past 5,000 characters while on Haiku, the tool silently switches to Sonnet and shows a small amber badge — "auto-switched — input over 5k". If they delete back below 5,000, it reverts to Haiku. If the user manually picks a model, autoSwitched is cleared and the auto-revert no longer fires — their choice takes over.

2026-05-09

Decision Add a Haiku / Sonnet model switcher — borrowed from the lore checker pattern — to solve the truncation bug properly and add cost transparency at the same time. The fix: define TRUNCATION_NOTE as a constant, slice to hardLimit - TRUNCATION_NOTE.length before appending, so the total is always exactly hardLimit. The switcher makes hardLimit model-dependent: 5,000 chars for Haiku, 10,000 for Sonnet.

2026-05-09 — truncation bug

Issue Rainbow spotted a bug in the character limit: slicing input to exactly 5,000 characters and then appending '\n\n[Input truncated at 5,000 characters]' (36 chars) sends 5,036 characters to the API — silently exceeding the stated limit. A small off-by-one in principle, but it breaks the contract with the user and means the truncation note itself is the overflow.

2026-05-08 — verified

Done Both tools confirmed working end-to-end — support triage and lore checker tested live with a real API key and real input. The proxy resolves the CORS issue across all browsers. Removed the "best on desktop" browser warning from the tools since it's no longer accurate. Total time from "Load failed" with no context to a working cross-browser solution: one debugging session, one Cloudflare account, 25 lines of Worker code.

2026-05-08 — proxy deployed

Done Cloudflare Worker deployed at anthropic-proxy.work-specious.workers.dev. 25 lines. Handles CORS preflight (OPTIONS → 204 with Access-Control-Allow-Origin: *), forwards POST to Anthropic server-to-server, returns response with CORS headers attached. All three tools updated to call the proxy instead of api.anthropic.com directly. Worker code saved to proxy/worker.js in the repo. Free tier covers 100,000 requests/day — more than enough for a portfolio demo.

2026-05-08

Note This affects all three tools — lore checker, support triage, FAQ chatbot — since all use the same fetch pattern and the same header. The anthropic-dangerous-direct-browser-calls approach does not work from iamspecious.github.io. The no-backend architecture has hit its hard limit for this use case. Decision: deploy a Cloudflare Worker as a thin CORS proxy — handles the preflight server-side, free tier (100,000 requests/day), no infrastructure to maintain, works everywhere. A non-working portfolio tool demonstrates nothing; a tool that works and has a documented fix for a real architectural problem demonstrates more than one that never broke.

2026-05-08

Note The anthropic-dangerous-direct-browser-calls: true header is supposed to signal Anthropic's server to return Access-Control-Allow-Origin in its preflight response. The mechanism breaks at the preflight stage: the browser sends an OPTIONS request first, the server must include CORS headers in that response, and only then does the browser allow the actual POST through. Anthropic's server is not returning those CORS headers for the iamspecious.github.io origin — so the POST never fires. The header exists in the request the browser intends to send, but the server's OPTIONS response contains no CORS headers at all.

2026-05-08 — root cause confirmed

Issue Chrome console test on desktop, fresh install, no extensions, GitHub Pages origin. Exact error: "Access to fetch at 'https://api.anthropic.com/v1/messages' from origin 'https://iamspecious.github.io' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource." The domain is reachable (api.anthropic.com returns 404 at root, as expected). The failure is CORS, not DNS, not network, not key format.

2026-05-08

Note The fundamental constraint here is the "no backend" architecture. A backend proxy would handle the API call server-side and return the result to the browser — no cross-origin request, no CORS, no preflight, works everywhere including iOS Safari. But adding a backend contradicts the design constraint and the point of the demo: to show what you can build and deploy without infrastructure. That trade-off is real and documented. The right answer for a production tool is a thin backend. The right answer for a portfolio demo is the browser compatibility notice.

2026-05-08

Decision Added a browser compatibility notice to both tools — displayed above the API key bar. States clearly: best on desktop, Chrome and Firefox most reliable, mobile Safari may block cross-origin requests. Includes the iOS-specific workarounds: disable Content Blockers (Settings → Safari → Content Blockers), disable iCloud Private Relay, or try Chrome/Firefox for iOS.

2026-05-08

Note Mobile Safari also has no accessible developer console on-device. The "open F12 → Console" instruction in our improved error message is useless on iOS — there is no F12. Debugging a network error on mobile Safari requires pairing the device with a Mac running desktop Safari and using Remote Web Inspector, or testing on a desktop browser instead. Two issues surfaced from one test: the CORS restriction and the unreachable console.

2026-05-08 — browser confirmed

Issue Browser confirmed as mobile Safari on iOS. This is the specific environment where direct cross-origin API calls are most restricted. When a request includes custom headers (like x-api-key and anthropic-dangerous-direct-browser-calls), the browser fires a CORS preflight OPTIONS request first. iOS Safari handles this preflight more aggressively than desktop browsers — in some iOS configurations it blocks the request entirely before we get any response.

2026-05-08

Note Applied the same error handling improvements to the FAQ chatbot — it had identical single-line catch block with the same gap. Any tool making direct browser API calls needs this treatment. The lesson: a generic catch block is not error handling, it's error hiding. Stage-specific messages let users actually diagnose what went wrong instead of guessing.

2026-05-08

Decision Rewrote error handling across three stages. Stage 1 — before the request: validate that the API key starts with sk-ant- and show a specific message if not. Stage 2 — API error response: break down !response.ok by HTTP status. 401 = bad key, 403 = no permission, 429 = rate limited, 400 = bad request, 500+ = server error. Each gets its own message and its own next step. Stage 3 — network failure: catch TypeError separately from other errors and show a specific "network error" message with instructions to open the browser console (F12 → Console) for the actual error. Also added console.error() logging so there is always a full trace available.

2026-05-08

Note "Load failed" is what Safari throws when fetch() fails at the network layer before any response is received. Chrome calls it "Failed to fetch", Firefox calls it "NetworkError when attempting to fetch resource". All three are the same thing: the request never reached the API. Possible causes: a browser extension blocking api.anthropic.com, a CORS restriction, a firewall or VPN, or Safari's stricter cross-origin enforcement. The character cap was not the issue — the test message was under 200 characters.

2026-05-08 — live test

Issue First real test with a live API key failed. Button returned "Load failed" — two words, no context, no HTTP status, no indication of where in the chain it broke. From a debugging standpoint, completely useless.

2026-05-08

Note Updated the placeholder and system prompt to explicitly support error snippets, stack traces, and log excerpts. Structured text is actually easier to triage than messy prose — the model reads it cleanly. For log-only inputs the suggested response acknowledges the error and asks for context (steps to reproduce, environment, account details) rather than pretending it's a normal message.

2026-05-08

Decision Added a character counter with a soft warning at 3,000 and hard truncation at 5,000 — applied client-side before the API call. Support tickets rarely hit this. Error logs and paste dumps can. Truncation keeps a runaway paste from spiking cost; a note in the payload tells the model what happened. The limit also keeps max_tokens at 1,000 and the system prompt compact — three short sections is all this needs.

2026-05-08

Decision Switched model from Sonnet to Haiku. Triage is pattern recognition and professional writing — Haiku handles it comfortably. Cost drops from ~$1.80 to ~$0.42 per 1,000 triages. "Cents not euros" isn't a nice-to-have, it's a design constraint if this is going to be a viable tool rather than just a demo.

2026-05-08

Note The prompt is the meaningful part of this tool. Knowing what signals push a ticket from Medium to High, what tone to set, what categories fit a given product — that's the support judgment this tool demonstrates. The code is a vehicle for the prompt.

2026-05-08

Decision Added a copy button to the Suggested Response section. Not in the original brief, but practically obvious: in any real support workflow, the next thing you do after reading a suggested response is paste it somewhere. One click.

2026-05-08

Tried First prompt draft had Claude return urgency as a 1–5 score. Dropped it. Labels (Low / Medium / High / Critical) are what support teams actually use in tooling, and they read faster at a glance. Mapped each to a colour indicator so urgency is visible before you read a word.

2026-05-08

Decision Three outputs: Category, Urgency, Suggested Response. Category and Urgency are the triage layer. The Suggested Response is what turns classification into action — and the output that makes this demonstrable in an interview. "Here's the tool. Here's what it produces."

2026-05-08

Decision One input box, one button. The brief was explicit about minimal surface area. Resisted adding dropdowns, severity sliders, ticket history. The AI does the categorisation — that's the whole point. Adding a form would undermine the argument.

2026-05-08 — initial build

Decision Same architecture as the KH Lore Checker: single file, no backend, GitHub Pages. If the pattern works for one tool, it works for two. The whole portfolio stays static and the tools are shareable as direct links.


KH Lore Consistency Checker In Progress

May 2025 – Present

A theory stress-tester built for my YouTube channel LorebySpec. Kingdom Hearts has 25+ years of lore across 15+ titles — this tool lets you paste in a theory, supply relevant wiki links or context, and get a structured AI analysis: contradictions against canon, logical weaknesses, clarifying questions a skeptical viewer would ask, and an overall verdict. Built for real content prep, not as a demo.

Wins
  • Open by design — No account, no login, no subscription. Bring your own Anthropic API key and the tool works. Nothing stored, nothing tracked.
  • Costs cents, not euros — Default model (Haiku 4.5) runs a typical theory check for ~$0.001. You'd need 1,000 checks to spend a dollar. Cost shown live in the UI per check.
  • Zero infrastructure — Pure static HTML, no server, no database, no hosting cost. Lives on GitHub Pages. Nothing to maintain or secure.
  • Solved CORS without a backend — Browsers can't fetch external wiki pages directly. Used Jina Reader as a lightweight proxy to pull live KH wiki content client-side. No server code needed.
  • Prompt-engineered output — Returns four consistent sections every time: Contradictions, Weaknesses, Clarifying Questions, Overall Assessment. Structured for scriptwriting, not just reading.
  • Actually used in production — Built for real LorebySpec video prep, not as a demo. Every decision was made with cost, speed, and practical use in mind.
Constraints
  • Requires your own API key — the tool has no shared backend, so users need an Anthropic account. One-time setup, but it's friction that a hosted version would remove.
  • Context window caps lore volume — KH has 25+ years of lore across 15+ titles. Feeding the entire wiki is impractical. The right workflow is to link the specific pages relevant to the theory, not the whole series. Each URL is capped at 3,500 characters.
  • Jina Reader is best-effort — some wiki pages (behind login, heavy JS rendering, rate-limited) may fail to fetch. The tool flags this per URL but falls back to manual paste.
  • No session history — refresh the page and the inputs clear. There's no way to compare two theory runs side by side or review past checks. A persistence layer would fix this but needs a backend.

HTML/CSS/JS Claude API (Anthropic) Jina Reader Client-side only GitHub Pages

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-05-08

Fix Strip all non-printable-ASCII characters from the API key before it touches any header: rawKey.replace(/[^\x20-\x7E]/g, '').trim(). Added a format check that catches malformed keys early and tells the user to clear and re-paste rather than throwing a cryptic browser error.

2026-05-08

Note Root cause: pasting an API key from certain sources (email, PDF, browser autofill) can silently introduce Unicode characters — zero-width spaces, smart quotes, non-breaking spaces — that are invisible in the input field but have code points above 255. HTTP headers only accept ISO-8859-1 (0–255), so the browser rejects the whole request.

2026-05-08 — first live test bug

Issue First real test threw: "Failed to execute 'fetch' on 'Window': Failed to read the 'headers' property from 'RequestInit': String contains non ISO-8859-1 code point." The other two tools passed. Headers are identical across all three tools — the only dynamic header is the API key.

2026-05-08

Done v0.3 live. Model selector in UI shows live cost estimate per model. Estimate updates to show tokens, cost, and model name both during and after the call so there's a full paper trail per check.

2026-05-08

Decision Reduced max output tokens from 1,500 to 900. Output tokens cost 5× more than input on Anthropic's pricing. The four-section analysis rarely needs more than 600 tokens in practice — 900 gives headroom without waste.

2026-05-08

Decision Dropped URL fetch cap from 8,000 to 3,500 characters (~875 words) per page. KH wiki pages after Jina parsing still carry nav remnants and category lists. 875 words of actual article content is enough for the relevant section of any given page, and halves that input cost.

2026-05-08

Decision Switched default model to Claude Haiku 4.5 — same Anthropic family, ~$0.001 per typical check (input: $0.80/MTok, output: $4/MTok vs Sonnet's $3/$15). Quality is more than sufficient for lore analysis; Sonnet remains available via toggle for dense multi-game theories.

2026-05-08 — cost optimisation pass

Issue Goal is cents per check, not euros. v0.2 defaulted to Claude Sonnet (~$0.03/check). Fine occasionally, expensive if you're running it across 10 theories before a video. Needed a cheaper default.

2026-05-08

Done v0.2 live. Both lore context and anchors panels now accept URL inputs alongside free-text. Token estimate and approximate cost shown in the UI before the API call completes, so there are no surprise charges.

2026-05-08

Decision Each fetched URL is capped at 8,000 characters on the client before it's sent to Claude. Keeps individual page contributions predictable. Users can add multiple URLs — the right workflow is: link the specific wiki pages relevant to your theory, not the entire series.

2026-05-08

Pivot Solved CORS without a backend: Jina Reader (r.jina.ai) is a free service that converts any URL to clean plain text and allows browser fetches. Paste a wiki link, it returns readable content. Zero setup, no extra API key.

2026-05-08

Tried Considered direct fetch() to KHWiki / Fandom URLs from the browser. Blocked — external sites don't send CORS headers that allow browser requests. Would need a proxy or backend.

2026-05-08

Note Real constraint isn't the text box (no cap) — it's Claude's context window (~200,000 tokens ≈ ~150,000 words) and cost. Sending entire wikis is technically possible but expensive and unfocused. A theory about BbS doesn't need all of KH3's lore.

2026-05-08 — constraint: KH lore volume

Issue Kingdom Hearts spans 25+ years and 15+ titles. Pasting raw lore into a text box isn't realistic — the series wiki alone would blow past any reasonable input. Raised: is there a cap? What's the actual limit?

2026-05-08

Done v0.1 live. Four output sections: Contradictions Found, Potential Weaknesses, Clarifying Questions, Overall Assessment. System prompt instructs Claude to write as a collaborator, not a judge — keeps the output useful for scripting rather than just academic.

2026-05-08

Decision Three inputs: Lore Context (free-form canon dump), Hard Anchors (specific facts the theory must not break), and the Theory itself. Splitting context from anchors lets the AI treat them differently — context informs, anchors constrain.

2026-05-08

Decision API key lives in the session only (not localStorage) — user pastes it each visit. Trade-off: slight friction, but no credentials ever persisted to disk or sent anywhere. For a personal creative tool this is the right call.

2026-05-08

Tried Considered a mock/demo mode with hardcoded example outputs so visitors can see it work without an API key. Rejected — felt dishonest for a portfolio piece. A tool that actually calls the API is more compelling than a simulation.

2026-05-08 — initial build

Decision Approached this as a single-file, no-backend tool that can live on GitHub Pages. No server = no hosting cost, no auth complexity, and the portfolio stays purely static.


KH Description Optimizer Live

June 2026 – Present

A description optimizer built for my YouTube channel LoreBySpec. Feed it an old video's description and it produces three things in one pass: a rewritten description, 3–5 lore-aware hashtags, and a backend tags string trimmed to YouTube's 500-character budget. The rewrite front-loads the keyword in the first ~125 characters — the section YouTube's algorithm weights most — and builds in real KH context without inventing anything the source description doesn't support. Ships as a live Cowork artifact, so the AI rewrite runs on open.

Wins
  • Three outputs, one pass — description rewrite, hashtags, and backend tags from a single input with no copy-paste between steps.
  • Built-in KH lore dictionary — characters, worlds, terms, and games drive hashtag and tag generation automatically; no manual labelling needed.
  • Respects YouTube's limits automatically — backend tags trimmed to the 500-character budget and confirmed at 492/500.
  • Keyword above the fold — rewrite logic places the primary keyword in the first ~125 characters, where YouTube's ranking algorithm gives it most weight.
  • Live feedback in the UI — char count, hashtag count, and above-the-fold check update as you edit; per-output copy buttons and a "Try a sample" button for first-time use.
Constraints
  • AI rewrite requires Cowork — the rewrite only runs inside Cowork; hashtags and backend tags work anywhere.
  • Placeholders need a human pass — output uses [bracketed] placeholders for real timestamps and links that must be swapped in by hand before posting.
  • Won't invent lore — it elaborates sparse descriptions only as far as the source material supports; it won't fill gaps with guesswork.

Cowork Artifact On-device Claude inference KH Lore Dictionary YouTube SEO Hashtag generation

// devlog running build notes — decisions, pivots, wins, and constraints
2026-06

Done Shipped as a live Cowork artifact. Added per-output copy buttons, live meter (char count, hashtag count, above-the-fold check), and a "Try a sample" button. Open next step, not built: batch mode — feed a spreadsheet of old videos and optimize them all in one pass.

2026-06

Done Verification pass in a sandbox: entity detection fired correctly, 3–5 hashtags returned, backend tags landed at 492/500. Zero errors.

2026-06

Decision Backend tag generation: auto-generated from the same lore dictionary pass, then trimmed to YouTube's 500-char budget. No user input needed for this step.

2026-06

Decision Hashtag logic: scan the input text against the lore dictionary, return 3–5. Always anchored by #KingdomHearts and #KingdomHeartsLore; user can add or remove chips before copying.

2026-06

Decision Rewrite logic: keyword hook in first ~125 chars, 200–300 words of real context, chapter timestamps section, links section, CTA. [bracketed] placeholders for anything that needs a real URL or timestamp swapped in manually.

2026-06

Decision Built the KH lore dictionary first — characters, worlds, terms, games — as the backbone for both hashtag detection and tag generation. Getting this right before writing rewrite logic meant the entity detection was grounded in actual KH vocabulary, not a generic gaming taxonomy.

2026-06 — initial scoping

Decision Pinned down scope before building: how the tool needs to fit the actual posting workflow, what YouTube's current description structure rewards, and where the lore constraint kicks in. Researched 2026 YouTube SEO best practice — description length, above-the-fold keyword placement, hashtag count, and the tags-vs-hashtags distinction.


YouTube A/B Testing Tool Live

June 2026 – Present

A title and thumbnail generator for LoreBySpec videos. Paste a script, pick which angles to test, and it produces three variants — each built around a different lever — plus thumbnail concept briefs with ready-to-paste image prompts. Tuned for YouTube's watch-time share logic and the KH lore audience. It went through three formats: prompt template, standalone HTML prompt-builder, and finally a Cowork artifact. The Cowork version assembles the full prompt in-page and copies it in one click — zero-paste live generation is parked pending a runtime update (see dev log).

Wins
  • Three differentiated angles by default — Curiosity, Bold Claim, and List/Authority, so an A/B/C test produces actual signal rather than comparing near-duplicates.
  • Over-promise flag — bakes in the key best-practice nuance: YouTube's native Test & Compare picks winners by watch-time share, not raw clicks. Any variant likely to over-promise and tank retention gets flagged before you test it.
  • KH-tuned rules — lore-search patterns ("Explained," "Theory," "Timeline"), character keywords (Xehanort, Roxas, Keyblade War), and the franchise's visual identity built in.
  • Readable output — each title carries a char count and a "why it works" note; each thumbnail brief carries a copyable image prompt, a test plan, and a per-video tip.
  • Niche-agnostic — the niche field drives the persona and adapts visual-identity guidance per franchise. Verified on a Dragon Age script; KH is the default, not a hard-coded assumption.
  • Fails soft — prompt-assembly is decoupled from who runs it. When the live generation path isn't available, the same logic builds the prompt and copies it in one click. No dead ends.
Constraints
  • Offline HTML couldn't generate titles — a plain HTML file can't call an AI without an API key, so the v2 standalone builder could construct the prompt but not run it. This is what drove the move to Cowork.
  • Live generation parked — the Cowork artifact runtime documents a window.cowork.askClaude bridge, but the sandboxed cowork-file://preview/ render doesn't inject it. The supported path is build + one-click copy; the getCowork() poller will pick the bridge up automatically if it's ever mounted in this view.

Cowork Artifact Build + copy fallback A/B testing YouTube title optimisation Thumbnail concept briefs

// devlog running build notes — decisions, pivots, wins, and constraints
2026-06-26

Note Confirmed niche-agnostic: ran a Dragon Age: Inquisition vs Veilguard essay script through the fallback path — produced 3 full variants (titles, thumbnail briefs, image prompts) plus a test plan and per-video tip. All best-practice rules present in the output. KH is the default, not a hard-coded assumption.

2026-06-26 — root cause

Note Root cause confirmed. Diagnostics log: location.href: cowork-file://preview/kh_live_tool.html, iframed: false, typeof window.cowork: undefined, no globals matching cowork/claude/anthropic/mcp (only match was our own getCowork function). The sandboxed preview render doesn't inject the bridge — not a timing race, not a cross-origin frame, not an alternate global name. Zero-paste live generation parked; fallback is the supported path. Poller will pick the bridge up automatically if it's ever mounted.

2026-06-26

Done Added a Run diagnostics panel: logs typeof window.cowork and its keys, whether the view is iframed, cross-origin reachability of parent/top, any window globals matching cowork|claude|anthropic|bridge|mcp, location.href, referrer, and userAgent. Output is copyable. Ran it — captured the data that closed the investigation.

2026-06-26

Decision Shipped graceful fallback before root cause was known: when the bridge is unavailable, Generate builds the finished prompt in-page and renders it with a one-click Copy button. Flow degrades from 0 pastes to 1 paste. Tool is usable end-to-end regardless of what the runtime investigation finds.

2026-06-26

Issue H2 attempt — hypothesis: cross-origin iframe hides the bridge. If the artifact renders in a cross-origin iframe, window.parent.cowork throws a SecurityError before returning. Fix: wrapped parent/top access in try/catch inside the poller. ❌ Still not found — no SecurityError thrown, meaning window.cowork is genuinely undefined in the frame, not obscured by an error.

2026-06-26

Issue H1 attempt — hypothesis: bridge injected late (timing race). Wrapped the askClaude call in a getCowork() poller: checks window.cowork, window.parent.cowork, and window.top.cowork every 100ms for up to ~6s. ❌ Poller exhausted and returned null — bridge isn't late, it's absent for the full polling window.

2026-06-26

Issue v3 first run threw: "Cannot read properties of undefined (reading 'askClaude')". window.cowork is undefined at call time — the LLM call never fires.

2026-06-26 — v3

Done Rebuilt as a live Cowork artifact, light-mode themed to match. Same options as v2 (Curiosity/Bold Claim/List-Authority default; Emotional and Contrarian optional), but Generate was intended to produce titles and briefs in-page via window.cowork.askClaude. Persists, so it reopens with state.

2026-06-26

Pivot User's reaction to v2: if you have to paste the built prompt back into Claude anyway, the copy-paste step is wasted. Agreed and cut it. The standalone HTML builder was a good intermediary but the friction it introduced negated the convenience. Moved to a live Cowork artifact.

2026-06-26 — v2

Done Built a single-file offline HTML prompt-builder: paste script, choose variant count and angles, Build prompt, Copy to clipboard. Caught and fixed two stray CSS typos during testing. Limitation stated up front: builds the prompt but can't generate titles itself without an API key.

2026-06-26 — v2 intent

Decision Needed an actual interactive tool — v1 was a paste-in template, not something you could use repeatedly without opening a fresh chat. Built the offline HTML version as a self-contained prompt-builder.

2026-06-26 — v1 + initial scoping

Done Clarified scope: KH lore channel, reusable template, thumbnails handled by the user's own image tool (outputs briefs and image prompts, not images). Researched YouTube title/thumbnail best practice — key finding: Test & Compare picks winners by watch-time share, not CTR, so a high-CTR/low-retention title can lose. Built the over-promise flag around this. v1 shipped as a paste-in prompt template: drop script into a fresh chat, get 3 A/B/C variants each with title, thumbnail brief, and image prompt.


KH Title & Thumbnail Audit Complete

June 2026

A full-channel audit and prioritisation tool for LoreBySpec thumbnails. It visually reviewed all 69 videos against a 22-point rubric, scored them, and turned the results into an impact-ranked redo list. "Impact-ranked" means weakness × views — so the list favours fixing things that will actually move the numbers, not just things that look bad. It also produced finished reference thumbnails for the priority videos.

Wins
  • Full channel coverage — all 69 videos audited, no sampling. Average score 10.9/22; split 30 High (needs work), 34 Medium, 5 Low/keep.
  • Impact-ranked redo list — "redo rank" = weakness × views, so the list surfaces non-obvious priorities. "We Already Know What's In The Box" and "She Already Knew" led; "The Maleficent Problem" (926 views, score 9) landed in the top tier.
  • Dead-simple to-do flag — "Worth redoing? (top 20)" column removes the eyeballing: ★ Yes (20 highest-impact), Keep ✓ (already strong), — (not worth it now). Auto-recalculates on re-score or view-count update.
  • Named two systemic problems and one strength — ~14 low-view videos reuse a generic purple podcast template (no KH face, no hook); many mid-tier videos use raw screenshots with in-game subtitle captions instead of designed text. Strength: a good number already carry designed text — the gap is consistency across fonts, wordmark, and palette.
Constraints
  • Views-weighted by design — the scoring deliberately avoids flagging low-view videos for a redo; lifting a 40-view video isn't worth the time.
  • New uploads are a different case — for brand-new videos, ignore the views math and start from the strong template. You can't weight by views when there are none yet.

22-point scoring rubric Impact-weighted ranking Visual audit (69 videos) Reference thumbnail generation

// devlog running build notes — decisions, pivots, wins, and constraints
2026-06

Done Produced finished reference thumbnails for the next batch. Offered a monthly scheduled re-pull so the sheet can refresh its view counts automatically.

2026-06

Issue Fixed Logic check: a few already-strong "Low/keep" videos (e.g. "The Box Held the Answer") slipped into the top 20 on views alone. Flagging those as ★ Yes to redo contradicted their own band score. Changed the flag to mark them "Keep" instead. Flag now self-updates on re-score or re-pull.

2026-06

Done Added "Worth redoing? (top 20)" column on user feedback — removes the eyeballing step of working out which high-ranked videos are actually worth the time.

2026-06

Decision Added "redo rank" = weakness × views after building the initial scoring. Pure rubric rank was surfacing low-view videos that happened to score badly. The views multiplier corrects for this: a video with 5 views and a weak thumbnail is still lower priority than one with 900 views and a weak thumbnail.

2026-06

Done Built 22-point scoring rubric and banding: High ≤10, Medium 11–15, Low/keep 16–22. Scored all 69 videos and verified the ranking.

2026-06

Issue Fixed Caught a mis-read from an earlier pass: "The Mechanics That Make Kairi Impossible" was logged as a dark atmospheric shot. Wrong — it has a Kairi face and "THE KAIRI PROBLEM" title text. Corrected before final scoring.

2026-06 — full visual audit

Decision Reviewed all 69 thumbnails directly in the browser, batched — no sampling. Wanted eyes on every one before scoring. Logged three patterns during the pass: the recurring purple "Thinking Through the Darkness" podcast template on low-view videos; raw-screenshot-plus-subtitle thumbnails on mid-tier theory videos; and the already-designed-text thumbnails that just need consistent fonts, wordmark, and a fixed palette.


Sponsorship Strategy for a TikTok Personality

Jan 2025

End-to-end sponsorship project for a TikTok creator in the mental health and rehabilitation space. Covered sponsor research, outreach strategy, personalised outreach messages, and a full sponsorship document with audience metrics and collaboration packages.

Content Strategy TikTok Analytics Email Marketing CRM

Business Relocation & Expansion Strategy

Dec 2024

Supported a creative business expanding from the US to Europe — navigating HR compliance, EOR partner selection, local labour law, and scouting creative workspaces in a major European city. The business expanded without legal or operational setbacks.

Global Expansion HR Compliance Market Research

Leadership Coaching for a Growing Team

Nov–Dec 2024

Provided coaching to a scaling start-up's leadership team on conflict resolution, communication, and team alignment. Implemented team-building activities and cross-functional goal alignment. Outcome: stronger cohesion, improved retention, better collaboration.

Executive Coaching Conflict Resolution Strategic Communications

Optimising Customer Support Operations

Jun–Sep 2024

Partnered with a startup struggling with ticket volume. Implemented a priority-based ticket allocation system and knowledge base, paired with staff training. Reduced response times by 30% and increased customer satisfaction as the business scaled.

Ticketing Systems Workflow Management Staff Development

Niche Market Strategy for Web Development

Jan–Feb 2023

Identified an underserved niche — custom webhooks and integrations for Twitch streamers — and helped a developer position, price, and market into it. The client established a sustainable revenue stream and became a recognised name in the space.

Market Research Customer Acquisition Small Business Development

Date Proposal Website In Progress

June 2026 – Present

A recreation and technical deconstruction of a viral moment: a developer who built a website to ask someone out on a date — complete with a "No" button that runs from the cursor, progressive preference questions, live restaurant discovery, table booking, and a calendar invite delivered to her phone. The project started as a research question — how much of this was technically real, and how much was a well-designed illusion? — and became a working tool.

The interesting part isn't the UI mechanics. The interesting part is the booking. There is no public API that lets a third party programmatically complete a restaurant reservation — not OpenTable, not Resy, not Tock. What looked like magic was almost certainly a pre-constructed OpenTable deep-link with the restaurant, date, and party size already filled in. She booked it herself. She just didn't know she was the one doing it.

Wins
  • The running No button — Mouse proximity detection via mousemove events, getBoundingClientRect() for position, trigonometry for the escape direction. Switches from inline to fixed positioning on first escape to avoid a visible jump. Fades out after five near-misses. Mobile fallback degrades the button text through three taps before disabling it.
  • Four activity paths, one flow — Food (restaurant discovery), Coffee (café search, no booking), Activity (Eventbrite events), Cinema (TMDB films → showtime search deep-link). Each path is defined as an ordered array of step IDs in a FLOWS object; navigation is string-based, progress dots compute dynamically from the current path. No numeric counters, no off-by-one bugs.
  • Multi-API demo fallbacks — All three live APIs (Foursquare, Eventbrite, TMDB) have contextually appropriate demo data that activates when no key is configured. Demo cafés, demo events keyed by category, demo films keyed by genre. No broken states for portfolio visitors.
  • Calendar invite in 30 lines — Generates and downloads an .ics file (RFC 5545 iCalendar format). Opens natively in Google Calendar, Apple Calendar, and Outlook on any device. Cinema path generates an all-day event (showtime unknown); food and coffee generate timed events with a two-hour end time.
  • The booking gap is documented, not hidden — The README explains exactly why true programmatic booking doesn't exist for outside developers, and what the "Book the table" button actually does (an OpenTable search deep-link pre-filled with restaurant, date, time, and party size).
Constraints
  • Programmatic table booking doesn't exist via public API — OpenTable has an affiliate program (3–4 week approval, still a deep-link handoff, not a booking). Resy is enterprise-only. The third-party Apify wrapper ($3.99/booking) is ToS-violating and fragile. The booking step is an honest handoff, not real automation.
  • No cinema showtimes API — TMDB covers film metadata and posters. For local showtimes, no universal public API exists. Vue, Odeon, and Cineworld don't have developer APIs. The "Find Showtimes" button deep-links to a Google search for the film + location + "cinema showtimes." Effective handoff, not a direct booking.
  • Three API keys required for live data — Foursquare (restaurants and cafés), Eventbrite (events), TMDB (films). All free tiers. The config panel handles all three. Demo mode runs without any.
  • No email sending — The calendar invite downloads as an .ics file rather than being emailed. EmailJS (free: 200 emails/month) would close this gap.
  • Desktop-first mechanic — The No button escape uses mousemove events, which don't fire on touch devices. Mobile gets a tap-degradation fallback instead.
// roadmap
  1. EmailJS integration — Collect an email address early in the flow and auto-send the .ics as an attachment on confirmation. Free tier covers 200 emails/month. Closes the gap between downloading and receiving.
  2. Sender personalisation mode — A ?setup URL parameter opens extended config: custom opening message, pre-selected restaurants to steer the choice, a locked-in date. The "sinister version" — fully documented.
  3. OpenTable affiliate integration — Currently a search deep-link. With affiliate approval, a cleaner availability-surfacing handoff with direct reservation links per restaurant.

HTML/CSS/JS Foursquare Places API Eventbrite API TMDB API iCalendar (.ics) Client-side only No backend

Open Tool →
// devlog running build notes — decisions, pivots, wins, and constraints
2026-06-08 — research

Decision Mapped the local listings problem for cinema and theatre. No cinema chain (Vue, Odeon, Cineworld, AMC, UCI) has a public API — the data is locked in proprietary booking systems. TMDB gives film metadata only, not showtimes or venues. The best universal handoff is a structured Google search deep-link: Google aggregates real-time showtime data from chains worldwide and surfaces it as a rich widget. For theatre and professional events, Ticketmaster Discovery API (free, 5,000 calls/day, covers Europe) is a better fit than Eventbrite — returns actual shows with venue, date, time, and direct booking links. Eventbrite stays for community events and workshops.

2026-06-08 — planning

Decision New transport step before confirm: I'll pick you up (collects their address, generates two separate .ics files — the event and a "Collect [Name] from [address]" reminder timed 40 minutes before), I'll book a taxi (notes it in the ICS description, optionally deep-links to Uber pre-filled with the venue address), or You'll make your own way (single ICS, no extra inputs). Two files rather than one combined event — cleaner for the recipient and simpler to implement.

2026-06-08 — building

Done Built the full activity type selector: four paths, one flow. Food — existing restaurant path, unchanged. Coffee — Foursquare café search, no booking step, morning/afternoon time slots. Activity — Eventbrite API with Arts, Sports, Music, and Classes categories; skips date/time since events have their own date. Cinema — TMDB now-playing films with genre filtering; skips date/time; "Find Showtimes" deep-links to a Google search. Step architecture changed from numeric IDs to semantic string IDs with a FLOWS routing object — each path is an array of step IDs, progress dots compute from the current path. Config panel expanded from two fields to five (name, message, Foursquare key, Eventbrite key, TMDB key). All three APIs have contextually appropriate demo fallbacks.

2026-06-07 — fix

Fixed Demo mode was returning the same four restaurants regardless of cuisine or location — a hardcoded list that didn't change between sessions. Fixed by making demo data cuisine-specific: each of the six cuisine types now maps to its own set of four plausible restaurant names, so the cards at least match what was selected. An info banner now tells the user they're seeing sample results and where to add an API key for real ones. Also: changed location field label from "And where are you?" to "Where would you like to go?" — more accurate since it's asking about the destination, not the person's current position.

2026-06-07 — fix

Fixed Two UX fixes. Name collection is now a fill-in-the-blank embedded in the headline — the input is styled at headline scale with just a bottom border, so "I'm so glad, ______" reads as a sentence rather than a form field. If the sender pre-fills the recipient's name via the config panel, that step is replaced with a fully personalised "I'm so glad, [Name]." headline and the question never appears at all. Restaurant step: "That's the one →" is now a fixed button anchored to the bottom of the viewport, sliding up from below the moment a card is selected — visible on any screen size without scrolling.

2026-06-07

Decision The sticky button fix exposed a secondary issue: body { overflow: hidden } was blocking scroll on the restaurant step, clipping the bottom card behind the viewport edge on portrait mobile. Re-enabling scroll on step 5 only, plus an 88px spacer below the grid, means all four cards are reachable — the sticky button is just the faster path, not the only one.

2026-06-07 — building

Done v1 complete. Full seven-step wizard: the question, name and location, cuisine, vibe, restaurant selection (Foursquare API or demo), date and time, confirmation. Calendar .ics generation working. OpenTable deep-link booking handoff working. Config panel for sender API key setup — saves to localStorage, invisible to the recipient.

2026-06-07

Decision White background and Nunito font. Every other tool in this portfolio is dark — intentionally. This site should look like something built for a person, not a developer. The constraint in the brief was aesthetic before it was technical: bubbly, clean, fluid. Nunito's rounded letterforms suit the tone. One accent colour (rose #E8739A) used across the whole experience, nothing else.

2026-06-07

Decision The No button starts as an inline element and switches to position: fixed on the first escape, recording its getBoundingClientRect() coordinates first so the switch is invisible. After that it moves via left/top in fixed space. On mobile, mousemove events don't fire — instead three clicks degrade the button text ("Are you sure?" → "Really...?" → vanish). Same behavioural outcome, different mechanism.

2026-06-07

Decision Foursquare over Yelp. Yelp eliminated their free tier mid-2025 with minimal notice — now ~$8–$15 per 1,000 calls. Foursquare's basic tier gives 10,000 free calls/month with cuisine search, price tiers, photos, and addresses. More than adequate. Foursquare v3 also allows CORS from the browser directly — no proxy needed, keeping the no-backend principle intact.

2026-06-07 — research

Decision The booking gap. No public API exists for programmatically completing a restaurant reservation — OpenTable, Resy, and Tock are all gated behind partnership agreements or don't exist for outside developers at all. The original viral site was almost certainly constructing a pre-filled OpenTable deep-link. She completed the booking herself. The site made her feel like it happened automatically. The "Book the table" button in this project does the same thing and the README says so clearly.

Working with Spec

How I Work

Most situations that look like people problems are actually clarity problems, skills problems, or systems problems. My starting point is that people are doing the best they can with what they have. That's not a naïve assumption — it's the most practically useful one. People have complex lives and real pressures that exist long before they arrive at work. When something goes wrong, asking "what did this person need that they didn't have?" gets you to a fix. Asking "what kind of person does this?" just gets you stuck.

Things are rarely a character or moral issue. They are nearly always a skills issue, a clarity issue, or a systems issue. Find the gap. Close it.
Foundation Assume good intent

Start from the most charitable explanation and work backwards. It leads to better diagnoses, better conversations, and outcomes that actually stick.

Diagnosis Skills over character

Missed expectations are almost never moral failures. They're usually a gap in clarity, training, or tooling. That's a solvable problem — and it's the problem worth solving.

Style Pragmatic and intentional

You can be clear-eyed about how things actually are while still being deliberate about where you want them to go. Realism and ambition aren't in conflict.

Environment Async and remote-first

I write clearly, document decisions rather than relying on memory, and build visibility without requiring people to be always-on. The tools change; the principle doesn't.

Onboarding is the work

Onboarding is where I focus a lot of my energy — not as a 90-day checklist, but as the primary mechanism by which a company teaches people how to be there. How we communicate. How feedback works. What the values look like at 4pm on a Thursday when something's gone wrong. None of that lives in an orientation deck. It lives in the habits, the structures, and the conversations that surround the act of joining.

When those processes are clear, well-led, and maintained past day one — when onboarding is treated as an ongoing discipline rather than a one-time event — it sets everyone up for long-term success. That clarity has to be built before someone walks in the door, and maintained long after they settle in.

The work happens at every level of a company — and each layer needs something different.

Layer 1 Line Managers

What does working here actually look like day-to-day? Which tools, which rhythms, what's expected of their reports — and of them. Managers who have real clarity give it to their teams. Managers who are guessing pass that uncertainty downstream.

Layer 2 HR & Culture Leaders

Culture isn't what's written on the wall — it's what gets reinforced when no one's watching. Working with HR and culture teams to close the gap between the culture that's stated and the one that's actually practised.

Layer 3 Executive Suite

Values start at the top and distribute downstream. If the exec layer is living them, they become real throughout the organisation. If they're not, no programme will make them stick. That alignment needs to be in place before it becomes a credibility problem.

Communication

I'm direct — I say what I see and I try to be specific about it. Vague feedback helps no one; it leaves people uncertain about what to change and why. Directness, done from the right place, is a form of respect. It means you're taking the person and the situation seriously enough to be honest about both.

That directness is always from a good heart. The goal is never to make someone feel small — it's to give them something they can actually act on.

Working with Spec

I've spent a lot of time in organisations watching the gap between how people think work gets done and how it actually does. The notes below are less about personal preference and more about what I've observed consistently produces better outcomes — for the people I work with as much as for me.

Give me the shape of what good looks like.

My brain runs quite literally, so a concrete model of the goal is worth more to me than a detailed brief about the process. I don't need every step mapped out — I'm comfortable finding my own way through ambiguity — but I work best when the destination is clear and specific. The more precisely we can define what success actually looks like upfront, the less time we spend realigning midway through. It also tends to surface misaligned assumptions early, which is almost always worth doing before anyone has invested significant time.

An environment where people can say the uncomfortable thing is more functional, not just nicer.

The places where I've done the best work have been ones where flagging a problem early was treated as useful rather than inconvenient. That's not just a personal preference — organisations that suppress early signals don't stop problems from existing, they just find out about them later and at greater cost. I create that environment for the people I work with, and I do my best work when it exists around me.

I'm a generalist because that's where the most useful work tends to live.

Most of the problems I find genuinely interesting — and where I tend to add the most — sit at the intersections. The people issue that turns out to be a clarity issue. The process breakdown that's actually a trust issue two layers up. That kind of diagnosis requires being able to move across disciplines rather than staying within one. If you want someone with deep narrow expertise in a single domain, I'm probably not the right fit. If you want someone who can hold the shape of a whole problem and work across it, that's where I'm most useful.

If you've hired me for creative solutions, those need space to actually form.

Novel thinking — the kind that produces something genuinely useful rather than a competent rearrangement of what already exists — doesn't come from being always-on. It comes from having time to step away, let things settle, and come back with fresh eyes. That's not a preference for working less; it's how the particular kind of thinking you've hired me for actually works. When I've produced my best work, it's almost never been the thing I ground out in a single sitting. It's been the thing I slept on.

High priority and emergency are different things, and treating them the same degrades both.

Unless someone is in physical danger, what we have is a high-stakes situation that needs focus and clear thinking. Treating it as a crisis tends to shorten the diagnosis, compress the thinking, and produce the next problem before the current one is resolved. I take important things seriously — I just think the most important things deserve calm, considered responses rather than reactive ones.

The best results come from realistic expectations on both sides.

I'll invest genuinely — I'll think laterally, care about outcomes, and bring more than what's strictly in the brief. What I've found consistently is that the best working relationships are with people who understand that contributor-level commitment and owner-level commitment are different things, and that the difference is structural rather than motivational. When the terms of engagement feel honest, people give discretionary effort freely. When they don't, you eventually get attrition — usually quietly, and usually at the worst possible moment.

Let's not weaponise radical candour.

"Radical candour" has become a permission slip in a lot of workplaces — a way of framing bluntness as virtue and skipping the part where you assess whether the person in front of you is actually in a position to receive what you're about to say. Directness is genuinely valuable. But directness without reading the room isn't honesty — it's just impact without accountability. I give feedback that's specific and actionable, and I deliver it person-first and problem-second. I work best with people who operate the same way: who can spot early that something's off, address it before it compounds, and do that without making it bigger than it needs to be. If that kind of attunement isn't there, we're probably going to find each other frustrating.

Get in Touch

Email: work.specious@gmail.com