<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Likhan Siddiquee</title>
    <description>The latest articles on DEV Community by Likhan Siddiquee (@likhan).</description>
    <link>https://dev.to/likhan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2074020%2F69feec62-49a8-455a-beda-596a8618a826.jpg</url>
      <title>DEV Community: Likhan Siddiquee</title>
      <link>https://dev.to/likhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/likhan"/>
    <language>en</language>
    <item>
      <title>Fixing Gitea Actions on Synology Container Manager</title>
      <dc:creator>Likhan Siddiquee</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:07:29 +0000</pubDate>
      <link>https://dev.to/likhan/fixing-gitea-actions-on-synology-container-manager-33pj</link>
      <guid>https://dev.to/likhan/fixing-gitea-actions-on-synology-container-manager-33pj</guid>
      <description>&lt;p&gt;Gitea worked on my Synology NAS. Its CI jobs failed before they could check out a single file.&lt;/p&gt;

&lt;p&gt;I was hosting Gitea and its Actions runner through &lt;strong&gt;Synology Container Manager's Docker Compose support&lt;/strong&gt;. The server was running, but the runner could not create the container that would execute the workflow. None of the repository's code had run yet.&lt;/p&gt;

&lt;p&gt;The fix was to stop running a second Docker daemon inside the runner and use Synology's existing daemon instead. That removed the failing nested mount. It also gave CI access to the same daemon managing the other containers on my NAS. Those are two consequences of the same change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Compose Configuration
&lt;/h2&gt;

&lt;p&gt;This was the starting configuration. The server had its own persistent data directory, and the runner had both a registration directory and a volume for its inner Docker daemon. The registration token below is a placeholder, not a real credential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitea&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.gitea.com/gitea:28.2.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_APP_INI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/gitea/conf/app.ini&lt;/span&gt;
      &lt;span class="na"&gt;USER_UID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1026"&lt;/span&gt;
      &lt;span class="na"&gt;USER_GID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100"&lt;/span&gt;
      &lt;span class="na"&gt;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Amsterdam&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/gitea/data:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2222:2222"&lt;/span&gt;

  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/gitea/runner:5-dind&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea_runner&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_INSTANCE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://server:3000&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_RUNNER_REGISTRATION_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REPLACE_WITH_YOUR_REGISTRATION_TOKEN"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;server&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/gitea/runner:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;runner-docker:/var/lib/docker&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runner-docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;5-dind&lt;/code&gt; suffix matters here: that image runs a second Docker daemon inside the runner container. When a workflow needs a job container, the inner daemon creates it. In this setup, that extra Docker layer was where execution stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI Failed
&lt;/h2&gt;

&lt;p&gt;I inspected the failed runs with the logged-in &lt;code&gt;tea&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tea actions runs logs 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both runs I inspected failed at the same boundary: creating the job container. There was no dependency problem or failing test to investigate yet. Checkout, installation, linting, builds, and tests had never started.&lt;/p&gt;

&lt;p&gt;The decisive error was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to create container:
failed to mount ... fstype: overlay ... err: no such device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner Docker daemon was using containerd's OverlayFS snapshotter. It tried to mount the job container's filesystem, and that nested mount failed with &lt;code&gt;no such device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The runner already had &lt;code&gt;privileged: true&lt;/code&gt;. There was no missing privileged-mode switch to turn on, and privileges cannot supply a capability the host kernel lacks. The logs located the failure at the nested mount; they did not establish the exact Synology kernel limitation. I had enough evidence to change the Docker arrangement, not enough to name a specific kernel defect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Synology's Docker Daemon Instead
&lt;/h2&gt;

&lt;p&gt;I replaced &lt;code&gt;gitea/runner:5-dind&lt;/code&gt; with &lt;strong&gt;&lt;code&gt;gitea/runner:5&lt;/code&gt;&lt;/strong&gt;, the basic runner image, and mounted Synology's Docker socket. Instead of asking an inner daemon to create job containers, the runner would ask the daemon already running on the NAS.&lt;/p&gt;

