Skip to content

feat(autopilot): the ticket flow is a graph in the Workflow app that Autopilot runs - #285

Merged
hoangsnowy merged 10 commits into
mainfrom
feat/ticket-flow-graph
Oct 24, 2026
Merged

hoangsnowy merged 10 commits into
mainfrom
feat/ticket-flow-graph

Conversation

@hoangsnowy

Copy link
Copy Markdown
Owner

Change description

Ticket flow. Autopilot now runs, for every ticket, a graph that the owner sees and edits in the Workflow app. Before this, the flow was hard-coded twice: once in the TicketWorkflow MAF edges and once in the AutopilotPlanner rules. ADR-0014 records the decision.

  • Model (Modules.Pipeline/Agents/TicketFlowDefinition.cs): TicketFlowDefinition holds ordered step nodes (Code, Checks, Review, Test, Merge) and conditional edges (Pass → next step, Fail → back to Code).

    • Each agent node names its AgentRoles key: IssueWork, Reviewer or Acceptance.
    • A node can carry label filters: skip tickets labelled …, or run only for tickets labelled ….
    • TicketStepKinds is the extension point. A new kind (the Design/screenshot gate is next) is one enum value, one registry entry and one executor.
    • Default reproduces today's flow exactly.
  • Safety invariants (TicketFlowValidator, plain-language reasons): the flow starts at Code and ends at Merge, and Checks always runs before Merge (it can't be turned off or filtered). Every ticket, whatever its labels, passes Review or Test; every combination of the filter labels is checked. The only cycle allowed is a fail edge back to Code. Every step must be reachable, steps stay in their natural order, there is one step per kind, and each step's role must be one that can run it.

    • These are checked on save and on every load. A stored flow that breaks a rule is refused, the built-in flow runs instead, and the reason is logged once per board.
    • TicketFlowPlan.SafetyProblem re-checks the per-ticket plan before a MAF graph is built.
    • AutopilotPlanner.WithinCaps stays the outer guard.
  • Storage: a new workspaces.ticket_flows table in the Workspaces module, the module that owns board and Autopilot state.

    • Append-only versions record who and when. Revert appends a copy, and the change is audited as ticketflow.changed.
    • Each tenant has a default flow, and a board can have its own, which wins.
    • The table is under RLS, and every store method is tenant-explicit.
    • Migrations are Workspaces/AddTicketFlows and Sessions/AddTicketFlowVersion. The in-memory store keeps the standalone Web editable.
  • Engines:

    • TicketWorkflow.Build(steps, plan, …) builds the MAF WorkflowBuilder graph from the ticket's resolved plan, replacing the hard-coded edges.
    • The ticket snapshots the flow version it started with in sessions.TicketFlowVersion. Running tickets keep that version, including the step-by-step path after a restart. New tickets use the current version.
    • AutopilotPlanner reads PlanTask.Flow: there is no review move when Review is off for the ticket, and no test move when Test is off.
    • Fix rounds are still the persisted FixRounds against Autopilot:MaxFixRounds. There is no second counter.
  • Workflow › Ticket flow is a new tab in the Workflow app. Admins can:

    • see the steps as studio node cards (with each agent's character), or as a table view;
    • turn a step on or off, pick its agent and set its label filters;
    • see validation inline, then Save, Discard, Revert to an earlier version, or "Use the default flow" for a project;
    • see the live step a running ticket is on, read from TicketWorkflowRunner.

    Members see the same view read-only. Everything is keyboard accessible (role=switch, aria-pressed and named buttons) and uses tokens only. Nothing animates at rest: only the spinner of a running step moves, and it stops under prefers-reduced-motion or when Animations is off.

  • Board › Autopilot summarises each project's flow in one line ("Code → Checks → Review → Test → Merge") and has a Ticket flow button that opens the view on that project.

  • Docs: README (Autopilot section), docs/CONFIG.md (new "Workflow › Ticket flow" section), docs/plans/autonomy-levels.md (item 3d), and ADR-0014.

Claude Design push is pending: the new Ticket flow screen and its tf-* styles still need /design-sync (DesignSync write_files). This non-interactive session had no design-system auth.

Type of change

  • New feature (new agent / endpoint / capability)
  • Bugfix
  • Refactor (no behavior change)
  • Documentation / README
  • Infrastructure / CI configuration

Related issues

Follows #270 (one ticket = one MAF workflow), #278 (one agent registry) and #279 (Settings › Autopilot + fix rounds).

Test plan

  • Unit tests (tests/AgentOs.Tests):
    • TicketFlowDefinitionTests: every invariant, a JSON round trip, label resolution, and the default being today's flow.
    • TicketWorkflowTests: all existing tests stay green. New ones cover a custom flow with Review skipped for type:chore, a flow with Test off for type:docs (review send-back still counted), an unsafe plan never being built, and the plan/version snapshot.
    • TicketFlowServiceTests: run on both the in-memory and EF stores. They cover versioning and revert, invalid saves refused with nothing stored, members refused, the board's own flow winning, two tenants isolated, a task keeping its version, and a stored invalid flow being refused at run time.
    • AutopilotPlannerTests: Review skipped → straight to testing; Test skipped → merge after review, with the right reason; WithinCaps still caps flow moves; labels are resolved per task.
    • TicketFlowStylesheetTests: no idle animation, tokens only, no phantom classes.
  • Release build: 0 errors. The one warning is the existing ASPIRE010 from the AppHost's Aspire SDK, which this PR doesn't touch. The full dotnet test --solution AgentOs.slnx -c Release run passed 1756, skipped 80, failed 0.
  • Standalone E2E (TicketFlowTests, RUN_AGENTOS_E2E): renders the 5 steps and the table, shows that Checks can't be turned off, saves Review off as a new version, refuses Review+Test off with the reason ("Turn on Review or Test…"), discards, then reverts. It leaves zero residue. The whole standalone E2E suite is green (35 passed, 20 gated).
  • Screenshots (Playwright, light and dark; not committed):
    • Graph: five cards Code → Checks → Review → Test → Merge with each agent's character, "pass" arrows, and the "Fail → back to Code … up to 3 automatic fix rounds" lane.
    • Table: the same flow as a table with columns Step / Runs / Agent / For tickets / When it passes / When it fails / Now.
    • Refused: the red "This flow can't run as it is: Turn on Review or Test" panel, with Review and Test cards dashed and "Off".
    • Versions: v1…v4 with author, time and Revert buttons.
  • Real Postgres (a throwaway postgres:18.3 container, Web booted against it):
    • both migrations applied;
    • workspaces.ticket_flows has RLS enabled and forced, with its policy;
    • RowLevelSecurityRealDbTests, now including ticket_flows, is green;
    • the Ticket flow E2E against the DB-backed Web is green, and the rows have the right versions, author and the note "Reverted to v1";
    • as agentos_rls, tenant tenant-b sees 0 rows and the owning tenant sees 4.
  • Not run: the full Aspire stack with Keycloak and two real tenants, and a live Autopilot run on the lab running a custom flow. See the notes below.

Checklist

  • dotnet build passes locally in Release mode
  • dotnet test passes (with coverage if available)
  • No secrets committed (API keys, connection strings)
  • README / docs updated if public behavior changed
  • Measurable results provided when related to agent output quality (n/a — no agent output change)

Notes / not done

  • Each agent step kind currently has exactly one agent able to run it. The picker therefore shows a single choice per step; the role key is still stored and validated so that a second agent can be registered later.
  • Label filters use the ticket's labels when it starts (MAF), and the current labels on each pass (step by step).
  • The Claude Design push (/design-sync) is pending.

🤖 Generated with Claude Code

https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB

hoangsnowy and others added 9 commits October 24, 2026 16:49
…tenant storage

TicketFlowDefinition (ordered step nodes + pass/fail edges) with the safety invariants,
workspaces.ticket_flows (append-only versions, RLS) and the ticket flow snapshot on sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
…flow

Both engines follow the flow a ticket started with; the planner skips steps the flow leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
Graph + table of the ticket flow with inline validation, save, revert and the live step;
members read-only. Board › Autopilot summarises the flow and links to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
… versioning, tenants, planner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
…low and reverts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
…ed as true/false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
…onomy levels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
Comment on lines +366 to +372
foreach (var e in definition.Edges)
{
if (!byId.ContainsKey(e.From) || !byId.ContainsKey(e.To))
{
problems.Add(new(null, $"An edge points at a step that doesn't exist ({e.From} → {e.To})."));
}
}
Comment on lines +64 to +71
foreach (var step in new[] { "review", "test" })
{
if (await Card(win, step).GetAttributeAsync("class") is { } css && css.Contains("run-skipped", StringComparison.Ordinal))
{
await SetRunsAsync(win, step, on: true);
changed = true;
}
}
Comment on lines +232 to +236
foreach (var kind in System.Enum.GetValues<TicketStepKind>())
{
var info = TicketStepKinds.Of(kind);
info.Roles.ShouldAllBe(r => AgentRoles.Find(r) != null);
}
… says so

Review of #285: AppendAsync retries the unique (TenantId, Version) collision three times
and then let the DbUpdateException escape into the admin's circuit. The fourth collision
now raises TicketFlowSaveConflictException, which TicketFlowService turns into a plain
problem: "Someone else saved the ticket flow at the same moment. Reload it and try again."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WAdQ7Rm24xss5QDSvXtJB
@hoangsnowy
hoangsnowy merged commit 155936c into main Oct 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants