PageSourceSearch

https://dwyer.co.za/static/what-is-an-ai-product-engineer.html

html dwyer.co.za collected 2026-10-03 21:03:01 UTC 45,365 bytes, 1,149 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>What is an AI Product Engineer?</title>
7    <meta name="description" content="Everyone is looking for AI Engineers, but that's a really ambiguous term. Here's the role I think actually matters.">
8    
8<script defer data-domain="dwyer.co.za" src="https://plausible.io/js/script.js"></script>
8
9    <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23f5f5fa'/><line x1='16' y1='4' x2='5' y2='26' stroke='%23ccc' stroke-width='1.5'/><line x1='16' y1='4' x2='27' y2='26' stroke='%23ccc' stroke-width='1.5'/><line x1='5' y1='26' x2='27' y2='26' stroke='%23ccc' stroke-width='1.5'/><circle cx='16' cy='4' r='3.5' fill='%23f59e0b'/><circle cx='5' cy='26' r='3.5' fill='%2334d399'/><circle cx='27' cy='26' r='3.5' fill='%2360a5fa'/></svg>">
10    <style>
11        * {
12            margin: 0;
13            padding: 0;
14            box-sizing: border-box;
15        }
16
17        :root {
18            --bg-dark: #f5f5fa;
19            --bg-card: #ffffff;
20            --text-primary: #111118;
21            --text-secondary: #6b6b85;
22            --accent: #0891b2;
23            --accent-glow: rgba(8, 145, 178, 0.15);
24            --gradient-1: #0891b2;
25            --gradient-2: #0369a1;
26            --border: rgba(0, 0, 0, 0.09);
27        }
28
29        body.dark {
30            --bg-dark: #0a0a0f;
31            --bg-card: #12121a;
32            --text-primary: #e8e8ed;
33            --text-secondary: #8b8b9a;
34            --accent-glow: rgba(8, 145, 178, 0.3);
35            --border: rgba(255, 255, 255, 0.08);
36        }
37
38        html {
39            scroll-behavior: smooth;
40        }
41
42        body {
43            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44            background: var(--bg-dark);
45            color: var(--text-primary);
46            line-height: 1.7;
47            overflow-x: hidden;
48        }
49
50        .bg-gradient {
51            position: fixed;
52            top: 0;
53            left: 0;
54            right: 0;
55            bottom: 0;
56            z-index: -1;
57            background:
58                radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.07), transparent),
59                radial-gradient(ellipse 60% 40% at 100% 50%, rgba(168, 85, 247, 0.05), transparent);
60        }
61
62        body.dark .bg-gradient {
63            background:
64                radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.15), transparent),
65                radial-gradient(ellipse 60% 40% at 100% 50%, rgba(168, 85, 247, 0.1), transparent),
66                radial-gradient(ellipse 60% 40% at 0% 80%, rgba(236, 72, 153, 0.08), transparent);
67        }
68
69        /* Hero */
70        .hero {
71            min-height: 100vh;
72            display: flex;
73            flex-direction: column;
74            justify-content: center;
75            align-items: center;
76            text-align: center;
77            padding: 2rem;
78            position: relative;
79        }
80
81        .hero::after {
82            content: '';
83            position: absolute;
84            bottom: 0;
85            left: 0;
86            right: 0;
87            height: 200px;
88            background: linear-gradient(to top, var(--bg-dark), transparent);
89            pointer-events: none;
90        }
91
92        .hero h1 {
93            font-size: clamp(2.5rem, 8vw, 5rem);
94            font-weight: 700;
95            line-height: 1.1;
96            margin-bottom: 1.5rem;
97            color: var(--text-primary);
98        }
99
100        .hero-subtitle {
101            font-size: 1.25rem;
102            color: var(--text-secondary);
103            max-width: 600px;
104            margin-bottom: 2rem;
105        }
106
107        .hero-meta {
108            font-size: 0.875rem;
109            color: var(--text-secondary);
110        }
111
112        .hero-meta a {
113            color: var(--accent);
114            text-decoration: none;
115        }
116
117        .scroll-indicator {
118            position: absolute;
119            bottom: 40px;
120            left: 50%;
121            transform: translateX(-50%);
122            z-index: 10;
123        }
124
125        .scroll-indicator svg {
126            width: 24px;
127            height: 24px;
128            stroke: var(--text-secondary);
129        }
130
131        /* Content */
132        .content {
133            max-width: 800px;
134            margin: 0 auto;
135            padding: 0 2rem 6rem;
136        }
137
138        .section {
139            margin-bottom: 6rem;
140        }
141
142        h2 {
143            font-size: clamp(1.75rem, 4vw, 2.5rem);
144            font-weight: 600;
145            margin-bottom: 1.5rem;
146            color: var(--text-primary);
147        }
148
149        h3 {
150            font-size: 1.35rem;
151            font-weight: 600;
152            margin: 2.5rem 0 1rem;
153            color: var(--text-primary);
154        }
155
156        p {
157            font-size: 1.125rem;
158            color: var(--text-secondary);
159            margin-bottom: 1.5rem;
160        }
161
162        .diagram-label {
163            font-size: 0.75rem;
164            text-transform: uppercase;
165            letter-spacing: 0.12em;
166            color: var(--text-secondary);
167            text-align: center;
168            margin-bottom: 0.75rem;
169        }
170
171        .definition {
172            font-size: 1.2rem;
173            color: var(--text-primary);
174            font-style: italic;
175            margin-bottom: 2.5rem;
176            padding-left: 1rem;
177            border-left: 3px solid var(--accent);
178        }
179
180        ul {
181            margin: 0 0 1.5rem 1.25rem;
182        }
183
184        li {
185            font-size: 1.125rem;
186            color: var(--text-secondary);
187            margin-bottom: 0.5rem;
188            line-height: 1.6;
189        }
190
191        a {
192            color: var(--accent);
193            text-decoration: none;
194            transition: opacity 0.2s;
195        }
196
197        a:hover { opacity: 0.8; }
198
199        /* LinkedIn poll recreation */
200        .li-wrap {
201            margin: 2.5rem 0;
202            display: flex;
203            justify-content: center;
204        }
205
206        .li-card {
207            background: #1b1f23;
208            border: 1px solid #2a2f35;
209            border-radius: 12px;
210            padding: 16px;
211            width: 100%;
212            max-width: 500px;
213            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
214        }
215
216        .li-header {
217            display: flex;
218            align-items: flex-start;
219            gap: 10px;
220            margin-bottom: 14px;
221        }
222
223        .li-avatar {
224            width: 42px;
225            height: 42px;
226            border-radius: 50%;
227            background: linear-gradient(135deg, #0369a1, #0891b2);
228            flex-shrink: 0;
229            display: flex;
230            align-items: center;
231            justify-content: center;
232            font-size: 1.1rem;
233            color: white;
234            font-weight: 700;
235        }
236
237        .li-meta {
238            flex: 1;
239        }
240
241        .li-group {
242            font-size: 0.9rem;
243            font-weight: 600;
244            color: #e2e5e9;
245            line-height: 1.3;
246        }
247
248        .li-author {
249            font-size: 0.8rem;
250            color: #0a66c2;
251            font-weight: 500;
252        }
253
254        .li-time {
255            font-size: 0.78rem;
256            color: #888;
257            margin-top: 1px;
258        }
259
260        .li-close {
261            color: #888;
262            font-size: 1.1rem;
263            cursor: pointer;
264            padding: 0 4px;
265        }
266
267        .li-join {
268            font-size: 0.875rem;
269            font-weight: 600;
270            color: #0a66c2;
271            white-space: nowrap;
272            padding-top: 2px;
273        }
274
275        .li-body {
276            font-size: 0.9rem;
277            color: #a0a8b4;
278            line-height: 1.5;
279            margin-bottom: 14px;
280        }
281
282        .li-body em {
283            color: #e2e5e9;
284            font-style: normal;
285        }
286
287        .li-poll {
288            background: #13171b;
289            border: 1px solid #2a2f35;
290            border-radius: 10px;
291            padding: 14px;
292            margin-bottom: 12px;
293        }
294
295        .li-poll-q {
296            font-size: 0.925rem;
297            font-weight: 700;
298            color: #e2e5e9;
299            margin-bottom: 4px;
300        }
301
302        .li-poll-note {
303            font-size: 0.77rem;
304            color: #888;
305            margin-bottom: 12px;
306        }
307
308        .li-poll-note a {
309            color: #0a66c2;
310            text-decoration: none;
311            font-weight: 500;
312        }
313
314        .li-options {
315            display: flex;
316            flex-direction: column;
317            gap: 8px;
318            margin-bottom: 10px;
319        }
320
321        .li-option {
322            border: 2px solid #0a66c2;
323            border-radius: 999px;
324            padding: 9px 18px;
325            text-align: center;
326            font-size: 0.9rem;
327            font-weight: 600;
328            color: #0a66c2;
329            cursor: pointer;
330            transition: background 0.15s;
331        }
332
333        .li-option:hover {
334            background: rgba(10, 102, 194, 0.1);
335        }
336
337        .li-votes {
338            font-size: 0.78rem;
339            color: #888;
340        }
341
342        .li-footer {
343            display: flex;
344            justify-content: space-around;
345            border-top: 1px solid #2a2f35;
346            padding-top: 10px;
347            margin-top: 4px;
348        }
349
350        .li-action {
351            display: flex;
352            align-items: center;
353            gap: 5px;
354            font-size: 0.82rem;
355            color: #888;
356            cursor: pointer;
357            padding: 4px 8px;
358            border-radius: 6px;
359            transition: background 0.15s, color 0.15s;
360        }
361
362        .li-action:hover {
363            background: rgba(255,255,255,0.05);
364            color: #ccc;
365        }
366
367        .li-note {
368            font-size: 0.75rem;
369            color: #666;
370            text-align: center;
371            margin-top: 10px;
372            padding-top: 8px;
373            border-top: 1px solid #2a2f35;
374        }
375
376        /* Skill cards */
377        .skill-grid {
378            display: grid;
379            grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
380            gap: 1.25rem;
381            margin: 2.5rem 0;
382        }
383
384        .skill-card {
385            background: var(--bg-card);
386            border: 1px solid var(--border);
387            border-radius: 14px;
388            padding: 1.5rem;
389            transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
390        }
391
392        .skill-card:hover {
393            transform: translateY(-3px);
394            border-color: rgba(99, 102, 241, 0.35);
395            box-shadow: 0 12px 40px -12px var(--accent-glow);
396        }
397
398        .skill-icon {
399            width: 44px;
400            height: 44px;
401            background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
402            border-radius: 12px;
403            display: flex;
404            align-items: center;
405            justify-content: center;
406            font-size: 1.4rem;
407            margin-bottom: 1rem;
408        }
409
410        .skill-card h4 {
411            font-size: 1.05rem;
412            font-weight: 600;
413            color: var(--text-primary);
414            margin-bottom: 0.5rem;
415        }
416
417        .skill-card p {
418            font-size: 0.95rem;
419            margin: 0;
420        }
421
422        /* Callout */
423        .callout {
424            background: var(--bg-card);
425            border: 1px solid rgba(99, 102, 241, 0.3);
426            border-left: 3px solid var(--accent);
427            border-radius: 12px;
428            padding: 1.5rem 1.75rem;
429            margin: 2.5rem 0;
430        }
431
432        .callout p {
433            margin: 0;
434            color: var(--text-primary);
435            font-size: 1.1rem;
436            font-style: italic;
437        }
438
439        /* Criteria list */
440        .criteria-list {
441            display: flex;
442            flex-direction: column;
443            gap: 1rem;
444            margin: 2rem 0;
445        }
446
447        .criteria-item {
448            display: flex;
449            align-items: flex-start;
450            gap: 1rem;
451            background: var(--bg-card);
452            border: 1px solid var(--border);
453            border-radius: 12px;
454            padding: 1.25rem 1.5rem;
455            transition: border-color 0.3s ease;
456        }
457
458        .criteria-item:hover {
459            border-color: rgba(99, 102, 241, 0.3);
460        }
461
462        .criteria-check {
463            width: 28px;
464            height: 28px;
465            background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
466            border-radius: 8px;
467            display: flex;
468            align-items: center;
469            justify-content: center;
470            flex-shrink: 0;
471            margin-top: 2px;
472        }
473
474        .criteria-check svg {
475            width: 14px;
476            height: 14px;
477            stroke: white;
478            stroke-width: 2.5;
479            fill: none;
480        }
481
482        .criteria-text strong {
483            display: block;
484            color: var(--text-primary);
485            font-size: 1rem;
486            margin-bottom: 0.25rem;
487        }
488
489        .criteria-text span {
490            color: var(--text-secondary);
491            font-size: 0.95rem;
492        }
493
494        /* Divider */
495        .divider {
496            height: 1px;
497            background: var(--border);
498            margin: 4rem 0;
499        }
500
501        /* Footer */
502        .footer {
503            text-align: center;
504            padding: 4rem 2rem;
505            color: var(--text-secondary);
506            font-size: 0.9rem;
507        }
508
509        .footer a {
510            color: var(--accent);
511        }
512
513        @media (max-width: 600px) {
514            .skill-grid {
515                grid-template-columns: 1fr;
516            }
517        }
518
519        /* Venn triangle diagram */
520        .venn-wrap {
521            margin: 2.5rem 0;
522            background: #f0f0f8;
523            border-radius: 16px;
524            border: 1px solid var(--border);
525            overflow: hidden;
526            user-select: none;
527        }
528
529        body.dark .venn-wrap {
530            background: #060610;
531        }
532
533        .venn-wrap svg {
534            display: block;
535            width: 100%;
536            max-width: 480px;
537            margin: 0 auto;
538        }
539
540        .venn-node {
541            cursor: pointer;
542        }
543
544        .venn-node circle.outer {
545            transition: fill 0.3s ease, stroke 0.3s ease;
546        }
547
548        .venn-node circle.inner {
549            transition: fill 0.3s ease;
550        }
551
552        .venn-node text {
553            transition: fill 0.3s ease;
554            font-family: 'Inter', -apple-system, sans-serif;
555            font-weight: 600;
556            pointer-events: none;
557        }
558
559        .venn-description-wrap {
560            min-height: 72px;
561            display: flex;
562            align-items: center;
563            justify-content: center;
564            padding: 1rem 2rem 1.25rem;
565            border-top: 1px solid rgba(0,0,0,0.07);
566        }
567
568        body.dark .venn-description-wrap {
569            border-top-color: rgba(255,255,255,0.05);
570        }
571
572        .venn-description {
573            text-align: center;
574            font-size: 1rem;
575            transition: opacity 0.3s ease;
576            max-width: 480px;
577        }
578
579        .venn-description .combo-label {
580            font-size: 0.75rem;
581            text-transform: uppercase;
582            letter-spacing: 0.12em;
583            color: #6366f1;
584            margin-bottom: 0.35rem;
585        }
586
587        body.dark .venn-description .combo-label {
588            color: #22d3ee;
589        }
590
591        .venn-description .combo-text {
592            font-size: 1rem;
593            color: #111118;
594            line-height: 1.5;
595        }
596
597        body.dark .venn-description .combo-text {
598            color: #e8e8ed;
599        }
600
601        .venn-description .hint {
602            font-size: 0.85rem;
603            color: #999;
604        }
605
606        body.dark .venn-description .hint {
607            color: #6b6b85;
608        }
609
610        .venn-controls {
611            display: flex;
612            justify-content: center;
613            padding: 0 0 1.1rem;
614        }
615
616        .venn-play-btn {
617            display: flex;
618            align-items: center;
619            gap: 0.5rem;
620            background: none;
621            border: 1px solid rgba(0,0,0,0.15);
622            border-radius: 999px;
623            padding: 0.4rem 1rem;
624            color: #888;
625            font-size: 0.78rem;
626            font-family: inherit;
627            cursor: pointer;
628            transition: border-color 0.2s, color 0.2s, background 0.2s;
629        }
630
631        .venn-play-btn:hover {
632            border-color: rgba(0,0,0,0.3);
633            color: #333;
634        }
635
636        body.dark .venn-play-btn {
637            border-color: rgba(255,255,255,0.12);
638            color: #8b8b9a;
639        }
640
641        body.dark .venn-play-btn:hover {
642            border-color: rgba(255,255,255,0.25);
643            color: #e8e8ed;
644            background: rgba(255,255,255,0.04);
645        }
646
647        .venn-play-btn svg {
648            width: 13px;
649            height: 13px;
650            fill: currentColor;
651        }
652
653        /* Dark mode overrides for hardcoded li-card colours */
654        body.dark .li-card   { background: #1b1f23; border-color: #2a2f35; }
655        body.dark .li-body   { color: #a0a8b4; }
656        body.dark .li-group  { color: #e2e5e9; }
657        body.dark .li-time   { color: #888; }
658        body.dark .li-poll   { background: #13171b; border-color: #2a2f35; }
659        body.dark .li-poll-q { color: #e2e5e9; }
660        body.dark .li-footer { border-top-color: #2a2f35; }
661        body.dark .li-note   { color: #666; border-top-color: #2a2f35; }
662        body.dark .li-action { color: #888; }
663        body.dark .li-close  { color: #888; }
664        body.dark .li-votes  { color: #888; }
665        body.dark .li-poll-note { color: #888; }
666
667        /* Light mode li-card */
668        .li-card   { background: #f8f8fe; border-color: #e0e0ee; }
669        .li-body   { color: #555570; }
670        .li-group  { color: #111120; }
671        .li-time   { color: #aaa; }
672        .li-poll   { background: #efeffa; border-color: #dcdcee; }
673        .li-poll-q { color: #111120; }
674        .li-footer { border-top-color: #e0e0ee; }
675        .li-note   { color: #bbb; border-top-color: #e0e0ee; }
676        .li-action { color: #999; }
677        .li-close  { color: #bbb; }
678        .li-votes  { color: #aaa; }
679        .li-poll-note { color: #aaa; }
680
681        /* Mode toggle button */
682        .mode-btn {
683            position: fixed;
684            top: 1.1rem;
685            right: 1.25rem;
686            z-index: 100;
687            background: var(--bg-card);
688            border: 1px solid var(--border);
689            border-radius: 999px;
690            padding: 0.4rem 0.9rem;
691            font-size: 0.78rem;
692            font-family: inherit;
693            color: var(--text-secondary);
694            cursor: pointer;
695            display: flex;
696            align-items: center;
697            gap: 0.4rem;
698            transition: background 0.2s, border-color 0.2s, color 0.2s;
699            box-shadow: 0 1px 6px rgba(0,0,0,0.07);
700        }
701
702        .mode-btn:hover {
703            color: var(--text-primary);
704            border-color: var(--accent);
705        }
706    </style>
707</head>
708<body>
709    <button class="mode-btn" id="modeBtn" onclick="toggleMode()">
710        <span id="modeIcon">🌙</span><span id="modeLabel">Dark</span>
711    </button>
712
713    <div class="bg-gradient"></div>
714
715
716    <section class="hero">
717        <h1>What is an AI<br>Product Engineer?</h1>
718        <p class="hero-subtitle">Everyone is looking for AI-pilled Product Engineers, but what does that actually mean?</p>
719        <p class="hero-meta">By <a href="/">Gareth Dwyer</a> &middot; March 2026</p>
720        <div class="scroll-indicator">
721            <svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
722                <polyline points="6 9 12 15 18 9"></polyline>
723            </svg>
724        </div>
725    </section>
726
727    <main class="content">
728
729        <div class="section">
730            <h2>Product x Engineering x AI</h2>
731            <p class="definition"><strong>AI Product Engineer (noun):</strong> Someone who ships delightful, correct solutions to real problems fast.</p>
732            <p>The crude distinction has always been: Product decides the <em>what</em>, Engineering decides the <em>how</em>.</p>
733
734            <p>Traditionally, companies have separated the functions. Product people talk to customers, actively use the product, and tell the engineers what to build. The engineers build systems that are scalable and correct.</p>
735
736            <p>Some companies — famously Posthog — hire Product Engineers: people who can do both. This leads to big efficiency gains as you remove the communication overhead, and often a lot of conflict and personality clashes at the same time.</p>
737
738            <p>This hasn't changed. People who understand both what would be valuable to a customer and how to create it are valuable but hard to find.</p>
739
740            <p>AI lets Product people and Engineers go faster:</p>
741            <ul>
742                <li>Product people with AI can build demos fast, but they have bugs and don't scale</li>
743                <li>Engineers with AI can build correct systems fast, but they don't solve customer problems or create Shareholder Value™</li>
744            </ul>
745
746            <p>
746But Product people without engineering skills are still limited. The "80% done" demo falls apart and needs to be rebuilt from scratch.</p>
747
748            <p>And Engineers without product skills are building better software forges to let AI build AI-building tools faster and faster, but others don't value or understand the solutions and they have rough edges.</p>
749
750            <p>Combine all three and you get someone who can produce customer value fast and correctly.</p>
751
752            <p class="diagram-label">Build your own AI Product Engineer</p>
753
754            <!-- Interactive triangle diagram -->
755            <div class="venn-wrap">
756                <svg id="vennSvg" viewBox="0 0 360 310" xmlns="http://www.w3.org/2000/svg">
757                    <!-- Equilateral triangle: base 200, height≈173, centred at x=180 -->
758                    <!-- AI: (180,90)  Product: (80,263)  Engineer: (280,263) -->
759                    <line id="line-ai-product"  x1="180" y1="90"  x2="80"  y2="263" stroke="#c0c0d4" stroke-width="1.2"/>
760                    <line id="line-ai-engineer" x1="180" y1="90"  x2="280" y2="263" stroke="#c0c0d4" stroke-width="1.2"/>
761                    <line id="line-prod-eng"    x1="80"  y1="263" x2="280" y2="263" stroke="#c0c0d4" stroke-width="1.2"/>
762
763                    <!-- AI node (top) — label above -->
764                    <g id="node-ai" class="venn-node" onclick="toggleNode('ai')">
765                        <circle class="outer" cx="180" cy="90"  r="20" fill="#e4e4f0" stroke="#b8b8d0" stroke-width="1.5"/>
766                        <circle class="inner" cx="180" cy="90"  r="8"  fill="#b8b8d0"/>
767                        <text x="180" y="57" text-anchor="middle" fill="#999" font-size="13">AI</text>
768                    </g>
769
770                    <!-- Product node (bottom left) — label below -->
771                    <g id="node-product" class="venn-node" onclick="toggleNode('product')">
772                        <circle class="outer" cx="80"  cy="263" r="20" fill="#e4e4f0" stroke="#b8b8d0" stroke-width="1.5"/>
773                        <circle class="inner" cx="80"  cy="263" r="8"  fill="#b8b8d0"/>
774                        <text x="80"  y="300" text-anchor="middle" fill="#999" font-size="13">Product</text>
775                    </g>
776
777                    <!-- Engineer node (bottom right) — label below -->
778                    <g id="node-engineer" class="venn-node" onclick="toggleNode('engineer')">
779                        <circle class="outer" cx="280" cy="263" r="20" fill="#e4e4f0" stroke="#b8b8d0" stroke-width="1.5"/>
780                        <circle class="inner" cx="280" cy="263" r="8"  fill="#b8b8d0"/>
781                        <text x="280" y="300" text-anchor="middle" fill="#999" font-size="13">Engineer</text>
782                    </g>
783                </svg>
784                <div class="venn-description-wrap">
785                    <div class="venn-description" id="vennDesc">
786                        <div class="hint">Click a node to explore the skills</div>
787                    </div>
788                </div>
789                <div class="venn-controls">
790                    <button class="venn-play-btn" id="vennPlayBtn" onclick="togglePlay()">
791                        <svg id="vennPlayIcon" viewBox="0 0 16 16"><path d="M3 2.5l10 5.5-10 5.5V2.5z"/></svg>
792                        <span id="vennPlayLabel">Play</span>
793                    </button>
794                </div>
795            </div>
796
797            <div class="skill-grid">
798                <div class="skill-card">
799                    <div class="skill-icon">🤖</div>
800                    <h4>AI folk</h4>
801                    <p>Heavy users of Claude Code or similar for months. Burnt billions of tokens to figure out how to use new tools effectively.</p>
802                </div>
803                <div class="skill-card">
804                    <div class="skill-icon">🎯</div>
805                    <h4>Product folk</h4>
806                    <p>Understand the customer's problem. Creates delightful solutions. Communicates.</p>
807                </div>
808                <div class="skill-card">
809                    <div class="skill-icon">⚙️</div>
810                    <h4>Engineers</h4>
811                    <p>Efficiently and effectively create correct, scalable systems.</p>
812                </div>
813            </div>
814        </div>
815
816        <div class="section">
817            <h2>What to look for in an AI Product Engineer (or how to become one)</h2>
818
819            <p>Some of the best AI Product Engineers I know are, or were, 'indiehackers'. They don't always have management skills, or sometimes they just prefer to work alone. But because they can build things that people value, they often find far higher returns by selling their own products rather than becoming employees to help someone else.</p>
820
821            <p>These people are hard to categorise because they tend to be the mavericks and misfits of the world, coming from varied backgrounds and skill-sets. But there are some things they tend to have in common.</p>
822
823            <h3>Great at communication</h3>
824            <p>Usually they've written millions of words in blog posts, explaining what they're learning and what they're selling to anyone who will listen.</p>
825
826            <h3>Disciplined</h3>
827            <p>They've honed their craft for hours every day for years or decades. There are many wannabes who look similar on the surface but never stick with any project for more than a few weeks or months at a time, and often only w
827ork on it sporadically. They iterate and refine on what they build, not just building it once and then losing interest.</p>
828
829            <h3>They actually ship</h3>
830            <p>Discipline is about doing the reps, but putting real things in public for others to use is a different muscle. Product Engineers haven't just built the side projects, they've published them and gotten some traction from real users.</p>
831
832            <h3>They care</h3>
833            <p>It bothers them if something they built is broken or has an issue. They've carefully used and refined the UX of their own products so they make sense to others too. If you email them with a sensible suggestion for improvement or to point out an issue, you'll often see a fix published within hours or days.</p>
834
835            <h3>Systems thinking</h3>
836            <p>Product Engineers can see how a new tool fits into a broader ecosystem, what problems it solves for individual products but also what means, where it moves constraints and bottlenecks, and what downstream effects it will have.</p>
837
838            <h3>Open-minded</h3>
839            <p>They're always happy to listen for a few minutes longer than average if you say something they disagree with. They love experimenting with new tools, technology, and ideas. They don't jump to the most obvious solution, rather considering different options before settling on a path forwards.</p>
840
841            <h3>Generalists</h3>
842            <p>They know a bit of marketing, sales, product, engineering, customer support. Not married to a specific tech stack.</p>
843
844            <h3>Enjoy helping others</h3>
845            <p>They actually like it when customers come to them with problems — unlike the stereotypical hardcore engineer who hates being distracted from building.</p>
846
847            <h2>Building Product skills from Engineering ones</h2>
848            <p>You can become a better AI Product Engineer by trying out new tools and seeing how they can help you. You can have — or at least pretend to have — more respect for non-engineers and ask them about their problems without assuming you can instantly solve them. Be less defensive if someone tells you something you built doesn't work or doesn't work well. Learn more about what makes good UX and how to delight users. Learn more about metrics and how to present them.</p>
849
850            <h3>Understand the customer</h3>
851            <p>You need to deeply grok what a customer actually needs. This might be different from what they say they need. What are they actually trying to achieve and why can't they do it with existing offerings? What do they value? What are they willing to pay for? What makes them <strong>delighted</strong>?</p>
852
853            <h3>Understand what's possible</h3>
854            <p>You need to understand what's feasible. If you think you can promise the earth with no tradeoffs and then throw it over the fence to an engineering team to figure out how to build, you're in sales, not product. That doesn't mean knowing every detail of how something will be built, but understanding what's technically challenging and what isn't — and what's impossible — is important for product too.</p>
855
856            <h3>Understand different languages</h3>
857            <p>You have to be able to speak a bit of engineering, a bit of sales, a bit of marketing, a bit of exec, a lot of whatever language your customer talks. These often sound the same but are actually quite different, so you can cause confusion and have things 'lost in translation' if you don't know the terminology of each profile.</p>
858
859            <h3>Be good at UX</h3>
860            <p>Usually this means actually play-acting as the user: clicking through interfaces, reading documentation, understanding and eliminating every friction point, creating delight.</p>
861
862            <h3>Understand the business</h3>
863            <p>You have budgets in terms of people, time, money. Don't spend $100 to create $5.</p>
864
865            <h2>Building Engineering skills from Product ones</h2>
866            <p>You can become a better AI Product Engineer by understanding fundamentals. Learn how to use a terminal. Ask Claude Code to help you bootstrap it. Learn about constraints like memory. Learn about data models. Learn some basic SQL. Learn some basic networking. Learn how to debug — how to form a hypothesis about what's wrong and test it.</p>
867
868            <h3>Efficiency</h3>
869            <p>
869In civil engineering, it's not difficult to build a bridge that stays up. It's difficult to build a bridge that <em>only just</em> stays up — without wasting resources. In software engineering this is not as widely understood. You shouldn't be firing up $1000/month K8s clusters to serve 1000 users.</p>
870
871            <h3>Effectiveness</h3>
872            <p>The opposite of efficiency in some ways. You should know when to waste one resource in order to preserve a more valuable one — waste money to save time, or waste time to save money. You get things done without getting bogged down in details, hyperfocusing on one metric above all others, or letting the fact that you don't have all the information about a goal stop you from moving towards it.</p>
873
874            <h3>Data modelling</h3>
875            <p>Getting the schema right is foundational and expensive to undo. Before writing a line of code, an engineer should be able to sketch the entities, their relationships, and where the boundaries are. Correcting a broken data model six months into a product is a rewrite, not a refactor.</p>
876
877            <h3>Knowing when to abstract</h3>
878            <p>Over-engineering is as dangerous as under-engineering. The skill is recognising the seam where a good abstraction will pay for itself many times over — and ignoring the temptation to create one everywhere else. Three instances of similar code is usually fine. A premature abstraction that doesn't quite fit is a tax on every future change.</p>
879
880            <h3>Observability</h3>
881            <p>Systems fail in ways you didn't anticipate. The engineers who recover fast are the ones who instrumented their systems before things broke, not after. Logs, traces, and metrics aren't overhead — they're how you know what's actually happening versus what you think is happening.</p>
882
883            <h3>Understanding tradeoffs</h3>
884            <p>There is no free lunch. Consistency vs. availability. Speed vs. correctness. Flexibility vs. simplicity. A good engineer doesn't pretend these tensions don't exist — they make them explicit, decide consciously, and document why.</p>
885
886            <h2>Building AI Skills</h2>
887
888            <p>Whether you're more Product or more Engineering, you can always up your AI game. It's new. Nobody knows how to hold it yet, and it's changing every day. Choose your own adventure but this is what I'd recommend.</p>
889
890            <h3>Try Claude Code in yolo mode</h3>
891            <p>You're right to be concerned but you don't know the true power of AI until you've let Claude Code rip with <code>--dangerously-skip-permissions</code>. With good prompting it's safer than you think, and as long as it doesn't have access to anything critically production, it's worth finding a few footguns the hard way in order to learn how to use it effectively.</p>
892
893            <h3>Try things you don't think it can do</h3>
894            <p>Regularly ask the AI to do something that you think it has a 90% chance of failing at or doing badly. You might be surprised.</p>
895
896            <h3>Ask it how it did things</h3>
897            <p>Once it's done something well, it's tempting to move on. But especially in cases where it surprised you, ask it how it managed.</p>
898
899            <h3>Read a lot of output</h3>
900            <p>Agents produce a lot of output and it's very tempting to just skip over it. But reading it can help understand how it works — what kind of searches is it doing, how is it exploring your file system or codebase.</p>
901
902            <h3>Write longer prompts</h3>
903            <p>Often you give an agent a 5 word prompt and it somehow knows exactly what you meant. The next time it doesn't, and you assume they're limiting the resources over at Anthropic or giving you a dumber model to save costs. In reality, it'll do what it thinks you want and sometimes it gets that wrong if you weren't clear enough. Try writing a 1000 word prompt. Try writing a prompt into a file. It feels like 'writing' is a waste of time and you're tempted to just ask the agent to write its own prompts, but if there's information only in your head that the agent needs, then typing it out — or dictating if you're a slow typer — is still the most efficient way to get that information to the agent.</p>
904        </div>
905
906        <div class="divider"></div>
907
908    </main>
909
910    <footer class="footer">
911        <p><a href="/">
911gareth dwyer</a> &middot; dwyer.co.za</p>
912    </footer>
913
914    
914<script>
915        // Venn triangle
916        const state = { ai: false, product: false, engineer: false };
917
918        const nodeConfig = {
919            ai: {
920                el: document.getElementById('node-ai'),
921                cx: 180, cy: 90,
922                outerR: 20, innerR: 8,
923                activeOuter: '#fef3c7',
924                activeOuterStroke: '#f59e0b',
925                activeInner: '#f59e0b',
926                activeText: '#92400e',
927                activeDarkText: '#fcd34d',
928                glowColorDark: 'rgba(245,158,11,0.3)',
929                glowColorLight: 'rgba(245,158,11,0.2)',
930                label: 'AI'
931            },
932            product: {
933                el: document.getElementById('node-product'),
934                cx: 80, cy: 263,
935                outerR: 20, innerR: 8,
936                activeOuter: '#d4f0e4',
937                activeOuterStroke: '#34d399',
938                activeInner: '#34d399',
939                activeText: '#1a7a50',
940                activeDarkText: '#6ee7b7',
941                glowColorDark: 'rgba(52,211,153,0.3)',
942                glowColorLight: 'rgba(52,211,153,0.15)',
943                label: 'Product'
944            },
945            engineer: {
946                el: document.getElementById('node-engineer'),
947                cx: 280, cy: 263,
948                outerR: 20, innerR: 8,
949                activeOuter: '#d4e8ff',
950                activeOuterStroke: '#60a5fa',
951                activeInner: '#60a5fa',
952                activeText: '#1a5a9a',
953                activeDarkText: '#93c5fd',
954                glowColorDark: 'rgba(96,165,250,0.3)',
955                glowColorLight: 'rgba(96,165,250,0.15)',
956                label: 'Engineer'
957            }
958        };
959
960        function isDark() { return document.body.classList.contains('dark'); }
961
962        function inactiveAttrs() {
963            return isDark()
964                ? { outer: '#1a1a2e', stroke: '#3a3a5a', inner: '#3a3a5a', text: '#555' }
965                : { outer: '#e4e4f0', stroke: '#b8b8d0', inner: '#b8b8d0', text: '#999' };
966        }
967
968        function lineInactiveStroke() { return isDark() ? '#2a2a3a' : '#c0c0d4'; }
969        function lineActiveStroke()   { return isDark() ? '#5a5a7a' : '#8888aa'; }
970
971        const descriptions = {
972            '':               { label: '', text: '<span class="hint">Click a node to explore the skills</span>' },
973            'ai':             { label: 'AI', text: 'Go fast' },
974            'product':        { label: 'Product', text: 'Understand what customers want' },
975            'engineer':       { label: 'Engineer', text: 'Build correct systems' },
976            'ai+product':     { label: 'AI &times; Product', text: 'Build demos fast that customers value' },
977            'ai+engineer':    { label: 'AI &times; Engineer', text: 'Build correct systems fast' },
978            'product+engineer': { label: 'Product &times; Engineer', text: 'Build correct systems slowly that customers value' },
979            'ai+product+engineer': { label: 'AI &times; Product &times; Engineer', text: 'Build correct systems fast that customers value' }
980        };
981
982        const lineConfig = {
983            'ai+product':     document.getElementById('line-ai-product'),
984            'ai+engineer':    document.getElementById('line-ai-engineer'),
985            'product+engineer': document.getElementById('line-prod-eng')
986        };
987
988        function getKey() {
989            return ['ai', 'product', 'engineer'].filter(k => state[k]).join('+');
990        }
991
992        function updateNode(id) {
993            const cfg = nodeConfig[id];
994            const on = state[id];
995            const el = cfg.el;
996            const outer = el.querySelector('circle.outer');
997            const inner = el.querySelector('circle.inner');
998            const text = el.querySelector('text');
999            const dark = isDark();
1000
1001            if (on) {
1002                outer.setAttribute('fill', cfg.activeOuter);
1003                outer.setAttribute('stroke', cfg.activeOuterStroke);
1004                outer.setAttribute('stroke-width', '2');
1005                inner.setAttribute('fill', cfg.activeInner);
1006                text.setAttribute('fill', dark ? cfg.activeDarkText : cfg.activeText);
1007
1008                let glow = el.querySelector('circle.glow');
1009                if (!glow) {
1010                    glow = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1011                    glow.setAttribute('class', 'glow');
1012                    glow.setAttribute('cx', cfg.cx);
1013                    glow.setAttribute('cy', cfg.cy);
1014                    glow.setAttribute('r', cfg.outerR + 16);
1015                    glow.setAttribute('pointer-events', 'none');
1016                    el.insertBefore(glow, el.firstChild);
1017                }
1018                glow.setAttribute('fill', dark ? cfg.glowColorDark : cfg.glowColorLight);
1019                glow.style.display = '';
1020            } else {
1021                const ia = inactiveAttrs();
1022                outer.setAttribute('fill', ia.outer);
1023                outer.setAttribute('stroke', ia.stroke);
1024                outer.setAttribute('stroke-width', '1.5');
1025                inner.setAttribute('fill', ia.inner);
1026                text.setAttribute('fill', ia.text);
1027                const glow = el.querySelector('circle.glow');
1028                if (glow) glow.style.display = 'none';
1029            }
1030        }
1031
1032        function updateLines() {
1033            Object.entries(lineConfig).forEach(([pair, lineEl]) => {
1034                const [a, b] = pair.split('+');
1035                const active = state[a] && state[b];
1036                lineEl.setAttribute('stroke', active ? lineActiveStroke() : lineInactiveStroke());
1037                lineEl.setAttribute('stroke-width', active ? '1.8' : '1.2');
1038            });
1039        }
1040
1041        function updateAllNodes() {
1042            Object.keys(nodeConfig).forEach(updateNode);
1043            updateLines();
1044        }
1045
1046        function updateDescription() {
1047            const key = getKey();
1048            const desc = descriptions[key] || descriptions[''];
1049            const el = document.getElementById('vennDesc');
1050            el.style.opacity = '0';
1051            setTimeout(() => {
1052                if (desc.label) {
1053                    el.innerHTML = `<div class="combo-label">${desc.label}</div><div class="combo-text">${desc.text}</div>`;
1054                } else {
1055                    el.innerHTML = desc.text;
1056                }
1057                el.style.opacity = '1';
1058            }, 150);
1059        }
1060
1061        function toggleNode(id) {
1062            // If playing, pause and hand control to user
1063            if (isPlaying) pausePlay();
1064            state[id] = !state[id];
1065            updateNode(id);
1066            updateLines();
1067            updateDescription();
1068        }
1069
1070        // --- Auto-play ---
1071        const sequence = [
1072            { ai: true,  product: false, engineer: false },
1073            { ai: false, product: true,  engineer: false },
1074            { ai: false, product: false, engineer: true  },
1075            { ai: true,  product: true,  engineer: false },
1076            { ai: true,  product: false, engineer: true  },
1077            { ai: false, product: true,  engineer: true  },
1078            { ai: true,  product: true,  engineer: true  },
1079            { ai: false, product: false, engineer: false }, // reset
1080        ];
1081
1082        let isPlaying = false;
1083        let seqIndex = 0;
1084        let playTimer = null;
1085        const STEP_MS = 3600;
1086
1087        function applySequenceStep(step) {
1088            Object.keys(step).forEach(id => {
1089                state[id] = step[id];
1090                updateNode(id);
1091            });
1092            updateLines();
1093            updateDescription();
1094        }
1095
1096        function advanceSequence() {
1097            applySequenceStep(sequence[seqIndex]);
1098            seqIndex = (seqIndex + 1) % sequence.length;
1099            playTimer = setTimeout(advanceSequence, STEP_MS);
1100        }
1101
1102        function startPlay() {
1103            isPlaying = true;
1104            document.getElementById('vennPlayIcon').innerHTML = '<rect x="3" y="2" width="4" height="12"/><rect x="9" y="2" width="4" height="12"/>';
1105            document.getElementById('vennPlayLabel').textContent = 'Pause';
1106            advanceSequence();
1107        }
1108
1109        function pausePlay() {
1110            isPlaying = false;
1111            clearTimeout(playTimer);
1112            playTimer = null;
1113            document.getElementById('vennPlayIcon').innerHTML = '<path d="M3 2.5l10 5.5-10 5.5V2.5z"/>';
1114            document.getElementById('vennPlayLabel').textContent = 'Play';
1115        }
1116
1117        function togglePlay() {
1118            if (isPlaying) {
1119                pausePlay();
1120            } else {
1121                seqIndex = 0;
1122                startPlay();
1123            }
1124        }
1125
1126        // Dark mode toggle
1127        function toggleMode() {
1128            const dark = document.body.classList.toggle('dark');
1129            document.getElementById('modeIcon').textContent  = dark ? '☀️' : '🌙';
1130            document.getElementById('modeLabel').textContent = dark ? 'Light' : 'Dark';
1131            updateAllNodes();
1132        }
1133
1134        // Init node colours for light mode default
1135        updateAllNodes();
1136
1137        // Auto-start on first scroll into view
1138        const vennObserver = new IntersectionObserver((entries) => {
1139            entries.forEach(e => {
1140                if (e.isIntersecting && !isPlaying && seqIndex === 0) {
1141                    startPlay();
1142                    vennObserver.disconnect();
1143                }
1144            });
1145        }, { threshold: 0.5 });
1146        vennObserver.observe(document.getElementById('vennSvg'));
1147    </script>
1147
1148</body>
1149</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.