&lt;p&gt;That removed the nested daemon and its filesystem mount from the execution path. It did not require a change to the Gitea server.&lt;/p&gt;

&lt;p&gt;The changes were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch the runner image to &lt;code&gt;docker.io/gitea/runner:5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Mount &lt;code&gt;docker.sock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;privileged: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove the inner &lt;code&gt;/var/lib/docker&lt;/code&gt; volume mount.&lt;/li&gt;
&lt;li&gt;Preserve the existing runner registration directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security trade-off:&lt;/strong&gt; removing &lt;code&gt;privileged: true&lt;/code&gt; does not make this a low-authority runner. Docker socket access gives it extensive control over the NAS, including other containers. A workflow with that access is operating inside the NAS's trust boundary. Use this setup only for trusted repositories and workflows; a dedicated Linux VM provides stronger isolation from the NAS.&lt;/p&gt;

&lt;p&gt;I covered a related placement decision in &lt;a href="https://www.likhansiddiquee.com/blog/secured-deployment/" rel="noopener noreferrer"&gt;Deploying the gateway: containers, WSL, and your LAN&lt;/a&gt;: deciding where a service runs also means deciding what it can reach. Here, the Docker socket is the access that needs scrutiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting the Replacement
&lt;/h2&gt;

&lt;p&gt;Changing the Docker arrangement removed one problem, but the replacement still needed a configuration file and the right network. A stale Container Manager entry added a separate distraction.&lt;/p&gt;

&lt;p&gt;All commands below run through SSH &lt;strong&gt;on the Synology NAS&lt;/strong&gt;, not on a development workstation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Socket Permissions
&lt;/h3&gt;

&lt;p&gt;The first obstacle was inspecting Docker itself. My SSH user received:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permission denied while trying to connect to the Docker daemon socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;sudo&lt;/code&gt; let me inspect the containers and runner logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gitea_runner
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 gitea_runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was a permission error from my SSH user's Docker command. Using &lt;code&gt;sudo&lt;/code&gt; was enough for that inspection; there was no need to make the Docker socket world-writable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing Runner Configuration
&lt;/h3&gt;

&lt;p&gt;The replacement runner then reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open config file "/data/config.yaml": no such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compose configuration pointed to a file I had not created. The bind mount made the NAS's runner directory appear as &lt;code&gt;/data&lt;/code&gt; inside the container, so &lt;code&gt;/data/config.yaml&lt;/code&gt; needed a corresponding file on the NAS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/volume1/docker/gitea/runner/config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Actual Docker Network Name
&lt;/h3&gt;

&lt;p&gt;After creating the file, the next error was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network gitea not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had used the Compose network key, &lt;code&gt;gitea&lt;/code&gt;, as though it were the Docker network's name. The project was also named &lt;code&gt;gitea&lt;/code&gt;, so Compose had created &lt;code&gt;gitea_gitea&lt;/code&gt;. The runner was looking for a network that did not exist under the name I supplied.&lt;/p&gt;

