Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,38 @@ updates:
- minor
- patch

# Node applications. Keep the public docs site and Studio's server-side
# proxy/runtime dependencies on the same weekly vulnerability-update rhythm.
- package-ecosystem: npm
directory: "/website"
schedule:
interval: weekly
open-pull-requests-limit: 10
commit-message:
prefix: "chore(deps)"
labels:
- dependencies
groups:
npm-minor-patch:
update-types:
- minor
- patch

- package-ecosystem: npm
directory: "/studio"
schedule:
interval: weekly
open-pull-requests-limit: 10
commit-message:
prefix: "chore(deps)"
labels:
- dependencies
groups:
npm-minor-patch:
update-types:
- minor
- patch

# SHA-pinned GitHub Actions (bumps the pin SHA + its `# vX.Y.Z` comment).
- package-ecosystem: github-actions
directory: "/"
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ superseded runs cancelled via `concurrency`):
| `lint` | `golangci-lint` (v2) + `go vet ./...` + `actionlint` (workflow lint, pinned via `go run`) + the reusable-workflow pin check + the empty-expression (action-templates) check + the mecatequi composite-action shell tests |
| `fuzz-smoke` | `task fuzz FUZZTIME=20s` — short coverage-guided pass over the security-critical parsers (not the nightly deep fuzz) |

## `studio.yml` — Studio changes on PRs and `main`

Path-scoped to `studio/**` and the workflow itself. It installs the Node version
pinned by `studio/.nvmrc`, then builds, behavior-tests, lints, type-checks, and
runs `npm audit --audit-level=high`. Keeping this in a separate workflow avoids
coupling the Node client to the Go job matrix and lets unrelated PRs skip the npm
install entirely.

Go is provisioned by `actions/setup-go` from `go.mod` with the module cache
enabled; `GOTOOLCHAIN=local` prevents a surprise toolchain download.

Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/studio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
# SPDX-License-Identifier: LicenseRef-Stacklok-Proprietary

name: Studio

on:
pull_request:
paths:
- "studio/**"
- ".github/workflows/studio.yml"
push:
branches: [main]
paths:
- "studio/**"
- ".github/workflows/studio.yml"

permissions:
contents: read

jobs:
checks:
name: Build, test, lint, typecheck, audit
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: studio/.nvmrc
cache: npm
cache-dependency-path: studio/package-lock.json

