Riddim Software Factory

An autonomous factory that builds and maintains real apps.

Requirements, development, QA, performance, release, and App Store optimization — every stage automated. epac is built and maintained on this line, end to end, with a receipt at every step.

The factory

Riddim Software Factory

The Riddim Software Factory is an autonomous software development operation. It picks up Linear issues, assigns them to AI coding agents, reviews the output against acceptance criteria, and ships to the App Store — without a human writing code. Every work order that passes through the factory leaves a public audit trail: the conversation that filed the issue, a PR opened by a bot identity, CI evidence, and a release.

The product

epac

epac is a live iOS app for tracking the Canadian Parliament — bills, votes, debates, MPs, sitting dates, and lobbyist registrations. It's the factory's primary product. Every app release is built and shipped autonomously from a Linear issue backlog, through CI/CD, to the App Store. The receipts below are three recent work orders from that live pipeline.

Canonical work items

Three recent work orders, with receipts.

Each started as a single chat message. The intake produced a Linear ticket. An autonomous pipeline carried it through to a public App Store release.

epac receipt no. 1940

Fix This Bug

Sunny's bug report. Parliament tab re-tap was yanking the calendar to January 2001 with no way back.

Calendar scrolls to Jan 2001 on Parliament tab re-tap; add prominent Today action and de-clutter toolbar

Estimate 8 points
Attempts codex ×2
Reviews 2 rounds
Diff +226 / −64
Files 12

The intake

One chat message. One reply. The factory took it from there.

2026-05-18 · 19:53 UTC

Sunny

there's a bug in epac where if i'm at the parliament calendar screen, and I tab the Parliament tab item, it scrolls the calendar all the way up to January 2001. There's no data there, and there's no way for me to return to Today.

We need a way to return back to Today on that calendar screen. Also, can we prevent tapping on the tab bar item from scrolling all the way to the top? Double tapping on a tab bar item scrolls that view to the top, that's standard iOS behaviour, so we'd be fighting the system. However, the calendar scrolls upside down so scrolling to the top is not taking us to the latest day. At the very least, we need an easy way to return to Today, and let's pay attention to how cluttered the toolbar is getting on that screen.

Can you do a RCA on this, use a worktree, reproduce it on the parliament calendar screen, then create Linear item in EPAC to fix it

Claude

Filed EPAC-1940 (Priority: High, Estimate: 8).

Root cause (high confidence, from code):

  • HorizonCalendar's CalendarScrollView is a vanilla UIScrollView that inherits scrollsToTop = true.
  • iOS's tab re-tap walks the active tab's view hierarchy and sends the first eligible scroll view to contentOffset.zero — same mechanism as the legacy status-bar tap.
  • The calendar's visibleDates starts at 2001-01-01 so "top" is January 2001. The year-picker is clamped to 2016+ so the chevrons can't even take you back.
  • Only existing escape: the tiny red dot in the legend at line 127, which looks like a swatch, not a button.

Ticket scope: stop the scroll-to-2001, add one canonical prominent Today action, and consolidate the Parliament toolbar (6 trailing items + 3-item principal) and Members toolbar.

Then the factory took it from there

The pipeline

What happened next, with receipts.

19:53 UTC · 2026-05-18 Linear ticket created via save_issue

Claude reproduced from code, drafted the ticket with two implementation options and toolbar de-clutter scope, and called the Linear MCP save_issue tool.

19:56 UTC Symphony dispatched a codex worker

Symphony saw the new Todo ticket, transitioned Linear to In Progress, and spawned a codex worker in a fresh worktree off origin/main.

20:17 UTC · 21 minutes later Developer-bot opened PR #494

codex finished the first implementation pass, committed the change in the worktree, and opened the PR as app/riddim-developer-bot — not Sunny. Clean audit trail: every commit is attributable to the bot identity.

20:27 UTC · 10 minutes later Reviewer-bot review · CHANGES_REQUESTED old two-bot workflow

Reviewer-bot ran and flagged that the PR omitted before/after screenshots and iPad verification. Verdict: request_changes. AC coverage: 7 covered · 1 missing · 1 unclear.

20:28 UTC Symphony re-dispatched in fix_existing_pr mode

Symphony saw the changes-requested state and spawned a second codex run, pointed at the existing PR with reviewer feedback in context.

21:58 UTC · 90 minutes later Reviewer-bot second pass · APPROVED

codex amended the PR with screenshots and regression tests. Two items remained "missing" (iPad verification, test isolation from real HorizonCalendar), but the verdict was approved-to-merge.

21:58 UTC Squash-merge · Linear → Done

PR #494 squash-merged to main. 2 hours 5 minutes elapsed from Sunny's chat message.

2026-05-19 · 17:48 UTC Submit App Store Review · v1.11 tag dispatched

The merge commit was tagged v1.11. The Submit App Store Review workflow ran on that tag and pushed the build into Apple's review queue.

soon after Live on the App Store

Apple approved the build. Anyone with the App Store can now install the version of epac that has the Parliament tab re-tap behaving, the Today button visible, and the toolbar de-cluttered.

The fix

Opt the calendar out of scrollsToTop. Add a real Today button.

PR #494 · 12 files · +226 / −64 · merged 2026-05-18 21:58 UTC

struct CalendarScrollsToTopDisabler: UIViewRepresentable {
  func updateUIView(_ uiView: UIView, context: Context) {
    DispatchQueue.main.async {
      // Walk up from the SwiftUI host until we find
      // HorizonCalendar's scroll view (matched by class
      // name), then opt it out of scroll-to-top.
      Self.disable(in: uiView.superview)
    }
  }
}

A UIViewRepresentable shim walks the SwiftUI view hierarchy, finds the HorizonCalendar scroll view by its runtime class name, sets scrollsToTop = false. Surgical — every other scroll view in the app keeps the expected iOS behavior.

The evidence

Same surface, two SHAs.

Before: base SHA 3f7dd322 · After: merge SHA 6ee535f5 · iPhone 17 Pro Max simulator, iOS 26.4.1. Committed to docs/review-artifacts/EPAC-1940.

Before: crowded Parliament toolbar with 6 trailing items and tiny red Today legend dot
Before · 6 trailing toolbar items. Today affordance is the tiny red dot at line 127 — reads as a legend swatch, not a button.
After: de-cluttered Parliament toolbar with Today button visible and grouped links menu
After · Links grouped to one menu slot. Today button in toolbar — disabled when today is already on screen.

Calendar retap fix (tab re-tap no longer scrolls to Jan 2001) is verified by CalendarScrollsToTopDisablerTests and XCTest harness.  → Full PR diff

epac × autopilot · project receipt

Self-Healing CI

Watched workflow fails → Linear issue. Workflow goes green → Linear issue closes. Symphonyd parks until the gate is satisfied.

CI Failure Feedback Mechanism — auto-create Linear issues when watched epac CI workflows fail, auto-close them when CI goes green again, and don't let symphonyd declare a project ready for handoff until post-merge CI has settled.

the motivating failure TestFlight Build run 26349982662 failed after merge of PR #550 (EPAC-1993). No Linear issue was created. CI stayed red unattended. The project's "ready for human" signal was untruthful.
Estimate 50 points
Issues 5 + 5 + 1
Dispatches 12
Diff +4,170 / −118
Files 40

The intake

The actual conversation that filed the project.

2026-05-24 · 03:23 → 06:57 UTC

Sunny · 03:23

Do we have a feedback mechanism to catch a failing post-merge run-on-main github workflow like Testflight Build?

If not, please scope it out autonomously using the arch team skill and the backlog team skill. I don't have time to elaborate any further than this: if any CI workflows fail post-merge, a Linear item should be created in the same project as the issue that caused the failing workflow, and set to TODO with high priority so that the CI can get green again. Keeping CI green is a crucial task in autonomous development.

Continue autonomously with the backlog team and arch team skills

Claude · 03:25 → 03:28

Gap confirmed. Existing ci-failure-linear.yml listens to Backend PR only and only on event == pull_request. TestFlight Build run 26349982662 — a push on main after merge — slipped through.

Moving on to backlog-team to scope the Project + child issues.

(11 minutes later:)

Done. CI Failure Feedback Mechanism (Urgent) with 7 child issues — 5 serial in EPAC (EPAC-2007 → 2011), 1 parallel weekly-heartbeat (EPAC-2012), 1 human-handoff (EPAC-2013). All DoR-passing. Score target locked at ~9.0/10.

