{"id":51388350,"url":"https://github.com/l4ci/maestro","last_synced_at":"2026-07-03T21:35:25.328Z","repository":{"id":365880158,"uuid":"1259294580","full_name":"l4ci/maestro","owner":"l4ci","description":"A stateless daemon that drives GitLab/GitHub issues through a human-in-the-loop lifecycle using Claude as the coding agent.","archived":false,"fork":false,"pushed_at":"2026-06-19T08:53:21.000Z","size":1439,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T10:28:43.100Z","etag":null,"topics":["ai-agents","automation","claude","coding-agent","daemon","devtools","github","gitlab","monorepo","nodejs","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/l4ci.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T11:16:58.000Z","updated_at":"2026-06-19T08:53:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/l4ci/maestro","commit_stats":null,"previous_names":["l4ci/maestro"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/l4ci/maestro","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l4ci%2Fmaestro","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l4ci%2Fmaestro/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l4ci%2Fmaestro/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l4ci%2Fmaestro/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/l4ci","download_url":"https://codeload.github.com/l4ci/maestro/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l4ci%2Fmaestro/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35102741,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-03T02:00:05.635Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","automation","claude","coding-agent","daemon","devtools","github","gitlab","monorepo","nodejs","typescript"],"created_at":"2026-07-03T21:35:24.674Z","updated_at":"2026-07-03T21:35:25.320Z","avatar_url":"https://github.com/l4ci.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Maestro\n\nMaestro is a robot teammate for your code repositories. You assign it a ticket,\nand it does the work: it opens a branch, writes the code with Claude, proves the\nchange works, and then hands the result back to you for review. Once you approve,\nit merges. If it gets stuck or has a question, it asks and waits.\n\nIt works on top of **GitLab** and **GitHub**. You keep using issues and merge\nrequests the way you already do. Maestro just becomes another contributor on the\nteam — one that happens to be an AI.\n\n---\n\n## The one-paragraph version\n\nYou run a single long-lived program (the **daemon**) on a machine you control.\nIt reads a config file listing which repos to watch. Every so often it looks at\neach repo and asks: *is there a ticket here assigned to my bot account?* If yes,\nit picks it up and walks it through a fixed set of steps — write code, prove it,\nask for review, merge. The clever part is that Maestro keeps **almost no memory\nof its own**. Everything it needs to know lives in the ticket, the merge request,\nand the git history. So if the daemon crashes or you reboot the machine, it just\nre-reads the repo and carries on exactly where it left off.\n\n---\n\n## Why it's built this way\n\nTwo ideas drive the whole design.\n\n**1. The forge is the memory.** Maestro doesn't run a database. The \"state\" of\nany ticket — whether it's new, in progress, waiting for review, or done — is\nwritten directly into the forge as labels, merge-request status, and comments.\nAnything stored on the local disk (cloned repos, log files) is treated as a\nthrowaway cache that can be deleted and rebuilt at any time.\n\n```mermaid\nflowchart LR\n    subgraph durable[\"Durable — survives anything\"]\n        F[\"GitLab / GitHub\u003cbr/\u003etickets · labels · MRs · comments\"]\n        C[\"maestro.config.yaml\u003cbr/\u003ewhich repos to watch\"]\n    end\n    subgraph cache[\"Disposable cache — delete anytime\"]\n        W[\"workspaces/\u003cbr/\u003ecloned repos\"]\n        L[\"logs/\"]\n    end\n    D[\"Maestro daemon\"]\n    D --\u003e|reads \u0026 writes| F\n    D --\u003e|reads| C\n    D -.-\u003e|rebuildable| W\n    D -.-\u003e|rebuildable| L\n```\n\nBecause of this, a multi-day wait for a human reviewer costs nothing. The daemon\ncan die, sit idle for a week, then wake up and rebuild every ticket's status from\nthe forge.\n\n**2. The AI starts fresh every time.** Maestro never tries to \"resume\" a Claude\nsession. Each time it needs the agent, it starts a brand-new, cold session. The\nagent re-learns what it's doing by reading three things: the ticket, the merge\nrequest description (which doubles as its to-do list), and the recent git diff.\nThis sounds wasteful but it's actually robust — there's no fragile session to\nlose, and a human can read the exact same three sources to understand what\nhappened. Anything that should outlive a single ticket — coding conventions,\ndecisions the team has locked in — belongs in the repo's own `CLAUDE.md`, which\nthe cold agent reads automatically on every run (see [Repo-specific\nconventions](#repo-specific-conventions)).\n\n---\n\n## The lifecycle: how a ticket becomes a merge\n\nEvery ticket moves through the same set of stages. The stage is visible as a label\non the ticket (`maestro:in-progress`, `maestro:in-review`, and so on — on GitLab\nthey appear scoped, as `maestro::in-progress`), so you can see it at a glance on\nyour board. This is the default single-agent flow; a\nrepo can opt into a longer pipeline with separate define, plan, implement, and\nreview agents — see [Per-role prompts and the stage\npipeline](#per-role-prompts-and-the-stage-pipeline-29) below.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e New: assigned to bot\n    New --\u003e InProgress: open branch + draft MR,\u003cbr/\u003epost \"started\"\n    InProgress --\u003e InProgress: write code,\u003cbr/\u003eatomic commits\n    InProgress --\u003e Handoff: agent says \"done\"\n    InProgress --\u003e Blocked: agent has a question\n    Handoff --\u003e InReview: proof posted,\u003cbr/\u003ethen review requested\n    InReview --\u003e InProgress: changes requested\n    InReview --\u003e Done: you approve, then merge\n    Blocked --\u003e InProgress: human answers\n    Done --\u003e [*]: workspace cleaned up\n\n    note right of Handoff\n        Proof is always posted\n        BEFORE you're pinged,\n        so you're notified only\n        once everything is ready.\n    end note\n```\n\nIn words:\n\n- **New** — A ticket assigned to the bot, with no Maestro label yet. The daemon\n  creates a branch and a *draft* merge request, labels it in-progress, posts a\n  \"started\" comment, and begins work.\n- **In progress** — The agent works the ticket one small commit at a time, ticking\n  off items in its to-do list (which lives in the MR description) and posting the\n  occasional progress note.\n- **Handoff** — A brief, behind-the-scenes step. The agent says it's done, so\n  Maestro generates *proof* (see below), posts it on **both** the issue and the\n  MR, **then** requests your review on the merge request and posts a short\n  \"ready for review\" comment on the issue. The review request surfaces the MR\n  under your \"review requests\" and fires the forge's native notification; the\n  comment @-mentions you with a link and your response options, so you're\n  notified even if the review request can't land (no access to the repo, or a\n  shared bot account). From here you reply wherever is natural: approve or\n  request changes on the MR, or steer with a `/maestro` comment on either the\n  MR or the issue (on a dedicated bot account, a leading `@bot` mention works\n  too — see [Addressing the bot by name](#driving-maestro-from-a-merge-request--no-ticket-required)) —\n  all these channels reach the agent. The order matters: you're pinged last,\n  when there's actually something to look at.\n- **In review** — Maestro waits. If you approve, it merges using that repo's own\n  git rules and the ticket auto-closes. If you request changes, it flips back to\n  in-progress and feeds your feedback to the agent. Three channels count as\n  \"changes requested\": a formal review (GitHub \"Request changes\" / an unresolved\n  GitLab review thread), an MR/PR comment starting with `/maestro` (the\n  shared-account escape hatch — the bot account can't review its own MR), and\n  an issue comment starting with `/maestro` (any author — the explicit prefix is\n  what keeps ordinary review chatter from spinning up agents).\n- **Blocked** — The agent hit something it can't decide on its own. It posts the\n  question and waits for a human. No slot is consumed while it waits.\n  Maestro recognises your answer by its author: any reply from an account other\n  than the bot resumes work. If you **share the bot's account** (a per-host\n  `bot_user` pointing at your own user), start the reply with `/maestro` — a\n  body-start command is the one thing the agent can never produce (it has no\n  forge access; the daemon posts everything, and always behind a heading), so\n  it counts as provably human even from the bot's account.\n- **Done** — Ticket closed, local workspace cleaned up.\n\n---\n\n## What happens during one \"tick\"\n\nThe daemon runs on a loop. One pass over the repos is called a **tick**. Here's\nwhat a single ticket looks like as it gets picked up and worked:\n\n```mermaid\nsequenceDiagram\n    participant D as Daemon\n    participant F as Forge (GitLab/GitHub)\n    participant WS as Workspace\n    participant CL as Claude\n    participant P as Proof\n    participant H as Human\n\n    D-\u003e\u003eF: Any ticket assigned to the bot?\n    F--\u003e\u003eD: Yes — ticket #42, no label\n    D-\u003e\u003eWS: Clone repo, make a branch\n    D-\u003e\u003eF: Open draft MR, label in-progress\n    loop until done or blocked\n        D-\u003e\u003eCL: Start a fresh session (read ticket, MR, diff)\n        CL-\u003e\u003eWS: Write code, commit\n        CL--\u003e\u003eD: status: done\n    end\n    D-\u003e\u003eP: Run the proof (e.g. Playwright tests)\n    P--\u003e\u003eD: Artifacts\n    D-\u003e\u003eF: Post proof on ticket + MR\n    D-\u003e\u003eF: Assign MR to ticket author, mark ready\n    Note over D,H: Maestro now waits...\n    H-\u003e\u003eF: Approve\n    D-\u003e\u003eF: Merge per repo's git rules\n    F--\u003e\u003eD: Ticket auto-closes\n    D-\u003e\u003eWS: Clean up the clone\n```\n\nThe only thing the daemon ever hears back from Claude is a tiny status:\n`done`, `needs_input`, or `in_progress` (a review session adds its pass/fail\nverdict). Everything else it learns by reading the forge on the next tick.\n\n---\n\n## Driving Maestro from a merge request — no ticket required\n\nTickets aren't the only way in. Any **open MR/PR that has no backing issue** can\nbe handed to Maestro directly: assign the MR to the bot account and write a\ncomment that *starts with* `/maestro`:\n\n```\n/maestro the e2e tests fail on this branch — find out why and fix it\n```\n\nOn the next tick, Maestro starts a cold agent on that MR's branch, follows the\ninstruction (investigate, or change code), pushes commits if it changed\nanything, and **always** posts a reply — even if it only has findings, or\nfailed. One command, one reply. To ask for more, comment `/maestro …` again;\nthe newest unanswered command wins.\n\nTwo verbs never reach the agent and are executed by the daemon itself,\ninstantly: **`/maestro merge`** and **`/maestro close`**. (The agent never\nholds a forge token, so merging and closing are the daemon's job anyway.) A\ndraft MR refuses `merge` with a hint to mark it ready first.\n\nThe same trust rules as tickets apply: on a shared bot account the body-start\n`/maestro` is what marks a comment as provably human, and a non-empty\n`allowed_actors` list restricts who may command the bot. MRs that belong to a\nMaestro ticket (a `maestro/issue-*` branch, or one that `Closes #N`) are *not*\npicked up here — those stay with their ticket's lifecycle, where the same\n`/maestro` comment counts as review feedback.\n\n\u003e **Addressing the bot by name.** If the bot runs on its **own dedicated\n\u003e account** (not shared with you), you can start a command with an `@`-mention of\n\u003e that account instead of `/maestro` — `@maestro-bot fix the failing test` works\n\u003e anywhere a `/maestro` command does. It's an alias, nothing more: the mention\n\u003e must lead the comment (a passing `@maestro-bot` mid-sentence stays ordinary\n\u003e chatter), and it only counts from someone *other than* the bot — which is\n\u003e exactly why it's a dedicated-account feature. On a **shared** account every\n\u003e comment is authored by the bot itself, so the mention can't prove a human typed\n\u003e it and `/maestro` stays the only hatch.\n\n---\n\n## The pieces inside\n\nMaestro is a set of small, independently testable parts. The most important one\nis the **reconciler** — the \"brain\" that, given a snapshot of a ticket, decides\nthe single next action. It's pure logic with no side effects, which is why it can\nbe tested exhaustively and why it never changed when GitHub support was added on\ntop of GitLab.\n\n```mermaid\nflowchart TD\n    Config[\"Config loader\u003cbr/\u003ereads maestro.config.yaml\"]\n    WF[\"Workflow loader\u003cbr/\u003ereads each repo's WORKFLOW.md\"]\n    Forge[\"Forge adapter\u003cbr/\u003eGitLab + GitHub, one shared shape\"]\n    Rec[\"Reconciler\u003cbr/\u003e(snapshot) to one action\"]\n    WSM[\"Workspace manager\u003cbr/\u003eclone · branch · cleanup\"]\n    Run[\"Agent runner\u003cbr/\u003eheadless cold session\"]\n    Proof[\"Proof generator\u003cbr/\u003eplaywright · tests · diff · none\"]\n\n    Config --\u003e Rec\n    WF --\u003e Rec\n    Forge --\u003e Rec\n    Rec --\u003e|\"start work\"| WSM\n    WSM --\u003e Run\n    Run --\u003e|\"done\"| Proof\n    Proof --\u003e Forge\n    Rec --\u003e|\"merge / comment / label\"| Forge\n```\n\n- **Forge adapter** — The only part that knows the difference between GitLab and\n  GitHub. It translates each into one shared shape so nothing above it has to\n  care which forge a repo lives on.\n- **Reconciler** — Pure decision-making. Takes a ticket snapshot, returns at most\n  one action per tick.\n- **Workspace manager** — Clones a repo into a per-ticket folder, handles the\n  branch, cleans up when the ticket is done. This is also the seam where, later,\n  you could swap host folders for isolated containers.\n- **Agent runner** — Runs the same `claude` binary you use interactively, but\n  headless. Locally it uses your existing login and still loads your `CLAUDE.md`,\n  settings, skills, and permission modes. Set `defaults.agent.kind: codex` to run\n  OpenAI's Codex CLI (`codex exec`) instead — the daemon-global choice is the only\n  difference; the cold-session, status-contract, and proof flow are identical.\n  \u003e ⚠️ **Codex is unverified.** The Codex backend is unit-tested against the\n  \u003e published Codex SDK types but has not yet been run against a live `codex` CLI.\n  \u003e Treat it as experimental until [#122](https://github.com/l4ci/maestro/issues/122)\n  \u003e is closed.\n- **Proof generator** — Pluggable per repo. Pick `playwright`, `test-output`,\n  `diff-summary`, or `none`.\n- **CLI and Web** — Thin shells over the shared core (see below).\n\n---\n\n## Prerequisites\n\nMaestro is an orchestrator — it drives a handful of command-line tools rather than\nreimplementing them. These need to be installed and on your `PATH` before the\ndaemon will run:\n\n| Tool | Why | Install |\n|---|---|---|\n| **Node.js ≥ 20** + **pnpm** | runs Maestro itself | [nodejs.org](https://nodejs.org) · [pnpm.io](https://pnpm.io/installation) |\n| **git** | clone and branch each ticket's workspace | your package manager |\n| **claude** | the default coding agent, run headless — *unless you set `agent.kind: codex`* | [Claude Code](https://claude.com/claude-code) |\n| **codex** | alternative coding agent, run headless — *only if you set `agent.kind: codex`* | [OpenAI Codex CLI](https://github.com/openai/codex) |\n| **glab** | talk to the GitLab API — *only if you watch GitLab repos* | [gitlab.com/gitlab-org/cli](https://gitlab.com/gitlab-org/cli) |\n| **gh** | talk to the GitHub API — *only if you watch GitHub repos* | [cli.github.com](https://cli.github.com) |\n\nYou don't need to log into `glab`/`gh` — Maestro injects the token itself (from\nits environment, loaded from your `.env`). It only needs the binaries present. Run **`maestro doctor`** at any\ntime to check what's missing; the daemon also runs this check on startup and\nrefuses to boot (with a clear message) if a required tool is absent.\n\n---\n\n## Getting started\n\nThe fast path from a fresh clone to a running daemon and dashboard.\n\n```sh\n# 1. Clone and set up (installs deps, builds, scaffolds .env, checks your tools)\ngit clone https://github.com/l4ci/maestro.git\ncd maestro\n./scripts/setup.sh\n\n# 2. Add your secrets — paste the bot account's token(s)\n$EDITOR .env                 # MAESTRO_GITLAB_TOKEN / MAESTRO_GITHUB_TOKEN\n\n# 3. Point Maestro at your forge(s) — host + which env var holds each token\n$EDITOR maestro.config.yaml  # (see \"Setting it up\" below for the full schema)\n\n# 4. Confirm every required tool is on PATH\nnode packages/cli/dist/cli.js doctor\n\n# 5. Connect your first repo (creates its labels/board, commits the config change)\nnode packages/cli/dist/cli.js add gitlab.com/your-group/your-repo\n\n# 6. Load the tokens into your shell, then start the daemon\nset -a; . ./.env; set +a\nnode packages/cli/dist/cli.js daemon\n\n# 7. In another terminal, start the dashboard and open it in a browser\nnode packages/cli/dist/cli.js dashboard    # → http://127.0.0.1:4000\n```\n\nThat's the whole loop. Assign an issue on your repo to the bot account, and watch\nits state move across the dashboard as Maestro picks it up, works it, and hands it\nback for review.\n\n\u003e **Tip — a shorter `maestro`:** `./scripts/setup.sh` links the CLI onto your\n\u003e PATH automatically when pnpm has a global bin dir (`pnpm setup` creates one —\n\u003e then re-run the setup script, or run `pnpm -C packages/cli link --global`\n\u003e yourself). No global bin dir? Alias it instead:\n\u003e `alias maestro='node /path/to/maestro/packages/cli/dist/cli.js'`.\n\n---\n\n## Setting it up\n\nThere are two config files. One is global to your Maestro install; the other\nlives inside each repo you want watched.\n\n### 1. The global config — `maestro.config.yaml`\n\nThis lists your repos and global defaults. It's committed to git. **Secrets never\ngo here** — the config only names the *environment variable* that holds a token,\nnever the token itself.\n\n```yaml\ndefaults:\n  poll_interval_active: 30s   # how often to check repos with live work\n  poll_interval_idle: 5m      # how often to check quiet repos\n  bot_user: maestro-bot\n  concurrency:\n    global_max: 2             # how many tickets to actively work at once\n  agent:\n    kind: claude              # coding agent: 'claude' (default) or 'codex' (OpenAI Codex CLI)\n    # command: /usr/local/bin/claude   # optional: override the binary/path (defaults to the kind name)\n    # NOTE: 'codex' is experimental and not yet verified against a live codex CLI — see issue #122.\nforges:\n  # Single entry per forge (shorthand)…\n  github: { host: github.com, token_env: MAESTRO_GITHUB_TOKEN }\n  # …or a list for multiple hosts of the same kind. A username only exists on its\n  # own forge, so an entry may carry its own bot_user (else defaults.bot_user).\n  gitlab:\n    - { host: gitlab.com, token_env: MAESTRO_GITLAB_TOKEN }\n    - { host: git.acme.internal, token_env: MAESTRO_ACME_TOKEN, bot_user: acme-bot }\nrepos:\n  - url: gitlab.com/group/api\n  - url: github.com/org/web\n```\n\nThe actual token values go in a `.env` file, which is gitignored:\n\n```sh\ncp .env.example .env\n# then fill in MAESTRO_GITLAB_TOKEN / MAESTRO_GITHUB_TOKEN\n```\n\n### 2. The per-repo config — `WORKFLOW.md`\n\nEach watched repo carries its own `WORKFLOW.md`, version-controlled alongside the\ncode. It tells Maestro how *that* repo wants to be worked: which branch to target,\nhow to merge, how to prove a change works, and any house rules for the agent\n(test commands, conventions, definition of done). The prompt body of this file is\nthe agent's operating manual. When you run `maestro add`, a sensible default is\ngenerated for you from a template.\n\n---\n\n### Per-role prompts and the stage pipeline (#29)\n\nA `WORKFLOW.md` body may declare role sections:\n\n```markdown\nShared conventions every agent gets.\n\n## role: define\nRefine the request into acceptance criteria. Ask, don't assume.\n\n## role: plan\nProduce the implementation plan and the checkbox todo.\n\n## role: implement\nExecute the plan, one atomic commit per step.\n\n## role: review\nJudge the diff against the plan. Block on real problems, not taste.\n```\n\nText above the first role heading is shared by every agent. A repo **without**\nrole sections keeps the original single-agent flow unchanged — roles are opt-in\nper repo.\n\nDeclaring roles replaces the single generalist agent with a staged pipeline,\nwhere each stage runs a cold session with only its own instructions:\n\n```mermaid\nflowchart LR\n    B[\"backlog\u003cbr/\u003edefine agent drafts\u003cbr/\u003eacceptance criteria\"] --\u003e|\"human applies maestro:todo\u003cbr/\u003eor replies /maestro approve\"| T[\"todo\u003cbr/\u003eplan agent writes\u003cbr/\u003ethe plan\"]\n    T --\u003e|\"branch + draft MR,\u003cbr/\u003eplan from birth\"| I[\"in progress\u003cbr/\u003eimplement agent,\u003cbr/\u003eatomic commits\"]\n    I --\u003e|\"done, proof posted\"| R{\"internal review:\u003cbr/\u003ea fresh agent\u003cbr/\u003ejudges the diff\"}\n    R --\u003e|pass| H[\"handoff —\u003cbr/\u003ehuman review\"]\n    R --\u003e|\"fail, round n\"| I\n    R --\u003e|\"bounce cap hit\"| BL[\"blocked —\u003cbr/\u003eover to you\"]\n```\n\n- **Backlog** — new issues land here. The define agent refines the request into\n  acceptance criteria and posts them as an issue comment. Then it waits for a\n  human: apply the `maestro:todo` label (the daemon never sets that label itself,\n  so its presence proves a person signed off) or reply `/maestro approve`.\n  Labelling the issue `maestro:todo` at creation skips definition entirely.\n- **Todo** — the plan agent writes the implementation plan. Only after that does\n  Maestro create the branch and draft MR, so the MR carries the plan from its\n  first second.\n- **In progress** — implementation, as before. But when the agent says \"done\",\n  you are not pinged yet.\n- **Internal review** — Maestro posts the proof, then starts a *separate* cold\n  session whose only job is to judge the diff. Pass → the normal handoff: you're\n  assigned, the MR is marked ready. Fail → the findings land as an issue comment\n  (\"round 1\", \"round 2\", …) and the implement agent picks them up next tick.\n  After `review.max_rounds` consecutive fails (default 3, configurable in the\n  front matter), Maestro stops bouncing and flips the ticket to blocked with a\n  summary — it never auto-merges and never silently drops work. Any comment from\n  you resets the round count and resumes the loop.\n\nThe labels you'll see on a roled repo, in board order:\n\n| Label | Meaning | Who sets it |\n|---|---|---|\n| `maestro:backlog` | being defined | daemon |\n| `maestro:todo` | definition approved, awaiting plan | **a human** — this is the approval gate |\n| `maestro:in-progress` | plan landed, implementation underway | daemon |\n| `maestro:in-review` | proof posted; internal then human review | daemon |\n| `maestro:blocked` | a question (or the bounce cap) needs a human | daemon |\n| `maestro:queued` | wants a slot, none free | daemon |\n\n`maestro:queued` is a capacity marker, not a stage: it can sit alongside any of\nthe others, means only \"waiting for a free concurrency slot\", and is retracted\nwhen work actually starts (or when you unassign the bot). On GitLab the labels\nare scoped (`maestro::backlog`), so they exclude each other automatically.\n\nOne design note worth knowing: in a roled repo the labels are *projections* for\nyour board, not the daemon's memory. The stage is re-derived every tick from\nartifacts — does an MR exist, is it still a draft, was the AC draft approved —\nso a crashed daemon, a stripped label, or an unblocked ticket all recover to\nexactly the right place. The one exception is `maestro:todo`, which is itself an\nartifact: a human put it there.\n\n## A walkthrough of the default `WORKFLOW.md`\n\nA `WORKFLOW.md` has two parts: a **front-matter block** (the settings, in YAML\nbetween the `---` fences) and a **prompt body** (plain Markdown below the fences,\nwhich becomes the agent's instructions). Here's the default, annotated.\n\n### The front matter — settings\n\n```yaml\n---\nforge: gitlab                  # gitlab | github (guessed from the repo's host if left out)\nproject: group/repo            # GitLab path, OR GitHub org/repo\nbot_user: maestro-bot          # the account tickets get assigned to\nmanage_board: true             # auto-create the labels (and, on GitLab, the board lists)\n\ntrigger:                       # the gate for what the bot is allowed to pick up\n  assignee: bot                #   the ticket must be assigned to bot_user\n  require_label: null          #   optional: also require this maintainer-added label\n  allowed_actors: []           #   optional: only trust triggers from these users (turn ON for public repos)\n\nproof:                         # how this repo proves a change works\n  type: playwright             #   playwright | test-output | diff-summary | none\n  command: \"npx playwright test --reporter=line\"\n\ngit:                           # this repo's own merge rules\n  default_branch: main\n  target: main                 #   which branch the MR/PR targets\n  merge_strategy: squash       #   squash | merge | rebase\n  delete_source_branch: true\n\nenvironment:                   # how to reach or boot a running instance (for proof)\n  base_url: http://localhost:3000   #   an already-running local instance, if any\n  start_command: \"npm run dev\"      #   else, how to start one\n  seed_command: \"npm run db:seed\"   #   load sample/dummy data\n  health_check: \"curl -sf localhost:3000/health\"\n\nclaude:                        # how the agent runs\n  command: \"claude\"            #   same binary as interactive; the daemon runs it headless\n  max_turns: 40                #   safety cap on how long one session can churn\n  stall_timeout_seconds: 120   #   kill a session that's been silent this long (then retry once)\n  permission_mode: acceptEdits #   how much the agent may do without asking\n\nconcurrency:\n  max_active: 2                # most tickets this one repo will work at once\n\nci:                            # gate the handoff on the head commit's pipeline (default off)\n  gate: false                  #   true: hold the handoff until CI is conclusive, bounce red CI back to the agent\n  wait_timeout_seconds: 1200   #   a pipeline still running after this hands off anyway (stuck/external CI)\n  max_fix_rounds: 3            #   red-CI bounces before the ticket is parked as blocked for a human\n---\n```\n\nThe blocks worth understanding:\n\n- **`trigger`** is your safety gate. By default a ticket just needs to be assigned\n  to the bot. For anything public, turn on `require_label` and/or `allowed_actors`\n  so a stranger can't kick off work by assignment alone.\n- **`proof`** is how Maestro *demonstrates* the change is good before pinging you.\n  `playwright` runs browser tests, `test-output` runs your test suite, `diff-summary`\n  just summarizes the change, and `none` skips it. If proof *generation itself*\n  crashes (a Playwright crash, a health-check timeout, a misconfigured command),\n  the first two failures retry quietly; the third consecutive one parks the ticket\n  as blocked with the failure posted on the issue, so a broken proof setup never\n  loops silently. Any reply from you un-parks it.\n- **`claude.stall_timeout_seconds`** is a watchdog, not a turn limit: a session\n  that emits *nothing* for this long is killed and retried once. Size it above\n  your repo's slowest silent command — a cold dependency install or full build\n  can legitimately produce no output for minutes.\n- **`environment`** only matters when proof needs a running app. If you already\n  keep a local instance up, point `base_url` at it; otherwise Maestro uses\n  `start_command` to boot one, `seed_command` to fill it with data, and\n  `health_check` to know it's ready.\n- **`git`** lets each repo keep its own merge habits — Maestro never imposes one\n  global rule.\n- **`ci`** is an opt-in extra gate (off by default). With `gate: true`, Maestro\n  reads the head commit's pipeline before handing off: a **passing** pipeline hands\n  off as usual, a **running** one *holds* the handoff (so you're never pinged\n  seconds before CI goes red) until it ages past `wait_timeout_seconds`, and a\n  **failed** one bounces straight back to the agent with the failing job logs\n  threaded in as context. After `max_fix_rounds` red bounces (counted since your\n  last comment, so any reply resets it) the ticket parks as blocked for you, the\n  same way the internal-review cap does. Works on GitLab pipelines and GitHub\n  check-runs; repos without CI are unaffected. While in review, a pipeline that\n  *regresses* (the target branch moved, someone pushed) bounces the same way —\n  but an approval still merges, so your branch-protection rules own merge-time\n  enforcement.\n\n### The prompt body — the agent's instructions\n\nBelow the second `---` is plain Markdown that becomes the agent's operating\nmanual. The template ships with a shared spine (the same six steps from the\nlifecycle above) plus a spot for your repo's house rules:\n\n```markdown\n# Agent operating protocol\n\nYou are working a single issue end-to-end in a cold session. Reconstruct all\ncontext from the issue, the MR description (your durable plan/todo), recent\ncommits + diff, and the repo conventions below.\n\n1. Orient — read the issue, the MR description, recent commits + diff, and the\n   conventions in this file.\n2. First session only — gather context. If the task is ambiguous, post a comment\n   with questions, set maestro:blocked, and stop. Otherwise, write a plan +\n   checkbox todo list into the MR description.\n3. Work the next unchecked item — one atomic commit per meaningful step.\n4. After each step — tick the box in the MR description; post a short progress\n   comment if notable.\n5. Done — all boxes checked + definition-of-done met → emit done.\n6. Blocked anytime — need a human decision → comment the question, label\n   maestro:blocked, stop.\n\n## Repo-specific conventions\n\n- Test: `npm test`\n- Lint: `npm run lint`\n- Definition of done: tests + lint green; proof attached; MR todo all checked.\n```\n\n**You mostly edit the bottom section.** The numbered protocol is the shared\ndefault — leave it alone unless you have a reason. The \"Repo-specific conventions\"\nblock is where you teach the agent about *this* codebase: the exact test and lint\ncommands, architecture notes, naming rules, and what \"done\" means here. Whatever a\nnew human teammate would need to know on day one belongs there.\n\n\u003e **Why the MR description matters so much:** notice that step 2 puts the plan and\n\u003e to-do list *into the MR description*, not into the agent's memory. That's\n\u003e deliberate. Because the agent starts cold every session, the MR description is\n\u003e its only durable scratchpad — and it's one you can read too. Open the MR and you\n\u003e see exactly what the agent thinks it's doing and how far along it is.\n\n\u003e **A second channel — `CLAUDE.md`.** The conventions block above rides in the\n\u003e WORKFLOW.md body, so Maestro is the one injecting it. But the agent is Claude\n\u003e Code, and Claude Code loads a `CLAUDE.md` from the repo root on its own, every\n\u003e run, with no help from Maestro. That makes `CLAUDE.md` the home for durable,\n\u003e repo-owned knowledge: conventions, decisions the team has settled, architecture\n\u003e notes, links into `docs/` or your ADRs. And because a `CLAUDE.md` can point at\n\u003e other files, you can wire up a whole tree of standing context and keep all of it\n\u003e in git. It fits *the forge is the memory*: the knowledge survives because it's\n\u003e committed, and it changes only when an MR merges it.\n\n---\n\n## Running it\n\nThe daemon is one process that watches **all** your repos. You never run one\ndaemon per repo.\n\n```sh\n# load the forge tokens, then start the daemon (watches everything in the config)\nset -a; . ./.env; set +a\nmaestro daemon\n```\n\nThe daemon reads tokens from its environment, not from the `.env` file directly —\nhence the `source` line (a [service](#keeping-it-running-background-and-boot)\ndoes this for you via `EnvironmentFile=`).\n\nOn startup it preflights your tools (`git`, `claude`, and the forge binaries you\nneed) and refuses to boot if any are missing — so a misconfigured host fails fast\nwith a clear message instead of silently looping.\n\nDay-to-day you'll mostly use the CLI:\n\n| Command | What it does |\n|---|---|\n| `maestro daemon` | Start the daemon — one process that watches every repo in the config and works assigned issues. Preflights tools, then loops. |\n| `maestro add \u003curl\u003e` | Start watching a repo. Sets up its labels/board and commits the config change. Add `--public` to opt into a public repo (read the safety notes first). |\n| `maestro list` | Show all watched repos and what's in flight. |\n| `maestro status \u003cissue\u003e` | Show one ticket's current stage. |\n| `maestro logs \u003cissue\u003e` | Show the agent's logs for a ticket. |\n| `maestro run \u003cissue\u003e --attach` | Open an **interactive** Claude in that ticket's workspace so you can watch or drive it by hand. Local-dev only, not the daemon path. |\n| `maestro dashboard` | Start the web dashboard (same as `node packages/web/dist/main.js`) — see below. |\n| `maestro doctor` | Check that every required tool (`git`, `claude`, `glab`/`gh`) is on your `PATH`. Exits non-zero if anything's missing. |\n\n### The dashboard\n\nA small read-only **web dashboard** shows the same information in your browser —\na live status table of every watched repo and its tickets, plus an \"add a repo\"\nform — and auto-refreshes every few seconds.\n\n![The Maestro dashboard: two repos with their tickets and colour-coded lifecycle states](docs/assets/dashboard.png)\n\n*Example view — each ticket shows its current lifecycle state, and each repo\nsummarises its counts. A repo whose forge can't be reached shows as \"unreachable\"\ninstead of looking idle.*\n\n```sh\nmaestro dashboard      # → http://127.0.0.1:4000\n```\n\nOverride the bind address with `MAESTRO_WEB_HOST` / `MAESTRO_WEB_PORT`. The same\nendpoint also serves the raw read-model as JSON to any non-browser client (handy\nfor scripting), so `curl localhost:4000` gives you the data the page renders.\n\n**Adding repos from the dashboard is off by default.** The `GET` paths are\nread-only and always open, but `POST /repos` (the \"add a repo\" form) mutates your\nconfig and creates labels plus a bootstrap issue/PR on the forge — so it stays\ndisabled unless you opt in by setting `MAESTRO_DASHBOARD_TOKEN`:\n\n```sh\nMAESTRO_DASHBOARD_TOKEN=\"$(openssl rand -hex 32)\" maestro dashboard\n```\n\nWith no token set the write path doesn't exist (a `POST /repos` returns `404`) and\nthe add-repo form is hidden. With a token set, the form appears and each add must\ncarry it as `Authorization: Bearer \u003ctoken\u003e` (compared in constant time); a missing\nheader is `401`, a wrong token `403`. This keeps a read-only dashboard safe to\nexpose on a shared tailnet/LAN while gating the one write path behind a secret. On\nan untrusted network, still prefer binding `127.0.0.1` and fronting it with\n`tailscale serve` + ACLs.\n\n### Keeping it running: background and boot\n\n`maestro daemon` runs in the foreground. For a quick detached session, `tmux`\n(or `nohup maestro daemon \u0026`) does the job — but it dies with the machine. The\nproper way to survive reboots is a systemd **user** service; the repo ships a\nready unit at [`templates/maestro.service`](templates/maestro.service):\n\n```sh\nmkdir -p ~/.config/systemd/user\ncp templates/maestro.service ~/.config/systemd/user/\n$EDITOR ~/.config/systemd/user/maestro.service   # set the three EDIT lines (paths)\nsystemctl --user daemon-reload\nsystemctl --user enable --now maestro\n\n# start at boot without anyone logging in\nloginctl enable-linger $USER\n\n# watch it\njournalctl --user -u maestro -f\n```\n\nThree things the unit handles that a bare `nohup` doesn't:\n\n- **Tokens.** Nothing in Maestro reads the `.env` file itself — the tokens must\n  be in the daemon's process environment. In the foreground you load them into\n  your shell once (`set -a; . ./.env; set +a`); the unit does it declaratively\n  with `EnvironmentFile=`.\n- **PATH.** The daemon shells out to `git`, `claude`, `glab`/`gh`, and a systemd\n  user session's default `PATH` misses the usual homes of two of them\n  (`~/.local/bin` for claude, `/snap/bin` for glab). The unit extends `PATH`;\n  if a tool still can't be found, the startup preflight fails fast and names it.\n- **Restarts.** `Restart=on-failure` is safe precisely because of the design\n  above: the daemon keeps no state of its own, so a restarted process re-reads\n  the forge and picks up every ticket where it left off.\n\nA user service (not a system one) is deliberate: the daemon runs Claude with\n*your* login and settings, so it should run as your user, not root.\n\nThe dashboard has a matching unit,\n[`templates/maestro-web.service`](templates/maestro-web.service) — same install\nsteps with `maestro-web` as the unit name. It carries commented `Environment=`\nlines for the bind address and the optional `MAESTRO_DASHBOARD_TOKEN` write\ngate, so the on-by-default state stays read-only.\n\n---\n\n## How the daemon decides what to work on\n\nYou can watch a hundred repos on a tiny machine. The thing that costs real\nresources isn't *watching*, it's *working*. A repo with nothing assigned, or a\nticket sitting in review, costs only a cheap periodic check. Compute is spent only\nwhile a ticket is actively being worked, and each active ticket holds one \"slot\".\n\n```mermaid\nflowchart LR\n    R1[\"repo: api\u003cbr/\u003e2 tickets waiting\"] --\u003e Q\n    R2[\"repo: web\u003cbr/\u003e1 ticket in review\"] -. no slot needed .-\u003e Idle\n    R3[\"repo: docs\u003cbr/\u003enothing assigned\"] -. just polled .-\u003e Idle\n    Q[\"Work queue\"] --\u003e S{\"Free slot?\u003cbr/\u003e(global_max: 2)\"}\n    S --\u003e|yes| Work[\"Active worker\"]\n    S --\u003e|no| Wait[\"Queued\"]\n```\n\nIf more tickets are ready than you have slots, the extras simply queue — and get\nthe `maestro:queued` label, so the queue is visible on the forge instead of\nlooking like silence. Nothing breaks — throughput is just capped. The right\nnumber of slots depends on your machine's RAM, since each active worker runs\nClaude plus possibly a browser for proof. On a 4 GB box, 1–2 is sensible.\n\nTo scale up, you don't add daemons — you add **machines**, each running its own\nsingle daemon watching a few repos. They never coordinate with each other; the\nforge is the shared source of truth, so there's nothing to sync.\n\n---\n\n## Maestro can manage itself\n\nThe Maestro project is just a git repo, which means Maestro can watch *itself*.\nWant to add a repo or bump concurrency? File a ticket on the Maestro repo. The\nagent edits `maestro.config.yaml`, opens a merge request, you approve, it merges,\nand the daemon hot-reloads the new config. There's no separate admin panel —\nmanaging Maestro *is* using Maestro.\n\n---\n\n## A note on safety\n\nMaestro runs autonomous Claude (and possibly a browser for proof) directly on the\nhost machine, unsandboxed. For your own **private** repos with a dedicated bot\naccount, that's a reasonable tradeoff. For **public** repos it's riskier, and\nsupport for them is deliberately opt-in (`--public`):\n\n- **Who can start work** is gated by forge permissions — assigning a ticket to the\n  bot requires write/triage access. You can tighten this further with a required\n  label or an allowlist of trusted users.\n- **What a ticket says** is the harder problem. On a public repo, ticket text is\n  written by strangers, and the agent acts on it with the bot's credentials. The\n  real fix is per-ticket container isolation, which is a planned future step. Until\n  then, treat public-repo support as experimental and keep secrets out of the\n  workspace.\n- **Permission mode.** Headless, the agent has no human to approve tool calls, so\n  it ships defaulting to `bypassPermissions` (`--dangerously-skip-permissions`) —\n  otherwise it can't even `git commit` its work or run a proof. That means it runs\n  unsandboxed Bash on the host. Fine for a private repo you trust; for a public one,\n  override `claude.permission_mode` to a constrained mode (`acceptEdits`/`default`)\n  and accept that the agent can't commit or prove until the container sandbox lands.\n\n---\n\n## For developers\n\nIt's a pnpm + TypeScript monorepo.\n\n```\npackages/core   the brain: reconciler, forge adapters, agent runner, proof,\n                config + workflow loaders, daemon loop, tool preflight\npackages/cli    maestro add | status | list | logs | run | dashboard | doctor + daemon entry\npackages/web    read-only dashboard (HTML page + JSON API) + add-repo form\ntemplates/      the default WORKFLOW.md used when onboarding a repo\nscripts/        setup.sh — one-shot install + build + tool check\n```\n\nThe CLI and web packages are intentionally thin — almost all real logic lives in\n`core` so both interfaces behave identically.\n\n```sh\npnpm install\npnpm typecheck   # strict TypeScript\npnpm test        # vitest\npnpm lint        # biome\npnpm build       # per package\n```\n\n### Where to read more\n\n- **Design spec (the locked source of truth):**\n  [`docs/superpowers/specs/2026-06-03-maestro-design.md`](docs/superpowers/specs/2026-06-03-maestro-design.md)\n- **Build roadmap and milestone history:** [`tasks/todo.md`](tasks/todo.md)\n- **Architecture vocabulary and settled seams:** [`CONTEXT.md`](CONTEXT.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fl4ci%2Fmaestro","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fl4ci%2Fmaestro","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fl4ci%2Fmaestro/lists"}