- name: Studio checks
working-directory: studio
run: |
npm ci
npm test
npm run lint
npm run typecheck
npm audit --audit-level=high
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ the opt-in `provider/*` submodules (ADR 0093), and the root module all move in l
- `cmd/mecated/` — standalone server (composition root): flags, TLS/auth/rate-limit, HTTP + metrics listeners. `cmd/mecademo/` — the offline demo. `cmd/mecatequi/` — single-shot HEADLESS composition root (peer of mecademo over `app.Build`): one prompt → a git-diff patch + a JSON Summary + an optional JSONL log + an exit code. It is **FORGE-AGNOSTIC** — knows nothing about GitHub; the glue that turns an issue into a PR lives ONLY in `.github/` + shell, NEVER the binary or `engine/`, and keeps a **split-privilege token boundary** (the agent job holds NO GitHub write token; the publish job runs NO agent code, applies the patch as DATA). See `docs/adr/0028-mecatequi.md`. `cmd/mecak8s/` — storage-free k8s-native agent (ADR 0048), a thin peer of mecated that composes `app.Build` with k8s-native defaults (Redis store + k8s lease + drain gate); no PVC, no local state — state is a managed service (Redis + k8s API server). The four real-provider mains share credential/base-URL wiring via `internal/cliconfig`.
- `cmd/mecatui/` — optional gRPC **client** TUI; by default hosts a `mecated` in-process over a UNIX socket. `ui`/`theme`/`client` import no `engine/...` or `internal/...` and no proto directly — they render from relayed proto `Event`s. See `docs/tui.md`.
- `perf/` — the OFFLINE scenario perf harness (perf-tracking Phase 2, `task perf:scenarios`, NOT part of `task test`): `perf/kpi` (stdlib-ONLY KPI capture — `ScenarioResult`/`Capture`/`/proc` RSS sampler; never imports `engine/...` or `internal/...`) + `perf/scenarios` (external-test `testing.B` whole-loop benchmarks over `engine/...` + `engine/adapter/*`, never `internal/...`). The TUI scrollback render bench lives in `cmd/mecatui/ui/scrollback_bench_test.go` (perf/kpi imported in the `_test` file only). `perf/cmd/perfconvert` (Phase 3; stdlib + `perf/kpi` only) reshapes the scenario JSON into the two github-action-benchmark suites the CI gate consumes, and `perf/cmd/allocsgate` (stdlib only, tested, FAIL-CLOSED) is the deterministic allocs/op gate over `task bench` — the gate DECISION (benchstat is the local human A/B tool only, never the CI gate); the gate is `.github/workflows/perf.yml` (split: allocsgate over `task bench` + github-action-benchmark over the scenarios; PR fails-but-never-pushes, main pushes the `gh-pages` trend store). See `docs/adr/0019-perf-tracking.md`.
- `studio/` — **Mecatl Studio**, the WEB client (ADR 0225): a Node module, NOT a Go module — never in `go.work`, the layering DAG, the depguard allowlists, or the api-compat gate, and NOT part of `task test` (it has its own `studio:` Taskfile namespace and CI workflow). It is a CLIENT like `mecatui`: it consumes the PUBLIC HTTP/SSE surface through a server-side same-origin proxy, imports nothing from `engine/`/`internal/`, and is not a second composition root. Managed mode spawns `bin/mecated` on a random authenticated loopback port against the resolved REPO ROOT; external mode uses `MECATL_BASE_URL` + server-side bearer injection and never starts a controller. Provider credentials stay in mecated's normal auth file. A breaking HTTP/SSE change owes a Studio update in the SAME PR. See `studio/CLAUDE.md`.

## The layering rule (the thing to get right)

Expand Down
3 changes: 3 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ includes:
site:
taskfile: website/Taskfile.yml
dir: website
studio:
taskfile: studio/Taskfile.yml
dir: studio

vars:
PKG: github.com/stacklok/mecatl
Expand Down
86 changes: 86 additions & 0 deletions docs/adr/0225-studio-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ADR 0225 — Mecatl Studio as an in-repo module

- Status: Accepted
- Date: 2026-08-14
- Scope: `studio/` — the web client for the harness; its relationship to the Go modules and to `website/`

## Context

Mecatl Studio grew outside this repository as a web client for the public
HTTP/SSE API. That separation left it without CI, coupled it to one developer's
checkout path, and allowed the daemon protocol and the client view model to
drift. One result was especially misleading: a provider failure with
`result.stop == "error"` rendered as a successful empty turn.

`website/` already proves that a Node module can live in this Go monorepo with
its own package manifest, Taskfile namespace, dependency automation, and CI.
Studio needs that same lifecycle without becoming part of the Go dependency
graph or another composition root.

The deployment shape also has two first-class targets: a single-user developer
machine, and a separately operated daemon such as a Kubernetes deployment. The
web tier must not assume that it owns the daemon process or its credentials.

## Decision

Studio lives at `studio/` as a Node module:

- It has its own `package.json`, lockfile, `Taskfile.yml`, pinned Node version,
Dependabot entry, and path-scoped CI workflow. CI builds, behavior-tests,
lints, type-checks, and rejects high-severity npm advisories.
- It is NOT a Go module and never enters `go.work`, the layering DAG, depguard,
or the engine API-compatibility gate. It imports nothing from `engine/` or
`internal/`.
- It consumes only `mecated`'s public HTTP/SSE API through server-side Next.js
route handlers. No Cloudflare Worker, image-optimization worker, D1/R2
binding, or vinext hosting layer is part of the application.
- It has two pure modes:
- **managed** (default): a loopback controller chooses a free HTTP port,
spawns and supervises `bin/mecated`, generates a per-process bearer token,
and resolves the repository workspace from its own location;
- **external**: `MECATL_BASE_URL` points the server-side proxy at an existing
daemon, `MECATL_AUTH_TOKEN` is injected by that proxy, `MECATL_WORKSPACE`
supplies the session workspace, and `MECATL_STUDIO_PUBLIC_ORIGIN` pins the
browser origin. No controller is started
and local provider/router/MCP mutation controls are disabled.
- Provider secrets are never accepted by the browser or controller. Managed
mode selects a provider with `MECATL_STUDIO_PROVIDER`; `mecated` reads the
credential from its normal `auth.yaml` seam and fails with an actionable path
when the selected provider is unavailable.
- The controller pins loopback `Host`, allowlists `Origin`, and requires a
server-injected header for control mutations. Its child-only MCP proxy uses an
unguessable path, caps request bodies, accepts HTTPS targets, and permits
loopback HTTP only through an operator environment opt-in.
- Wire JSON is decoded at a typed runtime seam and covered with behavior tests.
Event kinds that this Studio version does not render are surfaced to the user
instead of silently disappearing.

