Skip to content

Factory M3 — Work items, labels and gates #114

Description

@artyomsv

Goal: work starts from a ticket, and autonomy is chosen per ticket.

Part of the software factory epic. M2 is delivered and unblocks this — PR #119 (2026-09-04),
and the loop was proved end to end on a live pull request on 2026-09-12 (runs
3987682681:1 and 3987682176:1 against artyomsv/spire-test#31: /fix comment → dispatch →
sandboxed agent → push to the pull request's own source branch → next review round → thread
resolved on the forge → verdict written to review_finding).

Delivers

  • spire-worksource plus GitHub Issues, GitLab Issues and Jira arms, reusing the existing
    spire-context-* adapters' HTTP clients — same hosts, same registry, wider rights.
  • Tracker webhooks on the gateway's keyed registry edge.
  • work_item bookkeeping — NOT a mirror. No title, no body, no status of the ticket
    itself. The tracker is the source of truth; owning a second one means owning sync, drift and
    permissions.
  • Autonomy profiles, label mapping, the operator ceiling, and a per-work-source actor
    allowlist
    .
  • Durable gates answerable from the dashboard, the tracker or a pull-request review, with
    expiry. Human takeover.
  • The Work items and Approvals screens.

Added after the first draft — decisions taken 2026-09-08 and 2026-09-10

These four were agreed after this ticket was written (2026-09-04) and are recorded in
docs/factory/ROADMAP.md § M3, docs/DECISIONS.md ("Deliberately deferred" under ADR-041) and
docs/superpowers/specs/2026-09-10-accounts-scopes-and-roles-analysis.md §7 and §11. They are M3
scope, not follow-up work.

  • Authorise /fix on push rights, not on authorship. The first live /fix (2026-09-10) was
    refused because the reviewer account's "May command this bot" list was empty and
    IntegrationSaga.requestFix denies by default. "The pull request's author may /fix" is
    unsafe as written — on a public repository that is any stranger (AUTONOMY.md Rule 3's
    drive-by contributor). The safe rule is the commenter can already push to this
    repository
    : owners, admins, collaborators and same-repository authors. All three forges
    expose a per-repository permission endpoint that answers it. Forks are already refused one
    layer down by FixTargets.whyNotPushable. The hand-kept list stays as the override in both
    directions
    — granting the command to somebody without write access, and taking it from
    somebody who has it.
  • Type a handle, store an id, show the handle. /fix matches on the stable provider user
    id on purpose, because a handle can change hands. But the form asks the operator to type
    3218389 and the Policy cell renders 1. Typing @artyomsv must resolve through the
    account's own credential, store the id, and render the handle back.
  • Workspace moves from the forge account to the repository. Today the account row carries
    the workspace as part of the lookup key (scm_provider UNIQUE (type, workspace, role)).
    That is the thing that changes. Assume one workspace per repository for now; a repository may
    later relate to more than one. Deferred here explicitly by ADR-041. This changes the
    registry key, so it needs its own ADR.
  • The repository becomes the centre of its own screen. Register a repository, list the
    accounts that may act on it, and list its webhooks — one per event kind (reviewer events,
    factory events, later issue events for a product-owner role). The Accounts form asking for a
    workspace goes away with the key change above.

Two things the first draft assumed and had to be designed instead

  • labelEvents carrying the actor, not a bare set of label strings. A set has no author, and
    the allowlist rule needs one. Where neither a webhook nor a tracker audit trail can attribute a
    label it is UNATTRIBUTED and selects nothing — the alternative is a rule that quietly
    applies only to labels a webhook happened to witness.
  • Policy re-resolved at every phase transition (FR-F30), with the profile version pinned at
    admission, lowest-label-wins, and retirement when a tracker issue moves repository.

Likely to need deciding here

M2's plan reconciliation (PR #110) left one question open that lands in this milestone: whether a
run aggregate exists at all.
DomainEvent carries no run member today — factory_run is
projected straight from cs.run-results, and the durable record of a run is factory_run +
llm_charge, not the event store (ADR-034). The work-item vocabulary sketched in
ARCHITECTURE.md §6 (WorkItemAdmitted, GateOpened, GateResolved, …) is the first thing that
would need one.

Exit criteria

  • A ticket labelled at each of three profiles produces three visibly different journeys.
  • A label naming a profile above the ceiling is clamped and says so.
  • A label applied by an actor outside the allowlist is ignored — and so is one whose applier
    cannot be determined
    .
  • Lowering the ceiling stops an in-flight item at its next phase.
  • A commenter with push access to the repository can /fix without appearing on any list,
    and a commenter without push access is refused — with the hand-kept list proved to override
    in both directions.
  • The allowlist form accepts @handle, stores the provider user id, and renders the handle
    back; a handle that cannot be resolved is refused at entry rather than stored as text.
  • A repository screen names its workspace, the accounts that may act on it, and one webhook per
    event kind — with no workspace field left on the account form.

FRs: FR-F16, F16a, F17, F22..F25, F30. Docs: docs/factory/ROADMAP.md § M3,
docs/factory/AUTONOMY.md, docs/DECISIONS.md (ADR-041 deferrals).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfactoryThe software factory subsystem (docs/factory)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions