<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>John Wilger</title>
      <link>https://johnwilger.com</link>
      <description>Senior Engineering Leader &amp; Principal Engineering Consultant</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://johnwilger.com/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Mon, 29 Dec 2025 00:00:00 +0000</lastBuildDate>
      <item>
          <title>The Tools You Build Are More Important Than The Tools You Use</title>
          <pubDate>Mon, 29 Dec 2025 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/the-tools-you-build-are-more-important-than-the-tools-you-use/</link>
          <guid>https://johnwilger.com/blog/the-tools-you-build-are-more-important-than-the-tools-you-use/</guid>
          <description xml:base="https://johnwilger.com/blog/the-tools-you-build-are-more-important-than-the-tools-you-use/">&lt;p&gt;There&#x27;s a pattern I&#x27;ve noticed among developers working with AI coding assistants. Some are having transformative experiences—shipping features faster, tackling problems they&#x27;d previously avoided, genuinely enjoying their work more. Others are frustrated, producing buggy code, and increasingly skeptical that these tools offer anything beyond fancy autocomplete.&lt;&#x2F;p&gt;
&lt;p&gt;The difference isn&#x27;t the model they&#x27;re using. It&#x27;s not their prompting technique. It&#x27;s not even their underlying programming skill, though that certainly matters.&lt;&#x2F;p&gt;
&lt;p&gt;The developers succeeding with LLM-augmented development have stopped waiting for the perfect out-of-the-box experience and started building their own tools to solve their own problems.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-myth-of-the-perfect-configuration&quot;&gt;The Myth of the Perfect Configuration&lt;&#x2F;h1&gt;
&lt;p&gt;When Claude Code, Cursor, Windsurf, and similar tools started gaining traction, a cottage industry of &quot;optimal configurations&quot; emerged. GitHub repositories full of system prompts. Blog posts about the &quot;ultimate&quot; rules file. YouTube videos promising 10x productivity if you just copy these exact settings.&lt;&#x2F;p&gt;
&lt;p&gt;I tried many of them. Some helped marginally. Most did nothing. A few actively made things worse because they were optimized for someone else&#x27;s workflow, someone else&#x27;s codebase, someone else&#x27;s pain points.&lt;&#x2F;p&gt;
&lt;p&gt;This shouldn&#x27;t have surprised me. I&#x27;ve spent two decades watching the same pattern play out with every development tool and methodology. Cargo-culting someone else&#x27;s Agile process doesn&#x27;t make you agile. Copying another team&#x27;s CI&#x2F;CD pipeline doesn&#x27;t give you their deployment confidence. Using the same text editor as a famous programmer doesn&#x27;t make you write better code.&lt;&#x2F;p&gt;
&lt;p&gt;The tool is never the thing. The thing is understanding your own problems deeply enough to know what tool you need.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;a-problem-worth-solving&quot;&gt;A Problem Worth Solving&lt;&#x2F;h1&gt;
&lt;p&gt;I&#x27;d been using Claude Code heavily for several months, and one friction point kept recurring: GitHub issue management.&lt;&#x2F;p&gt;
&lt;p&gt;Not the basic stuff—creating issues, closing them, adding labels. The &lt;code&gt;gh&lt;&#x2F;code&gt; CLI handles that fine, and Claude can drive it without much trouble. The friction was in the relationships between issues.&lt;&#x2F;p&gt;
&lt;p&gt;I work with hierarchical issue structures. Epics contain stories. Stories contain tasks. Tasks might have sub-tasks. When you&#x27;re trying to keep Claude oriented on what you&#x27;re building and why, being able to say &quot;this task is part of story #42, which is part of epic #15, which is about the payment system redesign&quot; provides crucial context.&lt;&#x2F;p&gt;
&lt;p&gt;GitHub added sub-issues and blocking relationships over the past year, but they&#x27;re only accessible through the web UI or the GraphQL API. Every time I needed Claude to help me restructure issue hierarchies or track dependencies, we&#x27;d end up in a frustrating dance: Claude would attempt some &lt;code&gt;gh api graphql&lt;&#x2F;code&gt; call, get the syntax wrong, try again, get the escaping wrong, try again, finally succeed, and by then I&#x27;d lost my train of thought on the actual problem I was trying to solve.&lt;&#x2F;p&gt;
&lt;p&gt;Worse, if I wanted to grant Claude permission to manage issues autonomously, I&#x27;d have to approve &lt;code&gt;Bash(gh api:*)&lt;&#x2F;code&gt; in my settings—which is far too broad. That pattern would let Claude make arbitrary API calls to GitHub, not just issue management operations.&lt;&#x2F;p&gt;
&lt;p&gt;I could have lived with this friction. Most people do. They work around limitations, accept the rough edges, wait for someone else to build a better solution.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, I decided to build the tool I needed.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-development-process-with-an-ai-partner&quot;&gt;The Development Process (With an AI Partner)&lt;&#x2F;h1&gt;
&lt;p&gt;Here&#x27;s where things get interesting. I didn&#x27;t just sit down and write a GitHub CLI extension from scratch. I used Claude Code to help me build the tool that would make Claude Code more effective.&lt;&#x2F;p&gt;
&lt;p&gt;The process started with research. I asked Claude to investigate the GitHub GraphQL API, find the relevant mutations for sub-issues and blocking relationships, and figure out what operations were possible. This took some trial and error—we created test issues, experimented with API calls, discovered that sub-issues require a special &lt;code&gt;GraphQL-Features: sub_issues&lt;&#x2F;code&gt; header, learned that blocking relationships use &lt;code&gt;issueId&lt;&#x2F;code&gt; and &lt;code&gt;blockingIssueId&lt;&#x2F;code&gt; parameters (not the more intuitive names I initially guessed).&lt;&#x2F;p&gt;
&lt;p&gt;Every failed API call taught us something. Every error message refined our understanding. Claude kept notes, I asked questions, and gradually a clear picture emerged of what was possible and what syntax was required.&lt;&#x2F;p&gt;
&lt;p&gt;Then came the key insight: we could wrap all these GraphQL operations in a &lt;code&gt;gh&lt;&#x2F;code&gt; CLI extension. Instead of Claude needing to construct complex API calls every time, it could use simple commands like &lt;code&gt;gh issue-ext sub add 10 42&lt;&#x2F;code&gt;. And critically, I could grant permission for &lt;code&gt;Bash(gh issue-ext:*)&lt;&#x2F;code&gt; without opening the door to arbitrary API access.&lt;&#x2F;p&gt;
&lt;p&gt;The extension itself took maybe 20 minutes to write—a bash script that handles argument parsing, constructs the appropriate GraphQL queries, and presents results in both human-readable and JSON formats. Claude did most of the implementation work while I reviewed, asked questions, and occasionally corrected course.&lt;&#x2F;p&gt;
&lt;p&gt;But the extension was only half the solution. I also needed Claude to &lt;em&gt;know&lt;&#x2F;em&gt; how to use it effectively. So we built a Claude Code plugin: a skill document that teaches Claude about GitHub issue management, comprehensive reference documentation for every command, a setup command that installs the extension, and a session-start hook that reminds users if they haven&#x27;t installed the extension yet.&lt;&#x2F;p&gt;
&lt;p&gt;The whole thing—research, experimentation, extension development, plugin creation, documentation—took an hour. And now I have a tool that solves my specific problem in exactly the way I need it solved.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;&#x2F;h1&gt;
&lt;p&gt;Let me be clear about something: the plugin I built isn&#x27;t revolutionary. It&#x27;s a wrapper around existing APIs with some documentation. Anyone could have built it.&lt;&#x2F;p&gt;
&lt;p&gt;But almost no one does.&lt;&#x2F;p&gt;
&lt;p&gt;Most developers treat their AI coding tools as fixed artifacts. The tool does what it does; your job is to figure out how to work within its constraints. If something is frustrating, you either live with it or switch to a different tool and hope it&#x27;s better.&lt;&#x2F;p&gt;
&lt;p&gt;This mindset made sense when tools were expensive to modify. Writing an IDE plugin used to be a significant undertaking. Customizing your build system required deep expertise. The cost of building your own tools was high enough that it was usually better to adapt your workflow to existing solutions.&lt;&#x2F;p&gt;
&lt;p&gt;But that equation has changed. When you have an AI assistant that can help you build tools, the cost of custom solutions drops dramatically. That time I spent building the GitHub issue extension? I couldn&#x27;t have done it that quickly five years ago. The research alone would have taken longer than the entire project did with Claude&#x27;s help.&lt;&#x2F;p&gt;
&lt;p&gt;This creates a flywheel effect. You use AI to build tools. Better tools make your AI assistant more effective. A more effective AI assistant helps you build better tools faster. Each iteration compounds.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-meta-skill&quot;&gt;The Meta-Skill&lt;&#x2F;h1&gt;
&lt;p&gt;The developers I see thriving with AI coding assistants have developed a specific meta-skill: they notice friction, investigate root causes, and build solutions—rather than accepting friction as the cost of using new technology.&lt;&#x2F;p&gt;
&lt;p&gt;This requires a particular mindset:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Treat configuration as code.&lt;&#x2F;strong&gt; Your rules files, system prompts, custom commands, and plugins are part of your development environment. They deserve the same attention you&#x27;d give any other code you maintain. Version control them. Iterate on them. Share them when they might help others, but don&#x27;t expect others&#x27; configurations to solve your problems.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Pay attention to repeated friction.&lt;&#x2F;strong&gt; Every time you find yourself working around a limitation, every time Claude makes the same mistake twice, every time you have to manually intervene in something that should be automatic—that&#x27;s a signal. You&#x27;ve found a problem worth solving.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Invest in understanding.&lt;&#x2F;strong&gt; Before building a solution, make sure you understand the problem deeply. My GitHub issue extension works because I spent time learning exactly how the GraphQL API behaves, what parameters it expects, what errors it returns. That understanding is embedded in the tool and the documentation.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Build incrementally.&lt;&#x2F;strong&gt; You don&#x27;t need to solve everything at once. I started with just sub-issue management because that was my most pressing pain point. Blocking relationships and linked branches came later. Each addition was motivated by a real problem I&#x27;d encountered.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Document for your AI partner.&lt;&#x2F;strong&gt; Half of building effective tooling is teaching your AI assistant how to use it. The skill documentation I wrote for Claude isn&#x27;t just for human readers—it&#x27;s optimized for Claude to consume and apply. Clear examples, explicit command syntax, common patterns and workflows.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;the-uncomfortable-truth&quot;&gt;The Uncomfortable Truth&lt;&#x2F;h1&gt;
&lt;p&gt;There&#x27;s an uncomfortable truth in all of this: the people who benefit most from AI coding assistants are the people who needed them least.&lt;&#x2F;p&gt;
&lt;p&gt;Experienced developers who already understand their problem domains deeply can direct AI assistants effectively. They know what questions to ask. They can evaluate generated solutions. They can identify when the AI is confidently wrong. And crucially, they have the skills to build custom tools when off-the-shelf solutions fall short.&lt;&#x2F;p&gt;
&lt;p&gt;Less experienced developers often struggle because they&#x27;re trying to use AI assistants as a shortcut past understanding. They want the tool to just work, to give them correct answers without requiring them to evaluate those answers critically. When friction appears, they lack the context to even recognize that a solution might exist.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t have a neat resolution for this tension. AI coding tools genuinely do lower the barrier to building software. But they lower it most for people who&#x27;ve already climbed over that barrier.&lt;&#x2F;p&gt;
&lt;p&gt;Perhaps the best thing experienced developers can do is model this tool-building behavior openly. When you solve a problem by building a custom extension or plugin, share not just the artifact but the process. Show how you identified the friction, how you investigated solutions, how you iterated toward something that worked.&lt;&#x2F;p&gt;
&lt;p&gt;The tools we build are artifacts of our understanding. Sharing them helps. But sharing how we built them helps more.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;getting-started&quot;&gt;Getting Started&lt;&#x2F;h1&gt;
&lt;p&gt;If you&#x27;ve read this far and want to try building your own Claude Code tooling, here&#x27;s what I&#x27;d suggest:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Start with your actual problems.&lt;&#x2F;strong&gt; Don&#x27;t go looking for things to optimize. Instead, pay attention over the next week. When do you feel friction? When does Claude struggle with something that should be straightforward? Write these down.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Pick the smallest valuable problem.&lt;&#x2F;strong&gt; You don&#x27;t need to build a comprehensive solution. Find something specific and bounded. Maybe it&#x27;s a single command that automates a repetitive task. Maybe it&#x27;s a snippet of documentation that helps Claude understand your project&#x27;s conventions.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Use Claude to build it.&lt;&#x2F;strong&gt; This is genuinely effective. Describe the problem you&#x27;re trying to solve, work through potential solutions, iterate until you have something that works. You&#x27;ll learn about Claude&#x27;s capabilities and limitations in the process.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Test it in real work.&lt;&#x2F;strong&gt; The best tools emerge from actual use. Build something minimal, use it for a few days, notice what&#x27;s missing or awkward, improve it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Share what you learn.&lt;&#x2F;strong&gt; Not just the finished tool—the process, the problems, the failed approaches. Someone else has the same friction you do. They might not have realized yet that building a solution is possible.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;The GitHub issue management plugin I built is available at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;claude-code-plugins&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;claude-code-plugins&lt;&#x2F;a&gt;, and the gh extension is at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;gh-issue-ext&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;gh-issue-ext&lt;&#x2F;a&gt;. You&#x27;re welcome to use them if they solve problems you have.&lt;&#x2F;p&gt;
&lt;p&gt;But more than that, I hope this piece encourages you to notice your own friction and do something about it. The tools you build for yourself will always fit better than the tools you borrow from others.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The Hidden Pitfalls of AI Software Development</title>
          <pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/the-hidden-pitfalls-of-ai-software-development/</link>
          <guid>https://johnwilger.com/blog/the-hidden-pitfalls-of-ai-software-development/</guid>
          <description xml:base="https://johnwilger.com/blog/the-hidden-pitfalls-of-ai-software-development/">&lt;p&gt;&lt;em&gt;Before we start, I want to make it clear that I&#x27;m not going to &quot;blame the controller&quot; for losing this game. I&#x27;ve always believed that a software engineer using AI assistants to write code is still responsible for reviewing and understanding every line of that code, and this situation is no different. Even though the issue I faced was unexpected, it&#x27;s still completely my responsibility. I&#x27;m just relieved that I&#x27;m the only one affected and that it didn&#x27;t happen in a situation involving a client.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been trying out different AI-assisted software development workflows recently so I can guide clients and colleagues on which tools and methods to use or avoid. One setup I find quite promising is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;block.github.io&#x2F;goose&#x2F;&quot;&gt;Goose&lt;&#x2F;a&gt;, an on-machine AI agent that interacts with the system through extensions using an MCP server. When used with a &lt;code&gt;.goosehints&lt;&#x2F;code&gt; file, which gives instructions for a ping-pong pairing, TDD approach to development, I&#x27;ve been quite impressed with its capabilities and how it can be adjusted to fit specific workflow preferences.&lt;&#x2F;p&gt;
