Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 074fa08bb7 | |||
| 69d74be6ba |
+36
-56
@@ -15,60 +15,42 @@
|
||||
<meta property="og:description" content="Dev logs, behind-the-scenes, and lessons learned from building games, tools, and platform at Tinqs Studio.">
|
||||
<meta property="og:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — index styles ── */
|
||||
/* ── Self-contained index styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.blog-header {
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
.blog-list {
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
/* ── Section label kicker ── */
|
||||
.section-label {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -81,16 +63,15 @@
|
||||
padding: 60px 24px 32px;
|
||||
}
|
||||
|
||||
/* ── Gradient index title — lime → violet ── */
|
||||
/* ── Gradient index title ── */
|
||||
.blog-header__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.4rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@@ -115,37 +96,36 @@
|
||||
.blog-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
background: var(--c-bg-raised);
|
||||
background: #0c1119;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
border-color: var(--c-lime);
|
||||
border-color: var(--c-accent);
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.blog-card__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.blog-card__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
color: var(--c-fg);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3;
|
||||
color: var(--c-text);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -158,13 +138,13 @@
|
||||
|
||||
/* ── Read link accent ── */
|
||||
.blog-card__read {
|
||||
color: var(--c-lime);
|
||||
font-size: 0.875rem;
|
||||
color: var(--c-blue);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.blog-card:hover .blog-card__read {
|
||||
color: var(--c-violet);
|
||||
color: var(--c-purple);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -183,7 +163,7 @@
|
||||
<a href="pi-flow-native-brain" class="blog-card">
|
||||
<span class="blog-card__date">4 June 2026</span>
|
||||
<h2 class="blog-card__title">How Pi Agents Build, Test, and Ship Game Code with Oracle-Backed Flows</h2>
|
||||
<p class="blog-card__excerpt">A flow spawns, agents fan out through five oracle gates, the game-builder fixes 19 red tests while vision judges check the live game — and it all runs as one autonomous flow.</p>
|
||||
<p class="blog-card__excerpt">We type a slash command, agents fan out through five oracle gates, the game-builder fixes 19 red tests while vision judges check the live game — and it all runs as one autonomous flow.</p>
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
{{CARDS}}
|
||||
|
||||
|
Before
After
|
+52
-67
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
Before
After
|
+87
-133
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -277,74 +262,43 @@
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">25 May 2026</span>
|
||||
<h1 class="post__title">What an Agent Harness Is and Why Game Dev Needs One</h1>
|
||||
<p class="post__lead">Open Claude or ChatGPT right now and ask it to review your last PR. It'll say "I don't have access to your repository." Ask it to take a screenshot of your game. It'll say "I can't interact with your operating system." Ask it what you were working on yesterday. It'll say "I don't have memory of previous conversations."
|
||||
|
||||
A raw AI model is a brain without hands, eyes, or memory. An <strong>agent harness</strong> is the layer that gives it all three — plus identity, tools, and guardrails. And game development needs one that understands binary assets, visual pipelines, and spatial systems.
|
||||
|
||||
## What a harness provides
|
||||
|
||||
Every agent harness, regardless of domain, needs five things:
|
||||
|
||||
<strong>Identity.</strong> Who the agent is, what it values, how it should behave. Not "you are a helpful assistant" — that's generic and unmoored. A soul file that says "you're working on Ariki, a survival colony sim. The team is four people. Never push to main without review. Prefer existing conventions." Identity creates consistency across sessions.
|
||||
|
||||
<strong>Memory.</strong> What happened last session. What decisions were made. What failed and why. Without memory, every conversation is a cold start — "let me explain the project..." Memory stored as markdown in git means it's version-controlled, diffable, and human-readable. When something goes wrong, you <code>git log</code> instead of debugging a vector database.
|
||||
|
||||
<strong>Tools.</strong> What the agent can actually do beyond generating text. A CLI that takes screenshots, checks service health, and loads project context. API wrappers for git, CI, image generation. Without tools, the agent is a very articulate oracle that can't touch anything.
|
||||
|
||||
<strong>Context.</strong> Which project this is. Who's asking. What machine they're on. What services are reachable. A single CLI call — <code>tinqs identity</code> — returns all of this in 100ms. No re-reading the README. No "what repo are we in?"
|
||||
|
||||
<strong>Guardrails.</strong> What the agent must never do. No merging to main without review. No pushing to public repos without approval. No running destructive commands. The harness enforces these at the platform layer, not in the prompt. Prompts can be ignored. Platform gates cannot.
|
||||
|
||||
## Why generic harnesses fail for game dev
|
||||
|
||||
LangChain, CrewAI, and AutoGen are built for web apps. They assume text-in, text-out. Game development is different in ways that break those assumptions:
|
||||
|
||||
<strong>Assets are binary.</strong> A web PR is a text diff. A game PR is a 150MB GLB file with textures, rigging, and animations. You can't review it without seeing it. Our harness renders 3D models in the browser during code review — rotate, zoom, check materials. The artist pushes, the lead inspects, no downloads required.
|
||||
|
||||
<strong>The pipeline is visual.</strong> Concept art → 3D model → rigged character → in-engine asset. Each step uses different tools. The harness needs to orchestrate image generators, 3D modellers, auto-riggers, and game engines as a single workflow — not as five separate API calls the human has to stitch together.
|
||||
|
||||
<strong>Scale is physical.</strong> A web app's complexity is in business logic. A game's complexity is in geometry — 12km worlds, 155 vegetation types, 2,000 crowd instances. The agent needs to understand spatial systems, GPU memory budgets, and frame timing. "Add more RAM" isn't an answer when you have 8GB of VRAM.
|
||||
|
||||
<strong>The team is small and cross-functional.</strong> Four people. No dedicated DevOps, no dedicated artist, no dedicated PM. The harness fills all those gaps, not just one.
|
||||
|
||||
## The toolchain that makes it work
|
||||
|
||||
Our harness runs on <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>, built on a Gitea fork with game-specific features. The key pieces:
|
||||
|
||||
<strong>The CLI</strong> — a single Go binary. One command (<code>tinqs identity</code>) gives the agent full project context in 100ms. Screenshots, cloud vision, health checks — all subcommands of the same binary.
|
||||
|
||||
<strong>The soul file</strong> — a markdown document in the repo root. The agent reads it on session start. It defines values, scope, and behavioural rules. The same soul file works in Cursor, Claude Code, or any tool that reads markdown.
|
||||
|
||||
<strong>Skills</strong> — markdown playbooks for specific workflows. Image generation, concept art pipeline, 3D model creation, video generation. Each skill is a procedure the agent follows. Write once, use forever.
|
||||
|
||||
<strong>3D preview</strong> — click a <code>.glb</code> file in a PR and rotate the model in your browser. 22 formats supported. This alone transformed our review process — nobody approves a binary diff blind anymore.
|
||||
|
||||
<strong>Guardrails</strong> — agents can file issues, draft announcements, generate assets, and write code. They cannot merge, deploy, or push to public repos without human approval. Branch protection rules enforced at the git platform layer.
|
||||
|
||||
## The cold-start problem, solved
|
||||
|
||||
Every AI agent session starts blank. Most teams solve this with long system prompts — but when your context is 200 markdown files, 15 skills, and 3 years of project history, you can't paste all of that.
|
||||
|
||||
The harness uses staged loading:
|
||||
|
||||
1. <strong>CLI identity call</strong> (100ms) — soul file, company context, machine info, service status
|
||||
2. <strong>Memory file</strong> (instant) — cross-session context from the docs repo
|
||||
3. <strong>Skills</strong> (on demand) — loaded only when the task matches a skill name
|
||||
4. <strong>Repo context</strong> (on demand) — files read as needed, not all upfront
|
||||
|
||||
Agent goes from cold to fully contextual in under a second. No "let me explain the project." No re-reading onboarding docs. Just start working.
|
||||
|
||||
## The bet
|
||||
|
||||
The gap between "I have an AI model" and "I have an AI team member" is infrastructure. Identity, memory, tools, context, guardrails. For game development, that infrastructure needs to understand binary assets, visual pipelines, and spatial systems.
|
||||
|
||||
We're betting that specialised harnesses beat generic ones. A harness built for game dev — with 3D preview, LFS management, and creative pipelines — will outperform a general-purpose agent framework on game dev tasks. Not because the AI is smarter, but because it has the right hands, eyes, and memory for the job.
|
||||
|
||||
—
|
||||
|
||||
<em>Tinqs Studio is an agent harness for game development — git hosting, AI agents, creative pipelines. Open for teams. We're building <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> with the same tools.</em></p>
|
||||
<p class="post__lead">Open Claude or ChatGPT right now and ask it to review your last PR. It'll say "I don't have access to your repository." Ask it to take a screenshot of your game. It'll say "I can't interact with your operating system." Ask it what you were working on yesterday. It'll say "I don't have memory of previous conversations."</p>
|
||||
|
||||
<div class="post__body">
|
||||
<p>A raw AI model is a brain without hands, eyes, or memory. An <strong>agent harness</strong> is the layer that gives it all three — plus identity, tools, and guardrails. And game development needs one that understands binary assets, visual pipelines, and spatial systems.</p>
|
||||
<h2>What a harness provides</h2>
|
||||
<p>Every agent harness, regardless of domain, needs five things:</p>
|
||||
<p><strong>Identity.</strong> Who the agent is, what it values, how it should behave. Not "you are a helpful assistant" — that's generic and unmoored. A soul file that says "you're working on Ariki, a survival colony sim. The team is four people. Never push to main without review. Prefer existing conventions." Identity creates consistency across sessions.</p>
|
||||
<p><strong>Memory.</strong> What happened last session. What decisions were made. What failed and why. Without memory, every conversation is a cold start — "let me explain the project..." Memory stored as markdown in git means it's version-controlled, diffable, and human-readable. When something goes wrong, you <code>git log</code> instead of debugging a vector database.</p>
|
||||
<p><strong>Tools.</strong> What the agent can actually do beyond generating text. A CLI that takes screenshots, checks service health, and loads project context. API wrappers for git, CI, image generation. Without tools, the agent is a very articulate oracle that can't touch anything.</p>
|
||||
<p><strong>Context.</strong> Which project this is. Who's asking. What machine they're on. What services are reachable. A single CLI call — <code>tstudio identity</code> — returns all of this in 100ms. No re-reading the README. No "what repo are we in?"</p>
|
||||
<p><strong>Guardrails.</strong> What the agent must never do. No merging to main without review. No pushing to public repos without approval. No running destructive commands. The harness enforces these at the platform layer, not in the prompt. Prompts can be ignored. Platform gates cannot.</p>
|
||||
<h2>Why generic harnesses fail for game dev</h2>
|
||||
<p>LangChain, CrewAI, and AutoGen are built for web apps. They assume text-in, text-out. Game development is different in ways that break those assumptions:</p>
|
||||
<p><strong>Assets are binary.</strong> A web PR is a text diff. A game PR is a 150MB GLB file with textures, rigging, and animations. You can't review it without seeing it. Our harness renders 3D models in the browser during code review — rotate, zoom, check materials. The artist pushes, the lead inspects, no downloads required.</p>
|
||||
<p><strong>The pipeline is visual.</strong> Concept art → 3D model → rigged character → in-engine asset. Each step uses different tools. The harness needs to orchestrate image generators, 3D modellers, auto-riggers, and game engines as a single workflow — not as five separate API calls the human has to stitch together.</p>
|
||||
<p><strong>Scale is physical.</strong> A web app's complexity is in business logic. A game's complexity is in geometry — 12km worlds, 155 vegetation types, 2,000 crowd instances. The agent needs to understand spatial systems, GPU memory budgets, and frame timing. "Add more RAM" isn't an answer when you have 8GB of VRAM.</p>
|
||||
<p><strong>The team is small and cross-functional.</strong> Four people. No dedicated DevOps, no dedicated artist, no dedicated PM. The harness fills all those gaps, not just one.</p>
|
||||
<h2>The toolchain that makes it work</h2>
|
||||
<p>Our harness runs on <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>, built on a Gitea fork with game-specific features. The key pieces:</p>
|
||||
<p><strong>The CLI</strong> — a single Go binary. One command (<code>tstudio identity</code>) gives the agent full project context in 100ms. Screenshots, cloud vision, health checks — all subcommands of the same binary.</p>
|
||||
<p><strong>The soul file</strong> — a markdown document in the repo root. The agent reads it on session start. It defines values, scope, and behavioural rules. The same soul file works in Cursor, Claude Code, or any tool that reads markdown.</p>
|
||||
<p><strong>Skills</strong> — markdown playbooks for specific workflows. Image generation, concept art pipeline, 3D model creation, video generation. Each skill is a procedure the agent follows. Write once, use forever.</p>
|
||||
<p><strong>3D preview</strong> — click a <code>.glb</code> file in a PR and rotate the model in your browser. 22 formats supported. This alone transformed our review process — nobody approves a binary diff blind anymore.</p>
|
||||
<p><strong>Guardrails</strong> — agents can file issues, draft announcements, generate assets, and write code. They cannot merge, deploy, or push to public repos without human approval. Branch protection rules enforced at the git platform layer.</p>
|
||||
<h2>The cold-start problem, solved</h2>
|
||||
<p>Every AI agent session starts blank. Most teams solve this with long system prompts — but when your context is 200 markdown files, 15 skills, and 3 years of project history, you can't paste all of that.</p>
|
||||
<p>The harness uses staged loading:</p>
|
||||
<p>1. <strong>CLI identity call</strong> (100ms) — soul file, company context, machine info, service status</p>
|
||||
<p>2. <strong>Memory file</strong> (instant) — cross-session context from the docs repo</p>
|
||||
<p>3. <strong>Skills</strong> (on demand) — loaded only when the task matches a skill name</p>
|
||||
<p>4. <strong>Repo context</strong> (on demand) — files read as needed, not all upfront</p>
|
||||
<p>Agent goes from cold to fully contextual in under a second. No "let me explain the project." No re-reading onboarding docs. Just start working.</p>
|
||||
<h2>The bet</h2>
|
||||
<p>The gap between "I have an AI model" and "I have an AI team member" is infrastructure. Identity, memory, tools, context, guardrails. For game development, that infrastructure needs to understand binary assets, visual pipelines, and spatial systems.</p>
|
||||
<p>We're betting that specialised harnesses beat generic ones. A harness built for game dev — with 3D preview, LFS management, and creative pipelines — will outperform a general-purpose agent framework on game dev tasks. Not because the AI is smarter, but because it has the right hands, eyes, and memory for the job.</p>
|
||||
<hr>
|
||||
<p><em>Tinqs Studio is an agent harness for game development — git hosting, AI agents, creative pipelines. Open for teams. We're building <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a> with the same tools.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
+57
-72
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -301,10 +286,10 @@
|
||||
<p>Agents don't just have instructions. They have <strong>skills</strong> — markdown playbooks that teach specific workflows. When someone says "generate concept art for a character," the agent reads <code>skills/image-generation.md</code> and follows the procedure. No prompt engineering per session. No "let me try a different prompt."</p>
|
||||
<p>We've open-sourced several skills:</p>
|
||||
<ul>
|
||||
<li><a href="../skills/image-generation.md" style="color: var(--c-lime);">Image Generation with fal.ai</a> — 4-layer prompt pattern that actually produces usable game art</li>
|
||||
<li><a href="../skills/concept-art-pipeline.md" style="color: var(--c-lime);">Concept Art Pipeline</a> — full 2D concept → 3D model workflow</li>
|
||||
<li><a href="../skills/tripo-browser-workflow.md" style="color: var(--c-lime);">3D Model Generation</a> — Tripo Studio text-to-3D</li>
|
||||
<li><a href="../skills/sora2-video.md" style="color: var(--c-lime);">Video Generation</a> — trailer clips with Sora 2</li>
|
||||
<li><a href="../skills/image-generation.md" style="color: var(–c-accent-l);">Image Generation with fal.ai</a> — 4-layer prompt pattern that actually produces usable game art</li>
|
||||
<li><a href="../skills/concept-art-pipeline.md" style="color: var(–c-accent-l);">Concept Art Pipeline</a> — full 2D concept → 3D model workflow</li>
|
||||
<li><a href="../skills/tripo-browser-workflow.md" style="color: var(–c-accent-l);">3D Model Generation</a> — Tripo Studio text-to-3D</li>
|
||||
<li><a href="../skills/sora2-video.md" style="color: var(–c-accent-l);">Video Generation</a> — trailer clips with Sora 2</li>
|
||||
</ul>
|
||||
<p>Each skill took about 30 minutes to write. After six months, our agents have 15+ skills covering art generation, competitive research, video production, and project management. Skills compound — every playbook you write makes every future session more capable.</p>
|
||||
<h2>What the agents actually do, every day</h2>
|
||||
@@ -332,7 +317,7 @@
|
||||
<p><strong>Skills compound exponentially.</strong> One skill saves 15 minutes per session. Fifteen skills save hours per day across the whole team. The investment curve is absurdly favourable — 30 minutes of writing per skill, compounding returns forever.</p>
|
||||
<p>We're four people. With agents doing the mechanical work, we operate like forty. Not because the AI is magic — because we gave it identity, memory, and the right playbooks, and then got out of its way.</p>
|
||||
<hr>
|
||||
<p><em>We're building <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a>, a survival colony sim, using the same agent workflow described here. Everything runs on <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a> — a game dev platform with built-in AI agents, git hosting, and creative pipelines.</em></p>
|
||||
<p><em>We're building <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a>, a survival colony sim, using the same agent workflow described here. Everything runs on <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a> — a game dev platform with built-in AI agents, git hosting, and creative pipelines.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
+53
-68
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -318,7 +303,7 @@ Done.</code></pre>
|
||||
<p><strong>Build systems make CSS changes safe.</strong> Because we never hand-edit <code>.html</code>, every style change is tested by regenerating all pages and grepping for the new selectors. If a rule doesn't ship, you know immediately.</p>
|
||||
<p>Two gaps we'll fill later: blockquote support in <code>build.js</code> (the callout CSS is waiting) and ordered lists (same story). In the meantime, the blog already looks intentional — and it took two template files, one build step, and zero dependencies.</p>
|
||||
<hr>
|
||||
<p><em>The blog is generated by <a href="https://tinqs.com/tinqs/blog" style="color: var(--c-lime);">build.js</a> and served by <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>. All styling is self-contained in the templates.</em></p>
|
||||
<p><em>The blog is generated by <a href="https://tinqs.com/tinqs/blog" style="color: var(–c-accent-l);">build.js</a> and served by <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>. All styling is self-contained in the templates.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
@@ -144,11 +144,11 @@ function inline(s) {
|
||||
s = s.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
// Inline code
|
||||
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
// Em dash / en dash — before links so CSS var(--x) in style attr isn't mangled
|
||||
// Links
|
||||
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: var(--c-accent-l);">$1</a>');
|
||||
// Em dash
|
||||
s = s.replace(/---/g, "—");
|
||||
s = s.replace(/--/g, "–");
|
||||
// Links
|
||||
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: var(--c-lime);">$1</a>');
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+54
-69
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -350,10 +335,10 @@ No extra fingers, no merged limbs, no floating accessories.</code></pre>
|
||||
<p><strong>The design context block is worth more than the rest of the prompt combined.</strong> Without it, every image is a one-off. With it, every image belongs to the same game.</p>
|
||||
<p><strong>Never iterate on expensive models.</strong> Schnell at $0.003/image is for exploration. Flux 2 Pro at $0.03 is for final output. The cheap model does 90% of the creative work.</p>
|
||||
<p><strong>Aggregation beats loyalty.</strong> No single model is best at everything. Flux for art, Ideogram for text, Recraft for design, Nano Banana for edits, BiRefNet for masks. Use the right tool for each job.</p>
|
||||
<p><strong>Let the agent handle prompting.</strong> We encode the 4-layer pattern, art style guide, and model selection rules in an <a href="../skills/image-generation.md" style="color: var(--c-lime);">agent skill file</a>. The AI writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.</p>
|
||||
<p><strong>Let the agent handle prompting.</strong> We encode the 4-layer pattern, art style guide, and model selection rules in an <a href="../skills/image-generation.md" style="color: var(–c-accent-l);">agent skill file</a>. The AI writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.</p>
|
||||
<p>AI art isn't magic and it isn't free. But at a penny per image, with the right prompt structure and model strategy, it eliminates the most expensive bottleneck in indie game development: the gap between "I know what this should look like" and "I have an asset I can actually use."</p>
|
||||
<hr>
|
||||
<p><em>Image generation is built into <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>. We've open-sourced the <a href="../skills/image-generation.md" style="color: var(--c-lime);">prompt engineering skill</a> and <a href="../skills/concept-art-pipeline.md" style="color: var(--c-lime);">concept art pipeline skill</a>. We're building <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> with these tools.</em></p>
|
||||
<p><em>Image generation is built into <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>. We've open-sourced the <a href="../skills/image-generation.md" style="color: var(–c-accent-l);">prompt engineering skill</a> and <a href="../skills/concept-art-pipeline.md" style="color: var(–c-accent-l);">concept art pipeline skill</a>. We're building <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a> with these tools.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
@@ -1,369 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript — Tinqs Blog</title>
|
||||
<meta name="description" content="We killed the static YAML DAG and rewrote our agent orchestration in 200 lines of JavaScript. Now a flow IS a session — you chat it, steer it, and it pauses for you at a human gate.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://www.tinqs.com/blog/flows-are-sessions">
|
||||
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://www.tinqs.com/blog/flows-are-sessions">
|
||||
<meta property="og:title" content="Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript">
|
||||
<meta property="og:description" content="YAML DAGs are dead. We rewrote our agent orchestration in JavaScript, made every flow a live session, and added a human-in-the-loop gate. The operator is the co-pilot, not the babysitter.">
|
||||
<meta property="og:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript">
|
||||
<meta name="twitter:description" content="YAML DAGs are dead. We rewrote our agent orchestration in JavaScript, made every flow a live session, and added a human-in-the-loop gate. The operator is the co-pilot, not the babysitter.">
|
||||
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript",
|
||||
"datePublished": "2026-06-11",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Ozan Bozkurt"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Tinqs Limited",
|
||||
"url": "https://www.tinqs.com"
|
||||
},
|
||||
"description": "We killed the static YAML DAG and rewrote our agent orchestration in 200 lines of JavaScript. Now a flow IS a session — you chat it, steer it, and it pauses for you at a human gate."
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Lead ── */
|
||||
.post__lead {
|
||||
color: var(--c-muted);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.post__body { font-size: 1rem; line-height: 1.7; }
|
||||
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
.post__body h4, .post__body h5, .post__body h6 {
|
||||
margin: 20px 0 4px;
|
||||
}
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--c-border);
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
/* ── Figures ── */
|
||||
.post__body figure { margin: 20px 0; }
|
||||
.post__body figure img {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__body figcaption {
|
||||
color: var(--c-muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Lists ── */
|
||||
.post__body ul, .post__body ol { padding-left: 1.5em; margin: 10px 0; }
|
||||
.post__body li { margin: 4px 0; }
|
||||
|
||||
/* ── Author ── */
|
||||
.post__author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 48px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__author-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post__author-info {
|
||||
font-size: 0.85rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- POST -->
|
||||
<article class="post">
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">11 June 2026</span>
|
||||
<h1 class="post__title">Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript</h1>
|
||||
<p class="post__lead">Our YAML flow engine had seven bespoke node types just to fake a <code>while</code> loop. We threw it out and rewrote everything in 200 lines of JavaScript. The flow engine is gone. The flow IS the session. Here's what we learned.</p>
|
||||
|
||||
<div class="post__body">
|
||||
<h2>The YAML Was a Compiler for a Language Nobody Wanted</h2>
|
||||
<p>The old system was a static DAG. You defined nodes and edges in YAML, the engine walked them top-to-bottom, and when it finished it was done. No mid-run interaction. No branching. No retry. If you wanted a loop, you didn't use a <code>while</code> statement — you used an <code>agent-loop-decision</code> node type. In YAML.</p>
|
||||
<pre><code class="language-yaml"># The old way: a "loop" was a bespoke node type
|
||||
steps:
|
||||
- agent-task: "generate document"
|
||||
- agent-loop-decision:
|
||||
condition: "check if quality > 0.8"
|
||||
if-true: "continue"
|
||||
if-false: "repeat-step: agent-task"</code></pre>
|
||||
<p>That's not configuration. That's a compiler for a language nobody wanted to write. Every orchestrator ends up here — GitHub Actions expressions, GitLab CI <code>rules</code>, Airflow's <code>BranchPythonOperator</code>, all of them start as "simple YAML config" and grow node types until they're Turing-complete nightmares held together by schema patches.</p>
|
||||
<p>We had seven: <code>agent-task</code>, <code>agent-loop-decision</code>, <code>fork</code>, <code>conditional</code>, <code>agent-join</code>, <code>pipeline-stage</code>, <code>human-review</code>. Each one existed because YAML can't express control flow. You weren't writing a flow. You were filing paperwork to describe a flow.</p>
|
||||
<p>The moment we knew it was wrong: someone asked "can I retry this step three times if it fails?" and the honest answer was "we'd need a new node type." When your config format needs an RFC to add a <code>for</code> loop, you've built a programming language by accident. Delete it.</p>
|
||||
<h2>200 Lines of JavaScript Replaced Seven Node Types</h2>
|
||||
<p>A flow is now an ES module. You export <code>default async (flow) => {}</code> and the runtime calls it. The API surface is five calls:</p>
|
||||
<ul>
|
||||
<li><code>agent(prompt, options)</code> — run one agent with a task</li>
|
||||
<li><code>parallel(thunks)</code> — run many agents concurrently, await all</li>
|
||||
<li><code>pipeline(items, ...stages)</code> — push items through stages</li>
|
||||
<li><code>phase(name)</code> — label progress for the dashboard</li>
|
||||
<li><code>human(config)</code> — pause and wait for a person</li>
|
||||
</ul>
|
||||
<p>Here's a real flow. It reviews a code route change with parallel researchers and a human gate. Ten lines.</p>
|
||||
<pre><code class="language-js">// .pi/flows/flows/review-routes.flow.mjs
|
||||
export const meta = { name: "review-routes", description: "audit routes for missing auth" };
|
||||
export default async (flow) => {
|
||||
const { agent, parallel, phase, human, task } = flow;
|
||||
phase("find");
|
||||
const findings = await parallel(["auth", "input"].map((d) => () =>
|
||||
agent(`Review ${d}: ${task}`, { agent: "researcher", model: "@planning" })
|
||||
));
|
||||
phase("review");
|
||||
const gate = await human({ title: "Eyeball it", prompt: "anything to fix before I finish?" });
|
||||
return { findings, gate };
|
||||
};</code></pre>
|
||||
<p>Why JavaScript wins is boring and fundamental: it has <code>while</code>, <code>if</code>, <code>try/catch</code>, and <code>parallel(thunks)</code> built in. The things our YAML needed custom schema types to fake are just language keywords. Bounded concurrency is a one-liner. Error recovery is a <code>try</code> block. A loop is <code>while (!approved)</code>. No plugin, no RFC, no new node type.</p>
|
||||
<p>We migrated all flows YAML-to-JS on 2026-06-10. One-way conversion script, every flow reviewed and running in 24 hours. The YAML parser was deleted — not deprecated, not kept for backwards compatibility, deleted. There's no config path left to reach for.</p>
|
||||
<p>The plan lives in code, not config. Config is for things that don't change. Agent orchestration changes every run.</p>
|
||||
<h2>A Flow IS a Session</h2>
|
||||
<p>The old model had a phantom problem. A flow was a card. A session was a separate card. The operator watched the flow run from a different window. There was a "New Flow" button that created a flow card, and a "Continue" button that attached a session to it, and the disconnect between "the thing running your work" and "the place you talk to it" was baked into the UI.</p>
|
||||
<p>We killed that architecture. Every spawn is a session.</p>
|
||||
<p>When you call <code>POST /api/flows/spawn {cwd, task, flowName}</code>, the session runs the flow inside itself with the <code>flow_run</code> tool. Steps stream inline into the chat — <code>flow:steps</code> injects progress into the session's own message stream. The session turns purple in the dashboard. It becomes the flow. One card. One identity. Persistent after the run finishes.</p>
|
||||
<p>No "New Flow" button. No "Continue" button. You spawn a session; with a <code>flowName</code> it runs that flow, without one it opens an interactive operator session that designs a flow with you first. The dashboard branding is tinqs Studio. The control surface is the host card, live agent cards, run history, and a chat to steer the run.</p>
|
||||
<p>There's no phantom card. No disconnect between "the thing running your work" and "the place you talk to it." It's one session. You're in the room.</p>
|
||||
<h2>The Human Gate: Pause, Take Over, Approve, Continue</h2>
|
||||
<p>Agents make mistakes. They guess when they shouldn't. They take irreversible actions because the prompt said "proceed." The model gets stuck and you sit there wondering — do I wait or abort?</p>
|
||||
<p><code>flow.human()</code> is our answer.</p>
|
||||
<p>When a flow hits a human gate, it stops. The dashboard shows the gate prompt: what to review, what to decide. The host session switches to takeover mode — coding tools are unblocked (normally the host is hands-off) and the system prompt becomes "flow is paused, help the operator finish this." You open files. You edit. You verify. You run commands.</p>
|
||||
<p>To release the gate, reply <code>approve</code> or <code>done</code> or <code>lgtm</code>. The flow resumes. Any other message is a work instruction — the takeover session executes it but does not release the gate. The flow loops on <code>notes</code> until the human says go.</p>
|
||||
<pre><code class="language-js">let approved = false;
|
||||
while (!approved) {
|
||||
const { notes } = await human({
|
||||
title: "Review before push",
|
||||
prompt: "Check the diff. Approve or tell me what to fix.",
|
||||
});
|
||||
if (notes.match(/^(approve|done|lgtm|looks good)/i)) approved = true;
|
||||
else await agent(`Fix: ${notes}`, { agent: "implementer" });
|
||||
}</code></pre>
|
||||
<p>Two patterns this enables. First: review-approve-before-push gates — nobody ships untested code because nobody set the <code>auto-approve</code> flag to true. Second: the "agent is stuck" hand-off — the flow pauses, you take over the exact same workspace, fix the problem, type <code>continue</code>, and the flow keeps going.</p>
|
||||
<p>The flow waits instead of guessing. This isn't a feature. It's an admission that some decisions shouldn't be automated.</p>
|
||||
<h2>The Operator Is Your Co-Pilot</h2>
|
||||
<p>The old way was a one-shot generator. Paste an objective, click Generate, get a YAML blob, pray it's right, run it, discover it's wrong 20 minutes in with no way to steer. We'd watch flows fail and think "I could have told it that before it started."</p>
|
||||
<p>The new flow operator doesn't write the flow for you and walk away. It designs it with you.</p>
|
||||
<p>Hit New Flow. A DeepSeek session opens — the same model that powers the dashboard, cheap at $0.28/MTok, steerable in natural language. It proposes a draft <code>.flow.mjs</code>, shows you the agents and phases and any human gate, and explains why. You tell it what to change. It does NOT launch until you say go. When you approve, it writes the flow file and spawns a separate host session, then attaches to monitor and report progress.</p>
|
||||
<p>The operator is still in the chat when the human gate fires. It's still there when you want to change the plan mid-run. It doesn't go away after launch. Co-pilot, not autopilot.</p>
|
||||
<p>There are three runner faces for the same engine: <strong>pi/dashboard</strong> (DeepSeek, cheap, steerable — the default), <strong>Claude Code</strong> (Workflow tool, one-shot fan-out for heavy research), and a <strong>cloud agent</strong> (remote deploy, clone, AWS). Pick by granularity and cost. The flow file is the same in all three modes.</p>
|
||||
<h2>What We Learned</h2>
|
||||
<p><strong>Numbers first.</strong> 43 out of 43 unit tests green. All flows migrated. The supervisor inbox — steering messages sent between steps — was silently dropping operator messages before 2026-06-10. You'd type "focus on the auth routes" and the flow never saw it. That's fixed now. Chat reaches the inbox, the inbox drains between <code>agent()</code> calls, and steering works.</p>
|
||||
<p><strong>The inbox rule.</strong> The supervisor inbox is applied between <code>agent()</code> calls, never mid-step. Steering mid-step is undefined behaviour. We learned this the hard way — early versions tried to inject mid-agent and got corrupted state, partial outputs, agents that forgot what they were doing. Between steps is the right boundary. Respect it.</p>
|
||||
<p><strong>Economics matter.</strong> DeepSeek V4 Pro at $0.28/MTok runs per-step. Per-step model override lets you swap in a premium reasoning model ($15/MTok) for the one critical call that needs it. $0.28 for routine, $15 for the hard parts. The three-tier strategy from our agent daemon applies at flow granularity too.</p>
|
||||
<p><strong>What's next.</strong> Richer on-card flow display — a pinned step strip so you can see progress without opening the session. Attachable asset and agent-structure viewers in the flow card. Run replay for finished sessions after a page reload (the session persists, but you can't rewatch the stream yet).</p>
|
||||
<p>But the principle is settled. A flow isn't a pipeline. A pipeline runs blind and reports back later. A flow is a pair-programming session where one of the pair happens to be code.</p>
|
||||
<hr>
|
||||
<p><em><a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a> is our agent-native development platform — git hosting, AI agents, and the flow engine described here. <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> is the survival colony sim we're building with it.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="post__author">
|
||||
<div class="post__author-avatar">OB</div>
|
||||
<div class="post__author-info">
|
||||
<span class="post__author-name">Ozan Bozkurt</span><br>
|
||||
CTO & Developer, Tinqs
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Before
|
+56
-71
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -291,7 +276,7 @@
|
||||
<p>Across three forks, we've never touched more than 0.5% of upstream code. If your fork hits 1%, you're doing too much — either the upstream tool is wrong for the job, or you're not trusting it enough.</p>
|
||||
<h2>Fork 1: Gitea → Tinqs Studio</h2>
|
||||
<p>Gitea is a self-hosted git server. Single Go binary, MIT license, 45k GitHub stars. We used GitHub for two years. It was fine for docs. For the game repo — 12GB in LFS, growing weekly — it was untenable. LFS bandwidth limits, slow clones, $5/50GB pricing. And nobody on the team could <strong>see</strong> what changed. A PR modifying a <code>.glb</code> file showed a binary diff. No preview. The artist pushed, the developer approved blindly, and three days later someone noticed the normals were inverted.</p>
|
||||
<p>We forked Gitea and built <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>. Our changes:</p>
|
||||
<p>We forked Gitea and built <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>. Our changes:</p>
|
||||
<p><strong>3D asset preview.</strong> Click a <code>.glb</code>, <code>.gltf</code>, or <code>.fbx</code> file in a PR and rotate the model in your browser. 22 formats supported via O3DV. This alone transformed our review process — the artist pushes, the lead inspects, nobody downloads anything.</p>
|
||||
<p><strong>HTML file preview.</strong> Sandboxed iframe rendering. Our internal docs and game design pages look like websites, not raw source.</p>
|
||||
<p><strong>Agent API.</strong> Six REST endpoints that let AI agents submit tasks, push code, check CI status, and open PRs. Agents are first-class users of the git platform, not bolt-on tools.</p>
|
||||
@@ -300,7 +285,7 @@
|
||||
<p>Total lines changed: about 2,000 out of Gitea's 500,000. We modify templates, add Go modules, tweak CSS. We <strong>never</strong> touch the database schema — upstream owns that, and we ride their migrations.</p>
|
||||
<p>The alternative was building a git platform from scratch. Multi-year project, multi-million dollar budget. Or using GitHub/GitLab and accepting their limitations. Neither gives you the ability to embed agents directly into the platform.</p>
|
||||
<h2>Fork 2: Pi → Agent Runtime with Game Tools</h2>
|
||||
<p><a href="https://pi.dev" style="color: var(--c-lime);">Pi</a> is an open-source coding agent by Mario Zechner. MIT license, TypeScript, minimal by design — four core tools (read, write, edit, bash) and an extension system. 51k stars.</p>
|
||||
<p><a href="https://pi.dev" style="color: var(–c-accent-l);">Pi</a> is an open-source coding agent by Mario Zechner. MIT license, TypeScript, minimal by design — four core tools (read, write, edit, bash) and an extension system. 51k stars.</p>
|
||||
<p>We forked it and added four extensions, each a single TypeScript file:</p>
|
||||
<ul>
|
||||
<li><strong>tinqs-provider</strong> — routes inference through our DeepSeek V4 proxy ($0.28/MTok vs Opus at $15/MTok)</li>
|
||||
@@ -311,7 +296,7 @@
|
||||
<p>The core Pi code is untouched — 900 lines of extensions added to a 15,000-line codebase. Agents get Gitea-native tools without a fork of the entire agent ecosystem.</p>
|
||||
<p>The alternative: building our own agent from scratch — tool-calling logic, context management, streaming, retry handling, conversation threading. Months of work to reinvent what Pi already does.</p>
|
||||
<h2>Fork 3: Godot → Agent-Aware Game Engine</h2>
|
||||
<p><a href="https://godotengine.org" style="color: var(--c-lime);">Godot</a> is the open-source game engine powering our survival colony sim. We forked 4.6.2 and added nine C++ modules that give agents direct access to the running game:</p>
|
||||
<p><a href="https://godotengine.org" style="color: var(–c-accent-l);">Godot</a> is the open-source game engine powering our survival colony sim. We forked 4.6.2 and added nine C++ modules that give agents direct access to the running game:</p>
|
||||
<ul>
|
||||
<li><strong>agent_api</strong> — HTTP server inside the engine so agents can query game state</li>
|
||||
<li><strong>agent_vision</strong> — screenshot capture for AI vision pipelines</li>
|
||||
@@ -330,7 +315,7 @@
|
||||
<p>We're building the layer that connects them. The git server that speaks agent. The coding agent that speaks Gitea. The game engine that speaks HTTP. Each fork is a bridge between an existing tool and the agentic future — not a replacement for either.</p>
|
||||
<p>The age of agents doesn't need more agents. It needs better platforms. Platforms that already exist as open-source projects. They just need someone to fork them and add the wiring.</p>
|
||||
<hr>
|
||||
<p><em><a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a> is our Gitea fork, open for game teams. <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> is the game we're building with every tool described here.</em></p>
|
||||
<p><em><a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a> is our Gitea fork, open for game teams. <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a> is the game we're building with every tool described here.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
+53
-68
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -334,7 +319,7 @@
|
||||
<p><strong>Always measure before optimizing.</strong> We added VRAM logging before writing a single line of optimization code. Half the "problems" we expected were non-issues. The other half were worse than expected. Profiling isn't optional.</p>
|
||||
<hr>
|
||||
<p>Godot 4 can handle open worlds at this scale, but it won't do it for you. You need to build streaming, manage your own caches, audit resource loading, and be disciplined about what runs per frame. The engine gives you the primitives — MultiMesh, <code>LoadThreadedRequest</code>, <code>QueueFree</code>. It's up to you to wire them into a system that scales.</p>
|
||||
<p><em>We're building <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a>, a survival colony sim, with these systems. The tools we use — git hosting, AI agents, creative pipelines — are part of <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>.</em></p>
|
||||
<p><em>We're building <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a>, a survival colony sim, with these systems. The tools we use — git hosting, AI agents, creative pipelines — are part of <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
@@ -1,358 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot — Tinqs Blog</title>
|
||||
<meta name="description" content="Godot has no built-in way to render 1,000 skinned characters in one draw call. We built a GPU skinned-instance renderer into Tinqs Engine that does — 25 crocodiles verified, 1,000+ projected. Pre-built binaries for macOS and Windows.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://www.tinqs.com/blog/gpu-skinned-herds">
|
||||
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://www.tinqs.com/blog/gpu-skinned-herds">
|
||||
<meta property="og:title" content="GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot">
|
||||
<meta property="og:description" content="One draw call, 1,000 animated characters. GPU-skinned herd renderer built into the Tinqs Engine fork of Godot.">
|
||||
<meta property="og:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot">
|
||||
<meta name="twitter:description" content="One draw call, 1,000 animated characters. GPU-skinned herd renderer built into the Tinqs Engine fork of Godot.">
|
||||
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot",
|
||||
"datePublished": "2026-06-14",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Ozan Bozkurt"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Tinqs Limited",
|
||||
"url": "https://www.tinqs.com"
|
||||
},
|
||||
"description": "Godot has no built-in way to render 1,000 skinned characters in one draw call. We built a GPU skinned-instance renderer into Tinqs Engine that does — 25 crocodiles verified, 1,000+ projected. Pre-built binaries for macOS and Windows."
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Lead ── */
|
||||
.post__lead {
|
||||
color: var(--c-muted);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.post__body { font-size: 1rem; line-height: 1.7; }
|
||||
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
.post__body h4, .post__body h5, .post__body h6 {
|
||||
margin: 20px 0 4px;
|
||||
}
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--c-border);
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
/* ── Figures ── */
|
||||
.post__body figure { margin: 20px 0; }
|
||||
.post__body figure img {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__body figcaption {
|
||||
color: var(--c-muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Lists ── */
|
||||
.post__body ul, .post__body ol { padding-left: 1.5em; margin: 10px 0; }
|
||||
.post__body li { margin: 4px 0; }
|
||||
|
||||
/* ── Author ── */
|
||||
.post__author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 48px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__author-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post__author-info {
|
||||
font-size: 0.85rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- POST -->
|
||||
<article class="post">
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">14 June 2026</span>
|
||||
<h1 class="post__title">GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot</h1>
|
||||
<p class="post__lead">Godot gives you one <code>Skeleton3D</code> per character. Want 200 animals in a herd? That's 200 skeleton nodes, 200 draw calls, and 200 <code>AnimationPlayer</code> ticks every frame. Want 1,000? Now you're measuring in seconds per frame, not frames per second.</p>
|
||||
|
||||
<div class="post__body">
|
||||
<p>We built a GPU skinned-instance renderer into Tinqs Engine that packs every pose into a single texture, uploads once, and draws every instance in one call. 25 crocodiles confirmed first. Then we threw 1,000 animals — 12 types mixed, random-walking — at it and the GPU didn't flinch. Same bone count, same animation fidelity, a tiny fraction of the cost.</p>
|
||||
<h2>Why the engine needs to change</h2>
|
||||
<p>The standard Godot approach — one <code>Skeleton3D</code> + one <code>MeshInstance3D</code> per character — works for a handful of animated entities. It breaks down hard at crowd scale:</p>
|
||||
<ul>
|
||||
<li><strong>CPU bone transforms.</strong> Computing <code>global_pose</code> for 200 skeletons × 100 bones each = 20,000 matrix multiplies per frame, all on the main thread.</li>
|
||||
<li><strong>Draw call explosion.</strong> Each <code>MeshInstance3D</code> is its own draw call. Even with MultiMesh, there's no built-in path for skinned meshes — <code>MultiMeshInstance3D</code> only handles static geometry.</li>
|
||||
<li><strong>AnimationPlayer sprawl.</strong> Each skeleton needs its own <code>AnimationPlayer</code> and its own <code>process()</code> tick.</li>
|
||||
</ul>
|
||||
<p>The alternative — baking animations to vertex textures — works for static crowds but locks you out of per-instance variation. No blending, no phase offsets, no reactive behaviour.</p>
|
||||
<p>What we need is simpler: <strong>share the skeleton, drive per-instance poses from a single animation, batch the draw call.</strong> That's what <code>agent_skinned</code> does.</p>
|
||||
<h2>How it works: two classes, one texture</h2>
|
||||
<p>The module lives in <code>modules/agent_skinned/</code> inside <a href="https://tinqs.com/tinqs/engine" style="color: var(--c-lime);">Tinqs Engine</a>. Two classes, one job:</p>
|
||||
<h3><code>MultiSkinnedMeshInstance3D</code> — the data plane</h3>
|
||||
<p>Holds the CPU-side bone matrices. Allocates an <code>ImageTexture</code> of size <code>[4 × max_bones, max_instances]</code> in RGBA32F — each texel is one column of a 4×4 bone matrix. For a 130-bone crocodile with 256 instances:</p>
|
||||
<pre><code>Texture: 520 × 256 RGBA32F ≈ 2 MB</code></pre>
|
||||
<p>That's the entire pose state for 256 animated crocodiles in a single GPU texture. The API is simple:</p>
|
||||
<pre><code class="language-gdscript">var data := MultiSkinnedMeshInstance3D.new()
|
||||
data.set_mesh(crocodile_mesh)
|
||||
data.set_skeleton(skeleton) # rest pose + bone hierarchy
|
||||
data.set_max_instances(256)
|
||||
data.set_max_bones(130)
|
||||
|
||||
# Each frame: push poses from the animated skeleton
|
||||
for instance in herd_positions:
|
||||
data.set_instance_pose_bones(instance.id, bone_transforms)
|
||||
data.update() # upload only dirty instances, not the whole texture</code></pre>
|
||||
<h3><code>MultiSkinnedInstance3D</code> — the renderer</h3>
|
||||
<p>A <code>MultiMeshInstance3D</code> subclass. Set its multimesh with the skinned mesh and instance transforms, point it at the data plane, call <code>refresh()</code> — it uploads the bone texture into the shader material's <code>bone_matrices_tex</code> uniform and the mesh is drawn in one call.</p>
|
||||
<p>The shader does 4-bone linear-blend skinning on the GPU:</p>
|
||||
<pre><code class="language-glsl">mat4 get_bone(int b) {
|
||||
return mat4(
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 0, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 1, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 2, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 3, INSTANCE_ID), 0)
|
||||
);
|
||||
}</code></pre>
|
||||
<p><code>INSTANCE_ID</code> is a Godot built-in — the GPU already knows which instance it's rendering. We just use it to index into the bone texture. No uniform arrays, no SSBOs, no compute shaders. Just a 2D texture and a custom vertex shader.</p>
|
||||
<h2>Two bugs we shipped and fixed</h2>
|
||||
<p>The module had data-plane doctests from day one — round-trip pose get/set, dirty tracking, size clamping, AABB. All green. Then we put it on screen for the first time and the crocodiles looked... wrong.</p>
|
||||
<p><strong>Bug 1: Shader compile failure.</strong> The default skinning shader compared <code>TANGENT</code> as <code>vec4</code>. Godot 4 exposes it as <code>vec3</code>. Fixed in one line, added <code>albedo_tex</code> uniform so herds texture out of the box.</p>
|
||||
<p><strong>Bug 2: Bone matrices stored transposed.</strong> The data plane wrote basis rows (standard Godot <code>Transform3D.basis</code> is row-major), but the shader unpacked as columns. Every bone matrix was transposed — the mesh crumpled. Not a scale bug, not an orientation bug — a layout mismatch. Fixed by storing column-major, with a doctest to prevent regression.</p>
|
||||
<p>The lesson: doctests catch logic. Rendering catches truth. You need both.</p>
|
||||
<h2>What's driving it</h2>
|
||||
<p>In <a href="https://www.arikigame.com" style="color: var(--c-lime);">Ariki</a>, the sim tracks animal migration across a 12km archipelago. <code>AnimalHerdRenderer.cs</code> groups sim <code>ViewerState.animals</code> by type, feeds positions to <code>skinned_herd.gd</code> (a reusable per-type herd backend), which drives the renderer. One <code>AnimationPlayer</code> animates a single driver skeleton; poses propagate to every instance.</p>
|
||||
<p>The crocodile herd scene was 25 instances, one draw call. The perf test scene does 1,000 animals across 12 types — Boar, Cow, Crab, Crocodile, Deer, Fish, Goat, Hen, Pig, Rabbit, Sheep, Tiger — each type its own GPU herd, all mixed, all random-walking, FPS holding steady.</p>
|
||||
<h2>What's deliberately not here</h2>
|
||||
<ul>
|
||||
<li><strong>No C# wrapper.</strong> Instantiate from GDScript via <code>ClassDB.instantiate()</code> — the binding surface is small and stable.</li>
|
||||
<li><strong>No automatic <code>AnimationPlayer</code> integration.</strong> You drive poses. We give you the texture. Freedom to animate however you want.</li>
|
||||
<li><strong>No GPU occlusion or LOD.</strong> That's the game's job. The engine provides the tool; the game decides what to draw.</li>
|
||||
</ul>
|
||||
<h2>Get the build</h2>
|
||||
<p>Pre-built editor binaries with <code>agent_skinned</code> baked in — no engine compile required. The game's <code>animal_perf_test.tscn</code> lets you toggle 10 / 100 / 1000 animals and read live FPS:</p>
|
||||
<p>| Platform | Binary | Engine commit |</p>
|
||||
<p>|———-|——–|—————|</p>
|
||||
<p>| <strong>macOS ARM64</strong> | <a href="https://tinqs.com/tinqs/builds/media/branch/main/engine/macos-arm64/tinqs.macos.editor.arm64.mono" style="color: var(--c-lime);"><code>tinqs.macos.editor.arm64.mono</code></a> | <code>4fe1323</code> (4.6.4, Xcode 26.3) |</p>
|
||||
<p>| <strong>Windows x64</strong> | <a href="https://tinqs.com/tinqs/builds/media/branch/main/engine/windows-x64/tinqs.windows.editor.x86_64.mono.exe" style="color: var(--c-lime);"><code>tinqs.windows.editor.x86_64.mono.exe</code></a> | <code>64fb5cc</code> (4.6.4, MSVC 2022) |</p>
|
||||
<p>All builds live in the public <a href="https://tinqs.com/tinqs/builds" style="color: var(--c-lime);"><code>tinqs/builds</code></a> repo — engine source is private, but the binaries are yours. See <a href="https://tinqs.com/tinqs/builds/src/branch/main/manifest.json" style="color: var(--c-lime);"><code>manifest.json</code></a> for checksums and build details.</p>
|
||||
<p>The engine source lives in <a href="https://tinqs.com/tinqs/engine" style="color: var(--c-lime);"><code>tinqs/engine</code></a> (private). Module docs: <code>modules/agent_skinned/README.md</code> and <code>.agents/wiki/agent-skinned-gpu-herd.md</code>.</p>
|
||||
<hr>
|
||||
<p><strong>Related:</strong> <a href="fork-dont-build" style="color: var(--c-lime);">Fork, Don't Build</a> — why we modify existing platforms instead of building new ones. <a href="godot-optimisation" style="color: var(--c-lime);">Streaming a 12km Archipelago in Godot 4</a> — the terrain and vegetation streaming layers that work alongside this.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="post__author">
|
||||
<div class="post__author-avatar">OB</div>
|
||||
<div class="post__author-info">
|
||||
<span class="post__author-name">Ozan Bozkurt</span><br>
|
||||
CTO & Developer, Tinqs
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Before
|
+35
-69
@@ -15,60 +15,42 @@
|
||||
<meta property="og:description" content="Dev logs, behind-the-scenes, and lessons learned from building games, tools, and platform at Tinqs Studio.">
|
||||
<meta property="og:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — index styles ── */
|
||||
/* ── Self-contained index styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.blog-header {
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
.blog-list {
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
/* ── Section label kicker ── */
|
||||
.section-label {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -81,16 +63,15 @@
|
||||
padding: 60px 24px 32px;
|
||||
}
|
||||
|
||||
/* ── Gradient index title — lime → violet ── */
|
||||
/* ── Gradient index title ── */
|
||||
.blog-header__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.4rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@@ -115,37 +96,36 @@
|
||||
.blog-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
background: var(--c-bg-raised);
|
||||
background: #0c1119;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.blog-card:hover {
|
||||
border-color: var(--c-lime);
|
||||
border-color: var(--c-accent);
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.blog-card__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.blog-card__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
color: var(--c-fg);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3;
|
||||
color: var(--c-text);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -158,13 +138,13 @@
|
||||
|
||||
/* ── Read link accent ── */
|
||||
.blog-card__read {
|
||||
color: var(--c-lime);
|
||||
font-size: 0.875rem;
|
||||
color: var(--c-blue);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.blog-card:hover .blog-card__read {
|
||||
color: var(--c-violet);
|
||||
color: var(--c-purple);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -187,20 +167,6 @@
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
|
||||
<a href="gpu-skinned-herds" class="blog-card">
|
||||
<span class="blog-card__date">14 June 2026</span>
|
||||
<h2 class="blog-card__title">GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot</h2>
|
||||
<p class="blog-card__excerpt">Godot can't batch-render 1,000 animated characters. We built a GPU skinned-instance herd renderer into the engine itself — already driving crocodile herds in Ariki. Pre-built editor binaries for macOS and Windows.</p>
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
|
||||
<a href="flows-are-sessions" class="blog-card">
|
||||
<span class="blog-card__date">11 June 2026</span>
|
||||
<h2 class="blog-card__title">Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript</h2>
|
||||
<p class="blog-card__excerpt">We killed the static YAML DAG and rewrote our agent orchestration in 200 lines of JavaScript. Now a flow IS a session — you chat it, steer it, and it pauses for you at a human gate.</p>
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
|
||||
<a href="voice-missing-input-game-dev" class="blog-card">
|
||||
<span class="blog-card__date">10 June 2026</span>
|
||||
<h2 class="blog-card__title">Why Voice Is the Missing Input for Game Development</h2>
|
||||
|
||||
|
Before
After
|
+53
-68
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -279,7 +264,7 @@
|
||||
<h1 class="post__title">Live Ozan Radio: A Personal AI Station in Cursor</h1>
|
||||
<p class="post__lead">I do not want a playlist. I want a station — something that feels like late-night desert dub and Anadolu psych drifting out of a speaker, but every track is composed fresh, never pulled from Spotify or Apple Music. So we built <strong>Live Ozan Radio</strong>: DeepSeek as the on-air DJ, Google Lyria 3 as the music engine, and our own Gitea instance as the host.
|
||||
|
||||
!<a href="https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png" style="color: var(--c-lime);">Live Ozan Radio in Cursor — player dashboard, saved songs, and DJ chat beside the editor</a>
|
||||
!<a href="https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png" style="color: var(–c-accent-l);">Live Ozan Radio in Cursor — player dashboard, saved songs, and DJ chat beside the editor</a>
|
||||
|
||||
That screenshot is how I actually use it: Cursor on the left with taste notes and vocal cues, the local player on <code>:8787</code> on the right, saved songs in a scrollable library, and a chat box to steer the next generation. It is dogfooding in the truest sense — we run our game studio on the same Gitea fork we sell as Tinqs Studio, and the radio lives in that repo too.
|
||||
|
||||
|
||||
|
Before
After
|
+218
-227
@@ -20,10 +20,6 @@
|
||||
<meta name="twitter:description" content="Pi flows + oracle-backed gates: agents that compile, test, drive the game, measure feel, fix CI, and ship green PRs.">
|
||||
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
@@ -47,72 +43,69 @@
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -131,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -154,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -188,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -244,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -261,41 +250,41 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Analogy callout box ── */
|
||||
.post__body .callout {
|
||||
background: linear-gradient(135deg, rgba(124,92,255,0.06), rgba(182,255,60,0.04));
|
||||
border: 1px solid rgba(124,92,255,0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: linear-gradient(135deg, rgba(56,189,248,0.06), rgba(168,85,247,0.06));
|
||||
border: 1px solid rgba(56,189,248,0.2);
|
||||
border-left: 4px solid #38bdf8;
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 18px 20px;
|
||||
margin: 22px 0;
|
||||
}
|
||||
.post__body .callout--amber {
|
||||
background: linear-gradient(135deg, rgba(182,255,60,0.06), rgba(124,92,255,0.04));
|
||||
border-color: rgba(182,255,60,0.15);
|
||||
border-left-color: var(--c-lime);
|
||||
background: linear-gradient(135deg, rgba(245,158,11,0.07), rgba(201,147,90,0.05));
|
||||
border-color: rgba(245,158,11,0.25);
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
.post__body .callout--purple {
|
||||
background: linear-gradient(135deg, rgba(124,92,255,0.07), rgba(182,255,60,0.04));
|
||||
border-color: rgba(124,92,255,0.2);
|
||||
border-left-color: var(--c-violet);
|
||||
background: linear-gradient(135deg, rgba(168,85,247,0.07), rgba(56,189,248,0.04));
|
||||
border-color: rgba(168,85,247,0.25);
|
||||
border-left-color: #a855f7;
|
||||
}
|
||||
.post__body .callout__kicker {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-violet);
|
||||
color: #38bdf8;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.post__body .callout--amber .callout__kicker { color: var(--c-lime); }
|
||||
.post__body .callout--purple .callout__kicker { color: var(--c-violet); }
|
||||
.post__body .callout p { margin: 6px 0 0; color: var(--c-fg); }
|
||||
.post__body .callout--amber .callout__kicker { color: #f59e0b; }
|
||||
.post__body .callout--purple .callout__kicker { color: #a855f7; }
|
||||
.post__body .callout p { margin: 6px 0 0; color: #cdd7e2; }
|
||||
.post__body .callout p + p { margin-top: 10px; }
|
||||
|
||||
/* ── Gate badge pills ── */
|
||||
@@ -308,18 +297,18 @@
|
||||
border-radius: 5px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.gate--build { background: rgba(124,92,255,0.12); color: #7C5CFF; border: 1px solid rgba(124,92,255,0.3); }
|
||||
.gate--test { background: rgba(182,255,60,0.10); color: #B6FF3C; border: 1px solid rgba(182,255,60,0.25); }
|
||||
.gate--behave { background: rgba(124,92,255,0.12); color: #7C5CFF; border: 1px solid rgba(124,92,255,0.3); }
|
||||
.gate--feel { background: rgba(182,255,60,0.10); color: #B6FF3C; border: 1px solid rgba(182,255,60,0.25); }
|
||||
.gate--visual { background: rgba(124,92,255,0.10); color: #8A95A3; border: 1px solid rgba(124,92,255,0.2); }
|
||||
.gate--build { background: rgba(56,189,248,0.12); color: #38bdf8; border: 1px solid rgba(56,189,248,0.3); }
|
||||
.gate--test { background: rgba(52,211,153,0.12); color: #34d399; border: 1px solid rgba(52,211,153,0.3); }
|
||||
.gate--behave { background: rgba(168,85,247,0.12); color: #a855f7; border: 1px solid rgba(168,85,247,0.3); }
|
||||
.gate--feel { background: rgba(245,158,11,0.12); color: #f59e0b; border: 1px solid rgba(245,158,11,0.3); }
|
||||
.gate--visual { background: rgba(201,147,90,0.12); color: #c9935a; border: 1px solid rgba(201,147,90,0.3); }
|
||||
|
||||
/* ── Section divider accent ── */
|
||||
.post__body hr { border-color: var(--c-border); margin: 36px 0; }
|
||||
.post__body hr { border-color: #2a3340; margin: 36px 0; }
|
||||
.post__body hr.accent {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--c-lime) 20%, var(--c-violet) 50%, var(--c-lime) 80%, transparent);
|
||||
background: linear-gradient(90deg, transparent, #38bdf8 20%, #a855f7 50%, #f59e0b 80%, transparent);
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
@@ -332,8 +321,8 @@
|
||||
}
|
||||
@media (max-width: 640px) { .kitchen-grid { grid-template-columns: 1fr; } }
|
||||
.kitchen-col {
|
||||
background: var(--c-bg-raised);
|
||||
border: 1px solid var(--c-border);
|
||||
background: #0c1119;
|
||||
border: 1px solid #2a3340;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
@@ -345,10 +334,10 @@
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
.kitchen-col__title--kitchen { color: var(--c-lime); }
|
||||
.kitchen-col__title--reality { color: var(--c-violet); }
|
||||
.kitchen-col p { font-size: 0.9rem; color: #8A95A3; margin: 4px 0; }
|
||||
.kitchen-col p strong { color: var(--c-fg); }
|
||||
.kitchen-col__title--kitchen { color: #f59e0b; }
|
||||
.kitchen-col__title--reality { color: #38bdf8; }
|
||||
.kitchen-col p { font-size: 0.9rem; color: #9aa7b4; margin: 4px 0; }
|
||||
.kitchen-col p strong { color: #e6edf3; }
|
||||
|
||||
/* ── Table styles ── */
|
||||
.post__body table {
|
||||
@@ -366,7 +355,7 @@
|
||||
}
|
||||
.post__body td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
border-bottom: 1px solid #1c2230;
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
@@ -383,47 +372,47 @@
|
||||
|
||||
<div class="callout">
|
||||
<span class="callout__kicker">The Kitchen ↔ Flows Analogy</span>
|
||||
<p><strong>The kitchen</strong> = Pi (the agent harness). <strong>The recipe</strong> = a JavaScript flow (<code>.flow.mjs</code>). <strong>The line cooks</strong> = agents (each with a station and tools). <strong>The pass</strong> = the flow engine (routes finished work). <strong>The head chef's inspection</strong> = the five gates. <strong>The order ticket</strong> = a spawn task or <code>tinqs flow run</code>. <strong>"Send it back!"</strong> = the fix loop.</p>
|
||||
<p><strong>The kitchen</strong> = Pi (the agent harness). <strong>The recipe</strong> = a flow YAML (the DAG). <strong>The line cooks</strong> = agents (each with a station and tools). <strong>The pass</strong> = the flow engine (routes finished work). <strong>The head chef's inspection</strong> = the five gates. <strong>The order ticket</strong> = a slash command. <strong>"Send it back!"</strong> = the fix loop.</p>
|
||||
</div>
|
||||
|
||||
<h2>What Happens When You Spawn a Flow</h2>
|
||||
<p>You run <code>tinqs flow run game-feature --task 'add a double-jump with cooldown'</code> or click Run Flow on the dashboard. The ticket hits the kitchen. What follows is not one agent doing everything — it's a brigade running their stations.</p>
|
||||
<h2>What Happens When You Type a Slash Command</h2>
|
||||
<p>You type <code>/game-feature add a double-jump with cooldown</code> and hit enter. The ticket hits the kitchen. What follows is not one agent doing everything — it's a brigade running their stations.</p>
|
||||
|
||||
<figure style="margin:28px 0;">
|
||||
<svg viewBox="0 0 920 350" role="img" aria-label="The verify-heavy flow: context, plan, implement, five gates, a Reflexion loop, and one judge" style="width:100%;height:auto;display:block;background:#0B0C0E;border:1px solid rgba(255,255,255,0.07);border-radius:12px;font-family:'JetBrains Mono',ui-monospace,monospace;">
|
||||
<svg viewBox="0 0 920 350" role="img" aria-label="The verify-heavy flow: context, plan, implement, five gates, a Reflexion loop, and one judge" style="width:100%;height:auto;display:block;background:#0a0e14;border:1px solid #2a3340;border-radius:12px;font-family:'IBM Plex Sans',system-ui,sans-serif;">
|
||||
<defs>
|
||||
<marker id="ah" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#8A95A3"/></marker>
|
||||
<marker id="ahA" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#B6FF3C"/></marker>
|
||||
<marker id="ah" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#5b6b7d"/></marker>
|
||||
<marker id="ahA" markerWidth="10" markerHeight="10" refX="7" refY="3.2" orient="auto"><path d="M0,0 L7,3.2 L0,6.4 Z" fill="#f59e0b"/></marker>
|
||||
</defs>
|
||||
<rect x="40" y="40" width="140" height="46" rx="9" fill="#15171A" stroke="rgba(255,255,255,0.07)"/>
|
||||
<text x="110" y="68" text-anchor="middle" fill="#8A95A3" font-size="15">Context</text>
|
||||
<rect x="210" y="40" width="140" height="46" rx="9" fill="#15171A" stroke="rgba(255,255,255,0.07)"/>
|
||||
<text x="280" y="68" text-anchor="middle" fill="#8A95A3" font-size="15">Plan</text>
|
||||
<rect x="400" y="40" width="150" height="46" rx="9" fill="#15171A" stroke="rgba(255,255,255,0.07)"/>
|
||||
<text x="475" y="68" text-anchor="middle" fill="#ECEEF1" font-size="15">Implement</text>
|
||||
<line x1="180" y1="63" x2="206" y2="63" stroke="#8A95A3" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<line x1="350" y1="63" x2="396" y2="63" stroke="#8A95A3" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<rect x="40" y="150" width="840" height="82" rx="12" fill="#15171A" stroke="rgba(255,255,255,0.07)"/>
|
||||
<text x="56" y="171" fill="#8A95A3" font-size="11" letter-spacing="1.4">VERIFY-HEAVY GATES — most compute is spent checking, not writing</text>
|
||||
<rect x="56" y="180" width="148" height="42" rx="8" fill="#15171A" stroke="#7C5CFF" stroke-opacity="0.55"/>
|
||||
<text x="130" y="206" text-anchor="middle" fill="#7C5CFF" font-size="13.5">G1 · Build</text>
|
||||
<rect x="222" y="180" width="148" height="42" rx="8" fill="#15171A" stroke="#B6FF3C" stroke-opacity="0.55"/>
|
||||
<text x="296" y="206" text-anchor="middle" fill="#B6FF3C" font-size="13.5">G2 · Tests</text>
|
||||
<rect x="388" y="180" width="148" height="42" rx="8" fill="#15171A" stroke="#7C5CFF" stroke-opacity="0.55"/>
|
||||
<text x="462" y="206" text-anchor="middle" fill="#B6FF3C" font-size="13.5">G3 · Behaviour</text>
|
||||
<rect x="554" y="180" width="148" height="42" rx="8" fill="#15171A" stroke="#B6FF3C" stroke-opacity="0.55"/>
|
||||
<text x="628" y="206" text-anchor="middle" fill="#B6FF3C" font-size="13.5">G4 · Feel</text>
|
||||
<rect x="720" y="180" width="148" height="42" rx="8" fill="#15171A" stroke="rgba(255,255,255,0.15)" stroke-opacity="0.55"/>
|
||||
<text x="794" y="206" text-anchor="middle" fill="#8A95A3" font-size="13.5">G5 · Visual</text>
|
||||
<line x1="475" y1="86" x2="475" y2="148" stroke="#8A95A3" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<line x1="460" y1="232" x2="460" y2="276" stroke="#8A95A3" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<text x="472" y="258" fill="#8A95A3" font-size="11">all green ⇒ done · any fail ⇒ report</text>
|
||||
<rect x="380" y="278" width="160" height="46" rx="9" fill="#15171A" stroke="#7C5CFF"/>
|
||||
<text x="460" y="306" text-anchor="middle" fill="#7C5CFF" font-size="15">Judge — honest verdict</text>
|
||||
<path d="M820,150 C 908,96 716,50 556,61" fill="none" stroke="#B6FF3C" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#ahA)"/>
|
||||
<text x="694" y="96" fill="#B6FF3C" font-size="12.5">Reflexion · fix & retry ≤ 3</text>
|
||||
<rect x="40" y="40" width="140" height="46" rx="9" fill="#121821" stroke="#2a3340"/>
|
||||
<text x="110" y="68" text-anchor="middle" fill="#cdd7e2" font-size="15">Context</text>
|
||||
<rect x="210" y="40" width="140" height="46" rx="9" fill="#121821" stroke="#2a3340"/>
|
||||
<text x="280" y="68" text-anchor="middle" fill="#cdd7e2" font-size="15">Plan</text>
|
||||
<rect x="400" y="40" width="150" height="46" rx="9" fill="#15202e" stroke="#3a4656"/>
|
||||
<text x="475" y="68" text-anchor="middle" fill="#e6edf3" font-size="15">Implement</text>
|
||||
<line x1="180" y1="63" x2="206" y2="63" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<line x1="350" y1="63" x2="396" y2="63" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<rect x="40" y="150" width="840" height="82" rx="12" fill="#0c1119" stroke="#2a3340"/>
|
||||
<text x="56" y="171" fill="#6b7a8d" font-size="11" letter-spacing="1.4">VERIFY-HEAVY GATES — most compute is spent checking, not writing</text>
|
||||
<rect x="56" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#38bdf8" stroke-opacity="0.55"/>
|
||||
<text x="130" y="206" text-anchor="middle" fill="#38bdf8" font-size="13.5">G1 · Build</text>
|
||||
<rect x="222" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#34d399" stroke-opacity="0.55"/>
|
||||
<text x="296" y="206" text-anchor="middle" fill="#9fe6c0" font-size="13.5">G2 · Tests</text>
|
||||
<rect x="388" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#a855f7" stroke-opacity="0.55"/>
|
||||
<text x="462" y="206" text-anchor="middle" fill="#c4a0f7" font-size="13.5">G3 · Behaviour</text>
|
||||
<rect x="554" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#f59e0b" stroke-opacity="0.55"/>
|
||||
<text x="628" y="206" text-anchor="middle" fill="#f5b44b" font-size="13.5">G4 · Feel</text>
|
||||
<rect x="720" y="180" width="148" height="42" rx="8" fill="#10141c" stroke="#c9935a" stroke-opacity="0.55"/>
|
||||
<text x="794" y="206" text-anchor="middle" fill="#d9ac7b" font-size="13.5">G5 · Visual</text>
|
||||
<line x1="475" y1="86" x2="475" y2="148" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<line x1="460" y1="232" x2="460" y2="276" stroke="#5b6b7d" stroke-width="1.6" marker-end="url(#ah)"/>
|
||||
<text x="472" y="258" fill="#6b7a8d" font-size="11">all green ⇒ done · any fail ⇒ report</text>
|
||||
<rect x="380" y="278" width="160" height="46" rx="9" fill="#1b1505" stroke="#c9935a"/>
|
||||
<text x="460" y="306" text-anchor="middle" fill="#f3d6a0" font-size="15">Judge — honest verdict</text>
|
||||
<path d="M820,150 C 908,96 716,50 556,61" fill="none" stroke="#f59e0b" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#ahA)"/>
|
||||
<text x="694" y="96" fill="#f59e0b" font-size="12.5">Reflexion · fix & retry ≤ 3</text>
|
||||
</svg>
|
||||
<figcaption style="color:#8A95A3;font-size:0.85rem;margin-top:8px;">A real failure loops back to <em>implement</em> with gate evidence (bounded to three tries); anything green falls through to the judge.</figcaption>
|
||||
<figcaption style="color:#9aa7b4;font-size:0.85rem;margin-top:8px;">A real failure loops back to <em>implement</em> with gate evidence (bounded to three tries); anything green falls through to the judge.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2>The Five Gates: What the Head Chef Checks</h2>
|
||||
@@ -468,7 +457,7 @@
|
||||
</div>
|
||||
<div class="kitchen-col">
|
||||
<span class="kitchen-col__title kitchen-col__title--reality">In the Flow</span>
|
||||
<p><span class="gate gate--visual">G5 · Visual</span> Captures 8 frames at 100ms intervals, grids them, feeds to <code>gemini-2.5-flash</code>. Checks: T-pose? Foot-slide? Frozen animation? Wrong clip? Missing transitions?</p>
|
||||
<p><span class="gate gate--visual">G5 · Visual</span> Captures 8 frames at 100ms intervals, grids them, feeds to <code>minimax-latest</code>. Checks: T-pose? Foot-slide? Frozen animation? Wrong clip? Missing transitions?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -478,7 +467,7 @@
|
||||
</div>
|
||||
|
||||
<h2>Composability: Adding a New Station</h2>
|
||||
<p>A kitchen doesn't redesign the whole line when they add a new dish. They add a station. Same in flows. Started with three gates — build, test, vision. Behaviour and feel came later, each a single-file extension. Gates aren't hardcoded. They're sub-agents called from JavaScript flows. Want a linting gate? Add an <code>agent()</code> call with a linter. Security scan? Same pattern. Asset bundle size check? Write the tool, declare the agent, wire it in.</p>
|
||||
<p>A kitchen doesn't redesign the whole line when they add a new dish. They add a station. Same in flows. Started with three gates — build, test, vision. Behaviour and feel came later, each a single-file extension. Gates aren't hardcoded. They're sub-agents declared in YAML. Want a linting gate? Add a sub-agent with a linter. Security scan? Same pattern. Asset bundle size check? Write the tool, declare the agent, wire it in.</p>
|
||||
|
||||
<div class="callout callout--purple">
|
||||
<span class="callout__kicker">Self-Improving Kitchen</span>
|
||||
@@ -526,19 +515,19 @@
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;margin:18px 0;font-size:0.92rem;">
|
||||
<thead>
|
||||
<tr style="text-align:left;border-bottom:1px solid rgba(255,255,255,0.07);">
|
||||
<th style="padding:10px 12px;color:#B6FF3C;font-weight:600;">Layer</th>
|
||||
<th style="padding:10px 12px;color:#B6FF3C;font-weight:600;">What</th>
|
||||
<th style="padding:10px 12px;color:#B6FF3C;font-weight:600;">How</th>
|
||||
<tr style="text-align:left;border-bottom:1px solid #2a3340;">
|
||||
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">Layer</th>
|
||||
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">What</th>
|
||||
<th style="padding:10px 12px;color:#c9935a;font-weight:600;">How</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">Flow engine</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">pi-flows orchestrator</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Composes agents, gates and decision points</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">Oracle gates</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">verify_build, drive_game, game_frames</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Return structured PASS/FAIL with evidence</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">Sub-agents</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">G1 build · G2 tests · G3 behaviour · G4 feel · G5 visual</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Role-split, each with its own toolset</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">CI loop</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">tinqs-ci extension</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">ci_status, ci_logs, ci_wait — polls Gitea Actions, reads logs, retries</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">Decision</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Agent-loop Reflexion</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Self-reflect on failures, retry (≤3) or escalate</td></tr>
|
||||
<tr><td style="padding:9px 12px;color:#ECEEF1;vertical-align:top;"><strong style="color:#B6FF3C;">Visualization</strong></td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">FlowDashboard</td><td style="padding:9px 12px;color:#8A95A3;vertical-align:top;">Real-time pipeline state</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Flow engine</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">pi-flows orchestrator</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Composes agents, gates and decision points</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Oracle gates</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">verify_build, drive_game, game_frames</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Return structured PASS/FAIL with evidence</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Sub-agents</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">G1 build · G2 tests · G3 behaviour · G4 feel · G5 visual</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Role-split, each with its own toolset</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">CI loop</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">tinqs-ci extension</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">ci_status, ci_logs, ci_wait — polls Gitea Actions, reads logs, retries</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Decision</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">Agent-loop Reflexion</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Self-reflect on failures, retry (≤3) or escalate</td></tr>
|
||||
<tr><td style="padding:9px 12px;color:#e6edf3;vertical-align:top;"><strong style="color:#f59e0b;">Visualization</strong></td><td style="padding:9px 12px;color:#cdd7e2;vertical-align:top;">FlowDashboard</td><td style="padding:9px 12px;color:#9aa7b4;vertical-align:top;">Real-time pipeline state</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -549,17 +538,17 @@
|
||||
|
||||
<div class="callout callout--amber">
|
||||
<span class="callout__kicker">Flow 1 · 4 June, 18:32</span>
|
||||
<p><strong>deep-implement</strong> — "Build the tinqs-gitea-read extension: list_org_repos, read_repo_file, list_repo_dir, search_repos." Nine steps, 14 minutes. Verdict: <span class="gate gate--test">PASS</span>. 31/31 vitest tests green, zero new TypeScript errors, session-level caching, path traversal protection. Every <code>execute()</code> body fully wired — no stubs, no placeholders. Like a saucier who doesn't just list ingredients but actually makes the sauce.</p>
|
||||
<p><strong>/deep-implement</strong> — "Build the tinqs-gitea-read extension: list_org_repos, read_repo_file, list_repo_dir, search_repos." Nine steps, 14 minutes. Verdict: <span class="gate gate--test">PASS</span>. 31/31 vitest tests green, zero new TypeScript errors, session-level caching, path traversal protection. Every <code>execute()</code> body fully wired — no stubs, no placeholders. Like a saucier who doesn't just list ingredients but actually makes the sauce.</p>
|
||||
</div>
|
||||
|
||||
<div class="callout callout--purple">
|
||||
<span class="callout__kicker">Flow 2 · 4 June, 19:04</span>
|
||||
<p><strong>game-feature</strong> — "Make the player jump." Build: <span class="gate gate--build">PASS</span>. Tests: <span class="gate gate--test">PASS</span>. Behaviour/Feel/Visual: <span style="color:#B6FF3C;">NOT RUN</span> — no live game instance was reachable. The flow didn't silently skip the visual gate. It <strong>hard-stopped</strong> and reported honestly: "FAIL — the feature has not been verified in-game." This is the kitchen saying: "The dish is cooked, but nobody tasted it. I'm not sending it out."</p>
|
||||
<p><strong>/game-feature</strong> — "Make the player jump." Build: <span class="gate gate--build">PASS</span>. Tests: <span class="gate gate--test">PASS</span>. Behaviour/Feel/Visual: <span style="color:#f59e0b;">NOT RUN</span> — no live game instance was reachable. The flow didn't silently skip the visual gate. It <strong>hard-stopped</strong> and reported honestly: "FAIL — the feature has not been verified in-game." This is the kitchen saying: "The dish is cooked, but nobody tasted it. I'm not sending it out."</p>
|
||||
</div>
|
||||
|
||||
<div class="callout callout--amber">
|
||||
<span class="callout__kicker">Flow 3 · 4 June, 19:49</span>
|
||||
<p><strong>cto-infra</strong> — "Synthesize cost, stability, and VCS research into an AWS architecture decision." Four research streams fed into one CTO agent. Output: 14 requirements mapped to specific decisions, cost-vs-stability tradeoffs resolved with dollar figures, EC2+EBS over Fargate+EFS, RDS Multi-AZ mandatory, S3+CloudFront for LFS. Like an executive chef reading four menu proposals, reconciling them into one service, and pricing every plate.</p>
|
||||
<p><strong>/cto-infra</strong> — "Synthesize cost, stability, and VCS research into an AWS architecture decision." Four research streams fed into one CTO agent. Output: 14 requirements mapped to specific decisions, cost-vs-stability tradeoffs resolved with dollar figures, EC2+EBS over Fargate+EFS, RDS Multi-AZ mandatory, S3+CloudFront for LFS. Like an executive chef reading four menu proposals, reconciling them into one service, and pricing every plate.</p>
|
||||
</div>
|
||||
|
||||
<hr class="accent">
|
||||
@@ -567,95 +556,97 @@
|
||||
<h2>Dinner Rush Recovery: The Crash That Interrupted Service</h2>
|
||||
<p>Earlier today, a machine crash cut off a flow mid-stream — the kitchen lost power during dinner rush. Nineteen tests were left red. Contracts written, implementation half-done. Half-cooked dishes on every station.</p>
|
||||
|
||||
<p>I spawned the same flow with a different task:</p>
|
||||
<p>I typed one slash command — the expediter reassembled the brigade:</p>
|
||||
|
||||
<pre><code>tinqs flow run game-feature --task 'Finish the leftover jump & locomotion animation work -- make the 19 FAILING tests GREEN.'</code></pre>
|
||||
<pre><code>/game-feature Finish the leftover jump & locomotion animation work — make the 19 FAILING tests GREEN.</code></pre>
|
||||
|
||||
<p>What happened next: the team picked up exactly where the crash left off. Here's the recipe — the exact JavaScript that runs in production:</p>
|
||||
<p>What happened next: the team picked up exactly where the crash left off. Here's the recipe — the exact YAML that runs in production:</p>
|
||||
|
||||
<pre><code>// .pi/flows/flows/game-feature.flow.mjs
|
||||
export const meta = {
|
||||
name: "game-feature",
|
||||
description: "Build a PLAYABLE game feature and prove it in the LIVE game.",
|
||||
task_required: true
|
||||
};
|
||||
<pre><code>name: game-feature
|
||||
description: Build a PLAYABLE game feature and prove it in the LIVE game.
|
||||
task_required: true
|
||||
|
||||
export default async function run({ task, flow }) {
|
||||
// G0: Pre-flight — validate vision CAN run before any build work
|
||||
await flow.agent("vision-preflight", {
|
||||
task: "Check GEMINI_API_KEY is set AND game_frames reaches a live instance."
|
||||
});
|
||||
steps:
|
||||
# G0: Pre-flight — validate vision CAN run before any build work
|
||||
- id: preflight
|
||||
agent: vision-preflight
|
||||
task: Check MINIMAX_API_KEY is set AND game_frames reaches a live instance.
|
||||
If EITHER fails, STOP — vision is not optional.
|
||||
|
||||
// Context + plan
|
||||
const context = await flow.agent("project-context-reader");
|
||||
const plan = await flow.agent("feature-planner", { context });
|
||||
# Context + plan
|
||||
- id: context
|
||||
agent: project-context-reader
|
||||
blockedBy: [preflight]
|
||||
|
||||
// TDD: write tests FIRST (different agent than implementer)
|
||||
const testSuite = await flow.agent("test-author", { plan });
|
||||
- id: plan
|
||||
agent: feature-planner
|
||||
blockedBy: [context]
|
||||
|
||||
// Implement
|
||||
const source = await flow.agent("game-builder", { testSuite, plan });
|
||||
# TDD: write tests FIRST (different agent than implementer)
|
||||
- id: test-author
|
||||
agent: test-author
|
||||
blockedBy: [plan]
|
||||
|
||||
// G1–G5: Oracle gates run via parallel for speed
|
||||
const gates = await flow.parallel([
|
||||
flow.agent("build-verifier", { source }),
|
||||
flow.agent("test-runner", { source }),
|
||||
flow.agent("behavioral-prober", { source }),
|
||||
flow.agent("feel-judge", { source }),
|
||||
flow.agent("animation-vision-judge", { source })
|
||||
]);
|
||||
- id: implement
|
||||
agent: game-builder
|
||||
blockedBy: [test-author]
|
||||
|
||||
// Self-recurring fix-loop: bounded loop back to implement with evidence
|
||||
const MAX_RETRIES = 3;
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
const decision = await flow.agent("flow-decision", { gates });
|
||||
if (decision.verdict === "pass") break;
|
||||
if (attempt === MAX_RETRIES) {
|
||||
const fixed = await flow.agent("game-builder", { source, failures: decision.evidence });
|
||||
}
|
||||
}
|
||||
# G1–G5: Oracle gates (build, tests, behaviour, feel, visual)
|
||||
- id: build → agent: build-verifier
|
||||
- id: tests → agent: test-runner
|
||||
- id: behavior → agent: behavioral-prober (drives LIVE game via drive_game)
|
||||
- id: feel → agent: feel-judge (apex, airtime, latency, rise/fall)
|
||||
- id: visual → agent: animation-vision-judge (multimodal minimax-latest)
|
||||
|
||||
// Final judge: one honest verdict
|
||||
return flow.agent("game-judge");
|
||||
}</code></pre>
|
||||
# Self-recurring fix-loop: bounded loop back to implement with evidence
|
||||
- id: fix-loop
|
||||
type: agent-loop-decision
|
||||
agent: flow-decision
|
||||
loop_target: implement
|
||||
exit_target: report
|
||||
max_iterations: 3
|
||||
|
||||
<p>Eight logical steps, seven cooks, five inspection points, one head chef. Triggered by a single spawn.</p>
|
||||
# Final judge: one honest verdict
|
||||
- id: report
|
||||
agent: game-judge</code></pre>
|
||||
|
||||
<p>Here's how the brigade actually worked. The <strong>vision-preflight</strong> agent — the chef who checks the gas is on before anyone starts cooking — verified <code>GEMINI_API_KEY</code> was set and <code>game_frames</code> could reach the live game. Both green in under a second. Without this, the whole kitchen would prep for an hour only to discover the oven doesn't work.</p>
|
||||
<p>Eighteen steps, seven cooks, five inspection points, one head chef. Triggered by a single order ticket.</p>
|
||||
|
||||
<p>Here's how the brigade actually worked. The <strong>vision-preflight</strong> agent — the chef who checks the gas is on before anyone starts cooking — verified <code>MINIMAX_API_KEY</code> was set and <code>game_frames</code> could reach the live game. Both green in under a second. Without this, the whole kitchen would prep for an hour only to discover the oven doesn't work.</p>
|
||||
|
||||
<p>The <strong>project-context-reader</strong> — the commis who reads the entire recipe book — ingested <code>PlayerController.cs</code>, <code>PlayerAnimController.cs</code>, <code>PlayerAnimationLogic.cs</code>, the test files, the manifest. The <strong>feature-planner</strong> — the sous-chef who breaks down the order into station tasks — decomposed 19 failures into four fix groups: vegetation manifest (146 broken <code>prefabPath</code> items), animation controller (crouch parameter not plumbed), jump physics (coyote time, variable height, air control — all missing), and animation tree (entire state machine absent).</p>
|
||||
|
||||
<p>Then the <strong>game-builder</strong> — the line cook at the hot station — read each test failure like a dish ticket, traced it to the source, and started cooking. Coyote time: 100ms grace period after feet leave the ground. Variable jump height: velocity scaled by hold duration, tap gives 3.5, full hold gives 6.5. Air control: horizontal speed cut 40% while airborne. Jump phases: minimum 0.15s on jump_start before transitioning up. Landing timer: wait the full animation length, not length-minus-blend. Animation tree: <code>jump_start → jump → jump_land</code> states with 0.1s blends.</p>
|
||||
|
||||
<p>Then the inspection line: <strong>build-verifier</strong> compiled. <strong>Test-runner</strong> ran the suite. <strong>Behavioral-prober</strong> sent <code>{"jump":true}</code> to the live game and sampled the player body. <strong>Feel-judge</strong> measured apex, airtime, liftoff latency. <strong>Animation-vision-judge</strong> captured 8 frames, gridded them, had <code>gemini-2.5-flash</code> scan for T-poses and foot-slide.</p>
|
||||
<p>Then the inspection line: <strong>build-verifier</strong> compiled. <strong>Test-runner</strong> ran the suite. <strong>Behavioral-prober</strong> sent <code>{"jump":true}</code> to the live game and sampled the player body. <strong>Feel-judge</strong> measured apex, airtime, liftoff latency. <strong>Animation-vision-judge</strong> captured 8 frames, gridded them, had <code>minimax-latest</code> scan for T-poses and foot-slide.</p>
|
||||
|
||||
<p>Anything red → ticket back to the cook with the specific failure → fix → re-enter the line. Bounded to 3 returns. Anything green → falls through. All green → <strong>game-judge</strong> gives the final verdict.</p>
|
||||
|
||||
<div class="callout">
|
||||
<span class="callout__kicker">Not a Demo</span>
|
||||
<p>This flow is a file at <code>.pi/flows/flows/game-feature.flow.mjs</code>. I trigger it by running <code>tinqs flow run game-feature</code> or clicking Run Flow on the dashboard. It dispatches agents, runs gates, loops on failures, reports a verdict. The dashboard at <code>:33634</code> is the control plane — spawn, steer mid-run, inspect state. That's the whole product.</p>
|
||||
<p>This flow is a file at <code>.pi/flows/flows/game-feature.yaml</code>. I trigger it by typing <code>/game-feature</code> in Pi. It dispatches agents, runs gates, loops on failures, reports a verdict. There is no dashboard with drag-and-drop. There is a YAML file and a slash command. That's the whole product.</p>
|
||||
</div>
|
||||
|
||||
<hr class="accent">
|
||||
|
||||
<h2>The Menu: Flows at Your Fingertips</h2>
|
||||
<p>Every flow lives in <code>.pi/flows/flows/*.flow.mjs</code> and is spawnable by name. You run <code>tinqs flow run <name> [task]</code> or click Run Flow on the dashboard.</p>
|
||||
<h2>The Menu: Flows Are Slash Commands</h2>
|
||||
<p>Every flow becomes a slash command — the menu you read to the expediter. <code>.pi/flows/flows/game-feature.yaml</code> → <code>/game-feature</code>. You don't invoke a pipeline from a terminal. You order a dish in conversation.</p>
|
||||
|
||||
<p>"Add wall-running" becomes the task argument. The flow reads it, wires it through the agents, routes it through the gates. The JavaScript is the recipe. The conversation provides the context.</p>
|
||||
<p>"Add wall-running" is not a CLI flag. It's natural language. The flow reads it, wires it through the agents, routes it through the gates. The YAML is the recipe. The conversation is the context.</p>
|
||||
|
||||
<p>The menu I call from daily:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>game-feature</strong> — "add a double-jump" or "fix the 19 red tests" → brigade assembles, cooks, inspects, plates</li>
|
||||
<li><strong>deep-implement</strong> — "build the gitea-read extension" → research → plan → implement → test → review → judge</li>
|
||||
<li><strong>cto-infra</strong> — "reconcile cost, stability, and VCS research into architecture decisions" → 4 research streams → 1 synthesis agent → 14 requirements mapped to decisions</li>
|
||||
<li><strong>flows:new</strong> — "I need a flow that..." → the Flow Architect reads the agent catalog, selects cooks, designs the recipe, writes the <code>.flow.mjs</code></li>
|
||||
<li><strong>/game-feature</strong> — "add a double-jump" or "fix the 19 red tests" → brigade assembles, cooks, inspects, plates</li>
|
||||
<li><strong>/deep-implement</strong> — "build the gitea-read extension" → research → plan → implement → test → review → judge</li>
|
||||
<li><strong>/cto-infra</strong> — "reconcile cost, stability, and VCS research into architecture decisions" → 4 research streams → 1 synthesis agent → 14 requirements mapped to decisions</li>
|
||||
<li><strong>/flows:new</strong> — "I need a flow that..." → the Flow Architect reads the agent catalog, selects cooks, designs the recipe, writes the YAML</li>
|
||||
</ul>
|
||||
|
||||
<h2>The Pass: How Agents Hand Off Work</h2>
|
||||
<p>In a real kitchen, cooks don't shout instructions across the room. They place finished plates on the pass. The expediter reads the ticket, checks the plate, routes it to the next station or to the dining room. Nobody yells. Nobody grabs someone else's pan.</p>
|
||||
|
||||
<p>Flows work the same way. Agents never talk to each other directly. When the game-builder finishes, it returns a result object — placing its work on the pass. The flow engine — the expediter — records it and routes it. The next agent receives the return value directly from <code>await flow.agent("game-builder")</code>.</p>
|
||||
<p>Flows work the same way. Agents never talk to each other directly. When the game-builder finishes, it doesn't ping the test-runner. It calls <code>finish({ summary: "...", artifacts: "...", files: "..." })</code> — placing its work on the pass. The flow engine — the expediter — records it and routes it. The next agent receives exactly the inputs wired in the YAML: <code>${{result.game-builder.summary}}</code>, <code>${{result.game-builder.files}}</code>.</p>
|
||||
|
||||
<div class="kitchen-grid">
|
||||
<div class="kitchen-col">
|
||||
@@ -664,39 +655,39 @@ export default async function run({ task, flow }) {
|
||||
</div>
|
||||
<div class="kitchen-col">
|
||||
<span class="kitchen-col__title kitchen-col__title--reality">What Actually Happens</span>
|
||||
<p>Agent A returns <code>{ verdict: "pass", findings: ["coyote_time=100ms"] }</code> → flow engine records it → Agent B receives the result as a direct return value of <code>await flow.agent("A")</code>. No chatter. Structured handoff.</p>
|
||||
<p>Agent A → <code>finish({verdict: "pass", findings: ["coyote_time=100ms"]})</code> → engine records → Agent B receives <code>${{result.A.findings}}</code> via <code>inputs:</code> block. No chatter. Structured handoff.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Why? Because unstructured chatter is how hallucination cascades start. Agent A confidently states something wrong. Agent B builds on it. Agent C compounds it. Three agents later, they're collectively wrong about a file that doesn't exist, and nobody can trace where the error came from. The pass — structured result-passing via typed return values from each <code>agent()</code> call — makes every handoff auditable, verifiable, and debuggable.</p>
|
||||
<p>Why? Because unstructured chatter is how hallucination cascades start. Agent A confidently states something wrong. Agent B builds on it. Agent C compounds it. Three agents later, they're collectively wrong about a file that doesn't exist, and nobody can trace where the error came from. The pass — structured result-passing with typed outputs — makes every handoff auditable, verifiable, and debuggable.</p>
|
||||
|
||||
<p>Pi itself is built for solo interactive work: you ask, it does, you review. The orchestration layer I wrote on top inverts that. Pi becomes the kitchen. The flow engine becomes the expediter. Agents become line cooks who place plates on the pass, never shouting across the room.</p>
|
||||
|
||||
<h2>The Setup: Extensions, Agents, and 15–20 Flows</h2>
|
||||
<p>"How did you set this up?" is the question I get most often. Here's the honest answer: there's no dashboard with drag-and-drop. You write three kinds of files.</p>
|
||||
|
||||
<p><strong style="color:#B6FF3C;">Extensions</strong> are TypeScript tools that agents call. Each is about 300 lines, MIT licensed:</p>
|
||||
<p><strong style="color:#f59e0b;">Extensions</strong> are TypeScript tools that agents call. Each is about 300 lines, MIT licensed:</p>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;margin:18px 0;font-size:0.89rem;">
|
||||
<thead>
|
||||
<tr style="text-align:left;border-bottom:1px solid rgba(255,255,255,0.07);">
|
||||
<th style="padding:8px 12px;color:#B6FF3C;">Extension</th>
|
||||
<th style="padding:8px 12px;color:#B6FF3C;">What agents call it for</th>
|
||||
<tr style="text-align:left;border-bottom:1px solid #2a3340;">
|
||||
<th style="padding:8px 12px;color:#c9935a;">Extension</th>
|
||||
<th style="padding:8px 12px;color:#c9935a;">What agents call it for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>verify_build</code></td><td style="padding:7px 12px;color:#8A95A3;">Compile the game + sim, return file:line errors</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>drive_game</code></td><td style="padding:7px 12px;color:#8A95A3;">Send input to the live game, sample player body</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>game_frames</code></td><td style="padding:7px 12px;color:#8A95A3;">Capture screenshot sequences for vision judging</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>ci_status</code></td><td style="padding:7px 12px;color:#8A95A3;">Check Gitea Actions pipeline state for a branch</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>ci_logs</code></td><td style="padding:7px 12px;color:#8A95A3;">Fetch full build log from the most recent failed run</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>ci_wait</code></td><td style="padding:7px 12px;color:#8A95A3;">Poll every 15 seconds until the pipeline finishes</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>gen_image</code></td><td style="padding:7px 12px;color:#8A95A3;">Generate brand/marketing images via fal.ai flux-2-pro</td></tr>
|
||||
<tr><td style="padding:7px 12px;color:#ECEEF1;"><code>agent_catalog</code></td><td style="padding:7px 12px;color:#8A95A3;">List available agents with their tools, inputs, outputs</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>verify_build</code></td><td style="padding:7px 12px;color:#cdd7e2;">Compile the game + sim, return file:line errors</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>drive_game</code></td><td style="padding:7px 12px;color:#cdd7e2;">Send input to the live game, sample player body</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>game_frames</code></td><td style="padding:7px 12px;color:#cdd7e2;">Capture screenshot sequences for vision judging</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>ci_status</code></td><td style="padding:7px 12px;color:#cdd7e2;">Check Gitea Actions pipeline state for a branch</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>ci_logs</code></td><td style="padding:7px 12px;color:#cdd7e2;">Fetch full build log from the most recent failed run</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>ci_wait</code></td><td style="padding:7px 12px;color:#cdd7e2;">Poll every 15 seconds until the pipeline finishes</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>gen_image</code></td><td style="padding:7px 12px;color:#cdd7e2;">Generate brand/marketing images via fal.ai flux-2-pro</td></tr>
|
||||
<tr><td style="padding:7px 12px;color:#e6edf3;"><code>agent_catalog</code></td><td style="padding:7px 12px;color:#cdd7e2;">List available agents with their tools, inputs, outputs</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong style="color:#B6FF3C;">Agents</strong> are Markdown files with YAML frontmatter. Each declares its role, model tier, tools, inputs, and outputs:</p>
|
||||
<p><strong style="color:#f59e0b;">Agents</strong> are Markdown files with YAML frontmatter. Each declares its role, model tier, tools, inputs, and outputs:</p>
|
||||
|
||||
<pre><code>---
|
||||
name: game-builder
|
||||
@@ -709,17 +700,17 @@ outputs: [summary, files]
|
||||
You are a game developer. Task: ${{task}}
|
||||
Context: ${{input.context}}</code></pre>
|
||||
|
||||
<p><strong style="color:#B6FF3C;">Flows</strong> are JavaScript modules (<code>.flow.mjs</code>) that coordinate agents with real control flow. I have about <strong>15–20 flows</strong> running across different domains:</p>
|
||||
<p><strong style="color:#f59e0b;">Flows</strong> are YAML DAGs that wire agents together. I have about <strong>15–20 flows</strong> running across different domains:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Game dev:</strong> game-feature, review, bug-hunt, refactor</li>
|
||||
<li><strong>Design:</strong> concept-art, sound-design (plans → ElevenLabs generation → judge evaluates with other models)</li>
|
||||
<li><strong>Marketing:</strong> brand-image, trailer-clip (Sora 2 video generation → vision judge)</li>
|
||||
<li><strong>Infra:</strong> ci-fix, deploy-check, tinqs-jobs (action runners on AWS Lambda, workspace management)</li>
|
||||
<li><strong>Game dev:</strong> /game-feature, /review, /bug-hunt, /refactor</li>
|
||||
<li><strong>Design:</strong> /concept-art, /sound-design (plans → ElevenLabs generation → judge evaluates with other models)</li>
|
||||
<li><strong>Marketing:</strong> /brand-image, /trailer-clip (Sora 2 video generation → vision judge)</li>
|
||||
<li><strong>Infra:</strong> /ci-fix, /deploy-check, /tstudio-jobs (action runners on AWS Lambda, workspace management)</li>
|
||||
<li><strong>Meta:</strong> A flow that periodically reads and improves the other flows — yes, flows that edit flows</li>
|
||||
</ul>
|
||||
|
||||
<p>The setup is not a product you install. It's a stack: Pi as the agent harness, custom extensions as the tool layer, markdown agents as the role layer, JavaScript flows as the orchestration layer. The whole thing lives in <code>.pi/flows/</code>. Version-controlled. CI-tested. Spawned via <code>tinqs flow run</code> or the dashboard.</p>
|
||||
<p>The setup is not a product you install. It's a stack: Pi as the agent harness, custom extensions as the tool layer, markdown agents as the role layer, YAML flows as the orchestration layer. The whole thing lives in <code>.pi/flows/</code>. Version-controlled. CI-tested. Slash-command invoked.</p>
|
||||
|
||||
<h2>The Recipe vs. The Technique</h2>
|
||||
<p>"Do you define the process with these trees, or do the agents freestyle?" Both. The recipe says what to make and in what order. The technique is how each cook executes their station.</p>
|
||||
@@ -727,7 +718,7 @@ Context: ${{input.context}}</code></pre>
|
||||
<div class="kitchen-grid">
|
||||
<div class="kitchen-col">
|
||||
<span class="kitchen-col__title kitchen-col__title--kitchen">The Recipe (Rigid)</span>
|
||||
<p>The flow's JavaScript is the recipe. It says: first the prep cook dices onions, then the saucier makes the base, then the grill cook sears the protein. After every station, the plate hits the pass for inspection. <strong>This order is not negotiable.</strong> A cook cannot skip the inspection because they feel confident. The inspection runs. Period.</p>
|
||||
<p>The flow YAML is the recipe. It says: first the prep cook dices onions, then the saucier makes the base, then the grill cook sears the protein. After every station, the plate hits the pass for inspection. <strong>This order is not negotiable.</strong> A cook cannot skip the inspection because they feel confident. The inspection runs. Period.</p>
|
||||
</div>
|
||||
<div class="kitchen-col">
|
||||
<span class="kitchen-col__title kitchen-col__title--reality">The Technique (Autonomous)</span>
|
||||
@@ -739,7 +730,7 @@ Context: ${{input.context}}</code></pre>
|
||||
|
||||
<div class="callout callout--purple">
|
||||
<span class="callout__kicker">The Meta-Kitchen</span>
|
||||
<p>And when a recipe is wrong? Another flow improves it. A meta-flow reads performance data, spots bottlenecks — "the feel gate keeps failing because the cook doesn't know the jump velocity threshold" — edits the <code>.flow.mjs</code> to pass that threshold into the builder's inputs, and commits the change. <strong>Flows that edit flows.</strong> The kitchen that renovates itself between services.</p>
|
||||
<p>And when a recipe is wrong? Another flow improves it. A meta-flow reads performance data, spots bottlenecks — "the feel gate keeps failing because the cook doesn't know the jump velocity threshold" — edits the YAML to wire that threshold into the builder's inputs, and commits the change. <strong>Flows that edit flows.</strong> The kitchen that renovates itself between services.</p>
|
||||
</div>
|
||||
|
||||
<hr class="accent">
|
||||
@@ -749,25 +740,25 @@ Context: ${{input.context}}</code></pre>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;margin:18px 0;font-size:0.89rem;">
|
||||
<thead>
|
||||
<tr style="text-align:left;border-bottom:1px solid rgba(255,255,255,0.07);">
|
||||
<th style="padding:8px 12px;color:#B6FF3C;">Tier</th>
|
||||
<th style="padding:8px 12px;color:#B6FF3C;">The Knife</th>
|
||||
<th style="padding:8px 12px;color:#B6FF3C;">What It Cuts</th>
|
||||
<tr style="text-align:left;border-bottom:1px solid #2a3340;">
|
||||
<th style="padding:8px 12px;color:#c9935a;">Tier</th>
|
||||
<th style="padding:8px 12px;color:#c9935a;">The Knife</th>
|
||||
<th style="padding:8px 12px;color:#c9935a;">What It Cuts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>@coding</code></td><td style="padding:7px 12px;color:#B6FF3C;">DeepSeek V4</td><td style="padding:7px 12px;color:#8A95A3;"><strong>Chef's knife</strong> — your workhorse. Reads 800-line files, writes 200-line diffs. Game-builder, fixer, test-author. Free.</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>@planning</code></td><td style="padding:7px 12px;color:#B6FF3C;">DeepSeek V4</td><td style="padding:7px 12px;color:#8A95A3;"><strong>Boning knife</strong> — precision decomposition. Breaks tasks into steps, designs DAGs. Flow architect, feature planner.</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>@fast</code></td><td style="padding:7px 12px;color:#B6FF3C;">DeepSeek V4 Flash</td><td style="padding:7px 12px;color:#8A95A3;"><strong>Paring knife</strong> — quick, decisive cuts. Gate pass/fail, fork choices, loop exits. No overthinking.</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>@research</code></td><td style="padding:7px 12px;color:#B6FF3C;">DeepSeek V4</td><td style="padding:7px 12px;color:#8A95A3;"><strong>Fillet knife</strong> — flexible, follows contours. Reads codebase, traces patterns, finds what matters.</td></tr>
|
||||
<tr style="border-bottom:1px solid rgba(255,255,255,0.07);"><td style="padding:7px 12px;color:#ECEEF1;"><code>@vision</code></td><td style="padding:7px 12px;color:#7C5CFF;">Gemini 2.5 Flash</td><td style="padding:7px 12px;color:#8A95A3;"><strong>The inspector's eyes</strong> — the only knife that sees. Multimodal frame judging: T-poses, foot-slide, frozen anims.</td></tr>
|
||||
<tr><td style="padding:7px 12px;color:#ECEEF1;"><code>@compact</code></td><td style="padding:7px 12px;color:#B6FF3C;">DeepSeek V4 Flash</td><td style="padding:7px 12px;color:#8A95A3;"><strong>Kitchen shears</strong> — lightweight, versatile. Summaries, verdicts, post-processing. Fast and cheap.</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>@coding</code></td><td style="padding:7px 12px;color:#f59e0b;">DeepSeek V4</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>Chef's knife</strong> — your workhorse. Reads 800-line files, writes 200-line diffs. Game-builder, fixer, test-author. Free.</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>@planning</code></td><td style="padding:7px 12px;color:#f59e0b;">DeepSeek V4</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>Boning knife</strong> — precision decomposition. Breaks tasks into steps, designs DAGs. Flow architect, feature planner.</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>@fast</code></td><td style="padding:7px 12px;color:#38bdf8;">DeepSeek V4 Flash</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>Paring knife</strong> — quick, decisive cuts. Gate pass/fail, fork choices, loop exits. No overthinking.</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>@research</code></td><td style="padding:7px 12px;color:#f59e0b;">DeepSeek V4</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>Fillet knife</strong> — flexible, follows contours. Reads codebase, traces patterns, finds what matters.</td></tr>
|
||||
<tr style="border-bottom:1px solid #1c2230;"><td style="padding:7px 12px;color:#e6edf3;"><code>@vision</code></td><td style="padding:7px 12px;color:#a855f7;">MiniMax latest</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>The inspector's eyes</strong> — the only knife that sees. Multimodal frame judging: T-poses, foot-slide, frozen anims.</td></tr>
|
||||
<tr><td style="padding:7px 12px;color:#e6edf3;"><code>@compact</code></td><td style="padding:7px 12px;color:#38bdf8;">DeepSeek V4 Flash</td><td style="padding:7px 12px;color:#cdd7e2;"><strong>Kitchen shears</strong> — lightweight, versatile. Summaries, verdicts, post-processing. Fast and cheap.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout callout--amber">
|
||||
<span class="callout__kicker">Why DeepSeek?</span>
|
||||
<p>Two reasons. <strong>It's free</strong> — no usage limits, which matters when your game-builder reads 800-line files and writes 200-line diffs ten times a session. <strong>It's genuinely good at C# and Godot</strong> — I've had it write a full lighting module for our Godot fork by reading Unity API docs and adapting patterns. No agent had pulled that off before. DeepSeek can't do multimodal, so vision goes to Gemini — but for everything else, it's the chef's knife you reach for 90% of the time.</p>
|
||||
<p>Two reasons. <strong>It's free</strong> — no usage limits, which matters when your game-builder reads 800-line files and writes 200-line diffs ten times a session. <strong>It's genuinely good at C# and Godot</strong> — I've had it write a full lighting module for our Godot fork by reading Unity API docs and adapting patterns. No agent had pulled that off before. DeepSeek V4 now has multimodal, but for vision we use MiniMax latest — it's sharper at frame-by-frame animation judging and costs less per image. For everything else, DeepSeek is the chef's knife you reach for 90% of the time.</p>
|
||||
</div>
|
||||
|
||||
<p>The point of the knife rack: you configure this <strong>once</strong>. Every agent declares <code>model: @coding</code> and gets DeepSeek V4 automatically. Swap models globally without touching any flow or agent file. The right blade, every time, no thinking required.</p>
|
||||
|
||||
|
Before
After
|
@@ -24,7 +24,7 @@ Every agent harness, regardless of domain, needs five things:
|
||||
|
||||
**Tools.** What the agent can actually do beyond generating text. A CLI that takes screenshots, checks service health, and loads project context. API wrappers for git, CI, image generation. Without tools, the agent is a very articulate oracle that can't touch anything.
|
||||
|
||||
**Context.** Which project this is. Who's asking. What machine they're on. What services are reachable. A single CLI call — `tinqs identity` — returns all of this in 100ms. No re-reading the README. No "what repo are we in?"
|
||||
**Context.** Which project this is. Who's asking. What machine they're on. What services are reachable. A single CLI call — `tstudio identity` — returns all of this in 100ms. No re-reading the README. No "what repo are we in?"
|
||||
|
||||
**Guardrails.** What the agent must never do. No merging to main without review. No pushing to public repos without approval. No running destructive commands. The harness enforces these at the platform layer, not in the prompt. Prompts can be ignored. Platform gates cannot.
|
||||
|
||||
@@ -44,7 +44,7 @@ LangChain, CrewAI, and AutoGen are built for web apps. They assume text-in, text
|
||||
|
||||
Our harness runs on [Tinqs Studio](https://tinqs.com), built on a Gitea fork with game-specific features. The key pieces:
|
||||
|
||||
**The CLI** — a single Go binary. One command (`tinqs identity`) gives the agent full project context in 100ms. Screenshots, cloud vision, health checks — all subcommands of the same binary.
|
||||
**The CLI** — a single Go binary. One command (`tstudio identity`) gives the agent full project context in 100ms. Screenshots, cloud vision, health checks — all subcommands of the same binary.
|
||||
|
||||
**The soul file** — a markdown document in the repo root. The agent reads it on session start. It defines values, scope, and behavioural rules. The same soul file works in Cursor, Claude Code, or any tool that reads markdown.
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
title: "Flows Are Sessions, Not Pipelines: Why We Moved Our Agent Orchestrator from YAML to JavaScript"
|
||||
slug: flows-are-sessions
|
||||
date: "2026-06-11"
|
||||
description: "We killed the static YAML DAG and rewrote our agent orchestration in 200 lines of JavaScript. Now a flow IS a session — you chat it, steer it, and it pauses for you at a human gate."
|
||||
og_description: "YAML DAGs are dead. We rewrote our agent orchestration in JavaScript, made every flow a live session, and added a human-in-the-loop gate. The operator is the co-pilot, not the babysitter."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "We killed the static YAML DAG and rewrote our agent orchestration in 200 lines of JavaScript. Now a flow IS a session — you chat it, steer it, and it pauses for you at a human gate."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
|
||||
Our YAML flow engine had seven bespoke node types just to fake a `while` loop. We threw it out and rewrote everything in 200 lines of JavaScript. The flow engine is gone. The flow IS the session. Here's what we learned.
|
||||
|
||||
## The YAML Was a Compiler for a Language Nobody Wanted
|
||||
|
||||
The old system was a static DAG. You defined nodes and edges in YAML, the engine walked them top-to-bottom, and when it finished it was done. No mid-run interaction. No branching. No retry. If you wanted a loop, you didn't use a `while` statement — you used an `agent-loop-decision` node type. In YAML.
|
||||
|
||||
```yaml
|
||||
# The old way: a "loop" was a bespoke node type
|
||||
steps:
|
||||
- agent-task: "generate document"
|
||||
- agent-loop-decision:
|
||||
condition: "check if quality > 0.8"
|
||||
if-true: "continue"
|
||||
if-false: "repeat-step: agent-task"
|
||||
```
|
||||
|
||||
That's not configuration. That's a compiler for a language nobody wanted to write. Every orchestrator ends up here — GitHub Actions expressions, GitLab CI `rules`, Airflow's `BranchPythonOperator`, all of them start as "simple YAML config" and grow node types until they're Turing-complete nightmares held together by schema patches.
|
||||
|
||||
We had seven: `agent-task`, `agent-loop-decision`, `fork`, `conditional`, `agent-join`, `pipeline-stage`, `human-review`. Each one existed because YAML can't express control flow. You weren't writing a flow. You were filing paperwork to describe a flow.
|
||||
|
||||
The moment we knew it was wrong: someone asked "can I retry this step three times if it fails?" and the honest answer was "we'd need a new node type." When your config format needs an RFC to add a `for` loop, you've built a programming language by accident. Delete it.
|
||||
|
||||
## 200 Lines of JavaScript Replaced Seven Node Types
|
||||
|
||||
A flow is now an ES module. You export `default async (flow) => {}` and the runtime calls it. The API surface is five calls:
|
||||
|
||||
- `agent(prompt, options)` — run one agent with a task
|
||||
- `parallel(thunks)` — run many agents concurrently, await all
|
||||
- `pipeline(items, ...stages)` — push items through stages
|
||||
- `phase(name)` — label progress for the dashboard
|
||||
- `human(config)` — pause and wait for a person
|
||||
|
||||
Here's a real flow. It reviews a code route change with parallel researchers and a human gate. Ten lines.
|
||||
|
||||
```js
|
||||
// .pi/flows/flows/review-routes.flow.mjs
|
||||
export const meta = { name: "review-routes", description: "audit routes for missing auth" };
|
||||
export default async (flow) => {
|
||||
const { agent, parallel, phase, human, task } = flow;
|
||||
phase("find");
|
||||
const findings = await parallel(["auth", "input"].map((d) => () =>
|
||||
agent(`Review ${d}: ${task}`, { agent: "researcher", model: "@planning" })
|
||||
));
|
||||
phase("review");
|
||||
const gate = await human({ title: "Eyeball it", prompt: "anything to fix before I finish?" });
|
||||
return { findings, gate };
|
||||
};
|
||||
```
|
||||
|
||||
Why JavaScript wins is boring and fundamental: it has `while`, `if`, `try/catch`, and `parallel(thunks)` built in. The things our YAML needed custom schema types to fake are just language keywords. Bounded concurrency is a one-liner. Error recovery is a `try` block. A loop is `while (!approved)`. No plugin, no RFC, no new node type.
|
||||
|
||||
We migrated all flows YAML-to-JS on 2026-06-10. One-way conversion script, every flow reviewed and running in 24 hours. The YAML parser was deleted — not deprecated, not kept for backwards compatibility, deleted. There's no config path left to reach for.
|
||||
|
||||
The plan lives in code, not config. Config is for things that don't change. Agent orchestration changes every run.
|
||||
|
||||
## A Flow IS a Session
|
||||
|
||||
The old model had a phantom problem. A flow was a card. A session was a separate card. The operator watched the flow run from a different window. There was a "New Flow" button that created a flow card, and a "Continue" button that attached a session to it, and the disconnect between "the thing running your work" and "the place you talk to it" was baked into the UI.
|
||||
|
||||
We killed that architecture. Every spawn is a session.
|
||||
|
||||
When you call `POST /api/flows/spawn {cwd, task, flowName}`, the session runs the flow inside itself with the `flow_run` tool. Steps stream inline into the chat — `flow:steps` injects progress into the session's own message stream. The session turns purple in the dashboard. It becomes the flow. One card. One identity. Persistent after the run finishes.
|
||||
|
||||
No "New Flow" button. No "Continue" button. You spawn a session; with a `flowName` it runs that flow, without one it opens an interactive operator session that designs a flow with you first. The dashboard branding is tinqs Studio. The control surface is the host card, live agent cards, run history, and a chat to steer the run.
|
||||
|
||||
There's no phantom card. No disconnect between "the thing running your work" and "the place you talk to it." It's one session. You're in the room.
|
||||
|
||||
## The Human Gate: Pause, Take Over, Approve, Continue
|
||||
|
||||
Agents make mistakes. They guess when they shouldn't. They take irreversible actions because the prompt said "proceed." The model gets stuck and you sit there wondering — do I wait or abort?
|
||||
|
||||
`flow.human()` is our answer.
|
||||
|
||||
When a flow hits a human gate, it stops. The dashboard shows the gate prompt: what to review, what to decide. The host session switches to takeover mode — coding tools are unblocked (normally the host is hands-off) and the system prompt becomes "flow is paused, help the operator finish this." You open files. You edit. You verify. You run commands.
|
||||
|
||||
To release the gate, reply `approve` or `done` or `lgtm`. The flow resumes. Any other message is a work instruction — the takeover session executes it but does not release the gate. The flow loops on `notes` until the human says go.
|
||||
|
||||
```js
|
||||
let approved = false;
|
||||
while (!approved) {
|
||||
const { notes } = await human({
|
||||
title: "Review before push",
|
||||
prompt: "Check the diff. Approve or tell me what to fix.",
|
||||
});
|
||||
if (notes.match(/^(approve|done|lgtm|looks good)/i)) approved = true;
|
||||
else await agent(`Fix: ${notes}`, { agent: "implementer" });
|
||||
}
|
||||
```
|
||||
|
||||
Two patterns this enables. First: review-approve-before-push gates — nobody ships untested code because nobody set the `auto-approve` flag to true. Second: the "agent is stuck" hand-off — the flow pauses, you take over the exact same workspace, fix the problem, type `continue`, and the flow keeps going.
|
||||
|
||||
The flow waits instead of guessing. This isn't a feature. It's an admission that some decisions shouldn't be automated.
|
||||
|
||||
## The Operator Is Your Co-Pilot
|
||||
|
||||
The old way was a one-shot generator. Paste an objective, click Generate, get a YAML blob, pray it's right, run it, discover it's wrong 20 minutes in with no way to steer. We'd watch flows fail and think "I could have told it that before it started."
|
||||
|
||||
The new flow operator doesn't write the flow for you and walk away. It designs it with you.
|
||||
|
||||
Hit New Flow. A DeepSeek session opens — the same model that powers the dashboard, cheap at $0.28/MTok, steerable in natural language. It proposes a draft `.flow.mjs`, shows you the agents and phases and any human gate, and explains why. You tell it what to change. It does NOT launch until you say go. When you approve, it writes the flow file and spawns a separate host session, then attaches to monitor and report progress.
|
||||
|
||||
The operator is still in the chat when the human gate fires. It's still there when you want to change the plan mid-run. It doesn't go away after launch. Co-pilot, not autopilot.
|
||||
|
||||
There are three runner faces for the same engine: **pi/dashboard** (DeepSeek, cheap, steerable — the default), **Claude Code** (Workflow tool, one-shot fan-out for heavy research), and a **cloud agent** (remote deploy, clone, AWS). Pick by granularity and cost. The flow file is the same in all three modes.
|
||||
|
||||
## What We Learned
|
||||
|
||||
**Numbers first.** 43 out of 43 unit tests green. All flows migrated. The supervisor inbox — steering messages sent between steps — was silently dropping operator messages before 2026-06-10. You'd type "focus on the auth routes" and the flow never saw it. That's fixed now. Chat reaches the inbox, the inbox drains between `agent()` calls, and steering works.
|
||||
|
||||
**The inbox rule.** The supervisor inbox is applied between `agent()` calls, never mid-step. Steering mid-step is undefined behaviour. We learned this the hard way — early versions tried to inject mid-agent and got corrupted state, partial outputs, agents that forgot what they were doing. Between steps is the right boundary. Respect it.
|
||||
|
||||
**Economics matter.** DeepSeek V4 Pro at $0.28/MTok runs per-step. Per-step model override lets you swap in a premium reasoning model ($15/MTok) for the one critical call that needs it. $0.28 for routine, $15 for the hard parts. The three-tier strategy from our agent daemon applies at flow granularity too.
|
||||
|
||||
**What's next.** Richer on-card flow display — a pinned step strip so you can see progress without opening the session. Attachable asset and agent-structure viewers in the flow card. Run replay for finished sessions after a page reload (the session persists, but you can't rewatch the stream yet).
|
||||
|
||||
But the principle is settled. A flow isn't a pipeline. A pipeline runs blind and reports back later. A flow is a pair-programming session where one of the pair happens to be code.
|
||||
|
||||
---
|
||||
|
||||
*[Tinqs Studio](https://tinqs.com) is our agent-native development platform — git hosting, AI agents, and the flow engine described here. [Ariki](https://arikigame.com) is the survival colony sim we're building with it.*
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
title: "GPU-Skinned Herds: One Draw Call for 1,000 Animated Characters in Godot"
|
||||
slug: gpu-skinned-herds
|
||||
date: "2026-06-14"
|
||||
description: "Godot has no built-in way to render 1,000 skinned characters in one draw call. We built a GPU skinned-instance renderer into Tinqs Engine that does — 25 crocodiles verified, 1,000+ projected. Pre-built binaries for macOS and Windows."
|
||||
og_description: "One draw call, 1,000 animated characters. GPU-skinned herd renderer built into the Tinqs Engine fork of Godot."
|
||||
og_image: "https://www.tinqs.com/img/og-cover.jpg"
|
||||
excerpt: "Godot can't batch-render 1,000 animated characters. We built a GPU skinned-instance herd renderer into the engine itself — already driving crocodile herds in Ariki. Pre-built editor binaries for macOS and Windows."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
Godot gives you one `Skeleton3D` per character. Want 200 animals in a herd? That's 200 skeleton nodes, 200 draw calls, and 200 `AnimationPlayer` ticks every frame. Want 1,000? Now you're measuring in seconds per frame, not frames per second.
|
||||
|
||||
We built a GPU skinned-instance renderer into Tinqs Engine that packs every pose into a single texture, uploads once, and draws every instance in one call. 25 crocodiles confirmed first. Then we threw 1,000 animals — 12 types mixed, random-walking — at it and the GPU didn't flinch. Same bone count, same animation fidelity, a tiny fraction of the cost.
|
||||
|
||||
## Why the engine needs to change
|
||||
|
||||
The standard Godot approach — one `Skeleton3D` + one `MeshInstance3D` per character — works for a handful of animated entities. It breaks down hard at crowd scale:
|
||||
|
||||
- **CPU bone transforms.** Computing `global_pose` for 200 skeletons × 100 bones each = 20,000 matrix multiplies per frame, all on the main thread.
|
||||
- **Draw call explosion.** Each `MeshInstance3D` is its own draw call. Even with MultiMesh, there's no built-in path for skinned meshes — `MultiMeshInstance3D` only handles static geometry.
|
||||
- **AnimationPlayer sprawl.** Each skeleton needs its own `AnimationPlayer` and its own `process()` tick.
|
||||
|
||||
The alternative — baking animations to vertex textures — works for static crowds but locks you out of per-instance variation. No blending, no phase offsets, no reactive behaviour.
|
||||
|
||||
What we need is simpler: **share the skeleton, drive per-instance poses from a single animation, batch the draw call.** That's what `agent_skinned` does.
|
||||
|
||||
## How it works: two classes, one texture
|
||||
|
||||
The module lives in `modules/agent_skinned/` inside [Tinqs Engine](https://tinqs.com/tinqs/engine). Two classes, one job:
|
||||
|
||||
### `MultiSkinnedMeshInstance3D` — the data plane
|
||||
|
||||
Holds the CPU-side bone matrices. Allocates an `ImageTexture` of size `[4 × max_bones, max_instances]` in RGBA32F — each texel is one column of a 4×4 bone matrix. For a 130-bone crocodile with 256 instances:
|
||||
|
||||
```
|
||||
Texture: 520 × 256 RGBA32F ≈ 2 MB
|
||||
```
|
||||
|
||||
That's the entire pose state for 256 animated crocodiles in a single GPU texture. The API is simple:
|
||||
|
||||
```gdscript
|
||||
var data := MultiSkinnedMeshInstance3D.new()
|
||||
data.set_mesh(crocodile_mesh)
|
||||
data.set_skeleton(skeleton) # rest pose + bone hierarchy
|
||||
data.set_max_instances(256)
|
||||
data.set_max_bones(130)
|
||||
|
||||
# Each frame: push poses from the animated skeleton
|
||||
for instance in herd_positions:
|
||||
data.set_instance_pose_bones(instance.id, bone_transforms)
|
||||
data.update() # upload only dirty instances, not the whole texture
|
||||
```
|
||||
|
||||
### `MultiSkinnedInstance3D` — the renderer
|
||||
|
||||
A `MultiMeshInstance3D` subclass. Set its multimesh with the skinned mesh and instance transforms, point it at the data plane, call `refresh()` — it uploads the bone texture into the shader material's `bone_matrices_tex` uniform and the mesh is drawn in one call.
|
||||
|
||||
The shader does 4-bone linear-blend skinning on the GPU:
|
||||
|
||||
```glsl
|
||||
mat4 get_bone(int b) {
|
||||
return mat4(
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 0, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 1, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 2, INSTANCE_ID), 0),
|
||||
texelFetch(bone_matrices_tex, ivec2(b * 4 + 3, INSTANCE_ID), 0)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`INSTANCE_ID` is a Godot built-in — the GPU already knows which instance it's rendering. We just use it to index into the bone texture. No uniform arrays, no SSBOs, no compute shaders. Just a 2D texture and a custom vertex shader.
|
||||
|
||||
## Two bugs we shipped and fixed
|
||||
|
||||
The module had data-plane doctests from day one — round-trip pose get/set, dirty tracking, size clamping, AABB. All green. Then we put it on screen for the first time and the crocodiles looked... wrong.
|
||||
|
||||
**Bug 1: Shader compile failure.** The default skinning shader compared `TANGENT` as `vec4`. Godot 4 exposes it as `vec3`. Fixed in one line, added `albedo_tex` uniform so herds texture out of the box.
|
||||
|
||||
**Bug 2: Bone matrices stored transposed.** The data plane wrote basis rows (standard Godot `Transform3D.basis` is row-major), but the shader unpacked as columns. Every bone matrix was transposed — the mesh crumpled. Not a scale bug, not an orientation bug — a layout mismatch. Fixed by storing column-major, with a doctest to prevent regression.
|
||||
|
||||
The lesson: doctests catch logic. Rendering catches truth. You need both.
|
||||
|
||||
## What's driving it
|
||||
|
||||
In [Ariki](https://www.arikigame.com), the sim tracks animal migration across a 12km archipelago. `AnimalHerdRenderer.cs` groups sim `ViewerState.animals` by type, feeds positions to `skinned_herd.gd` (a reusable per-type herd backend), which drives the renderer. One `AnimationPlayer` animates a single driver skeleton; poses propagate to every instance.
|
||||
|
||||
The crocodile herd scene was 25 instances, one draw call. The perf test scene does 1,000 animals across 12 types — Boar, Cow, Crab, Crocodile, Deer, Fish, Goat, Hen, Pig, Rabbit, Sheep, Tiger — each type its own GPU herd, all mixed, all random-walking, FPS holding steady.
|
||||
|
||||
## What's deliberately not here
|
||||
|
||||
- **No C# wrapper.** Instantiate from GDScript via `ClassDB.instantiate()` — the binding surface is small and stable.
|
||||
- **No automatic `AnimationPlayer` integration.** You drive poses. We give you the texture. Freedom to animate however you want.
|
||||
- **No GPU occlusion or LOD.** That's the game's job. The engine provides the tool; the game decides what to draw.
|
||||
|
||||
## Get the build
|
||||
|
||||
Pre-built editor binaries with `agent_skinned` baked in — no engine compile required. The game's `animal_perf_test.tscn` lets you toggle 10 / 100 / 1000 animals and read live FPS:
|
||||
|
||||
| Platform | Binary | Engine commit |
|
||||
|----------|--------|---------------|
|
||||
| **macOS ARM64** | [`tinqs.macos.editor.arm64.mono`](https://tinqs.com/tinqs/builds/media/branch/main/engine/macos-arm64/tinqs.macos.editor.arm64.mono) | `4fe1323` (4.6.4, Xcode 26.3) |
|
||||
| **Windows x64** | [`tinqs.windows.editor.x86_64.mono.exe`](https://tinqs.com/tinqs/builds/media/branch/main/engine/windows-x64/tinqs.windows.editor.x86_64.mono.exe) | `64fb5cc` (4.6.4, MSVC 2022) |
|
||||
|
||||
All builds live in the public [`tinqs/builds`](https://tinqs.com/tinqs/builds) repo — engine source is private, but the binaries are yours. See [`manifest.json`](https://tinqs.com/tinqs/builds/src/branch/main/manifest.json) for checksums and build details.
|
||||
|
||||
The engine source lives in [`tinqs/engine`](https://tinqs.com/tinqs/engine) (private). Module docs: `modules/agent_skinned/README.md` and `.agents/wiki/agent-skinned-gpu-herd.md`.
|
||||
|
||||
---
|
||||
|
||||
**Related:** [Fork, Don't Build](fork-dont-build) — why we modify existing platforms instead of building new ones. [Streaming a 12km Archipelago in Godot 4](godot-optimisation) — the terrain and vegetation streaming layers that work alongside this.
|
||||
@@ -21,11 +21,11 @@ We built a pre-commit hook with two layers: a regex blocklist that's instant and
|
||||
A text file of patterns, each tagged with scope and message:
|
||||
|
||||
```
|
||||
public|\b<internal-codename>\b|Classified codename — use the public-facing alias
|
||||
public|\bCosmos\b|Classified codename — use "advanced colonist AI"
|
||||
all|github\.com/(tinqs-ltd|tinqs)/|GitHub repos deleted — use tinqs.com
|
||||
all|sk-[a-zA-Z0-9]{20,}|Possible API key leaked
|
||||
all|AKIA[A-Z0-9]{16}|AWS access key leaked
|
||||
public|admin\.<internal-domain>|Internal admin URL in public content
|
||||
public|admin\.arikigame\.com|Internal admin URL in public content
|
||||
```
|
||||
|
||||
The scope field controls where patterns apply. `all` means every file. `public` means only public-facing content — blog posts, website, marketing pages. We *want* classified codenames in internal architecture docs. We just don't want them in blog posts.
|
||||
|
||||
+5
-5
@@ -12,11 +12,11 @@ author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
Every AI agent session starts the same way: cold. The agent doesn't know what project this is, who's asking, what tools are available, or what happened yesterday. You spend the first five minutes re-explaining context.
|
||||
|
||||
Our CLI solves this in 100ms. One command — `tinqs identity` — and the agent knows everything. The binary is 15MB, has zero runtime dependencies, and runs on every machine in the studio.
|
||||
Our CLI solves this in 100ms. One command — `tstudio identity` — and the agent knows everything. The binary is 15MB, has zero runtime dependencies, and runs on every machine in the studio.
|
||||
|
||||
## The identity command (100ms)
|
||||
|
||||
When an agent starts, the first thing it calls is `tinqs identity`. The output:
|
||||
When an agent starts, the first thing it calls is `tstudio identity`. The output:
|
||||
|
||||
- **Soul file** — the agent's persistent identity, values, operating principles
|
||||
- **Company context** — team members, roles, what the company does
|
||||
@@ -26,7 +26,7 @@ When an agent starts, the first thing it calls is `tinqs identity`. The output:
|
||||
|
||||
This data lives in markdown files in the docs repo. Any machine on the network can read it. The agent goes from blank to fully contextual in under a second.
|
||||
|
||||
This started as a convenience tool for humans. It became the single most important function in our stack. Every agent session — Cursor, Claude Code, Pi — starts with `tinqs identity`. Without it, every conversation begins with "let me explain the project." With it, the agent already knows.
|
||||
This started as a convenience tool for humans. It became the single most important function in our stack. Every agent session — Cursor, Claude Code, Pi — starts with `tstudio identity`. Without it, every conversation begins with "let me explain the project." With it, the agent already knows.
|
||||
|
||||
## Screenshots and cloud vision
|
||||
|
||||
@@ -38,7 +38,7 @@ This is how you file bugs without typing. Look at the game, tell the agent what'
|
||||
|
||||
## Health checks
|
||||
|
||||
`tinqs doctor` runs a comprehensive check:
|
||||
`tstudio doctor` runs a comprehensive check:
|
||||
|
||||
- Is the git platform reachable and authenticated?
|
||||
- Is the game server running?
|
||||
@@ -55,7 +55,7 @@ Cross-compilation is trivial. We build Windows, Mac (arm64 + amd64), and Linux b
|
||||
|
||||
## What we learned
|
||||
|
||||
**The CLI is the API for AI agents.** What started as a human convenience tool became the primary interface for agents. Every session starts with `tinqs identity`. The agent's "hands and eyes" — screenshots, vision, health checks — are subcommands of the same binary.
|
||||
**The CLI is the API for AI agents.** What started as a human convenience tool became the primary interface for agents. Every session starts with `tstudio identity`. The agent's "hands and eyes" — screenshots, vision, health checks — are subcommands of the same binary.
|
||||
|
||||
**One binary beats ten scripts.** Scripts rot. They have different shells, different PATH assumptions, different error handling. A compiled binary either works or it doesn't. It ships with dependencies baked in. It doesn't care if your Python is 3.9 or 3.12.
|
||||
|
||||
|
||||
+102
-148
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -277,45 +262,33 @@
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">25 May 2026</span>
|
||||
<h1 class="post__title">A Pre-Commit Agent That Guards Your Secrets for $0.001</h1>
|
||||
<p class="post__lead">Every small team has the same problem: too many things to remember before <code>git commit</code>. Don't leak API keys. Don't reference the classified AI codename in public posts. Don't link to GitHub repos we deleted six months ago. Don't push a blog post with a 90-character title.
|
||||
<p class="post__lead">Every small team has the same problem: too many things to remember before <code>git commit</code>. Don't leak API keys. Don't reference the classified AI codename in public posts. Don't link to GitHub repos we deleted six months ago. Don't push a blog post with a 90-character title.</p>
|
||||
|
||||
A checklist in the README doesn't work. Humans skip checklists. Code review catches some issues but not all — reviewers focus on logic, not whether a URL points to a deleted org.
|
||||
|
||||
We built a pre-commit hook with two layers: a regex blocklist that's instant and free, and an LLM review that costs $0.001. Together they catch everything.
|
||||
|
||||
## Layer 1: Regex blocklist (0ms, $0.00)
|
||||
|
||||
A text file of patterns, each tagged with scope and message:
|
||||
|
||||
``<code>
|
||||
public|\b<internal-codename>\b|Classified codename — use the public-facing alias
|
||||
<div class="post__body">
|
||||
<p>A checklist in the README doesn't work. Humans skip checklists. Code review catches some issues but not all — reviewers focus on logic, not whether a URL points to a deleted org.</p>
|
||||
<p>We built a pre-commit hook with two layers: a regex blocklist that's instant and free, and an LLM review that costs $0.001. Together they catch everything.</p>
|
||||
<h2>Layer 1: Regex blocklist (0ms, $0.00)</h2>
|
||||
<p>A text file of patterns, each tagged with scope and message:</p>
|
||||
<pre><code>public|\bCosmos\b|Classified codename — use "advanced colonist AI"
|
||||
all|github\.com/(tinqs-ltd|tinqs)/|GitHub repos deleted — use tinqs.com
|
||||
all|sk-[a-zA-Z0-9]{20,}|Possible API key leaked
|
||||
all|AKIA[A-Z0-9]{16}|AWS access key leaked
|
||||
public|admin\.<internal-domain>|Internal admin URL in public content
|
||||
</code>`<code>
|
||||
|
||||
The scope field controls where patterns apply. </code>all<code> means every file. </code>public<code> means only public-facing content — blog posts, website, marketing pages. We <em>want</em> classified codenames in internal architecture docs. We just don't want them in blog posts.
|
||||
|
||||
The blocklist runs grep against the staged diff. No network call, no API, no latency. Match found → commit blocked immediately with file path and explanation. This catches 80% of issues before the LLM wakes up.
|
||||
|
||||
## Layer 2: DeepSeek V4 Flash review (~4s, $0.001)
|
||||
|
||||
If the commit touches public-facing files, the hook sends the staged diff to DeepSeek V4 Flash. The system prompt tells it exactly what to check:
|
||||
|
||||
- <strong>Leaked secrets</strong> — API keys, tokens, credentials the regex might have missed
|
||||
- <strong>Classified terms</strong> — codenames not yet in the blocklist
|
||||
- <strong>Internal URLs</strong> — references to services that shouldn't be public
|
||||
- <strong>Blog quality</strong> — title length, meta description, slug consistency
|
||||
- <strong>Broken links</strong> — malformed URLs, obvious typos
|
||||
- <strong>Announcements</strong> — if it's a new blog post, draft a one-line summary
|
||||
|
||||
The model responds with structured JSON: </code>errors<code> (block) or </code>warnings<code> (inform but allow). If the API is unreachable or times out, the commit proceeds — the hook never blocks work for infrastructure reasons.
|
||||
|
||||
## The architecture
|
||||
|
||||
</code>`<code>
|
||||
git commit
|
||||
public|admin\.arikigame\.com|Internal admin URL in public content</code></pre>
|
||||
<p>The scope field controls where patterns apply. <code>all</code> means every file. <code>public</code> means only public-facing content — blog posts, website, marketing pages. We <em>want</em> classified codenames in internal architecture docs. We just don't want them in blog posts.</p>
|
||||
<p>The blocklist runs grep against the staged diff. No network call, no API, no latency. Match found → commit blocked immediately with file path and explanation. This catches 80% of issues before the LLM wakes up.</p>
|
||||
<h2>Layer 2: DeepSeek V4 Flash review (~4s, $0.001)</h2>
|
||||
<p>If the commit touches public-facing files, the hook sends the staged diff to DeepSeek V4 Flash. The system prompt tells it exactly what to check:</p>
|
||||
<ul>
|
||||
<li><strong>Leaked secrets</strong> — API keys, tokens, credentials the regex might have missed</li>
|
||||
<li><strong>Classified terms</strong> — codenames not yet in the blocklist</li>
|
||||
<li><strong>Internal URLs</strong> — references to services that shouldn't be public</li>
|
||||
<li><strong>Blog quality</strong> — title length, meta description, slug consistency</li>
|
||||
<li><strong>Broken links</strong> — malformed URLs, obvious typos</li>
|
||||
<li><strong>Announcements</strong> — if it's a new blog post, draft a one-line summary</li>
|
||||
</ul>
|
||||
<p>The model responds with structured JSON: <code>errors</code> (block) or <code>warnings</code> (inform but allow). If the API is unreachable or times out, the commit proceeds — the hook never blocks work for infrastructure reasons.</p>
|
||||
<h2>The architecture</h2>
|
||||
<pre><code>git commit
|
||||
↓
|
||||
Phase 0: Collect staged diff + classify files (public vs internal)
|
||||
↓
|
||||
@@ -331,52 +304,33 @@ Phase 3: Parse JSON response
|
||||
→ Errors → BLOCK
|
||||
→ Warnings → print, exit 0
|
||||
→ Announcement → print draft
|
||||
→ API failure → warn, exit 0 (never block on infra)
|
||||
</code>`<code>
|
||||
|
||||
The hook lives in </code>.githooks/<code> — committed, version-controlled, shared by the team. A setup script points </code>git config core.hooksPath<code> there.
|
||||
|
||||
## What it costs
|
||||
|
||||
| | Tokens | Cost |
|
||||
|–|——–|——|
|
||||
| Input (prompt + diff) | ~4,000 | $0.00056 |
|
||||
| Output (JSON response) | ~200 | $0.00006 |
|
||||
| <strong>Per commit</strong> | | <strong>$0.00062</strong> |
|
||||
|
||||
A tenth of a cent. Twenty commits a day: $0.012/day. About <strong>$0.40/month</strong>. Commits that only touch internal files skip the AI review entirely — zero cost.
|
||||
|
||||
## What it caught (first week)
|
||||
|
||||
- <strong>2 classified codename leaks</strong> in draft blog posts — caught by blocklist
|
||||
- <strong>1 GitHub URL</strong> from an old copy-paste — caught by blocklist
|
||||
- <strong>3 blog SEO warnings</strong> — titles over 60 chars, missing og_description — caught by AI
|
||||
- <strong>1 announcement draft</strong> auto-generated when a new post was committed
|
||||
|
||||
Zero false positives on the blocklist. Two false positives from the AI — flagged an internal URL in a code example that was clearly illustrative. We added a note to the prompt: ignore URLs inside fenced code blocks.
|
||||
|
||||
## Setup
|
||||
|
||||
</code>`<code>bash
|
||||
bash scripts/setup-hooks.sh # or .\scripts\setup-hooks.ps1 on Windows
|
||||
export TINQS_HOOK_TOKEN=<your-token> # same PAT used for git push
|
||||
</code>`<code>
|
||||
|
||||
That's it. Every </code>git commit<code> runs the two-layer review. Bypass with </code>git commit –no-verify` for emergencies.
|
||||
|
||||
## The pattern: guard rails at the edge
|
||||
|
||||
This is the same principle we apply everywhere: put the guard rail where the action happens. Don't rely on a human checklist. Don't wait for code review. Don't hope someone remembers.
|
||||
|
||||
The pre-commit hook is $0.001 of prevention. A leaked API key in a public post is hours of rotation, revocation, and audit. A classified codename in a blog post is a confidentiality breach. A dead link is a broken experience nobody notices for weeks.
|
||||
|
||||
The tools exist. DeepSeek V4 Flash is cheap enough to call on every commit. The hook is 150 lines of bash. The blocklist is a text file. Total infrastructure cost: zero — it runs on the developer's machine, calls an API we already pay for, adds 4 seconds to the commit flow.
|
||||
|
||||
—
|
||||
|
||||
<em>The pre-commit hook is part of <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>. The inference proxy, blocklist patterns, and review prompt are open and reusable. Every commit in <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> runs through the same guard.</em></p>
|
||||
|
||||
<div class="post__body">
|
||||
→ API failure → warn, exit 0 (never block on infra)</code></pre>
|
||||
<p>The hook lives in <code>.githooks/</code> — committed, version-controlled, shared by the team. A setup script points <code>git config core.hooksPath</code> there.</p>
|
||||
<h2>What it costs</h2>
|
||||
<p>| | Tokens | Cost |</p>
|
||||
<p>|–|——–|——|</p>
|
||||
<p>| Input (prompt + diff) | ~4,000 | $0.00056 |</p>
|
||||
<p>| Output (JSON response) | ~200 | $0.00006 |</p>
|
||||
<p>| <strong>Per commit</strong> | | <strong>$0.00062</strong> |</p>
|
||||
<p>A tenth of a cent. Twenty commits a day: $0.012/day. About <strong>$0.40/month</strong>. Commits that only touch internal files skip the AI review entirely — zero cost.</p>
|
||||
<h2>What it caught (first week)</h2>
|
||||
<ul>
|
||||
<li><strong>2 classified codename leaks</strong> in draft blog posts — caught by blocklist</li>
|
||||
<li><strong>1 GitHub URL</strong> from an old copy-paste — caught by blocklist</li>
|
||||
<li><strong>3 blog SEO warnings</strong> — titles over 60 chars, missing og_description — caught by AI</li>
|
||||
<li><strong>1 announcement draft</strong> auto-generated when a new post was committed</li>
|
||||
</ul>
|
||||
<p>Zero false positives on the blocklist. Two false positives from the AI — flagged an internal URL in a code example that was clearly illustrative. We added a note to the prompt: ignore URLs inside fenced code blocks.</p>
|
||||
<h2>Setup</h2>
|
||||
<pre><code class="language-bash">bash scripts/setup-hooks.sh # or .\scripts\setup-hooks.ps1 on Windows
|
||||
export TINQS_HOOK_TOKEN=<your-token> # same PAT used for git push</code></pre>
|
||||
<p>That's it. Every <code>git commit</code> runs the two-layer review. Bypass with <code>git commit –no-verify</code> for emergencies.</p>
|
||||
<h2>The pattern: guard rails at the edge</h2>
|
||||
<p>This is the same principle we apply everywhere: put the guard rail where the action happens. Don't rely on a human checklist. Don't wait for code review. Don't hope someone remembers.</p>
|
||||
<p>The pre-commit hook is $0.001 of prevention. A leaked API key in a public post is hours of rotation, revocation, and audit. A classified codename in a blog post is a confidentiality breach. A dead link is a broken experience nobody notices for weeks.</p>
|
||||
<p>The tools exist. DeepSeek V4 Flash is cheap enough to call on every commit. The hook is 150 lines of bash. The blocklist is a text file. Total infrastructure cost: zero — it runs on the developer's machine, calls an API we already pay for, adds 4 seconds to the commit flow.</p>
|
||||
<hr>
|
||||
<p><em>The pre-commit hook is part of <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>. The inference proxy, blocklist patterns, and review prompt are open and reusable. Every commit in <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a> runs through the same guard.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
Binary file not shown.
|
Before Width: | Height: | Size: 518 KiB |
+88
-123
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -277,64 +262,44 @@
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">18 May 2026</span>
|
||||
<h1 class="post__title">One Binary to Rule Them All: Our Studio CLI</h1>
|
||||
<p class="post__lead">Every AI agent session starts the same way: cold. The agent doesn't know what project this is, who's asking, what tools are available, or what happened yesterday. You spend the first five minutes re-explaining context.
|
||||
|
||||
Our CLI solves this in 100ms. One command — <code>tinqs identity</code> — and the agent knows everything. The binary is 15MB, has zero runtime dependencies, and runs on every machine in the studio.
|
||||
|
||||
## The identity command (100ms)
|
||||
|
||||
When an agent starts, the first thing it calls is <code>tinqs identity</code>. The output:
|
||||
|
||||
- <strong>Soul file</strong> — the agent's persistent identity, values, operating principles
|
||||
- <strong>Company context</strong> — team members, roles, what the company does
|
||||
- <strong>Machine context</strong> — hostname, OS, which repos are cloned, what services are running
|
||||
- <strong>Ecosystem</strong> — other repos and their purpose
|
||||
- <strong>Service status</strong> — which URLs are live and reachable
|
||||
|
||||
This data lives in markdown files in the docs repo. Any machine on the network can read it. The agent goes from blank to fully contextual in under a second.
|
||||
|
||||
This started as a convenience tool for humans. It became the single most important function in our stack. Every agent session — Cursor, Claude Code, Pi — starts with <code>tinqs identity</code>. Without it, every conversation begins with "let me explain the project." With it, the agent already knows.
|
||||
|
||||
## Screenshots and cloud vision
|
||||
|
||||
The CLI can capture any window from outside the process. No in-game overlay, no rendering pipeline integration. OS-level capture — GDI+ on Windows, screencapture on Mac.
|
||||
|
||||
A <code>photo</code> command sends the screenshot to a cloud vision model. The agent says "take a photo of the game" and gets back: "The player character is standing near a half-built hut. Three palm trees to the left. The terrain has a visible seam between two biomes."
|
||||
|
||||
This is how you file bugs without typing. Look at the game, tell the agent what's wrong. It takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached. Keyboard-free bug reporting.
|
||||
|
||||
## Health checks
|
||||
|
||||
<code>tinqs doctor</code> runs a comprehensive check:
|
||||
|
||||
- Is the git platform reachable and authenticated?
|
||||
- Is the game server running?
|
||||
- Are all expected repos cloned and on the right branch?
|
||||
- Are required tools installed at the right version?
|
||||
|
||||
Output is a green/yellow/red table. Essential for unattended agent sessions — the agent verifies its environment before starting work. No "the build failed because port 3000 was already taken" at 3am.
|
||||
|
||||
## Why Go
|
||||
|
||||
Go compiles to a single static binary. No Python virtualenvs, no Node.js version managers, no DLL hell on Windows. The same binary runs on a gaming PC, a designer's MacBook, and a CI runner in AWS.
|
||||
|
||||
Cross-compilation is trivial. We build Windows, Mac (arm64 + amd64), and Linux binaries from a single CI workflow. Push a tag, CI builds all three, uploads to S3. The binary is 15MB, starts in under 100ms, has zero runtime dependencies.
|
||||
|
||||
## What we learned
|
||||
|
||||
<strong>The CLI is the API for AI agents.</strong> What started as a human convenience tool became the primary interface for agents. Every session starts with <code>tinqs identity</code>. The agent's "hands and eyes" — screenshots, vision, health checks — are subcommands of the same binary.
|
||||
|
||||
<strong>One binary beats ten scripts.</strong> Scripts rot. They have different shells, different PATH assumptions, different error handling. A compiled binary either works or it doesn't. It ships with dependencies baked in. It doesn't care if your Python is 3.9 or 3.12.
|
||||
|
||||
<strong>Cloud vision is underrated for game dev.</strong> Sending a screenshot to a vision model sounds gimmicky. In practice, it's the fastest way to document visual bugs. "The tree is floating 2m above the terrain" is much faster to communicate when the AI is looking at the same screen.
|
||||
|
||||
<strong>Agent cold starts are the real problem.</strong> Without the identity system, every session starts with the agent asking "what project is this?" With it, the agent knows everything in 100ms. That's the difference between an AI assistant and an AI team member.
|
||||
|
||||
—
|
||||
|
||||
<em>The CLI is part of <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a>. Every time we find ourselves about to write a script that needs to work on multiple machines, we add a subcommand instead. One binary that makes the studio work — whether the operator is human or AI.</em></p>
|
||||
<p class="post__lead">Every AI agent session starts the same way: cold. The agent doesn't know what project this is, who's asking, what tools are available, or what happened yesterday. You spend the first five minutes re-explaining context.</p>
|
||||
|
||||
<div class="post__body">
|
||||
<p>Our CLI solves this in 100ms. One command — <code>tstudio identity</code> — and the agent knows everything. The binary is 15MB, has zero runtime dependencies, and runs on every machine in the studio.</p>
|
||||
<h2>The identity command (100ms)</h2>
|
||||
<p>When an agent starts, the first thing it calls is <code>tstudio identity</code>. The output:</p>
|
||||
<ul>
|
||||
<li><strong>Soul file</strong> — the agent's persistent identity, values, operating principles</li>
|
||||
<li><strong>Company context</strong> — team members, roles, what the company does</li>
|
||||
<li><strong>Machine context</strong> — hostname, OS, which repos are cloned, what services are running</li>
|
||||
<li><strong>Ecosystem</strong> — other repos and their purpose</li>
|
||||
<li><strong>Service status</strong> — which URLs are live and reachable</li>
|
||||
</ul>
|
||||
<p>This data lives in markdown files in the docs repo. Any machine on the network can read it. The agent goes from blank to fully contextual in under a second.</p>
|
||||
<p>This started as a convenience tool for humans. It became the single most important function in our stack. Every agent session — Cursor, Claude Code, Pi — starts with <code>tstudio identity</code>. Without it, every conversation begins with "let me explain the project." With it, the agent already knows.</p>
|
||||
<h2>Screenshots and cloud vision</h2>
|
||||
<p>The CLI can capture any window from outside the process. No in-game overlay, no rendering pipeline integration. OS-level capture — GDI+ on Windows, screencapture on Mac.</p>
|
||||
<p>A <code>photo</code> command sends the screenshot to a cloud vision model. The agent says "take a photo of the game" and gets back: "The player character is standing near a half-built hut. Three palm trees to the left. The terrain has a visible seam between two biomes."</p>
|
||||
<p>This is how you file bugs without typing. Look at the game, tell the agent what's wrong. It takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached. Keyboard-free bug reporting.</p>
|
||||
<h2>Health checks</h2>
|
||||
<p><code>tstudio doctor</code> runs a comprehensive check:</p>
|
||||
<ul>
|
||||
<li>Is the git platform reachable and authenticated?</li>
|
||||
<li>Is the game server running?</li>
|
||||
<li>Are all expected repos cloned and on the right branch?</li>
|
||||
<li>Are required tools installed at the right version?</li>
|
||||
</ul>
|
||||
<p>Output is a green/yellow/red table. Essential for unattended agent sessions — the agent verifies its environment before starting work. No "the build failed because port 3000 was already taken" at 3am.</p>
|
||||
<h2>Why Go</h2>
|
||||
<p>Go compiles to a single static binary. No Python virtualenvs, no Node.js version managers, no DLL hell on Windows. The same binary runs on a gaming PC, a designer's MacBook, and a CI runner in AWS.</p>
|
||||
<p>Cross-compilation is trivial. We build Windows, Mac (arm64 + amd64), and Linux binaries from a single CI workflow. Push a tag, CI builds all three, uploads to S3. The binary is 15MB, starts in under 100ms, has zero runtime dependencies.</p>
|
||||
<h2>What we learned</h2>
|
||||
<p><strong>The CLI is the API for AI agents.</strong> What started as a human convenience tool became the primary interface for agents. Every session starts with <code>tstudio identity</code>. The agent's "hands and eyes" — screenshots, vision, health checks — are subcommands of the same binary.</p>
|
||||
<p><strong>One binary beats ten scripts.</strong> Scripts rot. They have different shells, different PATH assumptions, different error handling. A compiled binary either works or it doesn't. It ships with dependencies baked in. It doesn't care if your Python is 3.9 or 3.12.</p>
|
||||
<p><strong>Cloud vision is underrated for game dev.</strong> Sending a screenshot to a vision model sounds gimmicky. In practice, it's the fastest way to document visual bugs. "The tree is floating 2m above the terrain" is much faster to communicate when the AI is looking at the same screen.</p>
|
||||
<p><strong>Agent cold starts are the real problem.</strong> Without the identity system, every session starts with the agent asking "what project is this?" With it, the agent knows everything in 100ms. That's the difference between an AI assistant and an AI team member.</p>
|
||||
<hr>
|
||||
<p><em>The CLI is part of <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a>. Every time we find ourselves about to write a script that needs to work on multiple machines, we add a subcommand instead. One binary that makes the studio work — whether the operator is human or AI.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
@@ -39,84 +39,73 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Tinqs Studio brand — post styles ── */
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
/* Studio near-black base */
|
||||
--c-bg: #0B0C0E;
|
||||
--c-bg-raised: #15171A;
|
||||
/* Foreground */
|
||||
--c-fg: #ECEEF1;
|
||||
--c-muted: #8A95A3;
|
||||
/* Family accents */
|
||||
--c-lime: #B6FF3C;
|
||||
--c-violet: #7C5CFF;
|
||||
/* Borders */
|
||||
--c-border: rgba(255,255,255,.07);
|
||||
--c-border-strong: rgba(255,255,255,.12);
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--c-bg); }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-fg);
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
background: var(--c-bg);
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 60px;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-muted);
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.post__back:hover { color: var(--c-lime); }
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title — lime → violet ── */
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(90deg, var(--c-lime), var(--c-violet));
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.2;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.18em;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
@@ -135,20 +124,16 @@
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-lime);
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--c-violet);
|
||||
font-size: 1.15rem;
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
@@ -158,27 +143,27 @@
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.84em;
|
||||
background: var(--c-bg-raised);
|
||||
color: var(--c-lime);
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-bg);
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.83rem;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
@@ -192,22 +177,22 @@
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(124, 92, 255, 0.06);
|
||||
border: 1px solid rgba(124, 92, 255, 0.15);
|
||||
border-left: 4px solid var(--c-violet);
|
||||
border-radius: 0 8px 8px 0;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: var(--c-fg);
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-lime); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.post__body a:hover { color: var(--c-violet); }
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-lime); font-weight: 600; }
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
@@ -248,8 +233,8 @@
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-violet);
|
||||
color: #fff;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -265,7 +250,7 @@
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-fg);
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -277,28 +262,21 @@
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">10 June 2026</span>
|
||||
<h1 class="post__title">Why Voice Is the Missing Input for Game Development</h1>
|
||||
<p class="post__lead">Every game developer knows this moment. You're playtesting, running through the world, and you see something wrong — a tree floating two meters above the terrain, a UI element clipping, an animation that stutters on frame 14. You make a mental note. Ten minutes later, back at the editor, you try to file it. The coordinates are fuzzy. The exact reproduction steps are gone. You type something vague like "tree floating on west beach maybe" and hope you remember more tomorrow.
|
||||
<p class="post__lead">Every game developer knows this moment. You're playtesting, running through the world, and you see something wrong — a tree floating two meters above the terrain, a UI element clipping, an animation that stutters on frame 14. You make a mental note. Ten minutes later, back at the editor, you try to file it. The coordinates are fuzzy. The exact reproduction steps are gone. You type something vague like "tree floating on west beach maybe" and hope you remember more tomorrow.</p>
|
||||
|
||||
Voice changes this entirely. Speak the bug while you're looking at it, and an agent turns your words into a structured issue — with a screenshot, a vision-model description, coordinates, and a severity estimate. No keyboard. No context switch. No memory loss.
|
||||
|
||||
## The latency that kills bug reports
|
||||
|
||||
The distance between seeing a bug and filing it is a memory decay curve. Every second that passes, your recollection loses precision:
|
||||
|
||||
| Elapsed time | What you remember |
|
||||
|—|—|
|
||||
| 0 seconds | Exact position, camera angle, what you were doing, what's on screen |
|
||||
| 30 seconds | "There was a tree... somewhere west... maybe floating?" |
|
||||
| 5 minutes | "I think there was a rendering issue? Or was it yesterday?" |
|
||||
|
||||
Typed bug reports are reconstructions from decaying memory. Voice bug reports are real-time captures. The difference in quality isn't marginal — it's the difference between a fix you can act on immediately and a ticket that sits in the backlog for three months while someone tries to reproduce it.
|
||||
|
||||
## The pipeline: voice → text → structured issue
|
||||
|
||||
Here's what actually happens when you speak a bug during playtesting:
|
||||
|
||||
``<code>
|
||||
1. You speak: "There's a tree floating two meters above the terrain
|
||||
<div class="post__body">
|
||||
<p>Voice changes this entirely. Speak the bug while you're looking at it, and an agent turns your words into a structured issue — with a screenshot, a vision-model description, coordinates, and a severity estimate. No keyboard. No context switch. No memory loss.</p>
|
||||
<h2>The latency that kills bug reports</h2>
|
||||
<p>The distance between seeing a bug and filing it is a memory decay curve. Every second that passes, your recollection loses precision:</p>
|
||||
<p>| Elapsed time | What you remember |</p>
|
||||
<p>|—|—|</p>
|
||||
<p>| 0 seconds | Exact position, camera angle, what you were doing, what's on screen |</p>
|
||||
<p>| 30 seconds | "There was a tree... somewhere west... maybe floating?" |</p>
|
||||
<p>| 5 minutes | "I think there was a rendering issue? Or was it yesterday?" |</p>
|
||||
<p>Typed bug reports are reconstructions from decaying memory. Voice bug reports are real-time captures. The difference in quality isn't marginal — it's the difference between a fix you can act on immediately and a ticket that sits in the backlog for three months while someone tries to reproduce it.</p>
|
||||
<h2>The pipeline: voice → text → structured issue</h2>
|
||||
<p>Here's what actually happens when you speak a bug during playtesting:</p>
|
||||
<pre><code>1. You speak: "There's a tree floating two meters above the terrain
|
||||
on the west beach, near the big rock formation. Happens after
|
||||
the vegetation culling pass kicks in around sunset."
|
||||
|
||||
@@ -316,68 +294,39 @@ Here's what actually happens when you speak a bug during playtesting:
|
||||
5. Agent files a structured issue with all of the above,
|
||||
tags the rendering engineer, and posts the digest to team chat.
|
||||
|
||||
Total latency: under 2 seconds. You keep playing.
|
||||
</code>``
|
||||
|
||||
This isn't theoretical. The pipeline runs on our own game project, and it's caught bugs that would have slipped through playtesting entirely — the ones you see, make a mental note about, and forget by the time you alt-tab.
|
||||
|
||||
## Why game dev is the perfect voice use case
|
||||
|
||||
<strong>You're already looking at the screen.</strong> Voice input doesn't require switching windows or breaking flow. You're playtesting — your hands are on the controller or WASD, your eyes are on the game. Speaking is the only input channel that doesn't interrupt the thing you're actually doing.
|
||||
|
||||
<strong>Game bugs are spatial and visual.</strong> "The crafting UI text overflows on items with names longer than 20 characters" is something you see, not something you calculate. Describing it verbally while looking at it produces a far richer bug report than typing from memory.
|
||||
|
||||
<strong>Reproduction is half the battle.</strong> When you speak the bug at the moment of occurrence, you naturally include the context: what you were doing, what just happened, what the game state was. You don't have to reconstruct it later.
|
||||
|
||||
<strong>Voice scales to the whole team.</strong> Artists see visual bugs. Designers see balance issues. Producers see UX friction. Not everyone on a game team is a fast typist or comfortable with issue trackers. Everyone can speak.
|
||||
|
||||
## What the agent adds beyond transcription
|
||||
|
||||
Raw transcription is useful — it's a notepad you don't have to type. But the agent layer is what makes voice input a pipeline rather than a dictation tool:
|
||||
|
||||
<strong>Screenshot coordination.</strong> The agent calls the game engine's HTTP API, captures the current frame, and attaches it to the issue. You don't take screenshots. The agent does.
|
||||
|
||||
<strong>Vision model description.</strong> The screenshot goes through a vision model that writes a text description of what's on screen. Future-you searching the issue tracker for "floating tree" finds it even if the transcription was garbled.
|
||||
|
||||
<strong>Coordinates and context.</strong> The game engine provides the player's world position, camera angle, and current game state. The agent bakes these into the issue. A developer can teleport directly to the bug location.
|
||||
|
||||
<strong>Severity and routing.</strong> The agent estimates severity from context ("floating" is visual, "crash" is critical) and tags the right team member. An artist doesn't get pinged for a shader bug. A rendering engineer doesn't get pinged for a UI text overflow.
|
||||
|
||||
## The numbers
|
||||
|
||||
| Method | Time from observation to filed issue | Information loss |
|
||||
|—|—|—|
|
||||
| Mental note → type later | 5-30 minutes | High (positions, steps, context) |
|
||||
| Alt-tab → type immediately | 30-60 seconds | Medium (screenshots missed, flow broken) |
|
||||
| Voice → agent pipeline | 2 seconds | Low (screenshot + position captured automatically) |
|
||||
|
||||
The throughput difference compounds. A 30-minute playtest session with keyboard-only bug filing might yield 3-4 issues, half of them vague. The same session with voice-to-agent produces 10-15 issues, all with screenshots, positions, and reproduction context.
|
||||
|
||||
## Setup is simpler than you think
|
||||
|
||||
You need three things, all of which you probably already have:
|
||||
|
||||
1. <strong>A microphone.</strong> The one in your headset is fine. Transcription models handle suboptimal audio surprisingly well.
|
||||
2. <strong>Transcription.</strong> Whisper runs locally and is free. Cloud APIs are sub-cent per minute. Both work.
|
||||
3. <strong>An agent that speaks your game engine's API.</strong> If your engine has an HTTP interface for screenshots and game state, the agent can wire the rest together. If it doesn't — add one. It's a weekend project.
|
||||
|
||||
The agent itself doesn't need to be custom-built. Any coding agent with tool access can be told "watch the game, transcribe voice input, file issues in the tracker." It's a skill file, not a product.
|
||||
|
||||
## What changes when you stop typing bugs
|
||||
|
||||
The most surprising effect isn't the speed. It's the coverage. When filing a bug costs two seconds of speaking, you file bugs you would have previously ignored. The minor visual glitch. The slight animation hitch. The UI element that's two pixels misaligned.
|
||||
|
||||
Individually these are low-priority. Collectively they're the difference between a game that feels polished and one that feels rough. And they only get caught when the cost of reporting approaches zero.
|
||||
|
||||
The second effect is that playtesting becomes a primary input channel. Instead of structured QA sessions with checklists and forms, you just play the game. The agent captures everything. When you're done, you have a list of filed issues with screenshots and context — generated from your spoken observations in real time.
|
||||
|
||||
Voice isn't a gimmick for game development. It's the input channel that matches the way we actually work — looking at the screen, noticing things, and talking about them. The tools exist. The latency is sub-second. The cost is negligible. The only thing missing is the habit.
|
||||
|
||||
—
|
||||
|
||||
<em>We build <a href="https://tinqs.com" style="color: var(--c-lime);">Tinqs Studio</a> — a game dev platform with built-in AI agents, git hosting, and creative pipelines. <a href="https://arikigame.com" style="color: var(--c-lime);">Ariki</a> is the survival colony sim we're building with every tool described here.</em></p>
|
||||
|
||||
<div class="post__body">
|
||||
Total latency: under 2 seconds. You keep playing.</code></pre>
|
||||
<p>This isn't theoretical. The pipeline runs on our own game project, and it's caught bugs that would have slipped through playtesting entirely — the ones you see, make a mental note about, and forget by the time you alt-tab.</p>
|
||||
<h2>Why game dev is the perfect voice use case</h2>
|
||||
<p><strong>You're already looking at the screen.</strong> Voice input doesn't require switching windows or breaking flow. You're playtesting — your hands are on the controller or WASD, your eyes are on the game. Speaking is the only input channel that doesn't interrupt the thing you're actually doing.</p>
|
||||
<p><strong>Game bugs are spatial and visual.</strong> "The crafting UI text overflows on items with names longer than 20 characters" is something you see, not something you calculate. Describing it verbally while looking at it produces a far richer bug report than typing from memory.</p>
|
||||
<p><strong>Reproduction is half the battle.</strong> When you speak the bug at the moment of occurrence, you naturally include the context: what you were doing, what just happened, what the game state was. You don't have to reconstruct it later.</p>
|
||||
<p><strong>Voice scales to the whole team.</strong> Artists see visual bugs. Designers see balance issues. Producers see UX friction. Not everyone on a game team is a fast typist or comfortable with issue trackers. Everyone can speak.</p>
|
||||
<h2>What the agent adds beyond transcription</h2>
|
||||
<p>Raw transcription is useful — it's a notepad you don't have to type. But the agent layer is what makes voice input a pipeline rather than a dictation tool:</p>
|
||||
<p><strong>Screenshot coordination.</strong> The agent calls the game engine's HTTP API, captures the current frame, and attaches it to the issue. You don't take screenshots. The agent does.</p>
|
||||
<p><strong>Vision model description.</strong> The screenshot goes through a vision model that writes a text description of what's on screen. Future-you searching the issue tracker for "floating tree" finds it even if the transcription was garbled.</p>
|
||||
<p><strong>Coordinates and context.</strong> The game engine provides the player's world position, camera angle, and current game state. The agent bakes these into the issue. A developer can teleport directly to the bug location.</p>
|
||||
<p><strong>Severity and routing.</strong> The agent estimates severity from context ("floating" is visual, "crash" is critical) and tags the right team member. An artist doesn't get pinged for a shader bug. A rendering engineer doesn't get pinged for a UI text overflow.</p>
|
||||
<h2>The numbers</h2>
|
||||
<p>| Method | Time from observation to filed issue | Information loss |</p>
|
||||
<p>|—|—|—|</p>
|
||||
<p>| Mental note → type later | 5-30 minutes | High (positions, steps, context) |</p>
|
||||
<p>| Alt-tab → type immediately | 30-60 seconds | Medium (screenshots missed, flow broken) |</p>
|
||||
<p>| Voice → agent pipeline | 2 seconds | Low (screenshot + position captured automatically) |</p>
|
||||
<p>The throughput difference compounds. A 30-minute playtest session with keyboard-only bug filing might yield 3-4 issues, half of them vague. The same session with voice-to-agent produces 10-15 issues, all with screenshots, positions, and reproduction context.</p>
|
||||
<h2>Setup is simpler than you think</h2>
|
||||
<p>You need three things, all of which you probably already have:</p>
|
||||
<p>1. <strong>A microphone.</strong> The one in your headset is fine. Transcription models handle suboptimal audio surprisingly well.</p>
|
||||
<p>2. <strong>Transcription.</strong> Whisper runs locally and is free. Cloud APIs are sub-cent per minute. Both work.</p>
|
||||
<p>3. <strong>An agent that speaks your game engine's API.</strong> If your engine has an HTTP interface for screenshots and game state, the agent can wire the rest together. If it doesn't — add one. It's a weekend project.</p>
|
||||
<p>The agent itself doesn't need to be custom-built. Any coding agent with tool access can be told "watch the game, transcribe voice input, file issues in the tracker." It's a skill file, not a product.</p>
|
||||
<h2>What changes when you stop typing bugs</h2>
|
||||
<p>The most surprising effect isn't the speed. It's the coverage. When filing a bug costs two seconds of speaking, you file bugs you would have previously ignored. The minor visual glitch. The slight animation hitch. The UI element that's two pixels misaligned.</p>
|
||||
<p>Individually these are low-priority. Collectively they're the difference between a game that feels polished and one that feels rough. And they only get caught when the cost of reporting approaches zero.</p>
|
||||
<p>The second effect is that playtesting becomes a primary input channel. Instead of structured QA sessions with checklists and forms, you just play the game. The agent captures everything. When you're done, you have a list of filed issues with screenshots and context — generated from your spoken observations in real time.</p>
|
||||
<p>Voice isn't a gimmick for game development. It's the input channel that matches the way we actually work — looking at the screen, noticing things, and talking about them. The tools exist. The latency is sub-second. The cost is negligible. The only thing missing is the habit.</p>
|
||||
<hr>
|
||||
<p><em>We build <a href="https://tinqs.com" style="color: var(–c-accent-l);">Tinqs Studio</a> — a game dev platform with built-in AI agents, git hosting, and creative pipelines. <a href="https://arikigame.com" style="color: var(–c-accent-l);">Ariki</a> is the survival colony sim we're building with every tool described here.</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
Before
After
|
Reference in New Issue
Block a user