&lt;p&gt;I confirmed it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker inspect gitea &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{range $name, $network := .NetworkSettings.Networks}}{{println $name}}{{end}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner's &lt;code&gt;container.network&lt;/code&gt; setting needs the &lt;strong&gt;actual Docker network name&lt;/strong&gt;, not the key in the Compose file. Putting the job containers on that network lets them reach Gitea at &lt;code&gt;http://server:3000&lt;/code&gt;. The complete configuration below makes the name explicit so the two files agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synology's Stale Container Entry
&lt;/h3&gt;

&lt;p&gt;Meanwhile, Container Manager displayed an old prefixed runner name. Opening it returned "No such container", which made the state look worse than Docker's own output showed.&lt;/p&gt;

&lt;p&gt;Docker showed the current containers correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitea_runner          gitea/runner:5
gitea                 docker.gitea.com/gitea:28.2.0
GITEA-ACTIONS-TASK-... docker.gitea.com/runner-images:ubuntu-latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The long Actions name was a temporary job container, not another runner to clean up. Docker showed the current runner and server, plus the container doing the work. The stale DSM entry was a separate UI problem, not a reason to recreate a working runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete Working Configuration
&lt;/h2&gt;

&lt;p&gt;This is the complete replacement Compose YAML, with the nested daemon removed and the network explicitly named &lt;code&gt;gitea_gitea&lt;/code&gt;. The runner configuration below uses that same name.&lt;/p&gt;

&lt;p&gt;Adjust the NAS paths, UID, GID, and ports for your installation. The Gitea server image is unchanged from the original setup; this fix does not require a server upgrade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitea&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea_gitea&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.gitea.com/gitea:28.2.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_APP_INI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/gitea/conf/app.ini&lt;/span&gt;
      &lt;span class="na"&gt;USER_UID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1026"&lt;/span&gt;
      &lt;span class="na"&gt;USER_GID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;100"&lt;/span&gt;
      &lt;span class="na"&gt;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Amsterdam&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/gitea/data:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2222:2222"&lt;/span&gt;

  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/gitea/runner:5&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea_runner&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;server&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_INSTANCE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://server:3000&lt;/span&gt;
      &lt;span class="na"&gt;GITEA_RUNNER_REGISTRATION_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REPLACE_WITH_YOUR_REGISTRATION_TOKEN"&lt;/span&gt;
      &lt;span class="na"&gt;CONFIG_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/config.yaml&lt;/span&gt;
      &lt;span class="na"&gt;DOCKER_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unix:///var/run/docker.sock&lt;/span&gt;
      &lt;span class="na"&gt;TZ&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Europe/Amsterdam&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gitea&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/volume1/docker/gitea/runner:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before deploying, create &lt;code&gt;/volume1/docker/gitea/runner/config.yaml&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitea_gitea&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a new installation, obtain a runner registration token from the appropriate Gitea Actions settings and replace the placeholder. &lt;strong&gt;After successful registration, remove the token environment variable from Compose.&lt;/strong&gt; The persisted &lt;code&gt;.runner&lt;/code&gt; file retains the runner's registration credentials.&lt;/p&gt;

&lt;p&gt;For an existing installation, keep the &lt;code&gt;.runner&lt;/code&gt; file and the rest of the runner directory. Changing how the runner reaches Docker is not a reason to discard its registration. Do not delete the server data or the old Docker volume during troubleshooting. Rotate any registration token accidentally shared in screenshots, logs, or messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying and Verifying
&lt;/h2&gt;

&lt;p&gt;Apply the changes by updating and redeploying the existing project through Synology Container Manager. A restart is not enough: changing the image and mounts requires the runner container to be recreated from the updated Compose configuration.&lt;/p&gt;

&lt;p&gt;Inspect the runner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker logs &lt;span class="nt"&gt;--since&lt;/span&gt; 2m &lt;span class="nt"&gt;--tail&lt;/span&gt; 50 gitea_runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it reports successful startup, rerun the failed workflow. Do not restart the runner while a job is active, because that can cancel the job.&lt;/p&gt;

&lt;p&gt;In this case, registration succeeded, a job container appeared on Synology's Docker daemon, and CI began progressing. That was the evidence I needed for this fix: the runner had crossed the container-creation boundary where both inspected runs had stopped.&lt;/p&gt;

&lt;p&gt;It was not proof of a green pipeline. Whether every build and test passes is a separate check. The original failure happened before the repository could run; now its workflow could start. The remaining decision is whether giving those workflows access to the NAS's Docker daemon is an acceptable place to run them.&lt;/p&gt;

&lt;p&gt;Would you let trusted CI workflows share your NAS's Docker daemon, or move the runner into a dedicated VM? Which repository trust boundary or isolation requirement would change your choice?&lt;/p&gt;

</description>
      <category>gitea</category>
      <category>docker</category>
      <category>synology</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Architectural Debt is the New Technical Debt</title>
      <dc:creator>Likhan Siddiquee</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:03:29 +0000</pubDate>
      <link>https://dev.to/likhan/architectural-debt-is-the-new-technical-debt-4dlf</link>
      <guid>https://dev.to/likhan/architectural-debt-is-the-new-technical-debt-4dlf</guid>
      <description>&lt;p&gt;Imagine a request to show payment status on an order page. An agent finds the order endpoint, adds a &lt;code&gt;paymentStatus&lt;/code&gt; column to the orders table, copies a field from the billing response, and updates the UI. The tests pass. The ticket closes.&lt;/p&gt;

&lt;p&gt;Which service owns the truth when a refund happens tomorrow?&lt;/p&gt;

&lt;p&gt;If billing owns payments but orders stores a second status, someone now has to define how those values converge. A retry, an out-of-order event, or a partial failure can show a paid order as unpaid. The added code may be tidy. The decision about ownership is not.&lt;/p&gt;

&lt;p&gt;That is architectural debt: the cost of a missing or compromised boundary, paid on every later change that crosses it. AI makes it easier to incur while making local code cleanup cheaper. The scarce skill is deciding what a system means and which part is allowed to say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The marginal cost of code fell, not the cost of change
&lt;/h2&gt;

&lt;p&gt;Generating a handler, test, and migration takes less typing than it used to. An agent can also rewrite an awkward function, remove duplication, and fill in boilerplate quickly. Those are useful gains. They do not make the output free: someone still has to review behavior, migrate data, deploy the change, and support it when production disagrees with the test fixture.&lt;/p&gt;

&lt;p&gt;Lint errors and duplicate code still matter, especially when they obscure a bug. But a clean lint report says nothing about whether &lt;code&gt;paymentStatus&lt;/code&gt; belongs on an order. The local quality of the patch and the global shape of the system are different questions. Faster patching shifts attention toward the question that cannot be answered by rewriting a file.&lt;/p&gt;

&lt;p&gt;I wrote about the &lt;a href="https://www.likhansiddiquee.com/blog/velocity-microphone/" rel="noopener noreferrer"&gt;velocity microphone&lt;/a&gt;: AI amplifies unclear intent and returns the consequences faster. Here the consequence is not a conspicuously broken function. It is a plausible new source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy path crosses the wrong boundary
&lt;/h2&gt;

&lt;p&gt;An agent asked to make the order page show payment status will search for the code that renders the order page. The shortest path may be to extend the order model, because that is where the current request is being handled. No malicious reasoning is required. The task, the nearby code, and the tests all point at a local completion condition.&lt;/p&gt;

&lt;p&gt;Then another feature reads &lt;code&gt;orders.paymentStatus&lt;/code&gt; to decide whether to ship. A third updates it after a chargeback. Each change is defensible within its own ticket. Together they turn a display shortcut into a domain rule, and now neither orders nor billing can change its state model without coordinating with the other.&lt;/p&gt;

&lt;p&gt;The same failure takes other forms: a shared database table becomes an unofficial integration API; a frontend duplicates an authorization rule; a "temporary" mapping layer becomes the place where two services disagree about customer identity. Generated code does not create these traps. It makes the easy path fast enough to take repeatedly before the coordination cost becomes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard the contract before generating the patch
&lt;/h2&gt;

&lt;p&gt;For the order-page request, I would first ask: is payment status a billing fact or an order fact? Suppose billing owns it. The order page can request a billing-owned view or consume a deliberately defined projection. If it caches a projection, the contract must name its source, update mechanism, failure behavior, and acceptable staleness. Orders should not become an independent authority for the same status.&lt;/p&gt;

&lt;p&gt;That decision changes the agent's task. Instead of "add payment status to orders," the request becomes: "display billing's payment status on the order page; do not add a writable payment field to the orders model; specify what the page shows when billing is unavailable." The constraint is small enough to review and concrete enough to test. A contract test can catch an unauthorized write or a missing unavailable state. It cannot choose the domain owner on its own.&lt;/p&gt;

&lt;p&gt;For any domain boundary, put the contract in the instructions the coding agent actually receives: &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;copilot-instructions.md&lt;/code&gt; for repository-wide rules, or a scoped &lt;code&gt;*.instructions.md&lt;/code&gt; for the code it governs. Name who owns each fact and state transition, how other domains may read it, which writes and dependencies are forbidden, and what happens when the owner is unavailable. Before editing, have the agent identify the relevant owners and explain how its proposed change crosses their boundary. If the instructions are missing or conflict with the code, or the request needs a new owner or exception, it should stop and ask the user to decide. It must not treat the nearest model as permission.&lt;/p&gt;

&lt;p&gt;Instructions make a boundary visible; checks make violations harder to ship. Where possible, add dependency rules, schema ownership checks, and contract tests for the allowed exchange and its failure path. Have the agent run those checks and report what they cover. They cannot prove that the domain model is correct, so a person still has to judge new ownership and consistency tradeoffs.&lt;/p&gt;

&lt;p&gt;This is the boundary guardian's work: name who owns a fact, define the permitted exchange, and make violations visible in scoped instructions, tests, APIs, and review. The goal is not to forbid every cross-service dependency. It is to give an agent a durable constraint it can follow and a check it cannot wave away, so the next change does not silently invent a second model.&lt;/p&gt;

&lt;p&gt;The same reasoning applies to planning. I argued that &lt;a href="https://www.likhansiddiquee.com/blog/sprint-planning-after-ai/" rel="noopener noreferrer"&gt;sprint plans still point at an old bottleneck&lt;/a&gt;. When implementation is faster, it is tempting to queue more small features. Before calling one small, check whether it adds a new owner, projection, or consistency rule. A one-line field can be the most expensive part of the feature.&lt;/p&gt;

&lt;p&gt;Code generation has changed what I can finish in an afternoon. It has not removed the need to decide where a fact belongs. If I skip that decision, faster implementation means I can accumulate architectural debt faster too.&lt;/p&gt;

&lt;p&gt;Where have you seen a tidy local change create a second owner for the same fact? What boundary would have caught it?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.likhansiddiquee.com/blog/architectural-debt-ai/" rel="noopener noreferrer"&gt;https://www.likhansiddiquee.com/blog/architectural-debt-ai/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Task state is a call stack for agent work</title>
      <dc:creator>Likhan Siddiquee</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:19:20 +0000</pubDate>
      <link>https://dev.to/likhan/task-state-is-a-call-stack-for-agent-work-a22</link>
      <guid>https://dev.to/likhan/task-state-is-a-call-stack-for-agent-work-a22</guid>
      <description>&lt;p&gt;I was one hero image away from publishing &lt;a href="https://www.likhansiddiquee.com/blog/sprint-planning-after-ai/" rel="noopener noreferrer"&gt;a post about sprint planning&lt;/a&gt;. Generate it, wire the manifest, build the route. Three steps, then done.&lt;/p&gt;

&lt;p&gt;I opened the image prompt for one last look and found it arguing with itself. It named colours in the same prompt that told the generator to avoid them. I asked why. That small question pulled us out of publishing and into an audit of every image instruction in the repository.&lt;/p&gt;

&lt;p&gt;The corrected render arrived as a 1.63 MB PNG. I asked another small question: is that normal? Now we were comparing existing heroes, JPEG quality, chroma sampling, and edge artifacts. A second investigation had opened inside the first task.&lt;/p&gt;

&lt;p&gt;The agent did not start either tangent. I did. On another day the agent spots the neighboring defect and asks whether to follow it. The risk is the same: the original task falls out of active context while one of us follows the more interesting question.&lt;/p&gt;

&lt;p&gt;At the bottom of this stack, the post still needed its manifest entry, route build, and removal of a forward link to an unpublished part two.&lt;/p&gt;

&lt;p&gt;We returned to all three because the task had a return address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task state is a call stack
&lt;/h2&gt;

&lt;p&gt;A to-do list records what remains. A call stack records more: the current frame, its local variables, and the address where execution should return when a nested call finishes.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;.local/scratch/task-state.md&lt;/code&gt; does for my agent work.&lt;/p&gt;

&lt;p&gt;In the analogy, the current frame is the immediate question. Its local variables are the working hypothesis, evidence, rejected paths, blockers, and decisions made so far. Its return address is the next step still waiting in the ledger.&lt;/p&gt;

&lt;p&gt;Before either of us changes focus, the instruction tells the agent to checkpoint the ledger. The tangent becomes the current work while the unfinished task remains recorded beneath it. When the tangent closes, we update its result and resume the waiting next step.&lt;/p&gt;

&lt;p&gt;In call-stack terms, the publishing task can be read like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish sprint-planning post
  -&amp;gt; create hero
     -&amp;gt; I notice excluded colours inside the positive prompt
        -&amp;gt; we inspect the prompt rules
        -&amp;gt; return: corrected sidecar
     -&amp;gt; I question the 1.63 MB render
        -&amp;gt; we audit the export settings
        -&amp;gt; return: normalized hero
  -&amp;gt; wire manifest
  -&amp;gt; remove unpublished forward link
  -&amp;gt; build and inspect route shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack did not stop us from going deep. It made the deep dive reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tangent can start on either side
&lt;/h2&gt;

&lt;p&gt;Most descriptions of agent drift make the agent sound like the only source. It chases a neighboring bug, reads too many files, or turns a local fix into an architecture review.&lt;/p&gt;

&lt;p&gt;I interrupt the plan too. I notice a strange output and ask how it happened. I challenge an assumption halfway through implementation. I open a related file while the agent is running and bring a new concern back into chat. Sometimes the side question is more useful than the task that&lt;br&gt;
exposed it.&lt;/p&gt;

&lt;p&gt;From the work's point of view, ownership of the tangent does not matter. Focus changed while a parent task remained unfinished. The parent needs a snapshot before the new question consumes the active context.&lt;/p&gt;

&lt;p&gt;This is why task state has to be shared. It is not the agent's private plan, and it is not my private set of browser tabs. It is the handoff surface between our two attention streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internal todo tracker is not enough
&lt;/h2&gt;

&lt;p&gt;An agent's internal todo tracker is useful. It can decompose the requested work, mark steps in progress, and show whether the current turn is moving.&lt;/p&gt;

&lt;p&gt;I do not treat it as durable task memory.&lt;/p&gt;

&lt;p&gt;It belongs to the active agent session. It may not survive context compaction, a new chat, an editor restart, or a switch to another agent. More importantly, it cannot see a branch that starts in my head. If I inspect an image, question a design choice, or return after lunch with a different hypothesis, the internal plan does not know until I explain the change.&lt;/p&gt;

&lt;p&gt;A todo item also tends to preserve the action while losing the state around it. "Normalize hero" does not say which settings were tested, why one candidate failed, what still needs visual judgment, or where publishing resumes afterward.&lt;/p&gt;

&lt;p&gt;The local task-state file is visible to both of us. I can correct it. The agent can update it. A new session can read it before reconstructing the task from Git and a partial transcript. Because it lives in the workspace rather than the chat, the work does not depend on one session remaining alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I keep the stack
&lt;/h2&gt;

&lt;p&gt;I do not rely on remembering to take a snapshot once the tangent has become interesting. This repository's always-on &lt;a href="https://github.com/lsiddiquee/mysite/blob/main/.github/copilot-instructions.md#context-persistence-never-lose-work-on-tangents" rel="noopener noreferrer"&gt;context-persistence instructions&lt;/a&gt; carry the protocol. Copilot reads that file as repository guidance, so the checkpoint triggers arrive with each new session rather than remaining in the chat that may disappear.&lt;/p&gt;

&lt;p&gt;The instruction is explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For any non-trivial, multi-step task, ensure &lt;code&gt;.local/scratch/task-state.md&lt;/code&gt; exists before the first edit. Create it only when absent.&lt;/li&gt;
&lt;li&gt;If it already exists, read and reconcile it with the request and working tree. Preserve unfinished state; never truncate the file to start a new task.&lt;/li&gt;
&lt;li&gt;Record the current focus, in-flight items and their status, the next step, open threads, and key findings.&lt;/li&gt;
&lt;li&gt;Checkpoint before switching focus or deep-diving, when a new sub-issue appears, and after a meaningful step completes.&lt;/li&gt;
&lt;li&gt;Re-read and reconcile the file when resuming.&lt;/li&gt;
&lt;li&gt;When completed history makes the active file hard to scan, move it to &lt;code&gt;.local/scratch/task-state-completed.md&lt;/code&gt; and promote any durable finding into tracked documentation in the same pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how I ensure snapshotting happens at the transition, not as cleanup after context is lost. It is still an instruction, not a transactional runtime guarantee. Its advantage is that the rule survives the session, the artifact is visible to both of us, and a missed checkpoint is reviewable.&lt;/p&gt;

&lt;p&gt;The storage choice is part of the same instruction. This repository explicitly lists &lt;code&gt;.local/&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt; because the contents are local until promoted. A current hypothesis, unfinished frame, or rejected path should not be pushed to every contributor as project guidance.&lt;/p&gt;

&lt;p&gt;Gitignored does not mean disposable. The devcontainer bind-mounts the repository workspace from the host. Rebuilding the container replaces the environment, not &lt;code&gt;.local/scratch/task-state.md&lt;/code&gt; inside that workspace. The stack is still there when the new container opens.&lt;/p&gt;

&lt;p&gt;I do not use VS Code chat memory as that recovery layer. A transcript may remain visible, but a new chat is not guaranteed to load it as working context; compaction can also remove the detail needed to resume. The task-state file has a stable path that the repository instruction tells the next session to read.&lt;/p&gt;

&lt;p&gt;The ledger has no fixed schema. This run uses &lt;code&gt;Current focus&lt;/code&gt;, &lt;code&gt;Ledger&lt;/code&gt;, &lt;code&gt;Chosen metadata&lt;/code&gt;, and &lt;code&gt;Boundaries&lt;/code&gt;; older states also use &lt;code&gt;Key findings&lt;/code&gt;, &lt;code&gt;Remaining&lt;/code&gt;, and &lt;code&gt;Gotchas&lt;/code&gt;. The headings follow the work.&lt;/p&gt;

&lt;p&gt;There is no literal stack API or required &lt;code&gt;Return&lt;/code&gt; field. I read &lt;code&gt;in-progress&lt;/code&gt; as the top frame, a pending item plus its next step as the return address, and open threads and findings as the state needed to resume. Completing the tangent and returning to that pending item behaves like a pop. Finished history moves into &lt;code&gt;task-state-completed.md&lt;/code&gt;; reusable findings are distilled into their tracked owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  I ask the session what remains
&lt;/h2&gt;

&lt;p&gt;Most of the time I do not open the task-state file myself. I ask the session:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What are my pending items?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent reads the ledger, reconciles it with the working tree, and gives me the unfinished items with their current status. I choose one. That item becomes the current work, and the session already has the findings, blockers, and next step recorded around it.&lt;/p&gt;

&lt;p&gt;The question works in the same chat, after a context compaction, or in a new session because the answer comes from the workspace artifact. I am not asking the model to recall an old conversation. I am asking it to query shared state, then continue from the item I select.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a protocol, not a file format
&lt;/h2&gt;

&lt;p&gt;Markdown under &lt;code&gt;.local/&lt;/code&gt; is how I implement the idea in these repositories. It is not the only way. An issue, worktree note, session database, or orchestration system can carry the same state.&lt;/p&gt;

&lt;p&gt;The direction matters more than the storage choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The state is shared between the human and the agent.&lt;/li&gt;
&lt;li&gt;It survives longer than one chat or internal plan.&lt;/li&gt;
&lt;li&gt;A tangent records its parent return address before consuming the context.&lt;/li&gt;
&lt;li&gt;Resume begins by loading that state, not reconstructing it from the current diff.&lt;/li&gt;
&lt;li&gt;Completed frames leave the active stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those properties, changing the filename does not solve the handoff problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared memory makes curiosity affordable
&lt;/h2&gt;

&lt;p&gt;I wrote about AI acting as a&lt;br&gt;
&lt;a href="https://www.likhansiddiquee.com/blog/velocity-microphone/" rel="noopener noreferrer"&gt;velocity microphone&lt;/a&gt;: it amplifies unclear intent and returns the consequences faster. It also makes investigation cheap. A side question can become a twenty-file audit before either of us feels the cost.&lt;/p&gt;

&lt;p&gt;I do not want to remove that curiosity from the workflow. I want a reliable way back.&lt;/p&gt;

&lt;p&gt;The ledger preserves the parent task's return address through context compaction, agent changes, and my own detours.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The velocity microphone: AI amplifies unclear intent</title>
      <dc:creator>Likhan Siddiquee</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:45:38 +0000</pubDate>
      <link>https://dev.to/likhan/the-velocity-microphone-ai-amplifies-unclear-intent-48i8</link>
      <guid>https://dev.to/likhan/the-velocity-microphone-ai-amplifies-unclear-intent-48i8</guid>
      <description>&lt;p&gt;Twenty minutes prompting. Three days repairing.&lt;/p&gt;

&lt;p&gt;That story is often used as proof that AI writes bad software.&lt;/p&gt;

&lt;p&gt;Sometimes it does. Models invent APIs, miss edge cases, and make poor design choices.&lt;/p&gt;

&lt;p&gt;But another failure is hiding inside that complaint: the classic tree-swing problem, compressed to machine speed.&lt;/p&gt;

&lt;p&gt;The engineer knows they want a swing. The brief asks for a “dynamic, scalable outdoor kinetic relaxation mechanism.” The model fills the gaps with a plausible direction. Nobody challenges the assumption.&lt;/p&gt;

&lt;p&gt;Twenty minutes later, the first real integration is also the first real test.&lt;/p&gt;

&lt;p&gt;A hallucinated API is a model failure.&lt;/p&gt;

&lt;p&gt;A coherent implementation of an unspoken assumption is an agreement failure.&lt;/p&gt;

&lt;p&gt;AI did not remove requirements engineering. It removed the waiting that used to expose weak requirements before they became architecture.&lt;/p&gt;

&lt;p&gt;The response is not one perfect prompt. It is a governance stack:&lt;/p&gt;

&lt;p&gt;• Durable repository instructions that preserve architectural intent&lt;br&gt;&lt;br&gt;
• Specialized agent and workflow files that define the job&lt;br&gt;&lt;br&gt;
• Task briefs with boundaries and acceptance evidence&lt;br&gt;&lt;br&gt;
• Scoped tools and permissions that limit the blast radius&lt;br&gt;&lt;br&gt;
• Hooks, tests, lint, schemas, security scans, and CI&lt;br&gt;&lt;br&gt;
• Human review and release gates for the remaining uncertainty  &lt;/p&gt;

&lt;p&gt;Instructions steer.&lt;br&gt;
Permissions constrain.&lt;br&gt;
Checks reject.&lt;br&gt;
Humans own the release.&lt;/p&gt;

&lt;p&gt;AI is a velocity microphone. It amplifies the signal, the ambiguity, and the consequences.&lt;/p&gt;

&lt;p&gt;I wrote about how to keep that speed useful:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.likhansiddiquee.com/blog/velocity-microphone/" rel="noopener noreferrer"&gt;https://www.likhansiddiquee.com/blog/velocity-microphone/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AgenticAI #SoftwareEngineering #AIGovernance #DeveloperExperience
&lt;/h1&gt;

</description>
      <category>agenticai</category>
      <category>ai</category>
      <category>aigovernance</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
