{
  "$schema": "./schema/workflow.schema.json",
  "specVersion": "1.0",
  "workflow": {
    "id": "bloom-sdlc",
    "name": "Bloom SDLC Product-Owner Workflow",
    "version": "0.1.0",
    "description": "End-to-end software development lifecycle as run by Bloom, the AI Product Owner: conversational inception -> living PRD -> approval -> milestone & ticket planning -> GitHub sync -> progress tracking -> PR review -> (post-MVP) deployment, testing, and feedback. Authored against the stack-agnostic workflow spec so the orchestration engine (currently LangGraph, tentative) can be swapped without rewriting the process.",
    "owner": "bloom",
    "tags": ["sdlc", "product-owner", "telegram", "github"]
  },

  "runtime": {
    "engine": "langgraph",
    "persistence": {
      "checkpointing": true,
      "threadKey": "state.thread_id"
    }
  },

  "integrations": {
    "telegram": {
      "kind": "messaging",
      "provider": "telegram",
      "config": { "botToken": "${env:TELEGRAM_BOT_TOKEN}" },
      "actions": ["send_message", "ask", "notify"]
    },
    "github": {
      "kind": "vcs",
      "provider": "github",
      "config": { "token": "${env:GITHUB_TOKEN}" },
      "actions": [
        "create_repository",
        "create_milestone",
        "create_issue",
        "update_issue",
        "close_issue",
        "list_issues",
        "read_issue_status",
        "read_pull_request",
        "list_project_status"
      ]
    },
    "llm": {
      "kind": "llm",
      "provider": "openclaw",
      "config": { "model": "${env:BLOOM_LLM_MODEL}", "fallbackProvider": "anthropic" }
    },
    "ci": {
      "kind": "ci",
      "provider": "github-actions",
      "config": {},
      "actions": ["trigger_pipeline", "read_pipeline_status"]
    },
    "deploy": {
      "kind": "deploy",
      "provider": "unset",
      "config": {},
      "actions": ["deploy", "read_deployment_status"]
    }
  },

  "triggers": [
    {
      "id": "user_message",
      "type": "message",
      "integration": "telegram",
      "binds": {
        "thread_id": "chat.id",
        "inbound_message": "message.text"
      },
      "description": "Any inbound Telegram message from the user."
    },
    {
      "id": "pr_event",
      "type": "webhook",
      "integration": "github",
      "filter": { "event": "pull_request", "action": ["opened", "synchronize", "reopened"] },
      "binds": {
        "pending_pull_requests": "pull_request"
      },
      "description": "A pull request was opened or updated on the tracked repository."
    },
    {
      "id": "issue_event",
      "type": "webhook",
      "integration": "github",
      "filter": { "event": "issues", "action": ["closed", "reopened"] },
      "binds": { "changed_issue": "issue" },
      "description": "An issue changed state (drives progress tracking)."
    }
  ],

  "state": {
    "fields": {
      "thread_id": { "type": "string", "reducer": "replace", "description": "Durable per-project run key. One chat can own several projects, so this is chat-scoped (e.g. \"<chat_id>:<seq>\"), not the raw chat id." },
      "chat_id": { "type": "integer", "reducer": "replace", "description": "The Telegram chat this project belongs to. Several projects in one chat share this; outbound messages are addressed to it." },
      "inbound_message": { "type": "string", "reducer": "replace", "description": "Latest user message text." },
      "conversation": { "type": "array", "reducer": "append", "initial": [], "description": "Rolling conversational history." },
      "requirements": { "type": "object", "reducer": "replace", "initial": {}, "description": "Discovered requirement facets (objective, users, features, constraints, success criteria). The orchestrator accumulates these additively before writing." },
      "requirements_complete": { "type": "boolean", "reducer": "replace", "initial": false, "description": "True once discovery has gathered enough to draft a useful PRD." },
      "readiness_holds": { "type": "integer", "reducer": "replace", "initial": 0, "description": "How many times the discovery readiness gate (M13-3) has held back a model-declared complete because essential facets were missing. After the cap, the owner's insistence wins (escape hatch) and the gaps are recorded as deferred_facets." },
      "deferred_facets": { "type": "array", "reducer": "replace", "initial": [], "description": "Essential requirement facets the owner chose to defer past the readiness gate. Recorded as PRD assumptions; the proactive tick follows up once real progress makes the gap matter (first completed milestone), and refreshes this list as facets get covered." },
      "deferred_facets_notified": { "type": "boolean", "reducer": "replace", "initial": false, "description": "Whether the one-time deferred-facets follow-up nudge has been sent, so the owner is never nagged repeatedly." },
      "credentials_ready_notified": { "type": "boolean", "reducer": "replace", "initial": false, "description": "Whether the one-time credentials-complete go-live nudge has been sent for the parked deployment (M19-6)." },
      "credentials_meta": { "type": "object", "reducer": "replace", "initial": {}, "description": "Credential METADATA (name -> provided-at ISO timestamp) for the project's deployment credentials (#282). Bloom brokers every save/removal, so it keeps the metadata itself and needs NO read access on the credential store - values live only in the store." },
      "prd": { "type": "object", "reducer": "merge", "initial": { "status": "draft", "revision": 0 }, "description": "The living PRD document + status (draft|awaiting_approval|approved|revising) and revision counter." },
      "approval": { "type": "object", "reducer": "replace", "initial": {}, "description": "Latest human approval decision: { decision: approved|changes_requested, notes }." },
      "milestones": { "type": "array", "reducer": "replace", "initial": [], "description": "Planned milestones." },
      "tickets": { "type": "array", "reducer": "replace", "initial": [], "description": "Generated implementation-agnostic tickets." },
      "github_sync": { "type": "object", "reducer": "merge", "initial": {}, "description": "Mapping of planned artifacts -> created GitHub ids." },
      "created_issues": { "type": "array", "reducer": "append", "initial": [], "description": "GitHub issues created for tickets (accumulated by the sync fan-out). An issue imported from an adopted repo's backlog (M47) is recorded here under its EXISTING number at import time, which is what makes the sync adopt it in place instead of minting a duplicate." },
      "backlog_import": { "type": "object", "reducer": "replace", "initial": {}, "description": "The plan-time backlog-import gate for an adopted repository (M47): the importable pre-existing issues offered ({candidates}) and, once the owner replied, the decision ({decision: all|selected|none, numbers}). Stays empty for a provisioned (greenfield) project or an adopted repo with nothing importable - the gate is then routed around, never prompted." },
      "repo_target": { "type": "object", "reducer": "replace", "initial": {}, "description": "Where the project's GitHub repo lives and how Bloom is authorized (bloom-hosted default, or client-owned via a GitHub App installation)." },
      "pending_pull_requests": { "type": "array", "reducer": "replace", "initial": [], "description": "PRs from the current event awaiting review (replaced per event, drained after review)." },
      "reviews": { "type": "array", "reducer": "append", "initial": [], "description": "Completed PR review summaries." },
      "specialist_reviews": { "type": "object", "reducer": "merge", "initial": {}, "description": "Per-PR specialist review rounds (M7-2), keyed by PR number (string) -> {marker, round}. A repeated tick for an unchanged PR overwrites its entry rather than duplicating a record. Advisory only; never a merge gate." },
      "engine_runs": { "type": "object", "reducer": "merge", "initial": {}, "description": "Durable production-engineer run records (M6-6), keyed by issue number (string) -> {state, attempt, retries, failure_kind, failure_reason, last_attempt_at, ...}. Secret-free; the auditable trail of what the engine did per ticket." },
      "changed_issue": { "type": "object", "reducer": "replace", "initial": {}, "description": "Most recent issue-state change." },
      "notified_milestones": { "type": "array", "reducer": "union", "initial": [], "description": "Milestone numbers already announced as complete, so completion is notified at most once (FR-10)." },
      "progress": { "type": "object", "reducer": "merge", "initial": {}, "description": "Snapshot of open/closed milestones & issues, blocked work." },
      "lifecycle": { "type": "string", "reducer": "replace", "initial": "active", "description": "Project lifecycle state: active | archived | deleted." },
      "pending_lifecycle": { "type": "string", "reducer": "replace", "initial": "", "description": "A destructive lifecycle op (archive|delete) awaiting the owner's confirmation; empty when none." },
      "pending_plan_change": { "type": "object", "reducer": "replace", "initial": {}, "description": "A proposed post-planning plan revision (revised PRD + desired milestones/tickets) awaiting the owner's approval; empty when none (FR-3/FR-4, M4)." },
      "pending_decision": { "type": "object", "reducer": "replace", "initial": {}, "description": "A decision record drafted at an approval gate, awaiting the owner's approval before it is committed to docs/decisions/; empty when none (M14-3)." },
      "pending_client_approvals": { "type": "array", "reducer": "replace", "initial": [], "description": "Bloom-approved deliverables parked for the owner's sign-off (status:awaiting-client, M11-5), as {number, title, kind, pr_number}. Rewritten wholesale on every park/resolve, so the reducer is replace; empty when none." },
      "decision_log": { "type": "array", "reducer": "append", "initial": [], "description": "Decision records committed to docs/decisions/, as {id, path, title, gate, supersedes} (M14-4/5). The latest plan-scope entry (prd or plan_change gate) is what a superseding plan change links - and retires - via its supersedes edge; the entries' supersedes lists are how de-duplication tells retired conditions from current ones." },
      "last_checked_at": { "type": "string", "reducer": "replace", "initial": "", "description": "ISO timestamp of Bloom's last proactive maintenance tick for this project (M5)." },
      "last_digest_at": { "type": "string", "reducer": "replace", "initial": "", "description": "ISO timestamp of the last periodic status digest sent (M5)." },
      "digest_signature": { "type": "string", "reducer": "replace", "initial": "", "description": "Signature of the project state at the last digest, so an unchanged project is not re-digested (M5)." },
      "notified_stalls": { "type": "array", "reducer": "replace", "initial": [], "description": "Issue numbers already flagged as stalled, so a persistent stall is nudged at most once (M5)." },
      "deployment": { "type": "object", "reducer": "merge", "initial": {}, "description": "(Post-MVP) latest deployment status." },
      "feedback": { "type": "array", "reducer": "append", "initial": [], "description": "(Post-MVP) user/production feedback feeding the next cycle." },
      "notifications": { "type": "array", "reducer": "append", "initial": [], "description": "Outbound notifications queued/sent to the user." }
    }
  },

  "nodes": [
    {
      "id": "await_message",
      "type": "event_wait",
      "title": "Await user turn",
      "phase": "inception",
      "config": { "eventWait": "await_message", "trigger": "user_message" }
    },
    {
      "id": "requirement_discovery",
      "type": "agent",
      "title": "Interview & discover requirements",
      "phase": "inception",
      "reads": ["state.conversation", "state.requirements", "state.inbound_message"],
      "writes": ["requirements", "requirements_complete", "conversation", "readiness_holds", "deferred_facets"],
      "config": {
        "agent": "product_owner",
        "model": "llm",
        "instructions": "${ref:prompts/requirement_discovery}",
        "input": { "history": "state.conversation", "known": "state.requirements", "message": "state.inbound_message" },
        "tools": [],
        "output": {
          "assign": { "requirements": "result.requirements", "requirements_complete": "result.complete" },
          "schema": {
            "type": "object",
            "required": ["requirements", "complete"],
            "properties": {
              "requirements": { "type": "object" },
              "complete": { "type": "boolean" },
              "reply": { "type": "string" }
            }
          }
        }
      }
    },
    {
      "id": "ask_clarification",
      "type": "human",
      "title": "Ask a clarifying question",
      "phase": "inception",
      "config": {
        "human": "clarification",
        "channel": "telegram",
        "prompt": "${ref:prompts/clarifying_question}",
        "input": { "known": "state.requirements" },
        "awaits": { "type": "object", "properties": { "text": { "type": "string" } } },
        "output": { "assign": { "inbound_message": "result.text", "conversation": "result" } }
      }
    },
    {
      "id": "prd_authoring",
      "type": "agent",
      "title": "Author / update the living PRD",
      "phase": "planning",
      "reads": ["state.requirements", "state.prd"],
      "writes": ["prd"],
      "config": {
        "agent": "product_owner",
        "model": "llm",
        "instructions": "${ref:prompts/prd_authoring}",
        "input": { "requirements": "state.requirements", "current_prd": "state.prd" },
        "output": {
          "assign": { "prd": "result" },
          "schema": {
            "type": "object",
            "required": ["document", "status", "revision"],
            "properties": {
              "document": { "type": "object" },
              "status": { "type": "string", "enum": ["awaiting_approval"] },
              "revision": { "type": "integer" }
            }
          }
        }
      }
    },
    {
      "id": "request_prd_approval",
      "type": "human",
      "title": "Request PRD approval",
      "phase": "planning",
      "config": {
        "human": "prd_approval",
        "channel": "telegram",
        "prompt": "${ref:prompts/request_prd_approval}",
        "input": { "prd": "state.prd" },
        "awaits": {
          "type": "object",
          "required": ["decision"],
          "properties": {
            "decision": { "type": "string", "enum": ["approved", "changes_requested"] },
            "notes": { "type": "string" }
          }
        },
        "output": { "assign": { "approval": "result" } }
      }
    },
    {
      "id": "freeze_prd",
      "type": "passthrough",
      "title": "Freeze approved PRD revision",
      "phase": "planning",
      "writes": ["prd"],
      "config": { "note": "Marks prd.status=approved and pins the revision; future changes create a new revision (FR-4)." }
    },
    {
      "id": "milestone_planning",
      "type": "agent",
      "title": "Plan milestones",
      "phase": "planning",
      "reads": ["state.prd"],
      "writes": ["milestones"],
      "config": {
        "agent": "product_owner",
        "model": "llm",
        "instructions": "${ref:prompts/milestone_planning}",
        "input": { "prd": "state.prd" },
        "output": {
          "assign": { "milestones": "result.milestones" },
          "schema": { "type": "object", "required": ["milestones"], "properties": { "milestones": { "type": "array" } } }
        }
      }
    },
    {
      "id": "ticket_generation",
      "type": "agent",
      "title": "Generate implementation-agnostic tickets",
      "phase": "planning",
      "reads": ["state.milestones", "state.prd"],
      "writes": ["tickets"],
      "config": {
        "agent": "product_owner",
        "model": "llm",
        "instructions": "${ref:prompts/ticket_generation}",
        "input": { "milestones": "state.milestones", "prd": "state.prd" },
        "output": {
          "assign": { "tickets": "result.tickets" },
          "schema": {
            "type": "object",
            "required": ["tickets"],
            "properties": {
              "tickets": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["title", "description", "acceptanceCriteria"],
                  "properties": {
                    "title": { "type": "string" },
                    "description": { "type": "string" },
                    "acceptanceCriteria": { "type": "array", "items": { "type": "string" } },
                    "labels": { "type": "array", "items": { "type": "string" } },
                    "dependencies": { "type": "array", "items": { "type": "string" } },
                    "complexity": { "type": "string" },
                    "milestone": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    {
      "id": "list_backlog_candidates",
      "type": "tool",
      "title": "List the adopted repo's importable backlog",
      "phase": "planning",
      "description": "For an adopted (pre-existing) repository only: the open issues Bloom could take on - unassigned, outside Bloom's coordination labels, not already one of its issues (M47). Writes nothing for a provisioned repo or an empty backlog, so the guard below skips the import gate.",
      "reads": ["state.repo_target", "state.tickets", "state.created_issues"],
      "writes": ["backlog_import"],
      "config": {
        "tool": "list_backlog_candidates",
        "integration": "github",
        "action": "list_issues",
        "params": { "repo": "state.repo_target" },
        "output": { "assign": { "backlog_import": "result" } }
      }
    },
    {
      "id": "backlog_import",
      "type": "human",
      "title": "Offer the adopted repo's backlog for import",
      "phase": "planning",
      "description": "The owner-confirmed import gate (M47): the importable pre-existing issues are listed and the owner picks which ones Bloom takes. Chosen issues join the plan as tickets (origin: imported) under the 'Imported backlog' milestone, keep their issue numbers, and are delegated like any generated ticket. Nothing is imported silently; a reply the grammar rejects re-asks.",
      "reads": ["state.backlog_import.candidates", "state.tickets", "state.milestones"],
      "writes": ["backlog_import", "tickets", "milestones", "created_issues"],
      "config": {
        "human": "backlog_import",
        "channel": "telegram",
        "prompt": "${ref:prompts/backlog_import}",
        "input": { "candidates": "state.backlog_import.candidates" },
        "awaits": {
          "type": "object",
          "required": ["decision"],
          "properties": {
            "decision": { "type": "string", "enum": ["all", "selected", "none"] },
            "numbers": { "type": "array", "items": { "type": "integer" } }
          }
        },
        "output": { "assign": { "backlog_import": "result" } }
      }
    },
    {
      "id": "sync_milestones",
      "type": "tool",
      "title": "Create GitHub milestones",
      "phase": "planning",
      "reads": ["state.milestones"],
      "writes": ["github_sync"],
      "config": {
        "tool": "create_github_milestones",
        "integration": "github",
        "action": "create_milestone",
        "params": { "milestones": "state.milestones" },
        "output": { "assign": { "github_sync.milestones": "result" } }
      }
    },
    {
      "id": "sync_tickets",
      "type": "map",
      "title": "Create a GitHub issue per ticket",
      "phase": "planning",
      "reads": ["state.tickets"],
      "writes": ["created_issues"],
      "config": {
        "map": "sync_tickets",
        "over": "state.tickets",
        "as": "ticket",
        "body": "create_issue_for_ticket",
        "concurrency": 4,
        "collect": "created_issues"
      }
    },
    {
      "id": "create_issue_for_ticket",
      "type": "tool",
      "title": "Create one GitHub issue",
      "phase": "planning",
      "reads": ["item.title", "item.description"],
      "config": {
        "tool": "create_issue",
        "integration": "github",
        "action": "create_issue",
        "params": {
          "title": "item.title",
          "body": "item.description",
          "labels": "item.labels",
          "milestone": "item.milestone"
        },
        "output": { "assign": { "result": "result" } }
      }
    },
    {
      "id": "notify_planning_ready",
      "type": "tool",
      "title": "Notify: planning synced to GitHub",
      "phase": "planning",
      "config": {
        "tool": "notify_planning_ready",
        "integration": "telegram",
        "action": "notify",
        "params": { "template": "planning_ready", "milestones": "state.github_sync.milestones", "issues": "state.created_issues" },
        "output": { "assign": { "notifications": "result" } }
      }
    },
    {
      "id": "await_project_event",
      "type": "event_wait",
      "title": "Await project activity",
      "phase": "execution",
      "description": "Parks the long-lived run between planning and ongoing lifecycle events (PRs, issue changes, or a new user message).",
      "config": { "eventWait": "await_project_event", "trigger": "pr_event" }
    },
    {
      "id": "review_pull_requests",
      "type": "map",
      "title": "Review each pending pull request",
      "phase": "review",
      "reads": ["state.pending_pull_requests"],
      "writes": ["reviews"],
      "config": {
        "map": "review_pull_requests",
        "over": "state.pending_pull_requests",
        "as": "pr",
        "body": "review_one_pull_request",
        "concurrency": 2,
        "collect": "reviews"
      }
    },
    {
      "id": "review_one_pull_request",
      "type": "agent",
      "title": "Review one PR against acceptance criteria",
      "phase": "review",
      "reads": ["item.number", "state.tickets"],
      "config": {
        "agent": "reviewer",
        "model": "llm",
        "instructions": "${ref:prompts/pr_review}",
        "input": { "pull_request": "item", "tickets": "state.tickets" },
        "tools": ["github.read_pull_request"],
        "output": {
          "assign": { "result": "result" },
          "schema": {
            "type": "object",
            "required": ["summary", "recommendation"],
            "properties": {
              "summary": { "type": "string" },
              "recommendation": { "type": "string", "enum": ["approve", "request_changes"] },
              "missingRequirements": { "type": "array", "items": { "type": "string" } },
              "documentationGaps": { "type": "array", "items": { "type": "string" } }
            }
          }
        }
      }
    },
    {
      "id": "notify_review",
      "type": "tool",
      "title": "Notify: PR review complete",
      "phase": "review",
      "config": {
        "tool": "notify_review",
        "integration": "telegram",
        "action": "notify",
        "params": { "template": "review_complete", "reviews": "state.reviews" },
        "output": { "assign": { "notifications": "result" } }
      }
    },
    {
      "id": "track_progress",
      "type": "tool",
      "title": "Refresh project progress snapshot",
      "phase": "execution",
      "writes": ["progress"],
      "config": {
        "tool": "track_progress",
        "integration": "github",
        "action": "list_project_status",
        "params": {},
        "output": { "assign": { "progress": "result" } }
      }
    },
    {
      "id": "deploy",
      "type": "tool",
      "title": "Deploy (post-MVP)",
      "phase": "deployment",
      "mvp": false,
      "writes": ["deployment"],
      "config": {
        "tool": "deploy",
        "integration": "deploy",
        "action": "deploy",
        "params": {},
        "output": { "assign": { "deployment": "result" } }
      }
    },
    {
      "id": "run_tests",
      "type": "tool",
      "title": "Run test/QA pipeline (post-MVP)",
      "phase": "testing",
      "mvp": false,
      "config": {
        "tool": "run_tests",
        "integration": "ci",
        "action": "trigger_pipeline",
        "params": {},
        "output": { "assign": { "progress.tests": "result" } }
      }
    },
    {
      "id": "gather_feedback",
      "type": "agent",
      "title": "Synthesize feedback into next-cycle requirements (post-MVP)",
      "phase": "feedback",
      "mvp": false,
      "reads": ["state.feedback", "state.reviews"],
      "writes": ["requirements", "feedback"],
      "config": {
        "agent": "product_owner",
        "model": "llm",
        "instructions": "${ref:prompts/feedback_synthesis}",
        "input": { "feedback": "state.feedback", "reviews": "state.reviews", "prd": "state.prd" },
        "output": { "assign": { "requirements": "result.requirements" } }
      }
    }
  ],

  "edges": [
    { "from": "await_message", "to": "requirement_discovery" },

    {
      "from": "requirement_discovery",
      "to": "prd_authoring",
      "when": { "op": "eq", "left": { "ref": "state.requirements_complete" }, "right": { "const": true } },
      "priority": 10,
      "label": "enough gathered"
    },
    {
      "from": "requirement_discovery",
      "to": "ask_clarification",
      "priority": 0,
      "label": "need more info"
    },
    { "from": "ask_clarification", "to": "requirement_discovery" },

    { "from": "prd_authoring", "to": "request_prd_approval" },

    {
      "from": "request_prd_approval",
      "to": "freeze_prd",
      "when": { "op": "eq", "left": { "ref": "state.approval.decision" }, "right": { "const": "approved" } },
      "priority": 10,
      "label": "approved"
    },
    {
      "from": "request_prd_approval",
      "to": "requirement_discovery",
      "priority": 0,
      "label": "changes requested -> new revision"
    },

    { "from": "freeze_prd", "to": "milestone_planning" },
    { "from": "milestone_planning", "to": "ticket_generation" },
    { "from": "ticket_generation", "to": "list_backlog_candidates" },

    {
      "from": "list_backlog_candidates",
      "to": "backlog_import",
      "when": { "op": "exists", "left": { "ref": "state.backlog_import.candidates[0]" } },
      "priority": 10,
      "label": "importable backlog -> offer it"
    },
    {
      "from": "list_backlog_candidates",
      "to": "sync_milestones",
      "priority": 0,
      "label": "nothing to import (provisioned repo or empty backlog)"
    },

    {
      "from": "backlog_import",
      "to": "sync_milestones",
      "when": { "op": "exists", "left": { "ref": "state.backlog_import.decision" } },
      "priority": 10,
      "label": "decided (all | selected | none)"
    },
    {
      "from": "backlog_import",
      "to": "backlog_import",
      "priority": 0,
      "label": "reply not understood -> gate stays open"
    },

    { "from": "sync_milestones", "to": "sync_tickets" },
    { "from": "sync_tickets", "to": "notify_planning_ready" },
    { "from": "notify_planning_ready", "to": "await_project_event" },

    {
      "from": "await_project_event",
      "to": "review_pull_requests",
      "when": { "op": "exists", "left": { "ref": "state.pending_pull_requests[0]" } },
      "priority": 10,
      "label": "PR activity"
    },
    {
      "from": "await_project_event",
      "to": "track_progress",
      "priority": 0,
      "label": "other project activity"
    },

    { "from": "review_pull_requests", "to": "notify_review" },
    { "from": "notify_review", "to": "track_progress" },
    { "from": "track_progress", "to": "await_project_event", "label": "keep tracking (MVP loop)" },

    {
      "from": "track_progress",
      "to": "deploy",
      "when": { "predicate": "post_mvp_enabled", "args": { "capability": "deployment" } },
      "priority": 5,
      "label": "post-MVP: deploy when milestone complete"
    },
    { "from": "deploy", "to": "run_tests" },
    { "from": "run_tests", "to": "gather_feedback" },
    { "from": "gather_feedback", "to": "prd_authoring", "label": "feedback -> next PRD revision" }
  ],

  "entrypoint": "await_message",
  "terminals": []
}
