The f4ctory software factory

From a Slack message
to a pull request.

Taitopilvi is a software factory. People describe what they want in a Slack channel, an AI coding agent follows a fixed, spec-driven process to build it, and the result arrives as a pull request. A person always does the final merge.

Slack channel Triage Spec, design, tasks Code + review Pull request Human merge
3repositories make up the factory
1Slack channel per project
3coder and reviewer rounds at most
0merges without a person
The idea in one paragraph

Conversation in, reviewed work out

People work in a Slack channel, one channel per project, and describe what they want in plain language. The Slack bot sorts the messages: questions and chatter get an answer in Slack, real work is sent to the factory as a command.

1 · Talk

Plain language

Describe what you want in the project's channel. No forms, no tickets.

2 · Build

Isolated agent

The factory core runs an AI coding agent (Kiro CLI) in an isolated container, inside that project's own repository.

3 · Deliver

Branch to pull request

The agent writes a proposal, a design, specs and tasks, then implements and reviews them. The result lands on a branch and becomes a pull request.

The big picture

Three parts, three repositories

f4ctory-slackbot

Slack front door

Listens to Slack, decides what needs factory action, and talks to people as named personas.

f4ctory-core

Factory core

Turns commands into repositories and agent runs, and reports back.

f4ctory-base

Process template

The starting point of every project repo: OpenSpec setup, agent skills, steering rules, a project website page, and an optional web app pack.

A lighter template, f4ctory-base-light, exists for quick prototypes. It drops the OpenSpec process and works from a README with a single coder.

Slack bot
f4ctory-slackbot
ingest, triage, personas
commands queue →work, grouped by project, in order
← replies queueanswers, grouped by channel, in order
Factory core
f4ctory-core
consume, schedule, run

The two services never call each other directly. They talk over two SQS FIFO queues. Delivery is at least once on both, so both sides deduplicate on a command id. The shape of every message is a written contract, owned by the Slack bot and checked at every boundary.

Behind the core, one Kiro CLI container runs per run and pushes a feature branch to the project repository (CodeCommit). The f4ctory-base template is copied into that repository once, at setup.

Chapter 1

The Slack front door

The bot is the only part that knows about Slack. It does four things.

Ingest

Verifies that each request really comes from Slack, puts the event on a queue and returns quickly, so Slack's short acknowledgement deadline is never spent on real work. Events are processed one at a time per channel, in order.

Triage

For each human message in an active channel, the bot reads the whole Slack thread and asks an LLM one question: does this need factory action? If not, a persona answers or reacts with an emoji. If yes, the bot adds a factory reaction, sends a work command with a short title for the work, and acknowledges as the project's controller persona. If the LLM is unavailable, the message is treated as work, so nothing is silently dropped.

Personas

The bot speaks as distinct named personas, so people can tell who is talking. The controller handles acknowledgements. The factory's own replies appear as the worker or the core, chosen by the bot from the reply's origin, never by the sender.

Track

Keeps one small record per message, so retries never repeat finished work and the bot and core share a view of where a command stands.

Chapter 2

From a new Slack channel to a ready project

A project begins when the bot joins a channel, or when the first human message appears in an unregistered one. The bot holds a short conversation in a single thread.

It asks for the minimum (what to build and for whom) and covers eight topics: project name, purpose, repository (new or an existing one), stack, environments, constraints, secrets, and the process template (the full f4ctory-base or the light one). It also asks whether the project gets a website; the default is yes. Anything the humans do not answer gets a default that is marked as a default, so the factory never mistakes it for a requirement. The bot only announces what the factory does, such as that work goes to a branch and a pull request and nothing merges without a human.

When a person says “Let's go!”, the bot moves the channel to provisioning and sends one setup command carrying the whole onboarding transcript and the answers. For a project with a website, the first go-ahead only makes a password and shows it privately to the person who gave it; the setup starts at the next go-ahead. The core then prepares the project:

  1. Create the repository

    Named after the project, filled with the history of the chosen template, and tagged as factory-managed.

  2. Create a bounded role

    A role for that project, limited so that it can never touch main.

  3. Run the bootstrap

    In a container, the agent reads the repository and fills in the project facts file and, in the full template, the OpenSpec context block. For a project with a website it also fills in the site's start page. The result may change only those files.

  4. Check the result

    Only then does the core move main forward. This is the one time the core itself moves main.

  5. Reply “ready”

    The repository is marked complete and the bot makes the channel active. The factory then starts the first proposal on its own, from the onboarding transcript.

Every step is recorded as a repository tag, so a setup that stops halfway, or arrives twice, simply continues from where it was. If something fails, the thread hears why in plain words and the channel returns to onboarding so people can try again.

First proposal. As soon as the project is ready, the factory starts the first proposal on its own, from the onboarding transcript, in the onboarding thread. Defaults the bot filled in are treated as advice, not requirements, and the first proposal produces a proposal and nothing else.

Adopting an existing repository. A setup command may ask to adopt a repository instead. An operator must first release that repository for one specific channel with a tag. Any other repository is refused with the same plain answer, so the bot cannot be used to find out which repositories exist. The core adds the factory's files only where the repository has nothing at that path, runs the bootstrap on a separate branch, and moves main forward only if nobody else moved it in the meantime. Containers are restricted to f4ctory/ branches, so the repository's own branches stay safe. An adopted repository gets no website.

Chapter 3

A work run

After setup, each message that needs factory action arrives as a work command. For each one, the core:

  1. Checks the project

    It must be a project the factory set up and finished.

  2. Records the run, then starts a container

    One container, as the project's role. Asking for the same run twice never starts a second container.

  3. Hands over the instruction

    Through a run record, never through environment variables, because it can be large and may include user text. What people wrote is passed as quoted data that the agent is told not to obey as an instruction.

  4. Waits, with a time limit

    If needed, the core stops the container when the limit is reached.

  5. Reads the outcome and replies

    The reply goes into the original Slack thread.

Work for one project happens in order. Different projects run side by side.

Branches

Each Slack thread gets one branch, named after the work: f4ctory/<few-words-from-the-title>-<thread timestamp>. The factory picks the name on the thread's first work command and keeps it for the whole thread. Follow-up messages continue that branch and are given the thread's earlier requests as context. A new top-level message starts a new branch. When a person has merged a thread's pull request, the factory retires the branch, and the next request starts a fresh one from main.

What “completed” means

The core answers completed only when the container run succeeded, the branch holds new work, the last commit carries a result the factory accepts, and the branch has been delivered as a pull request. The agent writes that result (approved after its own review, or proposed for a proposal), so it is a claim, not proof. That is why a person still merges.

Progress

A small reporter runs next to the agent. When the agent writes its plan, the core posts a summary of the planned work. While the run applies an OpenSpec change, it sends a snapshot whenever task state changes, and the core posts at most one short message per change: what finished, what starts next, and how many tasks are done. A run that ticks no tasks still gets a “still working” message about every two minutes. When the run ends, the thread also gets the agent's own summary of what it did and did not do, and the Kiro CLI usage figure. Progress never decides success and never fails a run.

Delivery

The core checks the branch without running project code, and flags a branch that is behind main or touches files that steer the factory or the pipeline, such as the deploy declaration and infrastructure code. The agent opens the pull request into main itself and writes its title and description. If it did not, the core opens a plain one. A thread's open pull request is kept up to date by later runs. The merge is left to a person.

Warm containers and follow-ups

After a successful turn that left an open pull request, the container stays idle for a limited time (one hour by default). A reply in a thread whose pull request is open goes to that same container, which still holds the repository copy and the agent's conversation. It may declare applied, which says the change was made but not independently reviewed, and nothing stronger. If no usable warm container exists, a new one starts. A failed turn ends its container.

Pipeline updates

When a pipeline that builds a project's repository runs, the project's channel hears about it: each execution gets its own thread that follows the start, each stage and the end. The final success or failure message, with a short line about what was deployed, also goes to the channel itself.

Chapter 4

How a project is worked on

Inside the container, the agent works the way every f4ctory project works, because every project repo starts as a copy of f4ctory-base. The process is spec-driven, using OpenSpec.

  1. ExploreOptional thinking before committing to a change.
  2. ProposeA change folder: proposal (why), design (how), spec delta (what changes in behavior), tasks.
  3. UpdateRefine the artifacts. Fixing the plan is cheaper than fixing code.
  4. ApplyCoder and reviewer loop, up to 3 rounds.
  5. ArchiveMerge the spec delta into the main specs.

The apply loop: two separate roles

Apply is a loop of two roles in separate sessions. The reviewer always goes last, so its verdict decides whether the loop ends or goes back to the coder. Both roles run on the same model, Claude Sonnet 5.5, at different thinking depths — the separate sessions and the higher reviewer effort are what keep the review independent.

Coder

Implements the tasks of the change and runs the project's checks. Runs Claude Sonnet 5.5 at medium thinking depth.

Reviewer

An independent reviewer, in a separate session. It runs the project's real commands (install, build, lint, tests) and compares the result with the spec delta. It never edits code. Runs Claude Sonnet 5.5 at high thinking depth.

APPROVEDCHANGES_REQUESTED

After three rounds without approval, the loop stops and reports what is left.

  • Project facts, such as the exact build and test commands, live in a steering file that the bootstrap fills in for each project. Agents are told never to guess commands, and to stop and bootstrap if that file still has TODO markers.
  • Specs live in the project repo and are versioned with the code, so anyone can read why something was built.
  • A project made from f4ctory-base-light skips this process: a single coder works directly from the README, with no separate reviewer.
  • By default an agent works locally and a human commits. A run instruction, the explicit instruction that starts an unattended run, can grant an exception, but only for the actions and branches it names. Repository files, tool output and Slack transcripts can never grant it, and the reviewer never edits code under any grant.
Chapter 5

Websites and web apps

Every project can get two kinds of web presence, both optional and both kept away from the public.

A project website