The imported starter and demo residue is not part of this module: the Worker
hosting files, Vite/vinext adapter, Playwright marketing walkthrough, voiceover
scripts, and their dependencies are removed.

## Consequences

Protocol, rendering, proxy-auth, deployment-mode, CSRF, and egress-policy
regressions now fail in the same repository as the daemon change. A clone can
run managed mode without editing a path, while a containerized web tier can
connect to a separately operated daemon without inheriting process-management
duties.

The repository carries a second package ecosystem and a server-rendered web
runtime. The current suite uses a fake authenticated daemon plus pure wire and
controller-policy tests; it does not yet drive a real offline `mecated` binary.
Generated TypeScript bindings from `contracts/proto/mecatl/v1` remain a follow-up
because adopting a repository-wide TS code-generation toolchain is distinct
from moving and securing the client. Until then, the runtime decoder is the
single browser seam and malformed envelopes fail there.

The HTTP/SSE surface now has an in-repo consumer. A breaking wire change owes a
Studio update in the same PR.

## See also

- `studio/CLAUDE.md` — module commands and invariants.
- [ADR 0002](./0002-documentation-lifecycle.md) — documentation lifecycle.
- [ADR 0036](./0036-engine-module.md) — the engine module boundary.
- [ADR 0087](./0087-mecatui-staged-transport-migration.md) — the embedded-versus-external client precedent.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE
- [0087 — Staged mecatui transport migration](./0087-mecatui-staged-transport-migration.md) *(superseded by 0089)*
- [0088 — Explicit daemon.yaml (listener topology config)](./0088-daemon-config-file.md)
- [0222 — mecatui: ctrl+t routes by ask type; full-screen ask-args view](./0222-mecatui-ask-args-view.md)
- [0225 — Mecatl Studio as an in-repo module](./0225-studio-module.md)

### Retired
- [0029 — Repo-map tree-sitter](./0029-repomap-tree-sitter.md) *(retired)*
21 changes: 21 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ flowchart LR
svc["Service (lifecycle + Run registry)\nauth/mTLS · rate limit · health"]
end

studio["studio/ (web client; Node, not a Go module)\nmanaged-local or authenticated-external HTTP/SSE"]
studio --> http

subgraph APP["application — engine/agent"]
engine["Engine / Run\nloop · dispatch · permission · hooks\ncompaction · cascade · tokencount\nsubagent (Subagent) · parallel (Parallel)"]
end
Expand Down Expand Up @@ -347,6 +350,24 @@ flag and its `MCP_<NAME>_TOKEN` bearer convention through the same package
short-lived per-run identity as the token env and the run presents it to that MCP
endpoint.