&lt;p&gt;As part of my experimentation, I used Goose to help create a small TypeScript CLI utility. This tool takes a CSV file, where each record is a latitude&#x2F;longitude coordinate pair, as input and outputs a new CSV file that includes the associated business name, website, and telephone number from the Google Places API. This task, having never used the Places API before and not being a regular TypeScript author, would likely have taken me 2-3 hours to complete. With AI assistance, it was done in about an hour. It was a complete, working solution that easily processed a test file with about 10,000 rows. It had full test coverage, and the code quality was decent, if not perfect. Success, right?&lt;&#x2F;p&gt;
&lt;p&gt;The problem is that bugs and low-quality code aren&#x27;t the only issues that can cause trouble.&lt;&#x2F;p&gt;
&lt;p&gt;In my first attempt at creating this utility, I only extracted the business names from the Places API for each record. A quick check of the Places API pricing showed that I could make 10,000 requests for free, and additional requests up to 100,000 would cost $5 per 1,000 requests. Running an input file with 20,000 rows would cost about $50. I thought this expense was reasonable for the experiment, but I also wanted to avoid unnecessary spending. So, I decided to use a 10,000-row input file to stay closer to the free tier and maybe only spend $5-10 on the extra requests needed during testing.&lt;&#x2F;p&gt;
&lt;p&gt;Once I got the basic version working by refining the solution with Goose, I then asked Goose if I could also include the website URL and phone number for the business in the output. Goose was happy to help and did a great job updating the existing solution, including the tests, without unnecessarily rewriting major parts of the code.&lt;&#x2F;p&gt;
&lt;p&gt;At this point, anyone familiar with the Google Places API is probably shaking their head and laughing at my mistake.&lt;&#x2F;p&gt;
&lt;p&gt;The pricing I was looking at was for their &quot;Place Details Essentials&quot; level of requests. By simply asking for these two extra fields, URL and phone number, the request automatically moved to their &quot;Place Details Enterprise&quot; tier, which is much more expensive. At this tier, you only get 1,000 requests per month for free, and you pay &lt;strong&gt;$20 per 1,000 requests&lt;&#x2F;strong&gt; for anything beyond that! You can imagine my shock and horror when I received emails from Google later that day saying they had received payments from me totaling just under $2,000.00!&lt;&#x2F;p&gt;
&lt;p&gt;This probably wouldn&#x27;t have happened if I had been writing this code without AI assistance. I would have needed to review the API documentation for the Places API to find out how to get those two additional fields of data. At that point, I likely would have noticed that this would move me into a different pricing tier. However, since using AI assistance meant I didn&#x27;t need to look up this information myself, I didn&#x27;t notice the change. I happily ran my tests and then the entire test file, and I was quite pleased with the output. It was a job well done, and it probably took about an hour less than it would have if I had written the code myself.&lt;&#x2F;p&gt;
&lt;p&gt;This post is not a criticism of Goose specifically; the issue I faced could happen with any LLM-based coding assistant. In fact, I would argue that the better the tool (and the more you trust it), the more likely you are to encounter a similar issue. The problem wasn&#x27;t with the code itself. I reviewed the code before running it and made sure I understood what each line was doing. What I didn&#x27;t do was thoroughly consider other factors that a professional software engineer should evaluate, such as the financial cost of running the solution. Thankfully, this was just a personal project and not a client system, so I&#x27;m the only one dealing with the consequences. Now imagine if a less-experienced developer made a similar mistake in a production system that processed many more records over a month. The consequences of that decision could be much worse.&lt;&#x2F;p&gt;
&lt;p&gt;While AI-assisted coding tools like Goose can significantly enhance productivity and streamline the development process, they also introduce new challenges and responsibilities for developers. It&#x27;s crucial for software engineers to remain vigilant and thoroughly review not just the code, but also the broader implications of their work, such as financial costs and potential impacts on production systems. This experience underscores the importance of maintaining a balance between leveraging AI capabilities and exercising professional diligence to avoid costly oversights. As AI tools continue to evolve, developers must adapt and refine their practices to ensure they harness these technologies effectively and responsibly.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>The Language Model Is Just Another User</title>
          <pubDate>Thu, 21 Mar 2024 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/the-language-model-is-just-another-user/</link>
          <guid>https://johnwilger.com/blog/the-language-model-is-just-another-user/</guid>
          <description xml:base="https://johnwilger.com/blog/the-language-model-is-just-another-user/">&lt;p&gt;The first time I worked on an application that heavily relied on OpenAI&#x27;s chat completion API, my years of experience managing APIs within an extensive service infrastructure shaped my approach. It seemed straightforward: it was just another JSON API where you send a request to a known endpoint and get back data in a specific format. However, as the development progressed, we encountered problems due to the unpredictable nature of the generative AI responses. Features that worked one day would suddenly cause errors the next, leading us into a repetitive cycle of tweaking the application code and the prompts we were using. This situation was unsustainable; I couldn&#x27;t in good conscience tell my client that their application was &quot;complete&quot; when it could break down at any moment. Is building anything more sophisticated than a fancy chatbot using this technology even possible?&lt;&#x2F;p&gt;