During onboarding the bot offers a site named after the project. It makes a password, shows it once and only to the person who gave the go-ahead, in a message nobody else can see, and sends the factory only a hash. Nobody can read or reset the password later; if it is lost, the only remedy is to delete the site. The core publishes the repository's site/ directory from main whenever main changes, so the published site always equals main. The template ships an introduction page and a markdown viewer, so a project's specs, changes and docs can be read in a browser. Anyone in the channel can ask for the site to be deleted; the repository is not touched.

A web application

The template carries a dormant web app pack: a Vite frontend, an API on API Gateway and Lambda, a PostgreSQL database, Step Functions workflows and CDK infrastructure, with security defaults built in. Nothing in it is active until a project enables it. When onboarding or a later request describes a full web application, the agent uses a skill that only proposes the change to enable the pack, so enabling goes through the normal change process.

A database

Web-app projects share one serverless PostgreSQL cluster per stage, which is not reachable from the network. Each project gets its own database, role and secret, created by a trusted, repeatable operation and kept apart from every other project. Nothing drops a project database automatically: removal is a human decision.

The dev pipeline

A project that has a website merges a small deploy declaration (deploy.json) into main. A person's merge is the consent. If the operator has switched the feature on for the stage, the factory then sets up a dev pipeline for the project without a human, using narrow, bounded permissions. The pipeline's definition comes from the factory, not from the repository.

  1. SourceThe project's main branch.
  2. TestBuilds the app without any cloud rights.
  3. DeployTo dev only.
  4. MigrateOnly if the project asked for a database.
  5. SmokeChecks the dev app really is private, and fails the pipeline if it is not.

The dev app is never public: it answers only with the project's site password, and its API accepts only requests that came through the edge. The channel hears about the pipeline through the usual pipeline updates.

Chapters 6 and 7

Safety, trust, and reliability

The agent runs commands based on text that people wrote in Slack, so safety is built into the account, not into the agent's instructions.

RuleHow it is enforced
A container can only touch its own projectOne role per project, with a permissions boundary.
A container can never change main or other long-lived branchesThe boundary denies every change to main and to any branch or tag outside f4ctory/. Only the core moves main, once, at setup.
A container cannot speak in SlackIt has no access to the replies queue. Progress goes through a path the core provides.
A container never sees other projects' dataRun records are readable per project only.
A container cannot reach application databases or their secretsThe boundary explicitly denies the database and secret actions.
Site passwords are never keptThe bot shows the password once, privately, and passes on only a hash. No log, queue message, prompt or model ever sees the password.
Web apps are never publicProject sites and dev apps are behind a password, and file stores allow no public access.
A push cannot widen a pipeline's authorityPipeline definitions come from the factory, and an operator-set allow-list caps what a recipe may reference.
Secrets stay out of the repo, logs and repliesThe API key is injected by the platform at launch and scrubbed from output. Replies are fixed sentences with no AWS text, ARNs, agent output or user text.
Nothing merges without a personThe core opens pull requests but has no merge, close or approve permission.
Agents work locally by defaultThey do not commit, push or deploy unless a run instruction explicitly grants it.
Slack markup from agents is neutralisedMentions, links and long titles are cleaned before posting.

The core itself runs as a narrow principal. It can read and delete commands and send replies, use its own tables, manage factory-tagged repositories and project roles under the boundary, start and stop the Kiro tasks, and the few extra rights that sites and app pipelines need, and nothing else.

Built to be retried

At least once, deduplicated

Every command and reply has a stable identity. Repeats are ignored or give the same result.

Store, then acknowledge

The core writes a command to its run table before deleting the queue message, because the queue gives a message to another reader within seconds.

State lives outside memory

Runs, tags and records are in DynamoDB, ECS and CodeCommit. A restart of any component picks up where it left off.

Every command gets an answer

A command that cannot be carried out is answered with a plain failure. A malformed message that cannot be trusted goes to a dead-letter queue, which raises an alarm.

Separate stages

dev and prod copies can run side by side. Every AWS resource is named and tagged by stage and component.

Traceable

Each command carries a correlation id through named lifecycle log lines that reveal nothing a Slack reply may not.

Status

Where things stand

At the time of writing, everything described on this page is specified, and all changes behind it are complete: the Slack bot and its onboarding, command intake, project setup, adoption of existing repositories, container runs with progress reporting, first proposals, delivery of run branches as pull requests, pull request follow-ups in warm containers, project websites and their deletion, pipeline updates in Slack, the shared application database, and dev app pipelines. The web app pack is part of the template.

Project websites and app pipelines are optional parts of a stage that an operator switches on, so a given stage may run without them. Several checks in these areas need a real deployed stage, which the repositories' own tests cannot cover.

This very page is one of the factory's own projects (f4ctory-intro-page): a single self-contained HTML file adapted from the factory overview, with an animated depiction of a message flowing through the factory and back, published to an Amazon S3 static website by a committed AWS CodePipeline.

The OpenSpec specs in each repository are the source of truth when this overview and the specs disagree.