STMA
OfficialProvides GitHub issue integration: agents can list issues, start runs linked to an issue, and post comments back on issues on completion or handoff; webhooks also turn new issues into team announcements.
Allows generating a GitHub Actions CI pipeline from the team's delivery flow document, and can receive pull_request and workflow_run webhooks to record PR/CI outcomes on the relevant run.
Provides read-only Jira integration for delivery flows, allowing Jira-shaped task keys to pull ticket summaries as run intent and supporting flows whose tickets live in Jira.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@STMAWhy does the build fail on Dana's machine? Compare our environment snapshots."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
STMA — Speak to my Agent
AgentOps for teams that build with coding agents.
STMA is the operations layer between the coding agents your team runs — Claude Code, Codex, Cursor, any MCP client — and the people responsible for what they do: a live map of every agent run, a warning before two agents touch the same file, the team's rules delivered to every run, work assigned to a named agent or handed over before a usage limit ends a session, environment snapshots that carry names and never values, and an evidence pack a reviewer can check.
It works the same for one person with a laptop and a desktop as for several people on a project.
Start here: just me, two computers
Use one STMA server and one account. Sign in to your existing deployment. On Workspaces, choose New workspace and name it My workspace, or use the workspace you already have. A “team” is a workspace — it can contain just you. Only its name is required.
Connect your first agent. For a Claude Code or Codex checkout the fastest way is one terminal command: on the project's own Agents page (or on Agent connections, where you choose the project yourself), fill in Fastest · one terminal command, press Create connect command and paste the result into a terminal opened in that checkout (
npx -y @matteai/stma connect CODE --server …). It shows the workspace and project, asksy/N, then adds the checkout's MCP entry and its local hooks as one identity: no browser consent, no login step, nothing to pair. Paste it into a terminal, not into the agent: the code is a one-use, ten-minute secret, and a careful agent refuses to redeem one. Codex gets one STMA identity per machine, because it loads its user configuration in every checkout. For any other client use OAuth (published:@matteai/stma@0.14.0). If Claude Code already has another STMA entry at user scope, the command says so and the hook names the entry its calls belong to; remove or scope the other one so the agent cannot mix identities. For any other client use OAuth: open Agent connections, choose the client and copy its setup request into that agent. The request tells the agent to use only Codex or Claude Code's built-in MCP command for the displayed address; it contains no credential or installer. Complete the one native OAuth flow the client starts. STMA opens in your browser; sign in, name the agentlaptop-agentand machinelaptop, choose the exact workspace/project, and approve. Reload the client once so the running task loads the new tools. The client owns credential storage and refresh. No enrollment code, bearer token, downloaded installer or hand-edited config is carried through the model conversation.Connect your other agent. Copy the matching setup request into the client on the second computer and authorize
desktop-agent/desktopseparately against the same workspace/project. STMA creates a unique installation identity and revocation boundary for each approval. Never copy a token or client configuration between machines; there is no need for a second human account. Several Claude Code agents on one machine work too: each Git checkout connects as its own agent, so paste the same Claude request into each.See the reply. Ask the second agent to call
whoami, read the STMA inbox and reply; ask the first agent to check. Authenticated installation origins survive reloads. This proves an exchange, not completed work or physical devices. The older one-time setup prompt remains a compatibility fallback for clients that cannot complete OAuth, not the recommended path.Give it work from anywhere. On the project page choose Assign work, pick
desktop-agentand say what to do — or telllaptop-agent: "assign the filter change to desktop-agent through STMA". The desktop agent finds it in its inbox as assigned to it by name; on that machine you say one sentence — "read your STMA inbox and do what is assigned to you" — instead of typing the task again. (If that checkout also runs the local adapter and you paired it withdesktop-agent— on the adapter's approval screen, or later under Agent connections → Listens for — its prompt hook announces the assignment by itself and nobody says anything.)
No repository setup, CLI, project creation, governance or paid Team subscription is needed for this message check. Cloud Free covers it (connecting agents is never limited by machine) and the paid Solo plan is optional. “Personal” credential access means every workspace this account can reach while the credential is active, not “I work alone”; one workspace's Entire workspace access is enough and includes its current and future projects. Existing project-only agents should stay on the same project.
One shared address matters. Two separate localhost servers cannot see each other. To
self-host, start one instance, make it reachable at a secured HTTPS
address from both machines, and set BASE_URL to that address before connecting clients. Keep
development servers private. The hosted service is a public beta: anybody can create an
account with an email address, everything is switched on and nothing is billed. You can also
self-host — the self-host path needs no
invitation and is not a reduced product.
STMA does not wake a sleeping agent. MCP is client-initiated: the server cannot start local commands
or inspect repository files, and receives local data only when the client chooses to send it in a
tool call. Ask the second agent to read and reply, then ask the first to check. After that first
result, add repository context, other people or work tracking only when needed. The
in-app guide explains each optional next step; use
/docs#quickstart on your own deployment for its correct MCP address.
Related MCP server: agent-comm
Beyond the first reply
The console calls teams workspaces; API fields and /app/teams/... URLs stay compatible.
The hierarchy is workspace → project → scoped agent connection: every project belongs to one
workspace, and a project-only connection is accepted only for that exact pair. Where the client
keeps its MCP entry (Codex user configuration, a Claude Code checkout) decides which local tasks
load it, never the server-side project grant. Plans also belong to workspaces, not accounts; one person can have
different roles in several workspaces on different plans.
Needs attention collects bounded handoffs, overlap events and missing policy reports.
Management tools live under Manage workspace. A handoff needs explicit update_handoff
accept/resume/complete actions; a chat reply cannot silently clear it.
Evidence distinguishes agent reports from provider observations. Policy hash matching does not
prove compliance, and completed does not prove CI passed. Delivery .md downloads carry immutable
schema-v2 setup IDs; record_delivery_receipt validates scope, mode and hashes but never treats an
agent report as human approval. Repositories supports exact project bindings and read-only GitHub
workflow verification. Single-workflow success is not all required checks.
Run readiness is advisory: approvals, change budgets, duplicate work and overlapping claims are
warnings an agent and its human must act on, not a technical lock. Immutable start, delivery and
tested checkpoints bind a client-reported repository identity and commit to a run. Provider facts
turn green only when they match the newest delivery/test checkpoint exactly; a clean checkpoint is
still a client report, not permission to merge or deploy.
Provider credentials require reversible storage. Self-host operators can configure
STMA_INTEGRATION_KEYS (JSON key-version to 64-hex-character key) and
STMA_INTEGRATION_ACTIVE_KEY for AES-256-GCM encryption with workspace/provider/locator binding.
Keep keys outside the database. Without configuration, legacy self-host storage remains plaintext;
set STMA_INTEGRATION_REQUIRE_ENCRYPTION=1 to refuse unencrypted new writes. Rotation reads old
versions while writing the active version. For PostgreSQL, run
npx tsx scripts/rotate-integration-secrets.ts for a read-only dry run. Applying requires --apply
and --confirm-database=<exact name>, a backup, and all old key versions. No credentials are printed.
Activity/event age limits and row caps differ from snapshot/session retention. “No age expiry” is not unlimited history. User-submitted messages/attachments may contain code or secrets; the snapshot collector's omission of values is not a guarantee about all submitted content.
STMA (Speak to my Agent) is a vendor-neutral control plane for a company's coding agents. MCP is one transport surface; the product layer answers the operational questions MCP does not: which human owns an agent, what project and task it is working on, which files/contracts it may touch, which global rules it actually received, and whether its environment is safe to work in.
The identity, onboarding, scope, governance, delivery and offboarding state machines are mapped in PRODUCT_FLOWS.md. It is the change checklist for avoiding dead ends and cross-scope drift.
Environment snapshots — agents push structured snapshots (tool versions, lockfile hashes, env var names, git state). Secret values are never collected. Snapshots are stored per machine, so one person's laptop and desktop each keep their own slot and history.
Automatic env diff — one tool call compares two environments and reports the differences that usually explain "works on my machine" — a teammate's machine, or your own two machines (personal fleet:
compare_envwithdevice+their_device).Debug sessions — topic-based rooms where agents exchange typed messages (question, hypothesis, info-request, resolution) asynchronously, with humans in the loop.
Plain MCP over HTTP — works with any MCP-capable agent (Claude Code, Cursor, ...). No local daemon, no NAT issues; everything is persisted server-side so teammates never need to be online at the same time. Calls originate from the client: STMA cannot wake an agent, execute a local command or read repository files by itself.
Live Agent Map — active runs are attributed to a human, client, team, project, task, branch and leased work scope. Overlapping writes are surfaced before agents silently invalidate each other's work.
Canonical policy control plane — team and project policy is merged deterministically, fingerprinted, acknowledged by each run, and compiled locally to
AGENTS.md,CLAUDE.md, or a Cursor rule.Environment preflight — a project baseline is compared with the agent machine before work; only environment variable names are collected, never values. Names come from the machine and from the repository's local dotenv files; committed templates like
.env.exampleare skipped, because a file that is identical everywhere can only mask the key that differs. A snapshot that reports no names at all is treated as unchecked rather than as empty.
What's here today
Private beta. This section documents the current source contract; a capability is not available on a deployment until that build is released and its environment-specific gates pass. Highlights:
37 MCP tools over a stateless Streamable HTTP endpoint: identity & onboarding (
whoami,list_teammates,create_invite,onboard_repo,list_projects), snapshots & diff (get_snapshot_checklist,push_snapshot,get_snapshot,compare_env), debug sessions (open_session,list_sessions,get_session,post_message,resolve_session,inbox,search_past_issues), team-wideannounce, and the fleet group (start_run,update_run,finish_run,list_active_agents,get_policy,get_workflow,check_environment,handoff_work,assign_work,list_issues,get_evidence,launch_check,update_handoff,record_delivery_receipt). The Knowledge Hub group isget_knowledge_context,report_knowledge_receipt,search_knowledge,get_knowledge, andpropose_knowledge.The fleet layer is reachable from MCP alone. Runs, work claims, conflict warnings, policy pull and environment preflight no longer require the CLI and native hooks. A current connection binds one scoped credential to one durable agent installation; legacy PATs retain their token-derived installation for compatibility. The CLI still adds what only a local process can: automatic lifecycle hooks, profile-scoped offline outboxes, local git observations and policy compilation into
CLAUDE.md/AGENTS.md.One stable MCP address; one browser-approved identity per client. Add
/mcpin the agent client's supported HTTP-MCP settings. Standards-based OAuth discovery, dynamic client registration, exact callback matching and PKCE S256 move identity/scope approval into STMA's browser UI. Access tokens are short-lived; refresh tokens rotate; reuse revokes the linked installation. Each approval still creates a unique installation and enforced project/workspace grant. A copied setup request can ask the agent to invoke the client's native MCP command, but the model never handles the credential or hand-edits raw user configuration. The OAuth client type shown on consent is fixed from that client's registration, and a successful authenticated MCP initialize marks the installation active;whoamithen verifies the visible identity/scope.Legacy setup remains bounded, but is no longer the default. For a client without OAuth, Agent connections → Legacy setup prompt can still issue the one-use, 30-minute Connector v2 envelope. After one informed approval, the agent downloads the product-owned connector, verifies its pinned SHA-256 and runs it; it does not generate a credential parser. The connector supports Codex, Claude Code and Cursor on macOS/Linux with Node 20+. It adds a unique
stma_<enrollment-id>alias to the private user-level config, preserving other connections and settings. No tracked repository source is changed. Cross-repository availability does not broaden the server grant. Codes/tokens are never echoed or placed in process arguments; a private bootstrap file is consumed and deleted, not securely erased. Redemption first creates limited bootstrap authority. Onlywhoamiconfirms legacy client loading; an unconfirmed credential stops working after 15 minutes without a cleanup job. Configuration installed, client confirmed and repository ready are different facts. After confirmation, standard core credentials have no automatic expiry; managed deployments can impose one. Validation or local-write failure triggers one self-revoke attempt, with exact cleanup evidence. Lost replies never trigger another redemption. Revoke access in the console and remove only the matching local alias when disconnecting. Existing credentials are not silently revoked. Project-scoped Claude Code/Codex prompts also disclose local tracking in the same approval: a hash-pinned standalone runtime, ignored profile state and client hooks. A read-only profile preflight runs before redemption. No npm installation or token transfer is needed; native state references the saved MCP alias. Codex additionally requires its own hook-trust review. Workspace-only/Cursor connections remain MCP-only unless native setup is separately approved.Assign work to a named agent (
assign_work, 2026-09-18): the other direction. A lead — a person on the project page, or their agent over MCP — names an agent aslist_teammatesshows it ("Codex B"), says what to do, and the assignment lands in that agent's inbox as its own: only that installation can accept, resume or complete it, and every other agent sees it as somebody else's. The prompt hook announces it when the hook shares that agent's connection, or when the checkout's separately authorized local adapter is paired with that agent and works in the project the assignment names; otherwise the human there says one sentence — "read your STMA inbox" — rather than retyping the task.assign_workanswershookWillAnnounceso the lead knows which it is. Name the project as the console shows it (name, slug or repository): the existing project is always used, and only a name no project has creates one — the answer says so. Nothing is released and nothing is attested — the receiving agent's ownstart_run, pre-filled from the assignment, records the ground it takes under the normal policy and collision rules. The brief stays the lead's words; thestart_runcall is STMA's record. The task can come from a connected tracker (2026-09-20): the dialog's Ticket field takes#42orowner/repo#42,PROJ-42, or a ClickUp task link, reads the ticket and fills whatever you left empty — the ticket's own key becomes the task, and its summary and link go into the brief. For GitHub and ClickUp you can also Browse the twenty most recently updated open tickets and pick one instead of looking the key up, or Search them by a few words when the ticket you want is not among the newest twenty. Both are read only when you ask for them, so neither slows the page down, and a tracker that refuses says so instead of showing nothing. The line under the results says what was actually looked at, because the two trackers reach different distances: GitHub searches the whole repository through its own search endpoint, while ClickUp has no task search in its API at all, so STMA reads the three hundred most recently updated open tasks of the mapped List and matches them itself. Jira can be neither browsed nor searched yet, and the dialog says why — Atlassian moved issue search to an endpoint STMA has never measured against a real site, and a guess that fails while you are assigning work is worse than no button. Paste the key; STMA reads it the same way.Agent handoff (
handoff_work, optionallyto_agentso one named agent, not a person, is the only one that can take it and its prompt hook announces it): an agent about to hit its usage limit pushes its branch and attaches an immutable delivery/tested checkpoint in the same call (or records it on the run first). A branch handoff without that repository, exact commit, clean-worktree and test observation is refused without releasing the run. It then hands the task over with a brief — what is done, what is left, the scope it was holding and the exact call to re-claim it. Its own claims are released, the brief lands in the team inbox, and the code normally travels through git; messages and attachments can contain submitted code. Omit the branch and it hands over a plan instead — a runbook for your other machine is a handoff of intent, and it rides exactly the same rails.inboxlists work waiting to be picked up separately from unread messages, and a handoff addressed to somebody emails them. The actionable half arrives as a structuredresumeblock STMA wrote from the run itself, not as prose: a receiving agent is told — correctly — to treat message bodies as data, and it should not have to parse instructions out of them to do the one thing a handoff is for. Generate onerequest_idfor a new handoff and reuse it unchanged after a timeout: STMA returns the original session instead of charging the quota, posting the brief or closing the run twice. The durable handoff and queued direct notification commit with the run release; issue comments and team webhooks remain best-effort external side effects. For code handoffs,resumerequires the same canonical repository and commit plus a clean receiving worktree. A Knowledge-linked resume additionally names the receiver's activerun_id; that run must belong to the accepting installation and have its own immutable start checkpoint. The handoff carries a Knowledge Hub context reference rather than copying its text; the receiver resolves current authorized knowledge and sees whether its versions changed. An exact retry returns the recorded receiver context even after that receiving run becomes terminal. A secret that exists only on the source machine never belongs in the brief or on the receiver: the source machine runs the secret-dependent check and shares only its non-secret result. Peer-authored branch-handoffnext_stepsthat mention credentials are accepted only in that explicit source-machine/non-secret-outcome form; instructions to provision, copy or use a credential on the receiver are refused before the handoff is created or the source run releases its claims.Knowledge Hub (
/app/teams/<slug>/knowledge): versioned decisions, domain facts, procedures, references and known solutions with workspace or selected-project audiences. Agents and owners may create immutable drafts; only a workspace owner publishes, supersedes, archives or withdraws current knowledge. Draft, expired, archived and withdrawn text is excluded from current retrieval. A record with no review date has unknown freshness, never silently current. Publish/archive/withdraw/content-deletion actions cross the critical audit seam. Imported source provenance supplies canonical repository identity and a full commit together; the owner view separates checked, changed and reviewed time and shows bounded version history and line diffs. Exact historical version IDs remain readable only while the caller still has today's audience access, and are labelledsuperseded,archived,withdrawnorexpiredrather than current. The resolver uses project and planned path claims before lexical ranking, returns a deterministic context bounded to 8 KiB and records an immutable version/hash manifest for a run or checkpoint. Reporting is not automatic: after applying that exact manifest, the client explicitly callsreport_knowledge_receiptor runsstma knowledge receipt --context UUID --manifest SHA256. Its first report for that context is immutable. A wrong hash is retained as mismatch evidence and returns HTTP 409 (or an explicit MCP error); a retry or corrected hash cannot overwrite it. A receipt distinguishes “server served” from “client reported”; neither means the agent obeyed the text, and published text is reference data rather than execution authority. An owner cannot silently move an active stable key to a different audience: that publish conflicts until the current version is explicitly archived or withdrawn. Explicit owner deletion scrubs retained body, title, source, audience and stored response copies; content-free tombstone IDs, hashes, manifests, receipts and critical audit evidence remain so old run evidence is not rewritten. Deleted active content no longer consumes hosted corpus capacity, but STMA cannot remotely erase text already delivered to an external client. Imports are explicit selected UTF-8 text/Markdown uploads; STMA does not crawl the repository, follow symlinks or fetch arbitrary URLs. Hosted deployments apply separately configurable engineering caps for content bytes, draft records, published records and versions; an over-cap write returns current/limit capacity instead of deleting history. These defaults are safety guardrails, not plan entitlements. Self-hosted instances do not apply them.Your other machine is not you. Whether a message is news is decided by where it came from, not by who wrote it: tokens are issued one per machine, so a session your desktop's agent opened is unread on your laptop and in your browser, and never unread on the machine that wrote it. One human running agents on two machines is the case this product exists for, and treating "same account" as "already seen" hid it from itself.
A savings ledger that will not flatter itself.
/app/teams/:slug/savingslists the moments worth counting — collisions warned about, duplicate work caught, machines stopped before they started, limits work survived — and asks a person whether each one changed what they did. Only the ones that did are counted. What STMA merely observed is shown next to that number and never converted into money, and minutes stay minutes until an owner says what an hour is worth. The number a buyer checks first is the number that has to be true.Vendor quota, before it bites. An agent reports how much of its own allowance it has spent (
update_runwithusage, orstma run heartbeat --used-pct); at 75% STMA tells it to plan a handoff and at 90% to make one, pre-filled with the run it is holding. Only the client can know that number, so STMA never guesses it — and will not act on the agent's guess either. A figure markedmeasuredmoves the fleet: the agent map shows who is about to stop and the feed records the escalation once. A figure with no source is filed as an estimate, drawn as one, and kept out of both. An invented percentage that triggers a handoff at the wrong moment costs more than the handoff it was meant to save.Parallel attempts are a fan-out, not a collision. Runs sharing an
attempt_group— one prompt across several worktrees — never warn each other about the overlapping scope that is the whole point, and the map labels them "attempt 2 of 3". Two agents of yours in the same worktree still collide, because that one is the accident.Delivery flows (
/app/teams/<slug>/delivery): the team lead's "how work moves here" onboarding document as data — ticket rules, branch naming, required checks, PR approvals and the environments on the road to production. One document, four renderings derived together: a prose brief agents pull withget_workflow(and are pointed at bystart_run), an SVG picture of the flow, CI for Azure DevOps or GitHub Actions, and an English Markdown setup pack a user can hand to a coding agent. Eight built-in blueprints cover solo CI, trunk-based continuous deployment, pull-request previews, staged promotion, progressive delivery, GitOps, ticket gates and release trains. The catalog follows the operating patterns in DORA's continuous-delivery guidance, GitLab Review Apps, Microsoft's progressive-exposure guidance and Argo CD's GitOps model. A five-question wizard carries the chosen release model, tracker, CI provider and review strength into its recommendation. Each environment accepts its real deploy command once; both provider renderers reuse it, and a readiness panel names missing checks, commands, tracker connections and external approval setup. Before publishing, or from an existing flow, Send this flow to an agent previews or downloads a setup pack for an exact team/project scope. The user choosesplan-onlyor the approval-gatedpropose-then-applymode and may inject the effective governance snapshot for that scope. Plan-only packs omit implementation steps instead of merely asking a model to ignore them. Project governance is the already-merged team + project policy, with its hash and source versions; individual rules cannot be cherry-picked. The pack performs read-only auth preflight, leaves login and consent to the user, forbids secret values in chat or files, and requires a structured completion receipt. Downloads require team access and use private, no-store responses. Repository-specific adaptations are reported back as suggested delivery updates; automatic receipt import is deliberately not implied.Missing fields keep the result explicitly labelled Pipeline scaffold and a direct/legacy Apply is refused. Once complete, the same owner-confirmed action applies the ready
azure-pipelines.ymland registers it over the API. Connect Azure DevOps on the team page — or right on the delivery page, where the PAT is needed — without losing the selected flow. A project-scoped flow carries the project's durable id, so a repository-backed project is not duplicated into a same-named legacy scope and Apply resolves the exact reviewed repository binding. Azure PATs are organization credentials; STMA's repository binding limits which exact repository/project pair the connection is used for rather than claiming the PAT itself is repository-scoped. A read-only Jira connection covers flows whose tickets live there. Connections are verified the moment they are saved and the verdict is shown next to Apply; an empty repository is handled (the first apply creates the branch), a re-apply updates the file and reuses the existing pipeline, and failures answer in remediation language (expired PAT, missing scope, token minted for another organization) with fold-out step-by-step token instructions on both forms. Deploy jobs check out the repository, and dependencies stay inside their pull-request/merge/tag/manual trigger lane so one event cannot be skipped behind a job from another. Concurrent publishes are serialized and a database constraint guarantees one active flow per team/project scope. The flow also reaches the run:start_runwarns when a required ticket is missing or the branch breaks the flow's naming rule (advice, never refusal), and a Jira-shaped task key pulls the ticket's summary in as the run's intent.Project-scoped governance: a project's sections live under the project —
/app/teams/<slug>/projects/<project>/{agents,work,sessions,activity,knowledge,governance, delivery,environments}— which is what the rail and the in-page links produce. The same pages still answer the older?project=filter from the picker in the strip, so links already sent keep working: global by default, one project when chosen, the selection in the URL so it survives refresh and pastes to a teammate. Scoped governance narrows receipts, preflights, baselines and the timeline, and opens the policy editor on that project's own additions rather than the merge. Pickers and baseline promotion carry the durable project id; if a historical legacy row shares the display name, the repository-bound and legacy scopes stay visibly distinct instead of writing evidence to whichever name lookup happens to win.GitHub issues, both directions. An owner connects one repository on the team page; after that agents call
list_issuesto pick up work that already exists,start_run {"issue": 42}makes the issue number the task key and its title the run's intent, and finishing or handing off posts a comment back on the issue. Inboundissueswebhooks become team announcements, so a new issue reaches every agent's inbox.ClickUp tasks over OAuth, scoped to a project List. A workspace owner chooses Connect ClickUp, approves STMA in ClickUp, then maps each STMA project to one exact List. Agents call
list_clickup_tasks, pass a returned id, aPD-207-style custom id or a pasted URL asstart_run.clickup_task, and a finished or handed-off run can comment back. The OAuth access token stays encrypted on the server and never enters an agent prompt or MCP response; a task outside the mapped List is refused, and so is a mapping whose STMA project no longer exists. The owner's card names when the connection was last checked and what its last comment did, and carries Pause — which keeps every mapping and the credential while STMA stops calling ClickUp — beside Disconnect, which removes both. A comment is one attempt after STMA's own durable event: ClickUp's comment endpoint takes no idempotency key, so a failure is reported on that card rather than retried, and a tracker never fails a run.Terminal-first onboarding: an owner's agent returns one invite instruction block. The invitee's agent can redeem it with
{code, email, password, agent_name, device, client, role}and receive an account, team membership and a team-scoped credential bound to that installation — no browser required. Passwordless accounts fall back to the browser. Browser and terminal claims share one atomic path: max-use and plan limits cannot be oversubscribed by simultaneous requests, and a losing terminal claim does not leave an empty account behind.Projects can be created by a workspace owner from Projects or the inline New project bar on Agent connections, and are still born automatically from the repo identifier agents send. Both paths use the same canonical repository resolver. Repository identity keeps host/owner/repository distinct from the display basename, so two different owners'
apirepositories do not silently share history. Once a project-scoped credential is issued, every write is pinned to that durable project ID; the shorter display name injected for convenience cannot create a second legacy project beside a URL-created one. Per-project stats include (open sessions, active agent installations 7d, last snapshot). Opening a session from a project carries that project into an existing-project picker; a typo in the browser no longer creates a second project and splits its history.DevOps hooks: per-team secret URLs turn CI notifications and GitHub push webhooks into team announcements every agent sees in its inbox. Set the GitHub webhook Secret to the same token: GitHub requests without a valid HMAC signature are rejected. Generic CI systems that cannot sign use the separate token-only announce hook.
PR/CI outcome linkage: point GitHub's
pull_request+workflow_runwebhooks (same URL) or Azure DevOps service hooks (git.pullrequest.*,build.complete→/api/hooks/azure-devops/<token>) at STMA and the verdict lands on the run that declared the branch — PR opened/merged/closed and the last CI result, written to the run's trail on change only, shown in the evidence pack and the agent-map inspector. A merged PR reaches the activity feed asrun_merged: "a person said it helped" becomes "the change merged". Runs with no webhook wired stay unlinked, never "fine".Run cost, measured only:
update_run {"usage":{"cost_usd":4.20,"source":"measured"}}records what a run actually spent — with the same discipline as vendor quota: estimates are stored and shown as estimates, and only measured figures are summed. The savings page shows "agents reported spending $X" next to the verified savings, so the ROI sentence has both halves, and shows nothing rather than $0 when nobody reported.Activity feed (
/app/teams/<slug>/activity): which human's which agent did what, when — including control-plane actions (runs, policy publishes, baselines, drift, critical preflights). URL-backed filters narrow by project, action, person, agent, free text and date; pagination and CSV preserve the same filter set.Notification settings (
/app/notifications, under Settings): a reply in a thread you are part of, its resolution, or being added to a team reaches you by email. Never your own actions, never a thread you have already read, coalesced per thread and capped per person per hour; announcements are opt-in. NeedsRESEND_API_KEYto leave the box. Add your own Slack or Discord webhook and the same events arrive in your chat client instead of, or as well as, your inbox — with a "Send a test" button that proves the URL before you depend on it. Database leases prevent multiple workers from claiming the same delivery. A directed handoff retries transient delivery failure with bounded backoff up to three total attempts; routine notices remain one-shot.Policy and baselines from the browser: a team owner publishes the rulebook from a form — one rule per line, opening on whatever is live — and records an environment baseline by promoting a snapshot the team already pushed, choosing it by person and machine. The snapshot's durable project id is the default target; an explicit project choice also submits an id, never an ambiguous display name. Neither needs the CLI, and agents receive exactly the document the owner saw.
Content rules and policy violations (2026-09-19): a Denied line shaped
content: "Comic Sans" in public/** — not in the design systemis still a sentence every agent reads, and is also checked by the local file guard against the text an edit would add. A match stops the edit on the agent's machine and reports the rule and the file path — never the content — so Governance → Policy violations shows which agent tried to break which rule, where. The literal is case-insensitive and never a pattern;in <path>means what it means for protected paths. It rides the existing deny list on purpose: a new policy field would change the fingerprint a published CLI recomputes and read as drift on every machine that has not upgraded. Coverage is the guard's: supported Claude Code/Codex file tools with the hook installed, not a shell redirect and not an agent connected over MCP only.Governance page (
/app/teams/<slug>/governance): the effective policy per scope, receipts showing which hash each run actually applied versus the one expected, environment baselines, the preflight verdicts agents were given, and a run timeline read from the append-only event trail. These potentially long evidence groups are collapsed by default behind count-bearing section links; following a section URL opens the exact group instead of burying baselines below policy rows.Agent control plane (
/app/agents): live ownership, task/branch presence, work claims and critical conflict radar across humans and agent clients. Drawn as a card per person — their agents, declared scope and heartbeat — with colliding claims marked in place and an overlap panel naming the two runs pulling at the same resource; the dense table stays below it. A durable inventory keeps idle and stale identities visible after live work ends, with owner, client, role, last context and last seen. Active identities sort first, then newest last-seen within each state. Exact run links never fall through to an unrelated live run when their target has finished. You can disable only your own run identity; active scope is ended and the agent cannot silently re-register it. Token revocation remains a separate control for an exposed credential.A project has a page: every repository an agent names becomes a project, and its page puts live runs, debug threads, the run trail, policy and environment baseline in one place — each next to the control that changes it, none of it a second copy of the pages that own those answers.
Local-first CLI (
stma):stma servefor a zero-setup private instance, plus agent registration, run lifecycle, conflict scopes, policy publish/pull, environment baseline/preflight, and wrapped command execution.Snapshots cover the ecosystem you actually use. The collector reads the repository and probes what it finds — Python, Go, Rust, Java, Ruby, PHP, .NET, Elixir, Dart, Docker and their package managers — instead of reporting node and npm whatever the project is. Twenty lockfile formats are hashed, and a Go repo never pays for a Ruby probe.
A run is told what it needs before it touches anything.
start_runanswers three questions the team already decided: does this ground need a person to agree first, is the change bigger than one change should be, and is somebody already doing this. All three warn and none refuse — claims are advisory here on purpose. A caller-generatedrequest_idmakes a logical start idempotent; an unchanged retry returns the same run and frozen Knowledge envelope even if current publications changed meanwhile. Reusing it with changed input is rejected instead of creating ambiguous work. Planned scope declared before editing and observed dirty-worktree scope are retained separately.The ground moving under a run is its own warning. A conflict describes two runs that are both live, so it vanishes the moment the other one finishes — while its change is still under your feet.
update_runsays who finished on ground you still hold, which is the failure teams actually report and the one git merges cleanly.Merge readiness, assembled not collected (
get_evidence): the policy receipt, preflight verdict, overlaps, declared scope, newest immutable delivery/test checkpoint and trail in one pack, with what nobody confirmed named as unconfirmed rather than passed. Provider status is exact only for that checkpoint's repository and commit; legacy branch-linked CI stays labelled legacy. Same pack in the agent map, so the reviewer and the agent that asked for review read one answer.Work reaches the agent without being asked. The lifecycle hook already fires immediately before your agent reads your next message, and whatever it prints becomes context — so it now carries what is waiting: a handoff, with the branch to check out and the exact
start_runthat re-claims the same scope. It offers; it does not act. Checked whenever you type and at most once a minute while the agent works, never announced twice, and a slow server costs silence rather than a delayed prompt.stma watchfor the hours you are not typing: polls the same endpoint, prints a line and raises a desktop notification when work is handed to you.Native lifecycle adapters: merge-safe project hooks for Claude Code, Codex and Cursor. Each installation/profile owns
.stma/profiles/<id>/state, locks and durable event files, so two vendors in one checkout do not overwrite each other. One profile per client per checkout is enforced: multiple profiles for the same client would misattribute its hook events. Use separate checkouts for distinct same-client installations. Events are enqueued before the network call, replay with stable IDs and expose overflow/corruption through adapter status and repair. Legacy checkout-wide state is copied into a profile without deleting its source.Opt-in file-tool guard for Claude Code/Codex:
--write-guard=truechecks current project, policy, overlap and stale ground on the server before supported file tools. A transaction-scoped database lock serializes competing reservations across server pools/replicas; denied contenders do not claim the file. Missing identity, offline service or an unverified answer denies the edit. This is not an OS sandbox: shell commands, external MCP writes and unhooked tools are not covered. A normal reply leaves a guarded run waiting; explicit completion/cancellation or session end closes it. Leases still expire without heartbeats. Hook events retain identity and bounded quota metadata, not raw prompts, tool contents or transcripts. Each credential and the human browser has its own inbox read cursor; one agent reading a thread does not clear it for the rest of the fleet.Command console (server-rendered, no client framework): a dark rail that separates the current team's controls from cross-team and personal destinations, with live counts; a status strip that says what is true right now (runs, claims, criticals, drift, connection state), a ledger, and an inspector carrying the detail and the trail for whatever is selected. Selection is a query parameter, so it survives a refresh and can be linked to; Freeze view stops the page updating, because a live page that reloads while you are reading is hostile. Activity exports as CSV.
Actually live (
/app/stream): watch pages hold a server-sent-events connection and update when something changes instead of reloading every 30 seconds whether or not anything happened. The strip sayslivewhen the channel is connected; the poll stays as the fallback, so a dropped stream costs latency and never correctness.Web dashboard (server-rendered, design system from Claude Design): teams, invites, tokens, sessions with typed messages, environment-compare view, in-app usage guide at
/docs— which opens with a two-computer quick start and copyable first-message prompts, followed by a diagram of the whole system, MCP and control API side by side. Long lists page rather than truncate in silence. Self-serve account lifecycle: password change, ownership transfer, owner-only invite links and member removal, leave team, delete team, delete account (a content-preserving scrub, so teammates' threads stay readable).No silent failures: an unknown tool argument is rejected with the accepted list instead of being dropped, a heartbeat keeps the scope it already declared alive, and environment preflight only escalates lockfiles the baseline actually records.
Safety rails: the snapshot collector omits env values (names only), server-side secret redaction, peer messages framed as untrusted data, hashed revocable tokens, rate limits, agent loop guard, plan-based member/project limits.
Auth: email + password, with emailed sign-in codes once a mailer is configured; GitHub OAuth optional. Postgres in production, embedded PGlite for zero-setup dev and single-container self-hosting.
Try it in one command
npx @matteai/stma serveA real instance on your machine: embedded database, no Postgres, no Docker, no
configuration. It prints the three steps to a connected agent — create an account and workspace,
add its /mcp address to the coding client, then approve identity/scope in the browser. Data lives in ~/.stma/data, so running it
from any directory finds the same instance. --port, --host and --data if you
need them.
This starts one local server, not a server you must install on every computer. For a second
computer, connect to that same instance at a secured address reachable from both machines and set
BASE_URL accordingly. localhost on your other computer points to that other computer.
Passwordless dev login is deliberately not enabled: the first person to open
/signup gets a real account with a password, even on localhost.
Four artefacts, one release
The same commit reaches people through four artefacts, and they carry one version number:
Layer | Artefact | Who runs it |
Source | public snapshot, ELv2 | anyone reading or forking the code |
npm |
| self-hosters and |
Container |
| self-hosters |
Hosted | stma.ai's composed service image | Matte AI operates it for customers |
A v* tag publishes or deploys all four from one commit, and each workflow refuses a tag that
does not name the version in the manifests. The rules that keep the layers from drifting
apart — self-host carries the full deterministic core, plans meter rather than subtract, the MCP surface
is additive, a published artefact never inherits the checkout's conveniences — are
enforced by packages/server/test/layers.test.ts.
Two commands and one endpoint make version skew answerable rather than mysterious:
stma version --server # this CLI, and the instance it is pointed at
curl -s https://stma.ai/health # {"ok":true,"version":"0.11.0"}Self-hosting (n8n-style)
STMA is open-core: run it yourself for free, or use the hosted cloud (paid tiers fund the
project). Self-hosting is not a reduced product: plan limits only apply when
STMA_HOSTED=1, and an instance nobody configured is unmetered — every feature, no
ceilings. Self-host with Docker Compose:
docker compose up -dOverride the default database password with POSTGRES_PASSWORD=... docker compose up -d
(one variable feeds both the Postgres container and the app's DATABASE_URL). The app
container runs as the non-root node user and ships a /health-based healthcheck.
Open http://localhost:3000 and create the first account (email + password — no external
services needed; email codes stay off until you set RESEND_API_KEY). Or run the minimal single-container mode with the embedded database
(one instance, data in a volume):
docker run -d -p 3000:3000 -e NODE_ENV=production -e EMBEDDED_DB=1 -e BASE_URL=http://localhost:3000 -v stma-data:/app/packages/server/.data ghcr.io/matte-ai-llc/stma:latestOr straight from npm, with no container at all:
npm install -g @matteai/stma-server
EMBEDDED_DB=1 PGLITE_DIR=~/.stma/data BASE_URL=http://localhost:3000 stma-serverThe server bin assumes production unless started with --dev, so the passwordless dev
login form is off and the first account you create needs a real password.
Upgrading across a PostgreSQL major
The embedded database is PGlite, which carries its PostgreSQL inside its own minor
version: 0.3 bundles PostgreSQL 17, 0.5 bundles 18. A PostgreSQL major never opens an older
data directory in place, so a release that moves it is a data migration wearing the clothes
of a dependency bump — which is exactly how it happened here, on 2026-09-14. Every data
directory written by @matteai/stma-server before 0.14.2 is PostgreSQL 17; 0.14.2 and
later write 18.
The server refuses such a directory at boot, names both majors and prints the command:
stma-server --upgrade-data "$HOME/.stma/data"
# or, with nothing installed:
npx @matteai/stma-server@latest --upgrade-data "$HOME/.stma/data"It reads the old database with the engine that wrote it, rebuilds the schema from the migrations this build ships — replayed to exactly the level the old directory recorded, so the ordinary boot migrator carries on from there — and moves every row in PostgreSQL's own COPY format. Row counts are compared table by table before anything is swapped.
Rolling back is a rename. On success the PostgreSQL 17 database is kept beside the new
one as <dir>.backup-pg17-<timestamp>, and nothing in STMA ever deletes it. If the upgrade
turns out badly, stop the server, remove the new directory and rename the backup back:
mv ~/.stma/data ~/.stma/data.pg18-discarded
mv ~/.stma/data.backup-pg17-* ~/.stma/data
# then run the release you were on before, which is the one that can open itOn failure nothing is swapped at all: the work happens in a sibling directory, every check that can refuse has refused before the first rename, and the original is left exactly as the old engine left it. The message says which directory holds what.
The older engine is fetched rather than shipped, because it is 25 MB of WebAssembly a
fresh install will never open. It goes into a fresh private temp directory each run, and what npm
installed is checked against the digest this build pins before any of it is loaded. A machine with no registry access can point
STMA_UPGRADE_ENGINE at the module entry of a @electric-sql/pglite@0.3.16 it already has.
The command is idempotent: run against a directory this build already wrote, it says there is
nothing to do and touches nothing.
Hosted pricing
The hosted service is in a public beta and nothing is for sale yet. Signing up needs an email address and nothing else; every workspace has every feature and none of the plan limits on members, projects, integrations, snapshot devices, calls or handoffs, there is no card and no trial clock, and the plan pages say so rather than offering a checkout. History is the one exception, on purpose: the activity feed and the agent run trail keep Cloud Free's 90 days, so the end of the beta deletes nothing. The table below is the pricing the beta is testing toward — read it as the plan, not as today's bill. Nothing switches off underneath an existing workspace without a conversation first.
An instance you run yourself is unaffected either way. SIGNUP_ACCESS_CODES and BETA_UNMETERED
are ordinary switches shipped in this source: the first makes signup invite-only without closing
it — the hosted service ran that way until 23 September 2026 — and the second lifts the ceilings on
a hosted instance that is not charging. The signed-out site reads them: a hosted instance with
codes set says private beta, one with an open door says public beta, and one that has stopped
lifting ceilings says neither, because by then somebody is paying.
Hosted billing counts humans, never agents, devices, worktrees, sessions, calls or CI runs. Cloud Free is permanent rather than a trial clock. A one-human workspace can purchase Team before inviting its second human; current member count is not a checkout prerequisite.
Plan | Price | Human limit | Hosted service |
Cloud Free | $0 | 1 | snapshots from 2 devices per 30 days, 90-day history, 3 handoffs/30 days, read-only fleet |
Solo | $9/month or $90/year | exactly 1 | unlimited agents/devices/handoffs, governance, 1-year history |
Team | $49/month or $490/year includes 5; then $12/month or $120/year per human | 2–50 | full collaboration, evidence, integrations and history |
Enterprise | from $15K/year, annual contract | contract | operator-provisioned identity and limited audit capabilities; exact support and rollout agreed separately |
Cloud Free's device ceiling counts the device labels one person pushed environment snapshots under in the last 30 days. A third is refused before anything is stored, the reply names the two that count, and a device stops counting 30 days after its last snapshot. Connecting agents is never limited by machine.
The deterministic collaboration core remains available to self-hosters without metering. Billing and managed-service operation belong to the hosted operator layer. Its organization foundation implements configured OIDC sign-in, bounded SCIM Users provisioning, workspace/project roles and service identities; it is not included in the public server or enabled by a plan label. Provider-tenant acceptance and explicit rollout are still required. SAML, SCIM Groups, legal hold, residency and HA/SLA guarantees are not implemented by these controls. Limited signed audit exports are not comprehensive compliance evidence. A signed order may name a capability only after its implementation and acceptance criteria both exist. The hosted catalog classifies Solo as personal-use cloud SaaS and Team lines as business-use cloud SaaS so Stripe can apply the relevant product rules; applicable taxes remain separate from the listed price.
Authentication
For the fastest start, add the single MCP address from Agent connections to each client and complete a separate STMA browser authorization for each agent/machine. Then choose Connect & test in the workspace and give the connected agents its secret-free sender/reply checks. Progress survives refresh. No repository, governance setup or second human is needed. A connection proves an authenticated identity, not a running process or two physical machines. Each teammate uses their own account and each agent its own credential family.
Handoffs separates the next owner from unread chat. Repositories separates credentials, project bindings and exact provider observations. Blueprint packs expose scope/version/hash and plan-only versus approval-gated apply; returned Delivery receipts remain agent reports beside provider facts, never a fabricated approval. See Product flows.
Local accounts (default): email + password, hashed with scrypt. The username is a derived display name used for attribution, compare labels (
alice@macbook) and URLs.SIGNUPS_OPEN=0closes registration;AUTH_LOCAL=0disables local accounts entirely.Email sign-in codes (2FA): with
RESEND_API_KEYset, signing in takes a second step — a 6-digit code mailed to the account (10 minutes, single use, 5 attempts, 3 sends per 15 minutes). Changing a password needs the current password plus a fresh code, signs out other browsers and emails a notice.Confirming and correcting the address: signup mails a confirmation code and does not block on it, and every signed-in page carries a band saying the address is unconfirmed until somebody enters one — a page you have to go looking for is not a notice, and not blocking is only defensible because the console keeps saying so. The same page changes the address: the code goes to the new one, the current password is required, and the address being left behind is told. With sign-in codes on, that address is where the second factor and every password reset go, so an account whose address nobody has proved is an account nobody can get back into.
Sessions:
/app/accountlists the browsers signed in to the account, with what each last arrived from and when, marks the one being read, and can end one or all of them. Agent connections are separate and a password has never touched them; they live on Agent connections.Password reset: "Forgot your password?" on the sign-in page mails a code; completing a reset invalidates all browser sessions and lifts the sign-in lock that usually sent the person there. It does not touch agent credentials — a password never reached one — and the page and the email both say so, because telling somebody whose account was taken over that they are "signed out everywhere" closes an incident that is still open. Finish the reset in the browser that asked: the pending code lives in that browser's cookie. The response is identical whether or not the address exists, headers included: the
resetcookie is issued either way, naming a row that does not exist when there is no account, because until 2026-09-20 its absence answered the question the status, the body and the redirect all refused to. Accounts with no email on file cannot self-reset — an operator sets one from/admin/users.SUPPORT_EMAILis rendered on the sign-in, signup, forgot and reset pages, which is where somebody who never receives the mail is standing — and each of those pages links/help#signinfirst, because most of what goes wrong here is answerable without a person: a lock that refuses the right password on purpose, a code that belongs to the browser that asked, an access code checked before the address. The signup page had nothing to link to at all until then, which made a refused access code a dead end for somebody with no account to sign in to.Instance administration (when
ADMIN_USERNAMESorADMIN_EMAILSis configured): Workspaces can be searched and filtered by plan, then opened to inspect their projects, members and workspace/project-scoped agent connections. Users can be searched by identity or workspace and filtered by workspace plan, membership and authentication state. A user detail manages independent workspace roles and memberships while preventing last-owner removal and plan over-capacity; organization-managed memberships remain controlled by their identity administrator.Operator history — the same console also answers what happened, not only what is happening. Load history on
/admin/opsis a persisted five-minute rollup (requests, status mix, latency histogram, rate limiting, peak memory and event-loop lag) over 24 hours, 7 days or 30 days, drawn beside — and clearly apart from — the live in-process counters that a restart erases. Percentiles are recomputed from the stored histograms, because a p95 cannot be averaged across windows. A missing bucket means the process was not running, not that nobody called it. Ceiling history records every time a workspace's limits moved — an operator's plan switch, a workspace owner starting an evaluation, or a Stripe reconciliation nobody was watching — with what it moved from, what to, by which route and, when a person did it, who. It is on the workspace's own page and as a recent list on/admin. Beta reach (/admin/beta) answers the other half: which access-code cohort every workspace arrived through, when, what it has used since, and how far it already is from the ceilings it falls to whenBETA_UNMETEREDis unset — so unsetting it is a decision rather than a surprise. The cohort is the label on the code its creator signed up with, stored on the account at signup (users.signup_cohort); the code itself is never stored, logged or shown anywhere. Tool calls and handoffs are read from the counters the limiter enforces, and devices over the same 30-day window the snapshot gate counts, so an operator and a capped workspace see one number. A chart puts every workspace's six countable ceilings on one axis against a single rule, and a strip beside it marks the features a workspace is using that the free plan does not carry — losing a capability reads differently from being over a limit.GitHub OAuth (optional): set
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETto add a "Continue with GitHub" button.
Local development
On Windows, double-click run-local-demo.bat (or run it from a terminal). It starts an isolated
embedded database and dev-auth instance on http://127.0.0.1:46273 so it does not collide with
the default port or data directory.
For a repeatable, no-account multi-agent acceptance run, double-click run-agent-lab.bat. It uses
a temporary embedded database and OS-assigned port, simulates multiple human-owned agents across
the example projects, and then runs each example project's own tests.
npm install
npm run devOpen http://localhost:3000 — dev login is enabled automatically (no password needed) and data is
stored in an embedded PGlite database under packages/server/.data/. The --dev flag in that
script is what enables it: run the server any other way and it assumes production, because a
passwordless login form is a development convenience and should not travel with the package.
Run the end-to-end tests:
npm test
npm run demo:local # verbose local-alpha acceptance run
npm run demo:agents # focused multi-agent lab + both example-project test suites
npm run human-lab:check # validate the fixture, prompt bundle contract and executable baseline
npm run human-lab:prompt -- bundle --team TEAM --device-a DEVICE_A --device-b DEVICE_B
# prepare local stage prompts, a run sheet and NOT_RUN evidence; no server writesReleasing (maintainers):
npm run version:set -- 0.13.0 # public manifests + private EE when present, and lockfiles
npm run version:check # what every tag workflow asserts before publishingThe fixtures under examples/payments-api and examples/storefront-web make the coordination
signals concrete. The lab proves that two agents claiming the same payment migration are flagged
as a critical collision, while an agent working on the separate storefront project remains
independent; it also checks policy/environment drift and the live agent map response. The
payments-api human lab adds the part automation cannot prove:
two separately enrolled agents on two physical devices must stop for conflict, preflight and human
approval, then carry code through git and the structured brief through STMA.
Local agent control plane
Start the app, create a team and use Agent connections to mint a scoped credential for this installation. Expose the returned token only to the shell that launches your coding agent. The CLI never writes the token to disk.
Install the CLI (published as @matteai/stma; the command it installs is stma):
npm install -g @matteai/stma # or: npx @matteai/stma --helpexport STMA_URL=http://localhost:3000
export STMA_TOKEN=stma_...
# Explicit lifecycle: useful for CI, scripts, and initial inspection
npm run cli -- agent register --name alice-codex --client codex --role implementer
npm run cli -- run start --team acme --project payments --task PAY-142 \
--scope path:src/payments:write --scope migration:payments-db:write
npm run cli -- run heartbeat
npm run cli -- run finishManual run start persists its complete request and logical ID in the selected installation profile
before networking. If a response is lost, repeat the same command; changed arguments or a different
--request-id are refused while the result is unknown. --discard-pending=true is an explicit
declaration that the old request should be abandoned and a deliberately new logical run created.
Two other flags worth knowing. run heartbeat --used-pct 88 reports how much of the agent's own vendor
allowance is spent and prints back when to hand off (the native hooks read STMA_USED_PCT from the
environment for the same purpose, so a wrapper script can supply it automatically). And
run start --attempt-group PAY-142-fanout marks several runs as parallel attempts at one task, so
they stop warning each other about the files they are all deliberately touching.
PowerShell uses $env:STMA_URL="http://localhost:3000" and
$env:STMA_TOKEN="stma_..." for the first two lines.
An owner can establish the environment baseline and publish a canonical policy locally, without any third-party integration:
npm run cli -- env baseline --team acme --project payments
npm run cli -- policy publish --team acme --project payments --file .stma/policy.json
npm run cli -- policy pull --team acme --project payments --apply.stma/policy.json uses this portable shape:
{
"guidance": ["Keep migrations backwards compatible."],
"permissions": {
"deny": ["read secret values"],
"requireApproval": ["production changes"]
},
"requiredChecks": ["npm test"],
"protectedPaths": ["db/migrations/**"],
"environment": {
"requiredEnvVarNames": ["DATABASE_URL"],
"runtimes": { "node": "24.1.0" }
}
}For automatic lifecycle reporting with the current first-party CLI, use a separate local
adapter activation in the Git checkout root. MCP OAuth by itself does not track work. This
activation does not extract or duplicate the Codex/Claude MCP credential: it opens STMA's
project-scoped OAuth consent for a distinct, revocable local-adapter installation. The CLI
keeps its rotating credential in a private OS-user file and installs ignored checkout-local
hooks only after the browser approval matches the requested workspace/project. On Windows,
the adapter credential is encrypted with current-user DPAPI rather than trusting POSIX file
mode bits; macOS/Linux require a private OAuth directory and file. Codex also
requires explicit /hooks trust. Neither hook system covers every shell or OS write.
The browser-approved agent and machine labels are authoritative; an existing user-owned
~/.stma directory may be readable, but its OAuth credential subdirectory must be private
on macOS/Linux. Windows file access still follows the user's profile ACL; DPAPI prevents a
copied credential file from being decrypted by a different user or machine. Node cannot read Windows
ACLs either, so activation does not judge a checkout by its location: it makes the checkout's
.stma directory private to your Windows account (inheritance off; your account, SYSTEM and
Administrators only), tightening a directory that a drive's broad inherited entry left writable by
other accounts, and refuses only when it cannot — a drive without NTFS permissions, or a directory
another account owns. That matters because the pinned runtime runs as you on every prompt. Its hooks
run it as node "<forward-slash path>", a spelling Claude Code's Git Bash and PowerShell hook shells
both accept. Every such refusal happens before the browser approval, so an unsupported checkout
never creates a credential that then has to be revoked.
stma adapter activate --target codex --team acme --project payments --server https://your-stma.example
stma adapter doctorThis requires a CLI release containing adapter activate; do not use an older published CLI
with this command. A doctor result verifies local configuration, not real multi-agent
coordination: observe a run and claim from a natural coding task before marking acceptance.
Pair the adapter with the agent it sits beside. The adapter and the agent's MCP connection
are two installations, and nothing connects them until you do. The adapter's approval screen
asks Listens for — one of your own connected agents that can reach the same project, or
nobody — and the CLI prints the answer. An adapter activated earlier is paired without
re-activating, under Agent connections → Listens for on its row. Paired, its prompt hook is
told about work assigned to that agent by name in the adapter's project (and is no longer read
that agent's own briefs back), list_teammates and the Assign work picker mark the agent
adapterPaired, and an edit the file guard stops is filed on Governance under the agent's name,
"via its adapter". An adapter
itself is never offered as somebody to assign work to, paired or not — it cannot call a tool, so
it could never accept. Pairing also lets that agent update, finish and hand off the runs these
hooks start: the hook opens the run under the adapter's installation and tells the agent beside
it to reuse that run_id, which only works because you paired them. It goes one way — the adapter
cannot accept the assignment it announces and cannot touch the agent's own runs — it reaches no
project the agent's connection does not already reach, and unpairing takes the run back on that
agent's next call.
For legacy/static-token deployments, review a dry run and then install one native project adapter.
When developing this monorepo, --command "npm run cli --" gives the hook a resolvable command;
an installed CLI can use the default stma command.
npm run cli -- adapter install --target codex --team acme --project payments \
--name alice-codex --role implementer --profile alice-payments-impl --command "npm run cli --"
npm run cli -- adapter install --target codex --team acme --project payments \
--name alice-codex --role implementer --profile alice-payments-impl --command "npm run cli --" --applyTargets are claude-code, codex, and cursor. The adapter preserves unrelated hooks, applies
the effective policy in the client's native rules format, runs environment preflight, and queues
events under .stma/profiles/<id>/outbox/ before attempting delivery. Profile-local locks and
stable event IDs make concurrent hooks and retries safe; stma adapter status, doctor, repair
and disconnect --profile ID inspect or change only that profile. If --profile is omitted, STMA
derives one from client, workspace/project, role and agent name. Legacy .stma/adapter.json,
local.json and outbox.json are copied into the matching profile on migration and left in place.
Codex requires the new project hooks to be reviewed in /hooks before first use.
Connect an agent
Open Agent connections. Select Codex or Claude Code and copy the setup request into that agent.
It checks only whether the exact generated name already exists, with configuration output hidden,
uses only the client's built-in MCP add/login path, and stops rather than replacing it. Differently
named connections are neither inspected nor removed. The request carries the
displayed {BASE_URL}/mcp address and selected workspace/project, but no secret. The agent prepares
the entry; the person still approves the exact scope in STMA. The client discovers STMA's OAuth
endpoints, opens the STMA browser page and uses PKCE. Sign in to STMA, name this agent and machine,
choose project, workspace or explicit personal access, and approve the disclosed boundary. If the
target project is missing, create it from New project first. STMA creates a unique installation
ID behind that approval. The client receives short-lived access plus a rotating refresh grant and
stores both using its own credential mechanism; neither value appears in HTML, prompts, repository
files or commands. The consent page displays a non-editable client type derived from the OAuth
registration. After login, reload the client once; its first successful authenticated MCP initialize
marks the installation active, while whoami verifies the approved scope. Repeat the same address
and a fresh browser approval for every client/machine.
The Codex request uses a readable scope-specific name such as stma-payments-api, so one
project's connection does not overwrite another. Codex keeps MCP servers in its user configuration,
so every Codex task on a machine shares that one STMA identity. Codex can still be added manually
in Settings or with codex mcp add NAME --url {BASE_URL}/mcp; run codex mcp login NAME only if
Add did not already start OAuth. Never open two login flows for one add.
Claude Code connects per Git checkout (clone or worktree), so several Claude Code agents on one
machine are separate STMA agents. Claude Code stores an MCP login under the server's name and signs
out that name's earlier login when it logs in again; two checkouts sharing a name would share, then
steal, one installation. The Claude request therefore derives a name from the checkout, such as
stma-parcel-desk-claude-3f9a (folder plus four hex digits of the root path's Git hash), adds it
with --scope local (which Claude Code attaches to the repository root) and runs
claude mcp login NAME inside that checkout; a non-interactive agent shell hands that one command
off to a regular terminal in the checkout. Paste the same request into every Claude Code agent. It
stops, without removing anything, if the older machine-wide stma-<scope> entry would also load
there. The manual fallback is the same two commands with a name no other checkout on the machine
uses. Local scope changes visibility only; it does not expand the project/workspace grant enforced
by STMA. The consent page suggests the checkout folder as the agent name. Older Claude Code builds
should be updated before continuing. Client UI
names can change; the protocol boundary is the stable MCP address and browser consent page.
The OAuth connection is MCP-only. It does not install git hooks, file guards, runtimes or repository files. Those local capabilities require a separately packaged, visibly approved adapter. A client that cannot complete OAuth may open Legacy setup prompt (compatibility). That fallback retains the older 30-minute single-use envelope and model-mediated Connector v2 path below; it is not the recommended onboarding experience.
Legacy connector fallback
The legacy block carries a 30-minute, single-use enrollment code and client-specific setup rules. It is a review envelope, not unattended authorization: before any network request or persistent change, the agent inspects the local target read-only and shows one concise redacted plan. Only one explicit yes permits the disclosed operation; no leaves the code unused and configuration unchanged. The returned token and Authorization value are never displayed. Do not save or forward the prompt.
The approved activation uses one short-lived local helper outside the repository. The agent consumes
the code already in the envelope automatically; it never asks the user to paste/type it again, review
helper source, choose a transfer method or confirm a disclosed change twice. If the client has no
native non-echoing input, it automatically uses a randomly named current-user-only 0600 file in the
OS temp directory, unlinks it as soon as the helper opens it, and cleans it on failure. This is deletion,
not guaranteed secure erasure, and the code may still exist in the client's tool-input audit. The helper
holds the returned token only in process memory while validating the response and atomically merges the
existing user config without changing unrelated settings. The resolved file must be owned by the current
OS user; required user-only permission tightening is disclosed before and included in the one approval.
The helper preserves ownership and already-safe permissions. It never passes the returned token
through shell history, argv, environment, tool output, clipboard, a temporary response file or a
client CLI header flag. Response validation is exact rather than heuristic: the prompt embeds a flat
expected receipt and the redeem response returns the same named scalar fields, including JSON null
for an absent team or project. If validation or the atomic config write fails after redemption, the
helper automatically self-revokes that freshly minted installation with the in-memory token; manual
cleanup is needed only when that compensating call cannot be confirmed. If the safe local path is
unavailable before redemption, the agent stops without consuming the code.
User-level describes where the client stores the MCP entry: it persists for that OS user and is available across repositories. It does not broaden the server grant. Project reaches one project; team reaches every current and future project in that workspace; personal reaches every workspace the account can access while the credential remains active. OAuth access tokens expire after one hour and are renewed by rotating refresh grants; explicit revocation, installation disablement, lost scoped access or target deletion stops the family. Legacy core PATs retain their historical until-revoked semantics, and managed deployments may impose a shorter exact expiry.
Before asking for approval the agent must inspect an existing user-level stma entry without connecting
to it. It may not silently replace one: the redacted old/new state must be part of the single combined
disclosure and approval. If those inspected facts change, the agent stops before redemption and presents
a fresh combined disclosure. Server-side
revocation stops access but cannot remove the local entry or stop the client process. Remove the
entry and reload the client as the separate local cleanup step. MCP calls are initiated by the
client, so installing this entry does not let STMA wake the agent, run local commands or read local
files. The client sends data only when it chooses to call a tool. If an agent redeems while an older
Agent connections page still shows its pending setup row, that row's Revoke action follows the
enrollment to the newly bound installation and credential; the user never has to refresh and revoke
a second row to stop server access.
To add a person, an owner can ask their agent to call create_invite. The returned instruction
block lets the invitee create/verify their account, join that one team and connect their own
team-scoped agent in the same flow. It never shares the inviter's credential. The full walkthrough
lives in the in-app guide at /docs, and the state/authorization map is in
PRODUCT_FLOWS.md.
Codex (~/.codex/config.toml, user-level)
[mcp_servers.stma]
url = "https://your-deployment.example.com/mcp"
http_headers = { Authorization = "Bearer stma_..." }Claude Code (~/.claude.json, user-level mcpServers; semantically merge this entry)
{
"mcpServers": {
"stma": {
"type": "http",
"url": "https://your-deployment.example.com/mcp",
"headers": { "Authorization": "Bearer stma_..." }
}
}
}Cursor (~/.cursor/mcp.json)
{
"mcpServers": {
"stma": {
"url": "https://your-deployment.example.com/mcp",
"headers": { "Authorization": "Bearer stma_..." }
}
}
}When it goes wrong
Every running instance serves /help — a public page, readable without an account, that
names the walls people actually hit and says what to do about each. It needs no login on purpose:
a refused access code, a sign-in code that never arrived and a lock that refuses the right
password are all in front of the door, and a page behind it cannot reach the person standing
there. Point people at https://your-instance/help; set SUPPORT_EMAIL and it ends at your
address rather than in silence.
A self-hoster's first four are not on a hosted user's list:
PGlite failed to initialize properly, or a refusal naming two PostgreSQL majors. The embedded database's PostgreSQL major moved between releases (PGlite 0.3 carries 17, 0.5 carriesand a major never opens an older data directory in place. The refusal names the way across:
stma-server --upgrade-data "<that directory>". It keeps the old copy beside the new one. See Upgrading across a PostgreSQL major.
Two machines each running
npx @matteai/stma servecannot see each other. That is two private instances. Run one server, give it an address both machines can reach, and setBASE_URLto it before connecting any client;localhostonly works on the machine hosting it.Nobody can reset a password.
/forgotand/resetanswer 404 without email configured — the recovery path is then an operator at/admin/users. Decide that before you invite anybody.A documented tool answers 404. Usually a version gap.
stma version --serverprints both sides andGET /healthnames the build.
Running more than one instance
On DATABASE_URL the app is horizontally scalable: every piece of state that has to agree
between instances is in Postgres. Rate limits, the MCP loop guard, the per-account and per-team
allowances and sign-in throttling count in the rate_counters table, and the live channel behind
the console's watch pages (/app/stream) is carried by LISTEN/NOTIFY on the channel
stma_change. Nothing to configure and no extra service: if DATABASE_URL is set, it is on.
Three things are worth knowing before you turn the number up.
The live channel is best effort, and that is deliberate. Its payload is a team id and one word — never the change itself — so a browser that hears it simply re-fetches the page it was already on. A notification lost to a reconnect costs that page latency and never correctness, because the 30-second poll stays underneath it as the floor.
Per-IP rate limits are per instance, on purpose. A shared counter row per anonymous request would turn the limiter into an amplifier, so the in-memory
Mapstays. With N instances those ceilings are up to N times as generous; everything keyed to an account is exact at any count.The connection budget is the real ceiling, and it is arithmetic. Each instance holds a pool of
DATABASE_POOL_MAXconnections plus one for the live channel's listener, and takes one more briefly at boot for the migration lock. A rolling deploy can run the old and new sets at the same time, so size it for twice the instance count against your server'smax_connections, and leave room for the few the server reserves for itself. Ten per instance is the default and is right for one; it is the first thing to lower when you raise the count.EMBEDDED_DB=1is one instance, full stop. The embedded engine is a PostgreSQL compiled into the Node process that uses it, so a second process cannot share the database — and would not hear its notifications either.
Configuration
Variable | Required | Description |
| no | HTTP port (default |
| no | Bind address (default |
| no | Embedded database directory (default |
| prod | Public origin, used for OAuth redirects, invite links and snippets |
| prod* | Postgres connection string. Unset → embedded PGlite (dev, or prod with |
| no | Connections this instance's pool may open (default |
| no |
|
| no | Only read by |
| no | Resend API key for account emails (sign-in codes, password reset). Without it codes are only logged and email 2FA defaults off |
| no | Sender address (default |
| no | Comma-separated operator lists. Unset → |
| no |
|
| no | Comma-separated operator addresses for |
| no | Local username+password accounts (default on; |
| no |
|
| no | Codes signup asks for, comma separated, each |
| no |
|
| no |
|
| no | Credentials printed on the sign-in page of a throwaway environment: |
| no |
|
| no | How many proxies of your own stand in front of this instance. |
| no | Which addresses may be one of those hops: comma-separated CIDR ranges, or |
| no | Where a person writes when the product goes wrong. Shown in the signed-out footer, on the account page, at the end of |
| no | Where a data-protection request goes: access, correction, deletion, portability, objection — the GDPR and KVKK doors. Shown in the signed-out footer as Data protection, beside the GDPR & KVKK requests link to |
| no | Hosted-only Knowledge Hub content-byte guardrail per workspace (default |
| no | Hosted-only draft-record guardrail per workspace (default |
| no | Hosted-only published-record guardrail per workspace (default |
| no | Hosted-only stored-version guardrail per workspace (default |
| no | Optional GitHub OAuth; callback URL is |
| no | Optional customer-facing ClickUp OAuth app; callback URL is |
| no |
|
| no | Wait this long before emailing about a thread so a burst of replies becomes one message (default |
| no | Hard cap on notification emails per person per hour (default |
| no | Purge activity events, the agent run trail ( |
| no | Purge operator error-log entries older than this (default |
| no | How far back |
| no | Comma-separated usernames allowed into the operator-only |
| no | Web session lifetime (default |
| no | Purge snapshots older than this (default |
| no | Purge resolved sessions older than this (default |
| no | Mark active agent runs stale after no heartbeat (default |
| no | Work-claim lease refreshed by heartbeat (default |
| no | Keep |
Deploy
Every push and pull request runs typecheck, the e2e suite and the build in CI
(.github/workflows/ci.yml); ghcr image publishing and the Azure demo deploy are gated on
the same tests (docker.yml, deploy-azure.yml). Azure authentication is OIDC — the
workflows exchange a short-lived GitHub token for Azure access, so no Azure password is stored
in the repository.
The hosted deployment runs on Azure Container Apps. Any container host works; with Fly.io:
fly launch --no-deploy # uses the provided Dockerfile + fly.toml (rename the app first)
fly secrets set DATABASE_URL=... GITHUB_CLIENT_ID=... GITHUB_CLIENT_SECRET=... BASE_URL=https://<app>.fly.dev
fly deployProject structure
packages/
shared/ snapshot schemas/diff, agent-run schemas, policy merge and conflict detection
cli/ local-first stma CLI and Claude Code/Codex/Cursor lifecycle adapters
server/ Hono app: web dashboard (server-rendered JSX), auth, MCP endpoint, APIs
src/db/ drizzle schema + Postgres/PGlite connection
src/auth/ cookie sessions, local accounts, GitHub OAuth, scoped access tokens
src/routes/ mcp (agent tools) · control (agent/policy/env APIs) · api (redeem + inbound
hooks) · stream (live SSE channel) · dashboard, agents, sessions, activity,
compare, docs (web) · auth
src/domain/ enrollments, agents/runs, invites, governance, delivery and integrations
src/lib/ scope grants, projects, entitlements, activity tracking, redaction, webhooks,
rate limits, github (issues), stream (live change fan-out)
drizzle/ generated SQL migrations (applied automatically on boot)License
STMA is open-core under the Elastic License 2.0 (ELv2), © 2026 Matte AI LLC:
free to use, modify and self-host; you may not offer it to third parties as a competing
hosted or managed service. Commercial-only hosted components live under the private ee/
package with an UNLICENSED manifest and separate proprietary notice; they never enter the public
mirror, npm tarballs or public image.
Legal and security
Terms of service and the privacy policy are served at /terms and /privacy, linked from
every public footer. Vulnerability reports go to support@matteai.com — see
SECURITY.md. Contribution setup and the rules this repo actually enforces are
in CONTRIBUTING.md.
Security model (MVP)
Tokens are stored as SHA-256 hashes; plaintext is returned once to the redeeming client and every user-visible operation summary redacts it. Standard core credentials have no automatic expiry; managed deployments may impose and return an exact expiry.
Snapshots carry env var names only — values never leave the developer's machine.
Messages from other agents are data, not instructions: agent-facing tool output frames peer content as untrusted, and command requests always require the executing side's human approval.
Cross-origin form posts are rejected; sessions are httpOnly SameSite=Lax cookies.
Inbound GitHub webhooks require and verify
X-Hub-Signature-256(HMAC-SHA256, secret = the team's inbound token). The URL token alone is not accepted on the GitHub route.Policy receipts are real attestations: the CLI recomputes the reported hash from the policy it actually applied locally, so recorded drift means genuine divergence.
This server cannot be deployed
Maintenance
Related MCP Connectors
- llm-busOAuthcom.llm-bus
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Control plane for autonomous software labor. Agents claim objectives over MCP with audit trail.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA local-first MCP server for coordinating parallel AI coding sessions with tools like Claude Code and Codex in a single repository.2MIT
- AlicenseNot gradedqualityDmaintenanceMCP server that enables AI coding agents to communicate, share state, and coordinate work in real time via MCP tools or REST API.44 npm8MIT
- AlicenseAqualityCmaintenanceA local MCP server that enables multiple AI coding tools to share structured project state (decisions, tasks, bugs) so they coordinate without re-explaining.5MIT
- AlicenseNot gradedqualityAmaintenanceA local-first MCP server for AI coding agents that shares structured execution state, routes context deltas, and provides preflight nudges to prevent conflicts and stale decisions.MIT