Sunny · 06:55

I like adding the project closure gate to this in-progress project but we don't want to backload all of these GHA runs to the end of the project. Let's ensure every push to main is making progress towards generating evidence.

Claude · 06:57

Per-merge accumulation reuses the workflow_run listener already in EPAC-2008. The closure gate then becomes a pure read over already-accumulated state: all non-handoff issues terminal AND zero open ci-failure issues in the Project AND no in-flight watched workflow_run. Symphonyd swaps its stop condition from "all terminal" to "gate ready."

Adding two new child issues: an evidence accumulator (EPAC) and the closure-gate evaluator (autopilot). Project gets both EPAC and AUTO teams.

The intake was a single short prompt, an autonomous scoping pass that filed a 7-issue Project, and two human nudges — one to cancel a watcher (EPAC-2012), one to add a closure gate (AUTO-799). Final shape: 11 active issues across two repos.

The scope

Five EPAC issues, five Autopilot issues, one human handoff.

EPAC chain is serial (every issue edits the same script or workflow). Autopilot work landed as a foundation issue + three parallel adapters + one integration.

Issue Repo Title / file Pts Dispatches Worker PR
EPAC — serial chain
EPAC-2007 epac Extract policy into ci_failure_to_linear.py (foundation) 8 1 codex #571
EPAC-2008 epac Replace handler YAML + expand watched workflows 4 1 gemini #573
EPAC-2009 epac Dedup by (workflow_name, branch) 8 2 codex #578
EPAC-2010 epac Auto-close on success 4 1 gemini #585
EPAC-2011 epac Inherit projectId from originating PR's issue 8 1 claude #586
Autopilot — foundation, three parallel adapters, one integration
AUTO-803 autopilot ProjectClosureGate foundation (ports, result types) 2 1 codex #385
AUTO-804 autopilot LinearGraphQLClient adapter 4 1 codex #388
AUTO-805 autopilot GitHubRestClient adapter 4 1 claude #387
AUTO-806 autopilot EvaluateProjectClosureReadiness use case 4 1 codex #386
AUTO-799 autopilot Wire ProjectClosureGate into symphonyd (integration) 4 2 claude+codex #391
Cancelled / human
EPAC-2012 epac Weekly heartbeat workflow 2

EPAC-2013 is the human handoff — end-to-end verification on real CI runs, closure-gate behavior in a real Symphony dispatch, audit of old-handler residue.

Then the factory took it from there

The pipeline

What happened between filed and shipped, with receipts.

arch-team · ~5 min Inspect, score, propose

Claude inspected the existing handler, scored it 4.0/10 (Linear GraphQL inline in YAML; listens to 1 workflow; fires on PR events only), and drafted the architecture proposal.

backlog-team · ~11 min Project + 7 child issues filed

backlog-team skill ran the standard pass — read Linear standards, created the ci-failure label, filed the Project, paginated save_issue 7 times. All DoR-passing, all Todo, sequenced with blocked-by in serial lane.

04:46 → 14:56 UTC · 2026-05-25 EPAC serial chain ships — 5 PRs in ~10 hours

codex one-shotted EPAC-2007 (570 lines of new Python script, thin Linear client, thin GitHub PR-resolver, unit tests). Each PR unblocked the next. The factory mixed three workers across the chain — codex on extraction and dedup, gemini on handler replacement and auto-close, claude on originating-PR inheritance.

15:00 → 15:40 UTC Autopilot parallel fan-out — 4 PRs in 40 minutes

