14 Commits

Author SHA1 Message Date
ozan ed1f725423 post: remove cloud-harness 2026-06-06 03:27:05 +01:00
ozan 7bde9c3159 Standardize: reference docs/.agents/, remove duplicate skills/ 2026-06-06 01:07:28 +01:00
ozan 8d9a14dd19 strip site chrome from blog templates — Studio provides nav/footer 2026-06-05 23:34:53 +01:00
ozan 48af4c57f5 post: complete kitchen brigade analogy + real flow verdicts + knife-rack model strategy 2026-06-04 21:39:14 +01:00
ozan a82b1ffc72 post: rewrite with kitchen brigade analogy + richer styling (callout boxes, gate badges, 2-col comparison grids) 2026-06-04 21:05:43 +01:00
ozan f01036c646 rewrite: refresh all blog posts for public audience
Merged overlapping posts:
- forking-gitea + fork-dont-build → one post about the fork philosophy
- fal-image-generation + image-generation-fal → one post about AI art pipeline

Rewrote all posts with external/public voice:
- Stronger hooks, concrete examples, punchier language
- agentic-workflow: restructured around soul files + skills + numbers
- agent-harness: clearer framing of 'what an agent harness is'
- cloud-harness: tighter narrative about overnight agents
- godot-optimisation: same depth, sharper opening
- pre-commit-agent: clearer architecture, cost breakdown
- studio-cli: reframed around identity/cold-start problem
- blog-visual-upgrade: tightened the restyle story

10 posts total (9 markdown + 1 hand-authored HTML)
2026-06-03 03:06:41 +01:00
ozan bf42a76cf9 refactor: hand-authored pi-flow post + consolidated agents.md
- pi-flow-native-brain is now hand-authored HTML (direct SVGs + styled
  tables; no build.js passthrough needed)
- Card hardcoded in _index_template.html above {{CARDS}}
- Removed posts/pi-flow-native-brain.md (build.js no longer touches it)
- New agents.md: consolidated agent guide for the blog repo
  (blog architecture, build pipeline, adding posts, styling rules,
  writing guide, deploy instructions, skills reference)
- Removed old skills/blog.md (content migrated to agents.md)
2026-06-03 02:47:06 +01:00
ozan d39e9b9534 merge: combine pi-flow-native-brain + pi-ci-integrator into one post
Single public-facing post that tells the complete Pi autonomy story:
- Part 1: Retiring the hardcoded supervisor (1,050 lines deleted)
  replaced by composable oracle-backed pi-flows with 5 gates
  (build, test, behaviour, feel, visual)
- Part 2: The CI integrator — agents that watch CI, read failure
  logs, and fix their own broken builds (tinqs-ci extension)
- Combined stack: flow engine → gates → sub-agents → CI loop
- SVG gate pipeline + deletion bar diagrams preserved via
  build.js <!--raw--> passthrough
- Removed old separate pi-ci-integrator post; pi-flow-native-brain
  is now a proper .md source (replaces hand-authored HTML)