&lt;p&gt;![](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1710518866017&#x2F;e8877cff-7fb0-4e49-ae69-1f47a2847c94.webp align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;The system architecture we were using led me to a realization that has completely changed how I now integrate generative AI features into an application. I&#x27;ve always appreciated event-sourcing and CQRS (Command Query Responsibility Separation) architectures. We were developing this application in Elixir using the Commanded library. In this architecture, whenever a user performs an action that changes the system&#x27;s state (like submitting an order form), this action is captured as a &quot;command&quot; that shows the intent to change the state. This command is checked and carried out, leading to either an error message or a state-change event. These events are the truth for the entire application&#x27;s state, and no changes to the system&#x27;s data happen without a matching event. The system records events in response to the language model&#x27;s changes, just like those made by human users. Although it&#x27;s possible to write an event directly to the event stream, the Commanded library makes it much easier to create a simple command that the system can execute, which results in the recording of an event.&lt;&#x2F;p&gt;
&lt;p&gt;It took me longer than I&#x27;d like to admit to see how everything fit together. The human user and the language model change the system&#x27;s state using the same basic process: the command. Once I realized this, I wondered if we were approaching this from the wrong perspective. What are we using generative AI for? In most situations, a language model is creating results that a skilled user could also achieve on their own; we use these models as an aid to help us bridge a gap in either knowledge or efficiency. They aren&#x27;t a &lt;em&gt;part&lt;&#x2F;em&gt; of the application as much as they are an assistant in &lt;em&gt;working with&lt;&#x2F;em&gt; it.&lt;&#x2F;p&gt;
&lt;p&gt;As I mentioned in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;johnwilger.com&#x2F;generative-ai-is-a-ux-revolution&quot;&gt;an earlier piece about how generative AI is changing UX paradigms&lt;&#x2F;a&gt;, we successfully let the language model control many aspects of an application humans would have previously performed. When creating &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apex.artium.ai&#x2F;&quot;&gt;APEX&lt;&#x2F;a&gt;, a generative-AI-integrated application we made at Artium to help produce product plans that are well-grounded in the needs of the business, we could have taken a different approach to the interaction. We could have had the user click on nodes, click an edit button, fill out a form with the information they wanted in each section, and click &quot;save,&quot; and to incorporate generative AI, we could have put a little AI-sparkle button next to the text fields and used the model to fill in just that one piece.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, the primary means of interacting with APEX is via a back-and-forth conversation with &quot;the Artisan.&quot; The Artisan isn&#x27;t just a chatbot that gives you the text to put into a form field; it can also update that text for you in the right spot.&lt;&#x2F;p&gt;
&lt;p&gt;![](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1710490780687&#x2F;caa42080-902b-4113-aa17-a01b8be68c7e.png align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;Imagine you&#x27;re on a call with someone and you’re collaborating on a Google Doc. You read through the text they&#x27;ve just written and suggest changes. Right before your eyes, you see the text change as your writing partner edits the document on another computer. Once upon a time, that felt magical; today, it&#x27;s mundane.&lt;&#x2F;p&gt;
&lt;p&gt;Using APEX to build a product plan is similar to this style of collaborative editing. The difference is that your writing partner is a language model that understands how to use the application. Sometimes, it offers its own opinions, but it also does what you say verbatim when you are explicit. Sometimes, it produces results you will love; sometimes, you need to iterate on its suggestions.&lt;&#x2F;p&gt;
&lt;p&gt;Witnessing this approach come together, I realized that this is precisely where generative AI can shine when integrated into our applications. I also learned how we can make language models a much more reliable part of our systems.&lt;&#x2F;p&gt;
&lt;p&gt;Rather than treating the language model as an internal component of your system, a better approach is to consider it as just another user sitting at their computer and sending inputs to the system. By treating the output from the language model &lt;em&gt;precisely&lt;&#x2F;em&gt; as if it had been typed into a form by a human user, the solutions to non-deterministic inputs suddenly become clear. It&#x27;s just form validation.&lt;&#x2F;p&gt;
&lt;p&gt;![](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1710490806221&#x2F;24df3557-4a7a-46ca-8616-59286bca8716.webp align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;Build your application as though multiple humans can concurrently and collaboratively edit the same entities. Then, teach your model to control your application. For example, your two human users might have a text chat about the documents they are working on. Treat text responses from the language model the same way; your application code processes the response by executing the same SendMessage command that a human user invokes if they click the send button on a message they typed. If the language model determines it needs to change the title of a document, it must send a message back that your client code can translate into an application command such as UpdateTitle. This command, again, is the same UpdateTitle command that the system will execute if a user clicks an &quot;edit&quot; link next to the title, changes the text, and then hits &quot;save.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;How you handle an invalid attempt to change the system data is an essential aspect of this approach. Language models work best with plain, human language. While their ability to run completions of computer code is currently helpful and still improving, typical language models are better at working with good old-fashioned prose. Because of this, you should respond to the incorrect function call in the same way you&#x27;d react to the human user: show it the natural language descriptions of the errors.&lt;&#x2F;p&gt;
&lt;p&gt;For example, we have a rule that a document title must be unique in the system. We enforce this invariant at the command execution layer. When a title is not unique, instead of recording a TitleUpdated event, the system responds with an error message: &quot;Another document already uses the title Foo Bar Baz.&quot; If the language model sees this error in your response, it will have enough information to correct itself and attempt to execute the UpdateTitle command again with a different title. If, instead, the model receives a machine-friendly error message like &quot;dup_title,&quot; there is less of a chance that it will interpret that to mean an error occurred or that it will sufficiently address the mistake.&lt;&#x2F;p&gt;
&lt;p&gt;Treating generative AI as just another user of your application means that you&#x27;ll also be well-situated to add human&#x2F;human or human&#x2F;human&#x2F;AI collaboration, should that be desired. It becomes more apparent how to handle errors when you receive invalid data or when the model tries to interact with your application in ways that aren&#x27;t available to it, such as hallucinating functionality that doesn&#x27;t exist or isn&#x27;t permitted. Additionally, much knowledge and experience exists in designing UX for collaborative-editing applications. Rather than reinventing the wheel, we can rely on this existing body of knowledge to guide how we approach UX for generative-AI-enhanced applications.&lt;&#x2F;p&gt;
&lt;p&gt;By changing your perspective on the role that generative AI plays in your application, you can both delight your users and create an application that is more fault-tolerant and easier to maintain.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Generative AI is a UX Revolution</title>
          <pubDate>Tue, 12 Mar 2024 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/generative-ai-is-a-ux-revolution/</link>
          <guid>https://johnwilger.com/blog/generative-ai-is-a-ux-revolution/</guid>
          <description xml:base="https://johnwilger.com/blog/generative-ai-is-a-ux-revolution/">&lt;p&gt;When I first engaged with computers, the landscape was predominantly shaped by command-line interfaces, a stark contrast to the GUI-based systems like Windows or MacOS that later emerged and democratized computing. This transformation was monumental, making technology accessible to a broader audience. Yet, it also introduced a dichotomy: while GUIs simplified interactions for the majority, they often fell short for expert users who valued the precision and efficiency of command-line interfaces. This divide underscored a technological gap where neither GUIs nor command-line interfaces could entirely meet the diverse needs of users. Attempts to redesign expert interfaces into graphical formats frequently resulted in a confusing mishmash of buttons and menus, complicating the user experience for novices without offering significant advantages to experts over the command line. Consequently, we find ourselves in a world where applications are often neither intuitive nor efficient, highlighting a significant challenge in the quest for inclusive and effective user interfaces.&lt;&#x2F;p&gt;
&lt;p&gt;In my role as a software engineer, I prefer using command-line and keyboard-driven tools. I avoid using the mouse while programming, not because I think &quot;real programmers don&#x27;t use mice,&quot; but because reaching for the mouse is significantly slower for tasks like navigating files, editing text, and running programs. Therefore, you&#x27;ll often find me in a full-screen terminal window, using tmux (a tool for managing multiple terminal sessions in one window) and vim (a highly customizable text editor). Over time, I&#x27;ve developed a set of configuration tweaks and shell scripts that help me work efficiently. I use a minimalist UI that keeps the code I&#x27;m working on in focus, without the distraction of unnecessary UI elements that require mouse clicks.&lt;&#x2F;p&gt;
&lt;p&gt;![This is my entire main screen when programming.](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1709939093205&#x2F;b1a60861-5844-4855-8ec2-84377132c4e2.png align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;However, for those unfamiliar with these tools, becoming immediately productive in this environment can be quite challenging. I&#x27;ve been working this way for over 20 years, and I’m still finding new ways to boost my efficiency with vim. Given that I use this application almost daily for long hours as a crucial tool of my trade, the time invested in learning to use it as efficiently as possible is highly worthwhile. That said, I wouldn&#x27;t suggest that all user interfaces should require this level of expertise to be effective. When I come across a system I&#x27;m not familiar with, I really value a simple, intuitive interface that helps me do the right thing.&lt;&#x2F;p&gt;
&lt;p&gt;On the other side of the spectrum, there are people who aren&#x27;t as familiar with computer technology and can get frustrated by systems, even when those systems are meant to be used with a mouse or trackpad. My wife is one such person. She&#x27;s not computer illiterate, but she often gets frustrated with websites and applications when she knows what she wants to do but can&#x27;t find the right buttons to press to make it happen.&lt;&#x2F;p&gt;
&lt;p&gt;![](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1709941595065&#x2F;2804c54e-2218-4d1d-928c-f38cdce625f0.webp align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;Recently, at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;artium.ai&#x2F;&quot;&gt;Artium&lt;&#x2F;a&gt;, I&#x27;ve been involved in building &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apex.artium.ai&#x2F;&quot;&gt;a new product called APEX&lt;&#x2F;a&gt;, where we are exploring a new approach to how users interact with applications. APEX is a tool to help you take an idea for a software product and bring that idea to life by walking you through the process of defining the problem space, refining the product vision and value proposition, and then focusing on the key users of the application and how they will use it. The goal is to arrive at an actionable plan for building the initial version of your product and provide you with the information you need to create a proposal and business plan that will help you launch your product. The primary interaction with APEX involves exchanging messages with a generative AI assistant. However, this is not just an AI chatbot that is generating text in a chat window for you to copy and paste into your plan. Our assistant is actively updating the representation of your product on-screen as your conversation progresses.&lt;&#x2F;p&gt;
&lt;p&gt;![](https:&#x2F;&#x2F;cdn.hashnode.com&#x2F;res&#x2F;hashnode&#x2F;image&#x2F;upload&#x2F;v1710267850978&#x2F;5b83104c-e624-4469-99be-6325a60c1830.png align=&quot;center&quot;)&lt;&#x2F;p&gt;
&lt;p&gt;This style of interaction represents a pivotal shift from the traditional dichotomy of GUI and command-line interfaces. By adopting a conversational, AI-driven approach, we can create a unified interface that intuitively adapts to the user&#x27;s expertise level—simplifying the learning curve for novices while providing the depth and efficiency that experts crave. This adaptability showcases how generative AI can transcend the limitations of previous technologies, offering a seamless, inclusive user experience that traditional interfaces have struggled to provide.&lt;&#x2F;p&gt;
&lt;p&gt;While getting ready to launch our open beta for APEX, I asked my wife to try using the application to plan out a product. She was able to get through the entire process, and amazingly, I didn&#x27;t hear her get frustrated with it even once. She is a registered nurse by trade, and as I mentioned before, she is often frustrated with computer programs; this is not someone who has experience with product design or comes to the table with a high level of knowledge about computer systems. When I asked her about the experience, she told me that she much preferred this way of interacting with a computer because it was just like having a conversation with another person. Rather than having to feel overwhelmed by options or frustrated that she couldn&#x27;t figure out how to change something, she was able to simply converse with the assistant and watch the updates happen in the display of the product. And there are no &quot;wrong answers&quot; when talking to the assistant. If you misunderstand a question or simply choose to focus on a different aspect of the product, the assistant is able to adjust, guide, and redirect to help you complete your task.&lt;&#x2F;p&gt;
&lt;p&gt;What is amazing to me is that this style of interaction is equally appealing to me as an expert user of the system. While the conversation and guidance of the assistant can be essential for a novice user who doesn&#x27;t necessarily understand how to approach product planning, as someone who does understand both the process and the types of information that APEX manages, I am able to very quickly create a product plan without ever touching my computer&#x27;s mouse by simply being explicit with the assistant with my directions. I can directly say &quot;set the product title to MyAwesomeProduct&quot;, and it does it. If I say &quot;use Elixir, Phoenix, and LiveView with a Postgres data store&quot;, it updates the Technology section without me having to wait for the assistant to ask me about that. If I already have text-based documentation about a product idea (typed notes from a client planning session, for example), I can simply paste those notes into the assistant window and tell it to create a product plan based on those notes.&lt;&#x2F;p&gt;
&lt;p&gt;Despite the promise of generative AI in enhancing UX, several challenges loom. One significant concern is the potential for AI to misunderstand user intent, leading to frustrating experiences or, in worst-case scenarios, harmful outcomes. Ensuring that AI systems can accurately interpret and act on a wide range of human inputs is crucial for their success. Additionally, ethical considerations are at the heart of deploying generative AI in any user-facing application. Issues such as bias in AI responses, transparency in how AI decisions are made, and the autonomy of users in guiding the AI&#x27;s actions are critical. As we integrate AI more deeply into our lives, ensuring these systems enhance rather than undermine human autonomy, respect user privacy, and promote fairness and inclusivity will be essential. By addressing these ethical challenges head-on, we can harness the benefits of generative AI while minimizing potential harms. Addressing these challenges requires new approaches to software testing to account for the non-deterministic nature of generative AI responses. At Artium, we have also been pioneering the use of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;artium.ai&#x2F;insights&#x2F;test-driving-ai-applications&quot;&gt;Continuous Alignment Testing&lt;&#x2F;a&gt; to ensure that products using generative AI are able to function with a high degree of confidence and safety.&lt;&#x2F;p&gt;
&lt;p&gt;The impact of generative AI on UX design, as shown by our work on APEX, is clear. By enabling a more natural, conversational interaction with technology, we&#x27;re doing more than just making applications more accessible and efficient; we&#x27;re changing how humans interact with computers. This move towards intuitive, adaptive interfaces suggests a future where technology truly serves everyone, no matter their background or level of expertise. As we keep improving and broadening the abilities of generative AI, the potential for innovation in UX seems endless. The evolution from command lines to conversational interfaces is just the start of a UX revolution that will keep growing and surprising us.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;I did not and could not have produced APEX on my own in the time that we were able to bring this together. APEX would not have been possible without the team that created it:&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Cauri Jaye&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Ross Hale&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Serena Epstein&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Michael McCormick&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Ryan Durling&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Nick Mahoney&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;George Wambold&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Nafisa Rawji&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Mark Whaley&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;James Lenhart&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Chay Landaverde&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Gene Gurvich&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Randy Lutcavich&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;John Wilger&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;and the rest of the Artium team who supported our work&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
      <item>
          <title>Using the 27&quot; LG UltraFine 5k Display with Linux</title>
          <pubDate>Thu, 07 Dec 2023 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/using-the-27-lg-ultrafine-5k-display-with-linux/</link>
          <guid>https://johnwilger.com/blog/using-the-27-lg-ultrafine-5k-display-with-linux/</guid>
          <description xml:base="https://johnwilger.com/blog/using-the-27-lg-ultrafine-5k-display-with-linux/">&lt;section class=&quot;updates&quot;&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2019-07-25:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Based on some comments made on the original post below, I swapped out the Asus ThunderboltEX 2 AIC
for a Gigabyte GC-Titan Ridge Thunderbolt AIC that supports two DisplayPort inputs. I&#x27;m happy to
report that I can now use the monitor in full 5k resolution &lt;em&gt;and&lt;&#x2F;em&gt; that X11, GDM, and Gnome were
able to autodetect all of the correct settings (after removing the &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;xorg.conf&lt;&#x2F;code&gt;,
&lt;code&gt;$HOME&#x2F;.config&#x2F;monitors.xml&lt;&#x2F;code&gt;, and &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;gdm3&#x2F;.config&#x2F;monitors.xml&lt;&#x2F;code&gt; files in order to get the
autodetection to kick in.)&lt;&#x2F;p&gt;
&lt;&#x2F;section&gt;
&lt;section class=&quot;updates&quot;&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2019-07-26:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Interestingly, the computer stopped recognizing the USB devices (speaker and microphone) in the
monitor with the new, GC-Titan Ridge TB3 AIC installed. With the previous AIC, I did not
specifically turn on any of the Discrete Thunderbolt Support options in the BIOS and everything
was working fine. It would seem, however, that this AIC requires some BIOS settings to get
everything working.&lt;&#x2F;p&gt;
&lt;p&gt;I found &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.technopat.net&#x2F;konu&#x2F;success-prime-z390-a-9900k-thunderbolt-3-macos-10-14.651013&quot;&gt;these directions for setting up TB3 in the Asus Prime Z390-A
BIOS&lt;&#x2F;a&gt;.
In this case, the author was attempting to configure the BIOS for a Hackintosh build, but I
cargo-culted the setup anyway with the exception of setting the Windows 10 Thunderbolt Support
(when I attempted to set that to the specified value, the BIOS utility just froze up completely.)&lt;&#x2F;p&gt;
&lt;p&gt;After changing those BIOS settings, the USB audio in the monitor was detected just fine on
startup.&lt;&#x2F;p&gt;
&lt;&#x2F;section&gt;
&lt;p&gt;Lachlan and I built a new desktop PC this past weekend (something I haven&#x27;t done
in probably 15 years). Over the past couple of years, I&#x27;ve grown more and more
annoyed with some of the choices Apple has been making with MacOS that have made
it more annoying to use the system for the kind of software development I do,
and I&#x27;ve had an itch to return to my first love: Linux. I&#x27;ve also wanted to
share that experience of building up a computer with Lachlan for a while, since
he&#x27;s the one of my kids who is really into all things technology. To be honest,
while I brought the knowledge and experience of how to safely handle the
equipment and debug any issues, Lachlan was the one who brought the knowledge of
what technology is available today and was able to give great advice in terms of
picking out the processor, motherboard, and video card.&lt;&#x2F;p&gt;
&lt;p&gt;We had one particular challenge, though, in building this system. Last year, I
bought one of the new 27&quot; LG UltraFine 5k displays to use with my Macbook. It&#x27;s
truly a beautiful display in terms of picture quality, and...well...it was
pretty darned expensive, too. What I stupidly didn&#x27;t realize at the time is that
it &lt;em&gt;only&lt;&#x2F;em&gt; has a Thunderbolt input and would pretty much only work &quot;out of the
box&quot; with the newest Macbooks. D&#x27;oh!&lt;&#x2F;p&gt;
&lt;p&gt;A little research showed hints of being able to get the LG 5k monitor to work
with a PC if you have a motherboard that provides a Thunderbolt header &lt;em&gt;and&lt;&#x2F;em&gt; you
install a special Thunderbolt AIC (add-in card) that has a mini-DisplayPort
input. The idea is that you use a patch cable to feed the DisplayPort output
from your video card into the Thunderbolt AIC&#x27;s mini-DisplayPort input, and then
the AIC will send the video signal through its Thunderbolt output. Unfortunately
there was not a huge amount of information out there about this particular
setup, particularly with regard to Linux support.&lt;&#x2F;p&gt;
&lt;p&gt;We decided to go ahead and try to get it working. While we spec&#x27;ed out the
system with that as a central constraint, the AIC itself only cost around $35.00
(USD), and the rest of the system would still be great aside from that. If it
didn&#x27;t work out, the worst case was that I&#x27;d sell the LG 5k monitor and buy a
new monitor with regular DisplayPort inputs—a hassle, but not a disaster.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m happy to report that we &lt;em&gt;did&lt;&#x2F;em&gt; get the monitor working great with Linux after
a lot of trial and error (see below for the details about configuring Linux to
work with this setup) and all of the other hardware worked well out of the box
(except for the lack of any Linux drivers for all of the Asus Aura RGB stuff,
which is only an aesthetic complaint—although an annoying one, because there is
no way to turn off the slowly flashing red glow on the video card).&lt;&#x2F;p&gt;
&lt;p&gt;Here is the part-list for the build:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CPU: Intel Core i9-9900k 5.0 GHz&lt;&#x2F;li&gt;
&lt;li&gt;CPU Cooler: Noctua NH-D15 82.5 CFM CPU Cooler&lt;&#x2F;li&gt;
&lt;li&gt;Motherboard: Asus Prime Z390-A ATX LGA1151&lt;&#x2F;li&gt;
&lt;li&gt;Memory: Corsair Vengeance LPX 16 GB (2 x 8 GB) DDR4-3000&lt;&#x2F;li&gt;
&lt;li&gt;Storage: Samsung 970 Evo 250 GB M.2-2280 Solid State Drive&lt;&#x2F;li&gt;
&lt;li&gt;Video Card: Asus GeForce GTX 1070 8 GB STRIX Video Card&lt;&#x2F;li&gt;
&lt;li&gt;Case: Corsair 760T Black ATX Full Tower Case&lt;&#x2F;li&gt;
&lt;li&gt;Power Supply: EVGA SuperNOVA G2 650 W 80+ Gold Cerified Fully-Modular ATX&lt;&#x2F;li&gt;
&lt;li&gt;Wireless Adapter: Gigbyte GC-WB867D-I PCI-Express x1 802.11a&#x2F;b&#x2F;g&#x2F;n&#x2F;ac WiFi
Adapter&lt;&#x2F;li&gt;
&lt;li&gt;Thunderbolt AIC: Asus ThunderboltEX 3&lt;&#x2F;li&gt;
&lt;li&gt;Operating System: Ubuntu Linux 18.10&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;After building the system with this configuration, running the Nvidia&#x27;s DP
output into the Thunderbolt AIC, and connecting the LG 5k monitor via its
Thunderbolt cable, a fresh install of Ubuntu loaded up fine, and we were able to
log into the Gnome 3 desktop just fine. Unfortunately (or perhaps fortunately in
this case?), Nvidia does not provide their own open-source drivers for Linux,
and Ubuntu does not install the closed-source drivers by default. Instead, you
get the open-source nouveau driver. That driver works fine for most things, but
if we wanted to settle for &quot;most things&quot;, we&#x27;d just have used the on-board GPU
of the motherboard. In order to take advantage of the Nvidia&#x27;s capabilities, we
needed to install the proprietary Nvidia drivers. Ubuntu&#x27;s apt repository
contains the &lt;code&gt;nvidia-driver-390&lt;&#x2F;code&gt; package, and they even make it easy to switch
to it by running &lt;code&gt;ubuntu-drivers autoinstall&lt;&#x2F;code&gt;, however the distribution lags
behind the official release of the drivers, which are up to version 415 as of
this writing. In order to have access to the latest drivers, just add the PPA
and update apt:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo add-apt-repository ppa:graphics-drivers&#x2F;ppa
sudo apt-get update
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then you can install the proprietary driver with:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install nvidia-driver-415 # or whatever the latest version is
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will add the appropriate kernel module and some necessary configuration.&lt;&#x2F;p&gt;
&lt;p&gt;Then you&#x27;ll reboot your computer, and...&lt;&#x2F;p&gt;
&lt;p&gt;The desktop manager won&#x27;t load. You&#x27;ll probably be staring at a blank screen,
perhaps with a blinking cursor.&lt;&#x2F;p&gt;
&lt;p&gt;Luckily, I do have another display here at the house that has an HDMI input, so
we were able to hook that display up for some debugging. We hooked up the other
display and rebooted. This time, GDM loaded up on the HDMI display just fine. I
then reattached the LG display as a second monitor. Nothing showed up on it,
&lt;em&gt;but&lt;&#x2F;em&gt;, it showed up as a monitor in Gnome&#x27;s display settings. Heh. That&#x27;s
weird...&lt;&#x2F;p&gt;
&lt;p&gt;On a whim, I changed the resolution of the LG display from &quot;auto&quot; to 4096x2304,
and...&lt;em&gt;all of the sudden it worked!&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t know for sure, but my guess is that something about being passed through
the Thunderbolt AIC is causing the automatic detection of the resolution to
fail.&lt;&#x2F;p&gt;
&lt;p&gt;Great, we&#x27;re done now, right?&lt;&#x2F;p&gt;
&lt;p&gt;Not exactly. I&#x27;ll spare you the details of the rest of the debugging we went
through (&lt;em&gt;lot&#x27;s&lt;&#x2F;em&gt; of trial and error, searching Google, and piecing together what
I could find from people who had sort-of-kind-of similar symptoms here and
there, but never all in the same combination, and definitely never with this set
of hardware.) Suffice it to say that the solution lies in being &lt;em&gt;very&lt;&#x2F;em&gt; specific
about configuring the display with all three of Xorg, GDM, and Gnome. Here are
the contents of the configuration files that needed to be changed in order to
get this working on the new system (you may need to tweak them a bit if you have
a different combination of hardware, but hopefully this saves you from the hours
of trial and error!):&lt;&#x2F;p&gt;
&lt;p&gt;In &lt;code&gt;&#x2F;etc&#x2F;X11&#x2F;xorg.conf&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# nvidia-settings: X configuration file generated by nvidia-settings
# nvidia-settings:  version 415.27


Section &amp;quot;ServerLayout&amp;quot;
    Identifier     &amp;quot;Layout0&amp;quot;
    Screen      0  &amp;quot;Screen0&amp;quot; 0 0
    InputDevice    &amp;quot;Keyboard0&amp;quot; &amp;quot;CoreKeyboard&amp;quot;
    InputDevice    &amp;quot;Mouse0&amp;quot; &amp;quot;CorePointer&amp;quot;
    Option         &amp;quot;Xinerama&amp;quot; &amp;quot;0&amp;quot;
EndSection

Section &amp;quot;Files&amp;quot;
EndSection

Section &amp;quot;Module&amp;quot;
    Load           &amp;quot;dbe&amp;quot;
    Load           &amp;quot;extmod&amp;quot;
    Load           &amp;quot;type1&amp;quot;
    Load           &amp;quot;freetype&amp;quot;
    Load           &amp;quot;glx&amp;quot;
EndSection

Section &amp;quot;InputDevice&amp;quot;

    # generated from default
    Identifier     &amp;quot;Mouse0&amp;quot;
    Driver         &amp;quot;mouse&amp;quot;
    Option         &amp;quot;Protocol&amp;quot; &amp;quot;auto&amp;quot;
    Option         &amp;quot;Device&amp;quot; &amp;quot;&#x2F;dev&#x2F;psaux&amp;quot;
    Option         &amp;quot;Emulate3Buttons&amp;quot; &amp;quot;no&amp;quot;
    Option         &amp;quot;ZAxisMapping&amp;quot; &amp;quot;4 5&amp;quot;
EndSection

Section &amp;quot;InputDevice&amp;quot;

    # generated from default
    Identifier     &amp;quot;Keyboard0&amp;quot;
    Driver         &amp;quot;kbd&amp;quot;
EndSection

Section &amp;quot;Monitor&amp;quot;

    # HorizSync source: edid, VertRefresh source: edid
    Identifier     &amp;quot;Monitor0&amp;quot;
    VendorName     &amp;quot;Unknown&amp;quot;
    ModelName      &amp;quot;LG Electronics LG UltraFine&amp;quot;
    HorizSync       31.5 - 177.7
    VertRefresh     60.0 - 60.0
    Option         &amp;quot;DPMS&amp;quot;
EndSection

Section &amp;quot;Device&amp;quot;
    Identifier     &amp;quot;Device0&amp;quot;
    Driver         &amp;quot;nvidia&amp;quot;
    VendorName     &amp;quot;NVIDIA Corporation&amp;quot;
    BoardName      &amp;quot;GeForce GTX 1070&amp;quot;
EndSection

Section &amp;quot;Screen&amp;quot;

# Removed Option &amp;quot;metamodes&amp;quot; &amp;quot;DP-0: nvidia-auto-select +0+0&amp;quot;
    Identifier     &amp;quot;Screen0&amp;quot;
    Device         &amp;quot;Device0&amp;quot;
    Monitor        &amp;quot;Monitor0&amp;quot;
    DefaultDepth    24
    Option         &amp;quot;Stereo&amp;quot; &amp;quot;0&amp;quot;
    Option         &amp;quot;nvidiaXineramaInfoOrder&amp;quot; &amp;quot;DFP-3&amp;quot;
    Option         &amp;quot;ConnectedMonitor&amp;quot; &amp;quot;DP-0&amp;quot;
    Option         &amp;quot;metamodes&amp;quot; &amp;quot;DP-0: 4096x2304 +0+0&amp;quot;
    Option         &amp;quot;SLI&amp;quot; &amp;quot;Off&amp;quot;
    Option         &amp;quot;MultiGPU&amp;quot; &amp;quot;Off&amp;quot;
    Option         &amp;quot;BaseMosaic&amp;quot; &amp;quot;off&amp;quot;
    SubSection     &amp;quot;Display&amp;quot;
        Depth       24
    EndSubSection
EndSection
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In &lt;em&gt;both&lt;&#x2F;em&gt; &lt;code&gt;$HOME&#x2F;.config&#x2F;monitors.xml&lt;&#x2F;code&gt; &lt;em&gt;and&lt;&#x2F;em&gt;
&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;gdm3&#x2F;.config&#x2F;monitors.xml&lt;&#x2F;code&gt; (my guess is that you&#x27;ll need to change the
value for &lt;code&gt;&amp;lt;serial&amp;gt;&lt;&#x2F;code&gt; here to the serial number for your actual monitor):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;monitors version=&amp;quot;2&amp;quot;&amp;gt;
  &amp;lt;configuration&amp;gt;
    &amp;lt;logicalmonitor&amp;gt;
      &amp;lt;x&amp;gt;0&amp;lt;&#x2F;x&amp;gt;
      &amp;lt;y&amp;gt;0&amp;lt;&#x2F;y&amp;gt;
      &amp;lt;scale&amp;gt;1&amp;lt;&#x2F;scale&amp;gt;
      &amp;lt;primary&amp;gt;yes&amp;lt;&#x2F;primary&amp;gt;
      &amp;lt;monitor&amp;gt;
        &amp;lt;monitorspec&amp;gt;
          &amp;lt;connector&amp;gt;DP-0&amp;lt;&#x2F;connector&amp;gt;
          &amp;lt;vendor&amp;gt;GSM&amp;lt;&#x2F;vendor&amp;gt;
          &amp;lt;product&amp;gt;LG UltraFine&amp;lt;&#x2F;product&amp;gt;
          &amp;lt;serial&amp;gt;707NTFA2N827&amp;lt;&#x2F;serial&amp;gt;
        &amp;lt;&#x2F;monitorspec&amp;gt;
        &amp;lt;mode&amp;gt;
          &amp;lt;width&amp;gt;4096&amp;lt;&#x2F;width&amp;gt;
          &amp;lt;height&amp;gt;2304&amp;lt;&#x2F;height&amp;gt;
          &amp;lt;rate&amp;gt;59.999275207519531&amp;lt;&#x2F;rate&amp;gt;
        &amp;lt;&#x2F;mode&amp;gt;
      &amp;lt;&#x2F;monitor&amp;gt;
    &amp;lt;&#x2F;logicalmonitor&amp;gt;
  &amp;lt;&#x2F;configuration&amp;gt;
&amp;lt;&#x2F;monitors&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With those files in place, neither X nor Gnome were relying on autoconfiguration
of the display&#x27;s settings, and the LG 5k display seems to be working just fine
(although not actually at &quot;5k&quot;, but it still looks great.)&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Complex Unique Constraints with PostgreSQL Triggers in Ecto</title>
          <pubDate>Sun, 16 Feb 2020 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/complex-unique-constraints-with-postgresql-triggers-in-ecto/</link>
          <guid>https://johnwilger.com/blog/complex-unique-constraints-with-postgresql-triggers-in-ecto/</guid>
          <description xml:base="https://johnwilger.com/blog/complex-unique-constraints-with-postgresql-triggers-in-ecto/">&lt;p&gt;&lt;a rel=&quot;external&quot; title=&quot;Ecto&quot; href=&quot;https:&#x2F;&#x2F;hexdocs.pm&#x2F;ecto&quot;&gt;Ecto&lt;&#x2F;a&gt; makes it easy to work with typical uniqueness constraints in your database; you just
define your table like this:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule MyApp.Repo.Migrations.CreateFoos do
  use Ecto.Migration

  def change do
    create table(:foos) do
      add :name, :text, null: false
    end

    create unique_index(:foos, :name)
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and a module with a &lt;a rel=&quot;external&quot; title=&quot;Ecto.Changeset.unique_constraint&#x2F;3&quot; href=&quot;https:&#x2F;&#x2F;hexdocs.pm&#x2F;ecto&#x2F;Ecto.Changeset.html#unique_constraint&#x2F;3&quot;&gt;changeset validation for the uniqueness constraint&lt;&#x2F;a&gt;,
perhaps like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule MyApp.Foo do
  use Ecto.Schema

  import Ecto.Changeset

  schema &amp;quot;foos&amp;quot; do
    field :name, :string
  end

  def changeset(%__MODULE__{} = foo, %{} = changes) do
      foo
      |&amp;gt; cast(changes, [:name])
      |&amp;gt; unique_constraint(:name)
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then, when you run:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;result = MyApp.Foo.changeset(%MyApp.Foo{}, %{name: &amp;quot;bar&amp;quot;})
         |&amp;gt; MyApp.Repo.insert()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The Ecto library will attempt to insert your record, and if there is already a record where the
name column is set to &quot;bar&quot;, Ecto will see the uniqueness constraint violation error produced by
the database and turn it into a validation error on your changeset, so that it would look
something like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;{:error, %Ecto.Changeset{errors: [name: {&amp;quot;has already been taken&amp;quot;, _}]}} = result
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;something-more-complex&quot;&gt;Something More Complex&lt;&#x2F;h3&gt;
&lt;p&gt;I recently needed to enforce a database constraint similar in spirit to a unique index (if a
record were in violation, I wanted the same behavior on the Elixir end of things—the changeset
should report that the value for the field &quot;has already been taken&quot;) however the criteria for what
should be considered &quot;unique&quot; was more complex than what a simple unique index in PostgreSQL would
be able to deal with.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s pretend we have a project that is managing meeting room reservations. The process for
reserving a meeting room is as follows:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The customer chooses a room and selects the time range for the reservation.&lt;&#x2F;li&gt;
&lt;li&gt;Assuming the room is available at that time, the system places a hold on the room for 24 hours&lt;&#x2F;li&gt;
&lt;li&gt;Within that 24-hour period, the customer pays for the room and completes some contractual
information that must be signed by both the customer and the facility manager.&lt;&#x2F;li&gt;
&lt;li&gt;Once the payment is complete and the contracts are signed, the reservation is confirmed.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Here, then, are the business rules for creating a reservation:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A room reservation cannot be made for a given room and time-period if there exists another room
reservation for that same room with an overlapping time-period and the existing reservation is
in a &quot;confirmed&quot; status.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;A room reservation cannot be made for a given room and time-period if there exists another room
reservation for that same room with an overlapping time-period, the existing reservation is in a
&quot;hold&quot; status, and the existing reservation was created within the last 24 hours.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;We decide to store the room reservations in a table defined as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule Meetings.Repo.Migrations.CreateRoomReservations do
  use Ecto.Migration

  def change do
    create table(:room_reservations) do
      add :customer_id, :binary_id, null: false
      add :room_id, :binary_id, null: false
      add :reservation_starts_at, :timestamp, null: false
      add :reservation_ends_at, :timestamp, null: false
      add :status, :text, null: false, default: &amp;quot;hold&amp;quot;
      timestamps()
    end
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and ideally, we&#x27;d like to be able to model the &lt;code&gt;RoomReservation&lt;&#x2F;code&gt; in Elixir as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule Meetings.RoomReservation do
  use Ecto.Schema

  import Ecto.Changeset

  schema &amp;quot;room_reservations&amp;quot; do
    field :customer_id, :binary_id
    field :room_id, :binary_id
    field :reservation_starts_at, :utc_datetime
    field :reservation_ends_at, :utc_datetime
    field :status, :string
    timestamps()
  end

  def create(%{} = res_data) do
    %__MODULE__{}
    |&amp;gt; cast(res_data, [:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
    |&amp;gt; validate_required([:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
    |&amp;gt; put_change(:status, &amp;quot;hold&amp;quot;)
    |&amp;gt; unique_constraint(:room_id, message: &amp;quot;has already been reserved&amp;quot;)
    |&amp;gt; Meeting.Repo.insert()
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;with the interesting bit being the &lt;code&gt;|&amp;gt; unique_constraint(:room_id)&lt;&#x2F;code&gt; line. When a customer tries to
reserve a room that is already reserved for the given time period, we want the
&lt;code&gt;Meeting.RoomReservation.create&#x2F;1&lt;&#x2F;code&gt; function to return with a validation error:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;%{:error, %Ecto.Changeset{errors: [room_id: {&amp;quot;has already been reserved&amp;quot;, _}]}} =
  Meetings.RoomReservation.create(%{
    customer_id: &amp;quot;0af5716a-c3d2-4d4c-87f5-9fed9b2515d4&amp;quot;,
    room_id: &amp;quot;2c0d6242-3585-40cb-be92-a1818c0f4a73&amp;quot;,
    reservation_starts_at: DateTime.from_naive!(~N[2020-01-01 8:00:00], &amp;quot;Etc&#x2F;UTC&amp;quot;),
    reservation_ends_at: DateTime.from_naive!(~N[2020-01-01 17:00:00], &amp;quot;Etc&#x2F;UTC&amp;quot;)
  })
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Clearly, according to the business rules for the system, we can&#x27;t just put a unique index on the
&lt;code&gt;room_id&lt;&#x2F;code&gt; column. To the best of my knowledge, an exclusion constraint also won&#x27;t work here,
because of the need to &lt;em&gt;not&lt;&#x2F;em&gt; check against any rows that have a status of &quot;hold&quot; and an
&lt;code&gt;inserted_at&lt;&#x2F;code&gt; timestamp that is more than 24 hours ago relative to the current time. It &lt;em&gt;is&lt;&#x2F;em&gt;
however possible to use a trigger function to enforce the constraint in the database:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule Meetings.Repo.Migrations.CreateRoomReservations do
  use Ecto.Migration

  def change do
    create table(:room_reservations) do
      add :customer_id, :binary_id, null: false
      add :room_id, :binary_id, null: false
      add :reservation_starts_at, :timestamp, null: false
      add :reservation_ends_at, :timestamp, null: false
      add :status, :text, null: false, default: &amp;quot;hold&amp;quot;
      timestamps()
    end

    execute(
      # up
      ~S&amp;quot;&amp;quot;&amp;quot;
      CREATE FUNCTION room_reservations_check_room_availability() RETURNS TRIGGER AS
      $$
      BEGIN
      IF EXISTS(
        SELECT 1
        FROM room_reservations rr
        WHERE rr.room_id = NEW.room_id
          AND tsrange(rr.reservation_starts_at, rr.reservation_ends_at, &amp;#39;[]&amp;#39;) &amp;amp;&amp;amp;
              tsrange(NEW.reservation_starts_at, NEW.reservation_ends_at, &amp;#39;[]&amp;#39;)
          AND (
            rr.status = &amp;#39;confirmed&amp;#39;
              OR rr.inserted_at &amp;gt; CURRENT_TIMESTAMP - interval &amp;#39;24 hours&amp;#39;
          )
      )
      THEN
        RAISE &amp;quot;room already reserved&amp;quot;;
      END IF;
      RETURN NEW;
      END;
      $$ language plpgsql;
      &amp;quot;&amp;quot;&amp;quot;,

      # down
      &amp;quot;DROP FUNCTION room_reservations_check_room_availability;&amp;quot;
    )

    execute(
      # up
      ~S&amp;quot;&amp;quot;&amp;quot;
      CREATE TRIGGER room_reservations_room_availability_check
      BEFORE INSERT ON room_reservations
      FOR EACH ROW
      EXECUTE PROCEDURE room_reservations_check_room_availability();
      &amp;quot;&amp;quot;&amp;quot;,

      # down
      &amp;quot;DROP TRIGGER room_reservations_room_availability_check ON room_reservations;&amp;quot;
    )
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The problem is that instead of that nice validation error on the changeset that we &lt;em&gt;want&lt;&#x2F;em&gt; to get,
we instead end up with an unhandled &lt;code&gt;Postgrex.Error&lt;&#x2F;code&gt; exception. We could, of course, simply rescue
that exception in our &lt;code&gt;Meetings.RoomReservation.create&#x2F;1&lt;&#x2F;code&gt; function and add the error to the
changeset ourselves, but that starts to look a little ugly:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule Meetings.RoomReservation do
  # ...

  def create(%{} = res_data) do
    changeset =
      %__MODULE__{}
      |&amp;gt; cast(res_data, [:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
      |&amp;gt; validate_required([:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
      |&amp;gt; put_change(:status, &amp;quot;hold&amp;quot;)
    Meeting.Repo.insert(changeset)
  rescue
    error in Postgrex.Error -&amp;gt;
      case error do
        %{postgres: %{message: &amp;quot;room already reserved&amp;quot;}} -&amp;gt;
          changeset
          |&amp;gt; add_error(:room_id, &amp;quot;has already been reserved&amp;quot;)
      end
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;tl-dr-it-s-all-about-the-raise&quot;&gt;TL;DR - It&#x27;s All About the Raise&lt;&#x2F;h3&gt;
&lt;p&gt;Knowing that &lt;code&gt;Ecto.Changeset.unique_constraint&#x2F;3&lt;&#x2F;code&gt; works by intercepting an error raised by the
database, I set out to see if I could implement the complex unique constraint logic in the
database and still be able to use the &lt;code&gt;Ecto.Changeset.unique_constraint&#x2F;3&lt;&#x2F;code&gt; validation without
needing to modify any Elixir code. Looking at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;elixir-ecto&#x2F;ecto_sql&#x2F;blob&#x2F;0359b7ce5155974d566fcd0b127f8695aed8b3a9&#x2F;lib&#x2F;ecto&#x2F;adapters&#x2F;postgres&#x2F;connection.ex#L18-L19&quot;&gt;the relevant code in ecto_sql&lt;&#x2F;a&gt;, we
can see that the trick to getting the &lt;code&gt;room_reservations_check_room_availability&lt;&#x2F;code&gt; functionality to
work with that function is to change the PostgreSQL exception to use the correct error code as
well as a constraint name that is linked with the changeset validation. We can redifine the
PostgreSQL function as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE FUNCTION room_reservations_check_room_availability() RETURNS TRIGGER AS
$$
BEGIN
IF EXISTS(
  SELECT 1
  FROM room_reservations rr
  WHERE rr.room_id = NEW.room_id
    AND tsrange(rr.reservation_starts_at, rr.reservation_ends_at, &amp;#39;[]&amp;#39;) &amp;amp;&amp;amp;
        tsrange(NEW.reservation_starts_at, NEW.reservation_ends_at, &amp;#39;[]&amp;#39;)
    AND (
      rr.status = &amp;#39;confirmed&amp;#39;
        OR rr.inserted_at &amp;gt; CURRENT_TIMESTAMP - interval &amp;#39;24 hours&amp;#39;
    )
)
THEN
  RAISE unique_violation
    USING CONSTRAINT = &amp;#39;room_reservations_room_reserved&amp;#39;;
END IF;
RETURN NEW;
END;
$$ language plpgsql;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and then add the &lt;code&gt;:name&lt;&#x2F;code&gt; option to our &lt;code&gt;unique_constraint&lt;&#x2F;code&gt; in the changeset validation:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;elixir&quot;&gt;defmodule Meetings.RoomReservation do
  #...

  def create(%{} = res_data) do
    %__MODULE__{}
    |&amp;gt; cast(res_data, [:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
    |&amp;gt; validate_required([:customer_id, :room_id, :reservation_starts_at, :reservation_ends_at])
    |&amp;gt; put_change(:status, &amp;quot;hold&amp;quot;)
    |&amp;gt; unique_constraint(:room_id,
                         message: &amp;quot;has already been reserved&amp;quot;,
                         name: &amp;quot;room_reservations_room_reserved&amp;quot;)
    |&amp;gt; Meeting.Repo.insert()
  end
end
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Kookaburra 0.24.0 Released - Exorcised ActiveSupport</title>
          <pubDate>Tue, 15 May 2012 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/kookaburra-0240-released-exorcised-activesupport/</link>
          <guid>https://johnwilger.com/blog/kookaburra-0240-released-exorcised-activesupport/</guid>
          <description xml:base="https://johnwilger.com/blog/kookaburra-0240-released-exorcised-activesupport/">&lt;p&gt;I just released version 0.24.0 of &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;kookaburra&quot;&gt;Kookaburra&lt;&#x2F;a&gt; to
&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;kookaburra&#x2F;versions&#x2F;0.24.0&quot;&gt;Rubygems.org&lt;&#x2F;a&gt;. While there were no changes to &lt;em&gt;Kookaburra&#x27;s&lt;&#x2F;em&gt; API
with this release, it is a minor release rather than a patch release,
because I removed &lt;a rel=&quot;external&quot; title=&quot;ActiveSupport 3.0.0 API Documentation&quot; href=&quot;http:&#x2F;&#x2F;rubydoc.info&#x2F;gems&#x2F;activesupport&#x2F;3.0.0&#x2F;frames&quot;&gt;ActiveSupport&lt;&#x2F;a&gt; from Kookaburra&#x27;s dependencies.
Previously, Kookaburra depended on ActiveSupport &amp;gt;= 3.0, and this
prevented it from working smoothly with Rails 2.x applications (at least
in the same Bundler bundle.) Since Kookaburra only used a small fraction
of ActiveSupport, it seemed easiest just to break the dependency, so
that your application can use whatever version of ActiveSupport it
needs.&lt;&#x2F;p&gt;
&lt;p&gt;Because of the fact that ActiveSupport makes a number of modifications
to core Ruby classes, if your application does not otherwise explicitly
require ActiveSupport, some of these core classes may now behave
differently. Specifically, &lt;code&gt;Hash&lt;&#x2F;code&gt; and &lt;code&gt;String&lt;&#x2F;code&gt; core extensions were
being used, as well as &lt;code&gt;ActiveSupport::CoreExt::Module::Delegation&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Also, where &lt;code&gt;Kookaburra::JsonApiDriver&lt;&#x2F;code&gt; used &lt;code&gt;ActiveSupport::JSON&lt;&#x2F;code&gt; for
encoding and decoding of JSON data, it now uses &lt;code&gt;::JSON&lt;&#x2F;code&gt; as provided by
&lt;em&gt;either&lt;&#x2F;em&gt; the &lt;a rel=&quot;external&quot; title=&quot;json | RubyGems.org&quot; href=&quot;http:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;json&quot;&gt;json&lt;&#x2F;a&gt; or the &lt;a rel=&quot;external&quot; title=&quot;json_pure | RubyGems.org&quot; href=&quot;http:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;json_pure&quot;&gt;json_pure&lt;&#x2F;a&gt; gems. The tests
on Kookaburra itself pass just by swapping out ActiveSupport JSON
library for the new one, but these tests do not comprehensively test
JSON usage, and there may be differences that impact your application.&lt;&#x2F;p&gt;
&lt;p&gt;Kookaburra declares its dependency on the &lt;code&gt;json_pure&lt;&#x2F;code&gt; gem (a pure-ruby
version) in order to ensure that it works cross-platform. However, both
&lt;code&gt;json&lt;&#x2F;code&gt; and &lt;code&gt;json_pure&lt;&#x2F;code&gt; are written in such a way that, when you &lt;code&gt;require &#x27;json&#x27;&lt;&#x2F;code&gt;, it will detect if both libraries are installed and prefer the
natively compiled &lt;code&gt;json&lt;&#x2F;code&gt; gem&#x27;s version of the library.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>tmux and the OSX Clipboard</title>
          <pubDate>Thu, 12 Apr 2012 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/tmux-and-the-osx-clipboard/</link>
          <guid>https://johnwilger.com/blog/tmux-and-the-osx-clipboard/</guid>
          <description xml:base="https://johnwilger.com/blog/tmux-and-the-osx-clipboard/">&lt;p&gt;I started using &lt;a rel=&quot;external&quot; title=&quot;tmux&quot; href=&quot;http:&#x2F;&#x2F;tmux.sourceforge.net&#x2F;&quot;&gt;tmux&lt;&#x2F;a&gt; recently after a) I was informed that GNU screen is
basically an outdated POS, and b) an &lt;a rel=&quot;external&quot; title=&quot;tmux: Productive Mouse-Free Development&quot; href=&quot;http:&#x2F;&#x2F;pragprog.com&#x2F;book&#x2F;bhtmux&#x2F;tmux&quot;&gt;excellent book&lt;&#x2F;a&gt; on the subject
was published. I&#x27;m glad to have made the switch, as tmux is a wonderful
improvement over screen. However, on OSX, I found that the &lt;code&gt;pbcopy&lt;&#x2F;code&gt; and
&lt;code&gt;pbpaste&lt;&#x2F;code&gt; commands (among other things) would no longer work from within a tmux
session.&lt;&#x2F;p&gt;
&lt;p&gt;After a bit of searching, I found &lt;a rel=&quot;external&quot; title=&quot;Reintroducing tmux to the OSX Clipboard&quot; href=&quot;http:&#x2F;&#x2F;writeheavy.com&#x2F;2011&#x2F;10&#x2F;23&#x2F;reintroducing-tmux-to-the-osx-clipboard.html&quot;&gt;this post&lt;&#x2F;a&gt; that explains how to make
things right again.&lt;&#x2F;p&gt;
&lt;p&gt;TL;DR:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh run this&quot;&gt;brew install reattach-to-user-namespace
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh add this to ~&#x2F;.tmux.conf&quot;&gt;# Reattach to user namespace so that clipboard integration with OSX works again
set-option -g default-command &amp;quot;reattach-to-user-namespace -l zsh&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Obviously, replace &lt;code&gt;zsh&lt;&#x2F;code&gt; above with whatever you use for your shell.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Kookaburra Rewrite for 0.15.1</title>
          <pubDate>Sat, 10 Mar 2012 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/kookaburra-rewrite-for-0151/</link>
          <guid>https://johnwilger.com/blog/kookaburra-rewrite-for-0151/</guid>
          <description xml:base="https://johnwilger.com/blog/kookaburra-rewrite-for-0151/">&lt;p&gt;After getting some good feedback on &lt;a rel=&quot;external&quot; title=&quot;jwilger&#x2F;kookaburra&quot; href=&quot;http:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;kookaburra&quot;&gt;Kookaburra&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; title=&quot;jwilger&#x2F;kookaburra&quot; href=&quot;http:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;kookaburra&quot;&gt;Kookaburra&lt;&#x2F;a&gt; since the original
release announcement as well as using it in a few more projects, [Sam] &lt;a rel=&quot;external&quot; title=&quot;Sam Livingston-Gray&quot; href=&quot;http:&#x2F;&#x2F;livingston-gray.com&quot;&gt;SLG&lt;&#x2F;a&gt; and
I decided to treat all of the versions prior to 0.15.0 as a spike and rewrite
the framework from the ground up. Although we certainly learned a lot about the
approach with the pre-0.15 versions, the problems with continuing to grow
Kookaburra from that seed became apparent as we tried to use it in more
applications.&lt;&#x2F;p&gt;
&lt;p&gt;Because the original code was extracted from another project, the Kookaburra
framework itself was not well tested. It had been indirectly tested due to its
position within that other project&#x27;s tests, but as a seperate library it lacked
tests that would help bot document the project and ensure newcomers can work on
it with a good safety net. Additionally, there were a number of design decisions
made in the earlier versions of Kookaburra that were...questionable. Not only
would these decisions probably have been made differently if the development of
the framework itself had been driven with unit tests, these decisions in many
cases made it difficult to go back and add tests after the fact.&lt;&#x2F;p&gt;
&lt;p&gt;Starting with 0.15.1, which I just [released] &lt;a rel=&quot;external&quot; title=&quot;kookaburra | Rubygems.org&quot; href=&quot;http:&#x2F;&#x2F;rubygems.org&#x2F;gems&#x2F;kookaburra&quot;&gt;Gem&lt;&#x2F;a&gt; a few moments ago,
Kookaburra has been rewritten using a proper TDD approach. In some ways, 0.15.1
is possibly less functional than its 0.14.x cousins, but it is almost certainly
easier to understand and contribute to the project from this point on. Please
have a look at the new codebase, try it out in your projects, and send me a pull
request with updates to the library. Also, if you have any problems getting it
running, please don&#x27;t hesitate to [let me know] &lt;a rel=&quot;external&quot; title=&quot;Issues jwilger&#x2F;kookaburra&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jwilger&#x2F;kookaburra&#x2F;issues?sort=created&amp;amp;direction=desc&amp;amp;state=open&quot;&gt;GHIssues&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Using Jeweler for Private Gems</title>
          <pubDate>Wed, 25 Jan 2012 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/using-jeweler-for-private-gems/</link>
          <guid>https://johnwilger.com/blog/using-jeweler-for-private-gems/</guid>
          <description xml:base="https://johnwilger.com/blog/using-jeweler-for-private-gems/">&lt;p&gt;I really like using [Jeweler] &lt;a rel=&quot;external&quot; title=&quot;technicalpickles&#x2F;jeweler - GitHub&quot; href=&quot;http:&#x2F;&#x2F;github.com&#x2F;technicalpickles&#x2F;jeweler&quot;&gt;1&lt;&#x2F;a&gt; to create and manage gems, but its default
behavior is to publish your gem to [rubygems.org] &lt;a rel=&quot;external&quot; title=&quot;RubyGems.org | your community gem host&quot; href=&quot;http:&#x2F;&#x2F;rubygems.org&quot;&gt;2&lt;&#x2F;a&gt; whenever you run &lt;code&gt;rake release&lt;&#x2F;code&gt;. This is great for generally useful libraries that you want to
open-source, but not as great when you want to use gems as shared libraries for
internal use only (whether because the code contains business secrets or just
because it&#x27;s not something that would be useful to the community at large.)&lt;&#x2F;p&gt;
&lt;p&gt;After a bit of poking around, I figured out how to use Jeweler to manage
your versioning, gemspec and git tagging without pushing the result to
rubygems.org when running the release task. It turns out to be both obvious and
trivial. Just add the following line to the end of your gem project&#x27;s
&lt;code&gt;Rakefile&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;ruby Rakefile&quot;&gt;Rake::Task[:release].prerequisites.delete(&amp;#39;gemcutter:release&amp;#39;)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</description>
      </item>
      <item>
          <title>Acceptance and Integration Testing with Kookaburra</title>
          <pubDate>Sat, 21 Jan 2012 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/acceptance-and-integration-testing-with-kookaburra/</link>
          <guid>https://johnwilger.com/blog/acceptance-and-integration-testing-with-kookaburra/</guid>
          <description xml:base="https://johnwilger.com/blog/acceptance-and-integration-testing-with-kookaburra/">&lt;p&gt;&lt;strong&gt;UPDATE (2012-01-22):&lt;&#x2F;strong&gt; &lt;em&gt;I realized this morning that the credit I gave to &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;resume.livingston-gray.com&#x2F;&quot;&gt;Sam
Livingston-Gray&lt;&#x2F;a&gt; below may not have
adequately shown how instrumental he was in getting this project off the ground;
especially since much of his work was done in the private repository from which
this was extracted. So, thanks, Sam. This might not have gone anywhere if you
hadn&#x27;t worked to put the idea in practice in our application and helped everyone
on our team learn how to use the approach. I made a few minor changes below to
reflect this a bit better.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We&#x27;ve been using &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;cukes.info&#x2F;&quot;&gt;Cucumber&lt;&#x2F;a&gt; for acceptance testing at
&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;renewfund.com&quot;&gt;Renewable Funding&lt;&#x2F;a&gt; since back when it was still part of
the &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;rspec.info&quot;&gt;RSpec&lt;&#x2F;a&gt; project (indeed, since before we were even
Renewable Funding). While we&#x27;ve always liked the ability to have plain-language
feature documentation that we could automatically test against, after years of
adding to and maintaining a fairly large set of Cucumber scenarios, the cost of
that maintenance was starting to really slow us down. The test suite began to
grow fragile, and it seemed like every time one of our UX designers changed
anything about the application&#x27;s interface, the development team would spend a
bunch of time just babysitting Cucumber tests to get them passing again.&lt;&#x2F;p&gt;
&lt;p&gt;Last year, as I was reading Jez Humble&#x27;s excellent &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.amazon.com&#x2F;gp&#x2F;product&#x2F;0321601912?tag=contindelive-20&quot;&gt;Continuous
Delivery&lt;&#x2F;a&gt; book,
I was inspired when I came across the section titled &quot;The Application Driver
Layer&quot; (p. 198). This section describes an approach to acceptance testing where
the specification and the test implementation are isolated from the details of
the application&#x27;s user interface by inserting a layer between the two that uses
good old OOP to abstract the user interface components. Martin Fowler describes
it as the [Window Driver] &lt;a rel=&quot;external&quot; title=&quot;Window Driver - Martin Fowler&quot; href=&quot;http:&#x2F;&#x2F;martinfowler.com&#x2F;eaaDev&#x2F;WindowDriver.html&quot;&gt;1&lt;&#x2F;a&gt; pattern on his website.&lt;&#x2F;p&gt;
&lt;p&gt;I started a proof-of-concept implementation of this pattern last summer, then my
coworker, &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;resume.livingston-gray.com&#x2F;&quot;&gt;Sam Livingston-Gray&lt;&#x2F;a&gt; and I
started pulling it into a new project at work. After Sam and the rest of the
Renewable Funding team helped improve on my original attempt while putting it to
use for the last six or so months, we extracted a library to make it easier for
other Ruby developers to implement this pattern in their testing. I&#x27;m happy to
introduce &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;github.com&#x2F;projectdx&#x2F;kookaburra&quot;&gt;Kookaburra&lt;&#x2F;a&gt; to the world.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Capybara, Selenium and Firefox 3.5</title>
          <pubDate>Sat, 30 Apr 2011 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/capybara-selenium-and-firefox-35/</link>
          <guid>https://johnwilger.com/blog/capybara-selenium-and-firefox-35/</guid>
          <description xml:base="https://johnwilger.com/blog/capybara-selenium-and-firefox-35/">&lt;p&gt;I banged my head against this one while setting up our CI build for a new
project at work and just now figured it out. I added an example integration
spec that just visits the default Rails homepage, clicks the &quot;About your
application&#x27;s environment&quot; link, and verifies the Rails version. This link
uses an AJAX action to load the environment details, so I marked it as a
JavaScript test, which would cause it to use Capybara&#x27;s Selenium driver.&lt;&#x2F;p&gt;
&lt;p&gt;The test passed fine on my machine (with Firefox 4 installed), but it failed
consistently on CI, which has Firefox 3.5. It kept failing with the exception
&lt;code&gt;Capybara::TimeoutError: failed to resynchronize, AJAX request timed out&lt;&#x2F;code&gt;. I
didn&#x27;t dig enough into the guts of Selenium&#x2F;Firefox to find out exactly what
is going on (and a Google search for that error message turned up nothing that
really explained it) but I did at least find a workable fix. Just add the
following line to &lt;code&gt;spec&#x2F;support&#x2F;capybara.rb&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Capybara::Selenium::Driver::DEFAULT_OPTIONS[:resynchronize] = false
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</description>
      </item>
      <item>
          <title>Apprenticeship Program at Renewable Funding - Thoughts?</title>
          <pubDate>Fri, 04 Mar 2011 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/apprenticeship-program-at-renewable-funding-thoughts/</link>
          <guid>https://johnwilger.com/blog/apprenticeship-program-at-renewable-funding-thoughts/</guid>
          <description xml:base="https://johnwilger.com/blog/apprenticeship-program-at-renewable-funding-thoughts/">&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;renewfund.com&quot;&gt;Renewable Funding&lt;&#x2F;a&gt; technology team is moving into
some new office space in a few months, and now that we&#x27;ll have our own space,
I&#x27;d like to start up a variation on an apprenticeship program at work.  I&#x27;m
still in the early stages of designing this, and I want to gather some input
from the community before deciding how we might run the program.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Production Release Workflow with Git</title>
          <pubDate>Sat, 08 Jan 2011 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/production-release-workflow-with-git/</link>
          <guid>https://johnwilger.com/blog/production-release-workflow-with-git/</guid>
          <description xml:base="https://johnwilger.com/blog/production-release-workflow-with-git/">&lt;p&gt;After growing the &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.projectdx.com&quot;&gt;ProjectDX&lt;&#x2F;a&gt; team from three to
eight software developers, our release process was a complete pain, and it
typically took two to three hours to get a good build on the production branch
(and even then some insidious issues would sneak through). By making a few
changes to our development and acceptance process, we were able to turn it
into a five-minute, low-stress job.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>What it Really Means to be &quot;Agile</title>
          <pubDate>Wed, 15 Dec 2010 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/what-it-really-means-to-be-agile/</link>
          <guid>https://johnwilger.com/blog/what-it-really-means-to-be-agile/</guid>
          <description xml:base="https://johnwilger.com/blog/what-it-really-means-to-be-agile/">&lt;p&gt;Yesterday, Elizabeth Hendrickson posted her &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;testobsessed.com&#x2F;2010&#x2F;12&#x2F;14&#x2F;the-agile-acid-test&#x2F;&quot;&gt;Agile Acid
Test&lt;&#x2F;a&gt; in which she asks
three questions to determine whether or not a team is truly &quot;agile&quot;. There is
also the &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;agilemanifesto.org&#x2F;&quot;&gt;Agile Manifesto&lt;&#x2F;a&gt; which describes the
values that an agile team should adhere to. While there is nothing that I
disagree with in either Elizabeth&#x27;s post or the Agile Manifesto, there
are two simpler questions you can ask that get to the heart of whether or
not your team is agile: &lt;em&gt;Can you react immediately and without panic when
external constraints on your project are changed&lt;&#x2F;em&gt;, and &lt;em&gt;does your team regularly
and frequently review its processes to ensure the answer to the previous
question is always &quot;yes&quot;?&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Retrospective Facilitation</title>
          <pubDate>Tue, 13 Jan 2009 00:00:00 +0000</pubDate>
          <author>Unknown</author>
          <link>https://johnwilger.com/blog/retrospective-facilitation/</link>
          <guid>https://johnwilger.com/blog/retrospective-facilitation/</guid>
          <description xml:base="https://johnwilger.com/blog/retrospective-facilitation/">&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Sprint 1 (11&#x2F;03)&lt;&#x2F;th&gt;&lt;th&gt;Sprint 2 (11&#x2F;24)&lt;&#x2F;th&gt;&lt;th&gt;Alpha (12&#x2F;15)&lt;&#x2F;th&gt;&lt;th&gt;Beta (12&#x2F;29)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;11&#x2F;03&lt;&#x2F;td&gt;&lt;td&gt;11&#x2F;24&lt;&#x2F;td&gt;&lt;td&gt;12&#x2F;15&lt;&#x2F;td&gt;&lt;td&gt;12&#x2F;29&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;I facilitated my second release retrospective with the ProjectDX team yesterday. The first one, back in October, went reasonably well considering I was asked to facilitate at the start of the retrospective and had no time to prepare. However, yesterday&#x27;s retrospective went even better since I knew in advance that I would be facilitating.&lt;&#x2F;p&gt;
&lt;p&gt;The activities we used during the retrospective primarily came from Diana Larsen and Esther Derby&#x27;s book, Agile Retrospectives. We began with the &quot;ESVP&quot; (Explorer, Shopper, Vacationer, Prisoner) activity to gauge the group&#x27;s interest level concerning the work we were doing in the retrospective. Almost everyone in the group identified as an Explorer through an anonymous vote. With this group, I generally trust the outcome, although I did wonder if anyone might have provided the answer they thought they were &quot;supposed&quot; to give.&lt;&#x2F;p&gt;
&lt;p&gt;Next, I asked the team to construct a timeline of events from the past release. Before the retrospective began, I drew the timeline on our whiteboard and divided it into sections for each iteration (our cycle consists of two 3-week sprints followed by a 2-week internal Alpha testing period and a 2-week customer Beta testing period). The result looked something like:&lt;&#x2F;p&gt;
&lt;p&gt;During the retrospective, I asked everyone to take some time to think about any events during that time period that were meaningful to them in any way, write the event on a sticky note, and then place the note on the timeline in approximately the right spot. We spent about 45 minutes generating events, during which time we were free to look back at calendars and email, as well as the commit logs from our SCM. I set a 45-minute time box and mentioned that it was fine for people to take a break if they felt they couldn&#x27;t remember any more events, asking everyone to simply be back on time for the next step.&lt;&#x2F;p&gt;
&lt;p&gt;Instead of specifying that people work in pairs, I chose to let the team self-organize. It was interesting to see how, at first, everyone seemed to work on their own, but as the more obvious events were posted to the board, people started working in groups to come up with more data. At the end of the allotted time, we had a whiteboard full of events.&lt;&#x2F;p&gt;
&lt;p&gt;The area below the timeline was intended for a graph of energy&#x2F;emotion levels, which I&#x27;ll discuss in a moment, but JD Huntington had a great idea to also add a rough bar graph of our commit volume, as reported by GitHub&#x27;s activity graphs, to that area.&lt;&#x2F;p&gt;
&lt;p&gt;After everyone had a last opportunity to look at the board and add any last-minute stickies, I asked everyone to take a turn at the board and use markers to place a blue dot next to any event which they felt positive about and a red dot next to any event that they felt negative about. Then, I asked them to think about their positive and negative energy&#x2F;emotion levels throughout the release and draw a line graph in the bottom section to signify their ups and downs throughout the release cycle. At this point, we looked at each iteration in the cycle and I asked the team to make observations about the data. Using bullet lists at the bottom of the white board, we captured the observations as further data points for analysis.&lt;&#x2F;p&gt;
&lt;p&gt;After a break for lunch, we used the &quot;Patterns &amp;amp; Shifts&quot; activity to generate insights about the data on the time line. We gathered around the white board where I asked each person to mark on the time line any point at which they felt there was a shift or transition of some sort and to draw lines between any events, graph points and observations that they felt were somehow related.&lt;&#x2F;p&gt;
&lt;p&gt;After no one was able to see any more connections, I asked the team to look for any patterns among the connections and the shifts. The first thing we noticed was that we felt like the real shift between iterations was constantly happening about 3 days after the actual time box ended. Rather than get into a long discussion about it when it was noticed, I asked the team to simply keep it in mind as a possible concern during the next phase of the retrospective.&lt;&#x2F;p&gt;
&lt;p&gt;The next observation was that we had a number of connections with long lines spanning two or more iterations. I asked everyone to focus on these connections and look for those where we may have noticed an issue early on but it wasn&#x27;t addressed until much later. In some cases it turned out that while that was the issue, the length of the line just signified that either the work took that long or there was a conscious decision not to address the issue right away.&lt;&#x2F;p&gt;
&lt;p&gt;Reflecting on the other cases led to an interesting discovery, however. Chris noticed that where the lines crossed the bounds of the iterations it was likely signifying a parallel cycle which we were attempting to force into our release cycle: specifically, we have both an explicit product release cycle and an inexplicit customer deployment cycle occurring at the same time.&lt;&#x2F;p&gt;
&lt;p&gt;After we were finished recognizing patterns in the data, it was time to discuss these patterns (and anything else on our minds) and come up with a few action items for the next release cycle. Here I ran the team through the &quot;Circle of Questions&quot; activity. In this activity, the group sits in a circle, and each person takes turns asking a question to the person on their immediate left. The question can be about anything they like (barring anything offensive or attacking), but it&#x27;s helpful to focus on the insights gained during the previous stage of the retrospective. The person to the left answers the question to the best of their ability, and then they ask the person to their left any other question (or the same question if they feel they&#x27;d like a better answer). This continues until the alloted time is up or you have gone around the entire circle twice, whichever comes last. Make sure you go around the complete circle: if some people in the group get more turns to ask or answer a question than others, it can send the wrong message.&lt;&#x2F;p&gt;
&lt;p&gt;The thing that I really like about the Circle of Questions activity is that when you have a mixed group of people—some of whom tend to dominate the conversation while others tend to stay quiet—it gives everyone a chance to ask their questions and have their opinions heard without feeling like the most boisterous people have the only valuable input. We certainly have a mix like that in our case, and this activity worked wonderfully.&lt;&#x2F;p&gt;
&lt;p&gt;In the end, we came away with a number of things to change for the next release cycle. It&#x27;s interesting to note that none of our action items were directly related to the insights generated from the time line. I think the insights are still valuable (and are contained in the notes which we post to our internal wiki), so we may still come back to them in the near future; we were running over on time and had to wrap up even though the discussion could have continued longer.&lt;&#x2F;p&gt;
&lt;p&gt;Before we held this retrospective, I think some of the team members felt that we would not gain much from doing a full-day retrospective this time around. We had made good progress on our action items from the previous retrospective, and the general feeling was that things were going basically well. What could we possibly need to change?&lt;&#x2F;p&gt;
&lt;p&gt;People, please don&#x27;t hold retrospectives only when your team feels like there&#x27;s a problem that needs to be dealt with. If you hold retrospectives on a regular basis, whether you feel like you &quot;need&quot; to or not, you will uncover issues before the become problems. In our case, we managed to uncover a number of issues during our retrospective that could easily have grown into much larger problems if we didn&#x27;t bother to think about them until the effect was obvious.&lt;&#x2F;p&gt;
&lt;p&gt;If you do have obvious issues, then you might choose a different set of activities than what we did for this retrospective. The time line activity is great for discovering potential improvements even when everything seems to be going well, so I suggest giving it a try the next time your team is tempted to skip a retrospective because no one thinks there&#x27;s any huge problems.&lt;&#x2F;p&gt;
</description>
      </item>
    </channel>
</rss>
