Adeel Nayyer
← All work

AI / Developer tooling

Software AI Factory

A local board that relays Azure DevOps Work Items and GitHub issues through pipelines of Claude Code runs, each in its own git worktree, and asks a human to approve any tool outside an allowlist.

Role
Sole engineer: design, build and ship
When
2026
Source
Private · walkthrough on request
Stack
  • Bun
  • Hono
  • SQLite
  • React
  • Vite
  • shadcn/ui
  • Claude Code CLI
  • Web Push
  • 444 commits between 20 Oct and 10 Nov 2026, with seven ADRs recording the board's rules.
  • Runs work items from Azure DevOps and GitHub on one board, each in its own isolated worktree and branch.
  • Reviews open Azure DevOps pull requests automatically, and votes 'Waiting for author' when no work item is linked.
Software AI Factory: work items in, reviewed pull requests outBROWSERLOCAL SERVER · BUNAGENTS & STATEEXTERNALBoard PWAReactHono API + pollerbuckets pipelineClaude runnerclaude -p per itemSQLitework item stateApproval MCPoutside allowlistGit worktreesone per work itemAzure DevOps · GitHubbacklog, PRsREST · SSE · pushstateedits, commitstool requestapprove / denypoll, update
Software AI Factory: work items in, reviewed pull requests out

Problem

Running Claude Code on real tickets means repeating the same setup by hand. You make a worktree and branch, paste the ticket, watch for permission prompts, move the ticket in the tracker and clean up afterward. Do that for several tickets at once across Azure DevOps and GitHub and things start slipping: a worktree nobody deletes, a branch made from unpushed local commits, a run stuck on an approval no one saw. Software AI Factory puts all of it on a board. Work items are lined up into pipelines of steps, and each step is a Claude Code run that the board starts, watches and moves along.

My role

I was the sole engineer and handled design, build and shipping; git history shows a single author across 444 commits. I wrote the domain glossary (CONTEXT.md), the ADRs, the Bun server, the React PWA, and the launchd service script that runs two instances (work and personal) side by side.

Architecture

A Bun process serves a Hono API and the built PWA on 127.0.0.1 and keeps its state in SQLite (bun:sqlite). Tracker gateways talk to Azure DevOps and GitHub through the user’s az / gh sign-in, with a PAT in the macOS Keychain as the fallback. A poller refreshes the backlog and moves lined-up work items into a pipeline once they carry the ready tag.

Each run spawns the claude CLI headless with stream-json input and output. The bucket’s allowlist goes in --allowedTools. Any other tool call is routed through --permission-prompt-tool to a small MCP server the board starts locally, where it waits for Approve or Deny in the Inbox. The browser gets events over server-sent events, and Web Push notifications fire when a run finishes, fails or gets blocked.

Key decisions

  • A bucket either runs a prompt or sets a tracker state, never both (ADR 0001). That way it’s predictable what a drag will do. Later amendments let prompt buckets post their result as a comment and applied the same rules to GitHub.
  • The factory owns the worktree; the prompt doesn’t (ADR 0002). Each work item gets <tracker>-<number>/repo and artifacts/. Clean Up deletes exactly that folder and branch, and its confirmation lists only the commits no other ref holds. After a real incident, branches now start from the remote’s base branch instead of the local one.
  • Work items advance on their own (ADR 0003). Anything short of a clean success stops the relay with a sticky notification. Polling stands in for webhooks, which a local app cannot receive.
  • GitHub as a second tracker, with only open and closed as states (ADR 0004). I rejected labels-as-states and Projects fields because GitHub does not enforce them.
  • PR reviews sit outside pipelines (ADR 0005). Every PR gets the same single review, so pipeline vocabulary would add nothing.
  • Nudges steer a live run (ADR 0006). Input stays open, so a message can be heard next or now. A “now” message is sent as an interrupt control request followed by the message, because the CLI’s priority flag alone did not cancel tool calls.
  • shadcn/ui for behaviour only (ADR 0007). The look comes from the board’s own tokens, which shadcn’s variables are mapped onto.

Outcomes

  • Tests run the in-process API against fake tracker gateways, a scripted fake Claude runner, a recording push sender and throwaway git repos, with a fresh SQLite database per test.
  • Two isolated instances run as launchd agents, each signed in to its own Claude Code account.