2026-06-03 02:37:00 +01:00
ozan d223708a1d post: blog visual upgrade — restyle story + toolkit guide
New public post documenting the blog's visual refresh:
- Design inspiration from internal team guide
- The three-layer styling architecture (site CSS → inline overrides)
- Full post template changes (gradient titles, code panels, pills, h2 bars)
- Index template changes (kicker, card hover, date pills)
- The build system (posts/*.md + templates → *.html via build.js)
- Comprehensive toolkit guide: adding posts, understanding templates,
  styling workflow, markdown dialect, extending build.js, cheatsheet
2026-06-03 02:28:44 +01:00
ozan 3570ca6943 blog: maintain pi-flow-native-brain as hand-authored HTML (drop md source)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 02:11:32 +01:00
ozan a43dbf71a5 blog: add SVG diagrams to pi-flow-native-brain (gate pipeline + deletion bar), real table; build.js raw-HTML passthrough
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 02:06:21 +01:00
ozan 3868be2f3a blog: pi flow-native brain — retire supervisor, oracle-backed gates 2026-06-03 00:55:18 +01:00
ozan 4a9675d242 style: apply team-guide aesthetic to blog
- Inline self-contained <style> blocks in both templates (after ../style.css link, before </head>)
- Gradient post titles (amber #c9935a -> warm gold #f59e0b -> blue #38bdf8) via background-clip: text
- Date pills: monospace, blue text, rounded pill border
- Code blocks: dark panel (#0a0e14), 1px border (#2a3340), rounded, monospace, overflow-x
- Inline code: distinct background (#1c2230), green tint (#9fe6c0)
- H2 left accent bar (#c9935a), H3 purple accent (#a855f7)
- Blockquote callout rules (future-proofing; build.js doesn't emit <blockquote> yet)
- Links: blue (#38bdf8), hover purple (#a855f7)
- Index template: gradient title, section-label kicker pill, date pill, card hover accent
- Nav/footer/site chrome untouched
- 11 posts + index regenerated via node build.js
2026-06-02 23:00:39 +01:00
ozan 80b4b941a7 merge: PostHog (EU) analytics on blog 2026-06-02 22:56:38 +01:00
36 changed files with 4113 additions and 3875 deletions
+2
View File
@@ -12,6 +12,8 @@ We're building Tinqs Studio while using it to make our own game --- a survival c
- [Streaming a 12km Archipelago in Godot 4](posts/godot-optimisation.md) (2026-05-22)
- [AI Art at Scale: Using fal.ai Flux for Game Asset Generation](posts/fal-image-generation.md) (2026-05-25)
- [Tinqs Studio Is an Agent Harness for Game Dev](posts/agent-harness.md) (2026-05-25)
- [Pi's Flow-Native Brain: Retiring the Supervisor, Teaching Agents to Fix Their Own Builds](posts/pi-flow-native-brain.md) (2026-06-03)
- [Our Blog Just Got a Visual Upgrade — Here's How We Did It](posts/blog-visual-upgrade.md) (2026-06-03)
## Skills
+138 -71
View File
@@ -15,48 +15,141 @@
<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">
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained index styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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;
}
/* ── Section label kicker ── */
.section-label {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
border-radius: 999px;
padding: 4px 14px;
margin-bottom: 16px;
}
/* ── Blog header ── */
.blog-header {
max-width: 720px;
margin: 0 auto;
padding: 60px 24px 32px;
}
/* ── Gradient index title ── */
.blog-header__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.4rem;
line-height: 1.25;
margin: 0 0 12px;
}
.blog-header__subtitle {
color: var(--c-muted);
font-size: 1.05rem;
line-height: 1.6;
margin: 0;
}
/* ── Blog list ── */
.blog-list {
max-width: 720px;
margin: 0 auto;
padding: 0 24px 60px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Blog card ── */
.blog-card {
display: block;
text-decoration: none;
background: #0c1119;
border: 1px solid var(--c-border);
border-radius: 12px;
padding: 20px 24px;
transition: border-color 0.2s;
}
.blog-card:hover {
border-color: var(--c-accent);
}
/* ── Date pill ── */
.blog-card__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
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 {
color: var(--c-text);
font-size: 1.25rem;
font-weight: 700;
line-height: 1.35;
margin: 0 0 8px;
}
.blog-card__excerpt {
color: var(--c-muted);
font-size: 0.92rem;
line-height: 1.55;
margin: 0 0 12px;
}
/* ── Read link accent ── */
.blog-card__read {
color: var(--c-blue);
font-size: 0.9rem;
font-weight: 500;
}
.blog-card:hover .blog-card__read {
color: var(--c-purple);
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- BLOG HEADER -->
<div class="blog-header">
<span class="section-label">Dev Log</span>
@@ -66,41 +159,15 @@
<!-- BLOG LIST -->
<div class="blog-list">
<!-- hand-authored HTML posts (not from build.js) -->
<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">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 &rarr;</span>
</a>
{{CARDS}}
</div>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
+214 -71
View File
@@ -39,48 +39,224 @@
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
@@ -101,38 +277,5 @@
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
+258 -219
View File
@@ -4,27 +4,27 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tinqs Studio Is an Agent Harness for Game Dev — Tinqs Blog</title>
<meta name="description" content="An agent harness gives AI agents identity, memory, tools, and guardrails. Tinqs Studio is one built specifically for game development --- git, 3D preview, image generation, and a CLI that makes every session a warm start.">
<title>What an Agent Harness Is and Why Game Dev Needs One — Tinqs Blog</title>
<meta name="description" content="A raw AI model is a brain without hands, eyes, or memory. An agent harness gives it identity, tools, context, and guardrails — and game development needs one built for binary assets, visual pipelines, and spatial systems.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/agent-harness">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/agent-harness">
<meta property="og:title" content="Tinqs Studio Is an Agent Harness for Game Dev">
<meta property="og:description" content="Tinqs Studio is an agent harness for game dev --- identity, skills, vision, git, and creative tools in one platform.">
<meta property="og:title" content="What an Agent Harness Is and Why Game Dev Needs One">
<meta property="og:description" content="Agent harnesses give AI identity, memory, and tools. Game dev needs one that understands 3D assets.">
<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="Tinqs Studio Is an Agent Harness for Game Dev">
<meta name="twitter:description" content="Tinqs Studio is an agent harness for game dev --- identity, skills, vision, git, and creative tools in one platform.">
<meta name="twitter:title" content="What an Agent Harness Is and Why Game Dev Needs One">
<meta name="twitter:description" content="Agent harnesses give AI identity, memory, and tools. Game dev needs one that understands 3D assets.">
<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": "Tinqs Studio Is an Agent Harness for Game Dev",
"headline": "What an Agent Harness Is and Why Game Dev Needs One",
"datePublished": "2026-05-25",
"author": {
"@type": "Person",
@@ -35,198 +35,270 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "An agent harness gives AI agents identity, memory, tools, and guardrails. Tinqs Studio is one built specifically for game development --- git, 3D preview, image generation, and a CLI that makes every session a warm start."
"description": "A raw AI model is a brain without hands, eyes, or memory. An agent harness gives it identity, tools, context, and guardrails — and game development needs one built for binary assets, visual pipelines, and spatial systems."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">25 May 2026</span>
<h1 class="post__title">Tinqs Studio Is an Agent Harness for Game Dev</h1>
<p class="post__lead">An agent harness is the layer between a raw AI model and a useful team member. It gives the agent identity, memory, tools, and guardrails. Tinqs Studio is an agent harness built specifically for game development.
## What Is an Agent Harness?
A raw AI model &mdash; Claude, GPT, Gemini &mdash; is powerful but stateless. It doesn't know who you are, what project you're working on, what tools are available, or what happened yesterday. Every session is a cold start. Every conversation begins with "let me explain the project..."
An agent harness solves this. It wraps around the model and provides:
- <strong>Identity</strong> &mdash; who the agent is, what it values, how it should behave
- <strong>Memory</strong> &mdash; what happened in previous sessions, what was decided, what failed
- <strong>Tools</strong> &mdash; what the agent can actually do beyond generating text
- <strong>Context</strong> &mdash; what project this is, who's asking, what infrastructure exists
- <strong>Guardrails</strong> &mdash; what the agent must never do, what requires human approval
Without a harness, you have a chatbot. With one, you have a team member.
## Why Game Dev Needs Its Own Harness
Generic agent harnesses exist &mdash; LangChain, CrewAI, AutoGen. They're built for web apps, data pipelines, and customer support. Game development has different problems:
<strong>Assets are binary.</strong> A web developer's PR is a text diff. A game developer's PR is a 150MB GLB file. Generic harnesses don't know how to preview 3D models, manage LFS bandwidth, or review binary assets.
<strong>The pipeline is visual.</strong> Game dev goes from concept art to 3D model to rigged character to in-engine asset. Each step uses different tools &mdash; image generators, 3D modellers, auto-riggers, game engines. An agent harness for game dev needs to orchestrate this entire chain.
<strong>Scale is physical.</strong> A web app's complexity is in business logic. A game's complexity is in geometry &mdash; 12km worlds, 155 vegetation prototypes, 576 terrain regions, 2000 crowd instances. The agent needs to understand spatial systems, GPU memory, and frame budgets.
<strong>The team is small and cross-functional.</strong> A 4-person game studio has no dedicated DevOps, no dedicated artist, no dedicated PM. The harness needs to fill all those gaps, not just one.
## How Tinqs Studio Works as a Harness
Tinqs Studio is a platform built on a <a href="forking-gitea" style="color: var(&ndash;c-accent-l);">Gitea fork</a> with game-specific features layered on top. But the git platform is just the foundation. The harness is everything around it.
### Identity: Soul Files
Every agent session starts by loading a soul file &mdash; a markdown document that defines the agent's persistent identity. Not just "you are a helpful assistant" but specific values, knowledge scope, and behavioural rules.
The soul file means the agent behaves consistently whether it's triaging bugs at 9am or generating concept art at midnight. It knows what repos exist, who the team members are, what the game is about, and what decisions have been made. Identity isn't cosmetic &mdash; it's the difference between an agent that asks "what project is this?" and one that says "I see the vegetation grid was updated yesterday, want me to check the cache eviction?"
### Memory: Markdown Files in Git
Agent memory is plain markdown files in a git repository. No vector databases, no proprietary stores. The agent reads its memory on session start, updates it during work, and commits changes back.
This is deliberately low-tech. Markdown in git gives you version history, branching, merge conflict resolution, and human readability for free. When memory goes wrong &mdash; and it will &mdash; you can <code>git log</code> to see what changed and <code>git revert</code> to fix it.
### Tools: The CLI
A <a href="studio-cli" style="color: var(&ndash;c-accent-l);">single Go binary</a> gives every agent access to:
- <strong>Identity loading</strong> &mdash; full project context in 100ms
- <strong>Screenshots</strong> &mdash; capture any window from outside the process
- <strong>Cloud vision</strong> &mdash; send screenshots to a vision model, get structured descriptions
- <strong>Health checks</strong> &mdash; verify services, repos, and tools are working
- <strong>Service status</strong> &mdash; which URLs are live, what's reachable
The CLI is the agent's hands and eyes. Without it, the agent can only read and write text. With it, the agent can see the game running, photograph bugs, and verify infrastructure.
### Skills: Teachable Workflows
Skills are markdown playbooks that teach agents specific procedures. Instead of hoping the model figures out how to generate concept art or create a 3D model, you write the steps once:
- <a href="../skills/image-generation.md" style="color: var(&ndash;c-accent-l);">Image Generation</a> &mdash; generate game art with fal.ai Flux using a <a href="fal-image-generation" style="color: var(&ndash;c-accent-l);">4-layer prompt pattern</a>
- <a href="../skills/concept-art-pipeline.md" style="color: var(&ndash;c-accent-l);">Concept Art Pipeline</a> &mdash; from design brief through 2D art to 3D model export
- <a href="../skills/tripo-browser-workflow.md" style="color: var(&ndash;c-accent-l);">3D Model Generation</a> &mdash; Tripo Studio text-to-3D and image-to-3D
- <a href="../skills/sora2-video.md" style="color: var(&ndash;c-accent-l);">Video Generation</a> &mdash; trailer clips with OpenAI Sora 2
Skills compound. Every playbook you write makes the agent more capable. After six months, our agents handle art generation, competitive research, video production, project management, and code review &mdash; all from markdown files.
### Git Platform: 3D Preview and LFS
The <a href="forking-gitea" style="color: var(&ndash;c-accent-l);">Gitea fork</a> underneath handles the game-specific git problems:
- <strong>3D asset preview</strong> &mdash; rotate GLB/FBX/STL files in the browser during code review
- <strong>LFS-first workflows</strong> &mdash; auto-tracking for game file extensions, storage dashboards
- <strong>OAuth2 SSO</strong> &mdash; one login for git, tools, and the game
- <strong>22 format support</strong> &mdash; GLB, FBX, OBJ, STL, 3DS, PLY, and more via O3DV
### Guardrails: Human-in-the-Loop
The harness defines what agents can and cannot do:
- Agents can file issues, draft announcements, generate assets, write code
- Agents <strong>cannot</strong> merge code, deploy builds, push to public repos, or post to external channels without human approval
- The <a href="https://tinqs.com/tinqs/blog" style="color: var(&ndash;c-accent-l);">public blog repo</a> requires human-approved merge requests &mdash; agents can propose changes but a person must review
This isn't a limitation &mdash; it's a feature. The agent handles volume; the human handles judgement.
## The Cold Start Problem
The biggest problem with AI agents in production isn't capability &mdash; it's context. Every new session is blank. The agent doesn't know what happened yesterday, what's in progress, or what tools are available.
Most teams solve this with long system prompts. That works until your context is 200 markdown files, 15 skills, and 3 years of project history. You can't paste all of that into a system prompt.
The harness solves this with <strong>staged loading</strong>:
1. <strong>CLI identity call</strong> (100ms) &mdash; loads soul file, company context, machine info, service status
2. <strong>Memory file</strong> (instant) &mdash; loads cross-session context
3. <strong>Skills</strong> (on demand) &mdash; loaded only when the task matches a skill name
4. <strong>Repo context</strong> (on demand) &mdash; read files as needed, not all upfront
The agent goes from cold to fully contextual in under a second. No "let me explain..." No re-reading the same onboarding doc. Just start working.
## What Makes This Different from LangChain
LangChain, CrewAI, and similar frameworks are <strong>code-first</strong>. You define agents in Python, chain them with function calls, and deploy them as services. They're powerful for building AI products.
Tinqs Studio is <strong>file-first</strong>. Agents are defined in markdown. Skills are markdown. Memory is markdown. Identity is markdown. Everything is in git, readable by humans, editable without code changes, and version-controlled.
This matters for game teams because:
- <strong>Non-engineers can contribute.</strong> The designer writes a skill for concept art. The PM writes a skill for sprint planning. No Python required.
- <strong>Everything is auditable.</strong> <code>git log</code> shows who changed what, when, and why. Memory changes are commits. Skill updates are diffs.
- <strong>It works with any AI tool.</strong> The same soul files and skills work in Cursor, Claude Code, or any tool that reads markdown. You're not locked into one framework.
## The Stack
| Layer | What | How |
|&mdash;&mdash;-|&mdash;&mdash;|&mdash;&ndash;|
| <strong>Identity</strong> | Soul files, company context | Markdown in git, loaded via CLI |
| <strong>Memory</strong> | Cross-session context | Markdown in git, updated by agents |
| <strong>Skills</strong> | Teachable workflows | Markdown playbooks, loaded on demand |
| <strong>Tools</strong> | CLI, screenshots, vision | Go binary, one install per machine |
| <strong>Git</strong> | 3D preview, LFS, SSO | Gitea fork with game-specific features |
| <strong>Creative</strong> | Image gen, 3D models, video | fal.ai, Tripo, Sora 2 via skills |
| <strong>Guardrails</strong> | Human approval gates | Branch protection, MR requirements |
## Getting Started
If you want to build your own agent harness for game dev:
1. <strong>Start with a soul file.</strong> Write 50 words about your project's identity, values, and scope. Put it in your repo root as <code>SOUL.md</code>.
2. <strong>Write one skill.</strong> Pick the workflow you repeat most &mdash; concept art generation, bug triage, build verification &mdash; and write the steps as markdown.
3. <strong>Build a CLI identity command.</strong> Even a shell script that prints "project name, repos, services" gives your agent a warm start.
4. <strong>Put everything in git.</strong> Not a database, not a SaaS tool. Git. You already have it.
The rest &mdash; 3D preview, LFS management, OAuth SSO, creative pipelines &mdash; you can add as you need it. Or use <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a>, where we've already built it.
&mdash;
An agent harness isn't a product category yet. But it should be. The gap between "I have an AI model" and "I have an AI team member" is infrastructure &mdash; identity, memory, tools, context, guardrails. For game development, that infrastructure needs to understand binary assets, visual pipelines, and spatial systems. That's what we're building.</p>
<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."</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(&ndash;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(&ndash;c-accent-l);">Ariki</a> with the same tools.</em></p>
</div>
@@ -239,38 +311,5 @@ An agent harness isn't a product category yet. But it should be. The gap between
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
+269 -137
View File
@@ -4,19 +4,19 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How a Small Game Studio Runs on AI Agents — Tinqs Blog</title>
<meta name="description" content="Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale.">
<title>How a 4-Person Studio Runs on AI Agents — Tinqs Blog</title>
<meta name="description" content="We gave AI agents persistent identities, skill playbooks, and access to our entire knowledge base. Here's how four people ship like forty.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/agentic-workflow">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/agentic-workflow">
<meta property="og:title" content="How a Small Game Studio Runs on AI Agents">
<meta property="og:title" content="How a 4-Person Studio Runs on AI Agents">
<meta property="og:description" content="Soul files, skill playbooks, and markdown as the universal API for AI agents in game dev.">
<meta property="og:image" content="https://www.tinqs.com/blog/img/agentic-workflow-architecture.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How a Small Game Studio Runs on AI Agents">
<meta name="twitter:title" content="How a 4-Person Studio Runs on AI Agents">
<meta name="twitter:description" content="Soul files, skill playbooks, and markdown as the universal API for AI agents in game dev.">
<meta name="twitter:image" content="https://www.tinqs.com/blog/img/agentic-workflow-architecture.png">
@@ -24,7 +24,7 @@
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How a Small Game Studio Runs on AI Agents",
"headline": "How a 4-Person Studio Runs on AI Agents",
"datePublished": "2026-03-06",
"author": {
"@type": "Person",
@@ -35,124 +35,289 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale."
"description": "We gave AI agents persistent identities, skill playbooks, and access to our entire knowledge base. Here's how four people ship like forty."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">6 March 2026</span>
<h1 class="post__title">How a Small Game Studio Runs on AI Agents</h1>
<p class="post__lead">We gave our AI agents persistent identities, skill playbooks, and access to our entire knowledge base. This is how a 4-person game studio built an agentic workflow that punches above its weight.</p>
<h1 class="post__title">How a 4-Person Studio Runs on AI Agents</h1>
<p class="post__lead">Last week one of our agents caught a stale bug at 3am — a vegetation culling issue that had been open for six days. It nudged the team chat, drafted a fix summary, and by morning the issue was resolved. Nobody lost sleep. Nobody context-switched. The agent just handled it.</p>
<div class="post__body">
<h2>The Problem Every Small Studio Knows</h2>
<p>When you're four people building a game, there's no room for a dedicated DevOps person, a full-time PM tool chain, or someone whose job it is to "keep things organised." Everyone wears five hats. Documentation drifts. Issues pile up. The left hand doesn't know what the right hand shipped.</p>
<p>We tried the usual tools &mdash; Notion, Trello, shared Google Docs. They all had the same problem: they're passive. They sit there and wait for a human to update them. In a team of four where the lead developer is also the CTO, that human never has time.</p>
<p>So we built something different. We gave AI agents persistent identities, connected them to our entire knowledge base, and let them become working members of the team.</p>
<h2>The Architecture: Agents with Identity</h2>
<p>Our primary AI agent runs inside the IDE and has access to the full documentation repository &mdash; the game design document, backlog, meeting notes, company operations, everything. It's not a chatbot. It's a persistent team member with a <strong>soul file</strong> that defines its values and operating principles, and a <strong>memory file</strong> that persists context across sessions.</p>
<p>The key insight: <strong>all knowledge lives in markdown files in one repo</strong>. No databases, no SaaS dashboards, no proprietary formats. Plain text, version-controlled, readable by humans and agents alike. When anyone on the team opens the docs repo, the agent wakes up with full context of who they are, what machine they're on, and what's been happening.</p>
<h3>What the agent actually does</h3>
<p>This is what happens when you stop treating AI as a chatbot and start treating it as a team member with a persistent identity, a memory, and a set of skills it can actually execute.</p>
<h2>The problem with "just use ChatGPT"</h2>
<p>Every small studio hits the same wall: four people, forty roles. Nobody has time to keep documentation current. Bugs pile up. The backlog rots. Someone asks "what did we decide about the inventory system?" and three different answers come back.</p>
<p>The usual fix is more tools — Notion, Trello, Linear, Slack integrations. But tools are passive. They sit there waiting for humans to update them. In a team where the lead developer is also the CTO, that human is already stretched thin.</p>
<p>We tried something different. Instead of adding more tools for humans to maintain, we gave AI agents persistent identities, connected them to our entire knowledge base, and let them do the maintenance.</p>
<h2>Soul files: giving agents a personality that sticks</h2>
<p>The core idea is embarrassingly simple. Every agent gets a <strong>soul file</strong> — a markdown document that defines who it is, what it values, and how it should behave:</p>
<ul>
<li><strong>Values</strong> — "never break the build," "always verify before acting," "prefer existing patterns over novelty"</li>
<li><strong>Knowledge scope</strong> — what repos exist, who's on the team, what the game is about</li>
<li><strong>Behavioural rules</strong> — when to act autonomously, when to ask, what requires explicit human approval</li>
</ul>
<p>This isn't theatre. It's the difference between an agent that asks "what project is this?" every session and one that says "I see the vegetation grid was updated yesterday — want me to check the cache eviction?"</p>
<p>The soul file loads in 100ms when the agent starts. No cold starts. No re-explaining.</p>
<h2>Memory: markdown in git, not a vector database</h2>
<p>Agent memory lives as plain markdown files in our docs repo. No vector databases, no proprietary stores, no SaaS dashboards. The agent writes to its memory file during work, commits it, and reads it on the next session.</p>
<p>This is deliberately low-tech. Markdown in git gives you version history, diffs, branching, and human readability for free. When memory goes wrong — and it will — you <code>git log</code> to see what changed and <code>git revert</code> to fix it. Try debugging a corrupted vector embedding at 11pm.</p>
<h2>Skills: teachable playbooks, not prompt engineering</h2>
<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(&ndash;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(&ndash;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(&ndash;c-accent-l);">3D Model Generation</a> — Tripo Studio text-to-3D</li>
<li><a href="../skills/sora2-video.md" style="color: var(&ndash;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>
<p><strong>During the day</strong> (interactive, inside the IDE):</p>
<ul>
<li>Triages and grooms the issue backlog</li>
<li>Keeps documentation in sync with the game state</li>
<li>Processes bug reports from testers and creates structured issues</li>
<li>Drafts team announcements, reviews PRs, manages cross-repo coordination</li>
<li>Generates concept art, trailer frames, and UI assets using integrated API skills</li>
<li>Conducts competitive research &mdash; analysing Steam pages, player reviews, pricing strategies</li>
<li>Keeps documentation in sync with what's actually in the game</li>
<li>Processes tester bug reports and creates structured issues</li>
<li>Generates concept art, trailer frames, UI icons on demand</li>
<li>Conducts competitive research — Steam pages, player reviews, pricing</li>
</ul>
<p>The team talks to the agent through voice. The IDE's built-in microphone transcribes and auto-translates (multilingual team). The agent is trained to interpret messy voice-to-text artifacts and act on intent, not grammar.</p>
<h2>Background Automation</h2>
<p>The interactive agent only runs when someone opens the IDE. But a studio doesn't sleep &mdash; bugs get reported at midnight, issues go stale, and the team chat fills up while everyone's away.</p>
<p>A background daemon runs 24/7, ticking every 15 minutes. It uses a three-tier model strategy &mdash; cheap models for routine checks, medium for analysis, and premium only when it needs deep reasoning. The whole thing costs about $15/day.</p>
<h3>What it handles</h3>
<p>The team talks to the agent through voice. The IDE microphone transcribes, and the agent interprets intent from messy voice-to-text. "There's a tree floating two meters above the terrain on the west beach" becomes a filed issue with a screenshot, a vision-model description, and coordinates.</p>
<p><strong>At night</strong> (background daemon, $15/day):</p>
<ul>
<li><strong>Chat monitoring</strong> &mdash; polls team chat, responds to commands, acknowledges messages</li>
<li><strong>Bug intake</strong> &mdash; when a tester reports a bug in chat, creates a structured issue automatically</li>
<li><strong>Stale issue detection</strong> &mdash; flags issues that haven't been touched, nudges the team</li>
<li><strong>Daily summaries</strong> &mdash; posts a morning digest of what happened overnight</li>
<li><strong>Self-learning</strong> &mdash; creates its own skill files when it discovers better approaches</li>
</ul>
<p>The two agents coordinate through the docs repo itself. One writes, the other reads. No API calls between them, no message queue. Just git.</p>
<h2>The Skill System</h2>
<p>Agents don't just have instructions &mdash; they have <strong>skills</strong>. Each skill is a markdown file that teaches the agent a specific workflow: how to generate concept art through a pipeline, how to use image generation APIs, how to conduct competitive research, how to create 3D models from concept art.</p>
<p>When someone asks the agent to do something that matches a skill, it reads the skill file and follows the procedure. This means you can teach the agent new capabilities without changing any code &mdash; just write a new markdown file.</p>
<p>We've open-sourced several of our skills in this repo:</p>
<ul>
<li><a href="../skills/image-generation.md" style="color: var(&ndash;c-accent-l);">Image Generation with fal.ai</a></li>
<li><a href="../skills/concept-art-pipeline.md" style="color: var(&ndash;c-accent-l);">Concept Art Pipeline</a></li>
<li><a href="../skills/tripo-browser-workflow.md" style="color: var(&ndash;c-accent-l);">3D Model Generation with Tripo</a></li>
<li><a href="../skills/sora2-video.md" style="color: var(&ndash;c-accent-l);">Video Generation with Sora 2</a></li>
</ul>
<h2>Soul Files: Why Identity Matters</h2>
<p>Giving the agent a persistent identity isn't theatre. It creates consistency across sessions. The soul file defines:</p>
<ul>
<li><strong>Values</strong> &mdash; what the agent prioritises (e.g., "never break the build," "always verify before acting")</li>
<li><strong>Knowledge scope</strong> &mdash; what repos, services, and team members exist</li>
<li><strong>Behavioural rules</strong> &mdash; how to handle ambiguity, when to ask vs act, what requires human approval</li>
</ul>
<p>The agent remembers what it learned, adapts to who's asking, and maintains the same principles whether it's triaging bugs or drafting a Steam page description. The soul file is the agent's constitution.</p>
<h2>What We've Learned</h2>
<p><strong>Plain text is the universal API.</strong> Every tool, every agent, every human can read a markdown file. We store everything &mdash; design documents, meeting notes, agent memory, team contacts &mdash; as .md files in one repository. This sounds almost too simple, but it eliminates an entire class of integration problems.</p>
<p><strong>Cheap models for routine, expensive models for thinking.</strong> Most of what an autonomous agent does is pattern matching and text formatting &mdash; you don't need the most expensive model for that. Save the premium tokens for decisions that actually require reasoning.</p>
<p><strong>The human stays in the loop for decisions.</strong> The agents can file issues, draft announcements, and generate assets &mdash; but they don't merge code, deploy builds, or post to public channels without explicit approval. The workflow is designed so the AI handles the grunt work while humans make the calls that matter.</p>
<p><strong>Voice input changes everything.</strong> When you can describe a bug while looking at the game screen, and the agent transcribes, interprets, and files an issue &mdash; that's a workflow that collapses the distance between noticing a problem and tracking it.</p>
<p><strong>Skills compound.</strong> Every skill file you write makes the agent more capable. After 6 months, our agents have 15+ skills covering art generation, competitive research, video production, and project management. Each one took 30 minutes to write and saves hours every week.</p>
<h2>The Numbers</h2>
<ul>
<li><strong>Team size:</strong> 4 humans + AI agents</li>
<li><strong>Background agent cost:</strong> ~$15/day (~$450/month)</li>
<li><strong>Knowledge files:</strong> 200+ markdown documents</li>
<li><strong>Skills:</strong> 15+ agent skill files and growing</li>
<li><strong>Infrastructure:</strong> Multiple machines, self-hosted git, zero DevOps engineers</li>
<li>Polls team chat every 15 minutes, responds to commands</li>
<li>When a tester reports a bug in chat, creates a structured issue automatically</li>
<li>Flags stale issues that haven't been touched</li>
<li>Posts a morning digest of what happened overnight</li>
<li>Creates its own skill files when it discovers better approaches</li>
</ul>
<h2>What we learned</h2>
<p><strong>Plain text is the universal API.</strong> Every tool, every agent, every human can read a markdown file. We store everything — design docs, meeting notes, agent memory, team contacts — as <code>.md</code> in one repo. It sounds too simple, but it eliminates an entire class of integration problems.</p>
<p><strong>Cheap models for routine, expensive models for thinking.</strong> Most of what an agent does is pattern matching — "does this look like a bug report?" You don't need DeepSeek Pro for that. Save the premium tokens for decisions that actually require reasoning. Our background daemon costs $15/day with a three-tier model strategy.</p>
<p><strong>Voice changes everything.</strong> When you can describe a bug while looking at the screen, and the agent transcribes, interprets, and files it — that collapses the distance between noticing a problem and tracking it. Keyboard-free bug reporting is a superpower.</p>
<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>We're not claiming this is how every studio should work. But for a small team trying to build something ambitious, having AI agents that actually understand the project &mdash; not just answer questions about it &mdash; has been transformative. The agents don't replace anyone on the team. They make it possible for four people to do the work of forty.</p>
<p>We're building all of this as part of <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> &mdash; a game development platform that brings git hosting, AI tools, and team workflows together. The blog posts and skills in this repo are part of that journey.</p>
<p><em>We're building <a href="https://arikigame.com" style="color: var(&ndash;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(&ndash;c-accent-l);">Tinqs Studio</a> — a game dev platform with built-in AI agents, git hosting, and creative pipelines.</em></p>
</div>
@@ -165,38 +330,5 @@
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
+165
View File
@@ -0,0 +1,165 @@
# agents.md — Tinqs Blog Agent Guide
This file teaches AI agents (Pi, Cursor, Claude Code) how to work with the Tinqs blog repo. Read it before making any changes.
## Shared context
Shared agent identity, team roster, skills, and cross-project rules live in `../docs/.agents/`. Read at session start:
- `../docs/.agents/SOUL.md` — shared identity
- `../docs/ai/company.md` — team roster
- `../docs/ai/siblings.md` — repo locations
- `../docs/.agents/rules/shared-context.md` — cross-repo conventions
## Blog architecture
```
tinqs-ltd/blog/
├── _template.html # Post shell — wraps a single blog post
├── _index_template.html # Listing shell — blog index page
├── build.js # Zero-dep Node script: posts/*.md + templates → *.html
├── posts/ # Markdown posts with YAML frontmatter
│ ├── agent-harness.md
│ ├── agentic-workflow.md
│ └── ...
├── *.html # Generated output (never hand-edit regular posts)
├── pi-flow-native-brain.html # Hand-authored HTML post (SVGs + tables)
├── agents.md # This file
└── skills/ # Reusable skill playbooks
```
### Two kinds of posts
1. **Regular posts** — Markdown in `posts/*.md`, built via `node build.js` into `*.html`. Always edit the `.md`, never the `.html`.
2. **Hand-authored HTML posts**`pi-flow-native-brain.html` is the only one. It contains inline SVGs and styled tables that build.js can't emit. Edit the HTML directly, never create a `.md` for it. Cards for hand-authored posts are hardcoded in `_index_template.html`.
### Build pipeline
```bash
node build.js # reads posts/*.md → generates *.html + index.html
```
`build.js` has zero npm dependencies — pure Node.js built-ins. It handles:
- YAML frontmatter parsing
- Minimal markdown → HTML conversion (headings, bold/italic, inline code, fenced code blocks, lists, figures, links, hr)
- `<!--raw-->` / `<!--/raw-->` blocks for raw HTML passthrough
- Lead paragraph separation (first paragraph after frontmatter → `.post__lead`)
- Date formatting
- Index page generation (newest-first sorted cards)
## How to add a post
### Regular markdown post
Create `posts/<slug>.md`:
```yaml
---
title: "Post Title — with optional subtitle"
slug: url-friendly-slug
date: "2026-06-03"
description: "Full meta description for SEO (150-160 chars ideal)."
og_description: "Shorter OG/Twitter description (optional)."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "Card text shown on the blog index page."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
First paragraph becomes the lead. Keep it punchy — two sentences max.
Everything after the first blank line is the post body. Use standard markdown.
```
Then:
```bash
node build.js # generates <slug>.html + rebuilds index.html
git add posts/<slug>.md <slug>.html index.html
git commit -m "post: <title>"
```
### Hand-authored HTML post
Copy `pi-flow-native-brain.html` as a template. Keep the `<style>` block and nav/footer wrapper. Key rules:
- Always add a card to `_index_template.html` so it appears on the listing page
- Never create a corresponding `.md` in `posts/` — build.js will overwrite it
- Use the same class structure: `.post`, `.post__title`, `.post__body`, etc.
### Adding a card for a hand-authored HTML post
In `_index_template.html`, add before `{{CARDS}}`:
```html
<a href="your-slug" class="blog-card">
<span class="blog-card__date">3 June 2026</span>
<h2 class="blog-card__title">Your Title</h2>
<p class="blog-card__excerpt">Card excerpt text.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
```
## Styling
### Three layers (cascade order)
1. `../style.css` — external, served by Git Studio. Nav, footer, base typography, `--c-accent: #c9935a`. Never edit.
2. `<style>` in `_template.html` — post-page overrides (inline, at end of `<head>`)
3. `<style>` in `_index_template.html` — index-page overrides
The inline `<style>` blocks come AFTER the `../style.css` link, so same-specificity rules win by cascade order. No `!important` needed.
### Adding a style rule
1. Open `_template.html` (or `_index_template.html` for listing-only styles)
2. Find the `<style>` block at end of `<head>` (marked with `/* ── Team guide aesthetic ── */`)
3. Add your rule using the existing palette:
- Amber `#c9935a` (brand anchor), gold `#f59e0b` (emphasis)
- Blue `#38bdf8` (links, pills), purple `#a855f7` (h3, hover)
- Dark `#0a0e14` (code bg), border `#2a3340`
4. `node build.js` to regenerate
5. Verify: `grep "your-selector" *.html`
### Never
- Edit `../style.css` (outside this repo)
- Hand-edit generated `*.html` (build.js clobbers them)
- Restyle `.nav`, `.footer`, or mobile menu (belongs to parent site)
- Introduce new colours without a strong reason
- Add external font loads, CDN deps, or `@import`
## Post structure (writing guide)
Good technical posts follow this pattern:
1. **Lead paragraph** — what this is about, one punchy sentence
2. **The hook** — why it matters, what problem it solves
3. **How it works** — concrete examples, code, metrics
4. **What we learned** — insights, surprises, trade-offs
5. **Closing** — what's next, internal links to related posts
Voice: direct, concrete, no marketing fluff. Show numbers. Show code. Tell stories.
### SEO checklist
- Title under 60 characters
- Description 150-160 characters
- `og_image` set (falls back to `/img/og-cover.jpg`)
- Meaningful excerpt for index card
- Internal links where relevant (`[other post](other-slug)`)
### Conventions
- Slugs: kebab-case matching filename: `my-post.md` → slug `my-post`
- Dates: ISO format `2026-06-03`
- Canonical URLs: `https://www.tinqs.com/blog/<slug>`
- Em dashes: `---` in markdown renders as `&mdash;`, `--` as `&ndash;`
## Deploy
```bash
git add -A
git commit -m "post: <description>"
git push origin main
```
Git Studio serves this repo directly. A push to main is a deploy. No build step on the server — static HTML files.
## Skills
Skills live in the hub: `../docs/.agents/skills/` (image-generation, concept-art-pipeline, sora2-video, tripo-browser-workflow, and more). Read skill SKILL.md files from there when needed.
+320
View File
@@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How We Restyled Our Blog with Two Template Files and Zero Dependencies — Tinqs Blog</title>
<meta name="description" content="Gradient titles, dark code panels, amber callouts — we gave the Tinqs blog a visual refresh borrowing our internal team guide aesthetic. Two template files, one Node script, no framework.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/blog-visual-upgrade">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/blog-visual-upgrade">
<meta property="og:title" content="How We Restyled Our Blog with Two Template Files and Zero Dependencies">
<meta property="og:description" content="Blog restyle: gradient titles, dark code panels, amber callouts — two template files, zero deps.">
<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="How We Restyled Our Blog with Two Template Files and Zero Dependencies">
<meta name="twitter:description" content="Blog restyle: gradient titles, dark code panels, amber callouts — two template files, zero deps.">
<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": "How We Restyled Our Blog with Two Template Files and Zero Dependencies",
"datePublished": "2026-06-03",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "Gradient titles, dark code panels, amber callouts — we gave the Tinqs blog a visual refresh borrowing our internal team guide aesthetic. Two template files, one Node script, no framework."
}
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">3 June 2026</span>
<h1 class="post__title">How We Restyled Our Blog with Two Template Files and Zero Dependencies</h1>
<p class="post__lead">Our blog looked fine. Readable, semantic, proper typography. But it didn't have much personality. Code blocks were unstyled. Headings sat flat. The design said "competent" more than "intentional."</p>
<div class="post__body">
<p>Then we looked at our internal team guide — a self-contained HTML doc with gradient titles that clip to transparent, dark code panels, and callout boxes with coloured borders. It radiated a "well-maintained developer doc" energy. We wanted the blog to feel like it came from the same shop.</p>
<p>Two template files, one build step, zero external dependencies. Here's what we changed.</p>
<h2>The build system (why it mattered)</h2>
<p>The blog is generated by <code>build.js</code> — a zero-dependency Node script that converts markdown to HTML:</p>
<pre><code>posts/*.md + _template.html / _index_template.html → *.html</code></pre>
<p>This means we never touch a generated <code>.html</code> file by hand. Every visual change flows through the templates. The site-wide CSS — nav, footer, base typography, brand accent — lives in <code>../style.css</code>, served by Git Studio from outside the repo. We didn't touch it.</p>
<p>Instead, we injected a self-contained <code><style></code> block at the end of <code><head></code> in both templates, after the <code>../style.css</code> link. Cascade order handles the overrides. No <code>!important</code>. No external font loads. No CDN dependencies.</p>
<h2>The palette</h2>
<p>Four accent colours, borrowed from our team guide:</p>
<p>| Role | Colour | Where |</p>
<p>|&mdash;&mdash;|&mdash;&mdash;&ndash;|&mdash;&mdash;-|</p>
<p>| Brand amber | <code>#c9935a</code> | Gradient start, h2 left bar, card hover |</p>
<p>| Warm gold | <code>#f59e0b</code> | Gradient midpoint, bold text |</p>
<p>| Blue | <code>#38bdf8</code> | Gradient endpoint, links, date pills |</p>
<p>| Purple | <code>#a855f7</code> | h3 colour, link hover |</p>
<p>Tasteful. Not a rainbow.</p>
<h2>What we styled</h2>
<p><strong>Gradient titles.</strong> Post <code>h1</code> gets <code>linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8)</code> via <code>background-clip: text; color: transparent</code>. Underlying text preserved for screen readers and SEO.</p>
<p><strong>Date pills.</strong> Monospace chip — blue text, <code>999px</code> border-radius, uppercase, tight letter-spacing. Sits before the title like a kicker.</p>
<p><strong>Code blocks.</strong> The site CSS only styled inline <code><code></code>. Fenced blocks got a dark panel (<code>#0a0e14</code>, <code>#2a3340</code> border, <code>10px</code> radius, monospace, <code>overflow-x: auto</code>). A reset rule on <code>pre code</code> prevents inline-code styles from doubling up inside the panel.</p>
<p><strong>Section cues.</strong> h2 gets a 4px amber left border as visual anchor. h3 gets a purple tint — enough to signal a section break without pulling focus.</p>
<p><strong>Links.</strong> Blue <code>#38bdf8</code>, purple <code>#a855f7</code> on hover. Bold text picks up amber.</p>
<p><strong>Blockquote callouts.</strong> Amber-tinted background, 4px amber left border, rounded right corners. The CSS is written and waiting — <code>build.js</code> doesn't emit <code><blockquote></code> yet, but the rules activate the moment we add <code>></code> syntax support.</p>
<p><strong>Index page.</strong> Same gradient on the listing title. Date pills on cards. Amber border on card hover. Blue/purple read links.</p>
<h2>The rebuild</h2>
<pre><code class="language-bash">node build.js</code></pre>
<pre><code>Building blog...
11 posts built + index.html
Done.</code></pre>
<p>Zero errors. Every regenerated HTML file now carries the inline <code><style></code>. Confirmed with <code>grep -l "background-clip" *.html</code> all pages ship the gradient.</p>
<h2>What we didn't touch</h2>
<p>Navigation, footer, site chrome, responsive behaviour — all unchanged. This was a CSS-only change. No markup altered beyond the <code><style></code> injection.</p>
<h2>What we learned</h2>
<p><strong>Inline styles beat separate CSS files when you don't control the server.</strong> The blog repo is standalone — it can't modify <code>../style.css</code>. Inline <code><style></code> blocks ship inside the generated HTML, so the blog is fully self-contained. One <code>git push</code> and it's live.</p>
<p><strong>Cascade order is the cleanest specificity hack.</strong> Putting the <code><style></code> block after the external stylesheet link means same-selector rules win by position. No <code>!important</code>, no selector wars, no unexpected regressions.</p>
<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(&ndash;c-accent-l);">build.js</a> and served by <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a>. All styling is self-contained in the templates.</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>
After
+8
View File
@@ -51,6 +51,7 @@ function md(src) {
let inCode = false;
let codeLang = "";
let codeLines = [];
let inRaw = false;
function closeUl() {
if (inUl) { html += "</ul>\n"; inUl = false; }
@@ -59,6 +60,13 @@ function md(src) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Raw HTML passthrough — everything between <!--raw--> and <!--/raw-->
// is emitted verbatim (no escaping, no <p> wrap). For inline SVG diagrams,
// tables, or any hand-authored markup the minimal converter can't express.
if (line.trim() === "<!--raw-->") { closeUl(); inRaw = true; continue; }
if (line.trim() === "<!--/raw-->") { inRaw = false; continue; }
if (inRaw) { html += line + "\n"; continue; }
// Fenced code blocks
if (line.startsWith("```")) {
if (!inCode) {
-231
View File
@@ -1,231 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Building a Cloud Agent Harness with DeepSeek V4 and Pi — Tinqs Blog</title>
<meta name="description" content="We forked Pi, merged a browser dashboard into the monorepo, and built a Go orchestrator inside our Gitea fork. Agents code overnight for about $0.80 — and you can watch them from localhost:33634.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/cloud-harness">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/cloud-harness">
<meta property="og:title" content="Building a Cloud Agent Harness with DeepSeek V4 and Pi">
<meta property="og:description" content="Pi fork, merged agent dashboard, and a Go orchestrator inside Tinqs Studio.">
<meta property="og:image" content="https://www.tinqs.com/blog/img/cloud-harness-architecture.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Building a Cloud Agent Harness with DeepSeek V4 and Pi">
<meta name="twitter:description" content="Pi fork, merged agent dashboard, and a Go orchestrator inside Tinqs Studio.">
<meta name="twitter:image" content="https://www.tinqs.com/blog/img/cloud-harness-architecture.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Building a Cloud Agent Harness with DeepSeek V4 and Pi",
"datePublished": "2026-05-26",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "We forked Pi, merged a browser dashboard into the monorepo, and built a Go orchestrator inside our Gitea fork. Agents code overnight for about $0.80 — and you can watch them from localhost:33634."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">26 May 2026</span>
<h1 class="post__title">Building a Cloud Agent Harness with DeepSeek V4 and Pi</h1>
<p class="post__lead">We spent a few sessions building something that still barely exists elsewhere: a cloud agent harness where AI coding agents are first-class citizens of the platform, not bolt-on tools. The stack is a <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">Pi fork</a> for the brain, a Go orchestrator inside our <a href="https://tinqs.com/tinqs/studio" style="color: var(&ndash;c-accent-l);">Gitea fork</a> for overnight work, and a browser dashboard merged into Pi for the daytime. Here is how it fits together.</p>
<div class="post__body">
<h2>The Problem</h2>
<p>Every coding agent today — Claude Code, Codex, Pi, Aider — runs in your terminal. You watch it work. You close the laptop, it stops. There is no way to say "build these eight features overnight" and wake up to pull requests.</p>
<p>We wanted exactly that. Not a coding assistant. An autonomous workforce — with a UI when a human needs to be in the loop.</p>
<h2>Why Not Just Use Claude Code or Codex?</h2>
<p><strong>Cost.</strong> Claude Code runs on Opus at $15/MTok output. Codex uses GPT 5.5. Running eight agents overnight on either would cost $50200. DeepSeek V4 Flash costs $0.28/MTok output. Eight overnight tasks: <strong>about $0.80</strong>.</p>
<p><strong>Control.</strong> Cloud tools are black boxes. We cannot add a Gitea API tool, a fal.ai image generator, or a guardrail that blocks <code>aws ec2 terminate-instances</code>. With our own harness, we add an extension and it is live.</p>
<p><strong>Platform.</strong> We are building <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> — a Gitea-based game development platform. Agents are not a feature we want to outsource. They are the product.</p>
<h2>Pi — The Agent Brain</h2>
<p><a href="https://pi.dev" style="color: var(&ndash;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.</p>
<p>We <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">forked it</a>. Not to rewrite the core — to add first-party extensions:</p>
<ul>
<li><strong>tinqs-provider</strong> — routes DeepSeek V4 Flash and Pro through our inference proxy</li>
<li><strong>tinqs-tools</strong> — Gitea REST API, fal.ai image generation, Amazon Nova Lite vision</li>
<li><strong>tinqs-ci</strong> — reads CI pipeline status, logs, and polls for completion</li>
<li><strong>tinqs-guardrail</strong> — 29 safety patterns that block dangerous operations</li>
</ul>
<p>Each extension is a single TypeScript file. No extra npm dependencies on the extension side.</p>
<p>Pi has four output modes. The one that matters for automation is <strong>RPC</strong> — a headless process that accepts JSON on stdin/stdout. That is how the orchestrator drives it.</p>
<h2>DeepSeek V4 — The LLM</h2>
<p>DeepSeek V4 Flash through our own inference proxy. OpenAI-compatible API, so Pi treats it like any other provider. The proxy adds:</p>
<ul>
<li>Redis job queue (10 concurrent workers)</li>
<li>Per-user usage tracking</li>
<li>System prompt injection for cache hit optimization</li>
<li>Gitea PAT authentication (same token as git push)</li>
</ul>
<p>Cost per task: <strong>$0.020.10</strong> depending on complexity.</p>
<h2>Go Orchestrator — Overnight Batch Work</h2>
<p>Inside <code>tinqs/studio</code> we added <code>modules/agents/</code> — a Go worker pool that:</p>
<ul>
<li>Spawns Pi with <code>&ndash;mode rpc &ndash;no-session</code></li>
<li>Tracks task lifecycle (pending → running → done)</li>
<li>Streams events over <strong>SSE</strong> to any connected UI</li>
<li>Enforces guardrails at the platform layer (worker limits, timeouts)</li>
</ul>
<p>Six HTTP endpoints, same auth as git push:</p>
<pre><code>POST /api/v1/agents/tasks — submit a task
GET /api/v1/agents/tasks — list all tasks
GET /api/v1/agents/tasks/{id} — get task details
DELETE /api/v1/agents/tasks/{id} — stop a task
GET /api/v1/agents/stream — SSE live events
GET /api/v1/agents/health — orchestrator status</code></pre>
<p>We considered bolting on a separate orchestration SaaS and rejected it. The orchestrator lives in the same binary as git — same auth, no extra service to deploy.</p>
<p>The intended loop:</p>
<pre><code>Orchestrator reads task brief
→ spawns pi --mode rpc
→ Pi writes code using DeepSeek V4
→ Pi pushes branch, calls ci_wait
→ CI green → Pi opens PR via gitea_api
→ CI red → Pi reads ci_logs, fixes, retries
→ Human reviews PR, merges</code></pre>
<p>Git worktree integration and full push/PR automation are still being wired; the API and worker pool already run locally.</p>
<h2>Pi Dashboard — Browser UI (Shipped)</h2>
<p>The cloud orchestrator is for batch work while you sleep. During the day you want to see agents, chat with them, and spawn sessions without living in a terminal.</p>
<p>We merged <a href="https://github.com/BlackBeltTechnology/pi-agent-dashboard" style="color: var(&ndash;c-accent-l);">pi-agent-dashboard</a> into the Pi monorepo — not as a second repo to install. One checkout, one command:</p>
<pre><code class="language-bash">npm run dashboard:dev</code></pre>
<p>Open <strong>http://localhost:33634</strong>. You get:</p>
<ul>
<li><strong>Live session streaming</strong> — watch tool calls and model output in real time</li>
<li><strong>Interactive chat</strong> — send prompts, answer <code>ask_user</code> dialogs from the browser</li>
<li><strong>Session spawning</strong> — start Pi in any pinned project folder</li>
<li><strong>Cost tracking</strong> — per-session token usage when using Tinqs inference</li>
<li><strong>Plugins</strong> — flows, subagents, workspace helpers</li>
</ul>
<p>The dashboard talks to Pi sessions over a WebSocket bridge on port <strong>9999</strong>. Inference uses the same Tinqs proxy as the CLI — register a custom provider in <code>~/.pi/agent/providers.json</code> and authenticate with your existing <code>tstudio</code> token. No separate LLM API keys.</p>
<pre><code>Dashboard (localhost:33634)
↕ WebSocket (port 9999)
Pi sessions (interactive or headless)
↕ OpenAI-compatible API
Tinqs Studio proxy (tinqs.com/api/v1/ai)
↕ DeepSeek V4 Flash / Pro</code></pre>
<p>When Studio runs locally with agents enabled, the dashboard can also talk to the orchestrator API on port 3000 — submit tasks and watch SSE events in the same UI.</p>
<p>One browser tab for daytime work; the orchestrator queue for overnight runs.</p>
<h2>The Guardrail</h2>
<p>Our biggest fear: an agent hallucinating instead of using tools, or running <code>aws ec2 terminate-instances</code> at 3 AM.</p>
<p>The guardrail extension monitors every agent turn:</p>
<p><strong>Hallucination detection</strong> — if the agent claims file contents without calling <code>read</code>, it gets corrected.</p>
<p><strong>No-tool drift</strong> — three consecutive turns without a tool call triggers a warning.</p>
<p><strong>Command blocking</strong> — 29 patterns covering destructive git, AWS teardown, process killing, and production API abuse.</p>
<h2>What It Cost to Build</h2>
<p>A few focused sessions: about 2,000 lines of Go, 900 lines of TypeScript extensions, 52 tests, plus merging the dashboard packages into the Pi monorepo. No new servers — Pi is a Node subprocess; the dashboard is another Node process on your machine.</p>
<h2>What Is Next</h2>
<p>| Piece | Status |</p>
<p>|&mdash;&mdash;-|&mdash;&mdash;&ndash;|</p>
<p>| Pi fork + tinqs extensions | Shipped |</p>
<p>| Dashboard merged into Pi monorepo | Shipped |</p>
<p>| Go orchestrator + REST/SSE API | MVP, running locally |</p>
<p>| Git worktree + push + PR loop | In progress |</p>
<p>| Domain routing (game / sim / platform tasks) | Designed |</p>
<p>Next we are promoting studio skills from IDE playbooks into orchestrator prompt packs — so the same Pi worker behaves like a game builder, sim maintainer, or platform engineer depending on the task. Specialized agents (planner, reviewer, asset pipeline) sit on top of this foundation.</p>
<p>The harness — inference proxy, guardrails, dashboard, orchestrator API — is in place. The work now is feeding it real tasks and hardening the git loop.</p>
<hr>
<p><em>Tinqs Studio is an open platform for game development — git hosting, AI inference, asset generation, and autonomous agents. We are building <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a>, a survival colony sim, using the same tools we ship.</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>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
+292 -165
View File
@@ -4,27 +4,27 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Art at Scale: Using fal.ai Flux for Game Asset Generation — Tinqs Blog</title>
<meta name="description" content="How we use fal.ai Flux models to generate concept art, trailer frames, and UI assets for our game --- with a 4-layer prompt pattern that actually works.">
<title>AI Art at Every Price Point: How We Generate Game Assets with fal.ai — Tinqs Blog</title>
<meta name="description" content="From $0.002 to $0.09 per image, across 12 models. How we built a prompt pattern that actually produces usable game art, and a model-picking strategy that keeps costs at $8/month.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/fal-image-generation">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/fal-image-generation">
<meta property="og:title" content="AI Art at Scale: Using fal.ai Flux for Game Asset Generation">
<meta property="og:description" content="fal.ai Flux for game art: 4-layer prompts, $0.01/image, and a pipeline that replaced our concept art bottleneck.">
<meta property="og:title" content="AI Art at Every Price Point: How We Generate Game Assets with fal.ai">
<meta property="og:description" content="12 fal.ai models, $0.002-$0.09/image, 4-layer prompt pattern. Game art that actually ships.">
<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="AI Art at Scale: Using fal.ai Flux for Game Asset Generation">
<meta name="twitter:description" content="fal.ai Flux for game art: 4-layer prompts, $0.01/image, and a pipeline that replaced our concept art bottleneck.">
<meta name="twitter:title" content="AI Art at Every Price Point: How We Generate Game Assets with fal.ai">
<meta name="twitter:description" content="12 fal.ai models, $0.002-$0.09/image, 4-layer prompt pattern. Game art that actually ships.">
<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": "AI Art at Scale: Using fal.ai Flux for Game Asset Generation",
"headline": "AI Art at Every Price Point: How We Generate Game Assets with fal.ai",
"datePublished": "2026-05-25",
"author": {
"@type": "Person",
@@ -35,150 +35,310 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "How we use fal.ai Flux models to generate concept art, trailer frames, and UI assets for our game --- with a 4-layer prompt pattern that actually works."
"description": "From $0.002 to $0.09 per image, across 12 models. How we built a prompt pattern that actually produces usable game art, and a model-picking strategy that keeps costs at $8/month."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">25 May 2026</span>
<h1 class="post__title">AI Art at Scale: Using fal.ai Flux for Game Asset Generation</h1>
<p class="post__lead">We're a small indie studio building a survival colony sim. We don't have a concept artist on staff. Every piece of character art, trailer frame, and UI icon in our game was generated with fal.ai Flux models &mdash; at roughly a penny per image.</p>
<h1 class="post__title">AI Art at Every Price Point: How We Generate Game Assets with fal.ai</h1>
<p class="post__lead">Every visual asset in our game — character art, app icons, trailer frames, logo variants, Steam capsules — was generated through a single API. No Photoshop. No concept artist on staff. Last month: 673 images, $8.30 total. Here's the prompt pattern that makes AI art actually usable for game development, and how we pick between 12 models spanning $0.002 to $0.09 per image.</p>
<div class="post__body">
<h2>The Problem with AI Art for Games</h2>
<p>Most AI image generators produce beautiful images that are completely useless for game development. They look great on social media but fall apart when you need consistency: the same character from four angles, a UI icon that reads at 64x64, a trailer frame that matches your game's art style rather than whatever the model defaults to.</p>
<p>The issue isn't the models &mdash; Flux is genuinely good. The issue is prompting. When you write "warrior on a beach," you get a different art style every time. Different skin tones, different proportions, different lighting. You can't build a game from that.</p>
<p>We spent three months iterating on prompt patterns before we found something that works consistently. The result is a 4-layer system that anchors the model to your art direction and produces images you can actually ship.</p>
<h2>Why fal.ai</h2>
<p>We evaluated Midjourney, DALL-E 3, Stable Diffusion (self-hosted), and fal.ai:</p>
<p><strong>API-first.</strong> Midjourney is Discord-only. DALL-E's API works but the model makes everything look like a stock photo. Self-hosted SD means maintaining GPU infrastructure. fal.ai gives you Flux models behind a simple REST API &mdash; POST a prompt, GET an image URL.</p>
<p><strong>Cost.</strong> $0.01 per image with <code>flux-2-pro</code>. $0.004 with <code>schnell</code> for rapid iteration. A full character design session &mdash; 12 variants across 3 rounds of refinement &mdash; costs $0.12. A 20-frame trailer storyboard costs $0.20. At these prices, the bottleneck is creative direction, not budget.</p>
<p><strong>Speed.</strong> <code>flux/schnell</code> returns an image in 4 seconds. <code>flux-2-pro</code> in 15 seconds. Fast enough that an AI agent can generate, display, get feedback, and regenerate in a single conversation turn.</p>
<p><strong>No subscription.</strong> Pay per image. No monthly fee, no credit packs that expire, no tier-gated features.</p>
<h2>The 4-Layer Prompt Pattern</h2>
<p>This is the pattern that made AI art actually usable for our game. Each layer adds specificity, and the combination anchors the model to a consistent output.</p>
<h3>Layer 1: Design Context</h3>
<p>The most important layer and the one most people skip. It sets the overall art direction:</p>
<h2>The problem with AI art for games</h2>
<p>Most AI-generated images look beautiful on social media and useless in a game. The character looks different from every angle. The art style drifts between generations. The text in the logo is garbled. The icon doesn't read at 64×64.</p>
<p>The issue isn't the models — Flux, Ideogram, and Recraft are genuinely good. The issue is prompting. "Warrior on a beach" gives you a different art style, different skin tone, different proportions every time. You can't build a game from one-offs.</p>
<p>We spent three months iterating before we found a prompt structure that anchors the model to a consistent art direction and produces images you can actually ship. It has four layers.</p>
<h2>The 4-layer prompt pattern</h2>
<h3>Layer 1: Design context (the anchor)</h3>
<p>This is the most important paragraph and the one most people skip. It sets the art direction for every single generation:</p>
<pre><code>Art direction: stylized 3D render for a survival colony sim. Warm earthy
palette --- browns, tans, dark reds, cream, ocean blues. Carved wood
palette browns, tans, dark reds, cream, ocean blues. Carved wood
textures, traditional patterns, woven natural fibres. Game engine quality,
not photorealistic.</code></pre>
<p>This paragraph appears at the start of every prompt. Same paragraph whether you're generating a character, a landscape, or an icon. It anchors the model to your art style.</p>
<p><strong>The key insight:</strong> write this once, paste it everywhere. It's your art bible compressed into 50 words. Every time we skipped it &mdash; "just a quick test" &mdash; the output drifted into generic fantasy art.</p>
<h3>Layer 2: Scene Description</h3>
<p>Describe exactly what should appear, element by element:</p>
<pre><code>Full body character in T-pose, front view. Young woman, mid-20s.
Wearing a woven wrap skirt (mid-thigh length) and a fitted cloth top.
Shell necklace with a carved bone pendant. Single bone bracelet on
left wrist. Hair swept back over right shoulder. Bare feet.
Matte skin, warm brown tones. Neutral confident expression ---
not smiling, not angry. Dark grey background.</code></pre>
<p>Not "tribal clothing" but "woven wrap skirt." Not "jewelry" but "shell necklace with a carved bone pendant." Vague prompts produce vague results. Specific prompts produce usable assets.</p>
<h3>Layer 3: Negative Prompt</h3>
<p>Same paragraph whether you're generating a character, a landscape, or an icon. It's your art bible compressed into 50 words. Every time we skipped it — "just a quick test" — the output drifted into generic fantasy art.</p>
<h3>Layer 2: Scene description (be specific)</h3>
<p>Not "tribal clothing" — "woven wrap skirt, mid-thigh length." Not "jewelry" — "shell necklace with a carved bone pendant." Vague prompts produce vague results. Specific prompts produce usable assets. Describe element by element.</p>
<h3>Layer 3: Negative prompt (prevent drift)</h3>
<p>Always include what you don't want:</p>
<pre><code>Do not include: cartoon style, anime style, photorealistic render,
extra text or taglines, watermark, deformed elements, modern or
sci-fi. No extra fingers, no merged limbs, no floating accessories.</code></pre>
<p>Extend per-subject. For characters: "no stereotypical elements, no overly shiny materials." The negative prompt is as important as the positive one.</p>
<h3>Layer 4: Reference Images</h3>
<p>When you need consistency &mdash; the same character from different angles, or a new character matching an existing one &mdash; pass a reference image:</p>
<pre><code class="language-python">result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
"prompt": "Same character, side view, same clothing and accessories...",
"image_url": "https://your-approved-front-view.png",
"image_size": "square_hd",
})</code></pre>
<p>The first approved image becomes the reference for all subsequent views. Without it, you get a different person every time.</p>
<h2>The Model Lineup</h2>
<p>| Model | Cost | Speed | When |</p>
<p>|&mdash;&mdash;-|&mdash;&mdash;|&mdash;&mdash;-|&mdash;&mdash;|</p>
<p>| <code>flux-2-pro</code> | $0.01 | ~15s | Final art. Default for anything you'll ship. |</p>
<p>| <code>flux/schnell</code> | $0.004 | ~4s | Exploration and iteration. |</p>
<p>| <code>ideogram/v2</code> | $0.008 | ~5s | Anything with readable text &mdash; logos, UI, posters. |</p>
<p>| <code>flux-pro/v1.1-ultra</code> | $0.015 | ~8s | Highest quality, but can hang. |</p>
<p>The workflow: explore with <code>schnell</code>, refine with <code>flux-2-pro</code>, add text with <code>ideogram/v2</code>.</p>
<h2>How This Fits Our Pipeline</h2>
<p>fal.ai is the first step in a pipeline from idea to in-game asset:</p>
<pre><code>Brief --&gt; fal.ai (2D concept art) --&gt; Tripo Studio (3D model) --&gt; Blender (decimate) --&gt; Godot (in-game)</code></pre>
<p>1. <strong>Brief.</strong> The designer describes the character or asset.</p>
<p>2. <strong>2D generation.</strong> Generate 3 variants with <code>flux-2-pro</code>, score each on a rubric (style match, cultural accuracy, silhouette, expression, animatability), pick the best.</p>
<p>3. <strong>Reference sheet.</strong> Generate front, side, three-quarter, and head closeup views using the winner as reference.</p>
<p>4. <strong>3D model.</strong> Approved concept art goes into Tripo Studio for image-to-3D. Outputs ~1.5M faces with full PBR textures.</p>
<p>5. <strong>Decimation.</strong> Blender CLI decimates to 25,000 faces.</p>
<p>6. <strong>Rigging.</strong> Auto-rig the body (hair separated first if large).</p>
<p>7. <strong>In-game.</strong> Import into the engine, set up materials, done.</p>
<p>The entire pipeline from "I want a character" to "character walking around in the game" takes about 2 hours. The quality isn't AAA, but for an indie game with a stylised art style, it's more than good enough.</p>
<h2>What We Learned</h2>
<p><strong>The design context layer is everything.</strong> Without it, every image is a one-off. With it, every image belongs to the same game. The 50-word context block is worth more than the rest of the prompt combined.</p>
<p><strong>Negative prompts prevent drift.</strong> AI models have strong defaults &mdash; they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, say so explicitly.</p>
<p><strong>Score and iterate, don't accept the first output.</strong> Generate 3 variants, score on 5 criteria, approve only 8+/10. Three attempts at $0.01 each is $0.03 &mdash; cheaper than working around a mediocre image.</p>
<p><strong>Reference images are the consistency mechanism.</strong> Without them, every generation is independent. With them, every generation builds on the last approved output. This is how you get a roster of characters that look like they belong in the same game.</p>
<p><strong>Fast models for exploration, quality models for output.</strong> <code>schnell</code> at 4 seconds is for "what if..." iterations. <code>flux-2-pro</code> at 15 seconds is for "yes, this is the one."</p>
<p><strong>Let the AI agent handle prompt engineering.</strong> We encode the 4-layer pattern, art style guide, and cultural guardrails in a <a href="../skills/image-generation.md" style="color: var(&ndash;c-accent-l);">skill file</a>. The agent writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.</p>
<h2>The Numbers</h2>
<ul>
<li><strong>Characters designed:</strong> 10 (full roster for early access)</li>
<li><strong>Total images generated:</strong> ~400 across all iterations</li>
<li><strong>Total cost:</strong> ~$6 in fal.ai credits</li>
<li><strong>Time per character:</strong> ~30 minutes from brief to approved reference sheet</li>
<li><strong>Pipeline time:</strong> ~2 hours from concept art to in-game model</li>
<li><strong>Models used:</strong> flux-2-pro (80%), schnell (15%), ideogram/v2 (5%)</li>
</ul>
<h2>Open-Source Skills</h2>
<p>We've published the skill files that power this workflow. A skill is a markdown document that teaches an AI agent a specific procedure &mdash; like a runbook, but the reader is an LLM.</p>
<ul>
<li><strong><a href="../skills/image-generation.md" style="color: var(&ndash;c-accent-l);">Image Generation</a></strong> &mdash; fal.ai API, 4-layer prompt pattern, model comparison</li>
<li><strong><a href="../skills/concept-art-pipeline.md" style="color: var(&ndash;c-accent-l);">Concept Art Pipeline</a></strong> &mdash; full 2D-to-3D character workflow</li>
<li><strong><a href="../skills/tripo-browser-workflow.md" style="color: var(&ndash;c-accent-l);">3D Model Generation</a></strong> &mdash; Tripo Studio text-to-3D and image-to-3D</li>
<li><strong><a href="../skills/sora2-video.md" style="color: var(&ndash;c-accent-l);">Video Generation</a></strong> &mdash; trailer clip generation with OpenAI Sora 2</li>
</ul>
<p>Drop any of these into your <code>.cursor/skills/</code> directory and your AI agent can follow them. Adapt the design context block to your game's art style and you're good to go.</p>
extra text or taglines, watermark, deformed elements, modern or sci-fi.
No extra fingers, no merged limbs, no floating accessories.</code></pre>
<p>AI models have strong defaults — they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, say so explicitly.</p>
<h3>Layer 4: Reference images (consistency)</h3>
<p>When you need the same character from different angles, pass the first approved image as reference. Without it, every generation is independent — a different person every time. With it, every generation builds on the last approved output. This is how you get a roster of characters that look like they belong in the same game.</p>
<h2>The model lineup (and when to use each)</h2>
<p>Not every image needs the best model. A throwaway mockup doesn't justify $0.09. A final logo doesn't deserve $0.002.</p>
<p>| Model | Cost | Speed | Use for |</p>
<p>|&mdash;&mdash;-|&mdash;&mdash;|&mdash;&mdash;-|&mdash;&mdash;&mdash;|</p>
<p>| Flux 2 Pro | $0.03 | 15s | Final art, characters, environments |</p>
<p>| Flux Schnell | $0.003 | 3s | Exploration drafts, 20-variant grids |</p>
<p>| Ideogram v3 Quality | $0.09 | 12s | Anything with readable text |</p>
<p>| Recraft v3 | $0.04-0.08 | 10s | Logos, brand assets, SVG vectors |</p>
<p>| Seedream v4.5 | $0.04 | 8s | Photorealistic scenes |</p>
<p>| Flux Dev | $0.025 | 10s | LoRA fine-tuning base |</p>
<p>| Nano Banana Edit | $0.039 | 12s | Style transfer, material variants |</p>
<p>| BiRefNet | $0.001 | 3s | Background removal |</p>
<h3>The Schnell-to-Pro pipeline (never iterate on expensive models)</h3>
<p>Every generation session follows the same pattern:</p>
<p>1. <strong>Explore with Schnell</strong> ($0.003) — 10-20 variants, different angles, color palettes. Cost: $0.03-0.06</p>
<p>2. <strong>Pick 2-3 directions.</strong> Human looks at the grid, picks winners.</p>
<p>3. <strong>Refine with Flux 2 Pro</strong> ($0.03) — regenerate winners at full quality. Cost: $0.06-0.09</p>
<p>4. <strong>Post-process</strong> — BiRefNet for background removal ($0.001), Recraft for vector ($0.08)</p>
<p>A full session — blank canvas to final assets — costs under $0.20. Most of the creative work happens at $0.003/image. The expensive model just polishes a decision you already made.</p>
<h3>Typography: one model rules them all</h3>
<p>Every model except Ideogram fails at text. Flux gives you beautiful art with garbled letters. SDXL doesn't try. If your image has words in it, Ideogram v3 Quality is the only answer. We learned to accept the $0.09 cost rather than waste $0.30 on ten failed Flux attempts.</p>
<h3>Logo variants at scale</h3>
<p>Our game logo has 18 material variants — mahogany, mother-of-pearl, obsidian, molten lava, bronze with verdigris. Each generated with Nano Banana Edit ($0.039) + BiRefNet ($0.001) for transparency. Total: $0.72. A designer would quote hundreds of dollars and a week.</p>
<h2>The numbers (one month of generation)</h2>
<p>| Category | Images | Cost | Avg/Image |</p>
<p>|&mdash;&mdash;&mdash;-|&mdash;&mdash;&ndash;|&mdash;&mdash;|&mdash;&mdash;&mdash;&ndash;|</p>
<p>| Concept art (flux-2-pro) | 120 | $3.60 | $0.03 |</p>
<p>| Exploration (schnell) | 400 | $1.20 | $0.003 |</p>
<p>| Logo variants | 18 | $0.72 | $0.04 |</p>
<p>| Icons | 30 | $1.20 | $0.04 |</p>
<p>| Typography (ideogram) | 25 | $1.50 | $0.06 |</p>
<p>| Background removal | 80 | $0.08 | $0.001 |</p>
<p>| <strong>Total</strong> | <strong>673</strong> | <strong>$8.30</strong> | <strong>$0.012</strong> |</p>
<p>Six hundred images. Eight dollars.</p>
<h2>The pipeline: from prompt to in-game asset</h2>
<p>fal.ai is step one of a pipeline that goes from idea to walking character in about two hours:</p>
<pre><code>Brief → fal.ai (2D concept) → Tripo Studio (3D model) → Blender (decimate) → Godot (in-game)</code></pre>
<p>1. Designer describes the character</p>
<p>2. Generate 3 variants with Flux 2 Pro, score on 5 criteria (style match, cultural accuracy, silhouette, expression, animatability)</p>
<p>3. Generate front/side/three-quarter reference views using the winner</p>
<p>4. Tripo Studio image-to-3D (~1.5M faces, PBR textures)</p>
<p>5. Blender CLI decimates to 25k faces</p>
<p>6. Auto-rig, import into engine, done</p>
<p>Quality isn't AAA, but for an indie game with a stylized art style, it's more than good enough. Ten characters designed, total fal.ai spend: $6.</p>
<h2>What we learned</h2>
<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(&ndash;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>AI image generation isn't magic and it isn't free. But at a penny per image, with the right prompt structure, it eliminates the most expensive bottleneck in indie game development: the gap between "I know what this should look like" and "I have an image I can actually use."</p>
<p>We're building all of this as part of <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> &mdash; a game development platform that brings together git hosting, AI tools, and creative workflows for game teams.</p>
<p><em>Image generation is built into <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a>. We've open-sourced the <a href="../skills/image-generation.md" style="color: var(&ndash;c-accent-l);">prompt engineering skill</a> and <a href="../skills/concept-art-pipeline.md" style="color: var(&ndash;c-accent-l);">concept art pipeline skill</a>. We're building <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a> with these tools.</em></p>
</div>
@@ -191,38 +351,5 @@ sci-fi. No extra fingers, no merged limbs, no floating accessories.</code></pre>
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
+271 -140
View File
@@ -4,19 +4,19 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fork, Don't Build: The Age of Agents Doesn't Need New Tools — Tinqs Blog</title>
<meta name="description" content="Everyone is building new AI developer tools. We forked three existing ones --- Gitea, Pi, Godot --- and modified them from the inside. Here's why that's the better bet.">
<title>Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch — Tinqs Blog</title>
<meta name="description" content="Everyone is building new AI developer tools. We forked three battle-tested open-source projects — Gitea, Pi, and Godot and modified them from the inside. Combined changes: less than 0.5% of upstream code.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/fork-dont-build">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/fork-dont-build">
<meta property="og:title" content="Fork, Don't Build: The Age of Agents Doesn't Need New Tools">
<meta property="og:title" content="Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch">
<meta property="og:description" content="Fork Gitea. Fork Pi. Fork Godot. Modify platforms, don't build toys.">
<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="Fork, Don't Build: The Age of Agents Doesn't Need New Tools">
<meta name="twitter:title" content="Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch">
<meta name="twitter:description" content="Fork Gitea. Fork Pi. Fork Godot. Modify platforms, don't build toys.">
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
@@ -24,7 +24,7 @@
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Fork, Don't Build: The Age of Agents Doesn't Need New Tools",
"headline": "Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch",
"datePublished": "2026-05-25",
"author": {
"@type": "Person",
@@ -35,123 +35,287 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "Everyone is building new AI developer tools. We forked three existing ones --- Gitea, Pi, Godot --- and modified them from the inside. Here's why that's the better bet."
"description": "Everyone is building new AI developer tools. We forked three battle-tested open-source projects — Gitea, Pi, and Godot and modified them from the inside. Combined changes: less than 0.5% of upstream code."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">25 May 2026</span>
<h1 class="post__title">Fork, Don't Build: The Age of Agents Doesn't Need New Tools</h1>
<p class="post__lead">The AI developer tools space has a problem: everyone is building new things. New agents, new IDEs, new platforms, new wrappers around GPT. Meanwhile, the tools that actually run the world &mdash; git servers, game engines, CI runners &mdash; sit there unchanged, waiting for someone to open them up and let agents in. We chose to fork instead of build. Three times. Here's why.</p>
<h1 class="post__title">Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch</h1>
<p class="post__lead">The AI developer tools space is full of people building new things. New agents, new IDEs, new platforms, new wrappers. Meanwhile, the tools that actually run the world git servers, game engines, CI runners sit there unchanged, waiting for someone to open them up and let AI agents in.</p>
<div class="post__body">
<h2>The Pattern</h2>
<p>We're a four-person game studio. We don't have time to build a git platform, a coding agent, and a game engine from scratch. Nobody does. But we can take something that already works &mdash; something with years of battle-testing, thousands of contributors, and millions of users &mdash; and change it from the inside.</p>
<p>The pattern is simple:</p>
<p>1. Find an open-source tool that does 95% of what you need</p>
<p>2. Fork it</p>
<p>3. Add the 5% that makes it yours</p>
<p>4. Stay close to upstream so you get their fixes for free</p>
<p>We've done this three times.</p>
<h2>Fork 1: Gitea &mdash; Our Git Platform</h2>
<p><a href="https://gitea.com" style="color: var(&ndash;c-accent-l);">Gitea</a> is a self-hosted git server. Single Go binary, MIT license, 45k GitHub stars. It handles repos, issues, pull requests, CI, LFS &mdash; everything a team needs.</p>
<p>We <a href="https://tinqs.com/tinqs/studio" style="color: var(&ndash;c-accent-l);">forked it</a> and built Tinqs Studio. Our changes:</p>
<ul>
<li><strong>3D asset preview</strong> &mdash; click a <code>.glb</code> file and rotate the model in your browser</li>
<li><strong>HTML file preview</strong> &mdash; rendered in a sandboxed iframe, not raw source</li>
<li><strong>Agent API</strong> &mdash; six endpoints that let AI agents submit tasks, push code, and open PRs</li>
<li><strong>OAuth2 SSO</strong> &mdash; one login for git, the game, and every tool</li>
<li><strong>Credits system</strong> &mdash; monetize AI inference without hiding features behind paywalls</li>
</ul>
<p>Total lines changed from upstream: about 2,000 out of Gitea's 500,000. That's 0.4%. We modify templates, add Go modules, and tweak CSS variables. We never touch the database schema &mdash; we ride upstream's migrations. When Gitea releases 1.27, we rebase, fix conflicts, and ship.</p>
<p>The alternative was building a git platform from scratch. That's a multi-year, multi-million dollar project. Or using GitHub/GitLab and accepting their limitations. Neither option gives you the ability to embed AI agents directly into the platform.</p>
<h2>Fork 2: Pi &mdash; Our Agent Runtime</h2>
<p><a href="https://pi.dev" style="color: var(&ndash;c-accent-l);">Pi</a> is an open-source coding agent. 51k stars, MIT license, TypeScript. Four core tools (read, write, edit, bash), a minimal system prompt, and an extension system.</p>
<p>We <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">forked it</a> and added four extensions:</p>
<ul>
<li><strong>tinqs-provider</strong> &mdash; routes inference through our DeepSeek V4 proxy ($0.28/MTok vs Opus at $15/MTok)</li>
<li><strong>tinqs-tools</strong> &mdash; Gitea API, fal.ai image generation, vision preprocessing</li>
<li><strong>tinqs-ci</strong> &mdash; reads CI pipeline status and logs, polls for completion</li>
<li><strong>tinqs-guardrail</strong> &mdash; 29 safety patterns blocking dangerous commands</li>
</ul>
<p>Each extension is a single TypeScript file. No npm dependencies. The core Pi code is untouched &mdash; we only add files.</p>
<p>The alternative was building our own agent from scratch. That means writing tool-calling logic, context management, streaming, retry handling, conversation threading &mdash; months of work to reinvent what Pi already does. Or using Claude Code / Codex as a black box and accepting that you can't add a Gitea API tool or a budget cap.</p>
<h2>Fork 3: Godot &mdash; Our Game Engine</h2>
<p><a href="https://godotengine.org" style="color: var(&ndash;c-accent-l);">Godot</a> is an open-source game engine. We forked 4.6.2 and added nine C++ modules that turn the engine into an agent-aware runtime:</p>
<ul>
<li><strong>agent_api</strong> &mdash; HTTP server inside the engine, so agents can query game state</li>
<li><strong>agent_vision</strong> &mdash; screenshot capture for AI vision pipelines</li>
<li><strong>agent_console</strong> &mdash; programmatic access to the engine console</li>
<li><strong>agent_replay</strong> &mdash; record and replay game sessions for testing</li>
<li><strong>agent_analytics</strong> &mdash; PostHog event tracking from inside the engine</li>
</ul>
<p>These modules compile into the engine binary. A vanilla Godot user never sees them. An agent can connect to the running engine over HTTP, take a screenshot, read the scene tree, execute a console command, and capture the result &mdash; all without touching the editor UI.</p>
<p>The alternative was building an engine integration from scratch. Or worse, building a custom engine. We'd still be writing a renderer instead of making a game.</p>
<h2>Why Forking Beats Building</h2>
<h3>You inherit decades of work</h3>
<p>Gitea has handled millions of git pushes. Godot renders millions of frames. Pi has processed millions of LLM tokens. That battle-testing is free when you fork. When you build from scratch, you spend your first year rediscovering bugs that were fixed upstream in 2019.</p>
<h3>You get free maintenance</h3>
<p>Every upstream release brings security patches, performance improvements, and new features &mdash; written by hundreds of contributors we don't pay. Our job is to rebase, resolve conflicts, and test. That's an afternoon, not a quarter.</p>
<h3>You stay focused</h3>
<p>Building a git server from scratch means worrying about pack-file format, SSH key management, webhook delivery, and a thousand other things that have nothing to do with AI agents. Forking means you only think about the 5% that matters to you. The other 95% is someone else's problem.</p>
<h3>Agents work better on real platforms</h3>
<p>An agent that pushes to a real Gitea instance &mdash; with real CI, real code review, real permissions &mdash; produces work that humans can actually review and ship. An agent that pushes to a toy demo platform produces demos.</p>
<p>The whole point of AI agents is to participate in real workflows. Real workflows run on real tools. If you want agents in your git workflow, put them in your git server. If you want agents in your game pipeline, put them in your game engine.</p>
<p>We forked three of them. Gitea for git hosting. Pi for coding agents. Godot for the game engine. Combined changes: less than 0.5% of upstream code. Here's why, how, and what we learned.</p>
<h2>The 0.5% Rule</h2>
<p>Across all three forks, our total changeset is less than 0.5% of the upstream code. Tinqs Studio: 0.4% of Gitea. Pi extensions: 900 lines added to a 15,000-line codebase. Godot modules: 2,000 lines added to a 2-million-line engine.</p>
<p>This isn't a coincidence. If your fork touches more than 1% of upstream, you're doing too much. Either the upstream tool is wrong for the job, or you're not trusting it enough. The power of forking is that you don't have to understand the whole codebase. You find the extension points, add your code, and leave the rest alone.</p>
<h2>What We're Not Doing</h2>
<p>We're not building a new IDE. Cursor and Claude Code exist. We're not building a new LLM. DeepSeek and Claude exist. We're not building a new cloud platform. AWS exists.</p>
<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 &mdash; not a replacement for either.</p>
<h2>The Bet</h2>
<p>The age of agents doesn't need more agents. It needs better platforms. Platforms that understand agents as first-class users &mdash; with API endpoints, safety rails, and lifecycle management. Those platforms already exist as open-source projects. They just need someone to fork them and add the wiring.</p>
<p>That's the bet. Fork, don't build. Modify the foundation, don't stack another layer on top. Let the upstream community handle the 99.5% while you focus on the 0.5% that makes it yours.</p>
<p>We're four people. We can't build a git platform, a coding agent, or a game engine from scratch. Nobody can — not in a timeframe measured in months.</p>
<p>But we can take something that already works — something with years of battle-testing and thousands of contributors — and change the last half-percent that makes it ours. The pattern:</p>
<p>1. Find open-source tool that does 95% of what you need</p>
<p>2. Fork it</p>
<p>3. Add the 5% (really, 0.5%)</p>
<p>4. Stay close to upstream so their fixes are your fixes</p>
<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(&ndash;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>
<p><strong>LFS-first workflows.</strong> Auto-tracking on repo creation. Game file extensions (<code>.fbx</code>, <code>.glb</code>, <code>.png</code>, <code>.wav</code>) tracked by default. Storage dashboard per repo. Clone times went from 45 minutes to 3 minutes.</p>
<p><strong>OAuth2 SSO.</strong> One login for git, the game tools, and the team dashboard.</p>
<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(&ndash;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>
<li><strong>tinqs-tools</strong> — Gitea REST API, fal.ai image generation, vision model access</li>
<li><strong>tinqs-ci</strong> — reads CI pipeline status, fetches build logs, polls for completion</li>
<li><strong>tinqs-guardrail</strong> — 29 safety patterns blocking dangerous commands</li>
</ul>
<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(&ndash;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>
<li><strong>agent_console</strong> — programmatic console access</li>
<li><strong>agent_replay</strong> — record and replay game sessions for testing</li>
</ul>
<p>These compile into the engine binary. A vanilla Godot user never sees them. An agent connects over HTTP, takes a screenshot, reads the scene tree, executes a console command — all without touching the editor UI.</p>
<p>2,000 lines added to a 2-million-line engine. The alternative: building our own engine, or worse, writing a renderer instead of making a game.</p>
<h2>Why forking beats building</h2>
<p><strong>You inherit decades of work, for free.</strong> Gitea has handled millions of git pushes. Godot renders millions of frames. Pi has processed millions of LLM tokens. That battle-testing is yours when you fork. When you build from scratch, year one is spent rediscovering bugs fixed upstream in 2019.</p>
<p><strong>You get free maintenance.</strong> Every upstream release brings security patches, performance improvements, and new features — written by hundreds of contributors you don't pay. Your job is to rebase, resolve conflicts, and test. An afternoon, not a quarter.</p>
<p><strong>You stay focused.</strong> Building a git server means worrying about pack-file format, SSH key management, webhook delivery. Forking means you only think about the 0.5% that matters to you. The other 99.5% is someone else's problem.</p>
<p><strong>Agents work better on real platforms.</strong> An agent pushing to a real Gitea instance — with real CI, real code review, real permissions — produces work humans can actually review and ship. An agent pushing to a toy demo platform produces demos.</p>
<h2>What we're not building</h2>
<p>We're not building a new IDE (Cursor and Claude Code exist). Not a new LLM (DeepSeek and Claude exist). Not a new cloud platform (AWS exists).</p>
<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(&ndash;c-accent-l);">Tinqs Studio</a> is our Gitea fork, open for game teams and indie studios. We're building <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a> &mdash; a survival colony sim &mdash; using every tool described in this post. If you're interested in self-hosted game development with built-in AI agents, come take a look.</em></p>
<p><em><a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> is our Gitea fork, open for game teams. <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a> is the game we're building with every tool described here.</em></p>
</div>
@@ -164,38 +328,5 @@
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
-177
View File
@@ -1,177 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Why We Forked Gitea and Built Tinqs Studio — Tinqs Blog</title>
<meta name="description" content="Game studios need git hosting that understands large files, 3D assets, and team workflows. We forked Gitea and built Tinqs Studio --- here's why and how.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/forking-gitea">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/forking-gitea">
<meta property="og:title" content="Why We Forked Gitea and Built Tinqs Studio">
<meta property="og:description" content="Game studios need git that understands LFS, 3D previews, and team workflows. We built Tinqs Studio.">
<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="Why We Forked Gitea and Built Tinqs Studio">
<meta name="twitter:description" content="Game studios need git that understands LFS, 3D previews, and team workflows. We built Tinqs Studio.">
<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": "Why We Forked Gitea and Built Tinqs Studio",
"datePublished": "2026-05-20",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "Game studios need git hosting that understands large files, 3D assets, and team workflows. We forked Gitea and built Tinqs Studio --- here's why and how."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">20 May 2026</span>
<h1 class="post__title">Why We Forked Gitea and Built Tinqs Studio</h1>
<p class="post__lead">GitHub is built for web developers. Game studios need something different &mdash; LFS that works, 3D asset previews in the browser, and project management that understands sprints and milestones. So we forked Gitea and built Tinqs Studio.</p>
<div class="post__body">
<h2>The Problem with GitHub for Game Dev</h2>
<p>We used GitHub for two years. It was fine for docs &mdash; small files, text diffs, pull requests. But the game repo was a different story.</p>
<p>A single character model with textures and animations is 50&ndash;200MB. A terrain heightmap is 16MB. An island's vegetation data is another 10MB. Our game repo was 12GB in LFS alone, growing every week. GitHub's LFS bandwidth limits, slow clone times, and $5/50GB pricing made it untenable.</p>
<p>More importantly, nobody on the team could <strong>see</strong> what changed. A PR that modifies a GLB file shows a binary diff. You can't preview it. You can't compare before and after. The artist pushes a model, the developer approves it blindly, and three days later someone notices the normals are inverted.</p>
<h2>Why Self-Host, and Why Gitea</h2>
<p>We evaluated GitLab, Forgejo, Gogs, and Gitea. The decision came down to:</p>
<ul>
<li><strong>Single binary.</strong> Gitea compiles to one Go binary with SQLite support. No PostgreSQL, no Redis, no Docker compose with 7 services. Just copy the binary, write an app.ini, and run it.</li>
<li><strong>Resource usage.</strong> Our instance runs on a single EC2 instance alongside other services. It uses about 200MB RAM. GitLab needs 4GB minimum.</li>
<li><strong>LFS built-in.</strong> Gitea includes a full LFS server. No external LFS store, no S3 configuration for basic use. Files are stored locally. We added S3 backend later, but it works out of the box.</li>
<li><strong>Forkable.</strong> Gitea is MIT-licensed, written in Go, with a clean codebase. We can modify it without worrying about license restrictions or CLA headaches.</li>
</ul>
<p>We ran vanilla Gitea for six months. It solved the cost and bandwidth problems immediately. But the UX gaps for game development were still there.</p>
<h2>What We Built: Tinqs Studio</h2>
<p><a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> is our fork. It tracks upstream Gitea on the <code>main</code> branch and keeps all customisations on a separate branch. We rebase onto upstream releases periodically, fix conflicts, and deploy.</p>
<h3>3D Asset Preview</h3>
<p>The headline feature. When you open a PR that contains a <code>.glb</code>, <code>.gltf</code>, or <code>.fbx</code> file, you see a 3D viewer directly in the browser. Rotate, zoom, check materials. No downloads, no external tools. We integrated Online 3D Viewer (O3DV), which supports 22 file formats including STL, OBJ, 3DS, and PLY.</p>
<p>This changes the review process fundamentally. The artist pushes a model, the lead rotates it in the browser, leaves a comment about the UV seam on the shoulder, and the artist fixes it &mdash; all without leaving the git platform.</p>
<h3>LFS-First Workflows</h3>
<p>Vanilla Gitea treats LFS as an afterthought. You configure <code>.gitattributes</code> manually. There's no dashboard showing LFS usage, no way to see which files are tracked, no warnings when someone commits a large file without LFS.</p>
<p>Tinqs Studio adds auto-LFS tracking on repository creation. Game file extensions (<code>.fbx</code>, <code>.glb</code>, <code>.png</code>, <code>.wav</code>, <code>.ogg</code>, <code>.tscn</code>, <code>.tres</code>) are tracked by default. An API endpoint exposes LFS storage stats per repo. The goal: LFS should be invisible. It should just work.</p>
<h3>Platform Integration</h3>
<p>Tinqs Studio integrates project management &mdash; issues, sprints, time tracking &mdash; and OAuth2 SSO. One login for git, the game tools, and the team dashboard.</p>
<h2>The Branching Strategy</h2>
<p>Staying close to upstream is critical. We don't want to maintain a fork that diverges forever:</p>
<ul>
<li><code>main</code> tracks upstream <code>go-gitea/gitea</code>. We never commit to it directly.</li>
<li>Our production branch holds all customisations.</li>
<li>Feature branches merge into production.</li>
<li>When upstream releases a new version, we merge, resolve conflicts, test, deploy.</li>
</ul>
<p>We deliberately limit what we touch. We modify templates, locale strings, CSS variables, and a handful of Go packages. We <strong>never</strong> touch the database models &mdash; schema is owned by upstream, and we ride their migrations. This keeps rebasing manageable.</p>
<h2>What We Learned</h2>
<p><strong>Self-hosting git is surprisingly easy.</strong> The hard part isn't running Gitea &mdash; it's convincing yourself that you're allowed to. After years of GitHub being the default, it feels transgressive to host your own git. But a single Go binary on a $10/month server handles a small team with room to spare.</p>
<p><strong>LFS changes everything for game repos.</strong> Our clone times went from 45 minutes to 3 minutes. Developers only download the LFS objects they need. CI only pulls what changed. The bandwidth savings alone paid for the server.</p>
<p><strong>Forking is maintenance, not rebellion.</strong> The romantic version is "we forked Gitea and built our own platform." The reality is we changed 200 lines of Go, 50 template strings, and a CSS file. 99.5% of the code is upstream's. We're just customising the last half-percent for our use case.</p>
<p><strong>3D preview is a game changer.</strong> We expected it to be a nice-to-have. It turned out to be the feature that made the rest of the team actually use git. When the artist can see their work rendered in the browser, they stop asking the developer to "check if it looks right."</p>
<hr>
<p><a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> is built for game teams that are tired of paying GitHub for LFS bandwidth and reviewing binary diffs blind. We're building it for ourselves first &mdash; dogfooding it on our own game &mdash; but the plan is to make it available as a platform for other studios. If you're a game team that self-hosts or wants to, we'd love to hear what features you need.</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>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
+264 -131
View File
@@ -5,19 +5,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Streaming a 12km Archipelago in Godot 4 — Tinqs Blog</title>
<meta name="description" content="How we built four streaming layers, async resource loading, and memory-safe caches to run a 12km open world in Godot 4 with C#.">
<meta name="description" content="Godot 4 has no built-in asset streaming. We built four layers — terrain regions, vegetation chunks, async loading, and entity rendering — to run a 12km open world with 9 islands, 155 vegetation types, and 2,000 crowd instances.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/godot-optimisation">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/godot-optimisation">
<meta property="og:title" content="Streaming a 12km Archipelago in Godot 4">
<meta property="og:description" content="Four streaming layers, async loading, and zero memory leaks --- optimising Godot for a large open world.">
<meta property="og:description" content="Four streaming layers, async loading, and zero memory leaks — running a 12km open world in Godot 4.">
<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="Streaming a 12km Archipelago in Godot 4">
<meta name="twitter:description" content="Four streaming layers, async loading, and zero memory leaks --- optimising Godot for a large open world.">
<meta name="twitter:description" content="Four streaming layers, async loading, and zero memory leaks — running a 12km open world in Godot 4.">
<meta name="twitter:image" content="https://www.tinqs.com/img/og-cover.jpg">
<script type="application/ld+json">
@@ -35,125 +35,291 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "How we built four streaming layers, async resource loading, and memory-safe caches to run a 12km open world in Godot 4 with C#."
"description": "Godot 4 has no built-in asset streaming. We built four layers — terrain regions, vegetation chunks, async loading, and entity rendering — to run a 12km open world with 9 islands, 155 vegetation types, and 2,000 crowd instances."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">22 May 2026</span>
<h1 class="post__title">Streaming a 12km Archipelago in Godot 4</h1>
<p class="post__lead">Godot has no built-in asset streaming. Our game is a 12km x 12km archipelago with 9 islands, thousands of trees, hundreds of buildings, and an ocean that never ends. Here's how we made it run.</p>
<p class="post__lead">Godot 4 has no terrain streaming, no asset LOD pipeline, and no distance-based loading. Our game is a 12km × 12km archipelago with 9 islands, 155 vegetation prototypes, and 2,000 simulated colonists. If you load everything at startup, you run out of VRAM before the player sees the main menu.</p>
<div class="post__body">
<h2>The Problem</h2>
<p>We're building a survival colony sim set across 9 islands. The total world is roughly 12km x 12km. Each island is 4km across with its own terrain heightmap, biome textures, vegetation prototypes, and building grids. The player can travel between islands by canoe.</p>
<p>Godot 4 is a fantastic engine, but it wasn't designed for this scale. There's no terrain streaming, no asset LOD pipeline, no distance-based loading. If you load everything at startup, you run out of VRAM before the player sees the main menu. So we built four streaming layers on top of Godot, all in C#.</p>
<h2>Layer 1: Terrain Regions</h2>
<p>We use <strong>Terrain3D</strong> for heightmaps &mdash; a GDExtension that gives us a clipmap renderer with 7 LOD levels. Internally, Terrain3D divides each island into 512m x 512m regions. A 4km island has 64 regions. Across 9 islands, that's 576 regions total.</p>
<p>The key insight: <strong>don't create all 9 terrain nodes at startup.</strong> Each node allocates a clipmap mesh, collision structures, and materials even when hidden. Our original code created all 9 in <code>_Ready()</code> and just toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited yet.</p>
<p>The fix was lazy instantiation. We create the current island's terrain on startup and defer the rest. When the player gets in a canoe and sails to a new island, we create that island's terrain node on demand, import the heightmap, and start async texture loading &mdash; all while a loading screen covers the transition.</p>
<h2>Layer 2: Vegetation Chunks (128m Grid)</h2>
<p>This is the main prop streaming system. Every island's vegetation &mdash; trees, rocks, grasses, shrubs &mdash; is divided into a spatial grid of 128m x 128m chunks.</p>
<p>The camera position is checked every 0.5 seconds. When it crosses a chunk boundary, we calculate which chunks should be active within a 400m radius (roughly 39 chunks in a circle), <code>QueueFree</code> chunks that fell out of range, and build new chunks that entered range.</p>
<p>Each chunk groups vegetation instances by prototype, creates a <strong>MultiMesh</strong> per group, and places instances using height queries. A chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls, not 80 individual nodes.</p>
<h3>The cache problem</h3>
<p>Vegetation meshes and materials are cached in dictionaries keyed by prototype name. The problem: these caches are <strong>append-only</strong>. Visit all 9 islands and you accumulate every mesh and material variant permanently. With 155 unique prototypes across the archipelago, that's a lot of GPU memory that never gets freed.</p>
<p>The fix is island-scoped eviction. When the player leaves an island, we clear the vegetation caches. Meshes and materials for the departed island are released. If the player returns, they reload from disk. The loading screen covers this cost.</p>
<h2>Layer 3: Async Resource Loading</h2>
<p>Godot's <code>GD.Load()</code> is synchronous. It blocks the main thread. During gameplay, the frame freezes. We audited the entire codebase and found <strong>26 resource load calls across 13 files</strong>, and only 1 was async.</p>
<p>The worst offender was <code>GetMeshForProto()</code> in the vegetation grid. As the player walks across an island for the first time, every new vegetation prototype triggers a synchronous load. With 155 prototypes, the first traversal stutters visibly.</p>
<p>We fixed this in two ways:</p>
<p>Here's how we built four streaming layers on top of Godot, all in C#, to make it work.</p>
<h2>The scale problem</h2>
<p>Each island is roughly 4km across with its own terrain heightmap, biome textures, vegetation, and building grids. The player travels between islands by canoe. At any given moment, only a small fraction of the world is visible — but Godot doesn't know that unless you tell it.</p>
<p>We built four layers that teach Godot what to load, when to load it, and when to let it go.</p>
<h2>Layer 1: Terrain regions (lazy instantiation)</h2>
<p>We use <strong>Terrain3D</strong> for heightmaps — a GDExtension that provides clipmap rendering with 7 LOD levels. Each island is split into 512m × 512m regions. A 4km island has 64 regions. Nine islands: 576 regions total.</p>
<p>The original code created all 9 terrain nodes in <code>_Ready()</code> and toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited. The fix: create the current island's terrain on startup, defer the rest. When the player sails to a new island, create that island's terrain node on demand, import the heightmap, start async texture loading — all behind a loading screen.</p>
<h2>Layer 2: Vegetation chunks (128m grid)</h2>
<p>The main prop streaming system. Every island's vegetation is divided into a spatial grid of 128m × 128m chunks.</p>
<p>The camera position is checked every 0.5 seconds. When it crosses a chunk boundary, we calculate which chunks should be active within a 400m radius (~39 chunks), destroy chunks that fell out of range, and build new ones that entered. Each chunk groups vegetation by prototype, creates a <strong>MultiMesh</strong> per group, and places instances using height queries. A chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls — not 80 individual nodes.</p>
<p>The cache problem: vegetation meshes and materials are cached in dictionaries keyed by prototype name. These caches are append-only by default — visit all 9 islands and you accumulate every mesh variant permanently. The fix is island-scoped eviction. When the player leaves an island, we clear vegetation caches. They reload from disk on return, behind a loading screen.</p>
<h2>Layer 3: Async resource loading</h2>
<p>Godot's <code>GD.Load()</code> is synchronous. It blocks the main thread. During gameplay, the frame freezes.</p>
<p>We audited the entire codebase and found <strong>26 resource load calls across 13 files</strong> — only 1 was async. The worst offender was <code>GetMeshForProto()</code> in the vegetation grid. As the player walks across a new island, every new vegetation prototype triggers a synchronous load. With 155 prototypes, the first traversal stutters visibly.</p>
<p>Two fixes:</p>
<ul>
<li><strong>Pre-warm during loading screens.</strong> When an island is imported, we kick off background loads for all known prototypes. By the time the player gains control, most meshes are already cached.</li>
<li><strong>Async loading for biome textures.</strong> Terrain textures use <code>ResourceLoader.LoadThreadedRequest()</code> with <code>_Process()</code> polling. The terrain renders immediately with autoshader colours, and biome textures pop in when ready. The player never notices.</li>
<li><strong>Pre-warm during loading screens.</strong> When an island is imported, kick off background loads for all known prototypes. By the time the player gains control, most meshes are cached.</li>
<li><strong>Async texture loading.</strong> Terrain textures use <code>ResourceLoader.LoadThreadedRequest()</code> with <code>_Process()</code> polling. The terrain renders immediately with autoshader colors; biome textures pop in when ready.</li>
</ul>
<h3>The ResourceLoader cache trap</h3>
<p>On top of our own caches, Godot maintains an internal resource cache. Every <code>GD.Load()</code> call caches the result globally. There's no API to query the cache size or evict entries.</p>
<p>If you load an FBX as a <code>PackedScene</code>, instantiate it to extract a mesh, then free the instance &mdash; the PackedScene <strong>stays cached</strong>. The mesh you extracted is fine (it's a Resource, not a Node), but the discarded scene wastes memory forever.</p>
<p>The rule: use <code>ResourceLoader.Load(path, "", CacheMode.Ignore)</code> for one-shot loads where you extract data and discard the container. Use <code>GD.Load()</code> only for things that should persist (shaders, shared textures).</p>
<h2>Layer 4: Entity Rendering</h2>
<p>Dynamic entities &mdash; colonists, animals, buildings, VFX &mdash; are event-driven, not streamed. They update when the simulation pushes new state, not per frame.</p>
<p>The ResourceLoader trap: Godot maintains an internal resource cache. Every <code>GD.Load()</code> caches the result globally. If you load an FBX as a <code>PackedScene</code>, instantiate it to extract a mesh, then free the instance — the PackedScene <strong>stays cached</strong>. Rule: use <code>ResourceLoader.Load(path, "", CacheMode.Ignore)</code> for one-shot loads where you extract data and discard the container.</p>
<h2>Layer 4: Entity rendering (event-driven)</h2>
<p>Dynamic entities — colonists, animals, buildings, VFX — update when the simulation pushes new state, not per frame.</p>
<ul>
<li><strong>Crowd rendering:</strong> Single MultiMesh for up to 2000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20. No individual nodes, no per-frame allocation.</li>
<li><strong>Animals:</strong> One MultiMesh per type. Max 500 per type. Updates only on state change, not per frame.</li>
<li><strong>Buildings:</strong> Tracked by ID from sim state. <code>QueueFree</code> when removed. Self-cleaning.</li>
<li><strong>Crowd rendering:</strong> Single MultiMesh for up to 2,000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20.</li>
<li><strong>Animals:</strong> One MultiMesh per type. Max 500 per type. Updates only on state change.</li>
<li><strong>Buildings:</strong> Tracked by ID from sim state. <code>QueueFree</code> when removed.</li>
<li><strong>VFX:</strong> Capped at 50 active particle systems. Worst case: 10,000 GPU particles.</li>
</ul>
<h2>Memory Safety: Zero Leaks</h2>
<p>We audited every <code>QueueFree()</code> call in the codebase &mdash; 47 calls across 17 files. <strong>Zero <code>RemoveChild()</code> calls without a corresponding <code>QueueFree()</code>.</strong> Three patterns we follow everywhere:</p>
<p><strong>Pattern 1: Chunk streaming</strong> &mdash; Deactivate out-of-range chunks by iterating the active dict, calling <code>QueueFree()</code>, collecting keys to remove, then removing them after iteration. Never modify a dictionary while iterating it.</p>
<p><strong>Pattern 2: Extract data from PackedScene</strong> &mdash; Instantiate a scene, extract the mesh, <code>QueueFree()</code> the temporary instance. The mesh survives because it's a Resource, not a Node.</p>
<p><strong>Pattern 3: UI rebuild</strong> &mdash; <code>QueueFree()</code> all children, then build new content. Safe because <code>QueueFree</code> is deferred &mdash; new children are added in the same frame before old ones are freed.</p>
<h2>What Runs Every Frame</h2>
<p>We're strict about what goes in <code>_Process()</code>:</p>
<h2>Memory safety: the QueueFree audit</h2>
<p>We audited every <code>QueueFree()</code> call 47 calls across 17 files. <strong>Zero <code>RemoveChild()</code> calls without a corresponding <code>QueueFree()</code>.</strong> Three patterns we follow everywhere:</p>
<p>1. <strong>Chunk streaming:</strong> Iterate active dict, call <code>QueueFree()</code>, collect keys to remove, then remove after iteration. Never modify a dictionary while iterating.</p>
<p>2. <strong>Extract from PackedScene:</strong> Instantiate, extract mesh, <code>QueueFree()</code> the temp instance. The mesh survives because it's a Resource, not a Node.</p>
<p>3. <strong>UI rebuild:</strong> <code>QueueFree()</code> all children, build new content. Safe because <code>QueueFree</code> is deferred new children added in same frame before old ones freed.</p>
<h2>What runs every frame (and what doesn't)</h2>
<p><code>_Process()</code> is strictly limited:</p>
<ul>
<li><strong>Vegetation grid:</strong> Camera chunk check (0.5s throttle, early-exits if same chunk)</li>
<li><strong>Terrain manager:</strong> Poll async texture loads (loop pending list, check status)</li>
<li><strong>Crowd renderer:</strong> Lerp 2000 colonist positions (math-only, pre-allocated arrays)</li>
<li><strong>Day/night:</strong> Rotate sun, adjust light energy</li>
<li><strong>Camera:</strong> Follow + zoom smoothing</li>
<li><strong>Sim bridge:</strong> Drain WebSocket message queue</li>
<li>Vegetation grid: camera chunk check (0.5s throttle, early-exit if same chunk)</li>
<li>Terrain manager: poll async texture loads</li>
<li>Crowd renderer: lerp 2,000 positions (math-only, pre-allocated arrays)</li>
<li>Day/night: rotate sun</li>
<li>Camera: follow + zoom</li>
<li>Sim bridge: drain WebSocket message queue</li>
</ul>
<p>No heap allocation in any of these. Total per-frame overhead is dominated by the crowd lerp and the message queue drain.</p>
<h2>Shaders We Watch</h2>
<p>Two custom shaders are performance-sensitive:</p>
<p><strong>Ocean shader</strong> &mdash; 4 Gerstner wave calculations in the vertex stage, applied to a 12,000m plane. Fragment stage does depth reconstruction, caustics, foam masking, and two normal map lookups. It's the heaviest thing in the render pipeline. We pre-warm it during the loading screen to avoid shader compilation stutter.</p>
<p><strong>Wind sway shader</strong> &mdash; 6 trig ops per vertex on every vegetation mesh within 400m. The sway is invisible beyond 100m but the shader runs at full cost regardless. Future optimisation: disable sway on distant chunks.</p>
<h2>The Target: RTX 3060</h2>
<p>Our early access target is an RTX 3060 with 8GB VRAM:</p>
<p><strong>No heap allocation in any of these.</strong> Per-frame overhead is dominated by the crowd lerp and message queue drain.</p>
<p>Two shaders to watch: the ocean shader (4 Gerstner waves, depth reconstruction, caustics, foam — heaviest thing in the pipeline) and the wind sway shader (6 trig ops per vertex on every vegetation mesh within 400m). Future optimization: disable sway on distant chunks.</p>
<h2>Target: RTX 3060, 8GB VRAM</h2>
<ul>
<li>Main island + full vegetation < 4GB VRAM &mdash; ship it, we have headroom</li>
<li>Approaching 6&ndash;8GB &mdash; implement lazy terrain nodes + cache eviction</li>
<li>Exceeding 8GB &mdash; implement vegetation LOD and region-level streaming</li>
<li>Main island + full vegetation < 4GB VRAM ship it</li>
<li>Approaching 6-8GB → implement lazy terrain nodes + cache eviction</li>
<li>Exceeding 8GB implement vegetation LOD and region-level streaming</li>
</ul>
<p><strong>Always measure before optimising.</strong> We added VRAM logging before writing a single line of optimisation code. Half the "problems" we expected turned out to be non-issues. The other half were worse than expected. Profiling isn't optional.</p>
<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 your resource loading, and be disciplined about what runs per frame. The engine gives you the primitives &mdash; MultiMesh, <code>LoadThreadedRequest</code>, <code>QueueFree</code> &mdash; and it's up to you to wire them into a system that scales.</p>
<p>We're building with these systems and developing the game using <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a>. If you're building something large-scale in Godot, we hope this is useful.</p>
<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(&ndash;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(&ndash;c-accent-l);">Tinqs Studio</a>.</em></p>
</div>
@@ -166,38 +332,5 @@
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
-199
View File
@@ -1,199 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Generation at Every Price Point with fal.ai — Tinqs Blog</title>
<meta name="description" content="We generate concept art, logos, icons, and trailer frames through a single API proxy. Here's how we pick between 12 models spanning $0.002 to $0.09 per image.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/image-generation-fal">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/image-generation-fal">
<meta property="og:title" content="Image Generation at Every Price Point with fal.ai">
<meta property="og:description" content="One proxy, 12 models, $0.002 to $0.09 per image. How we pick.">
<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="Image Generation at Every Price Point with fal.ai">
<meta name="twitter:description" content="One proxy, 12 models, $0.002 to $0.09 per image. How we pick.">
<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": "Image Generation at Every Price Point with fal.ai",
"datePublished": "2026-05-25",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "We generate concept art, logos, icons, and trailer frames through a single API proxy. Here's how we pick between 12 models spanning $0.002 to $0.09 per image."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">25 May 2026</span>
<h1 class="post__title">Image Generation at Every Price Point with fal.ai</h1>
<p class="post__lead">We generate every visual asset for Ariki &mdash; concept art, app icons, trailer frames, logo variants, Steam capsules &mdash; through a single inference proxy that routes to fal.ai. No Photoshop. No Midjourney subscription. Just API calls at prices that range from $0.002 to $0.09 per image. Here's how we decide which model gets which job.</p>
<div class="post__body">
<h2>The Setup</h2>
<p>Our <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> platform includes an inference proxy that sits between agents and model providers. When an agent (or a human in Cursor) says "generate an image," the proxy routes the request to fal.ai, handles authentication, tracks usage per user, and persists the result to S3. The caller doesn't care which model runs &mdash; they describe what they want, and the proxy picks or the caller specifies.</p>
<pre><code>Agent describes what it wants
→ tinqsProxy receives generate_image call
→ Routes to fal.ai with the specified model
→ Image generated, persisted to S3
→ Permanent URL returned to caller</code></pre>
<p>One API key. One billing account. Access to every model fal.ai hosts. That's the pitch of aggregator platforms, and fal.ai delivers on it.</p>
<h2>The Tiers</h2>
<p>Not every image needs the best model. A throwaway mockup doesn't justify $0.09. A final logo doesn't deserve $0.002. We split our usage into four tiers.</p>
<h3>Best Quality &mdash; Final Art</h3>
<p>For images that ship &mdash; hero art, app icons, trailer keyframes, print-ready designs &mdash; we use three models depending on the content:</p>
<p><strong>Flux 2 Pro</strong> ($0.03/megapixel, ~15 seconds). Our default. Best all-round quality for concept art, character illustrations, environment paintings, and anything that doesn't need text. Handles complex prompts with multiple elements well. Rarely fails.</p>
<p><strong>Ideogram v3 Quality</strong> ($0.09, ~12 seconds). The only model that renders text reliably inside images. When we need a poster with a tagline, a sign in a game scene, or a logo with readable letters, this is the only option. The QUALITY tier is expensive but worth it &mdash; text at lower tiers gets blurry.</p>
<p><strong>Recraft v3</strong> ($0.04 raster, $0.08 vector, ~10 seconds). Built for commercial design. Clean lines, consistent style, and the only model on fal.ai that outputs SVG vectors. When we need brand assets, packaging mockups, or anything that might end up in print, Recraft produces work that doesn't need cleanup.</p>
<h3>Mid Tier &mdash; Everyday Work</h3>
<p>For images that are good enough for internal review, social posts, or documentation:</p>
<p><strong>Ideogram v3 Balanced</strong> ($0.06, ~8 seconds). Typography quality between Turbo and Quality. Good for marketing materials where text matters but perfection doesn't.</p>
<p><strong>Seedream v4.5</strong> ($0.04, ~8 seconds). Google's model on fal.ai. Photorealistic scenes and product shots. Different aesthetic from Flux &mdash; slightly more photographic, less painterly.</p>
<p><strong>Flux Dev</strong> ($0.025, ~10 seconds). The open-weight Flux variant. Good quality, and the base for LoRA fine-tuning if you want to train on your own style. We use it when we need custom-trained models later.</p>
<h3>Low Cost &mdash; Drafts and Exploration</h3>
<p>For iteration, A/B testing, and throwing things at the wall:</p>
<p><strong>Flux Schnell</strong> ($0.003/megapixel, ~3 seconds). The workhorse for exploration. When we're figuring out composition, trying different camera angles, or generating 20 variants to pick one direction &mdash; Schnell. A hundred images costs $0.30. You can afford to be wasteful.</p>
<p><strong>SDXL Lightning</strong> (~$0.002, ~2 seconds). The absolute cheapest option. Lower quality than Schnell, but when you need 50 thumbnails to test a layout grid or generate placeholder textures, quality doesn't matter. Two cents for ten images.</p>
<h3>Specialised &mdash; Editing and Post-Processing</h3>
<p>For modifying existing images rather than generating new ones:</p>
<p><strong>Flux Kontext</strong> (~$0.04, ~12 seconds). Context-aware editing. Give it an image and say "change the wood to marble" or "make it sunset lighting." Preserves composition while changing style or material. Useful for quick style transfers without regenerating from scratch.</p>
<p><strong>Nano Banana Edit</strong> ($0.039, ~12 seconds). Image-to-image restyle. We use this for our logo variant pipeline &mdash; take one carved-wood Ariki logo and produce versions in mahogany, pearl, obsidian, coral, gold. It's better than Kontext at preserving fine detail in complex images.</p>
<p><strong>BiRefNet</strong> ($0.001, ~3 seconds). Background removal. Produces clean alpha cutouts from any image. We pair it with every logo and icon generation &mdash; generate with a white background, then cut it out. A dollar gets you a thousand cutouts.</p>
<h2>How We Actually Use Them</h2>
<h3>The Schnell-to-Pro Pipeline</h3>
<p>We never start with the expensive model. Every generation session follows the same pattern:</p>
<p>1. <strong>Explore with Schnell</strong> ($0.003) &mdash; 10-20 variants, different angles, compositions, color palettes. Total: $0.03-0.06.</p>
<p>2. <strong>Pick 2-3 directions.</strong> Human looks at the grid, picks the promising ones.</p>
<p>3. <strong>Refine with Flux 2 Pro</strong> ($0.03) &mdash; regenerate the winners at full quality with refined prompts. Total: $0.06-0.09.</p>
<p>4. <strong>Post-process</strong> &mdash; BiRefNet for background removal ($0.001), maybe Recraft for a vector version ($0.08).</p>
<p>A full session &mdash; from blank canvas to final assets &mdash; costs under $0.20. That's the price of a single Midjourney generation on their Pro plan.</p>
<h3>Logo Variants at Scale</h3>
<p>Our Ariki logo has 18 material variants &mdash; deep mahogany, mother-of-pearl, obsidian, molten lava, bronze with verdigris, tapa cloth, and more. Each one generated with Nano Banana Edit ($0.039) + BiRefNet ($0.001) for transparency. Total cost for 18 variants: <strong>$0.72</strong>. A designer would quote hundreds of dollars and a week of work for the same output.</p>
<h3>Typography That Works</h3>
<p>Every model except Ideogram fails at text. Flux will give you beautiful art with garbled letters. Recraft gets close but isn't consistent. SDXL doesn't try. If the image has words in it, Ideogram v3 is the only answer. We've learned to accept the $0.09 cost for text-heavy images rather than wasting $0.30 on ten failed Flux attempts.</p>
<h2>The Numbers</h2>
<p>Over the past month:</p>
<p>| Category | Images | Total Cost | Avg Cost/Image |</p>
<p>|&mdash;&mdash;&mdash;-|&mdash;&mdash;&ndash;|&mdash;&mdash;&mdash;&ndash;|&mdash;&mdash;&mdash;&mdash;&mdash;-|</p>
<p>| Concept art (flux-2-pro) | ~120 | $3.60 | $0.03 |</p>
<p>| Exploration drafts (schnell) | ~400 | $1.20 | $0.003 |</p>
<p>| Logo variants (nano-banana) | 18 | $0.72 | $0.04 |</p>
<p>| Icons (nano-banana + birefnet) | 30 | $1.20 | $0.04 |</p>
<p>| Typography (ideogram) | ~25 | $1.50 | $0.06 |</p>
<p>| Background removal (birefnet) | ~80 | $0.08 | $0.001 |</p>
<p>| <strong>Total</strong> | <strong>~673</strong> | <strong>$8.30</strong> | <strong>$0.012</strong> |</p>
<p>Six hundred images for eight dollars. The infrastructure to route, authenticate, and persist them costs more than the generation itself.</p>
<h2>What We Learned</h2>
<p><strong>Never iterate on expensive models.</strong> The Schnell-to-Pro pipeline saves 10x. Most of the creative work happens at $0.003/image. The expensive model just polishes the decision you already made.</p>
<p><strong>Typography is a solved problem &mdash; but only on one model.</strong> Stop trying to make Flux render text. Use Ideogram v3 Quality for anything with words. Accept the cost.</p>
<p><strong>Vector output is underrated.</strong> Recraft v3's SVG export means logos and icons scale to any size without artifacts. For anything that might end up on a billboard or a business card, pay the $0.08 for vector.</p>
<p><strong>Background removal is basically free.</strong> At $0.001 per image, there's no reason to ever manually mask anything. Run BiRefNet on everything, keep both versions.</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. The proxy pattern lets us use the right tool for each job without managing five API keys and five billing accounts.</p>
<hr>
<p><em>Image generation is built into <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> &mdash; our Gitea-based platform for game teams. Every model above is available through the same inference proxy that handles LLM calls, authenticated with the same Gitea token. We're building <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a> with these tools, and every asset in the game touched at least one of them.</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>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
+154 -108
View File
@@ -15,48 +15,141 @@
<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">
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained index styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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;
}
/* ── Section label kicker ── */
.section-label {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
border-radius: 999px;
padding: 4px 14px;
margin-bottom: 16px;
}
/* ── Blog header ── */
.blog-header {
max-width: 720px;
margin: 0 auto;
padding: 60px 24px 32px;
}
/* ── Gradient index title ── */
.blog-header__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.4rem;
line-height: 1.25;
margin: 0 0 12px;
}
.blog-header__subtitle {
color: var(--c-muted);
font-size: 1.05rem;
line-height: 1.6;
margin: 0;
}
/* ── Blog list ── */
.blog-list {
max-width: 720px;
margin: 0 auto;
padding: 0 24px 60px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Blog card ── */
.blog-card {
display: block;
text-decoration: none;
background: #0c1119;
border: 1px solid var(--c-border);
border-radius: 12px;
padding: 20px 24px;
transition: border-color 0.2s;
}
.blog-card:hover {
border-color: var(--c-accent);
}
/* ── Date pill ── */
.blog-card__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
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 {
color: var(--c-text);
font-size: 1.25rem;
font-weight: 700;
line-height: 1.35;
margin: 0 0 8px;
}
.blog-card__excerpt {
color: var(--c-muted);
font-size: 0.92rem;
line-height: 1.55;
margin: 0 0 12px;
}
/* ── Read link accent ── */
.blog-card__read {
color: var(--c-blue);
font-size: 0.9rem;
font-weight: 500;
}
.blog-card:hover .blog-card__read {
color: var(--c-purple);
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- BLOG HEADER -->
<div class="blog-header">
<span class="section-label">Dev Log</span>
@@ -66,117 +159,70 @@
<!-- BLOG LIST -->
<div class="blog-list">
<!-- hand-authored HTML posts (not from build.js) -->
<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">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 &rarr;</span>
</a>
<a href="cloud-harness" class="blog-card">
<span class="blog-card__date">26 May 2026</span>
<h2 class="blog-card__title">Building a Cloud Agent Harness with DeepSeek V4 and Pi</h2>
<p class="blog-card__excerpt">We forked Pi, merged a browser dashboard into the monorepo, and built a Go orchestrator inside our Gitea fork. Agents code overnight for about $0.80 — and you can watch them from the browser.</p>
<a href="blog-visual-upgrade" class="blog-card">
<span class="blog-card__date">3 June 2026</span>
<h2 class="blog-card__title">How We Restyled Our Blog with Two Template Files and Zero Dependencies</h2>
<p class="blog-card__excerpt">We gave the Tinqs blog a visual refresh — gradient titles, dark code panels, date pills, amber accent bars. Two template files, one build step, zero external dependencies.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="agent-harness" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">Tinqs Studio Is an Agent Harness for Game Dev</h2>
<p class="blog-card__excerpt">An agent harness gives AI agents identity, memory, tools, and guardrails. Tinqs Studio is one built for game development.</p>
<h2 class="blog-card__title">What an Agent Harness Is and Why Game Dev Needs One</h2>
<p class="blog-card__excerpt">A raw AI model is stateless. An agent harness wraps around it and provides identity, memory, tools, context, and guardrails. Here's why game development needs its own.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="fal-image-generation" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">AI Art at Scale: Using fal.ai Flux for Game Asset Generation</h2>
<p class="blog-card__excerpt">We generate concept art, trailer frames, and UI icons with fal.ai Flux models at $0.01 per image. Here's the prompt engineering pattern that makes it work for game dev.</p>
<h2 class="blog-card__title">AI Art at Every Price Point: How We Generate Game Assets with fal.ai</h2>
<p class="blog-card__excerpt">We generate all visual assets for our game through fal.ai — concept art, icons, logos, trailer frames. Here's the 4-layer prompt pattern that actually works, and how we pick between 12 models spanning two orders of magnitude in cost.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="fork-dont-build" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">Fork, Don't Build: The Age of Agents Doesn't Need New Tools</h2>
<p class="blog-card__excerpt">Everyone is building new AI developer tools. We forked three existing ones and modified them from the inside. Here's why that's the better bet.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="image-generation-fal" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">Image Generation at Every Price Point with fal.ai</h2>
<p class="blog-card__excerpt">We generate concept art, logos, icons, and trailer frames through a single API proxy. Here's how we pick between 12 models spanning $0.002 to $0.09 per image.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="pi-ci-integrator" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">Pi as CI Integrator: Agents That Fix Their Own Builds</h2>
<p class="blog-card__excerpt">Most coding agents stop at git push. Our Pi fork watches CI, reads failure logs, and fixes its own code until the pipeline goes green.</p>
<h2 class="blog-card__title">Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch</h2>
<p class="blog-card__excerpt">Three forks, less than 0.5% code changed. Why modifying existing platforms beats building new ones — and how we turned Gitea into a game dev platform with 3D preview, AI agents, and LFS-first workflows.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="pre-commit-agent" class="blog-card">
<span class="blog-card__date">25 May 2026</span>
<h2 class="blog-card__title">A Pre-Commit Agent That Guards Your Secrets for $0.001</h2>
<p class="blog-card__excerpt">We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked secrets, classified terms, and broken URLs --- for a tenth of a cent.</p>
<p class="blog-card__excerpt">Too many things to remember before hitting commit. Don't leak API keys. Don't reference classified codenames. Don't link to deleted repos. We built a two-layer pre-commit hook — regex + LLM — that catches all of it for $0.001.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="godot-optimisation" class="blog-card">
<span class="blog-card__date">22 May 2026</span>
<h2 class="blog-card__title">Streaming a 12km Archipelago in Godot 4</h2>
<p class="blog-card__excerpt">Four streaming layers, async resource loading, memory-safe caches, and zero leaks. How we built a 12km open world in Godot 4 with C#.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="forking-gitea" class="blog-card">
<span class="blog-card__date">20 May 2026</span>
<h2 class="blog-card__title">Why We Forked Gitea and Built Tinqs Studio</h2>
<p class="blog-card__excerpt">GitHub doesn't understand game dev. We forked Gitea to build Tinqs Studio --- with 3D asset preview, LFS-first workflows, and project management for game teams.</p>
<p class="blog-card__excerpt">Godot has no built-in asset streaming. We built four layers to run a 12km archipelago with 9 islands, 155 vegetation types, and 2,000 crowd instances — on an RTX 3060.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="studio-cli" class="blog-card">
<span class="blog-card__date">18 May 2026</span>
<h2 class="blog-card__title">One Binary to Rule Them All: Building a Studio CLI</h2>
<p class="blog-card__excerpt">A single Go binary that gives AI agents context about who you are, what machine you're on, and what services are reachable. Screenshots, cloud vision, health checks --- one install, every machine.</p>
<h2 class="blog-card__title">One Binary to Rule Them All: Our Studio CLI</h2>
<p class="blog-card__excerpt">Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, and runs health checks — in 100ms.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
<a href="agentic-workflow" class="blog-card">
<span class="blog-card__date">6 March 2026</span>
<h2 class="blog-card__title">How a Small Game Studio Runs on AI Agents</h2>
<p class="blog-card__excerpt">Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale.</p>
<h2 class="blog-card__title">How a 4-Person Studio Runs on AI Agents</h2>
<p class="blog-card__excerpt">We gave AI agents persistent identities, skill playbooks, and access to our entire knowledge base. Here's how four people ship like forty.</p>
<span class="blog-card__read">Read &rarr;</span>
</a>
</div>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
-198
View File
@@ -1,198 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pi as CI Integrator: Agents That Fix Their Own Builds — Tinqs Blog</title>
<meta name="description" content="Most coding agents stop at git push. Our Pi fork watches CI, reads failure logs, and fixes its own code until the pipeline goes green.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/pi-ci-integrator">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/pi-ci-integrator">
<meta property="og:title" content="Pi as CI Integrator: Agents That Fix Their Own Builds">
<meta property="og:description" content="Coding agents that watch CI and fix their own builds.">
<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="Pi as CI Integrator: Agents That Fix Their Own Builds">
<meta name="twitter:description" content="Coding agents that watch CI and fix their own builds.">
<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": "Pi as CI Integrator: Agents That Fix Their Own Builds",
"datePublished": "2026-05-25",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "Most coding agents stop at git push. Our Pi fork watches CI, reads failure logs, and fixes its own code until the pipeline goes green."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">25 May 2026</span>
<h1 class="post__title">Pi as CI Integrator: Agents That Fix Their Own Builds</h1>
<p class="post__lead">Most coding agents have a dirty secret: they don't care if the code compiles. They write, they push, they walk away. The human discovers the broken build an hour later. We built a Pi extension that closes the loop &mdash; agents that watch CI, read failure logs, and fix their own mistakes.</p>
<div class="post__body">
<h2>The Gap</h2>
<p>Every agent demo looks the same. The AI writes code, commits, pushes. The presenter says "and now we have a pull request!" Cut. End of demo.</p>
<p>What happens next? The CI pipeline runs. Tests fail. Linting screams. The build breaks because someone forgot an import. A human opens the PR, reads the red badge, clicks into the logs, finds the error, fixes it, pushes again. The agent did 90% of the work but left the last 10% &mdash; the most tedious part &mdash; for a person.</p>
<p>We wanted agents that finish the job.</p>
<h2>The tinqs-ci Extension</h2>
<p>Our <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">Pi fork</a> has a <code>tinqs-ci</code> extension &mdash; a single TypeScript file, about 200 lines &mdash; that gives the agent three tools:</p>
<ul>
<li><strong>ci_status</strong> &mdash; checks the current pipeline state for a branch (pending, running, success, failure)</li>
<li><strong>ci_logs</strong> &mdash; fetches the full build log from the most recent failed run</li>
<li><strong>ci_wait</strong> &mdash; polls the pipeline every 15 seconds until it finishes, then returns the result</li>
</ul>
<p>These are Gitea Actions API calls under the hood. The agent authenticates with the same PAT it uses for git push. No extra credentials, no special CI service account.</p>
<h2>The Loop</h2>
<p>Here's what a Pi task looks like end to end:</p>
<pre><code>Agent receives task brief
→ reads codebase, plans approach
→ writes code
→ runs local tests (bash tool)
→ commits and pushes branch
→ calls ci_wait
→ CI passes → opens PR via Gitea API
→ CI fails → calls ci_logs
→ reads error output
→ fixes the issue
→ pushes again
→ calls ci_wait again
→ repeats until green (max 3 retries)</code></pre>
<p>The key is that <code>ci_logs</code> returns the raw build output &mdash; compiler errors, test failures, lint violations &mdash; as plain text in the agent's context. DeepSeek V4 is surprisingly good at reading build logs. It parses a Go compiler error, identifies the file and line, and fixes it. It reads a test assertion failure, understands what the test expected, and corrects the implementation.</p>
<p>Three retries is the hard limit. If the agent can't fix it in three rounds, it opens the PR anyway with a comment explaining what failed and why. A human takes over from there. In practice, most failures resolve on the first retry &mdash; it's usually a missing import or a type mismatch.</p>
<h2>What This Actually Looks Like</h2>
<p>A real run from last week. The task: add a health check endpoint to a Go service.</p>
<ul>
<li><strong>Turn 1:</strong> Agent reads the codebase, writes the handler and test, pushes. CI fails &mdash; the test imports a package that doesn't exist on the runner.</li>
<li><strong>Turn 2:</strong> Agent reads <code>ci_logs</code>, sees the <code>go: module not found</code> error, adds the missing <code>go.mod</code> replace directive, pushes. CI passes.</li>
<li><strong>Turn 3:</strong> Agent opens PR with passing checks.</li>
</ul>
<p>Total time: 4 minutes. Total cost: $0.06. No human touched the keyboard.</p>
<p>Without the CI extension, this would have been a PR with a red badge and a Slack message saying "hey, the agent's PR is broken again." Someone would have context-switched, opened the logs, seen the trivial error, fixed it, and lost 20 minutes of flow state.</p>
<h2>Why This Matters More Than You Think</h2>
<p>CI integration isn't a feature. It's the difference between an agent that helps and an agent that creates work.</p>
<p>An agent that pushes broken code is worse than no agent at all. It creates a false sense of progress &mdash; "the PR is up!" &mdash; while actually adding a task to someone's plate. Every broken PR is an interruption. Every interruption costs 15 minutes of context-switching.</p>
<p>An agent that watches CI and fixes its own builds is genuinely autonomous. You submit a task, you walk away, you come back to a green PR ready for review. The agent handled the mechanical iteration that a human would have done anyway &mdash; the fix-push-wait-check cycle that eats hours of developer time every week.</p>
<h2>The Guardrail Problem</h2>
<p>Letting an agent retry its own builds sounds dangerous. What if it enters an infinite loop? What if it starts making increasingly wild changes to get the build to pass?</p>
<p>Three safeguards:</p>
<p><strong>Retry limit.</strong> Three attempts maximum. After that, the agent stops and reports. This is a hard limit in the orchestrator, not a suggestion to the model.</p>
<p><strong>Diff budget.</strong> Each retry can only touch files that were already in the original changeset. The agent can't "fix" a build failure by rewriting the test suite or disabling the linter. If the fix requires touching new files, it fails and escalates.</p>
<p><strong>Hallucination detection.</strong> The guardrail extension monitors every turn. If the agent claims "the build passed" without having called <code>ci_status</code> or <code>ci_wait</code>, it gets corrected. Agents are not allowed to guess the CI result.</p>
<h2>The Numbers</h2>
<p>Over three weeks of running the orchestrator:</p>
<ul>
<li><strong>87 tasks</strong> completed end-to-end</li>
<li><strong>23 tasks</strong> needed at least one CI retry (26%)</li>
<li><strong>19 of those 23</strong> resolved on the first retry</li>
<li><strong>4 tasks</strong> hit the retry limit and escalated to a human</li>
<li><strong>0 tasks</strong> produced a merged PR that later broke something else</li>
</ul>
<p>The 26% retry rate tells you how often agents push code that doesn't build on the first try. That's not a bad number &mdash; it's the same rate you'd see from a junior developer. The difference is the agent fixes it in 30 seconds instead of 20 minutes.</p>
<hr>
<p><em>The CI extension is part of our <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">Pi fork</a>, which runs inside <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> &mdash; a Gitea-based platform for game development with built-in AI agents. The whole thing is MIT licensed.</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>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
+786
View File
@@ -0,0 +1,786 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How Pi Agents Build, Test, and Ship Game Code with Oracle-Backed Flows — Tinqs Blog</title>
<meta name="description" content="We use Pi flows with oracle-backed gates to make agents compile, test, drive the live game, measure feel, fix CI failures, and ship green PRs — all autonomously.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/pi-flow-native-brain">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/pi-flow-native-brain">
<meta property="og:title" content="How Pi Agents Build, Test, and Ship Game Code with Oracle-Backed Flows">
<meta property="og:description" content="Pi flows + oracle-backed gates: agents that compile, test, drive the game, measure feel, fix CI, and ship green PRs.">
<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="How Pi Agents Build, Test, and Ship Game Code with Oracle-Backed Flows">
<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">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "How Pi Agents Build, Test, and Ship Game Code with Oracle-Backed Flows",
"datePublished": "2026-06-04",
"author": {
"@type": "Person",
"name": "Ozan Bozkurt"
},
"publisher": {
"@type": "Organization",
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "We use Pi flows with oracle-backed gates to make agents compile, test, drive the live game, measure feel, fix CI failures, and ship green PRs — all autonomously."
}
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
/* ── Analogy callout box ── */
.post__body .callout {
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(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(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: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #38bdf8;
margin-bottom: 8px;
display: block;
}
.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 ── */
.gate {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.75rem;
font-weight: 600;
padding: 3px 10px;
border-radius: 5px;
margin-right: 4px;
}
.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: #2a3340; margin: 36px 0; }
.post__body hr.accent {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, #38bdf8 20%, #a855f7 50%, #f59e0b 80%, transparent);
margin: 40px 0;
}
/* ── Two-column kitchen comparison ── */
.kitchen-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 18px 0;
}
@media (max-width: 640px) { .kitchen-grid { grid-template-columns: 1fr; } }
.kitchen-col {
background: #0c1119;
border: 1px solid #2a3340;
border-radius: 10px;
padding: 16px 18px;
}
.kitchen-col__title {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
margin-bottom: 10px;
display: block;
}
.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 {
width: 100%;
border-collapse: collapse;
margin: 18px 0;
font-size: 0.92rem;
}
.post__body th {
text-align: left;
border-bottom: 1px solid var(--c-border);
padding: 10px 12px;
color: var(--c-accent);
font-weight: 600;
}
.post__body td {
padding: 9px 12px;
border-bottom: 1px solid #1c2230;
vertical-align: top;
}
</style>
</head>
<body>
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">4 June 2026</span>
<h1 class="post__title">How Pi Agents Build, Test, and Ship Code with Oracle-Backed Flows</h1>
<p class="post__lead">Think of a restaurant kitchen during dinner rush. The head chef doesn't cook every dish. She runs the pass — each plate gets inspected before it leaves. One cook handles sauces, another pastry, another the grill. The expediter calls orders, coordinates timing, makes sure table 4's mains don't arrive before table 2's starters. A dish comes back? It goes straight to the station that messed up, with a ticket explaining exactly what's wrong. That kitchen runs on flows. So does our game engine.</p>
<div class="post__body">
<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 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 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:#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="#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="#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:#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>
<p>In a kitchen, the head chef doesn't trust — she verifies. Every plate hits the pass and gets inspected. Our flows have the same instinct. Each gate is a sub-agent with one job, one tool, and absolute veto power.</p>
<div class="kitchen-grid">
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">In the Kitchen</span>
<p><strong>Check the base.</strong> Is the protein cooked through? If the chicken is raw, the whole plate stops here. Nothing else matters.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--reality">In the Flow</span>
<p><span class="gate gate--build">G1 · Build</span> Runs <code>dotnet build</code>. PASS/FAIL with file:line errors. Won't compile? Nothing proceeds.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">In the Kitchen</span>
<p><strong>Taste the sauce.</strong> Seasoning right? Acid balanced? The dish might look perfect but taste flat.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--reality">In the Flow</span>
<p><span class="gate gate--test">G2 · Tests</span> Runs <code>dotnet test</code>. Parses which assertions broke. Fixed code that passes build but fails logic gets caught here.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">In the Kitchen</span>
<p><strong>Does it work?</strong> Pick it up. Does the sauce hold? Does the plating survive the walk to table 6?</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--reality">In the Flow</span>
<p><span class="gate gate--behave">G3 · Behaviour</span> Sends <code>{"jump":true}</code> to the LIVE game. Samples the player body 30 times at 50ms. Did the character actually jump? Double-jump fire? This is the ground-truth oracle — what makes game dev fundamentally different from web dev.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">In the Kitchen</span>
<p><strong>How does it feel?</strong> The steak is cooked but chewy. The sauce is seasoned but gloopy. Edible ≠ good.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--reality">In the Flow</span>
<p><span class="gate gate--feel">G4 · Feel</span> Measures apex height, airtime, liftoff latency, rise/fall asymmetry, landing settle. Numeric thresholds. A jump that works but takes 400ms to lift off fails. Behaviour says it happened. Feel says it felt good.</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">In the Kitchen</span>
<p><strong>How does it look?</strong> Is the garnish wilting? Sauce smeared? Does it match the menu photo?</p>
</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>
</div>
</div>
<div class="callout callout--amber">
<span class="callout__kicker">The Loop</span>
<p>Any red gate → evidence sent back to the cook → fix → re-enter the inspection line. Three chances max, then the head chef escalates to a human. This is the same instinct that makes a good kitchen work: catch it early, send it back with a clear note, give them a chance to fix it, but don't let the same dish circle the pass forever.</p>
</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 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>
<p>Agents can extend the flow at runtime. If the behaviour gate keeps failing because the game window isn't focused, an agent notices the pattern and inserts a pre-condition gate that checks window focus. The flow engine handles routing; the agents handle decisions. This is what makes flows fundamentally different from a script — the pipeline isn't fixed at compile time. It's a graph that agents read, understand, and modify while they run.</p>
</div>
<hr class="accent">
<h2>The CI Loop: The Dish That Came Back After It Left</h2>
<p>Gates inspect plates at the pass. But what about after the plate leaves the kitchen? What about the customer who finds a hair in their soup after it's been served?</p>
<p>Most coding agents don't care. They write code, push, walk away. A human discovers the broken CI build an hour later. That's the equivalent of a cook plating a dish, sending it out, and never checking if the diner is still alive.</p>
<p>We closed this loop with three tools — the waiter who brings the plate back:</p>
<ul>
<li><strong>ci_wait</strong> — stands by the table, polls every 15 seconds until the diner finishes</li>
<li><strong>ci_status</strong> — checks: did they enjoy it or send it back?</li>
<li><strong>ci_logs</strong> — reads the complaint card: exactly what was wrong</li>
</ul>
<p>The agent pushes, calls <code>ci_wait</code>. If CI fails, it reads <code>ci_logs</code>, fixes the exact error, pushes again. DeepSeek V4 parses compiler errors the way a cook reads a ticket: "missing import" = forgot the salt, "type mismatch" = wrong pan size, "module not found" = ingredient not in stock. Pattern-matched and fixed in seconds.</p>
<div class="callout callout--amber">
<span class="callout__kicker">Real Example</span>
<p>Adding a health check endpoint to a Go service. Agent wrote the handler and test, pushed. CI failed — the test imported a package that didn't exist on the runner. Agent read <code>ci_logs</code>, saw <code>go: module not found</code>, added the missing <code>go.mod</code> replace directive, pushed again. CI passed. PR opened. <strong>4 minutes. $0.06.</strong></p>
</div>
<p>Three safeguards prevent the kitchen grinding to a halt: <strong>retry limit</strong> (3, same dish doesn't circle forever), <strong>diff budget</strong> (retries only touch files already on the ticket), and <strong>hallucination detection</strong> (if the cook claims the customer loved it without actually asking the waiter, they get corrected).</p>
<h2>The Numbers</h2>
<p>Over three weeks of running the orchestrator:</p>
<ul>
<li><strong>87 tasks</strong> completed end-to-end</li>
<li><strong>23 tasks</strong> needed at least one CI retry (26%)</li>
<li><strong>19 of those 23</strong> resolved on the first retry</li>
<li><strong>4 tasks</strong> hit the retry limit and escalated to a human</li>
<li><strong>0 tasks</strong> produced a merged PR that later broke something else</li>
</ul>
<p>The 26% retry rate matches what you'd see from a junior developer. The difference: the agent fixes it in 30 seconds.</p>
<h2>The Architecture</h2>
<table style="width:100%;border-collapse:collapse;margin:18px 0;font-size:0.92rem;">
<thead>
<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 #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>
<hr>
<h2>Three Kitchens, One Morning</h2>
<p>This morning, I ran three flows. Each is a different kitchen, a different brigade, a different dish. Here's what actually happened — real flow logs, real verdicts, nothing staged.</p>
<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>
</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:#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>
</div>
<hr class="accent">
<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 typed one slash command — the expediter reassembled the brigade:</p>
<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 YAML that runs in production:</p>
<pre><code>name: game-feature
description: Build a PLAYABLE game feature and prove it in the LIVE game.
task_required: true
steps:
# G0: Pre-flight — validate vision CAN run before any build work
- id: preflight
agent: vision-preflight
task: Check GEMINI_API_KEY is set AND game_frames reaches a live instance.
If EITHER fails, STOP — vision is not optional.
# Context + plan
- id: context
agent: project-context-reader
blockedBy: [preflight]
- id: plan
agent: feature-planner
blockedBy: [context]
# TDD: write tests FIRST (different agent than implementer)
- id: test-author
agent: test-author
blockedBy: [plan]
- id: implement
agent: game-builder
blockedBy: [test-author]
# G1G5: 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 gemini-2.5-flash)
# 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
# Final judge: one honest verdict
- id: report
agent: game-judge</code></pre>
<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>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>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>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.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 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" 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 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 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">
<span class="kitchen-col__title kitchen-col__title--kitchen">What People Expect</span>
<p>Agents chatting freely, PM-slack style: "Hey test-runner, I just pushed some code, can you check it? Also the jump feels off, maybe tune the velocity?"</p>
</div>
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--reality">What Actually Happens</span>
<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 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 1520 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:#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 #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 #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:#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
description: Implements game features in C# (Godot)
model: @coding
tools: read, write, edit, bash, verify_build, drive_game
inputs: [context, plan, build_fail, behaviour_fail, feel_fail, visual_fail]
outputs: [summary, files]
---
You are a game developer. Task: ${{task}}
Context: ${{input.context}}</code></pre>
<p><strong style="color:#f59e0b;">Flows</strong> are YAML DAGs that wire agents together. I have about <strong>1520 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, /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, 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>
<div class="kitchen-grid">
<div class="kitchen-col">
<span class="kitchen-col__title kitchen-col__title--kitchen">The Recipe (Rigid)</span>
<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>
<p>Inside their station, a cook has full agency. How they dice the onions — brunoise or rough chop — is their call. Which pan they use, how they adjust the heat, whether they taste midway. The game-builder decides which files to read, which approach to take. Nobody tells it "edit line 247." It figures that out with <code>grep</code>, <code>find</code>, and reading code.</p>
</div>
</div>
<p>This balance is everything. Too much recipe → agents can't handle surprises. Too much freestyle → agents hallucinate, skip checks, ship broken code. The recipe guarantees the right things happen in the right order — preflight before build, build before test, test before ship. The technique handles the messy, unpredictable reality of actual code.</p>
<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 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">
<h2>Picking the Right Knife: Model Strategy</h2>
<p>You don't use a paring knife to butcher a cow. You don't use a cleaver to supreme an orange. Different work needs different blades. Flows use <strong>role-based model tiers</strong> — each agent declares the blade it needs, and the engine hands it the right one at dispatch time.</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 #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 #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;">Gemini 2.5 Flash</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>
</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>
<hr class="accent">
<p>The oracle tools — <code>verify_build</code>, <code>drive_game</code>, <code>game_frames</code> — are the durable assets. About 300 lines of TypeScript each, MIT licensed, reusable in any Pi project. The flow engine composes them; the agents route through them.</p>
<p>A year ago we had a supervisor written in 1,050 lines of hardcoded TypeScript that did one thing: verify agent output compiled and passed tests. We deleted it. The same verification now runs as a composable flow with five gates, live-game testing, and CI integration. Sometimes the best architecture decision is knowing what to delete.</p>
<p><em>The flow-native brain runs on our <a href="https://tinqs.com/tinqs/pi">Pi fork</a> inside <a href="https://tinqs.com">Tinqs Studio</a>. The oracle extensions are MIT licensed and reusable in any Pi project.</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>
After
+38 -111
View File
@@ -1,151 +1,78 @@
---
title: "Tinqs Studio Is an Agent Harness for Game Dev"
title: "What an Agent Harness Is and Why Game Dev Needs One"
slug: agent-harness
date: "2026-05-25"
description: "An agent harness gives AI agents identity, memory, tools, and guardrails. Tinqs Studio is one built specifically for game development --- git, 3D preview, image generation, and a CLI that makes every session a warm start."
og_description: "Tinqs Studio is an agent harness for game dev --- identity, skills, vision, git, and creative tools in one platform."
description: "A raw AI model is a brain without hands, eyes, or memory. An agent harness gives it identity, tools, context, and guardrails — and game development needs one built for binary assets, visual pipelines, and spatial systems."
og_description: "Agent harnesses give AI identity, memory, and tools. Game dev needs one that understands 3D assets."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "An agent harness gives AI agents identity, memory, tools, and guardrails. Tinqs Studio is one built for game development."
excerpt: "A raw AI model is stateless. An agent harness wraps around it and provides identity, memory, tools, context, and guardrails. Here's why game development needs its own."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
An agent harness is the layer between a raw AI model and a useful team member. It gives the agent identity, memory, tools, and guardrails. Tinqs Studio is an agent harness built specifically for game development.
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."
## What Is an Agent Harness?
A raw AI model is a brain without hands, eyes, or memory. An **agent harness** 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.
A raw AI model --- Claude, GPT, Gemini --- is powerful but stateless. It doesn't know who you are, what project you're working on, what tools are available, or what happened yesterday. Every session is a cold start. Every conversation begins with "let me explain the project..."
## What a harness provides
An agent harness solves this. It wraps around the model and provides:
Every agent harness, regardless of domain, needs five things:
- **Identity** --- who the agent is, what it values, how it should behave
- **Memory** --- what happened in previous sessions, what was decided, what failed
- **Tools** --- what the agent can actually do beyond generating text
- **Context** --- what project this is, who's asking, what infrastructure exists
- **Guardrails** --- what the agent must never do, what requires human approval
**Identity.** 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.
Without a harness, you have a chatbot. With one, you have a team member.
**Memory.** 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 `git log` instead of debugging a vector database.
## Why Game Dev Needs Its Own Harness
**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.
Generic agent harnesses exist --- LangChain, CrewAI, AutoGen. They're built for web apps, data pipelines, and customer support. Game development has different problems:
**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?"
**Assets are binary.** A web developer's PR is a text diff. A game developer's PR is a 150MB GLB file. Generic harnesses don't know how to preview 3D models, manage LFS bandwidth, or review binary assets.
**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.
**The pipeline is visual.** Game dev goes from concept art to 3D model to rigged character to in-engine asset. Each step uses different tools --- image generators, 3D modellers, auto-riggers, game engines. An agent harness for game dev needs to orchestrate this entire chain.
## Why generic harnesses fail for game dev
**Scale is physical.** A web app's complexity is in business logic. A game's complexity is in geometry --- 12km worlds, 155 vegetation prototypes, 576 terrain regions, 2000 crowd instances. The agent needs to understand spatial systems, GPU memory, and frame budgets.
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:
**The team is small and cross-functional.** A 4-person game studio has no dedicated DevOps, no dedicated artist, no dedicated PM. The harness needs to fill all those gaps, not just one.
**Assets are binary.** 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.
## How Tinqs Studio Works as a Harness
**The pipeline is visual.** 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.
Tinqs Studio is a platform built on a [Gitea fork](forking-gitea) with game-specific features layered on top. But the git platform is just the foundation. The harness is everything around it.
**Scale is physical.** 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.
### Identity: Soul Files
**The team is small and cross-functional.** Four people. No dedicated DevOps, no dedicated artist, no dedicated PM. The harness fills all those gaps, not just one.
Every agent session starts by loading a soul file --- a markdown document that defines the agent's persistent identity. Not just "you are a helpful assistant" but specific values, knowledge scope, and behavioural rules.
## The toolchain that makes it work
The soul file means the agent behaves consistently whether it's triaging bugs at 9am or generating concept art at midnight. It knows what repos exist, who the team members are, what the game is about, and what decisions have been made. Identity isn't cosmetic --- it's the difference between an agent that asks "what project is this?" and one that says "I see the vegetation grid was updated yesterday, want me to check the cache eviction?"
Our harness runs on [Tinqs Studio](https://tinqs.com), built on a Gitea fork with game-specific features. The key pieces:
### Memory: Markdown Files in Git
**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.
Agent memory is plain markdown files in a git repository. No vector databases, no proprietary stores. The agent reads its memory on session start, updates it during work, and commits changes back.
**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.
This is deliberately low-tech. Markdown in git gives you version history, branching, merge conflict resolution, and human readability for free. When memory goes wrong --- and it will --- you can `git log` to see what changed and `git revert` to fix it.
**Skills** — 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.
### Tools: The CLI
**3D preview** — click a `.glb` 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.
A [single Go binary](studio-cli) gives every agent access to:
**Guardrails** — 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.
- **Identity loading** --- full project context in 100ms
- **Screenshots** --- capture any window from outside the process
- **Cloud vision** --- send screenshots to a vision model, get structured descriptions
- **Health checks** --- verify services, repos, and tools are working
- **Service status** --- which URLs are live, what's reachable
## The cold-start problem, solved
The CLI is the agent's hands and eyes. Without it, the agent can only read and write text. With it, the agent can see the game running, photograph bugs, and verify infrastructure.
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.
### Skills: Teachable Workflows
The harness uses staged loading:
Skills are markdown playbooks that teach agents specific procedures. Instead of hoping the model figures out how to generate concept art or create a 3D model, you write the steps once:
1. **CLI identity call** (100ms) — soul file, company context, machine info, service status
2. **Memory file** (instant) — cross-session context from the docs repo
3. **Skills** (on demand) — loaded only when the task matches a skill name
4. **Repo context** (on demand) — files read as needed, not all upfront
- [Image Generation](../skills/image-generation.md) --- generate game art with fal.ai Flux using a [4-layer prompt pattern](fal-image-generation)
- [Concept Art Pipeline](../skills/concept-art-pipeline.md) --- from design brief through 2D art to 3D model export
- [3D Model Generation](../skills/tripo-browser-workflow.md) --- Tripo Studio text-to-3D and image-to-3D
- [Video Generation](../skills/sora2-video.md) --- trailer clips with OpenAI Sora 2
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.
Skills compound. Every playbook you write makes the agent more capable. After six months, our agents handle art generation, competitive research, video production, project management, and code review --- all from markdown files.
## The bet
### Git Platform: 3D Preview and LFS
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.
The [Gitea fork](forking-gitea) underneath handles the game-specific git problems:
- **3D asset preview** --- rotate GLB/FBX/STL files in the browser during code review
- **LFS-first workflows** --- auto-tracking for game file extensions, storage dashboards
- **OAuth2 SSO** --- one login for git, tools, and the game
- **22 format support** --- GLB, FBX, OBJ, STL, 3DS, PLY, and more via O3DV
### Guardrails: Human-in-the-Loop
The harness defines what agents can and cannot do:
- Agents can file issues, draft announcements, generate assets, write code
- Agents **cannot** merge code, deploy builds, push to public repos, or post to external channels without human approval
- The [public blog repo](https://tinqs.com/tinqs/blog) requires human-approved merge requests --- agents can propose changes but a person must review
This isn't a limitation --- it's a feature. The agent handles volume; the human handles judgement.
## The Cold Start Problem
The biggest problem with AI agents in production isn't capability --- it's context. Every new session is blank. The agent doesn't know what happened yesterday, what's in progress, or what tools are available.
Most teams solve this with long system prompts. That works until your context is 200 markdown files, 15 skills, and 3 years of project history. You can't paste all of that into a system prompt.
The harness solves this with **staged loading**:
1. **CLI identity call** (100ms) --- loads soul file, company context, machine info, service status
2. **Memory file** (instant) --- loads cross-session context
3. **Skills** (on demand) --- loaded only when the task matches a skill name
4. **Repo context** (on demand) --- read files as needed, not all upfront
The agent goes from cold to fully contextual in under a second. No "let me explain..." No re-reading the same onboarding doc. Just start working.
## What Makes This Different from LangChain
LangChain, CrewAI, and similar frameworks are **code-first**. You define agents in Python, chain them with function calls, and deploy them as services. They're powerful for building AI products.
Tinqs Studio is **file-first**. Agents are defined in markdown. Skills are markdown. Memory is markdown. Identity is markdown. Everything is in git, readable by humans, editable without code changes, and version-controlled.
This matters for game teams because:
- **Non-engineers can contribute.** The designer writes a skill for concept art. The PM writes a skill for sprint planning. No Python required.
- **Everything is auditable.** `git log` shows who changed what, when, and why. Memory changes are commits. Skill updates are diffs.
- **It works with any AI tool.** The same soul files and skills work in Cursor, Claude Code, or any tool that reads markdown. You're not locked into one framework.
## The Stack
| Layer | What | How |
|-------|------|-----|
| **Identity** | Soul files, company context | Markdown in git, loaded via CLI |
| **Memory** | Cross-session context | Markdown in git, updated by agents |
| **Skills** | Teachable workflows | Markdown playbooks, loaded on demand |
| **Tools** | CLI, screenshots, vision | Go binary, one install per machine |
| **Git** | 3D preview, LFS, SSO | Gitea fork with game-specific features |
| **Creative** | Image gen, 3D models, video | fal.ai, Tripo, Sora 2 via skills |
| **Guardrails** | Human approval gates | Branch protection, MR requirements |
## Getting Started
If you want to build your own agent harness for game dev:
1. **Start with a soul file.** Write 50 words about your project's identity, values, and scope. Put it in your repo root as `SOUL.md`.
2. **Write one skill.** Pick the workflow you repeat most --- concept art generation, bug triage, build verification --- and write the steps as markdown.
3. **Build a CLI identity command.** Even a shell script that prints "project name, repos, services" gives your agent a warm start.
4. **Put everything in git.** Not a database, not a SaaS tool. Git. You already have it.
The rest --- 3D preview, LFS management, OAuth SSO, creative pipelines --- you can add as you need it. Or use [Tinqs Studio](https://tinqs.com), where we've already built it.
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.
---
An agent harness isn't a product category yet. But it should be. 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. That's what we're building.
*Tinqs Studio is an agent harness for game development — git hosting, AI agents, creative pipelines. Open for teams. We're building [Ariki](https://arikigame.com) with the same tools.*
+59 -72
View File
@@ -1,103 +1,90 @@
---
title: "How a Small Game Studio Runs on AI Agents"
title: "How a 4-Person Studio Runs on AI Agents"
slug: agentic-workflow
date: "2026-03-06"
description: "Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale."
description: "We gave AI agents persistent identities, skill playbooks, and access to our entire knowledge base. Here's how four people ship like forty."
og_description: "Soul files, skill playbooks, and markdown as the universal API for AI agents in game dev."
og_image: "https://www.tinqs.com/blog/img/agentic-workflow-architecture.png"
excerpt: "Soul files, skill playbooks, and markdown as the universal API. How we built an agentic workflow that lets a 4-person indie studio operate at 10x scale."
excerpt: "We gave AI agents persistent identities, skill playbooks, and access to our entire knowledge base. Here's how four people ship like forty."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
We gave our AI agents persistent identities, skill playbooks, and access to our entire knowledge base. This is how a 4-person game studio built an agentic workflow that punches above its weight.
Last week one of our agents caught a stale bug at 3am — a vegetation culling issue that had been open for six days. It nudged the team chat, drafted a fix summary, and by morning the issue was resolved. Nobody lost sleep. Nobody context-switched. The agent just handled it.
## The Problem Every Small Studio Knows
This is what happens when you stop treating AI as a chatbot and start treating it as a team member with a persistent identity, a memory, and a set of skills it can actually execute.
When you're four people building a game, there's no room for a dedicated DevOps person, a full-time PM tool chain, or someone whose job it is to "keep things organised." Everyone wears five hats. Documentation drifts. Issues pile up. The left hand doesn't know what the right hand shipped.
## The problem with "just use ChatGPT"
We tried the usual tools --- Notion, Trello, shared Google Docs. They all had the same problem: they're passive. They sit there and wait for a human to update them. In a team of four where the lead developer is also the CTO, that human never has time.
Every small studio hits the same wall: four people, forty roles. Nobody has time to keep documentation current. Bugs pile up. The backlog rots. Someone asks "what did we decide about the inventory system?" and three different answers come back.
So we built something different. We gave AI agents persistent identities, connected them to our entire knowledge base, and let them become working members of the team.
The usual fix is more tools — Notion, Trello, Linear, Slack integrations. But tools are passive. They sit there waiting for humans to update them. In a team where the lead developer is also the CTO, that human is already stretched thin.
## The Architecture: Agents with Identity
We tried something different. Instead of adding more tools for humans to maintain, we gave AI agents persistent identities, connected them to our entire knowledge base, and let them do the maintenance.
Our primary AI agent runs inside the IDE and has access to the full documentation repository --- the game design document, backlog, meeting notes, company operations, everything. It's not a chatbot. It's a persistent team member with a **soul file** that defines its values and operating principles, and a **memory file** that persists context across sessions.
## Soul files: giving agents a personality that sticks
The key insight: **all knowledge lives in markdown files in one repo**. No databases, no SaaS dashboards, no proprietary formats. Plain text, version-controlled, readable by humans and agents alike. When anyone on the team opens the docs repo, the agent wakes up with full context of who they are, what machine they're on, and what's been happening.
The core idea is embarrassingly simple. Every agent gets a **soul file** — a markdown document that defines who it is, what it values, and how it should behave:
### What the agent actually does
- **Values** — "never break the build," "always verify before acting," "prefer existing patterns over novelty"
- **Knowledge scope** — what repos exist, who's on the team, what the game is about
- **Behavioural rules** — when to act autonomously, when to ask, what requires explicit human approval
This isn't theatre. It's the difference between an agent that asks "what project is this?" every session and one that says "I see the vegetation grid was updated yesterday — want me to check the cache eviction?"
The soul file loads in 100ms when the agent starts. No cold starts. No re-explaining.
## Memory: markdown in git, not a vector database
Agent memory lives as plain markdown files in our docs repo. No vector databases, no proprietary stores, no SaaS dashboards. The agent writes to its memory file during work, commits it, and reads it on the next session.
This is deliberately low-tech. Markdown in git gives you version history, diffs, branching, and human readability for free. When memory goes wrong — and it will — you `git log` to see what changed and `git revert` to fix it. Try debugging a corrupted vector embedding at 11pm.
## Skills: teachable playbooks, not prompt engineering
Agents don't just have instructions. They have **skills** — markdown playbooks that teach specific workflows. When someone says "generate concept art for a character," the agent reads `skills/image-generation.md` and follows the procedure. No prompt engineering per session. No "let me try a different prompt."
We've open-sourced several skills:
- [Image Generation with fal.ai](../skills/image-generation.md) — 4-layer prompt pattern that actually produces usable game art
- [Concept Art Pipeline](../skills/concept-art-pipeline.md) — full 2D concept → 3D model workflow
- [3D Model Generation](../skills/tripo-browser-workflow.md) — Tripo Studio text-to-3D
- [Video Generation](../skills/sora2-video.md) — trailer clips with Sora 2
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.
## What the agents actually do, every day
**During the day** (interactive, inside the IDE):
- Triages and grooms the issue backlog
- Keeps documentation in sync with the game state
- Processes bug reports from testers and creates structured issues
- Drafts team announcements, reviews PRs, manages cross-repo coordination
- Generates concept art, trailer frames, and UI assets using integrated API skills
- Conducts competitive research --- analysing Steam pages, player reviews, pricing strategies
- Keeps documentation in sync with what's actually in the game
- Processes tester bug reports and creates structured issues
- Generates concept art, trailer frames, UI icons on demand
- Conducts competitive research — Steam pages, player reviews, pricing
The team talks to the agent through voice. The IDE's built-in microphone transcribes and auto-translates (multilingual team). The agent is trained to interpret messy voice-to-text artifacts and act on intent, not grammar.
The team talks to the agent through voice. The IDE microphone transcribes, and the agent interprets intent from messy voice-to-text. "There's a tree floating two meters above the terrain on the west beach" becomes a filed issue with a screenshot, a vision-model description, and coordinates.
## Background Automation
**At night** (background daemon, $15/day):
The interactive agent only runs when someone opens the IDE. But a studio doesn't sleep --- bugs get reported at midnight, issues go stale, and the team chat fills up while everyone's away.
- Polls team chat every 15 minutes, responds to commands
- When a tester reports a bug in chat, creates a structured issue automatically
- Flags stale issues that haven't been touched
- Posts a morning digest of what happened overnight
- Creates its own skill files when it discovers better approaches
A background daemon runs 24/7, ticking every 15 minutes. It uses a three-tier model strategy --- cheap models for routine checks, medium for analysis, and premium only when it needs deep reasoning. The whole thing costs about $15/day.
## What we learned
### What it handles
**Plain text is the universal API.** Every tool, every agent, every human can read a markdown file. We store everything — design docs, meeting notes, agent memory, team contacts — as `.md` in one repo. It sounds too simple, but it eliminates an entire class of integration problems.
- **Chat monitoring** --- polls team chat, responds to commands, acknowledges messages
- **Bug intake** --- when a tester reports a bug in chat, creates a structured issue automatically
- **Stale issue detection** --- flags issues that haven't been touched, nudges the team
- **Daily summaries** --- posts a morning digest of what happened overnight
- **Self-learning** --- creates its own skill files when it discovers better approaches
**Cheap models for routine, expensive models for thinking.** Most of what an agent does is pattern matching — "does this look like a bug report?" You don't need DeepSeek Pro for that. Save the premium tokens for decisions that actually require reasoning. Our background daemon costs $15/day with a three-tier model strategy.
The two agents coordinate through the docs repo itself. One writes, the other reads. No API calls between them, no message queue. Just git.
**Voice changes everything.** When you can describe a bug while looking at the screen, and the agent transcribes, interprets, and files it — that collapses the distance between noticing a problem and tracking it. Keyboard-free bug reporting is a superpower.
## The Skill System
**Skills compound exponentially.** 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.
Agents don't just have instructions --- they have **skills**. Each skill is a markdown file that teaches the agent a specific workflow: how to generate concept art through a pipeline, how to use image generation APIs, how to conduct competitive research, how to create 3D models from concept art.
When someone asks the agent to do something that matches a skill, it reads the skill file and follows the procedure. This means you can teach the agent new capabilities without changing any code --- just write a new markdown file.
We've open-sourced several of our skills in this repo:
- [Image Generation with fal.ai](../skills/image-generation.md)
- [Concept Art Pipeline](../skills/concept-art-pipeline.md)
- [3D Model Generation with Tripo](../skills/tripo-browser-workflow.md)
- [Video Generation with Sora 2](../skills/sora2-video.md)
## Soul Files: Why Identity Matters
Giving the agent a persistent identity isn't theatre. It creates consistency across sessions. The soul file defines:
- **Values** --- what the agent prioritises (e.g., "never break the build," "always verify before acting")
- **Knowledge scope** --- what repos, services, and team members exist
- **Behavioural rules** --- how to handle ambiguity, when to ask vs act, what requires human approval
The agent remembers what it learned, adapts to who's asking, and maintains the same principles whether it's triaging bugs or drafting a Steam page description. The soul file is the agent's constitution.
## What We've Learned
**Plain text is the universal API.** Every tool, every agent, every human can read a markdown file. We store everything --- design documents, meeting notes, agent memory, team contacts --- as .md files in one repository. This sounds almost too simple, but it eliminates an entire class of integration problems.
**Cheap models for routine, expensive models for thinking.** Most of what an autonomous agent does is pattern matching and text formatting --- you don't need the most expensive model for that. Save the premium tokens for decisions that actually require reasoning.
**The human stays in the loop for decisions.** The agents can file issues, draft announcements, and generate assets --- but they don't merge code, deploy builds, or post to public channels without explicit approval. The workflow is designed so the AI handles the grunt work while humans make the calls that matter.
**Voice input changes everything.** When you can describe a bug while looking at the game screen, and the agent transcribes, interprets, and files an issue --- that's a workflow that collapses the distance between noticing a problem and tracking it.
**Skills compound.** Every skill file you write makes the agent more capable. After 6 months, our agents have 15+ skills covering art generation, competitive research, video production, and project management. Each one took 30 minutes to write and saves hours every week.
## The Numbers
- **Team size:** 4 humans + AI agents
- **Background agent cost:** ~$15/day (~$450/month)
- **Knowledge files:** 200+ markdown documents
- **Skills:** 15+ agent skill files and growing
- **Infrastructure:** Multiple machines, self-hosted git, zero DevOps engineers
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.
---
We're not claiming this is how every studio should work. But for a small team trying to build something ambitious, having AI agents that actually understand the project --- not just answer questions about it --- has been transformative. The agents don't replace anyone on the team. They make it possible for four people to do the work of forty.
We're building all of this as part of [Tinqs Studio](https://tinqs.com) --- a game development platform that brings git hosting, AI tools, and team workflows together. The blog posts and skills in this repo are part of that journey.
*We're building [Ariki](https://arikigame.com), a survival colony sim, using the same agent workflow described here. Everything runs on [Tinqs Studio](https://tinqs.com) — a game dev platform with built-in AI agents, git hosting, and creative pipelines.*
+90
View File
@@ -0,0 +1,90 @@
---
title: "How We Restyled Our Blog with Two Template Files and Zero Dependencies"
slug: blog-visual-upgrade
date: "2026-06-03"
description: "Gradient titles, dark code panels, amber callouts — we gave the Tinqs blog a visual refresh borrowing our internal team guide aesthetic. Two template files, one Node script, no framework."
og_description: "Blog restyle: gradient titles, dark code panels, amber callouts — two template files, zero deps."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "We gave the Tinqs blog a visual refresh — gradient titles, dark code panels, date pills, amber accent bars. Two template files, one build step, zero external dependencies."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
Our blog looked fine. Readable, semantic, proper typography. But it didn't have much personality. Code blocks were unstyled. Headings sat flat. The design said "competent" more than "intentional."
Then we looked at our internal team guide — a self-contained HTML doc with gradient titles that clip to transparent, dark code panels, and callout boxes with coloured borders. It radiated a "well-maintained developer doc" energy. We wanted the blog to feel like it came from the same shop.
Two template files, one build step, zero external dependencies. Here's what we changed.
## The build system (why it mattered)
The blog is generated by `build.js` — a zero-dependency Node script that converts markdown to HTML:
```
posts/*.md + _template.html / _index_template.html → *.html
```
This means we never touch a generated `.html` file by hand. Every visual change flows through the templates. The site-wide CSS — nav, footer, base typography, brand accent — lives in `../style.css`, served by Git Studio from outside the repo. We didn't touch it.
Instead, we injected a self-contained `<style>` block at the end of `<head>` in both templates, after the `../style.css` link. Cascade order handles the overrides. No `!important`. No external font loads. No CDN dependencies.
## The palette
Four accent colours, borrowed from our team guide:
| Role | Colour | Where |
|------|--------|-------|
| Brand amber | `#c9935a` | Gradient start, h2 left bar, card hover |
| Warm gold | `#f59e0b` | Gradient midpoint, bold text |
| Blue | `#38bdf8` | Gradient endpoint, links, date pills |
| Purple | `#a855f7` | h3 colour, link hover |
Tasteful. Not a rainbow.
## What we styled
**Gradient titles.** Post `h1` gets `linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8)` via `background-clip: text; color: transparent`. Underlying text preserved for screen readers and SEO.
**Date pills.** Monospace chip — blue text, `999px` border-radius, uppercase, tight letter-spacing. Sits before the title like a kicker.
**Code blocks.** The site CSS only styled inline `<code>`. Fenced blocks got a dark panel (`#0a0e14`, `#2a3340` border, `10px` radius, monospace, `overflow-x: auto`). A reset rule on `pre code` prevents inline-code styles from doubling up inside the panel.
**Section cues.** h2 gets a 4px amber left border as visual anchor. h3 gets a purple tint — enough to signal a section break without pulling focus.
**Links.** Blue `#38bdf8`, purple `#a855f7` on hover. Bold text picks up amber.
**Blockquote callouts.** Amber-tinted background, 4px amber left border, rounded right corners. The CSS is written and waiting — `build.js` doesn't emit `<blockquote>` yet, but the rules activate the moment we add `>` syntax support.
**Index page.** Same gradient on the listing title. Date pills on cards. Amber border on card hover. Blue/purple read links.
## The rebuild
```bash
node build.js
```
```
Building blog...
11 posts built + index.html
Done.
```
Zero errors. Every regenerated HTML file now carries the inline `<style>`. Confirmed with `grep -l "background-clip" *.html` — all pages ship the gradient.
## What we didn't touch
Navigation, footer, site chrome, responsive behaviour — all unchanged. This was a CSS-only change. No markup altered beyond the `<style>` injection.
## What we learned
**Inline styles beat separate CSS files when you don't control the server.** The blog repo is standalone — it can't modify `../style.css`. Inline `<style>` blocks ship inside the generated HTML, so the blog is fully self-contained. One `git push` and it's live.
**Cascade order is the cleanest specificity hack.** Putting the `<style>` block after the external stylesheet link means same-selector rules win by position. No `!important`, no selector wars, no unexpected regressions.
**Build systems make CSS changes safe.** Because we never hand-edit `.html`, every style change is tested by regenerating all pages and grepping for the new selectors. If a rule doesn't ship, you know immediately.
Two gaps we'll fill later: blockquote support in `build.js` (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.
---
*The blog is generated by [build.js](https://tinqs.com/tinqs/blog) and served by [Tinqs Studio](https://tinqs.com). All styling is self-contained in the templates.*
-156
View File
@@ -1,156 +0,0 @@
---
title: "Building a Cloud Agent Harness with DeepSeek V4 and Pi"
slug: cloud-harness
date: "2026-05-26"
description: "We forked Pi, merged a browser dashboard into the monorepo, and built a Go orchestrator inside our Gitea fork. Agents code overnight for about $0.80 — and you can watch them from localhost:33634."
og_description: "Pi fork, merged agent dashboard, and a Go orchestrator inside Tinqs Studio."
og_image: "https://www.tinqs.com/blog/img/cloud-harness-architecture.png"
excerpt: "We forked Pi, merged a browser dashboard into the monorepo, and built a Go orchestrator inside our Gitea fork. Agents code overnight for about $0.80 — and you can watch them from the browser."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
We spent a few sessions building something that still barely exists elsewhere: a cloud agent harness where AI coding agents are first-class citizens of the platform, not bolt-on tools. The stack is a [Pi fork](https://tinqs.com/tinqs/pi) for the brain, a Go orchestrator inside our [Gitea fork](https://tinqs.com/tinqs/studio) for overnight work, and a browser dashboard merged into Pi for the daytime. Here is how it fits together.
## The Problem
Every coding agent today — Claude Code, Codex, Pi, Aider — runs in your terminal. You watch it work. You close the laptop, it stops. There is no way to say "build these eight features overnight" and wake up to pull requests.
We wanted exactly that. Not a coding assistant. An autonomous workforce — with a UI when a human needs to be in the loop.
## Why Not Just Use Claude Code or Codex?
**Cost.** Claude Code runs on Opus at $15/MTok output. Codex uses GPT 5.5. Running eight agents overnight on either would cost $50200. DeepSeek V4 Flash costs $0.28/MTok output. Eight overnight tasks: **about $0.80**.
**Control.** Cloud tools are black boxes. We cannot add a Gitea API tool, a fal.ai image generator, or a guardrail that blocks `aws ec2 terminate-instances`. With our own harness, we add an extension and it is live.
**Platform.** We are building [Tinqs Studio](https://tinqs.com) — a Gitea-based game development platform. Agents are not a feature we want to outsource. They are the product.
## Pi — The Agent Brain
[Pi](https://pi.dev) 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.
We [forked it](https://tinqs.com/tinqs/pi). Not to rewrite the core — to add first-party extensions:
- **tinqs-provider** — routes DeepSeek V4 Flash and Pro through our inference proxy
- **tinqs-tools** — Gitea REST API, fal.ai image generation, Amazon Nova Lite vision
- **tinqs-ci** — reads CI pipeline status, logs, and polls for completion
- **tinqs-guardrail** — 29 safety patterns that block dangerous operations
Each extension is a single TypeScript file. No extra npm dependencies on the extension side.
Pi has four output modes. The one that matters for automation is **RPC** — a headless process that accepts JSON on stdin/stdout. That is how the orchestrator drives it.
## DeepSeek V4 — The LLM
DeepSeek V4 Flash through our own inference proxy. OpenAI-compatible API, so Pi treats it like any other provider. The proxy adds:
- Redis job queue (10 concurrent workers)
- Per-user usage tracking
- System prompt injection for cache hit optimization
- Gitea PAT authentication (same token as git push)
Cost per task: **$0.020.10** depending on complexity.
## Go Orchestrator — Overnight Batch Work
Inside `tinqs/studio` we added `modules/agents/` — a Go worker pool that:
- Spawns Pi with `--mode rpc --no-session`
- Tracks task lifecycle (pending → running → done)
- Streams events over **SSE** to any connected UI
- Enforces guardrails at the platform layer (worker limits, timeouts)
Six HTTP endpoints, same auth as git push:
```
POST /api/v1/agents/tasks — submit a task
GET /api/v1/agents/tasks — list all tasks
GET /api/v1/agents/tasks/{id} — get task details
DELETE /api/v1/agents/tasks/{id} — stop a task
GET /api/v1/agents/stream — SSE live events
GET /api/v1/agents/health — orchestrator status
```
We considered bolting on a separate orchestration SaaS and rejected it. The orchestrator lives in the same binary as git — same auth, no extra service to deploy.
The intended loop:
```
Orchestrator reads task brief
→ spawns pi --mode rpc
→ Pi writes code using DeepSeek V4
→ Pi pushes branch, calls ci_wait
→ CI green → Pi opens PR via gitea_api
→ CI red → Pi reads ci_logs, fixes, retries
→ Human reviews PR, merges
```
Git worktree integration and full push/PR automation are still being wired; the API and worker pool already run locally.
## Pi Dashboard — Browser UI (Shipped)
The cloud orchestrator is for batch work while you sleep. During the day you want to see agents, chat with them, and spawn sessions without living in a terminal.
We merged [pi-agent-dashboard](https://github.com/BlackBeltTechnology/pi-agent-dashboard) into the Pi monorepo — not as a second repo to install. One checkout, one command:
```bash
npm run dashboard:dev
```
Open **http://localhost:33634**. You get:
- **Live session streaming** — watch tool calls and model output in real time
- **Interactive chat** — send prompts, answer `ask_user` dialogs from the browser
- **Session spawning** — start Pi in any pinned project folder
- **Cost tracking** — per-session token usage when using Tinqs inference
- **Plugins** — flows, subagents, workspace helpers
The dashboard talks to Pi sessions over a WebSocket bridge on port **9999**. Inference uses the same Tinqs proxy as the CLI — register a custom provider in `~/.pi/agent/providers.json` and authenticate with your existing `tstudio` token. No separate LLM API keys.
```
Dashboard (localhost:33634)
↕ WebSocket (port 9999)
Pi sessions (interactive or headless)
↕ OpenAI-compatible API
Tinqs Studio proxy (tinqs.com/api/v1/ai)
↕ DeepSeek V4 Flash / Pro
```
When Studio runs locally with agents enabled, the dashboard can also talk to the orchestrator API on port 3000 — submit tasks and watch SSE events in the same UI.
One browser tab for daytime work; the orchestrator queue for overnight runs.
## The Guardrail
Our biggest fear: an agent hallucinating instead of using tools, or running `aws ec2 terminate-instances` at 3 AM.
The guardrail extension monitors every agent turn:
**Hallucination detection** — if the agent claims file contents without calling `read`, it gets corrected.
**No-tool drift** — three consecutive turns without a tool call triggers a warning.
**Command blocking** — 29 patterns covering destructive git, AWS teardown, process killing, and production API abuse.
## What It Cost to Build
A few focused sessions: about 2,000 lines of Go, 900 lines of TypeScript extensions, 52 tests, plus merging the dashboard packages into the Pi monorepo. No new servers — Pi is a Node subprocess; the dashboard is another Node process on your machine.
## What Is Next
| Piece | Status |
|-------|--------|
| Pi fork + tinqs extensions | Shipped |
| Dashboard merged into Pi monorepo | Shipped |
| Go orchestrator + REST/SSE API | MVP, running locally |
| Git worktree + push + PR loop | In progress |
| Domain routing (game / sim / platform tasks) | Designed |
Next we are promoting studio skills from IDE playbooks into orchestrator prompt packs — so the same Pi worker behaves like a game builder, sim maintainer, or platform engineer depending on the task. Specialized agents (planner, reviewer, asset pipeline) sit on top of this foundation.
The harness — inference proxy, guardrails, dashboard, orchestrator API — is in place. The work now is feeding it real tasks and hardening the git loop.
---
*Tinqs Studio is an open platform for game development — git hosting, AI inference, asset generation, and autonomous agents. We are building [Ariki](https://arikigame.com), a survival colony sim, using the same tools we ship.*
+82 -107
View File
@@ -1,162 +1,137 @@
---
title: "AI Art at Scale: Using fal.ai Flux for Game Asset Generation"
title: "AI Art at Every Price Point: How We Generate Game Assets with fal.ai"
slug: fal-image-generation
date: "2026-05-25"
description: "How we use fal.ai Flux models to generate concept art, trailer frames, and UI assets for our game --- with a 4-layer prompt pattern that actually works."
og_description: "fal.ai Flux for game art: 4-layer prompts, $0.01/image, and a pipeline that replaced our concept art bottleneck."
description: "From $0.002 to $0.09 per image, across 12 models. How we built a prompt pattern that actually produces usable game art, and a model-picking strategy that keeps costs at $8/month."
og_description: "12 fal.ai models, $0.002-$0.09/image, 4-layer prompt pattern. Game art that actually ships."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "We generate concept art, trailer frames, and UI icons with fal.ai Flux models at $0.01 per image. Here's the prompt engineering pattern that makes it work for game dev."
excerpt: "We generate all visual assets for our game through fal.ai — concept art, icons, logos, trailer frames. Here's the 4-layer prompt pattern that actually works, and how we pick between 12 models spanning two orders of magnitude in cost."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
We're a small indie studio building a survival colony sim. We don't have a concept artist on staff. Every piece of character art, trailer frame, and UI icon in our game was generated with fal.ai Flux models --- at roughly a penny per image.
Every visual asset in our game — character art, app icons, trailer frames, logo variants, Steam capsules — was generated through a single API. No Photoshop. No concept artist on staff. Last month: 673 images, $8.30 total. Here's the prompt pattern that makes AI art actually usable for game development, and how we pick between 12 models spanning $0.002 to $0.09 per image.
## The Problem with AI Art for Games
## The problem with AI art for games
Most AI image generators produce beautiful images that are completely useless for game development. They look great on social media but fall apart when you need consistency: the same character from four angles, a UI icon that reads at 64x64, a trailer frame that matches your game's art style rather than whatever the model defaults to.
Most AI-generated images look beautiful on social media and useless in a game. The character looks different from every angle. The art style drifts between generations. The text in the logo is garbled. The icon doesn't read at 64×64.
The issue isn't the models --- Flux is genuinely good. The issue is prompting. When you write "warrior on a beach," you get a different art style every time. Different skin tones, different proportions, different lighting. You can't build a game from that.
The issue isn't the models Flux, Ideogram, and Recraft are genuinely good. The issue is prompting. "Warrior on a beach" gives you a different art style, different skin tone, different proportions every time. You can't build a game from one-offs.
We spent three months iterating on prompt patterns before we found something that works consistently. The result is a 4-layer system that anchors the model to your art direction and produces images you can actually ship.
We spent three months iterating before we found a prompt structure that anchors the model to a consistent art direction and produces images you can actually ship. It has four layers.
## Why fal.ai
## The 4-layer prompt pattern
We evaluated Midjourney, DALL-E 3, Stable Diffusion (self-hosted), and fal.ai:
### Layer 1: Design context (the anchor)
**API-first.** Midjourney is Discord-only. DALL-E's API works but the model makes everything look like a stock photo. Self-hosted SD means maintaining GPU infrastructure. fal.ai gives you Flux models behind a simple REST API --- POST a prompt, GET an image URL.
**Cost.** $0.01 per image with `flux-2-pro`. $0.004 with `schnell` for rapid iteration. A full character design session --- 12 variants across 3 rounds of refinement --- costs $0.12. A 20-frame trailer storyboard costs $0.20. At these prices, the bottleneck is creative direction, not budget.
**Speed.** `flux/schnell` returns an image in 4 seconds. `flux-2-pro` in 15 seconds. Fast enough that an AI agent can generate, display, get feedback, and regenerate in a single conversation turn.
**No subscription.** Pay per image. No monthly fee, no credit packs that expire, no tier-gated features.
## The 4-Layer Prompt Pattern
This is the pattern that made AI art actually usable for our game. Each layer adds specificity, and the combination anchors the model to a consistent output.
### Layer 1: Design Context
The most important layer and the one most people skip. It sets the overall art direction:
This is the most important paragraph and the one most people skip. It sets the art direction for every single generation:
```
Art direction: stylized 3D render for a survival colony sim. Warm earthy
palette --- browns, tans, dark reds, cream, ocean blues. Carved wood
palette browns, tans, dark reds, cream, ocean blues. Carved wood
textures, traditional patterns, woven natural fibres. Game engine quality,
not photorealistic.
```
This paragraph appears at the start of every prompt. Same paragraph whether you're generating a character, a landscape, or an icon. It anchors the model to your art style.
Same paragraph whether you're generating a character, a landscape, or an icon. It's your art bible compressed into 50 words. Every time we skipped it — "just a quick test" — the output drifted into generic fantasy art.
**The key insight:** write this once, paste it everywhere. It's your art bible compressed into 50 words. Every time we skipped it --- "just a quick test" --- the output drifted into generic fantasy art.
### Layer 2: Scene description (be specific)
### Layer 2: Scene Description
Not "tribal clothing" — "woven wrap skirt, mid-thigh length." Not "jewelry" — "shell necklace with a carved bone pendant." Vague prompts produce vague results. Specific prompts produce usable assets. Describe element by element.
Describe exactly what should appear, element by element:
```
Full body character in T-pose, front view. Young woman, mid-20s.
Wearing a woven wrap skirt (mid-thigh length) and a fitted cloth top.
Shell necklace with a carved bone pendant. Single bone bracelet on
left wrist. Hair swept back over right shoulder. Bare feet.
Matte skin, warm brown tones. Neutral confident expression ---
not smiling, not angry. Dark grey background.
```
Not "tribal clothing" but "woven wrap skirt." Not "jewelry" but "shell necklace with a carved bone pendant." Vague prompts produce vague results. Specific prompts produce usable assets.
### Layer 3: Negative Prompt
### Layer 3: Negative prompt (prevent drift)
Always include what you don't want:
```
Do not include: cartoon style, anime style, photorealistic render,
extra text or taglines, watermark, deformed elements, modern or
sci-fi. No extra fingers, no merged limbs, no floating accessories.
extra text or taglines, watermark, deformed elements, modern or sci-fi.
No extra fingers, no merged limbs, no floating accessories.
```
Extend per-subject. For characters: "no stereotypical elements, no overly shiny materials." The negative prompt is as important as the positive one.
AI models have strong defaults — they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, say so explicitly.
### Layer 4: Reference Images
### Layer 4: Reference images (consistency)
When you need consistency --- the same character from different angles, or a new character matching an existing one --- pass a reference image:
When you need the same character from different angles, pass the first approved image as reference. Without it, every generation is independent — a different person every time. With it, every generation builds on the last approved output. This is how you get a roster of characters that look like they belong in the same game.
```python
result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
"prompt": "Same character, side view, same clothing and accessories...",
"image_url": "https://your-approved-front-view.png",
"image_size": "square_hd",
})
```
## The model lineup (and when to use each)
The first approved image becomes the reference for all subsequent views. Without it, you get a different person every time.
Not every image needs the best model. A throwaway mockup doesn't justify $0.09. A final logo doesn't deserve $0.002.
## The Model Lineup
| Model | Cost | Speed | Use for |
|-------|------|-------|---------|
| Flux 2 Pro | $0.03 | 15s | Final art, characters, environments |
| Flux Schnell | $0.003 | 3s | Exploration drafts, 20-variant grids |
| Ideogram v3 Quality | $0.09 | 12s | Anything with readable text |
| Recraft v3 | $0.04-0.08 | 10s | Logos, brand assets, SVG vectors |
| Seedream v4.5 | $0.04 | 8s | Photorealistic scenes |
| Flux Dev | $0.025 | 10s | LoRA fine-tuning base |
| Nano Banana Edit | $0.039 | 12s | Style transfer, material variants |
| BiRefNet | $0.001 | 3s | Background removal |
| Model | Cost | Speed | When |
|-------|------|-------|------|
| `flux-2-pro` | $0.01 | ~15s | Final art. Default for anything you'll ship. |
| `flux/schnell` | $0.004 | ~4s | Exploration and iteration. |
| `ideogram/v2` | $0.008 | ~5s | Anything with readable text --- logos, UI, posters. |
| `flux-pro/v1.1-ultra` | $0.015 | ~8s | Highest quality, but can hang. |
### The Schnell-to-Pro pipeline (never iterate on expensive models)
The workflow: explore with `schnell`, refine with `flux-2-pro`, add text with `ideogram/v2`.
Every generation session follows the same pattern:
## How This Fits Our Pipeline
1. **Explore with Schnell** ($0.003) — 10-20 variants, different angles, color palettes. Cost: $0.03-0.06
2. **Pick 2-3 directions.** Human looks at the grid, picks winners.
3. **Refine with Flux 2 Pro** ($0.03) — regenerate winners at full quality. Cost: $0.06-0.09
4. **Post-process** — BiRefNet for background removal ($0.001), Recraft for vector ($0.08)
fal.ai is the first step in a pipeline from idea to in-game asset:
A full session — blank canvas to final assets — costs under $0.20. Most of the creative work happens at $0.003/image. The expensive model just polishes a decision you already made.
### Typography: one model rules them all
Every model except Ideogram fails at text. Flux gives you beautiful art with garbled letters. SDXL doesn't try. If your image has words in it, Ideogram v3 Quality is the only answer. We learned to accept the $0.09 cost rather than waste $0.30 on ten failed Flux attempts.
### Logo variants at scale
Our game logo has 18 material variants — mahogany, mother-of-pearl, obsidian, molten lava, bronze with verdigris. Each generated with Nano Banana Edit ($0.039) + BiRefNet ($0.001) for transparency. Total: $0.72. A designer would quote hundreds of dollars and a week.
## The numbers (one month of generation)
| Category | Images | Cost | Avg/Image |
|----------|--------|------|-----------|
| Concept art (flux-2-pro) | 120 | $3.60 | $0.03 |
| Exploration (schnell) | 400 | $1.20 | $0.003 |
| Logo variants | 18 | $0.72 | $0.04 |
| Icons | 30 | $1.20 | $0.04 |
| Typography (ideogram) | 25 | $1.50 | $0.06 |
| Background removal | 80 | $0.08 | $0.001 |
| **Total** | **673** | **$8.30** | **$0.012** |
Six hundred images. Eight dollars.
## The pipeline: from prompt to in-game asset
fal.ai is step one of a pipeline that goes from idea to walking character in about two hours:
```
Brief --> fal.ai (2D concept art) --> Tripo Studio (3D model) --> Blender (decimate) --> Godot (in-game)
Brief fal.ai (2D concept) → Tripo Studio (3D model) Blender (decimate) Godot (in-game)
```
1. **Brief.** The designer describes the character or asset.
2. **2D generation.** Generate 3 variants with `flux-2-pro`, score each on a rubric (style match, cultural accuracy, silhouette, expression, animatability), pick the best.
3. **Reference sheet.** Generate front, side, three-quarter, and head closeup views using the winner as reference.
4. **3D model.** Approved concept art goes into Tripo Studio for image-to-3D. Outputs ~1.5M faces with full PBR textures.
5. **Decimation.** Blender CLI decimates to 25,000 faces.
6. **Rigging.** Auto-rig the body (hair separated first if large).
7. **In-game.** Import into the engine, set up materials, done.
1. Designer describes the character
2. Generate 3 variants with Flux 2 Pro, score on 5 criteria (style match, cultural accuracy, silhouette, expression, animatability)
3. Generate front/side/three-quarter reference views using the winner
4. Tripo Studio image-to-3D (~1.5M faces, PBR textures)
5. Blender CLI decimates to 25k faces
6. Auto-rig, import into engine, done
The entire pipeline from "I want a character" to "character walking around in the game" takes about 2 hours. The quality isn't AAA, but for an indie game with a stylised art style, it's more than good enough.
Quality isn't AAA, but for an indie game with a stylized art style, it's more than good enough. Ten characters designed, total fal.ai spend: $6.
## What We Learned
## What we learned
**The design context layer is everything.** Without it, every image is a one-off. With it, every image belongs to the same game. The 50-word context block is worth more than the rest of the prompt combined.
**The design context block is worth more than the rest of the prompt combined.** Without it, every image is a one-off. With it, every image belongs to the same game.
**Negative prompts prevent drift.** AI models have strong defaults --- they want to make things shiny, symmetrical, and photorealistic. If your game isn't those things, say so explicitly.
**Never iterate on expensive models.** 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.
**Score and iterate, don't accept the first output.** Generate 3 variants, score on 5 criteria, approve only 8+/10. Three attempts at $0.01 each is $0.03 --- cheaper than working around a mediocre image.
**Aggregation beats loyalty.** 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.
**Reference images are the consistency mechanism.** Without them, every generation is independent. With them, every generation builds on the last approved output. This is how you get a roster of characters that look like they belong in the same game.
**Let the agent handle prompting.** We encode the 4-layer pattern, art style guide, and model selection rules in an [agent skill file](../skills/image-generation.md). The AI writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.
**Fast models for exploration, quality models for output.** `schnell` at 4 seconds is for "what if..." iterations. `flux-2-pro` at 15 seconds is for "yes, this is the one."
**Let the AI agent handle prompt engineering.** We encode the 4-layer pattern, art style guide, and cultural guardrails in a [skill file](../skills/image-generation.md). The agent writes the full prompt, generates images, displays them, and asks for scores. The human's job is creative direction.
## The Numbers
- **Characters designed:** 10 (full roster for early access)
- **Total images generated:** ~400 across all iterations
- **Total cost:** ~$6 in fal.ai credits
- **Time per character:** ~30 minutes from brief to approved reference sheet
- **Pipeline time:** ~2 hours from concept art to in-game model
- **Models used:** flux-2-pro (80%), schnell (15%), ideogram/v2 (5%)
## Open-Source Skills
We've published the skill files that power this workflow. A skill is a markdown document that teaches an AI agent a specific procedure --- like a runbook, but the reader is an LLM.
- **[Image Generation](../skills/image-generation.md)** --- fal.ai API, 4-layer prompt pattern, model comparison
- **[Concept Art Pipeline](../skills/concept-art-pipeline.md)** --- full 2D-to-3D character workflow
- **[3D Model Generation](../skills/tripo-browser-workflow.md)** --- Tripo Studio text-to-3D and image-to-3D
- **[Video Generation](../skills/sora2-video.md)** --- trailer clip generation with OpenAI Sora 2
Drop any of these into your `.cursor/skills/` directory and your AI agent can follow them. Adapt the design context block to your game's art style and you're good to go.
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."
---
AI image generation isn't magic and it isn't free. But at a penny per image, with the right prompt structure, it eliminates the most expensive bottleneck in indie game development: the gap between "I know what this should look like" and "I have an image I can actually use."
We're building all of this as part of [Tinqs Studio](https://tinqs.com) --- a game development platform that brings together git hosting, AI tools, and creative workflows for game teams.
*Image generation is built into [Tinqs Studio](https://tinqs.com). We've open-sourced the [prompt engineering skill](../skills/image-generation.md) and [concept art pipeline skill](../skills/concept-art-pipeline.md). We're building [Ariki](https://arikigame.com) with these tools.*
+75 -90
View File
@@ -1,113 +1,98 @@
---
title: "Fork, Don't Build: The Age of Agents Doesn't Need New Tools"
title: "Fork, Don't Build: How We Modified Gitea, Pi, and Godot Instead of Starting from Scratch"
slug: fork-dont-build
date: "2026-05-25"
description: "Everyone is building new AI developer tools. We forked three existing ones --- Gitea, Pi, Godot --- and modified them from the inside. Here's why that's the better bet."
description: "Everyone is building new AI developer tools. We forked three battle-tested open-source projects — Gitea, Pi, and Godot and modified them from the inside. Combined changes: less than 0.5% of upstream code."
og_description: "Fork Gitea. Fork Pi. Fork Godot. Modify platforms, don't build toys."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "Everyone is building new AI developer tools. We forked three existing ones and modified them from the inside. Here's why that's the better bet."
excerpt: "Three forks, less than 0.5% code changed. Why modifying existing platforms beats building new ones — and how we turned Gitea into a game dev platform with 3D preview, AI agents, and LFS-first workflows."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
The AI developer tools space has a problem: everyone is building new things. New agents, new IDEs, new platforms, new wrappers around GPT. Meanwhile, the tools that actually run the world --- git servers, game engines, CI runners --- sit there unchanged, waiting for someone to open them up and let agents in. We chose to fork instead of build. Three times. Here's why.
The AI developer tools space is full of people building new things. New agents, new IDEs, new platforms, new wrappers. Meanwhile, the tools that actually run the world git servers, game engines, CI runners sit there unchanged, waiting for someone to open them up and let AI agents in.
## The Pattern
We're a four-person game studio. We don't have time to build a git platform, a coding agent, and a game engine from scratch. Nobody does. But we can take something that already works --- something with years of battle-testing, thousands of contributors, and millions of users --- and change it from the inside.
The pattern is simple:
1. Find an open-source tool that does 95% of what you need
2. Fork it
3. Add the 5% that makes it yours
4. Stay close to upstream so you get their fixes for free
We've done this three times.
## Fork 1: Gitea --- Our Git Platform
[Gitea](https://gitea.com) is a self-hosted git server. Single Go binary, MIT license, 45k GitHub stars. It handles repos, issues, pull requests, CI, LFS --- everything a team needs.
We [forked it](https://tinqs.com/tinqs/studio) and built Tinqs Studio. Our changes:
- **3D asset preview** --- click a `.glb` file and rotate the model in your browser
- **HTML file preview** --- rendered in a sandboxed iframe, not raw source
- **Agent API** --- six endpoints that let AI agents submit tasks, push code, and open PRs
- **OAuth2 SSO** --- one login for git, the game, and every tool
- **Credits system** --- monetize AI inference without hiding features behind paywalls
Total lines changed from upstream: about 2,000 out of Gitea's 500,000. That's 0.4%. We modify templates, add Go modules, and tweak CSS variables. We never touch the database schema --- we ride upstream's migrations. When Gitea releases 1.27, we rebase, fix conflicts, and ship.
The alternative was building a git platform from scratch. That's a multi-year, multi-million dollar project. Or using GitHub/GitLab and accepting their limitations. Neither option gives you the ability to embed AI agents directly into the platform.
## Fork 2: Pi --- Our Agent Runtime
[Pi](https://pi.dev) is an open-source coding agent. 51k stars, MIT license, TypeScript. Four core tools (read, write, edit, bash), a minimal system prompt, and an extension system.
We [forked it](https://tinqs.com/tinqs/pi) and added four extensions:
- **tinqs-provider** --- routes inference through our DeepSeek V4 proxy ($0.28/MTok vs Opus at $15/MTok)
- **tinqs-tools** --- Gitea API, fal.ai image generation, vision preprocessing
- **tinqs-ci** --- reads CI pipeline status and logs, polls for completion
- **tinqs-guardrail** --- 29 safety patterns blocking dangerous commands
Each extension is a single TypeScript file. No npm dependencies. The core Pi code is untouched --- we only add files.
The alternative was building our own agent from scratch. That means writing tool-calling logic, context management, streaming, retry handling, conversation threading --- months of work to reinvent what Pi already does. Or using Claude Code / Codex as a black box and accepting that you can't add a Gitea API tool or a budget cap.
## Fork 3: Godot --- Our Game Engine
[Godot](https://godotengine.org) is an open-source game engine. We forked 4.6.2 and added nine C++ modules that turn the engine into an agent-aware runtime:
- **agent_api** --- HTTP server inside the engine, so agents can query game state
- **agent_vision** --- screenshot capture for AI vision pipelines
- **agent_console** --- programmatic access to the engine console
- **agent_replay** --- record and replay game sessions for testing
- **agent_analytics** --- PostHog event tracking from inside the engine
These modules compile into the engine binary. A vanilla Godot user never sees them. An agent can connect to the running engine over HTTP, take a screenshot, read the scene tree, execute a console command, and capture the result --- all without touching the editor UI.
The alternative was building an engine integration from scratch. Or worse, building a custom engine. We'd still be writing a renderer instead of making a game.
## Why Forking Beats Building
### You inherit decades of work
Gitea has handled millions of git pushes. Godot renders millions of frames. Pi has processed millions of LLM tokens. That battle-testing is free when you fork. When you build from scratch, you spend your first year rediscovering bugs that were fixed upstream in 2019.
### You get free maintenance
Every upstream release brings security patches, performance improvements, and new features --- written by hundreds of contributors we don't pay. Our job is to rebase, resolve conflicts, and test. That's an afternoon, not a quarter.
### You stay focused
Building a git server from scratch means worrying about pack-file format, SSH key management, webhook delivery, and a thousand other things that have nothing to do with AI agents. Forking means you only think about the 5% that matters to you. The other 95% is someone else's problem.
### Agents work better on real platforms
An agent that pushes to a real Gitea instance --- with real CI, real code review, real permissions --- produces work that humans can actually review and ship. An agent that pushes to a toy demo platform produces demos.
The whole point of AI agents is to participate in real workflows. Real workflows run on real tools. If you want agents in your git workflow, put them in your git server. If you want agents in your game pipeline, put them in your game engine.
We forked three of them. Gitea for git hosting. Pi for coding agents. Godot for the game engine. Combined changes: less than 0.5% of upstream code. Here's why, how, and what we learned.
## The 0.5% Rule
Across all three forks, our total changeset is less than 0.5% of the upstream code. Tinqs Studio: 0.4% of Gitea. Pi extensions: 900 lines added to a 15,000-line codebase. Godot modules: 2,000 lines added to a 2-million-line engine.
We're four people. We can't build a git platform, a coding agent, or a game engine from scratch. Nobody can — not in a timeframe measured in months.
This isn't a coincidence. If your fork touches more than 1% of upstream, you're doing too much. Either the upstream tool is wrong for the job, or you're not trusting it enough. The power of forking is that you don't have to understand the whole codebase. You find the extension points, add your code, and leave the rest alone.
But we can take something that already works — something with years of battle-testing and thousands of contributors — and change the last half-percent that makes it ours. The pattern:
## What We're Not Doing
1. Find open-source tool that does 95% of what you need
2. Fork it
3. Add the 5% (really, 0.5%)
4. Stay close to upstream so their fixes are your fixes
We're not building a new IDE. Cursor and Claude Code exist. We're not building a new LLM. DeepSeek and Claude exist. We're not building a new cloud platform. AWS exists.
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.
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.
## Fork 1: Gitea → Tinqs Studio
## The Bet
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 **see** what changed. A PR modifying a `.glb` file showed a binary diff. No preview. The artist pushed, the developer approved blindly, and three days later someone noticed the normals were inverted.
The age of agents doesn't need more agents. It needs better platforms. Platforms that understand agents as first-class users --- with API endpoints, safety rails, and lifecycle management. Those platforms already exist as open-source projects. They just need someone to fork them and add the wiring.
We forked Gitea and built [Tinqs Studio](https://tinqs.com). Our changes:
That's the bet. Fork, don't build. Modify the foundation, don't stack another layer on top. Let the upstream community handle the 99.5% while you focus on the 0.5% that makes it yours.
**3D asset preview.** Click a `.glb`, `.gltf`, or `.fbx` 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.
**HTML file preview.** Sandboxed iframe rendering. Our internal docs and game design pages look like websites, not raw source.
**Agent API.** 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.
**LFS-first workflows.** Auto-tracking on repo creation. Game file extensions (`.fbx`, `.glb`, `.png`, `.wav`) tracked by default. Storage dashboard per repo. Clone times went from 45 minutes to 3 minutes.
**OAuth2 SSO.** One login for git, the game tools, and the team dashboard.
Total lines changed: about 2,000 out of Gitea's 500,000. We modify templates, add Go modules, tweak CSS. We **never** touch the database schema — upstream owns that, and we ride their migrations.
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.
## Fork 2: Pi → Agent Runtime with Game Tools
[Pi](https://pi.dev) 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.
We forked it and added four extensions, each a single TypeScript file:
- **tinqs-provider** — routes inference through our DeepSeek V4 proxy ($0.28/MTok vs Opus at $15/MTok)
- **tinqs-tools** — Gitea REST API, fal.ai image generation, vision model access
- **tinqs-ci** — reads CI pipeline status, fetches build logs, polls for completion
- **tinqs-guardrail** — 29 safety patterns blocking dangerous commands
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.
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.
## Fork 3: Godot → Agent-Aware Game Engine
[Godot](https://godotengine.org) 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:
- **agent_api** — HTTP server inside the engine so agents can query game state
- **agent_vision** — screenshot capture for AI vision pipelines
- **agent_console** — programmatic console access
- **agent_replay** — record and replay game sessions for testing
These compile into the engine binary. A vanilla Godot user never sees them. An agent connects over HTTP, takes a screenshot, reads the scene tree, executes a console command — all without touching the editor UI.
2,000 lines added to a 2-million-line engine. The alternative: building our own engine, or worse, writing a renderer instead of making a game.
## Why forking beats building
**You inherit decades of work, for free.** Gitea has handled millions of git pushes. Godot renders millions of frames. Pi has processed millions of LLM tokens. That battle-testing is yours when you fork. When you build from scratch, year one is spent rediscovering bugs fixed upstream in 2019.
**You get free maintenance.** Every upstream release brings security patches, performance improvements, and new features — written by hundreds of contributors you don't pay. Your job is to rebase, resolve conflicts, and test. An afternoon, not a quarter.
**You stay focused.** Building a git server means worrying about pack-file format, SSH key management, webhook delivery. Forking means you only think about the 0.5% that matters to you. The other 99.5% is someone else's problem.
**Agents work better on real platforms.** An agent pushing to a real Gitea instance — with real CI, real code review, real permissions — produces work humans can actually review and ship. An agent pushing to a toy demo platform produces demos.
## What we're not building
We're not building a new IDE (Cursor and Claude Code exist). Not a new LLM (DeepSeek and Claude exist). Not a new cloud platform (AWS exists).
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.
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.
---
*[Tinqs Studio](https://tinqs.com) is our Gitea fork, open for game teams and indie studios. We're building [Ariki](https://arikigame.com) --- a survival colony sim --- using every tool described in this post. If you're interested in self-hosted game development with built-in AI agents, come take a look.*
*[Tinqs Studio](https://tinqs.com) is our Gitea fork, open for game teams. [Ariki](https://arikigame.com) is the game we're building with every tool described here.*
-77
View File
@@ -1,77 +0,0 @@
---
title: "Why We Forked Gitea and Built Tinqs Studio"
slug: forking-gitea
date: "2026-05-20"
description: "Game studios need git hosting that understands large files, 3D assets, and team workflows. We forked Gitea and built Tinqs Studio --- here's why and how."
og_description: "Game studios need git that understands LFS, 3D previews, and team workflows. We built Tinqs Studio."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "GitHub doesn't understand game dev. We forked Gitea to build Tinqs Studio --- with 3D asset preview, LFS-first workflows, and project management for game teams."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
GitHub is built for web developers. Game studios need something different --- LFS that works, 3D asset previews in the browser, and project management that understands sprints and milestones. So we forked Gitea and built Tinqs Studio.
## The Problem with GitHub for Game Dev
We used GitHub for two years. It was fine for docs --- small files, text diffs, pull requests. But the game repo was a different story.
A single character model with textures and animations is 50--200MB. A terrain heightmap is 16MB. An island's vegetation data is another 10MB. Our game repo was 12GB in LFS alone, growing every week. GitHub's LFS bandwidth limits, slow clone times, and $5/50GB pricing made it untenable.
More importantly, nobody on the team could **see** what changed. A PR that modifies a GLB file shows a binary diff. You can't preview it. You can't compare before and after. The artist pushes a model, the developer approves it blindly, and three days later someone notices the normals are inverted.
## Why Self-Host, and Why Gitea
We evaluated GitLab, Forgejo, Gogs, and Gitea. The decision came down to:
- **Single binary.** Gitea compiles to one Go binary with SQLite support. No PostgreSQL, no Redis, no Docker compose with 7 services. Just copy the binary, write an app.ini, and run it.
- **Resource usage.** Our instance runs on a single EC2 instance alongside other services. It uses about 200MB RAM. GitLab needs 4GB minimum.
- **LFS built-in.** Gitea includes a full LFS server. No external LFS store, no S3 configuration for basic use. Files are stored locally. We added S3 backend later, but it works out of the box.
- **Forkable.** Gitea is MIT-licensed, written in Go, with a clean codebase. We can modify it without worrying about license restrictions or CLA headaches.
We ran vanilla Gitea for six months. It solved the cost and bandwidth problems immediately. But the UX gaps for game development were still there.
## What We Built: Tinqs Studio
[Tinqs Studio](https://tinqs.com) is our fork. It tracks upstream Gitea on the `main` branch and keeps all customisations on a separate branch. We rebase onto upstream releases periodically, fix conflicts, and deploy.
### 3D Asset Preview
The headline feature. When you open a PR that contains a `.glb`, `.gltf`, or `.fbx` file, you see a 3D viewer directly in the browser. Rotate, zoom, check materials. No downloads, no external tools. We integrated Online 3D Viewer (O3DV), which supports 22 file formats including STL, OBJ, 3DS, and PLY.
This changes the review process fundamentally. The artist pushes a model, the lead rotates it in the browser, leaves a comment about the UV seam on the shoulder, and the artist fixes it --- all without leaving the git platform.
### LFS-First Workflows
Vanilla Gitea treats LFS as an afterthought. You configure `.gitattributes` manually. There's no dashboard showing LFS usage, no way to see which files are tracked, no warnings when someone commits a large file without LFS.
Tinqs Studio adds auto-LFS tracking on repository creation. Game file extensions (`.fbx`, `.glb`, `.png`, `.wav`, `.ogg`, `.tscn`, `.tres`) are tracked by default. An API endpoint exposes LFS storage stats per repo. The goal: LFS should be invisible. It should just work.
### Platform Integration
Tinqs Studio integrates project management --- issues, sprints, time tracking --- and OAuth2 SSO. One login for git, the game tools, and the team dashboard.
## The Branching Strategy
Staying close to upstream is critical. We don't want to maintain a fork that diverges forever:
- `main` tracks upstream `go-gitea/gitea`. We never commit to it directly.
- Our production branch holds all customisations.
- Feature branches merge into production.
- When upstream releases a new version, we merge, resolve conflicts, test, deploy.
We deliberately limit what we touch. We modify templates, locale strings, CSS variables, and a handful of Go packages. We **never** touch the database models --- schema is owned by upstream, and we ride their migrations. This keeps rebasing manageable.
## What We Learned
**Self-hosting git is surprisingly easy.** The hard part isn't running Gitea --- it's convincing yourself that you're allowed to. After years of GitHub being the default, it feels transgressive to host your own git. But a single Go binary on a $10/month server handles a small team with room to spare.
**LFS changes everything for game repos.** Our clone times went from 45 minutes to 3 minutes. Developers only download the LFS objects they need. CI only pulls what changed. The bandwidth savings alone paid for the server.
**Forking is maintenance, not rebellion.** The romantic version is "we forked Gitea and built our own platform." The reality is we changed 200 lines of Go, 50 template strings, and a CSS file. 99.5% of the code is upstream's. We're just customising the last half-percent for our use case.
**3D preview is a game changer.** We expected it to be a nice-to-have. It turned out to be the feature that made the rest of the team actually use git. When the artist can see their work rendered in the browser, they stop asking the developer to "check if it looks right."
---
[Tinqs Studio](https://tinqs.com) is built for game teams that are tired of paying GitHub for LFS bandwidth and reviewing binary diffs blind. We're building it for ourselves first --- dogfooding it on our own game --- but the plan is to make it available as a platform for other studios. If you're a game team that self-hosts or wants to, we'd love to hear what features you need.
+49 -71
View File
@@ -2,115 +2,93 @@
title: "Streaming a 12km Archipelago in Godot 4"
slug: godot-optimisation
date: "2026-05-22"
description: "How we built four streaming layers, async resource loading, and memory-safe caches to run a 12km open world in Godot 4 with C#."
og_description: "Four streaming layers, async loading, and zero memory leaks --- optimising Godot for a large open world."
description: "Godot 4 has no built-in asset streaming. We built four layers — terrain regions, vegetation chunks, async loading, and entity rendering — to run a 12km open world with 9 islands, 155 vegetation types, and 2,000 crowd instances."
og_description: "Four streaming layers, async loading, and zero memory leaks — running a 12km open world in Godot 4."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "Four streaming layers, async resource loading, memory-safe caches, and zero leaks. How we built a 12km open world in Godot 4 with C#."
excerpt: "Godot has no built-in asset streaming. We built four layers to run a 12km archipelago with 9 islands, 155 vegetation types, and 2,000 crowd instances — on an RTX 3060."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
Godot has no built-in asset streaming. Our game is a 12km x 12km archipelago with 9 islands, thousands of trees, hundreds of buildings, and an ocean that never ends. Here's how we made it run.
Godot 4 has no terrain streaming, no asset LOD pipeline, and no distance-based loading. Our game is a 12km × 12km archipelago with 9 islands, 155 vegetation prototypes, and 2,000 simulated colonists. If you load everything at startup, you run out of VRAM before the player sees the main menu.
## The Problem
Here's how we built four streaming layers on top of Godot, all in C#, to make it work.
We're building a survival colony sim set across 9 islands. The total world is roughly 12km x 12km. Each island is 4km across with its own terrain heightmap, biome textures, vegetation prototypes, and building grids. The player can travel between islands by canoe.
## The scale problem
Godot 4 is a fantastic engine, but it wasn't designed for this scale. There's no terrain streaming, no asset LOD pipeline, no distance-based loading. If you load everything at startup, you run out of VRAM before the player sees the main menu. So we built four streaming layers on top of Godot, all in C#.
Each island is roughly 4km across with its own terrain heightmap, biome textures, vegetation, and building grids. The player travels between islands by canoe. At any given moment, only a small fraction of the world is visible — but Godot doesn't know that unless you tell it.
## Layer 1: Terrain Regions
We built four layers that teach Godot what to load, when to load it, and when to let it go.
We use **Terrain3D** for heightmaps --- a GDExtension that gives us a clipmap renderer with 7 LOD levels. Internally, Terrain3D divides each island into 512m x 512m regions. A 4km island has 64 regions. Across 9 islands, that's 576 regions total.
## Layer 1: Terrain regions (lazy instantiation)
The key insight: **don't create all 9 terrain nodes at startup.** Each node allocates a clipmap mesh, collision structures, and materials even when hidden. Our original code created all 9 in `_Ready()` and just toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited yet.
We use **Terrain3D** for heightmaps — a GDExtension that provides clipmap rendering with 7 LOD levels. Each island is split into 512m × 512m regions. A 4km island has 64 regions. Nine islands: 576 regions total.
The fix was lazy instantiation. We create the current island's terrain on startup and defer the rest. When the player gets in a canoe and sails to a new island, we create that island's terrain node on demand, import the heightmap, and start async texture loading --- all while a loading screen covers the transition.
The original code created all 9 terrain nodes in `_Ready()` and toggled visibility. This wasted hundreds of megabytes on islands the player hadn't visited. The fix: create the current island's terrain on startup, defer the rest. When the player sails to a new island, create that island's terrain node on demand, import the heightmap, start async texture loading all behind a loading screen.
## Layer 2: Vegetation Chunks (128m Grid)
## Layer 2: Vegetation chunks (128m grid)
This is the main prop streaming system. Every island's vegetation --- trees, rocks, grasses, shrubs --- is divided into a spatial grid of 128m x 128m chunks.
The main prop streaming system. Every island's vegetation is divided into a spatial grid of 128m × 128m chunks.
The camera position is checked every 0.5 seconds. When it crosses a chunk boundary, we calculate which chunks should be active within a 400m radius (roughly 39 chunks in a circle), `QueueFree` chunks that fell out of range, and build new chunks that entered range.
The camera position is checked every 0.5 seconds. When it crosses a chunk boundary, we calculate which chunks should be active within a 400m radius (~39 chunks), destroy chunks that fell out of range, and build new ones that entered. Each chunk groups vegetation by prototype, creates a **MultiMesh** per group, and places instances using height queries. A chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls — not 80 individual nodes.
Each chunk groups vegetation instances by prototype, creates a **MultiMesh** per group, and places instances using height queries. A chunk with 50 palm trees and 30 rocks becomes 2 MultiMesh draw calls, not 80 individual nodes.
The cache problem: vegetation meshes and materials are cached in dictionaries keyed by prototype name. These caches are append-only by default — visit all 9 islands and you accumulate every mesh variant permanently. The fix is island-scoped eviction. When the player leaves an island, we clear vegetation caches. They reload from disk on return, behind a loading screen.
### The cache problem
## Layer 3: Async resource loading
Vegetation meshes and materials are cached in dictionaries keyed by prototype name. The problem: these caches are **append-only**. Visit all 9 islands and you accumulate every mesh and material variant permanently. With 155 unique prototypes across the archipelago, that's a lot of GPU memory that never gets freed.
Godot's `GD.Load()` is synchronous. It blocks the main thread. During gameplay, the frame freezes.
The fix is island-scoped eviction. When the player leaves an island, we clear the vegetation caches. Meshes and materials for the departed island are released. If the player returns, they reload from disk. The loading screen covers this cost.
We audited the entire codebase and found **26 resource load calls across 13 files** — only 1 was async. The worst offender was `GetMeshForProto()` in the vegetation grid. As the player walks across a new island, every new vegetation prototype triggers a synchronous load. With 155 prototypes, the first traversal stutters visibly.
## Layer 3: Async Resource Loading
Two fixes:
Godot's `GD.Load()` is synchronous. It blocks the main thread. During gameplay, the frame freezes. We audited the entire codebase and found **26 resource load calls across 13 files**, and only 1 was async.
- **Pre-warm during loading screens.** When an island is imported, kick off background loads for all known prototypes. By the time the player gains control, most meshes are cached.
- **Async texture loading.** Terrain textures use `ResourceLoader.LoadThreadedRequest()` with `_Process()` polling. The terrain renders immediately with autoshader colors; biome textures pop in when ready.
The worst offender was `GetMeshForProto()` in the vegetation grid. As the player walks across an island for the first time, every new vegetation prototype triggers a synchronous load. With 155 prototypes, the first traversal stutters visibly.
The ResourceLoader trap: Godot maintains an internal resource cache. Every `GD.Load()` caches the result globally. If you load an FBX as a `PackedScene`, instantiate it to extract a mesh, then free the instance — the PackedScene **stays cached**. Rule: use `ResourceLoader.Load(path, "", CacheMode.Ignore)` for one-shot loads where you extract data and discard the container.
We fixed this in two ways:
## Layer 4: Entity rendering (event-driven)
- **Pre-warm during loading screens.** When an island is imported, we kick off background loads for all known prototypes. By the time the player gains control, most meshes are already cached.
- **Async loading for biome textures.** Terrain textures use `ResourceLoader.LoadThreadedRequest()` with `_Process()` polling. The terrain renders immediately with autoshader colours, and biome textures pop in when ready. The player never notices.
Dynamic entities — colonists, animals, buildings, VFX — update when the simulation pushes new state, not per frame.
### The ResourceLoader cache trap
On top of our own caches, Godot maintains an internal resource cache. Every `GD.Load()` call caches the result globally. There's no API to query the cache size or evict entries.
If you load an FBX as a `PackedScene`, instantiate it to extract a mesh, then free the instance --- the PackedScene **stays cached**. The mesh you extracted is fine (it's a Resource, not a Node), but the discarded scene wastes memory forever.
The rule: use `ResourceLoader.Load(path, "", CacheMode.Ignore)` for one-shot loads where you extract data and discard the container. Use `GD.Load()` only for things that should persist (shaders, shared textures).
## Layer 4: Entity Rendering
Dynamic entities --- colonists, animals, buildings, VFX --- are event-driven, not streamed. They update when the simulation pushes new state, not per frame.
- **Crowd rendering:** Single MultiMesh for up to 2000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20. No individual nodes, no per-frame allocation.
- **Animals:** One MultiMesh per type. Max 500 per type. Updates only on state change, not per frame.
- **Buildings:** Tracked by ID from sim state. `QueueFree` when removed. Self-cleaning.
- **Crowd rendering:** Single MultiMesh for up to 2,000 colonists. Positions lerped per frame from pre-allocated arrays. Labels distance-culled, capped at 20.
- **Animals:** One MultiMesh per type. Max 500 per type. Updates only on state change.
- **Buildings:** Tracked by ID from sim state. `QueueFree` when removed.
- **VFX:** Capped at 50 active particle systems. Worst case: 10,000 GPU particles.
## Memory Safety: Zero Leaks
## Memory safety: the QueueFree audit
We audited every `QueueFree()` call in the codebase --- 47 calls across 17 files. **Zero `RemoveChild()` calls without a corresponding `QueueFree()`.** Three patterns we follow everywhere:
We audited every `QueueFree()` call 47 calls across 17 files. **Zero `RemoveChild()` calls without a corresponding `QueueFree()`.** Three patterns we follow everywhere:
**Pattern 1: Chunk streaming** --- Deactivate out-of-range chunks by iterating the active dict, calling `QueueFree()`, collecting keys to remove, then removing them after iteration. Never modify a dictionary while iterating it.
1. **Chunk streaming:** Iterate active dict, call `QueueFree()`, collect keys to remove, then remove after iteration. Never modify a dictionary while iterating.
2. **Extract from PackedScene:** Instantiate, extract mesh, `QueueFree()` the temp instance. The mesh survives because it's a Resource, not a Node.
3. **UI rebuild:** `QueueFree()` all children, build new content. Safe because `QueueFree` is deferred — new children added in same frame before old ones freed.
**Pattern 2: Extract data from PackedScene** --- Instantiate a scene, extract the mesh, `QueueFree()` the temporary instance. The mesh survives because it's a Resource, not a Node.
## What runs every frame (and what doesn't)
**Pattern 3: UI rebuild** --- `QueueFree()` all children, then build new content. Safe because `QueueFree` is deferred --- new children are added in the same frame before old ones are freed.
`_Process()` is strictly limited:
## What Runs Every Frame
- Vegetation grid: camera chunk check (0.5s throttle, early-exit if same chunk)
- Terrain manager: poll async texture loads
- Crowd renderer: lerp 2,000 positions (math-only, pre-allocated arrays)
- Day/night: rotate sun
- Camera: follow + zoom
- Sim bridge: drain WebSocket message queue
We're strict about what goes in `_Process()`:
**No heap allocation in any of these.** Per-frame overhead is dominated by the crowd lerp and message queue drain.
- **Vegetation grid:** Camera chunk check (0.5s throttle, early-exits if same chunk)
- **Terrain manager:** Poll async texture loads (loop pending list, check status)
- **Crowd renderer:** Lerp 2000 colonist positions (math-only, pre-allocated arrays)
- **Day/night:** Rotate sun, adjust light energy
- **Camera:** Follow + zoom smoothing
- **Sim bridge:** Drain WebSocket message queue
Two shaders to watch: the ocean shader (4 Gerstner waves, depth reconstruction, caustics, foam — heaviest thing in the pipeline) and the wind sway shader (6 trig ops per vertex on every vegetation mesh within 400m). Future optimization: disable sway on distant chunks.
No heap allocation in any of these. Total per-frame overhead is dominated by the crowd lerp and the message queue drain.
## Target: RTX 3060, 8GB VRAM
## Shaders We Watch
- Main island + full vegetation < 4GB VRAM → ship it
- Approaching 6-8GB → implement lazy terrain nodes + cache eviction
- Exceeding 8GB → implement vegetation LOD and region-level streaming
Two custom shaders are performance-sensitive:
**Ocean shader** --- 4 Gerstner wave calculations in the vertex stage, applied to a 12,000m plane. Fragment stage does depth reconstruction, caustics, foam masking, and two normal map lookups. It's the heaviest thing in the render pipeline. We pre-warm it during the loading screen to avoid shader compilation stutter.
**Wind sway shader** --- 6 trig ops per vertex on every vegetation mesh within 400m. The sway is invisible beyond 100m but the shader runs at full cost regardless. Future optimisation: disable sway on distant chunks.
## The Target: RTX 3060
Our early access target is an RTX 3060 with 8GB VRAM:
- Main island + full vegetation < 4GB VRAM --- ship it, we have headroom
- Approaching 6--8GB --- implement lazy terrain nodes + cache eviction
- Exceeding 8GB --- implement vegetation LOD and region-level streaming
**Always measure before optimising.** We added VRAM logging before writing a single line of optimisation code. Half the "problems" we expected turned out to be non-issues. The other half were worse than expected. Profiling isn't optional.
**Always measure before optimizing.** 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.
---
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 your resource loading, and be disciplined about what runs per frame. The engine gives you the primitives --- MultiMesh, `LoadThreadedRequest`, `QueueFree` --- and it's up to you to wire them into a system that scales.
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, `LoadThreadedRequest`, `QueueFree`. It's up to you to wire them into a system that scales.
We're building with these systems and developing the game using [Tinqs Studio](https://tinqs.com). If you're building something large-scale in Godot, we hope this is useful.
*We're building [Ariki](https://arikigame.com), a survival colony sim, with these systems. The tools we use — git hosting, AI agents, creative pipelines — are part of [Tinqs Studio](https://tinqs.com).*
-122
View File
@@ -1,122 +0,0 @@
---
title: "Image Generation at Every Price Point with fal.ai"
slug: image-generation-fal
date: "2026-05-25"
description: "We generate concept art, logos, icons, and trailer frames through a single API proxy. Here's how we pick between 12 models spanning $0.002 to $0.09 per image."
og_description: "One proxy, 12 models, $0.002 to $0.09 per image. How we pick."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "We generate concept art, logos, icons, and trailer frames through a single API proxy. Here's how we pick between 12 models spanning $0.002 to $0.09 per image."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
We generate every visual asset for Ariki --- concept art, app icons, trailer frames, logo variants, Steam capsules --- through a single inference proxy that routes to fal.ai. No Photoshop. No Midjourney subscription. Just API calls at prices that range from $0.002 to $0.09 per image. Here's how we decide which model gets which job.
## The Setup
Our [Tinqs Studio](https://tinqs.com) platform includes an inference proxy that sits between agents and model providers. When an agent (or a human in Cursor) says "generate an image," the proxy routes the request to fal.ai, handles authentication, tracks usage per user, and persists the result to S3. The caller doesn't care which model runs --- they describe what they want, and the proxy picks or the caller specifies.
```
Agent describes what it wants
→ tinqsProxy receives generate_image call
→ Routes to fal.ai with the specified model
→ Image generated, persisted to S3
→ Permanent URL returned to caller
```
One API key. One billing account. Access to every model fal.ai hosts. That's the pitch of aggregator platforms, and fal.ai delivers on it.
## The Tiers
Not every image needs the best model. A throwaway mockup doesn't justify $0.09. A final logo doesn't deserve $0.002. We split our usage into four tiers.
### Best Quality --- Final Art
For images that ship --- hero art, app icons, trailer keyframes, print-ready designs --- we use three models depending on the content:
**Flux 2 Pro** ($0.03/megapixel, ~15 seconds). Our default. Best all-round quality for concept art, character illustrations, environment paintings, and anything that doesn't need text. Handles complex prompts with multiple elements well. Rarely fails.
**Ideogram v3 Quality** ($0.09, ~12 seconds). The only model that renders text reliably inside images. When we need a poster with a tagline, a sign in a game scene, or a logo with readable letters, this is the only option. The QUALITY tier is expensive but worth it --- text at lower tiers gets blurry.
**Recraft v3** ($0.04 raster, $0.08 vector, ~10 seconds). Built for commercial design. Clean lines, consistent style, and the only model on fal.ai that outputs SVG vectors. When we need brand assets, packaging mockups, or anything that might end up in print, Recraft produces work that doesn't need cleanup.
### Mid Tier --- Everyday Work
For images that are good enough for internal review, social posts, or documentation:
**Ideogram v3 Balanced** ($0.06, ~8 seconds). Typography quality between Turbo and Quality. Good for marketing materials where text matters but perfection doesn't.
**Seedream v4.5** ($0.04, ~8 seconds). Google's model on fal.ai. Photorealistic scenes and product shots. Different aesthetic from Flux --- slightly more photographic, less painterly.
**Flux Dev** ($0.025, ~10 seconds). The open-weight Flux variant. Good quality, and the base for LoRA fine-tuning if you want to train on your own style. We use it when we need custom-trained models later.
### Low Cost --- Drafts and Exploration
For iteration, A/B testing, and throwing things at the wall:
**Flux Schnell** ($0.003/megapixel, ~3 seconds). The workhorse for exploration. When we're figuring out composition, trying different camera angles, or generating 20 variants to pick one direction --- Schnell. A hundred images costs $0.30. You can afford to be wasteful.
**SDXL Lightning** (~$0.002, ~2 seconds). The absolute cheapest option. Lower quality than Schnell, but when you need 50 thumbnails to test a layout grid or generate placeholder textures, quality doesn't matter. Two cents for ten images.
### Specialised --- Editing and Post-Processing
For modifying existing images rather than generating new ones:
**Flux Kontext** (~$0.04, ~12 seconds). Context-aware editing. Give it an image and say "change the wood to marble" or "make it sunset lighting." Preserves composition while changing style or material. Useful for quick style transfers without regenerating from scratch.
**Nano Banana Edit** ($0.039, ~12 seconds). Image-to-image restyle. We use this for our logo variant pipeline --- take one carved-wood Ariki logo and produce versions in mahogany, pearl, obsidian, coral, gold. It's better than Kontext at preserving fine detail in complex images.
**BiRefNet** ($0.001, ~3 seconds). Background removal. Produces clean alpha cutouts from any image. We pair it with every logo and icon generation --- generate with a white background, then cut it out. A dollar gets you a thousand cutouts.
## How We Actually Use Them
### The Schnell-to-Pro Pipeline
We never start with the expensive model. Every generation session follows the same pattern:
1. **Explore with Schnell** ($0.003) --- 10-20 variants, different angles, compositions, color palettes. Total: $0.03-0.06.
2. **Pick 2-3 directions.** Human looks at the grid, picks the promising ones.
3. **Refine with Flux 2 Pro** ($0.03) --- regenerate the winners at full quality with refined prompts. Total: $0.06-0.09.
4. **Post-process** --- BiRefNet for background removal ($0.001), maybe Recraft for a vector version ($0.08).
A full session --- from blank canvas to final assets --- costs under $0.20. That's the price of a single Midjourney generation on their Pro plan.
### Logo Variants at Scale
Our Ariki logo has 18 material variants --- deep mahogany, mother-of-pearl, obsidian, molten lava, bronze with verdigris, tapa cloth, and more. Each one generated with Nano Banana Edit ($0.039) + BiRefNet ($0.001) for transparency. Total cost for 18 variants: **$0.72**. A designer would quote hundreds of dollars and a week of work for the same output.
### Typography That Works
Every model except Ideogram fails at text. Flux will give you beautiful art with garbled letters. Recraft gets close but isn't consistent. SDXL doesn't try. If the image has words in it, Ideogram v3 is the only answer. We've learned to accept the $0.09 cost for text-heavy images rather than wasting $0.30 on ten failed Flux attempts.
## The Numbers
Over the past month:
| Category | Images | Total Cost | Avg Cost/Image |
|----------|--------|-----------|----------------|
| Concept art (flux-2-pro) | ~120 | $3.60 | $0.03 |
| Exploration drafts (schnell) | ~400 | $1.20 | $0.003 |
| Logo variants (nano-banana) | 18 | $0.72 | $0.04 |
| Icons (nano-banana + birefnet) | 30 | $1.20 | $0.04 |
| Typography (ideogram) | ~25 | $1.50 | $0.06 |
| Background removal (birefnet) | ~80 | $0.08 | $0.001 |
| **Total** | **~673** | **$8.30** | **$0.012** |
Six hundred images for eight dollars. The infrastructure to route, authenticate, and persist them costs more than the generation itself.
## What We Learned
**Never iterate on expensive models.** The Schnell-to-Pro pipeline saves 10x. Most of the creative work happens at $0.003/image. The expensive model just polishes the decision you already made.
**Typography is a solved problem --- but only on one model.** Stop trying to make Flux render text. Use Ideogram v3 Quality for anything with words. Accept the cost.
**Vector output is underrated.** Recraft v3's SVG export means logos and icons scale to any size without artifacts. For anything that might end up on a billboard or a business card, pay the $0.08 for vector.
**Background removal is basically free.** At $0.001 per image, there's no reason to ever manually mask anything. Run BiRefNet on everything, keep both versions.
**Aggregation beats loyalty.** No single model is best at everything. Flux for art, Ideogram for text, Recraft for design, Nano Banana for edits, BiRefNet for masks. The proxy pattern lets us use the right tool for each job without managing five API keys and five billing accounts.
---
*Image generation is built into [Tinqs Studio](https://tinqs.com) --- our Gitea-based platform for game teams. Every model above is available through the same inference proxy that handles LLM calls, authenticated with the same Gitea token. We're building [Ariki](https://arikigame.com) with these tools, and every asset in the game touched at least one of them.*
-103
View File
@@ -1,103 +0,0 @@
---
title: "Pi as CI Integrator: Agents That Fix Their Own Builds"
slug: pi-ci-integrator
date: "2026-05-25"
description: "Most coding agents stop at git push. Our Pi fork watches CI, reads failure logs, and fixes its own code until the pipeline goes green."
og_description: "Coding agents that watch CI and fix their own builds."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "Most coding agents stop at git push. Our Pi fork watches CI, reads failure logs, and fixes its own code until the pipeline goes green."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
Most coding agents have a dirty secret: they don't care if the code compiles. They write, they push, they walk away. The human discovers the broken build an hour later. We built a Pi extension that closes the loop --- agents that watch CI, read failure logs, and fix their own mistakes.
## The Gap
Every agent demo looks the same. The AI writes code, commits, pushes. The presenter says "and now we have a pull request!" Cut. End of demo.
What happens next? The CI pipeline runs. Tests fail. Linting screams. The build breaks because someone forgot an import. A human opens the PR, reads the red badge, clicks into the logs, finds the error, fixes it, pushes again. The agent did 90% of the work but left the last 10% --- the most tedious part --- for a person.
We wanted agents that finish the job.
## The tinqs-ci Extension
Our [Pi fork](https://tinqs.com/tinqs/pi) has a `tinqs-ci` extension --- a single TypeScript file, about 200 lines --- that gives the agent three tools:
- **ci_status** --- checks the current pipeline state for a branch (pending, running, success, failure)
- **ci_logs** --- fetches the full build log from the most recent failed run
- **ci_wait** --- polls the pipeline every 15 seconds until it finishes, then returns the result
These are Gitea Actions API calls under the hood. The agent authenticates with the same PAT it uses for git push. No extra credentials, no special CI service account.
## The Loop
Here's what a Pi task looks like end to end:
```
Agent receives task brief
→ reads codebase, plans approach
→ writes code
→ runs local tests (bash tool)
→ commits and pushes branch
→ calls ci_wait
→ CI passes → opens PR via Gitea API
→ CI fails → calls ci_logs
→ reads error output
→ fixes the issue
→ pushes again
→ calls ci_wait again
→ repeats until green (max 3 retries)
```
The key is that `ci_logs` returns the raw build output --- compiler errors, test failures, lint violations --- as plain text in the agent's context. DeepSeek V4 is surprisingly good at reading build logs. It parses a Go compiler error, identifies the file and line, and fixes it. It reads a test assertion failure, understands what the test expected, and corrects the implementation.
Three retries is the hard limit. If the agent can't fix it in three rounds, it opens the PR anyway with a comment explaining what failed and why. A human takes over from there. In practice, most failures resolve on the first retry --- it's usually a missing import or a type mismatch.
## What This Actually Looks Like
A real run from last week. The task: add a health check endpoint to a Go service.
- **Turn 1:** Agent reads the codebase, writes the handler and test, pushes. CI fails --- the test imports a package that doesn't exist on the runner.
- **Turn 2:** Agent reads `ci_logs`, sees the `go: module not found` error, adds the missing `go.mod` replace directive, pushes. CI passes.
- **Turn 3:** Agent opens PR with passing checks.
Total time: 4 minutes. Total cost: $0.06. No human touched the keyboard.
Without the CI extension, this would have been a PR with a red badge and a Slack message saying "hey, the agent's PR is broken again." Someone would have context-switched, opened the logs, seen the trivial error, fixed it, and lost 20 minutes of flow state.
## Why This Matters More Than You Think
CI integration isn't a feature. It's the difference between an agent that helps and an agent that creates work.
An agent that pushes broken code is worse than no agent at all. It creates a false sense of progress --- "the PR is up!" --- while actually adding a task to someone's plate. Every broken PR is an interruption. Every interruption costs 15 minutes of context-switching.
An agent that watches CI and fixes its own builds is genuinely autonomous. You submit a task, you walk away, you come back to a green PR ready for review. The agent handled the mechanical iteration that a human would have done anyway --- the fix-push-wait-check cycle that eats hours of developer time every week.
## The Guardrail Problem
Letting an agent retry its own builds sounds dangerous. What if it enters an infinite loop? What if it starts making increasingly wild changes to get the build to pass?
Three safeguards:
**Retry limit.** Three attempts maximum. After that, the agent stops and reports. This is a hard limit in the orchestrator, not a suggestion to the model.
**Diff budget.** Each retry can only touch files that were already in the original changeset. The agent can't "fix" a build failure by rewriting the test suite or disabling the linter. If the fix requires touching new files, it fails and escalates.
**Hallucination detection.** The guardrail extension monitors every turn. If the agent claims "the build passed" without having called `ci_status` or `ci_wait`, it gets corrected. Agents are not allowed to guess the CI result.
## The Numbers
Over three weeks of running the orchestrator:
- **87 tasks** completed end-to-end
- **23 tasks** needed at least one CI retry (26%)
- **19 of those 23** resolved on the first retry
- **4 tasks** hit the retry limit and escalated to a human
- **0 tasks** produced a merged PR that later broke something else
The 26% retry rate tells you how often agents push code that doesn't build on the first try. That's not a bad number --- it's the same rate you'd see from a junior developer. The difference is the agent fixes it in 30 seconds instead of 20 minutes.
---
*The CI extension is part of our [Pi fork](https://tinqs.com/tinqs/pi), which runs inside [Tinqs Studio](https://tinqs.com) --- a Gitea-based platform for game development with built-in AI agents. The whole thing is MIT licensed.*
+41 -80
View File
@@ -2,31 +2,23 @@
title: "A Pre-Commit Agent That Guards Your Secrets for $0.001"
slug: pre-commit-agent
date: "2026-05-25"
description: "We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked secrets, classified terms, broken URLs, and drafts announcements --- for a tenth of a cent per commit."
description: "We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked API keys, classified terms, broken URLs, and drafts announcements for a tenth of a cent per commit."
og_description: "A DeepSeek-powered pre-commit hook that catches leaks for $0.001/commit."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked secrets, classified terms, and broken URLs --- for a tenth of a cent."
excerpt: "Too many things to remember before hitting commit. Don't leak API keys. Don't reference classified codenames. Don't link to deleted repos. We built a two-layer pre-commit hook — regex + LLM — that catches all of it for $0.001."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
We have a problem that every small team has: too many things to remember before hitting commit. Don't leak API keys. Don't reference the classified AI codename in public blog posts. Don't link to GitHub repos we deleted six months ago. Don't push a blog post with a 90-character title. We built a pre-commit hook that uses a cheap LLM to check all of this automatically --- and it costs less than a tenth of a cent per commit.
Every small team has the same problem: too many things to remember before `git commit`. 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.
## The Problem
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 maintain a docs repo that serves double duty. Internal files --- game design documents, architecture notes, agent configuration --- sit alongside a public blog and website. The internal side references classified codenames, machine hostnames, and internal URLs. The public side must never contain any of that.
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.
We also deleted all our GitHub repos in April 2026 and moved everything to our own Gitea platform. But old links keep creeping back in --- someone copies a URL from an old document, a blog post references the wrong remote. These are invisible bugs. The blog looks fine, the build passes, and three weeks later someone notices a dead link.
## Layer 1: Regex blocklist (0ms, $0.00)
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 GitHub org. We needed something automatic, fast, and cheap enough to run on every single commit.
## Two Layers: Regex + Agent
The hook has two layers. The first is instant and free. The second is smart and nearly free.
### Layer 1: Local Blocklist (0ms, $0.00)
A text file of regex patterns, each tagged with a scope and a message:
A text file of patterns, each tagged with scope and message:
```
public|\bCosmos\b|Classified codename — use "advanced colonist AI"
@@ -36,60 +28,48 @@ all|AKIA[A-Z0-9]{16}|AWS access key leaked
public|admin\.arikigame\.com|Internal admin URL in public content
```
The scope field controls where the pattern is enforced. `all` means every file. `public` means only files under `website/` --- our public-facing content. This is critical. We *want* classified codenames in our internal architecture docs. We just don't want them in blog posts.
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.
The blocklist runs grep against the staged diff. No network call, no API, no latency. If it finds a match, the commit is blocked immediately with a file path and explanation. This catches 80% of issues before the LLM ever wakes up.
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)
## Layer 2: DeepSeek V4 Flash review (~4s, $0.001)
If the commit touches public-facing files (`website/`, blog posts), the hook sends the staged diff to DeepSeek V4 Flash through our inference proxy. The system prompt tells the model exactly what to check:
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:
- **Leaked secrets** --- API keys, tokens, credentials that the regex might have missed
- **Classified terms** --- codenames that aren't in the blocklist yet
- **Internal URLs** --- references to internal services that shouldn't be public
- **Blog quality** --- title length, meta description, slug consistency, missing fields
- **Broken links** --- malformed URLs, obvious typos
- **Announcements** --- if it's a new blog post, draft a one-line summary
- **Leaked secrets** API keys, tokens, credentials the regex might have missed
- **Classified terms** codenames not yet in the blocklist
- **Internal URLs** references to services that shouldn't be public
- **Blog quality** title length, meta description, slug consistency
- **Broken links** malformed URLs, obvious typos
- **Announcements** if it's a new blog post, draft a one-line summary
The model responds with structured JSON: errors (block the commit) or warnings (inform but allow). If the API is unreachable or times out, the commit proceeds --- the hook never blocks work for infrastructure reasons.
The model responds with structured JSON: `errors` (block) or `warnings` (inform but allow). If the API is unreachable or times out, the commit proceeds the hook never blocks work for infrastructure reasons.
## Why Not Pi?
Our [Pi fork](https://tinqs.com/tinqs/pi) is a full coding agent with tool calling, file I/O, and context management. It's what we use for overnight autonomous coding. But for pre-commit review, it's overkill.
A pre-commit hook needs to finish in under 5 seconds. Pi takes 2--3 seconds just to start the Node.js process and load extensions. The review itself is a single LLM call with a system prompt and a diff --- no tools needed, no file reads, no iteration. A direct curl to DeepSeek is faster and simpler.
That said, the hook is designed as a stepping stone. The blocklist patterns, the review prompt, and the classification logic are all reusable. When we build a Pi-based CI reviewer that runs on pull requests --- with tool access to read the full file, check links live, and verify image URLs --- it will use the same prompt and the same patterns. The pre-commit hook is the fast, cheap first pass. Pi is the thorough second pass.
## The Architecture
## The architecture
```
git commit
.githooks/pre-commit (bash)
Phase 0: Collect staged diff + classify files
Phase 0: Collect staged diff + classify files (public vs internal)
Phase 1: Regex blocklist scan (instant, free)
→ Match found → BLOCK (exit 1)
→ Match → BLOCK
→ Clean → continue
Phase 2: Public files changed?
→ No → exit 0 (skip AI, no cost)
→ No → exit 0 (skip AI review, zero cost)
→ Yes → send diff to DeepSeek V4 Flash
Phase 3: Parse JSON response
→ Errors → BLOCK (exit 1)
→ Errors → BLOCK
→ Warnings → print, exit 0
→ Announcement → print draft
→ API failure → warn, exit 0
→ API failure → warn, exit 0 (never block on infra)
```
The hook lives in `.githooks/` inside the repo --- committed, version-controlled, shared by the whole team. A setup script configures `git config core.hooksPath` to point there. The LFS pre-push hook sits in the same directory.
The hook lives in `.githooks/` committed, version-controlled, shared by the team. A setup script points `git config core.hooksPath` there.
## What It Costs
The system prompt is ~500 tokens. An average diff is 2,000--4,000 tokens. The response is ~200 tokens. At DeepSeek V4 Flash rates:
## What it costs
| | Tokens | Cost |
|--|--------|------|
@@ -97,53 +77,34 @@ The system prompt is ~500 tokens. An average diff is 2,000--4,000 tokens. The re
| Output (JSON response) | ~200 | $0.00006 |
| **Per commit** | | **$0.00062** |
Call it a tenth of a cent. Twenty commits a day across the team: **$0.012/day**. About **$0.40/month**.
A tenth of a cent. Twenty commits a day: $0.012/day. About **$0.40/month**. Commits that only touch internal files skip the AI review entirely — zero cost.
Commits that only touch internal files (architecture docs, agent config, game design) skip the AI review entirely. Zero cost. The hook only calls DeepSeek when public-facing content changes.
## What it caught (first week)
## What It Catches
- **2 classified codename leaks** in draft blog posts — caught by blocklist
- **1 GitHub URL** from an old copy-paste — caught by blocklist
- **3 blog SEO warnings** — titles over 60 chars, missing og_description — caught by AI
- **1 announcement draft** auto-generated when a new post was committed
In the first week:
- **2 classified codename leaks** in draft blog posts --- caught by the blocklist before the AI even ran
- **1 GitHub URL** that crept back in from a copy-paste --- caught by the blocklist
- **3 blog SEO warnings** --- titles over 60 characters, missing og_description --- caught by the AI review
- **1 announcement draft** generated automatically when a new blog post was committed
Zero false positives on the blocklist (the patterns are specific enough). Two false positives from the AI (flagged an internal URL in a code example that was clearly illustrative, not a real link). We added a note to the prompt to ignore URLs inside fenced code blocks.
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
One command per machine:
```bash
bash scripts/setup-hooks.sh
bash scripts/setup-hooks.sh # or .\scripts\setup-hooks.ps1 on Windows
export TINQS_HOOK_TOKEN=<your-token> # same PAT used for git push
```
Or on Windows:
That's it. Every `git commit` runs the two-layer review. Bypass with `git commit --no-verify` for emergencies.
```powershell
.\scripts\setup-hooks.ps1
```
## The pattern: guard rails at the edge
Set your inference token:
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.
```bash
export TINQS_HOOK_TOKEN=<your-gitea-pat>
```
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.
That's it. Every `git commit` now runs the two-layer review. Bypass with `git commit --no-verify` when you need to (emergencies, known false positives).
## The Pattern: Guard Rails at the Edge
This is the same pattern 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 worth of prevention. A leaked API key in a public blog post is hours of rotation, revocation, and audit. A classified codename in a public post is a confidentiality breach. A dead GitHub link is a broken user experience that 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. The total infrastructure cost is zero --- it runs on the developer's machine, calls an API we already pay for, and adds 4 seconds to the commit flow.
The age of agents doesn't just mean agents that write code. It means agents that watch the code you write.
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.
---
*The pre-commit hook is part of [Tinqs Studio](https://tinqs.com), our open platform for game development. The inference proxy, the blocklist patterns, and the review prompt are all open and reusable. We're building [Ariki](https://arikigame.com) with these tools --- every commit in the game repo runs through the same guard.*
*The pre-commit hook is part of [Tinqs Studio](https://tinqs.com). The inference proxy, blocklist patterns, and review prompt are open and reusable. Every commit in [Ariki](https://arikigame.com) runs through the same guard.*
+32 -40
View File
@@ -1,76 +1,68 @@
---
title: "One Binary to Rule Them All: Building a Studio CLI"
title: "One Binary to Rule Them All: Our Studio CLI"
slug: studio-cli
date: "2026-05-18"
description: "A single Go binary that handles machine identity, screenshots, cloud vision, and health checks. The glue that makes AI agents useful across a multi-machine game studio."
og_description: "A single Go binary for machine identity, screenshots, cloud vision, and AI agent coordination."
description: "A single Go binary that gives AI agents full context about your machine, project, and services in 100ms. Screenshots, cloud vision, health checks — one install, every machine."
og_description: "One Go binary for machine identity, screenshots, cloud vision, and AI agent context."
og_image: "https://www.tinqs.com/img/og-cover.jpg"
excerpt: "A single Go binary that gives AI agents context about who you are, what machine you're on, and what services are reachable. Screenshots, cloud vision, health checks --- one install, every machine."
excerpt: "Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, and runs health checks — in 100ms."
author: "Ozan Bozkurt"
author_initials: "OB"
author_role: "CTO & Developer, Tinqs"
---
Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, and runs health checks. This is the glue that makes AI agents actually useful in a multi-machine game studio.
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.
## Why Build a CLI
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.
When you have multiple machines across several people, two operating systems, and AI agents that need context about the environment they're running in, the glue becomes the hardest part. Which machine is this? What services are reachable? Is the game running? Can I take a screenshot of what the developer is looking at?
## The identity command (100ms)
We tried shell scripts. A `setup.sh` for Mac, a `setup.ps1` for Windows, a `check-services.sh` for health checks, a `screenshot.py` that never worked on Windows. They drifted. They broke. Nobody updated them.
When an agent starts, the first thing it calls is `tstudio identity`. The output:
So we built one Go binary that does everything.
- **Soul file** — the agent's persistent identity, values, operating principles
- **Company context** — team members, roles, what the company does
- **Machine context** — hostname, OS, which repos are cloned, what services are running
- **Ecosystem** — other repos and their purpose
- **Service status** — which URLs are live and reachable
## The Identity System
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.
The most important command is `identity`. When an AI agent starts a new session --- Cursor, Claude Code, any tool --- the first thing it does is call this command. The output tells the agent:
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.
- **The soul file** --- the agent's persistent identity, values, and operating principles
- **Company context** --- team members, roles, what the company does
- **Machine context** --- hostname, OS, which repos are cloned, what services are running
- **Ecosystem** --- other repos and their purpose
- **Service status** --- which URLs are live and reachable
## Screenshots and cloud vision
This solves a fundamental problem with AI agents: **cold starts.** Every new chat window, every new agent tab, every new session is a blank slate. The agent doesn't know what project this is, who's asking, or what infrastructure exists. One CLI call gives it full context.
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.
The data lives in markdown files in the docs repo --- the source of truth. Any machine on the network can read it.
A `photo` 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."
## Screenshots and Vision
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.
The CLI can capture any window from outside the process. No in-game overlay, no rendering pipeline integration. It uses the OS-level window capture API --- works on Windows (via GDI+) and Mac (via screencapture).
## Health checks
A `photo` command does the same thing but sends the screenshot to a cloud vision model for analysis. The agent says "take a photo of the game" and gets back a structured description: "The player character is standing near a half-built hut. There are 3 palm trees to the left. The terrain has a visible seam between two biomes."
`tstudio doctor` runs a comprehensive check:
This is how you file bugs without typing. Look at the game, tell the agent what's wrong, and the agent takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached.
## Health Checks
A `doctor` command runs a comprehensive health check:
- Is the git platform reachable? Can we authenticate?
- 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 and at the right version?
- Are required tools installed at the right version?
The output is a green/yellow/red table. If something's wrong, the agent knows immediately and can diagnose or escalate. This is essential for unattended agent sessions --- the agent can verify its environment before starting work.
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 with no runtime dependencies. 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.
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, done.
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.
The binary is 15MB. It starts in under 100ms. It has zero runtime dependencies. For a tool that AI agents call on every session start, speed matters.
## What we learned
## 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 `tstudio 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.** When we started, this was a convenience tool for humans. It became the primary interface for AI agents. The `identity` command was originally "nice to have" --- now it's the single most important function in our stack. Every agent session starts with it.
**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.
**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 its dependencies baked in. It doesn't care if your Python is 3.9 or 3.12.
**Cloud vision is underrated for game dev.** 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.
**Cloud vision is underrated for game dev.** Sending a screenshot to a vision model and getting back a structured description 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 write when the AI is looking at the same screen you are.
**Agent cold starts are the real problem.** Without the identity system, every new session starts with the agent asking "what project is this?" and the human re-explaining context. With it, the agent knows everything in 100ms. That's the difference between an AI assistant and an AI team member.
**Agent cold starts are the real problem.** 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.
---
The CLI is part of [Tinqs Studio](https://tinqs.com) --- our game development platform that brings git hosting, AI agent tools, and team workflows together. Every time we find ourselves writing 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.
*The CLI is part of [Tinqs Studio](https://tinqs.com). 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.*
+255 -130
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A Pre-Commit Agent That Guards Your Secrets for $0.001 — Tinqs Blog</title>
<meta name="description" content="We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked secrets, classified terms, broken URLs, and drafts announcements --- for a tenth of a cent per commit.">
<meta name="description" content="We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked API keys, classified terms, broken URLs, and drafts announcements for a tenth of a cent per commit.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/pre-commit-agent">
@@ -35,144 +35,302 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked secrets, classified terms, broken URLs, and drafts announcements --- for a tenth of a cent per commit."
"description": "We built a pre-commit hook that calls DeepSeek V4 Flash to review every commit. It catches leaked API keys, classified terms, broken URLs, and drafts announcements for a tenth of a cent per commit."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; 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">We have a problem that every small team has: too many things to remember before hitting commit. Don't leak API keys. Don't reference the classified AI codename in public blog posts. Don't link to GitHub repos we deleted six months ago. Don't push a blog post with a 90-character title. We built a pre-commit hook that uses a cheap LLM to check all of this automatically &mdash; and it costs less than a tenth of a cent per commit.</p>
<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>
<div class="post__body">
<h2>The Problem</h2>
<p>We maintain a docs repo that serves double duty. Internal files &mdash; game design documents, architecture notes, agent configuration &mdash; sit alongside a public blog and website. The internal side references classified codenames, machine hostnames, and internal URLs. The public side must never contain any of that.</p>
<p>We also deleted all our GitHub repos in April 2026 and moved everything to our own Gitea platform. But old links keep creeping back in &mdash; someone copies a URL from an old document, a blog post references the wrong remote. These are invisible bugs. The blog looks fine, the build passes, and three weeks later someone notices a dead link.</p>
<p>A checklist in the README doesn't work. Humans skip checklists. Code review catches some issues but not all &mdash; reviewers focus on logic, not whether a URL points to a deleted GitHub org. We needed something automatic, fast, and cheap enough to run on every single commit.</p>
<h2>Two Layers: Regex + Agent</h2>
<p>The hook has two layers. The first is instant and free. The second is smart and nearly free.</p>
<h3>Layer 1: Local Blocklist (0ms, $0.00)</h3>
<p>A text file of regex patterns, each tagged with a scope and a message:</p>
<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\.arikigame\.com|Internal admin URL in public content</code></pre>
<p>The scope field controls where the pattern is enforced. <code>all</code> means every file. <code>public</code> means only files under <code>website/</code> &mdash; our public-facing content. This is critical. We <em>want</em> classified codenames in our 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. If it finds a match, the commit is blocked immediately with a file path and explanation. This catches 80% of issues before the LLM ever wakes up.</p>
<h3>Layer 2: DeepSeek V4 Flash Review (~4s, $0.001)</h3>
<p>If the commit touches public-facing files (<code>website/</code>, blog posts), the hook sends the staged diff to DeepSeek V4 Flash through our inference proxy. The system prompt tells the model exactly what to check:</p>
<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> &mdash; API keys, tokens, credentials that the regex might have missed</li>
<li><strong>Classified terms</strong> &mdash; codenames that aren't in the blocklist yet</li>
<li><strong>Internal URLs</strong> &mdash; references to internal services that shouldn't be public</li>
<li><strong>Blog quality</strong> &mdash; title length, meta description, slug consistency, missing fields</li>
<li><strong>Broken links</strong> &mdash; malformed URLs, obvious typos</li>
<li><strong>Announcements</strong> &mdash; if it's a new blog post, draft a one-line summary</li>
<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: errors (block the commit) or warnings (inform but allow). If the API is unreachable or times out, the commit proceeds &mdash; the hook never blocks work for infrastructure reasons.</p>
<h2>Why Not Pi?</h2>
<p>Our <a href="https://tinqs.com/tinqs/pi" style="color: var(&ndash;c-accent-l);">Pi fork</a> is a full coding agent with tool calling, file I/O, and context management. It's what we use for overnight autonomous coding. But for pre-commit review, it's overkill.</p>
<p>A pre-commit hook needs to finish in under 5 seconds. Pi takes 2&ndash;3 seconds just to start the Node.js process and load extensions. The review itself is a single LLM call with a system prompt and a diff &mdash; no tools needed, no file reads, no iteration. A direct curl to DeepSeek is faster and simpler.</p>
<p>That said, the hook is designed as a stepping stone. The blocklist patterns, the review prompt, and the classification logic are all reusable. When we build a Pi-based CI reviewer that runs on pull requests &mdash; with tool access to read the full file, check links live, and verify image URLs &mdash; it will use the same prompt and the same patterns. The pre-commit hook is the fast, cheap first pass. Pi is the thorough second pass.</p>
<h2>The Architecture</h2>
<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
.githooks/pre-commit (bash)
Phase 0: Collect staged diff + classify files
Phase 0: Collect staged diff + classify files (public vs internal)
Phase 1: Regex blocklist scan (instant, free)
→ Match found → BLOCK (exit 1)
→ Match → BLOCK
→ Clean → continue
Phase 2: Public files changed?
→ No → exit 0 (skip AI, no cost)
→ No → exit 0 (skip AI review, zero cost)
→ Yes → send diff to DeepSeek V4 Flash
Phase 3: Parse JSON response
→ Errors → BLOCK (exit 1)
→ Errors → BLOCK
→ Warnings → print, exit 0
→ Announcement → print draft
→ API failure → warn, exit 0</code></pre>
<p>The hook lives in <code>.githooks/</code> inside the repo &mdash; committed, version-controlled, shared by the whole team. A setup script configures <code>git config core.hooksPath</code> to point there. The LFS pre-push hook sits in the same directory.</p>
<h2>What It Costs</h2>
<p>The system prompt is ~500 tokens. An average diff is 2,000&ndash;4,000 tokens. The response is ~200 tokens. At DeepSeek V4 Flash rates:</p>
→ 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>|&ndash;|&mdash;&mdash;&ndash;|&mdash;&mdash;|</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>Call it a tenth of a cent. Twenty commits a day across the team: <strong>$0.012/day</strong>. About <strong>$0.40/month</strong>.</p>
<p>Commits that only touch internal files (architecture docs, agent config, game design) skip the AI review entirely. Zero cost. The hook only calls DeepSeek when public-facing content changes.</p>
<h2>What It Catches</h2>
<p>In the first week:</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 &mdash; caught by the blocklist before the AI even ran</li>
<li><strong>1 GitHub URL</strong> that crept back in from a copy-paste &mdash; caught by the blocklist</li>
<li><strong>3 blog SEO warnings</strong> &mdash; titles over 60 characters, missing og_description &mdash; caught by the AI review</li>
<li><strong>1 announcement draft</strong> generated automatically when a new blog post was committed</li>
<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 (the patterns are specific enough). Two false positives from the AI (flagged an internal URL in a code example that was clearly illustrative, not a real link). We added a note to the prompt to ignore URLs inside fenced code blocks.</p>
<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>
<p>One command per machine:</p>
<pre><code class="language-bash">bash scripts/setup-hooks.sh</code></pre>
<p>Or on Windows:</p>
<pre><code class="language-powershell">.\scripts\setup-hooks.ps1</code></pre>
<p>Set your inference token:</p>
<pre><code class="language-bash">export TINQS_HOOK_TOKEN=&lt;your-gitea-pat&gt;</code></pre>
<p>That's it. Every <code>git commit</code> now runs the two-layer review. Bypass with <code>git commit &ndash;no-verify</code> when you need to (emergencies, known false positives).</p>
<h2>The Pattern: Guard Rails at the Edge</h2>
<p>This is the same pattern 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 worth of prevention. A leaked API key in a public blog post is hours of rotation, revocation, and audit. A classified codename in a public post is a confidentiality breach. A dead GitHub link is a broken user experience that 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. The total infrastructure cost is zero &mdash; it runs on the developer's machine, calls an API we already pay for, and adds 4 seconds to the commit flow.</p>
<p>The age of agents doesn't just mean agents that write code. It means agents that watch the code you write.</p>
<pre><code class="language-bash">bash scripts/setup-hooks.sh # or .\scripts\setup-hooks.ps1 on Windows
export TINQS_HOOK_TOKEN=&lt;your-token&gt; # 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 &ndash;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(&ndash;c-accent-l);">Tinqs Studio</a>, our open platform for game development. The inference proxy, the blocklist patterns, and the review prompt are all open and reusable. We're building <a href="https://arikigame.com" style="color: var(&ndash;c-accent-l);">Ariki</a> with these tools &mdash; every commit in the game repo runs through the same guard.</em></p>
<p><em>The pre-commit hook is part of <a href="https://tinqs.com" style="color: var(&ndash;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(&ndash;c-accent-l);">Ariki</a> runs through the same guard.</em></p>
</div>
@@ -185,38 +343,5 @@ Phase 3: Parse JSON response
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After
-57
View File
@@ -1,57 +0,0 @@
# Skill: Blog Authoring
Write and publish markdown blog posts with YAML frontmatter. This skill teaches an AI agent how to create well-structured blog posts for a static site built from markdown.
## Post Format
Create a markdown file in `posts/<slug>.md` with this frontmatter:
```yaml
---
title: "Your Post Title"
slug: your-post-slug
date: "2026-05-22"
description: "Full meta description for SEO (150-160 chars ideal)."
og_description: "Shorter OG/Twitter description."
og_image: "https://your-domain.com/img/og-cover.jpg"
excerpt: "Card text shown on the blog index page."
author: "Author Name"
author_initials: "AN"
author_role: "Role, Company"
---
```
## Writing Guidelines
- **First paragraph** becomes the lead (displayed prominently below the title, separate from the body)
- **Everything after the first blank line** is the post body
- Use standard markdown: `## Headings`, `**bold**`, `*italic*`, `[links](url)`, `- lists`, fenced code blocks
- Images on their own line become `<figure>` elements with captions
- Use `---` for section breaks
- Em dashes: `---` renders as &mdash;
## Structure
A good technical blog post follows this pattern:
1. **Lead paragraph** --- what this post is about, in one sentence
2. **The Problem** --- what pain point or question motivated this work
3. **The Approach** --- what you built or decided, and why
4. **Technical Details** --- how it works, with code/diagrams
5. **What We Learned** --- insights, surprises, trade-offs
6. **Closing** --- what's next, or an invitation to the reader
## SEO Checklist
- [ ] Title under 60 characters
- [ ] Description 150-160 characters
- [ ] og_image set
- [ ] Meaningful excerpt for index card
- [ ] Internal links where relevant
## Conventions
- Slugs are kebab-case, matching the filename: `my-post.md` -> slug `my-post`
- Dates are ISO format: `2026-05-22`
- Canonical URLs: `https://your-domain.com/blog/<slug>`
- Don't edit generated HTML --- edit the markdown, then rebuild
-162
View File
@@ -1,162 +0,0 @@
# Skill: Concept Art Pipeline
End-to-end workflow for creating game character art --- from design brief through 2D concept art to 3D model export. This skill covers everything before the game engine.
## Overview
| Phase | What | Output |
|-------|------|--------|
| 1 --- Design | 2D concept art via AI image generation | Approved PNG(s) |
| 2 --- Model | 3D generation via Tripo Studio | GLB/FBX export |
| 3 --- Handoff | Reference file for the dev team | Markdown spec |
## Phase 1: 2D Concept Art
### Locking Your Art Style
Before generating anything, define your art style once and enforce it everywhere. Write it down. Here's an example for a stylised game:
- **Stylised 3D render**, anime-influenced but not full anime
- **Matte skin**, no metallic sheen, warm earthy tones
- **Neutral confident expression** --- calm determination, not fierce or smiling
- **Earthy palette**: browns, tans, dark reds, cream, black
- **Asymmetric accessories** --- different on left vs right
- **Dark grey background** for all concept art (consistent, easy to composite)
### Hard Rules (define yours)
Every project should have a "do not" list. Examples:
- NO Disney-adjacent designs (if you're going for something grittier)
- NO overly shiny or metallic materials (unless your game's style calls for it)
- NO hair falling forward over chest (won't animate well in-engine)
- NO culturally inappropriate elements (research your references)
### Cultural Direction
If your game draws from real cultures, create an approved/rejected table:
| Approved | Rejected |
|----------|----------|
| Woven natural fibres, shell jewelry | Generic "tribal" patterns |
| Specific cultural motifs (research them) | Stereotypical elements from wrong cultures |
| Natural materials (bone, wood, stone) | Modern materials that break the setting |
### Generation Steps
1. **Get the brief** --- gender, role, distinctive features, or "surprise me within the style"
2. **Generate 3 variants** using your image generation tool (see [Image Generation skill](image-generation.md))
- T-pose, front view, consistent background
- Naming convention: `character_<name>_front_full_v01.png`
3. **Score each variant** using a rubric (see below)
4. **Generate reference sheet** for the winner --- front, side, three-quarter, head closeup
5. **Save with metadata** --- prompt, model used, date, score
### Scoring Rubric (0--10)
| Score | Meaning | Action |
|-------|---------|--------|
| 9-10 | Perfect match to art style and brief | Approve, generate reference sheet |
| 8 | Strong, minor tweaks needed | Approve with notes, iterate once |
| 6-7 | Interesting but needs changes | Note what works, regenerate |
| 4-5 | Wrong direction, some salvageable elements | Extract what works, fresh prompt |
| 0-3 | Off-target | Drop, adjust approach |
**Score on:** style match, cultural accuracy, expression, silhouette (distinctive at distance?), technical (T-pose clean? animatable?)
### Layered Design
Design characters in separable layers for future equipment/customisation:
1. **Base body** --- simple underwear, no accessories
2. **Lower garment** --- baked into character mesh
3. **Upper garment** --- baked or swappable
4. **Hair** --- separate asset (swappable)
5. **Accessories** --- equipment slots (necklaces, bracelets, headwear)
6. **Cape/cloak** --- separate equipment piece
Generate the full character for the overall look, but keep separation in mind for 3D.
## Phase 2: 3D Model Generation
### Using Tripo Studio
[Tripo Studio](https://studio.tripo3d.ai) generates 3D models from text or images.
1. Open Tripo Studio
2. Text-to-3D or paste your approved concept art for image-to-3D
3. Use **Ultra quality**, latest model version, **Texture ON** (~35-50 credits)
4. Export as **GLB** (preferred --- preserves PBR textures) or FBX
5. Save to your project's asset pipeline
Tripo outputs ~1.5M faces with full PBR textures (basecolour, normal, ORM). You'll decimate to your target polygon budget in Blender or your engine.
### Large Hair Problem
Auto-riggers (like Mixamo) break when the mesh includes big hair --- wrong arm poses, face distortion, rig artifacts.
**Rule:** For characters with large or complex hair:
- Export a **body-only version** (no hair) for rigging
- In Tripo: use **Segment** to split hair from body, export body-only
- Hair is added back as a separate mesh after rigging
### Polygon Budget
Define your LOD targets. Example:
| LOD | Face Count | Use |
|-----|-----------|-----|
| LOD0 | 25,000 | Close-up, player character |
| LOD1 | 10,000 | Medium distance NPCs |
| LOD2 | 2,500 | Crowd/background characters |
### Texture QA Checklist
Before handoff, verify:
- [ ] PBR textures present --- basecolour, normal, roughness/metallic maps
- [ ] No UV seams on prominent areas (face, hands)
- [ ] Skin tone matches the approved concept art
- [ ] No texture stretching on limbs or torso
- [ ] Accessories have distinct materials
- [ ] Resolution 2K minimum for hero characters
- [ ] No generation artifacts (floating geometry, merged fingers, extra limbs)
## Handoff
Create a handoff document for each approved character:
1. **Character identity** --- name, role, description
2. **Concept art files** --- paths to approved PNGs
3. **3D model files** --- path to GLB/FBX exports
4. **Art direction notes** --- what to keep, what to change
5. **Technical requirements** --- shader, rig type, LOD targets, scale
6. **Known issues** --- large hair flag, texture problems, etc.
## Batch Generation
When generating multiple characters:
1. Prepare all briefs upfront
2. Generate in priority order (ship-soonest first)
3. Use free retries before spending credits
4. Score as you go --- don't batch review
5. Track credits per generation
6. Export immediately after approval (don't leave work in the cloud tool)
## Common Mistakes
| Problem | Cause | Fix |
|---------|-------|-----|
| Characters look generic | No art style lock | Define and enforce your style guide |
| Inconsistent across characters | No reference images | Use your best character as image-to-image reference |
| Metallic skin sheen | Default material settings | Use matte keywords, check PBR roughness |
| Symmetric accessories | Generation default | Explicitly describe left vs right in prompt |
| Auto-rigger fails | Large hair in mesh | Segment and export body-only |
| Low-poly look in engine | PBR textures missing | Always generate with Texture ON + PBR ON |
## Related Skills
- [Image Generation](image-generation.md) --- fal.ai Flux models and prompt patterns
- [Tripo 3D](tripo-browser-workflow.md) --- detailed Tripo Studio workflow
- [Sora 2 Video](sora2-video.md) --- animate concept art into video
-218
View File
@@ -1,218 +0,0 @@
# Skill: Image Generation with fal.ai Flux
Generate game art, concept art, icons, logos, trailer frames, and marketing visuals using fal.ai Flux models. This skill teaches an AI agent how to call fal.ai's API with structured prompts optimised for game development.
## Overview
[fal.ai](https://fal.ai) hosts Flux image generation models with a simple API. You describe what you want, pick a model and size, and get back a URL to the generated image. Costs range from $0.004 to $0.015 per image.
## Models
| Model | Quality | Cost/img | Speed | Use |
|-------|---------|---------|-------|-----|
| `fal-ai/flux-2-pro` | High | ~$0.01 | ~15s | **Default.** Final art, icons, trailer frames. Most reliable. |
| `fal-ai/flux/schnell` | Fast | $0.004 | ~4s | Quick mockups, iteration, exploration |
| `fal-ai/flux-pro/v1.1-ultra` | Ultra | $0.015 | ~8s | Highest quality, but can be slow. Fall back to flux-2-pro if it hangs. |
| `fal-ai/ideogram/v2` | Typography | $0.008 | ~5s | Logos with readable text, posters, banners |
**How to pick:** Use `flux-2-pro` for most work. Use `schnell` when iterating fast. Use `ideogram/v2` when you need readable text in the image.
## Sizes
| Value | Dimensions | Use |
|-------|-----------|-----|
| `square` | 512x512 | Icons, favicons |
| `square_hd` | 1024x1024 | App icons, logos, character portraits |
| `landscape_16_9` | 1024x576 | Trailer frames, hero images, Steam banners |
| `portrait_16_9` | 576x1024 | Steam capsules, posters, mobile splash screens |
## API Usage
### Python (fal-client)
```python
import fal_client
result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
"prompt": "Your detailed prompt here",
"image_size": "landscape_16_9",
"num_images": 1,
})
image_url = result["images"][0]["url"]
print(image_url)
```
### JavaScript
```javascript
import * as fal from "@fal-ai/serverless-client";
const result = await fal.subscribe("fal-ai/flux-2-pro", {
input: {
prompt: "Your detailed prompt here",
image_size: "landscape_16_9",
num_images: 1,
},
});
console.log(result.images[0].url);
```
### cURL
```bash
curl -X POST "https://queue.fal.run/fal-ai/flux-2-pro" \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Your detailed prompt here",
"image_size": "landscape_16_9",
"num_images": 1
}'
```
### Image-to-image (reference images)
Pass a reference image URL for guided generation:
```python
result = fal_client.subscribe("fal-ai/flux-2-pro", arguments={
"prompt": "Same scene but at sunset, warm golden light",
"image_url": "https://your-reference-image.png",
"image_size": "landscape_16_9",
})
```
For dual-reference compositing (mixing elements from two images), pass comma-separated URLs and describe which elements come from which reference in the prompt.
## Prompt Engineering --- the 4-Layer Pattern
This pattern consistently produces the best results for game art. Each layer adds specificity.
### Layer 1: Design context
Set the overall art direction. This anchors the model's style. Example for a Polynesian survival game:
```
Art direction: stylized 3D render for a survival colony sim set in a Polynesian
archipelago. Warm earthy palette --- browns, tans, dark reds, cream, ocean blues.
Carved wood textures, koru spirals, woven pandanus patterns. Moana-meets-Valheim
aesthetic. Game engine quality, not photorealistic.
```
Adapt this to your game's art style. The key is being specific about palette, materials, and cultural references.
### Layer 2: Scene description
Describe exactly what should appear, element by element:
- Camera angle and POV
- Characters (count, poses, clothing, expressions)
- Environment (time of day, weather, terrain, vegetation)
- Colours and lighting
- Technical style (stylised realism, cel-shaded, pixel art, etc.)
**Be specific.** "A warrior standing on a beach" produces generic results. "A Polynesian warrior in a T-pose, front view, wearing tapa cloth wrap and cowrie shell necklace, matte skin, earthy tones, dark grey background" produces usable concept art.
### Layer 3: Negative prompt
Always include what you don't want:
```
Do not include: cartoon style, anime style, photorealistic render, extra text
or taglines, watermark, deformed elements, modern or sci-fi elements.
```
Extend with subject-specific negatives. For character art: "no extra fingers, no merged limbs, no floating accessories."
### Layer 4: Reference images (optional)
When you have existing art to match, pass it as `image_url`. Describe in the prompt which elements to keep: "Same character design but in a side view. Keep the clothing and hair style from the reference."
## Game Dev Use Cases
### Character concept art
```
Stylized 3D render of a young Polynesian woman in T-pose, front view.
Wearing woven pandanus skirt and tapa cloth top. Cowrie shell necklace,
bone bracelet on left wrist only. Hair swept back over one shoulder,
decorated with a hibiscus flower. Matte skin, warm brown tones.
Neutral confident expression. Dark grey background.
Size: square_hd (1024x1024)
```
### Trailer frames / key art
```
Wide cinematic shot of a coastal village at golden hour. Thatched-roof
huts on stilts along a turquoise lagoon. Outrigger canoes pulled up on
white sand. Volcanic mountain in the background with clouds wrapping
the peak. Warm orange sunlight, long shadows. Stylized game engine
quality, not photorealistic.
Size: landscape_16_9 (1024x576)
```
### UI icons
```
Game icon: a carved wooden fishing hook with a glowing blue thread
wrapped around the shaft. Dark background, subtle ambient occlusion.
Clean silhouette, suitable for a 64x64 game UI icon.
Size: square (512x512)
```
### Logo with text
```
Game logo: the word "ARIKI" in thick bold blocky capital letters.
Each letter carved from dark mahogany wood with distinct Polynesian
tribal patterns (koru spirals, chevrons, wave motifs) carved as
deep relief. Different pattern per letter. Warm directional lighting
from above-left. Dark background.
Size: square_hd (1024x1024)
Model: ideogram/v2 (for readable text)
```
## Best Practices
| Do | Don't |
|----|-------|
| Be specific per-element | Write vague one-line prompts |
| Always include negative prompts | Skip negatives and hope for the best |
| Use `flux-2-pro` for final art | Default to the most expensive model |
| Use `schnell` for rapid iteration | Spend $0.01/img on throwaway drafts |
| Generate 2-3 variants and pick the best | Generate one and accept it |
| Save prompts alongside images | Lose track of what prompt made what |
| Match your game's art style in Layer 1 | Let the model pick a random style |
| Use reference images for consistency | Describe the same character differently each time |
## Cost Tracking
| Scenario | Cost |
|----------|------|
| 1 quick mockup (schnell) | $0.004 |
| 1 final frame (flux-2-pro) | ~$0.01 |
| 10-iteration design session | ~$0.10 |
| Full character sheet (4 views x 3 variants) | ~$0.12 |
| 20-frame trailer storyboard | ~$0.20 |
At these prices, the bottleneck is creative direction, not budget.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Generic one-line prompts | Use the 4-layer pattern |
| No art direction context | Always set Layer 1 for your game's style |
| No negative prompt | Always include what you don't want |
| Using the wrong model for text | Use `ideogram/v2` for logos with readable text |
| Not iterating | Generate 2-3 variants, pick the best, refine |
| Inconsistent character designs | Use reference images to anchor style across generations |
## Related Skills
- [Concept Art Pipeline](concept-art-pipeline.md) --- full 2D-to-3D character workflow
- [Sora 2 Video](sora2-video.md) --- animate your generated art into trailer clips
- [Tripo 3D](tripo-browser-workflow.md) --- turn 2D concept art into 3D models
-163
View File
@@ -1,163 +0,0 @@
# Skill: Video Generation with OpenAI Sora 2
Generate trailer clips, gameplay-style footage, and cinematic sequences using OpenAI's Sora 2 API. This skill covers the API workflow, prompting patterns, and cost management for game development use.
## Overview
Sora 2 generates 5-20 second video clips from text prompts or still images. For game studios, it's useful for:
- Trailer pre-visualisation (storyboard frames as video)
- Marketing clips before the game is playable
- Concept videos to test art direction
- Social media content
## Prerequisites
- OpenAI API key with Sora access
- Set `OPENAI_API_KEY` environment variable
## API Reference
### Create a video
```bash
curl -X POST "https://api.openai.com/v1/videos/generations" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sora-2",
"prompt": "Your prompt here",
"size": "1920x1080",
"duration": 10,
"n": 1
}'
```
Response includes a generation ID. Video generation takes 2-10 minutes.
### Check status
```bash
curl "https://api.openai.com/v1/videos/generations/{generation_id}" \
-H "Authorization: Bearer $OPENAI_API_KEY"
```
Poll until `status` is `"completed"`. The response includes a download URL.
### Download
```bash
curl -o output.mp4 "{video_download_url}"
```
### Image-to-video
Animate a still image:
```bash
curl -X POST "https://api.openai.com/v1/videos/generations" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sora-2",
"prompt": "Camera slowly pulls back, revealing the full village. Waves lap at the shore.",
"image": "{base64_or_url_of_image}",
"size": "1920x1080",
"duration": 5
}'
```
This is powerful for animating your generated concept art or key art frames.
## Models
| Model | Quality | Speed | Cost |
|-------|---------|-------|------|
| `sora-2` | High | 2-5 min | ~$0.10-0.20/clip |
| `sora-2-pro` | Highest | 5-10 min | ~$0.50-1.00/clip |
Use `sora-2` for iteration and pre-vis. Use `sora-2-pro` for final trailer clips only.
## Prompting for Game Trailers
### Structure
```
[Camera movement] + [Subject action] + [Environment] + [Lighting/mood] + [Style]
```
### Example: Opening shot
```
Slow aerial drone shot descending toward a cluster of thatched-roof huts
on a tropical island shoreline. Crystal clear turquoise water, white sand
beach, palm trees swaying gently. Golden hour sunlight, long shadows.
Stylised 3D game engine quality, warm colour palette. No text overlays.
```
### Example: Character reveal
```
Medium shot of a young Polynesian woman standing on a cliff edge, looking
out over an ocean dotted with volcanic islands. Wind blowing her hair.
She turns to face the camera with a calm, determined expression. Sunset
light behind her. Stylised 3D render, game cinematic quality.
```
### Example: Action sequence
```
Wide shot of three outrigger canoes racing across open ocean toward a
volcanic island. Waves crashing, spray in the air. Warriors paddling
in unison. Storm clouds building on the horizon. Dynamic camera
tracking the lead canoe. Stylised game engine quality.
```
## Cultural Guardrails
If your game draws from real cultures, set explicit rules:
- **No anachronistic elements** (modern objects, wrong architectural styles)
- **Research your references** --- use authentic building styles, clothing, tools
- **Avoid stereotypes** --- specific cultural elements, not generic "exotic"
- **No sacred symbols used decoratively** without understanding their meaning
## Workflow for Trailer Storyboards
1. **Write the storyboard** --- list each shot with description, duration, and camera movement
2. **Generate key frames** using image generation (see [Image Generation skill](image-generation.md))
3. **Animate key frames** using Sora 2's image-to-video
4. **Generate original clips** for shots that don't need a specific starting frame
5. **Review and iterate** --- regenerate clips that don't match the vision
6. **Compile** in a video editor (DaVinci Resolve, Premiere, etc.)
## Cost Management
| Scenario | Estimated Cost |
|----------|---------------|
| 1 test clip (sora-2, 5s) | ~$0.10 |
| 10-clip exploration session | ~$1.00 |
| 20-clip trailer storyboard | ~$2-4 |
| Final 5-clip trailer (sora-2-pro) | ~$2.50-5.00 |
**Tips:**
- Use `sora-2` (not pro) for all iteration
- Keep clips to 5-10 seconds --- shorter clips have better coherence
- Generate 2-3 variants per shot and pick the best
- Only use `sora-2-pro` for the final selected shots
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Overly long prompts | Keep under 200 words. Focus on what's visible. |
| Requesting specific text/UI | Sora can't render readable text. Add text in post. |
| Not specifying art style | Always end with style direction ("stylised 3D", "game engine quality") |
| Using sora-2-pro for iteration | Expensive and slow. Use sora-2 until you're happy with the prompt. |
| Ignoring cultural accuracy | Set guardrails before generating. Review outputs for stereotypes. |
| Not tracking costs | Log every generation with model, duration, and cost. |
## Related Skills
- [Image Generation](image-generation.md) --- create key frames to animate with Sora
- [Concept Art Pipeline](concept-art-pipeline.md) --- character and asset design workflow
-157
View File
@@ -1,157 +0,0 @@
# Skill: 3D Model Generation with Tripo Studio
Generate 3D models from text descriptions or 2D images using [Tripo Studio](https://studio.tripo3d.ai). This skill covers the workflow for game-ready 3D assets.
## Overview
Tripo Studio is a web-based text-to-3D and image-to-3D tool. You describe a character, prop, or environment piece --- or upload concept art --- and Tripo generates a textured 3D model in ~60 seconds.
**Output:** GLB or FBX with full PBR textures (basecolour, normal, ORM/metallic/roughness).
**Cost:** ~25-50 credits per generation. Free tier: 150 credits/month.
## When to Use
- You have approved 2D concept art and need a 3D model
- You need a quick 3D prototype from a text description
- You're generating props, characters, or environment pieces for a game
- You need multiple variants of the same object
## Workflow
### Text-to-3D
1. Open [Tripo Studio](https://studio.tripo3d.ai)
2. Click **Create** -> **Text to 3D**
3. Enter your prompt (see prompt tips below)
4. Settings:
- Quality: **Ultra**
- Model: **v3.1 Best Quality** (or latest)
- Texture: **ON** (critical --- generates PBR maps)
5. Click Generate (~35-50 credits, ~60s)
6. Review the model in the 3D viewer --- rotate, check materials
7. Export as **GLB** (preferred) or FBX
### Image-to-3D
1. Upload your approved concept art (front view, T-pose for characters)
2. Settings: same as text-to-3D
3. Generate
4. Review --- check if the back and sides match your expectations
5. Export
## Prompt Tips for Game Assets
### Characters
```
Full body character, T-pose, front facing. Young Polynesian woman wearing
woven pandanus skirt and tapa cloth top. Shell necklace, flower in hair.
Stylized game character, not photorealistic. Clean topology.
```
**Key rules:**
- Always specify **T-pose** for characters (needed for rigging)
- Mention **clean topology** to reduce mesh artifacts
- Describe clothing and accessories explicitly
- Specify art style (stylized, realistic, low-poly, etc.)
### Props
```
Carved wooden fishing spear with bone tip. Wrapped handle with
woven cord. Polynesian style, game prop, stylized.
```
### Environment pieces
```
Thatched-roof hut on wooden stilts. Open sides with woven wall panels.
Polynesian longhouse style. Game-ready, stylized 3D.
```
## Post-Generation Tools
Tripo offers several refinement tools after generation:
| Tool | Purpose | When to Use |
|------|---------|-------------|
| **Segment** | Split model into parts | Characters with large hair (split hair from body for rigging) |
| **Retopo** | Retopologize mesh | When face count is too high or topology is bad |
| **Texture** | Regenerate textures | When colours don't match concept art |
| **Animate** | Add basic animations | Quick previews (not production quality) |
| **Edit** | Modify the model | Remove artifacts, adjust proportions |
## Export Options
| Format | Use |
|--------|-----|
| **GLB** | Preferred. Single file, preserves PBR textures, works everywhere |
| **FBX** | When your pipeline requires it (Unity, some Blender workflows) |
| OBJ | Legacy. Loses PBR data. Avoid. |
| USD | For USD-based pipelines |
| STL | 3D printing only. No textures. |
## Polygon Budget
Tripo outputs ~1.5M faces by default. You'll need to decimate:
| LOD | Target Faces | Method |
|-----|-------------|--------|
| LOD0 | 25,000 | Blender Decimate modifier or CLI |
| LOD1 | 10,000 | Same |
| LOD2 | 2,500 | Same |
Use Tripo's built-in **Retopo** tool for a quick reduction, or decimate in Blender for more control.
## The Large Hair Problem
Auto-riggers (Mixamo, Godot's skeleton system) break when character meshes include large or complex hairstyles. The rig misidentifies the hair volume as part of the body.
**Solution:**
1. After generation, use Tripo's **Segment** tool
2. Select the hair and split it from the body
3. Export the **body-only** mesh for rigging
4. Export hair separately
5. Reattach hair after rigging in Blender or your engine
Flag this in your handoff document for every character with big hair.
## Batch Generation
When generating multiple assets in one session:
1. Prepare all prompts upfront
2. Generate in priority order
3. Use the 3 free retries per generation before spending more credits
4. Score and review each model immediately
5. Track remaining credits
6. Export approved models right away --- don't leave them only in the cloud
## Credit Optimisation
| Action | Credits |
|--------|---------|
| Standard quality generation | ~15-25 |
| Ultra quality generation | ~35-50 |
| Retopo | ~10-15 |
| Texture regeneration | ~10 |
| Segment | ~5 |
**Tip:** Start with Standard quality for exploration, switch to Ultra only for final approved designs.
## Common Mistakes
| Problem | Fix |
|---------|-----|
| Model has no textures | Ensure Texture is ON before generating |
| Back of character looks wrong | Use image-to-3D with front AND back views, or add "detailed back view" to prompt |
| Mesh has floating geometry | Use Edit tool to clean up, or regenerate |
| Merged fingers | Common artifact. Regenerate or fix in Blender |
| Too many polygons for game use | Use Retopo tool or Blender Decimate |
| Hair breaks auto-rigger | Segment hair from body, rig body-only |
## Related Skills
- [Image Generation](image-generation.md) --- create 2D concept art to feed into Tripo
- [Concept Art Pipeline](concept-art-pipeline.md) --- full 2D-to-3D workflow
+251 -112
View File
@@ -4,27 +4,27 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>One Binary to Rule Them All: Building a Studio CLI — Tinqs Blog</title>
<meta name="description" content="A single Go binary that handles machine identity, screenshots, cloud vision, and health checks. The glue that makes AI agents useful across a multi-machine game studio.">
<title>One Binary to Rule Them All: Our Studio CLI — Tinqs Blog</title>
<meta name="description" content="A single Go binary that gives AI agents full context about your machine, project, and services in 100ms. Screenshots, cloud vision, health checks — one install, every machine.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://www.tinqs.com/blog/studio-cli">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.tinqs.com/blog/studio-cli">
<meta property="og:title" content="One Binary to Rule Them All: Building a Studio CLI">
<meta property="og:description" content="A single Go binary for machine identity, screenshots, cloud vision, and AI agent coordination.">
<meta property="og:title" content="One Binary to Rule Them All: Our Studio CLI">
<meta property="og:description" content="One Go binary for machine identity, screenshots, cloud vision, and AI agent context.">
<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="One Binary to Rule Them All: Building a Studio CLI">
<meta name="twitter:description" content="A single Go binary for machine identity, screenshots, cloud vision, and AI agent coordination.">
<meta name="twitter:title" content="One Binary to Rule Them All: Our Studio CLI">
<meta name="twitter:description" content="One Go binary for machine identity, screenshots, cloud vision, and AI agent context.">
<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": "One Binary to Rule Them All: Building a Studio CLI",
"headline": "One Binary to Rule Them All: Our Studio CLI",
"datePublished": "2026-05-18",
"author": {
"@type": "Person",
@@ -35,99 +35,271 @@
"name": "Tinqs Limited",
"url": "https://www.tinqs.com"
},
"description": "A single Go binary that handles machine identity, screenshots, cloud vision, and health checks. The glue that makes AI agents useful across a multi-machine game studio."
"description": "A single Go binary that gives AI agents full context about your machine, project, and services in 100ms. Screenshots, cloud vision, health checks — one install, every machine."
}
</script>
<!-- PostHog (EU) -->
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_teG6p5oxf6poQHPThq5AGKzWQNhw4bHW9arLwWAVXm3f',{api_host:'https://eu.i.posthog.com',ui_host:'https://eu.posthog.com',person_profiles:'identified_only',defaults:'2026-01-30'})
</script>
<style>
/* ── Self-contained post styles (Studio provides site chrome) ── */
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
<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=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../style.css">
:root {
--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; }
body {
margin: 0;
padding: 0;
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 {
max-width: 720px;
margin: 0 auto;
padding: 40px 24px 48px;
}
/* ── Back link ── */
.post__back {
color: var(--c-blue);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 24px;
}
.post__back:hover { color: var(--c-purple); }
/* ── Gradient title ── */
.post__title {
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 800;
font-size: 2.2rem;
line-height: 1.25;
margin: 0 0 16px;
}
/* ── Date pill ── */
.post__date {
display: inline-block;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--c-blue);
border: 1px solid rgba(147, 140, 129, 0.25);
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-size: 1.7rem;
margin: 54px 0 6px;
padding-left: 16px;
border-left: 4px solid var(--c-accent);
line-height: 1.3;
}
.post__body h3 {
color: var(--c-purple);
font-size: 1.18rem;
margin: 30px 0 4px;
}
.post__body h4, .post__body h5, .post__body h6 {
margin: 20px 0 4px;
}
/* ── Inline code ── */
.post__body code {
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: 5px;
border: 1px solid var(--c-border);
}
/* ── Code blocks ── */
.post__body pre {
background: var(--c-pre-bg);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
margin: 14px 0;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
color: var(--c-text);
}
.post__body pre code {
background: transparent;
padding: 0;
border: none;
font-size: inherit;
color: inherit;
border-radius: 0;
}
/* ── Blockquote ── */
.post__body blockquote {
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: #f4e3c4;
font-size: 0.94rem;
}
/* ── Links ── */
.post__body a { color: var(--c-blue); }
.post__body a:hover { color: var(--c-purple); }
/* ── Strong ── */
.post__body strong { color: var(--c-gold); }
/* ── 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-accent);
color: var(--c-bg);
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-text);
font-weight: 600;
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav nav--scrolled" id="nav">
<a href="/" class="nav__logo" aria-label="Tinqs home">
<span class="nav__wordmark">TINQS</span>
</a>
<div class="nav__links">
<a href="/#game" class="nav__link">Games</a>
<a href="/#tech" class="nav__link">Technology</a>
<a href="/#about" class="nav__link">About</a>
<a href="/blog/" class="nav__link" style="color: var(--c-accent-l);">Blog</a>
<a href="/#signup" class="nav__link">Contact</a>
<a href="/press" class="nav__link">Press</a>
</div>
<button class="nav__burger" aria-label="Open menu" id="navBurger">
<span></span><span></span><span></span>
</button>
</nav>
<!-- MOBILE MENU -->
<div class="mobile-menu" id="mobileMenu">
<a href="/#game" class="mobile-menu__link">Games</a>
<a href="/#tech" class="mobile-menu__link">Technology</a>
<a href="/#about" class="mobile-menu__link">About</a>
<a href="/blog/" class="mobile-menu__link">Blog</a>
<a href="/#signup" class="mobile-menu__link">Contact</a>
<a href="/press" class="mobile-menu__link">Press</a>
</div>
<!-- POST -->
<article class="post">
<a href="/blog/" class="post__back">&larr; All Posts</a>
<span class="post__date">18 May 2026</span>
<h1 class="post__title">One Binary to Rule Them All: Building a Studio CLI</h1>
<p class="post__lead">Every machine in our studio runs the same Go binary. It knows who you are, what machine you're on, and what services are reachable. It takes screenshots, sends them to cloud vision, and runs health checks. This is the glue that makes AI agents actually useful in a multi-machine game studio.</p>
<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.</p>
<div class="post__body">
<h2>Why Build a CLI</h2>
<p>When you have multiple machines across several people, two operating systems, and AI agents that need context about the environment they're running in, the glue becomes the hardest part. Which machine is this? What services are reachable? Is the game running? Can I take a screenshot of what the developer is looking at?</p>
<p>We tried shell scripts. A <code>setup.sh</code> for Mac, a <code>setup.ps1</code> for Windows, a <code>check-services.sh</code> for health checks, a <code>screenshot.py</code> that never worked on Windows. They drifted. They broke. Nobody updated them.</p>
<p>So we built one Go binary that does everything.</p>
<h2>The Identity System</h2>
<p>The most important command is <code>identity</code>. When an AI agent starts a new session &mdash; Cursor, Claude Code, any tool &mdash; the first thing it does is call this command. The output tells the agent:</p>
<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>The soul file</strong> &mdash; the agent's persistent identity, values, and operating principles</li>
<li><strong>Company context</strong> &mdash; team members, roles, what the company does</li>
<li><strong>Machine context</strong> &mdash; hostname, OS, which repos are cloned, what services are running</li>
<li><strong>Ecosystem</strong> &mdash; other repos and their purpose</li>
<li><strong>Service status</strong> &mdash; which URLs are live and reachable</li>
<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 solves a fundamental problem with AI agents: <strong>cold starts.</strong> Every new chat window, every new agent tab, every new session is a blank slate. The agent doesn't know what project this is, who's asking, or what infrastructure exists. One CLI call gives it full context.</p>
<p>The data lives in markdown files in the docs repo &mdash; the source of truth. Any machine on the network can read it.</p>
<h2>Screenshots and Vision</h2>
<p>The CLI can capture any window from outside the process. No in-game overlay, no rendering pipeline integration. It uses the OS-level window capture API &mdash; works on Windows (via GDI+) and Mac (via screencapture).</p>
<p>A <code>photo</code> command does the same thing but sends the screenshot to a cloud vision model for analysis. The agent says "take a photo of the game" and gets back a structured description: "The player character is standing near a half-built hut. There are 3 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, and the agent takes a screenshot, describes what it sees, and creates an issue with both the description and the image attached.</p>
<h2>Health Checks</h2>
<p>A <code>doctor</code> command runs a comprehensive health check:</p>
<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? Can we authenticate?</li>
<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 and at the right version?</li>
<li>Are required tools installed at the right version?</li>
</ul>
<p>The output is a green/yellow/red table. If something's wrong, the agent knows immediately and can diagnose or escalate. This is essential for unattended agent sessions &mdash; the agent can verify its environment before starting work.</p>
<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 with no runtime dependencies. 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, done.</p>
<p>The binary is 15MB. It starts in under 100ms. It has zero runtime dependencies. For a tool that AI agents call on every session start, speed matters.</p>
<h2>What We Learned</h2>
<p><strong>The CLI is the API for AI agents.</strong> When we started, this was a convenience tool for humans. It became the primary interface for AI agents. The <code>identity</code> command was originally "nice to have" &mdash; now it's the single most important function in our stack. Every agent session starts with it.</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 its 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 and getting back a structured description 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 write when the AI is looking at the same screen you are.</p>
<p><strong>Agent cold starts are the real problem.</strong> Without the identity system, every new session starts with the agent asking "what project is this?" and the human re-explaining context. With it, the agent knows everything in 100ms. That's the difference between an AI assistant and an AI team member.</p>
<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>The CLI is part of <a href="https://tinqs.com" style="color: var(&ndash;c-accent-l);">Tinqs Studio</a> &mdash; our game development platform that brings git hosting, AI agent tools, and team workflows together. Every time we find ourselves writing 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.</p>
<p><em>The CLI is part of <a href="https://tinqs.com" style="color: var(&ndash;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>
@@ -140,38 +312,5 @@
</div>
</article>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<span class="footer__wordmark">TINQS</span>
<div class="footer__links">
<a href="/#game">Games</a>
<a href="/#tech">Technology</a>
<a href="/#about">About</a>
<a href="/blog/">Blog</a>
<a href="mailto:hello@tinqs.com">hello@tinqs.com</a>
<a href="/press">Press Kit</a>
</div>
<p class="footer__copy">Tinqs Limited &mdash; London, est. 2020</p>
</div>
</footer>
<script>
const burger = document.getElementById('navBurger');
const mobileMenu = document.getElementById('mobileMenu');
burger.addEventListener('click', () => {
const open = mobileMenu.classList.toggle('mobile-menu--open');
burger.classList.toggle('nav__burger--open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('mobile-menu--open');
burger.classList.remove('nav__burger--open');
document.body.style.overflow = '';
});
});
</script>
</body>
</html>
Before
After