maister / web
CLAUDE.md10 starsChanged 6 months agoMITView on GitHub
Read ../CLAUDE.md first. It holds the product spine, locked architectural decisions (ACP-driven HITL + checkpoint/resume, durable canonical SSE, typed MaisterError, concurrency cap, multi-executor via ACP, Flow Engine v2 plugin model) and the out-of-POC list. This file is the Web/Next.js slice only. Agent processes live in ../supervisor/, NOT here. This is the web tier of MAIster: UI + Route Handlers + server actions + Drizzle DB access + SSE bridge to supervisor/. Agent processes (claude, codex) and ACP sessions live in…
In this repository59 other files it gives its agents.
- AGENTS.mdAGENTS.md
- site-docs/AGENTS.mdAGENTS.md
- web/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- aif-architectureSkill
- aif-best-practicesSkill
- aif-build-automationSkill
- aif-ciSkill
- aif-commitSkill
- aif-distillationSkill
- aif-dockerizeSkill
- aif-docsSkill
- aif-evolveSkill
- aif-exploreSkill
- aif-fixSkill
- aif-groundedSkill
- aif-implementSkill
- aif-improveSkill
- aif-loopSkill
- aif-planSkill
- aif-qaSkill
- aif-referenceSkill
- aif-reviewSkill
- aif-roadmapSkill
- aif-rules-checkSkill
- aif-rulesSkill
- aif-security-checklistSkill
- aif-skill-generatorSkill
- aifSkill
- aif-verifySkill
- generate-claudemdSkill
- aif-architectureSkill
- aif-best-practicesSkill
- aif-build-automationSkill
- aif-ciSkill
- aif-commitSkill
- aif-distillationSkill
- aif-dockerizeSkill
- aif-docsSkill
- aif-evolveSkill
- aif-exploreSkill
- aif-fixSkill
- aif-groundedSkill
- aif-implementSkill
- aif-improveSkill
- aif-loopSkill
- aif-planSkill
- aif-qaSkill
- aif-referenceSkill
- aif-reviewSkill
- aif-roadmapSkill
- aif-rules-checkSkill
- aif-rulesSkill
- aif-security-checklistSkill
- aif-skill-generatorSkill
- aifSkill
- aif-verifySkill
- generate-claudemdSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
19 rules. About 0 are common, 7 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 38
Do not: add other component libraries (no shadcn/ui, no MUI, no Chakra).
- Distinctive: line 118
No URL-prefix routing. Locale is purely cookie-driven.
- Very specific: line 264
Prefer awaiting the real condition over polling for a side effect.
- Very specific: line 392
After the rebase onto master 0afffb50 (the ownership-residuals merge): unit 869 files / 9105 tests, 0 failures.
- Distinctive: line 397
After the 2026-09-28 review-remediation cycle (same branch, this Mac): unit
- Very specific: line 465
A first-hit Next-dev compile can exhaust a 10s e2e timeout: Before calling such a failure a regression, re-run the spec in isolation and …
- Distinctive: line 557
PUT/DELETE per changed file with no navigation at all.
- Very specific: line 564
A freshly loaded page can be in the DOM twice: Every (app) page streams behind app/(app)/loading.tsx.
- Very specific: line 772
When the dev server is saturated that round-trip is starved long enough for the coordinator's node to give up, its run-bound facade token …
- Distinctive: line 985
Run status is the execution axis (richer state machine).
- Very specific: line 1015
If Running/NeedsInput (worker live) → supervisor delivers ACP message → resume in-process.
- Very specific: line 1017
If NeedsInputIdle → supervisor spawns a fresh process and restores context via session/resume on acp_session_id → reads the input artifact …
- Very specific: line 1033
Use the @/... alias for imports rooted at web/ — never deep relative ../../...
- Very specific: line 1038
Default to Server Components.
- Very specific: line 1040
Default theme is dark (<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}> in app/layout.tsx).
- Distinctive: line 1088
Tables are view-only: No inline editing, row dropdowns, or row-level mutate buttons.
- Distinctive: line 1141
Feedback and confirmation (UI completion batch): Use the single shared feedback provider for completed mutations.
- Very specific: line 1174
All writes to .maister/<project-slug>/runs/<run-id>/ are atomic (tmp + rename via atomicWriteJson).
- Distinctive: line 1176
Every externally visible ACP event must be committed to the supervisor outbox before publication and projected through canonical Postgres …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md — web/ (MAIster Web Control Plane)
Read
../CLAUDE.mdfirst. It holds the product spine, locked architectural decisions (ACP-driven HITL + checkpoint/resume, durable canonical SSE, typedMaisterError, concurrency cap, multi-executor via ACP, Flow Engine v2 plugin model) and the out-of-POC list. This file is the Web/Next.js slice only. Agent processes live in../supervisor/, NOT here.
What lives here
This is the web tier of MAIster: UI + Route Handlers + server actions +
Drizzle DB access + SSE bridge to supervisor/. Agent processes (claude,
codex) and ACP sessions live in the separate ../supervisor/ daemon —
this slice is the human-facing surface and the registry/board/HITL UX.
The personal librarian lives in lib/librarian/, components/librarian/, and
app/api/librarian/; it keeps per-user history and delegates turn-scoped MCP
access through the supervisor (ADR-185..189).
Current state: scaffolded from the official HeroUI Next.js template
(heroui-inc/next-app-template). The app/, components/, config/ content
is template demo material to be replaced as we implement MAIster routes.
Stack (concrete versions in package.json)
| Layer | Choice |
| Framework | Next.js 16.4.3 (App Router) |
| React | 19.3.1 |
| Language | TypeScript 6.0.3, strict, @/* → ./* |
| UI library | @heroui/react 3.2.6 + @heroui/styles |
| Styling | Tailwind CSS 4.3.3 via @tailwindcss/postcss |
| Variants | tailwind-variants 3.3.1 |
| Theming | Local script-free theme provider (default dark) |
| Lint | ESLint 9 flat config + Prettier |
| Pkg manager | pnpm |
| Node | 24 (per root CLAUDE.md container target) |
Do not add other component libraries (no shadcn/ui, no MUI, no Chakra).
HeroUI v3 + Tailwind 4 + tailwind-variants covers all UI primitives.
Main/default UI language - EN, project should be i18n-ized. RU interface support is REQUIRED
M9 additions (shipped 2026-05-29)
Design tokens (forest theme)
styles/globals.css defines the @theme block with forest-* CSS custom
properties (backgrounds, surfaces, borders, text, accent, status colours).
Light/dark variants switch through lib/theme.tsx, which toggles the
.light / .dark class on <html> without injecting client-rendered script
tags. Components import
the tokens as Tailwind utilities (bg-forest-bg, text-forest-text-primary,
etc.). The tokens are also referenced via the @custom-variant dark rule so
dark: prefixes resolve correctly.
Authentication (Auth.js v5 credentials)
Two-file edge/Node split:
auth.config.ts— edge-safe (no DB).jwt/sessioncallbacks seedid/role/mustChangePasswordfromuserat sign-in, plustrustHost: true. Imported byproxy.ts(which buildsNextAuth(authConfig)).auth.ts— Node.js runtime only.@auth/drizzle-adapter+ theCredentialsProvider(email + password, bcrypt), and a DB-backedjwtcallback that re-readsusers.role/users.mustChangePasswordon every refresh and returnsnull(signs the user out) if the user row is gone. Re-exportsauth,signIn,signOut,handlers.
web/proxy.ts protects all (app) route-group pages. Unauthenticated
requests redirect to /login. API routes under app/api/ call
requireSession() / requireProjectAction() from lib/authz.ts directly
to return machine-readable JSON errors.
Admin bootstrap: the single default admin is seeded by migration
0005 (admin@maister.local / maister-admin, must_change_password =
true) — NOT by first-user registration. register() always creates
member (no admin auto-promotion → no concurrent-first-user admin race).
must_change_password is enforced by the (app) layout, which redirects to
/change-password until the user sets a new password (changePassword
server action). The column is reusable for admin-forced resets.
RBAC model
lib/authz.ts exports:
requireSession()— throwsUNAUTHENTICATED(401) if no session.requireGlobalRole(min)— throwsUNAUTHORIZED(403) ifusers.roleis belowmin. Order:viewer < member < admin.requireProjectRole(projectId, min)— checksproject_members.role. Globaladminusers bypass this check and receiveowneraccess. Order:viewer < member < admin < owner.requireProjectAction(projectId, action)— convenience wrapper mapping named actions (readBoard,launchRun,createTask,answerHitl,editSettings, …) to minimum project roles viaPROJECT_ACTION_MIN. M22 (ADR-053) addsreadRepoFiles(member), gating the workbench git-tracked file routes — strictly abovereadBoard/viewer, so a viewer cannot browse source. The flow-graph view is read-only (authored layout inflow.yaml, ADR-064 — no layout write action). The workbench diff staysreadBoard(viewer, run-scoped).httpStatusForAuthz(code)— mapsUNAUTHENTICATED→401,UNAUTHORIZED→403 for use in API route error handlers.
The projectId passed to project-scoped functions must always be a
server-derived value (from a DB row), never a body field.
DB-authoritative: getSessionUser() re-reads the live users row
(role + mustChangePassword + existence) from the DB on every call — the
JWT supplies only the user id. A demoted/deleted user therefore loses
authority on their next request, not at the 30-day JWT expiry. Never branch
authorization on the cached session.user.role.
i18n (next-intl, cookie-based)
i18n/request.ts— locale resolution:NEXT_LOCALEcookie first, thenAccept-Languageheader, defaulting toen.messages/en.jsonandmessages/ru.json— message catalogs.- In-app toggle calls the
setLocaleserver action (sets the cookie). - No URL-prefix routing. Locale is purely cookie-driven.
- Server Components use
getTranslations()fromnext-intl/server. Client Components useuseTranslations().
Route groups
app/
(auth)/ # Public: /login — no session required.
login/
(app)/ # Protected: session required (proxy redirect).
page.tsx # Portfolio home (/)
projects/
page.tsx # Project list (/projects)
new/
page.tsx # Add project form (/projects/new)
[slug]/
page.tsx # Project board (/projects/[slug])
tasks/
new/
page.tsx # Task creation (/projects/[slug]/tasks/new)Screen reference. Each user-facing screen and the shared chrome are documented screenshot-free under
../docs/screens/— see../docs/screens/README.mdfor the IA map, per-doc template, and index. Update the matchingscreens/*doc when you change a screen.
Scope note
Auth.js/NextAuth, project RBAC, the 6-column board design, and the
credentials login flow are M9 additions that go beyond the original POC
"out of scope" list in ../CLAUDE.md. They were sanctioned by the user as
part of this milestone.
Known M9 deferrals (do not implement without explicit instruction):
- The PRs board tab placeholder was superseded by a real PR-state surface:
board.ts'sprState/prHasConflictsDTO fields, populated by thepr_state_scanjob (ADR-140), drive a PR-state chip on Done/InDelivery/ OnReview cards. (The MCPs board tab is the real M27 project MCP catalog; the platform MCP catalog is now reachable at admin/mcps, and the Inbox nav now reaches a real/inbox— both unlocked by the web-shell nav-unlock work, which also moved supervisor status to the footer only and added a Cmd/Ctrl+K launch shortcut + a runners-readiness rail block.) - The
projectstable now hasrepo_urlandprovidercolumns (ADR-025). It still does not havelang,description,tags, ormcpscolumns — those design ideas remain omitted from the schema until a migration is added. - The settings panel (
/projects/[slug]/settings) is read-only; there is no settings-write API route in M9. - The board's "In Delivery" stage is an approximation computed in
web/lib/board.tsfrom recently-merged run status — not a persisted board column. - Admin user management covers global users at
/admin/users(activate pending registrations, disable/re-enable accounts, change global roles, reset passwords). Project member management shipped after M9: the project members panel adds/changes-role/removes members (web/lib/project-members.ts,web/app/api/projects/[slug]/members/*, ADR-062).
Scripts
pnpm dev # next dev — http://localhost:3000
pnpm dev:clean # remove .next, then next dev — use when dev cache keeps stale UI
pnpm clean # remove .next
pnpm build # remove .next, then next build
pnpm start # next start (after build)
pnpm lint # eslint --fix
pnpm test:integration:ab # in-process A/B acceptance, real supervisor/Postgres
pnpm test:integration:ab --shard 1/2 # first explicit web half; 2/2 runs the complement
pnpm test:integration:isolation # serial production boot, real sandbox driver, invocation cleanup
pnpm install-package # ADR-088: install a multi-flow package (all flows + bundle)
pnpm validate-authored-flow # validate a portable authored Flow package dir
pnpm import-flow-package-draft # import a package as an inert authored draft
pnpm export-authored-flow # export an authored Flow to a portable dir
pnpm install-authored-flow-package # bridge export as untrusted installed packagepnpm typecheck runs tsc --noEmit (noEmit: true in tsconfig).
The web A/B runner accepts --shard i/n; CI uses 1/2 and 2/2 as complete,
disjoint file lists. The isolation runner does not accept this flag.
The test-only MAISTER_TEST_ISOLATION selector is owned by
test-support/process-isolation.ts; its platform/capability and implemented Linux
namespace semantics are in ../docs/configuration.md#test-only-isolation-selector.
Linux development evidence and hosted qualification are separate gates in
../docs/system-analytics/test-infrastructure.md. No product config/API/schema
change or production separate-user guarantee is implied.
Test database contract
Database-backed integration and E2E tests use test-support/pg-container.ts.
It is the only web Testcontainers constructor, owns pgvector image selection,
main/Brain migration order, and pool-before-container teardown. Build/unit work
must not require Docker; integration/E2E require it explicitly.
Supervisor contract in tests
baseUrl() in lib/supervisor-client.ts THROWS CONFIG when
MAISTER_SUPERVISOR_URL is unset under a test runner (ADR-167). It used to fall
through to the dev port, so any suite reaching the real transport silently drove
whatever supervisor was listening — and was green only while one happened to be.
A suite reaches that transport whenever its graph carries owned prompts:
runFlow only takes the execution-host branch when hasOwnedPrompts is true
and the run is Running/NeedsInput, so a graph of cli/check nodes or
artifact_required gates needs nothing, while one with ai_coding/judge/
orchestrator/consensus nodes or ai_judgment/skill_check gates needs a
host. The agent resume path and the
system_sweep event-plane activation reach it too.
Such a suite MUST mock the health seam — it does not need a real supervisor:
vi.mock("@/lib/supervisor-client", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/supervisor-client")>()),
checkSupervisorHealth: async () => readySupervisorHealth(),
getExecutionHostCapabilities: async () => readyExecutionHostCapabilities(),
}));with the two helpers from test-support/supervisor-health-fixture.ts. Only a
suite that genuinely exercises supervisor behaviour should reach for
test-support/real-supervisor.ts instead. Setting MAISTER_SUPERVISOR_URL
globally is NOT the fix: it would disarm the guard for every suite, which is the
hijack it exists to prevent.
Route suites: load the route in a hook, not in a test
await import("../route") pulls a large module graph — 700-800 ms for the HITL
respond and scratch-recover routes. Done lazily inside a per-test helper, that
cost lands on whichever case ran first, inside vitest's 5 s default. On a loaded
machine it blows the budget, and the damage does not stop at one red case: a
timed-out test's request keeps running, so its later calls hit module-level
spies whose mockImplementation now belongs to the NEXT case. That is how the
respond suite produced ["queued","queued","delivered","delivered"] for an
assertion expecting two entries — a false failure in a test that was itself
correct.
Load the module once in beforeAll (its own budget, outside every test) and
have helpers read the cached handle. After the hoist neither suite has a case
over 100 ms.
Prefer awaiting the real condition over polling for a side effect. expect.poll
defaults to a 1 s budget, which is ample idle and too tight under contention;
where a fake already offers a hook on the awaited call, signal a deferred from
it instead. Note that fake-execution-host's runCommand records a call before
running its behaviour, so a signal sent from setPromptBehavior fires with the
call already in fake.calls — and unlike an onCall hook it cannot leak, since
beforeEach reinstalls the default behaviour.
Suite baselines and known-flaky specs
Compare failure SETS, never counts — two runs can tie while one spec traded
places with another. As of 2026-09-03 on main + ADR-165:
- unit — 0 failures.
- integration — 0 failures, except the two-case
lib/runs/__tests__/dirty-resolution-race.integration.test.tspair, which flakes under parallel load and passes 4/4 in isolation.
Measured 2026-09-22 (ADR-179 branch REBASED onto master c4216cd5, this
Mac). unit 819 files / 8480 tests, 0 failures; integration 500 files /
4414 tests, 1 failure (25m); supervisor 26 files / 239 tests, 0 failures.
The lane totals moved because master gained 22 commits, not because this
branch added files. The one failure was
lib/execution-host/__tests__/deliverer.integration.test.ts "D3: runtime-object
reserve, upload, and delete use the assignment-bound command ledger" — a NEW
name, not the documented dirty-resolution-race pair. Classified load-sensitive,
not a regression: it passes 4/4 in four consecutive isolated runs (4 tests each),
and the branch's entire lib/execution-host/ + fake diff is +78/-0 — pure
insertion adding only checkEnvRefs/EnvRefPresence/EnvRefs, with no line
touching the runtime-object or deliverer path. It reproduced identically after
the rebase, so it is now a standing load-sensitive name, not a branch artefact:
treat a recurrence as load, and re-run the file idle before filing it.
Root-caused and fixed 2026-09-23: an ACK seal never makes the manager's
runtime-object row available — only the projected canonical event does — and
D3 deleted straight after publishing, so under load the delete was refused from
pending. The case now waits for the projected state; it is no longer a
standing name.
Measured 2026-09-26 (ADR-182 steering branch on master 8ff196d4, this
Mac). unit 849 files / 8788 tests, 0 failures; integration 528 files /
4652 tests (1 skipped: the opt-in host-span-load); supervisor 76 files /
724 tests, 0 failures (unit 45 / 455, integration 31 / 269). The full lane
ran at load 75-240 and was killed (exit 144) after 526 files had reported, so
its summary never printed: count the per-file lines, and run what is missing —
here lib/flows/graph/__tests__/prompt-owners.integration.test.ts (58/58,
23.5 min alone) and host-span-load. Under that load 8 tests in 4 files were
red, and every one passed re-run idle: lib/agents/__tests__/prompt-owners
"owner-agent-budget … resumes its exact interrupted source" (5 cases, each
failing at ~15.8 s — just past a 15 s wait — and green at 20-77 s idle),
projection-worker AT-03, execution-ab-process-cleanup O2 and
execution-ab-partitions P4. Treat those names as load-sensitive: re-run the
file idle before filing a recurrence. After the same day's review fix round:
unit 850 files / 8792 tests, 0 failures; supervisor 76 files / 730
tests, 0 failures; the full integration lane was NOT re-run — the 21 files the
round touched or depends on ran green (the steering, scratch-placement,
local-package-assistant, migration, ext-message, recovery, deliverer, ledger
and admission suites), and ledger.integration's unhandled "pool after end"
rejection is the one 8ff196d4 also shows. After the rebase onto master
b8003790 (the git-panel merge): unit 863 files / 8992 tests, 0 failures;
supervisor 76 files / 730 tests with one load-only red —
spawn.test.ts "emits bounded session-line events" (expected [] to deeply
equal [1, 2, 3]) failed while the unit lane ran beside it and passed 5/5
alone, neither side having touched spawn.ts; the 10 steering, migration,
scratch-placement, local-package-assistant, ext-message and turn-admission
integration files 100/100.
Measured 2026-09-26 (ownership-residuals branch on master 881619a5,
before efc7833b and 3ee5df87, this Mac). unit 867 files / 9066 tests, 0 failures; supervisor 78 files /
747 tests, 0 failures (unit 46 / 466, integration 32 / 281); mcp 6 files /
266 tests, 0 failures; integration 552 files / 4905 tests (2 files
skipped). The integration lane ran at load 90-99 and 39 files were red; all 39
passed re-run together at load ~10 (499/499), so none is a regression. 31
were whole-file TestDatabaseDockerUnavailableError: container runtime probe
timed out (one No host port found for host IP): every case SKIPPED in ~3 s,
followed by TypeError: The "path" argument … Received undefined from the
file's own teardown. That is the testcontainers probe, not a test — read a
file's first error before counting its skips as failures. The other 47 cases
were timeouts in 8 files: lib/flows/graph/__tests__/prompt-owners (32),
execution-ab-process-cleanup (5), permission-result-failure (3),
execution-ab-process-death (3), gate-permission-result, workspace-reconciler
"processes a due candidate after more than one batch of quarantined paths",
projection-worker AT-03 and execution-ab-partitions P3-live. New
load-sensitive names for this Mac: the Docker probe timeout,
workspace-reconciler's batch case, gate-permission-result and
permission-result-failure (a 767 s file under load, 190 s idle). e2e
m19-reconcile-gc + scratch-detail 13 passed (--workers=2).
Measured 2026-09-26 (ADR-183 outbox-pressure branch on 881619a5, this
Mac). unit 867 files / 9041 tests, 0 failures; supervisor 84 files /
777 tests, 0 failures (unit 49 / 484, integration 35 / 293); integration
549 files / 4871 tests in 29.4 min on a snapshot of the Phase 4 commit —
544 passed, 2 skipped (the opt-in load suites), 3 red and all classified:
app/api/runs/[runId]/recover/__tests__/route.integration.test.ts (Docker
"No host port found for host IP" at container start — 11/11 re-run),
orchestrator-park "re-drives a rebound orchestrator coordinator after death
before its first command" (a 10 s poll at lane load 60–85 — 4/4 idle; a new
load-sensitive name) and execution-ab-partitions P4 (6/6 re-run). The
ledger.integration "pool after end" rejection recurs. The Phase 5 lane
(same Mac, Docker Desktop saturated — setup 505 s against 19 s) had 72 files
red, 67 of them TestDatabaseDockerUnavailableError: container runtime probe
timed out; all 72 passed re-run (340 tests), as did the other 29 agent
integration files: read the setup time before reading the failures. The opt-in R20
(MAISTER_EVENT_PLANE_LOAD=1) is RED at the default budgets because of the
open-span limit — read ADR-183's "Measured limit" before filing it.
Measured 2026-09-27 (ADR-184 open-span prune, same branch, this Mac). unit
868 files / 9062 tests, 0 failures; supervisor 84 files / 782 tests, 0
failures (unit 49 / 486, integration 35 / 296); integration 550 files /
4877 tests on a snapshot of the Phase 2 commit — 546 passed, 2 skipped (the
opt-in load suites), 2 red and both classified: lib/brain/__tests__/ambient
(TestDatabaseDockerUnavailableError: container runtime probe timed out — 10/10
re-run) and consensus-prompt-owners "P0-5: verifier output past 1 MiB …" +
"owner-consensus-verify …" at lane load 120–190 — the same load-sensitive pair
the ADR-183 lane recorded (its plan's N13); the whole file passes 23/23 alone
and the P0-5 case 3/3 filtered, but it went red once more beside one other
file at load ~20, so treat it as a standing load-sensitive name. The Phase 1
lane's one red (execution-ab-process-cleanup O1-default, a nested-cleanup
AggregateError at load ~120) passed 13/13 re-run. The final lane (the capacity
fix included, 550 files / 4877 tests) had one red at load ~150 —
durable-workers-concurrency "E: SIGTERM while a claim is held …" (expected
['pending', 'applied'] to include 'applying': the web drain lost the race to the
load) — green 3/3 idle; a new load-sensitive name. The opt-in R20 is now
GREEN at the default budgets (two 10-minute runs, ADR-184 D7): if it goes red,
first check that the run was alone. A supervisor the fixture watchdog SIGKILLed
("fixture-watchdog-error … could not be inspected" as the last line of its log)
was starved — by the host's load, or by itself: before 2026-09-28 nine paused
producers pegged it at 100% CPU (the capacity cascade, EDGE-EVT-14), so a pegged
supervisor with paused producers is a product regression, not load.
After the rebase onto master 0afffb50 (the ownership-residuals merge):
unit 869 files / 9105 tests, 0 failures; supervisor 84 files / 785
tests, 0 failures; integration 558 files / 4968 tests, no test failure
(delegate-flow-arm failed at container start — Docker "No host port found for
host IP" — and passed 19/19 re-run).
After the 2026-09-28 review-remediation cycle (same branch, this Mac): unit
870 files / 9135 tests, 0 failures; supervisor 85 files / 798 tests, 0
failures (unit 50 / 494, integration 35 / 304); integration 559 files /
4984 tests, twice. On a snapshot of 56ec3da82 at load ~150, one red: the
standing durable-workers-concurrency E, plus five unhandled 57P01
("terminating connection due to administrator command") from
permission-crash-boundary — its Recover case detached the resumed
runFlow, which then outlived afterAll and held connections while the
container stopped (fixed the same day, cf7931090: the case settles its
driver and afterAll fails on a straggler). On the
final tree, one red: bounded-output (a new load-sensitive name — this branch
never touched it) and the ext … memory route skipped on the Docker probe
timeout. All four files passed together idle (50/50), with no unhandled error.
Current local qualification, 2026-09-30 (codex/stage-ab-ci-r9, tested
175e7c57c, Darwin ARM64 / Node 24.15.0). Web units: 878 files /
9,204 tests, zero failures. Ordinary integration: 589 files / 5,206 tests
qualified file by file from the actual two web shards, complete serial
remainder and owning reruns; every accepted result passes with no skips.
The diagnostic remainder's four failures were a proved 1,843-second macOS
sleep during D2a and obsolete placement/frozen-intent seed data in
crash-resume / emit-hitl; both corrected files pass, and the entire
mandatory isolation CLI passes 7 files / 50 cases in
801.079 seconds with zero leaks. The two existing opt-in load suites are
excluded explicitly. Supervisor units/integration: 86 files / 805 tests
(50/495 unit, 36/310 integration), all passed; mandatory A/B subset 10/111.
The exact original reporters, tested SHAs and production-source equivalence
are recorded in the CI/R9 plan.
Full source types, contracts/docs/ERD and migration checks pass; full lint has
zero errors (web 769 existing warnings, supervisor four). Hosted acceptance
remains open: master run 36979318143 at 5cf0076f9 fails the Node 24.15
rework control and the Intel isolation deadline. Only the independent preflight
report is complete. The linked CI/R9 plan records exact jobs and artifacts.
Hosted-failure follow-up (2026-10-02, local ARM64 / Node 24.15.0): status-transition, successor-activation and permission-cap fault controls are synchronized to their intended boundaries. The complete agent-owner suite and all seven isolation suites pass 100/100 in eight files, no skips or leaks, in 1,551.167 seconds including cleanup. Independent preflight passes 1/1 in 3.932 seconds. The existing suite-owned migrated templates and fresh case clones remain unchanged; their helper controls retain the earlier 8/8 integration and 2/2 unit qualification. Cleanup logs preserve failed stages and nested causes; bounded process-identity diagnostics retain strict signaling refusal. The final real foreign-process refusal control passes. These ARM results do not explain the two hosted cleanup-only failures or establish Intel budget acceptance. Exact RED/GREEN reporters and remaining gates are in the CI/R9 plan.
Latest corrective qualification (2026-10-02, local ARM64 / Node 24.15.0):
Scratch authority writers share FOR NO KEY UPDATE on runs before locking
the dialog, avoiding the transcript allocator/FK deadlock without allowing
concurrent writers. Two real-PG RED controls cover permission persistence and
recovery queueing. Rework tests enter genuine worktree-backed Review instead
of overwriting Done during directory cleanup. Final consumer suites qualify
273/273 cases; full isolation passes 50/50 in seven suites in
801.033 seconds including cleanup. Separate preflight passes 1/1 in
4.868 seconds; both isolation invocations have zero leaks and use the default
Docker probe deadline. Hosted 37010985789 remains failed: its Docker
initialization subphase and Intel budget remain unresolved. The CI/R9 plan
owns exact RED/GREEN reports and local probe settings. At that historical checkpoint S5.2 stayed 42/45;
the owner authorized proceeding next to S5.3a's Linux-driver proof.
Current S5.2 acceptance (2026-10-20): owner-pushed b8e72adfe passed
both macOS Intel isolation parts (19/31 cases), both preflights, four web
shards and two supervisor legs on the required Nodes, within unchanged budgets
and with zero leaks. Parent stabilization is 43/45; S5.3/S5.4 remain open.
Exact-source receipts and limits: CI-R9d checkpoint.
This does not qualify later sources or Linux.
A first-hit Next-dev compile can exhaust a 10s e2e timeout. Before calling
such a failure a regression, re-run the spec in isolation and read the RETRY
time: scratch-detail.spec.ts:50 ("suggests project skills"; :57 since
ADR-182) fails at 10.8s and passes on retry at 1.0s, reproducibly, on
BOTH this branch and plain master (2 runs each; re-confirmed 2026-09-26 on
the steering branch and 8ff196d4: 11-14 s, then ~1 s on retry — at load 245
the retry failed too).
A retry that passes in a second proves the data was there and the first hit paid
for compilation — the failure is warm-up, not behaviour.
e2e — 1 pre-existing failure in 1 spec file, enumerated below, plus one known-flaky spec. Ports 3100/7788 and the
maister_e2edatabase are shared across worktrees; kill both ports before a run. Before filing an e2e failure as environmental, read the[WebServer]lines in the run log: a React server-component stack there is a product bug, not a test one. That is not hypothetical — it is how the ADR-168 Integrations crash below was found.The set, not the count (re-measured 2026-09-17, one failing case per spec unless noted). An earlier entry said "34" with no list, which is unusable: this section's own first line says to compare SETS, and a bare number cannot be diffed.
studio-ai-assistant— it needs a real ACP turn from the test supervisor, which is infrastructure rather than a selector.A serial file hides tests behind its first failure.
forked-package-loopismode: "serial", so its batch failure had been SKIPPING the two tests after it — the old "(1)" undercounted by two whole tests, and both carried stale expectations of their own (an unreachablesync-notice, and a publish source selected by bare path rather than by thefile://url it is stored under). Count the SKIPS, not just the failures, before trusting a serial file's entry here.A control can be painted and still be unreachable.
review-commentsspent a long time looking like a broken selector. It was two things: the library renders a.diff-add-widgetin BOTH the gutter and content cells, pushed onto the column seam by opposingtranslateXso they stack at one rect (the content one is on top and is what a pointer hits); and at 1280x720 the split diff pane is SEVENTY-TWO pixels wide — shell 848 → diff column 410 → file tree- 72 — so the widget straddled the container's
overflow-hiddenedge and left the hit-test tree.elementsFromPointat a control's own centre is the cheap way to tell "wrong locator" from "nothing can click this": if it returns an ancestor, no amount offorceordispatchEventwill help.
Each has its OWN cause — this set has no shared theme left, so triage from the failure rather than looking for a pattern. Two of them (
evaluation-lab,forked-package-loop) share one: both POST an evaluation launch-batch with a hand-written recipe whoseflowRefId,flowRevisionIdand contract digests are placeholders, and the ADR-150 preflight refuses it withPRECONDITION -> 404. That is not fixable by adjusting the values:lab-queries.tsstates the contract outright — the server resolves the recipe scaffold and "the client can never fabricate a digest that would pass preflight", so the specs have to take the scaffold from the server.Closed on 2026-09-17 (26 spec files, 31 cases — 3 of them tests that a serial-mode skip had been hiding), all stale expectations except three product bugs.
evaluation-labandforked-package-loop's batch test needed the launch dialog rather than a hand-written recipe: since ADR-150 the route preflights every inline recipe, andlab-queries.tsrecords that "the client can never fabricate a digest that would pass preflight" — so a spec that POSTs one directly can only ever get a 404. The third:getDefaultBranchprobes withgit -C <dir>, which resolves UPWARD — so for a directory that is not itself a repo root it described the enclosing checkout. New-empty onboarding writes the manifest beforegitInitruns, so a greenfield folder inside another working tree stamped that tree's default into the manifest and the project row, for a repo then created onmain. The second:listProjectAutomationssorted by a rank written as a bare integer literal, which Postgres reads in ORDER BY as an ORDINAL POSITION —ORDER BY 0failed to analyse and the project Automations tab fell to the error boundary on every project, takingrun-schedulesandproject-automationswith it. Both had been filed as stale tests. Every test that touched that function mocked it, so the SQL was never executed. Additionally closed:platform-acp-runners·run-schedules·project-automations·studio-package-viewer·multi-run-cost-policy(2) ·run-sync·execution-host-contract·project-onboarding·project-registration, andplatform-agents-page, which now passes untouched. The first product bug:studio-diff·studio-import·package-management(staleflow.yamlfixtures missingcompat.engine_min, and a digest-labelled install button) ·flows-authoring(3) ·m11b-takeover·m11c-settings-enforcement·m13-assignments·m16-external-operations·m22-workbench·m27-flow-editor·m27-platform-mcp·adr160-rework-claim·adr161-node-interrupt(the last two needed no change) ·flow-package-viewer·flow-studio-artifacts·studio-local-edit(3). The product bug:IntegrationsPanel, a server component, calledisManagedTokenRowfrom a"use client"module, so the project Integrations tab crashed on any project holding at least one API token.Two causes recur — check both before diagnosing anything else.
waitForLoadState("networkidle")cannot settle against a Next dev server and will burn the whole test budget; wait for the specific response instead. No e2e spec calls it any more — keep it that way. Note that a save is not always a route POST: the LOCAL package editor passes a CLIENT function to<form action={...}>, so React calls it directly and it emits one PUT/DELETE per changed file with no navigation at all.And a UI that renders the same text twice (a header pill plus an inspector row, a page
<h1>plus a section<h3>) turns a baregetByText/getByRoleinto a strict-mode violation, which reads like a missing element but is the opposite.A freshly loaded page can be in the DOM twice. Every
(app)page streams behindapp/(app)/loading.tsx. React 19.2 reveals a finished boundary on a throttle ($RCqueues it,$RVswaps it in up to ~300 ms later), and until then the server's copy waits in a hidden<div id="S:0">at the end of<body>. An update that reaches the still-dehydrated boundary, such as an attention tick'srouter.refresh(), makes React client-render the page into<main>. Until ADR-171 D7 every load got that tick (the connect-time snapshot). Now only a page that something moved under does, so the window is rarer but not gone. For that window a page-levelgetByTestIdor CSS locator matches both copies (role locators skip the hidden one). The signature isstrict mode violation: getByTestId('<page test id>') resolved to 2 elements, the second one outside<main>, often after aReceived: hiddenpoll; a one-shotcount()reads double. Measured on the Desk on 2026-09-26: the refresh's RSC request at 308 ms, both copies at 487 ms, the hidden onedisplay: noneunderdiv#S:0. Read page content insidepage.getByRole("main"), asdesk.spec.tsdoes. Its race test holds the reveal (holdStreamedReveal) and pushes a tick once the copy is parked, so it fails if the scoping regresses. Scoping has one cost: a one-shotcount()taken before the page reaches<main>now reads 0, where it used to count the parked copy, so wait for the region to be visible first. The same signature showed onpush-notifications:103/218(notifications-panel),work-table:97(work-empty) andactivity-feed:51, and those specs now read page content inside<main>too.outbound-webhooks:363's violation was different: two visible rows left by a failed first attempt, i.e. a retry that lied.Assume things are collapsed. Much of this UI now hides content behind a disclosure, a non-default tab, or a collapsed tree folder — the workbench Files/Diff tabs, the package viewer's raw YAML, the composition Files tab and its folders, the Studio AI dock (which a graph-node selection also force- closes). Some render
hidden, some do not mount at all; the second kind fails fast, butscrollIntoViewIfNeeded()on the FIRST kind never fails at all — it retried for a full 180 s budget, which is what madestudio-local-editlook slow rather than red.Do not assume a click focused CodeMirror. Measured on
flow-package-viewer: on failing runsdocument.activeElementwas the body, on passing onescm-content. Keystrokes are then swallowed silently and the buffer keeps its original text, so the failure surfaces far away — at a later hidden-input assertion — looking like a broken form. Click until focus lands, and scope to the intended editor: a page can host several.They were deterministic, not interference. Sampled
studio-diff,project-registrationandm13-assignments— three different areas — and all three failed in isolation (2 workers, no cross-spec load) AND on a detachedmasterwith the identical pass/fail split. So the shared-DB-interference explanation this config'sretries: 1exists to absorb does NOT cover them; each failed both its attempts.m13-assignmentsfailed in 9.1 s, far too fast to be host saturation.A retry can lie. Several of these specs mutate seeded state one way — a claim, a git commit, a token create. Once the first attempt fails partway, the retry fails somewhere else entirely and its error describes the first attempt's leftovers, not the defect. Diagnose the FIRST attempt.
4 workers oversaturates this Mac on a wide selection. A 28-spec run at
--workers=4failedadr160-rework-claimandforked-package-loop:518in both attempts; the SAME 28 specs at--workers=2went 57 passed, exit 0, and finished faster (1.9 m vs 2.2 m). Both stragglers also pass alone and paired with each suspected neighbour. Halve the workers before reading anything into a wide run's failures — and note that the cap is NOT the mechanism here:playwright.config.tssetsMAISTER_MAX_CONCURRENT_RUNS: "64"for e2e.Read the host before believing a timeout-class failure. The 2026-09-17 measurement ran at load average 55-59 on 16 cores, against a competing
vitest --project integrationfrom another worktree and a 688 %-CPU VM. That does not explain a 9 s deterministic failure, but it does inflate the 30 stoBeVisibleand 180 sclicktimeouts in this set — re-measure those on a quiet host before diagnosing them as product bugs.Not in the set, deliberately:
execution-ab-content.spec.ts(4 cases) used to fail here every run. It owns a lane of its own (playwright.execution-ab.config.ts) with a dedicated supervisor and runtime roots, was not listed inAUTHED_SPEC, and so ran UNAUTHENTICATED in the defaultchromiumproject where it cannot pass. The default config now ignoresexecution-ab-*.spec.tsthe same way it already ignoredlive-*.- 72 — so the widget straddled the container's
Measured 2026-09-23 (ADR-181 branch, this Mac): two more load-sensitive
integration names, both in code that branch does not touch.
lib/flows/graph/__tests__/permission-resume.integration.test.ts
"owner-flow-repeated-permission" failed once in a lane that started at load 157
and passed in every re-run — re-run a hit idle before reading anything into it.
lib/__tests__/permission-deadline.integration.test.ts RED 13/14 (red at load
≥ 28, green at ≤ 20) was not load noise but a product bug the load exposed:
the resumable mock answered the cap's cancelled permission with end_turn,
which under load beat the SIGTERM, so the prompt settled succeeded and the
agent grant took kind: "result" — marking the operator's stored answer
delivered although no session.input ever carried it. Master's ADR-180
correction (04a3a39a) fixed it: a checkpointed permission without confirmed
input resumes as continue whatever the prompt's outcome — 12/12 at load
75-278 after the fix, with the succeeded-prompt race firing in 7 of 12 cases.
The tell was a stable red rate with one wrong VALUE and no timeout: a load
flake does not pick an answer. Ask whether the "by-rule" outcome tells the
operator the truth before blaming the fixture. The lane totals were 511 files
/ 4544 tests.
Budget ~25 min for the integration lane and do not mistake it for a hang. It
is gated by two very slow files — lib/flows/graph/__tests__/prompt-owners.integration.test.ts
(~23 min, 52 tests) and lib/agents/__tests__/prompt-owners.integration.test.ts
(~15 min, 50 tests). While they run, the execution-host resolver logs
sequence 0 / disposition duplicate every 2 s, several hundred times per run.
That is the normal shape of these suites' event replay, NOT a wedged stream.
Any test that reaches the supervisor transport must NAME a host (M51 T8.6,
2026-09-11). lib/supervisor-client.ts refuses an unnamed host under a test
runner, because the default used to fall through to the dev port and drive
whatever supervisor was listening on the developer's machine — which is why a set
of suites appeared to flip between green and red depending on whether
pnpm --filter @maister/supervisor dev happened to be up. Install the fake seam
instead: fakeExecutionHosts(db) in beforeAll for service paths that claim an
assignment, fakeGraphHosts(db, runId) per run for graph paths, and pass
executionHosts into runFlow. evidence-readiness-all-blocking-kinds,
hitl-hook-trip and lib/flows/__tests__/runner.integration.test.ts were the
last three offenders (8 failures) and are fixed; a test that wants a SPAWN
failure should script the fake's turn to throw rather than omit the host, since
an unresolvable host now fails at ADR-166 assignment — before the runner can
record any state.
The separate takeover-resume full-suite flake was an ENOTEMPTY on a git
worktree temp root, not a seam problem: a git child may still be writing pack
objects when the case returns. Both of that spec's rm calls now carry
maxRetries/retryDelay, the same remedy the sync-resolver temp-repo cleanup
uses. Reach for that before calling a cleanup failure flaky.
Measured 2026-09-21 (ADR-176 branch, --workers=2, both runs on the same
host back to back). The three durable workers DO boot in this lane — the
config runs next dev, which runs instrumentation.ts — and a direct next
dev probe confirms all three start with all five owner kinds. No e2e failure
is attributable to them. The two sets:
- branch (4 failed, 4 flaky, 185 passed):
desk.spec.ts:205,platform-agents-page.spec.ts:26,review-diff-scopes.spec.ts:99,studio-ai-assistant.spec.ts:69. - master (5 failed, 187 passed):
desk.spec.ts:205,forked-package-loop.spec.ts:518,platform-agents-page.spec.ts:26,review-diff-scopes.spec.ts:43,studio-ai-assistant.spec.ts:69.
Three fail in both. forked-package-loop:518 is master-only here. The one
branch-only entry is review-diff-scopes:99 — but that FILE fails in both
trees with the failing case trading places (:43 on master, :99 on the
branch, and :43 flaky-then-passing on the branch), and the whole file passes
3/3 in isolation on the branch across three consecutive runs. It is
load-sensitive, not worker-regressed.
Measured 2026-09-22 (ADR-179 branch REBASED onto master c4216cd5,
--workers=2, quiet host, ports 3100/7788 freed first). 199 tests: 5
failed, 4 flaky, 188 passed (7.8m). The suite grew from 193 because master
added specs.
- failed:
desk.spec.ts:205,platform-agents-page.spec.ts:26,review-diff-scopes.spec.ts:43,scratch-detail.spec.ts:50,studio-ai-assistant.spec.ts:69. - flaky:
activity-feed.spec.ts:51,push-notifications.spec.ts:103,push-notifications.spec.ts:218,work-table.spec.ts:97.
Nothing here is attributable to the MCP value model. Four of the five
failures are in the sets above (review-diff-scopes sits on :43, the
documented master line, and forked-package-loop:518 no longer fails). The
one name in neither set, scratch-detail.spec.ts:50, was settled by
MEASUREMENT rather than by reading the diff: it reproduces identically on plain
master (10.8s fail → 1.0s retry pass, two runs on each tree), and every file
implementing or exercising the suggestion list is byte-identical between the
two trees. Run the spec on master before attributing a name like this to a
branch — after a rebase, "a NEW name" no longer implies "this branch's",
because master moved too.
Measured 2026-09-23 (ADR-181 branch; its merge base c36ff5b1 reproduced in
a detached worktree on this Mac). Three names had joined the set on master
itself. flow-target-delegation.spec.ts:36 was a deterministic race, not load:
its two-node cli flow children reached Review before the coordinator's turn
ended, so the coordinator — correctly — completed without parking and the wake
the spec proves never ran. The children now wait at their first node for a file
the spec creates after it sees the park (e2e/_seed/delegated-release.ts).
orchestrator-loop.spec.ts:56 and m11b-takeover.spec.ts:67 were
budget-bound: ~19 s and ~22 s on a quiet host against the 30 s default, out of
budget at load 40-72 with --workers=2; both now carry 120 s (m11b also waits
15 s for the claim's refresh and 60 s for the post-return resume).
push-notifications.spec.ts:103 was intermittent here under either config
(Notification.permission === "denied" at mount). Measured 2026-09-26: this
headless Chromium reads it as "denied" whatever the context grants,
context-wide or origin-scoped. The panel hides the other-browsers count in
that state, so "1 other browser" was only ever read off the server's markup
before the panel mounted. The test now pins the permission "Allow" produces. What found the race was the
dev server's own log — temporarily set webServer.stdout: "pipe" in
playwright.config.ts ("orchestrator turn ended with no pending children").
That log also showed the observatory fixture's task-less Pending flow run
being promoted whenever a slot freed (promoteNextPending runFlow dispatch
failed — task not found); the fixture seeds no queued run any more.
pnpm lint on master c4216cd5 is RED: 3 errors, all
react/no-children-prop in components/observatory/__tests__/ — 2 in
observatory-filter-bar.test.ts, 1 in overview-table.test.ts, landed by the
observatory merge. The historical baseline (0 errors / 14 warnings) describes
the tree BEFORE that merge. Lint the changed files alone to see your own
contribution: this branch's 92 changed web files give 0 errors.
webServer.stdout defaults to "ignore", so this lane DISCARDS the dev
server's structured logs. Only its stderr is piped, which is why the
[WebServer] lines you see are deprecation warnings and React noise while
every pino INFO line — including prompt-owner-worker-started — is invisible.
Absence of a log line here is not evidence the code did not run; verify with a
direct next dev instead. Not changed here (it is an e2e-config change outside
this branch's request), but it costs real diagnosis time.
Known-flaky: e2e/recursive-harness.spec.ts fails roughly one run in three
under the full parallel suite and passes in isolation. The test supervisor
delegates SYNCHRONOUSLY inside its sendPrompt handler, so the supervisor's
response waits on Next while Next's runner waits on the supervisor's response.
When the dev server is saturated that round-trip is starved long enough for the
coordinator's node to give up, its run-bound facade token is revoked, and the
next delegation returns 401 UNAUTHENTICATED "revoked" — leaving a short
fan-out (one grandchild instead of two). orchestrator-loop shares the design
but makes 2 delegations to this spec's 6, so it is far less exposed. Restructuring
that handler is a backlog item; do NOT weaken the spec's fan-out assertion to
hide this.
Current code structure
web/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout: Providers + Navbar + container
│ ├── page.tsx # Home (template stub — to replace with run list)
│ ├── providers.tsx # local theme provider wrapper
│ ├── error.tsx # Root error boundary
│ ├── about/ # template stub
│ ├── blog/ # template stub
│ ├── docs/ # template stub
│ └── pricing/ # template stub
├── components/
│ ├── navbar.tsx # HeroUI Navbar wired to siteConfig.navItems
│ ├── theme-switch.tsx # local theme toggle
│ ├── icons.tsx # inline SVG icons (IconSvgProps type)
│ ├── primitives.ts # title()/subtitle() via tailwind-variants
│ └── counter.tsx # HeroUI <Button> demo (delete when no longer used)
├── config/
│ ├── site.ts # navItems, navMenuItems, external links
│ └── fonts.ts # system font CSS variable class names
├── styles/
│ └── globals.css # @import "tailwindcss"; @import "@heroui/styles";
│ # declares @custom-variant dark (&:is(.dark *))
├── types/
│ └── index.ts # IconSvgProps
├── public/
├── next.config.mjs # empty config
├── postcss.config.mjs # @tailwindcss/postcss plugin only
├── eslint.config.mjs # flat config (see Conventions below)
├── tsconfig.json # strict; paths `@/*` → `./*`; jsx: react-jsx
└── pnpm-workspace.yaml # only declares allowBuilds for native deps
# (NOT a real monorepo workspace)What to build here (mapped from ../docs/)
Pages — replace template stubs:
app/page.tsx→ Portfolio home (superset.sh-style): grid of active workspaces across all projects. Card = project · branch · status · last activity · quick actions. Filters by project + status.app/projects/page.tsx→ Projects list: registered projects + "Add project" button. Each row links to its board.app/projects/new/page.tsx(or modal) → Add project: paste path to a directory containingmaister.yaml; server validates & stores.app/projects/[slug]/page.tsx→ Project board: 2 columnsBacklog | In Flight. In Flight bucket:Running | NeedsInput | NeedsInputIdle | Review | Crashed. A task card in Backlog shows a Launch button — click runs precondition checks and creates a new Run (no drag-and-drop in POC). HITL form renders inline on the In-Flight card when state isNeedsInput*. Dedicated Inbox block beside the board lists pending HITL requests across all flows of the project. Done/Abandoned in a filter tab beside the board.app/projects/[slug]/tasks/new/page.tsx(or modal) → Task creation: title + prompt + Flow dropdown (frommaister.yamlflows[]). Runner selection happens at launch time from the platform ACP runner catalog.app/runs/[id]/page.tsx→ Run detail: status, live logs, HITL form panel, diff view, action buttons (mark ready, merge, abandon, recover). Worktree context shown (project + branch + worktree path + executor). Page sends periodicPOST /api/runs/[id]/activitywhile visible to extendkeepalive_untiland prevent premature checkpoint.
Route Handlers under app/api/:
POST /api/projects(validatemaister.yamlv2 + install referenced Flow plugins from git tags, persist row)DELETE /api/projects/[slug](soft-archive — setsarchived_at)POST /api/projects/[slug]/tasks(create task → statusBacklog)POST /api/runs(precondition checks, ACP runner resolution,git worktree add, supervisorPOST /sessions— body carriestaskIdand optionalrunnerId;projectIdderived from task)GET /api/runs/[id]/stream(SSE;lastEventIdreconnect; tails the per-step log file populated bysupervisor-client)POST /api/runs/[id]/hitl-response(atomic writeinput-<step-id>.jsonunder the project subtree → if worker live, supervisor delivers ACP message; if checkpointed, supervisor respawns + resumes viasession/resume)POST /api/runs/[id]/activity(extendkeepalive_untilby 30 min while user is actively on the run page)GET /api/runs/[id]/diff(raw diff + per-file+/−summary; rendered split/inline via@git-diff-view/react, ADR-066)GET/POST /api/runs/[id]/review-comments(ADR-072 — list line-anchored review threads with computedinline|outdatedplacement; create a root comment or reply at an open review gate)PATCH/DELETE /api/runs/[id]/review-comments/[commentId](ADR-072 — author-only body edit or root-only resolve/unresolve; author-only delete, root cascades replies)POST /api/runs/[id]/merge(git merge --no-ff; conflict → abort, run staysReview)POST /api/runs/[id]/abandon(supervisorDELETE /sessions/:idif alive; mark worktree stale; task →Abandoned)POST /api/runs/[id]/recover(Crashed → respawn +session/resumeonacp_session_idif present; otherwise force-discard worktree)GET /api/runs/[id]/artifacts(M12 — list the typed-artifact evidence index; optionalnode/kind/validityfilters)GET /api/runs/[id]/artifacts/[artifactId]/payload(M12 — raw payload per locator;text/plainorapplication/json,execution-objectbytes asapplication/octet-streamand gated byreadRepoFileslike the direct content route; always a sandboxed no-store attachment — AB-12; path-confined to the run dir)GET /api/cron/gc(Abandoned/Done/Failed worktrees + checkpointed sessions >7d, all projects)
Primary nav is the role-driven left rail (components/chrome/left-rail.tsx):
Projects (/), Inbox (/inbox), Flows (/flows), plus an
admin-only block (Agents /agents (M34), MCPs /mcps, Users,
Scheduler, Settings). Supervisor status lives once in the footer status
bar. The config/site.ts navItems are template leftovers, not the live nav.
Server-side modules (add as needed, names suggested — not yet present):
lib/errors.ts—MaisterErrordiscriminated union (see root §3, expanded taxonomy includesEXECUTOR_UNAVAILABLE | FLOW_INSTALL | ACP_PROTOCOL | CHECKPOINT).lib/atomic.ts—atomicWriteJson(tmp + rename).lib/worktree.ts—git worktree add|remove|listwrapper, project-scoped paths.lib/config.ts—maister.yamlv2 loader (project + flows[] with version pins and optional project runner refs),schemaVersioncheck, zod-validated, slug derivation. Also loads Flow plugin manifests (flow.yaml) with portablerunner_profiles.lib/flows.ts— Flow plugin loader:git clone --branch <tag>into~/.maister/flows/<id>@<tag>/system cache, symlink into.maister/<slug>/flows/<id>/, manifest validation, version-pin enforcement.lib/acp-runners/*— platform runner catalog, readiness, usage references, Flow runner remaps, and launch-time runner resolution (launch override → Flow step target → project Flow default → platform Flow default → project default → platform default).lib/execution-host/— (Implemented — ADR-166) the supervisor boundary: registrar/resolver, assignment mint, command ledger + deliverer,BoundClientper assignment /HostAdminClient.lib/supervisor-client.ts— HTTP+SSE local-direct transport to../supervisor/(importable only fromlib/execution-host/**):POST /sessions,DELETE /sessions/:id,GET /sessions/:id/stream,POST /sessions/:id/input(deliver HITL response when worker is live). Reconnect withlastEventId.lib/projects.ts— registry CRUD, slug derivation, slug +repo_pathuniqueness enforcement, recursiveMAISTER_PROJECTS_DIRauto-discovery, Flow plugin install on register.lib/scheduler.ts— global concurrency cap (MAISTER_MAX_CONCURRENT_RUNS), Pending queue, auto-promote on slot free.lib/db/— Drizzle schema (projects,platform_acp_runners,flows,flow_revisions,tasks,runs,workspaces,hitl_requests) + client.lib/reconcile.ts— startup hook: per-projectrunsvsgit worktree listvs supervisor's live session set; orphanRunningwith no live ACP session and no checkpoint →Crashed.NeedsInputIdlewith valid checkpoint stays valid.
There is NO lib/runner.ts — agent subprocess lifecycle lives in
../supervisor/, not Next.js. The web tier talks to supervisor via
lib/execution-host/ only (ADR-166, Implemented); lib/supervisor-client.ts
is that module's transport.
Drizzle schema sketch (server-only, lib/db/schema.ts):
// projects
{ id, slug (unique), name, repo_path (unique), main_branch, branch_prefix,
maister_yaml_path, default_runner_id?, created_at, archived_at? }
// platform_acp_runners // platform-scoped launch catalog
{ id, adapter: 'claude' | 'codex', capability_agent: 'claude' | 'codex',
model, provider (jsonb), permission_policy,
readiness_status, enabled, created_at, updated_at }
// flows // installed Flow plugins per project
{ id, project_id, flow_id, source_url, version_tag, // tag-pinned
install_path, // resolved symlink target
manifest (jsonb), // parsed flow.yaml (steps, etc.)
enabled_revision_id?,
installed_at }
// tasks
{ id, project_id, title, prompt, flow_id, status:
'Backlog' | 'InFlight' | 'Done' | 'Abandoned',
latest_run_id?, // FK to runs; null until first Launch
attempt_number, // monotonic per task, starts at 1
// UNIQUE (task_id, attempt_number) on runs
created_at, updated_at }
// runs // task : runs is 1 : N (retry / ralph-loop)
{ id, project_id, task_id?, flow_id?, workspace_id, runner_id?,
runner_resolution_tier?, capability_agent?, runner_snapshot?,
acp_session_id?, // resume handle for session/resume
flow_version_tag, // snapshot at launch time
status:
'Pending' | 'Running' | 'NeedsInput' | 'NeedsInputIdle' | 'Review' |
'Done' | 'Abandoned' | 'Crashed' | 'Failed',
// attempt counter lives on tasks.attempt_number in M5; (Designed M8)
// moves to runs.attempt_number with UNIQUE (task_id, attempt_number).
current_step_id?,
checkpoint_at?, // when graceful checkpoint happened
keepalive_until?, // 30 min sliding window in NeedsInput
started_at, finished_at? }
// workspaces
{ id, project_id, run_id, branch, worktree_path, status, created_at,
removed_at? }
// hitl_requests
{ id, run_id, step_id, kind: 'permission' | 'structured_form' | 'human_review',
question, context, response_schema (jsonb)?, // null for binary permission
response (jsonb)?, on_reject_goto?, // for human-review send-back
requested_at, responded_at?, expires_at }Task status is the board axis (Backlog | InFlight | Done | Abandoned). Run status is the execution axis (richer state machine).
Task lifecycle (1:N task→run):
- New task →
Backlog. - Launch click → precondition checks (project active, clean repo,
branch free, worktree path free, global cap not hit, selected executor
registered) → supervisor
POST /sessions→ task →InFlight. Task staysInFlightwhile the latest run is in any ofPending/Running/NeedsInput/ NeedsInputIdle/Review/Crashed. - Latest run merged → task →
Done(terminal). - Latest run
Failed | Abandoned→ task auto-returns toBacklog; Launch button reappears on the card; next click spawns attempt N+1 against the same task with a fresh worktree. Newattempt_number = max + 1. - "Discard task" (single explicit user action on the card) → task →
Abandoned(terminal). No automatic transition toAbandoned— it always takes an explicit user click. (Run-level abandon ≠ task-level abandon.)
Run lifecycle (ACP keep-alive + checkpoint+resume):
Pending→ scheduler promotes within concurrency cap →Running.- ACP session emits
session/request_permissionor agent writesneeds-input.json→Running→NeedsInput.keepalive_untilset tonow + 30 min. - Each web-console activity (run page open/focus/keystroke in form) →
bump
keepalive_untilby +30 min. now > keepalive_untilwhile stillNeedsInput→ supervisor checkpoints the agent (graceful exit,acp_session_idretained), setscheckpoint_at = now→NeedsInput→NeedsInputIdle.- User submits response →
lib/atomic.tswritesinput-<step-id>.json→- If
Running/NeedsInput(worker live) → supervisor delivers ACP message → resume in-process. - If
NeedsInputIdle→ supervisor spawns a fresh process and restores context viasession/resumeonacp_session_id→ reads the input artifact →Running.
- If
- 24h in
NeedsInputIdlewithout response →Abandoned. - Crash mid-
Running(heartbeat dead, no checkpoint) →Crashed. UI surfaces "Recover or discard" — Recover attemptssession/resumewith the lastacp_session_idif present, otherwise discard worktree.
This shape supports ralph-loop style retry without recreating tasks, AND keeps long human reviews from being penalized for thinking time.
Conventions
TypeScript
strict: trueis on. Honor it. Noanyunless flagged with// FIXME(any):.- Use the
@/...alias for imports rooted atweb/— never deep relative../../... target: "es5"is the template default; do not narrow further. Modern syntax compiles fine.
React / Next.js
- Default to Server Components. Add
"use client"only when a component uses state, effects, browser APIs, or HeroUI components that require client context (most do —<Button>, themed inputs, modals). Match the template:providers.tsx,counter.tsx,theme-switch.tsxare explicit"use client". - Route handlers and server actions live in
app/. Keep secret-touching logic server-side; never ship API keys to client. - Default theme is
dark(<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>inapp/layout.tsx). Use@/lib/themeuseTheme()to toggle.
Styling
- Tailwind 4 utility classes. Use HeroUI
Button,Card,Modal,Input,Navbaretc. instead of hand-rolling. Exception:components/workbench/lifecycle-actions.tsxowns a custom accessible lifecycle dialog matched to the compact workbench surface; preserve that convention unless the whole surface is redesigned. - For variant-based class composition use
tailwind-variants(tv(...)), mirroringcomponents/primitives.ts. - Dark variant is declared in
styles/globals.cssas@custom-variant dark (&:is(.dark *)). Usedark:prefix as usual. - Class merging:
clsxis fine for ad-hoc combos (already in the layout); for variant APIs usetailwind-variants.
Linting (auto-enforced by pnpm lint)
no-console: warn. Use a logger boundary, notconsole.login committed code.react/jsx-sort-props: callbacks last, shorthand first, reserved first. Auto-fixable.import/order: type → builtin → object → external → internal → parent → sibling → index, with blank lines between. Auto-fixable.padding-line-between-statements: blank line beforereturn; blank line afterconst/let/varblocks. Auto-fixable.unused-imports/no-unused-imports: warn. Strip dead imports.react/self-closing-comp: warn.
Files & naming
- React components:
kebab-case.tsxfile, named exports preferred (template uses both — pick named for new components). - Route handlers:
app/api/<segment>/route.ts, exportingGET/POSTetc. - Server-only modules importing
node:*/secrets: keep inlib/and never import from a Client Component.
Data-management page patterns (list / table / admin pages)
Build these pages to this bar the first time — the explicit goal is to not
rewrite page functionality later. Reference impl:
components/admin/users-table.tsx + components/admin/user-edit-modal.tsx +
app/(app)/admin/users/page.tsx.
The admin /settings platform ACP runner catalog is a second instance of
this pattern: components/settings/acp-runners-panel.tsx (view-only table) +
components/settings/acp-runner-modal.tsx (one create | edit modal, also owns
delete) + the DELETE /api/admin/acp-runners/[runnerId] usage-guard. Runner
CRUD is admin-only and the page is reachable from the admin section of
left-rail.tsx. Domain contract: docs/system-analytics/acp-runners.md
- ADR-065. (Filters are intentionally omitted — small N.)
The create | edit modal's key/value rows live in
components/settings/key-value-rows.tsx (ADR-179), extracted from the runner
modal and shared with the platform MCP modal, the project MCP modal, the MCP
binding-overlay dialog and the Studio MCP template editor. Its labels are PROPS
rather than useTranslations calls, so each surface keeps its own i18n
namespace; it exports rowsFromRecord / recordFromRows / duplicateKeyIds.
Reach for it before hand-rolling another Record<string, string> editor.
- Tables are view-only. No inline editing, row dropdowns, or row-level mutate buttons. Rows display data only.
- Edit lives in a popup or a dedicated edit page, never inline. Popup =
modal following
user-edit-modal.tsx; heavier edits get their own route. - Filter on the main fields of the list (e.g. users: name search, role, status, project access) — not just one field.
- Apply changes through ONE aggregating endpoint (partial body in a single
server transaction), never a client-side fan-out of per-field calls + manual
compensation. See
lib/users.tsupdateAdminUser+PATCH /api/admin/users/[userId]. - Full-width list/table pages: drop
mx-auto max-w-[...]; rely on themainpx gutter for air, put amin-w-[...]on the table insideoverflow-x-auto, and use responsivemd:breakpoints (mobile is a target). Forms stay narrow (520–760px) — do not widen forms. - URL-synchronized, deep-linkable state. Filters, tabs, pagination, and
expanded/selected state belong in URL query params (anchors /
searchParams, or nuqs) so views are shareable, survive refresh, and respect back/forward — notuseState-only. - Accessible interactions, built right: modals get focus-trap + initial
focus + focus-restore via refs, Escape-to-close, body scroll lock,
aria-labelledby; form controls get<label>/aria-label; async updates/errors getrole="alert"/aria-live; locale dates useIntl+suppressHydrationWarning. Use refs for focus/measurement, not to mask re-renders. - Role-driven nav + access: admin-only areas live in the left sidebar
(
components/chrome/left-rail.tsx), gated by role; the route still enforcesrequireGlobalRole. The hidden nav item is convenience, never the authorization boundary. - Cursor: enabled
<button>/[role=button]getcursor: pointerapp-wide via the@layer baserule instyles/globals.css— don't add per-buttoncursor-pointer.
UI affordance conventions
A visual affordance is read faster than a word — favour glyphs.
- Prefer icon buttons (or icon + label) over text-only buttons for
actions. A recognizable glyph makes the action scannable; reach for an
icon-only control where space is tight (table/row actions, toolbars) and
icon + label where the action is rare or ambiguous. Icon-only controls
MUST carry an accessible name (
aria-labelortitle); icon + label keeps the visible text as the name. Use@heroicons/react(already the rail/nav icon source) — do not hand-roll SVGs for standard actions. - Destructive actions (delete / remove) use a distinct icon and tone — e.g. a trash glyph with a danger/rose colour — never the same neutral treatment as Edit. A row's action cluster reads left-to-right primary → secondary → destructive.
- Signal success with a green check glyph, not the word "Succeeded".
Outcomes are status, and a green ✓ (or red ✗ / amber warning for
failure) is read at a glance; reserve a word only when the glyph alone is
genuinely ambiguous, and even then keep it short. This applies to inline
result indicators, toasts, and status cells (the readiness dot + the
Enabled
✓/—cell already follow this). - Feedback and confirmation (UI completion batch). Use the single shared feedback provider for completed mutations; user-facing action errors resolve to EN/RU copy and never show a raw code, HTTP status, or server message. Use the shared portaled confirmation for destructive actions. While it is busy, Escape, backdrop, cancel, and repeat confirmation are disabled; focus returns to its trigger after closure. Inline validation and durable remediation stay in their local surfaces.
- Liveness and route resilience (UI completion batch). Async route segments
use the shared token skeleton with
aria-busy; EventSource consumers show a text-plus-color accessible liveness state and reconnect action without changing persisted run state. A route boundary may expose a known error code only as a localized, explicitly labeled diagnostic field. - HeroUI buttons and the app-wide
cursor: pointerbase rule (above) still apply to every affordance, icon or not.
HeroUI integration notes
@heroui/stylesis imported once instyles/globals.css. Don't re-import it in components.- Providers chain in
app/providers.tsxcurrently wraps the local theme provider only. When HeroUI ships a top-level provider for v3 features (toast, modal stack), add it inside<ThemeProvider>. - For new themes/colors prefer extending CSS variables in
globals.cssover forking HeroUI tokens.
Template leftovers to clean (when you next touch them)
LICENSEinsideweb/duplicates the root MIT — keep root only, drop this one when convenient.README.md: HeroUI template README; replace when we have something to say.- Stub routes (
app/about,app/blog,app/docs,app/pricing) andcomponents/counter.tsx: delete as MAIster routes land. app/page.tsxandconfig/site.tsstill reference HeroUI demo content (navItems: Docs/Pricing/Blog/About). ReplacenavItemswith MAIster nav (Portfolio,Projects,Settings) when wiring real pages.
Flag these in PRs but do NOT mass-delete in unrelated commits (surgical-changes rule from root CLAUDE.md).
House rules (carried over from root, repeated for the slice)
- No
chokidar/fs.watch/ polling for state transitions. The live path is supervisor's ACP notifications bridged through SSE; the recovery path is supervisor heartbeat + reconcile-on-startup. - All writes to
.maister/<project-slug>/runs/<run-id>/are atomic (tmp + renameviaatomicWriteJson). Flow / agent may read them mid-write otherwise. - Throw
MaisterErrorwith acodefor known domain failures; UI branches oncode. New codes:EXECUTOR_UNAVAILABLE,FLOW_INSTALL,ACP_PROTOCOL,CHECKPOINT. - Every externally visible ACP event must be committed to the supervisor outbox before publication and projected through canonical Postgres state. Raw logs stay host-private and are exposed only as opaque runtime objects.
- Agent processes live in
../supervisor/, NOT in Next.js. Web tier talks to them vialib/execution-host/only (ADR-166, Implemented);lib/supervisor-client.tsis its lint-fenced transport. - Anything in the root CLAUDE.md "Out of POC scope" list does not get implemented here either.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