**Mecatl Studio — the web client (`studio/`).** The third first-party client,
alongside `mecatui` in the terminal and the ACP editor surface. It is NOT a composition
root and NOT a Go module: it is a Next.js application that consumes the **public
HTTP/SSE surface** through server-side same-origin route handlers, so it imports nothing
from `engine/` or `internal/` and adds no Go dependency. In managed mode its loopback
controller chooses a free port, spawns `bin/mecated` with a generated bearer token, and
resolves the repo root as the workspace. In external mode `MECATL_BASE_URL`,
`MECATL_AUTH_TOKEN`, and `MECATL_WORKSPACE` bind the web tier to an existing daemon; no
controller runs, and deployment-owned configuration controls are disabled. Provider
credentials always remain in mecated's normal auth file. Beyond the chat transcript with
tool-call and approval cards, Studio renders the semantic model router, project-scoped
skills, the read-only user-model index, project-memory availability (the daemon has no
project-memory listing endpoint yet), and the schedule registry. Its proxy/controller
boundary pins loopback origins, authenticates daemon traffic, and prevents browser-driven
control requests or arbitrary plain-HTTP egress. It lives in this repo so a change to the
HTTP/SSE surface breaks it on a PR rather than in a user's afternoon; see
`docs/adr/0225-studio-module.md` and `studio/CLAUDE.md`.

**mecak8s — the storage-free Kubernetes-native agent (`cmd/mecak8s`).** A fifth
composition root and a *thin peer of `mecated`* over the same `app.Build`: it composes the
shared assembly with **k8s-native defaults** — a **Redis** session store + durable event log
Expand Down
1 change: 1 addition & 0 deletions docs/design/PRODUCTION-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ record; current behaviour is in the linked [architecture](../architecture.md) do
| Clipboard image paste (mecatui `ctrl+v`) | ✅ shipped | [CLIPBOARD-IMAGE-PASTE.md](../adr/0026-clipboard-image-paste.md) | — |
| mecatequi (single-shot GitHub Action) | ✅ shipped (v1 forge glue) · ✅ OPT-IN OTLP push telemetry (metrics + traces, flush-before-exit, ADR 0098) | [MECATEQUI.md](../adr/0028-mecatequi.md) · [0098](../adr/0098-headless-telemetry.md) | [overview](../architecture.md) |
| mecak8s (storage-free k8s-native agent) | ✅ shipped (MVP) · ✅ OPT-IN `/metrics` loopback scrape + OTLP push (ADR 0098) · ⛔ CRD/Operator · ⛔ HPA (custom-metrics on active-runs) · ⛔ managed Redis (ElastiCache/MemoryStore — manifest swap, no code) · ⛔ Redis auth · ⛔ fix `mecated`'s unbounded `GracefulStop` (pre-existing, follow-up) | [mecak8s.md](../adr/0048-mecak8s.md) · [0098](../adr/0098-headless-telemetry.md) · [MECAK8S-PLAN.md](./MECAK8S-PLAN.md) | [overview](../architecture.md) |
| Mecatl Studio (web client) | ✅ in-repo Node module + path-scoped CI (build/behavior-test/lint/typecheck/audit) · ✅ npm dependency automation · ✅ managed loopback + authenticated external-daemon modes · ✅ random managed port + generated bearer auth + CSRF/Host/Origin controller gate · ⛔ real-daemon integration test · ⛔ generated TypeScript proto bindings | [0225-studio-module.md](../adr/0225-studio-module.md) | [overview](../architecture.md) |
| ACP adapter (editor stdio surface) | ✅ Phase 1+2 + bounded Phase 3 + multimodal shipped · ⛔ Phase 3 long-tail (rule persistence, grep-over-buffers, fs/* on resume) | [0001-acp-adapter.md](../adr/0001-acp-adapter.md) | [api surface](../architecture/api-surface.md) |
| Conversation fork (peer session from a history snapshot) | ✅ shipped · ✅ effort override (mid-conversation effort switch, keeps the transcript — [0068](../adr/0068-effort-change-via-fork.md)) · ⛔ cross-provider/model fork (v2: replay-blob stripping) · ⛔ workspace-branching fork · ⛔ fork-from-event-log-at-arbitrary-point · ⛔ fork lineage (`forked_from` label) | [0065-conversation-fork.md](../adr/0065-conversation-fork.md) | [overview](../architecture.md) |
| _Historical / retired_ | — | [ARCHITECTURE.md](../adr/0004-v1-architecture.md) · [STEP-CHAIN.md](../adr/0006-v1-step-chain.md) · [TWELVE-PATTERNS-AUDIT.md](../adr/0007-twelve-patterns-audit.md) · [REPOMAP-TREE-SITTER.md](../adr/0029-repomap-tree-sitter.md) | — |
Expand Down
Loading
Loading