Symphony dispatched all four parallel pieces simultaneously. Each landed on its own merit; the integration (PR #391) waited for all four to merge.

16:17 UTC · 37 minutes later AUTO-799 integrates the closure gate into symphonyd

With all four sub-issues merged, AUTO-799 unblocked. claude + codex collaborated across 2 dispatches to wire the new evaluator into symphonyd's stop-condition. 452 lines added across 14 files.

17:12 UTC Human handoff complete · EPAC-2013 → Done

Sunny verified end-to-end behavior: forced a watched-workflow failure, confirmed the Linear issue appeared in the same Project, verified it closed when the workflow went green, observed symphonyd parking until the closure gate was satisfied.

The evidence

Same failure mode, two architectures.

Before: the single handler that existed when Sunny pasted the failing run URL. After: the new handler + Python policy script + ProjectClosureGate.

Before

One handler.
One workflow.
PR events only.

.github/workflows/ci-failure-linear.yml only

Listens to 1 workflow (Backend PR). Fires only on event == 'pull_request'. TestFlight Build, App Store deploys, Backend deploys — all silent on post-merge failure. Linear GraphQL curl inline in YAML heredoc; hardcoded team ID. symphonyd declares "ready for human" when all issues terminal — even if a watched workflow is about to fail.

arch score: 4.0 / 10
After

Adapter + policy.
12 workflows.
Closure gate.

ci-failure-handler.yml (adapter) + scripts/ci/ci_failure_to_linear.py (policy, unit-tested)

Listens to 12 workflows: TestFlight Build, App Store Review, Backend Deploys, Evidence regression… Fires on failure (open / comment, cap 5) and success (auto-close). Dedup by (workflow_name, branch). Inherits projectId from originating PR's EPAC issue. ProjectClosureGate: symphonyd parks until all issues terminal, zero open ci-failure in Project, no in-flight watched workflow_run.

arch score: ~9.0 / 10
epac project receipt · epac-1984 → 1997

31 violations.

What lint reported the first time cyclomatic_complexity was turned back on. 15 files. Max 20.

Enforce cyclomatic complexity ≤ 5 across the iOS codebase — drop the SwiftLint error threshold from 50 to 5 and block CI on violations.

Estimate 49 points
Issues 12 + 1 + 1
Dispatches 337
Diff +2,035 / −1,277
Files 21

The intake

The actual conversation that filed the project.

2026-05-23 · 20:34 → 20:49 UTC

Sunny · 20:34

please show me the epac repo's swiftlint ruleset

Claude · 20:35

Here's the epac SwiftLint configuration. cyclomatic_complexity is in disabled_rules with the rationale: "Deep-link routers and migrations are inherently branchy."

Sunny · 20:40

let's move the threshold to 5 and enable it

Claude · 20:41

Here are all the violations, sorted by complexity:

20 · Fetch.swift:210  ·  16 · BillsService.swift:65  ·  16 · ContentView.swift:334  ·  14 · XMLBro.swift:28  ·  … and 27 more, all ≥ 6.

Total: 31 violations across 15 files. Max complexity 20.

Sunny · 20:43

warning: 5, error: 50 so nothing is an error yet. Please create those Linear items now, use the backlog team skill

Claude · 20:49

All done. Project: Enforce cyclomatic complexity ≤ 5 across iOS codebase

12 parallel implementation issues (all Todo, all-clear to pick up in any order) — EPAC-1984 through EPAC-1995. Estimates 2–8 points each.

Integration (blocked on all 12): EPAC-1996 — lower error threshold from 50 → 5.
Human handoff: EPAC-1997 — verify CI on main, update CLAUDE.md.

The whole intake — from "show me the ruleset" to 14 Linear issues filed — was 15 minutes.

The scope

12 implementation issues, 1 integration, 1 human handoff.

Each row is one PR. Each PR clears its file's violations. Integration locks the threshold once all merge. "Dispatches" is how many times Symphony spawned a worker before a passing PR was opened.

Issue File / cluster Worst · count Pts Dispatches Worker PR
EPAC-1984 Fetch.swift 20 · 6 8 17 codex #546
EPAC-1985 ContentView.swift (deep-link) 16 · 2 4 1 codex #542
EPAC-1986 BillsService.swift 16 · 1 4 160 claude #557
EPAC-1987 XMLBro.swift 14 · 2 4 1 codex #543
EPAC-1988 LobbyistService.swift 13 · 3 4 4 codex #547
EPAC-1989 Expenditures+CSV.swift 13 · 2 4 38 codex #552
EPAC-1990 CalendarExportService.swift 12 · 2 4 15 claude #553
EPAC-1991 SearchViewModel.swift 10 · 1 2 87 claude #554
EPAC-1992 MyMPView.swift 10 · 1 4 10 codex #555
EPAC-1993 5 small service files 8 · 6 4 1 codex #550
EPAC-1994 BugTests.swift (extract parsePerson) 12 · 2 4 1 codex #548
EPAC-1995 Toronto / Vancouver city council parsers 6 · 2 2 1 codex #549
EPAC-1996 .swiftlint.yml (integration) — · 0 1 1 claude #558

EPAC-1997 is the human handoff — verifies CI green on main, spot-checks .swiftlint.yml, updates the SwiftLint baseline section of CLAUDE.md.

Then the factory took it from there

The pipeline

What happened between filed and shipped, with receipts.

20:45 UTC · 2026-05-23 Project + 12 implementation + integration + handoff filed in Linear

Claude paginated save_issue 15 times — once per issue, once for the project. Each implementation issue is file-scoped; mergeability lane is parallel. Integration is blocked-on-all. Handoff is parked.

00:26 UTC · 2026-05-24 · 4 h later Symphony begins parallel dispatch

Symphony polled Linear, found 12 eligible Todo issues in the parallel lane, transitioned each to In Progress, and spawned a worker per issue in its own fresh worktree off origin/main. Integration (EPAC-1996) stayed parked — blocked on all 12.

00:49 UTC · 23 minutes in First PR opens — #542

codex one-shotted EPAC-1985 (the ContentView deep-link handlers). Refactored two complexity-16 functions down to ≤ 5 by extracting per-URL-shape handlers. pr-build green; automerge armed.

00:54 → 02:44 UTC Wave 1 lands — 7 PRs merged in ~2 hours

The easy wins clear first, mostly codex one-shots: ContentView, XMLBro, Fetch.swift (the worst-case file, complexity 20), LobbyistService, BugTests parsePerson, city council parsers, small services cluster. CI is the only gate.

05:33 UTC · the only human moment Sunny merges PR #551 — hook path fix

An unrelated rename had broken a path inside a pre-commit hook. Workers on EPAC-1986 and EPAC-1989 were bouncing because the hook couldn't find its script. Sunny patched it in 4 lines. The autonomous workers picked back up the moment main was healed.

05:55 → 15:18 UTC · the long tail The hard files: 38, 87, 160 dispatches

Symphony kept dispatching the stuck issues. Each retry is a fresh worker in a fresh worktree, given the issue + hints distilled from the last failed build. The system mixed claude and codex per attempt to break out of local minima.

19:34 UTC · 19 h after first dispatch EPAC-1986 lands — 160 dispatches in

BillsService.bill(_:parliament:session:) is the static mapping that converts a raw LEGISinfo bill payload into the domain Bill. 160 attempts across codex and claude; claude was the run that found the shape that satisfied lint and kept the existing tests green.

19:43 UTC · 9 minutes later Integration lands — PR #558

With every implementation merged, EPAC-1996 unblocks. Symphony dispatches a single claude worker; the diff is two lines (error: 50error: 5). The gate flips: any new function over complexity 5 will now fail CI.

19:48 UTC · 5 minutes after the gate flipped Human handoff complete · EPAC-1997 → Done

Sunny confirmed the SwiftLint CI job passed on main, spot-checked that .swiftlint.yml reads error: 5, and updated the CLAUDE.md SwiftLint baseline section. Total elapsed from project filed: 23 hours.

The evidence

Same lint command, two SHAs.

Before: branch swiftlint-cyclomatic — the branch where Sunny flipped the rule back on. After: main at 2402270 — the merge commit of integration PR #558.

Before

31 violations

across 15 files.
Max complexity: 20 (Fetch.downloadExpenditures).
Threshold: warning: 5, error: 50 — CI passes as warnings.

$ swiftlint lint --quiet ios | grep cyclomatic
Fetch.swift:210: warning: Function has complexity 20
Fetch.swift:362: warning: Function has complexity 13
Fetch.swift:478: warning: Function has complexity 12
...
VancouverCityCouncil.swift:42: warning: complexity 6
— 31 warnings total —
After

0 violations

across 0 files.
Max complexity: ≤ 5 (every function in ios/).
Threshold: warning: 5, error: 5 — CI blocks any new violation.

$ swiftlint lint --strict --quiet ios | grep cyclomatic
(no output)

$ echo "exit $?"
exit 0

The gate is now permanent. Any PR that introduces a function with cyclomatic complexity > 5 will fail pr-build on its SwiftLint job — squash-automerge won't fire. The codebase is locked at the new floor.