Source: https://bestboy.dev/malini/docs # What Malini is > Malini is a free, open source Mac app that runs many Claude Code agents side by side, each in its own git worktree, and takes each one to a merged pull request. Malini is a desktop app for the Mac. Each task you start is a **workstream**: its own git worktree, its own branch, and its own Claude Code chats. Agents write the code, and Malini owns git: it commits, opens the pull request, fixes red checks, answers reviews, and merges. Malini is free and open source under AGPL-3.0. There is no Malini account, no subscription, and no Malini server. ## How it works 1. **Start a task.** Each task is a workstream: its own git worktree, its own branch, its own chats. Start one or start a hundred. 2. **Keep it moving.** Queue prompts, run jobs, plan first if you like. Agents write code, and Malini owns git. 3. **Press Draft, then Merge.** Malini commits, opens the pull request, fixes red checks, addresses reviews, and merges once GitHub allows. ## What is in the box - [Workstreams](/malini/docs/workstreams.md): one git worktree and branch per task, and a merge queue that takes it to merged. - [Chat](/malini/docs/chat.md): queue or steer prompts, plan mode, model and effort per prompt, undo, edit, and fork. - [Jobs](/malini/docs/jobs.md): agent work you reuse by name, with five built in. - [Routines](/malini/docs/routines.md): jobs that run in order when something happens. - [Agent tools](/malini/docs/agent-tools.md): the browser, jobs, routines, and events, as tools the agent can call. - [Extensions](/malini/docs/extensions.md): the inspector, its panels, commands, settings, and workflows. - [Updates and compatibility](/malini/docs/updates.md): how Malini updates itself and keeps your jobs and routines working. ## Where your code goes Your code leaves your Mac only through your own Claude Code, to Anthropic, and the GitHub CLI, to GitHub. Malini makes two calls of its own: update checks to GitHub Releases, and anonymous usage data to PostHog. Usage data says which features are used and which errors happen, and never contains your code, prompts, paths, or keys. Turn it off under Settings, Privacy, Share usage data. ## For agents reading this Every page of these docs is plain markdown at its URL plus `.md`. [llms.txt](https://bestboy.dev/malini/llms.txt) lists every page, and [llms-full.txt](https://bestboy.dev/malini/llms-full.txt) holds all of them in one file. --- Source: https://bestboy.dev/malini/docs/install # Install > Download Malini for macOS, connect Claude Code and GitHub in setup, and add your first repository. ## Requirements - macOS 13 or later on Apple silicon. - git. Malini ships the GitHub CLI, and setup installs Claude Code if you don't have it. ## Install 1. [Download Malini](https://github.com/bestboyhq/malini/releases/latest/download/malini-arm64.dmg) and drag it to Applications. 2. Open it. 3. Add a repository. Your first workstream is ready. ## First launch - **Both accounts, side by side.** Setup connects Claude Code and GitHub one at a time: **Install** and **Sign in** for Claude Code, and a one-time code in your browser for GitHub. Malini ships the GitHub CLI, so you don't install it or run `gh auth login` yourself, and a `gh` you installed still comes first. - **Your jobs, introduced.** The built-in jobs are one slash away from any chat. - **Then a repository.** Clone one from GitHub, with your recent repositories one search away, or open a folder on your Mac. ## Updates Malini updates itself. Updates download in the background and install when you quit, or right away with **Restart to Update** in the app menu. See [Updates and compatibility](/malini/docs/updates.md). ## Build from source You need macOS on Apple silicon and Node 24. ```sh git clone https://github.com/bestboyhq/malini && cd malini corepack enable pnpm pnpm install pnpm dev ``` `pnpm dev` starts the app with hot reload. `pnpm dist` builds an unsigned `.app` under `apps/malini/release/`. --- Source: https://bestboy.dev/malini/docs/updates # Updates and compatibility > How Malini updates itself, and how the built-in Check compatibility job keeps your jobs and routines working when Malini's events change. ## Updates Malini checks GitHub Releases for a new version on launch, every hour, and when your Mac wakes. Updates download in the background and install when you quit. **Restart to Update** in the app menu installs the newest release right away, and **Check for Updates…** checks now. Malini must run from an Applications folder to update itself; started anywhere else, it offers to move itself there. Every merged `feat`, `fix`, or `perf` change ships as a release, so updates are small and frequent. ## Compatibility [Routines](/malini/docs/routines.md) can trigger on Malini's events, and jobs can rely on them. An update can add, remove, or reshape an event. After an update, Malini compares its events with the ones the version before it had. When something changed, it emits `app:updated` with the old and new version and the change: the channels added, the channels removed, and each channel whose payload changed, with its payload type before and after. Then Malini runs the built-in **Check compatibility** job with that change as context. The job: 1. reads the current events with `events_list`, and your jobs and routines with `jobs_list` and `routines_list`, 2. finds every routine whose trigger names a changed or removed channel, or filters on a field that moved, and every job whose instructions rely on one, 3. fixes each one with `jobs_save` or `routines_save`, keeping the rest exactly as you wrote it, 4. shows you each edit and waits for your approval before it saves, 5. never deletes a job or a routine, and leaves alone one that no new event can replace, 6. ends with a summary of what it changed and what still needs you. Check compatibility runs in your newest workstream. Like every built-in job, you can edit its instructions, and **Reset** brings back the original. `app:updated` is an event like any other, so a routine can also run `on app:updated`. ## Events reference The [events reference](/malini/docs/reference/events.md) lists every channel and its payload for the version of Malini these docs describe. --- Source: https://bestboy.dev/malini/docs/workstreams # Workstreams > A workstream is one task in Malini - its own git worktree, branch, and chats - and the merge queue that takes it from Draft to a merged pull request. ## One worktree per task The **+** next to a repository starts a workstream. Every workstream is its own **git worktree** on a branch named `malini/`, branched from a private clone of the repository. Malini installs dependencies for you from the lockfile: pnpm, npm, yarn, or bun. Two agents never edit one checkout, so they never clobber each other. ## Malini owns git The agent bridge refuses to commit, push, rebase, reset, or run `gh pr create`. Agents write code, and Malini writes the history: it commits, pushes, and opens the pull request when you ask it to. ## The sidebar - Lines added and removed, unread marks, queued prompts, and pull request status on every workstream. - Drag to reorder, or use **⌥↑** and **⌥↓**. - Hide a repository and keep its workstreams. Clone or open it again and it comes back. - **Recently archived**, at the bottom of the sidebar, lists archived workstreams, newest first. Drag its top edge to make it taller, or click **Show all**. Click one to restore it, or open the pull request it merged. Archiving stops the commands its agents left running in the background. - Connect another repository from the button at the bottom of the sidebar. - Open a workstream in your editor or reveal it in Finder. - Open a repository on GitHub from its right-click menu, the repository list, or its page. - Each workstream holds many chats, as tabs, named for you. **⌘W** closes one and **⌘K** finds the ones that don't fit. A repository opens on its **scratchpad**, where you keep ideas. Any paragraph moves into a new workstream. ## From prompt to merged - **Draft** opens a draft pull request. The agent writes the commit subject and the pull request title in your repository's convention, and Malini commits, pushes, and opens it. - **Merge** queues it: push, mark ready, update from main, wait for checks and review, and merge with the repository's own merge method. The workstream archives itself when it lands and moves to **Recently archived**. - **A dev server doesn't hold it.** The queue merges past a command the agent left running in the background, and archiving stops it. - **While it waits, it works.** The queue runs the **Fix checks**, **Update from main**, and **Address review** [jobs](/malini/docs/jobs.md) as needed, then replies to and resolves the review threads it fixed. - **Blockers clear themselves.** When main moves or a reviewer requests changes, **Update from main** or **Address review** runs on its own, and the top bar says **Clearing blockers**. Each review is addressed once, then Malini waits for the reviewer. - **Needs you, only when it does.** A reviewer asks something only you can answer, a fix gave up, GitHub blocks the merge, or your GitHub sign-in expired. The top bar names the reason, and the merge picks up again on its own once it is resolved. - **Exactly the commit that passed.** Malini merges with `--match-head-commit`. - **Closed on GitHub?** **Reopen** brings the same pull request back, with its number and reviews, or **Archive** puts the workstream away. Your open `malini/*` pull requests open as workstreams on every Mac you use Malini on. --- Source: https://bestboy.dev/malini/docs/chat # Chat > How a Malini chat runs Claude Code - queue or steer prompts, plan mode, agent access, model and effort per prompt, undo, edit, and fork. Every chat runs your own Claude Code, through the Claude Agent SDK, inside the chat's workstream. ## Queue or steer - **↵** sends a prompt, or queues it after the current run. Edit, reorder, or delete what is queued. - **⌘↵** steers: it interrupts the turn and sends the prompt now, while background commands keep running. A run stays open while its background commands and subagents run, and you can stop each one. ## Plan first Plan mode writes the plan, then **Implement plan** starts the work, with a model of its own if you like. ## Model and effort per prompt Pick Fable, Opus, Sonnet, or Haiku, and an effort from Low to Max, for every prompt. ## Agent access - **Auto**, the default, uses Claude Code's auto mode. - **Sandboxed** edits the workstream and runs commands in a sandbox, and asks for anything else. - **Full access** never asks. When the agent asks, approve once, or always for the chat or the workstream. ## Reports Every turn ends with a report: **Done**, **Done with something deferred**, or **Waiting on you**, with what changed, what was verified, and the commit and pull request titles. Reports run from a screenshot and one line to the full write-up. A **Your turn** list, unread marks, a chime, and a notification tell you which workstream needs you. ## Context - **@** mentions a file, and you can attach or paste images and files. - Thinking shows inline, step by step, with a diff for every file the chat changed. - The context window breakdown, your plan usage (five-hour and weekly), and MCP server status are one click from the composer. ## Undo, edit, fork Every run can be undone. Edit an earlier message to resend from there, or fork to a new chat while the agent is still busy. Runs that a restart cut short pick up where they left off the next time Malini opens. ## Skills A chat loads every Claude Code skill that is on. Settings, Skills ranks your skills and plugins by how often Claude Code used them, and turns them off in Claude Code's own settings. A [job](/malini/docs/jobs.md) can narrow its skills to a preset. ## Tools In a Malini chat the agent also has Malini's own tools: a browser it shares with you, and your jobs, routines, and events. See [Agent tools](/malini/docs/agent-tools.md). ## Keys | Key | | | --------------- | ------------------------------------------------------ | | **↵** | Send, or queue after the current run | | **⌘↵** | Steer now, interrupting the run | | **@** | Mention a file | | **/** | Run a job | | **⌘W** | Close the chat tab | | **⌘K** | Find a chat tab that doesn't fit | | **⌥↑** / **⌥↓** | Move a repository, a workstream, or an inspector panel | --- Source: https://bestboy.dev/malini/docs/jobs # Jobs > A Malini job is agent work you reuse by name - instructions, a model, an effort, and a skill preset - run in a workstream or a new one, with a draft or a merge at the end if you like. You never wanted skills. What you wanted was the outcome: checks green, branch up to date, review answered. A job is an outcome you ask for by name. ## What a job is A job has: - a **name**, - **instructions**, the prompt the agent runs, - a **model** and an **effort**, - a **skill preset**, so the job loads only the skills it needs, - where it **runs**: in this workstream, or in a new one, - what happens when it **finishes**: nothing, a draft pull request, or a queued merge. Jobs live on the **Jobs** page. ## Run a job - Type **/** in the composer to run a job from a chat. Its result lands in that chat. - Run it from the Jobs page, in a workstream or a new one. - Let a [routine](/malini/docs/routines.md) run it when something happens. - Ask the agent: in a Malini chat it can list, save, and run jobs with its [tools](/malini/docs/agent-tools.md). The **Job center** shows what is running and what just finished. Settings decides how many jobs run at once. ## Built-in jobs | Job | What it does | | ----------------------- | ------------------------------------------------------------------------------------------------------------- | | **Name chats** | Names a chat from its first message. | | **Fix checks** | Fixes a pull request's failing checks. The merge queue runs it. | | **Update from main** | Brings the branch up to date with its base. The merge queue runs it. | | **Address review** | Answers review comments. The merge queue runs it. | | **Check compatibility** | After an update, fixes jobs and routines that rely on changed events. See [Updates](/malini/docs/updates.md). | | **Update docs** | Brings a repository's user-facing docs up to date with its code, in a new workstream, as a draft. | The built-in jobs are yours to edit. **Reset** brings back the original. ## Suggested jobs Malini notices the prompts you keep repeating and offers to turn them into a job and a routine. --- Source: https://bestboy.dev/malini/docs/routines # Routines > A Malini routine runs jobs, extension commands, and workflows in order when a trigger fires - when you run it, when a workstream is created, when a frontend resource becomes ready, or on any Malini event. A routine is a list of steps that run in order when its trigger fires. A step is a [job](/malini/docs/jobs.md), an extension command, or an extension workflow, and a routine holds up to ten steps. ## Triggers A routine's trigger is a short phrase, its **when**. | When | Fires | | ------------------------------------------------ | --------------------------------------------------------- | | `when I run it` | Only when you run the routine. | | `when a new workstream is created` | When a new workstream is created. | | `when a frontend resource becomes ready` | When a frontend resource of the workstream becomes ready. | | `when a frontend Docker container becomes ready` | When a frontend Docker resource becomes ready. | | `on ` | When Malini emits that event. | ### Event triggers Any Malini event can trigger a routine: ```text on [where is | is set [and ...]] ``` - `` is an event channel, `:`, such as `pull-requests:ship-changed`. The [events reference](/malini/docs/reference/events.md) lists every channel and its payload. - `where` filters on the event's payload. `` is a field name, with dots for nested fields. - ` is ` matches when the field equals the value. - ` is set` matches when the field is present and not null. - Join conditions with `and`; all of them must match. ```text on pull-requests:ship-changed where merged is set ``` This routine runs when a pull request Malini ships is merged. A routine with an event trigger runs in the background, so every step must be a job. Each job gets the event channel and its payload as context, and runs in the workstream the event names. The same event fires a routine once. An unknown channel or a malformed condition is refused when you save the routine, with the channels Malini knows. ## Trust is earned A routine has one of three states. 1. **Draft**: it never runs on its own. 2. **Candidate**: every run its trigger starts waits for you to confirm it. 3. **Routine**: it runs unattended. You promote a routine from one state to the next, and demote it any time. A routine the agent creates in a chat, and a routine Malini suggests from your repeated prompts, starts as a draft. ## Make one - On the Jobs page, write the steps and the when. - Accept a suggestion: Malini notices the prompts you keep repeating and offers a job and a routine. - Ask the agent in a Malini chat, which saves it with `routines_save` (see [Agent tools](/malini/docs/agent-tools.md)). --- Source: https://bestboy.dev/malini/docs/agent-tools # Agent tools > The tools the agent gets in a Malini chat - a browser it shares with you, and your jobs, routines, and events - and how writes ask for your approval. In a Malini chat, the agent has Claude Code's own tools plus Malini's. Malini's tools reach Claude Code through an MCP server named `malini`, so their full names start with `mcp__malini__`. ## Browser The agent drives the same browser tabs you see in the inspector, and the tabs it opens show up beside yours. | Tool | What it does | | -------------------- | ---------------------------------------------- | | `browser_open` | Opens a web page or a file from the worktree. | | `browser_screenshot` | Captures the open page. | | `browser_click` | Clicks the first element a CSS selector finds. | | `browser_type` | Types into a field, optionally pressing Enter. | | `browser_back` | Goes back one page. | | `browser_forward` | Goes forward one page. | | `browser_reload` | Reloads the page. | You can also pick an element on a page, and it lands in your prompt. ## Jobs, routines, and events | Tool | What it does | | --------------- | ----------------------------------------------------------------------------------------------------- | | `jobs_list` | Lists your [jobs](/malini/docs/jobs.md): id, name, where they run, what they ship, model, and effort. | | `jobs_save` | Creates a job, or changes one by id. | | `jobs_run` | Starts a job now, with optional extra context. The run shows in the chat. | | `routines_list` | Lists your [routines](/malini/docs/routines.md): id, state, label, when, and steps. | | `routines_save` | Creates a routine, or changes one by id. | | `events_list` | Lists every event Malini emits: its channel, what it means, and its payload type. | `events_list` returns the same data as the [events reference](/malini/docs/reference/events.md). The agent uses it to write an `on ` trigger and its `where` clause. ## Writes ask first - The read tools, `jobs_list`, `routines_list`, and `events_list`, run without asking. - Every `jobs_save`, `jobs_run`, and `routines_save` asks for your approval in the chat. - A routine the agent creates starts as a **draft**: it never runs on its own until you promote it. ## Example > Every time one of my pull requests merges, update the docs. The agent can read `events_list`, find `pull-requests:ship-changed`, and save a draft routine: ```json { "label": "Docs after merge", "when": "on pull-requests:ship-changed where merged is set", "steps": ["Update docs"] } ``` You approve the save, then promote the routine when you trust it. --- Source: https://bestboy.dev/malini/docs/extensions # Extensions > Malini's inspector is built from extensions - panels, commands, settings, and workflows declared in a manifest, on an MIT licensed extension API. ## The inspector is built from extensions The inspector sits next to the chat. The built-in **Repository** extension gives you **Files**: changed files with their diffs, the full tree, file tabs, and todos. Browser tabs sit there too. ## What an extension contributes An extension declares its contributions in `manifest.json`: - **panels** in the inspector, - **commands**, - **settings**, - **workflows**. A [routine](/malini/docs/routines.md) can run an extension's commands and workflows as steps. ## The extension API An extension reaches the workstream and the repository through `@malini/extension-api`, in [`packages/extension-api`](https://github.com/bestboyhq/malini/tree/main/packages/extension-api). The API is MIT licensed, so an extension can use any license. ```ts import type { ExtensionModule } from '@malini/extension-api'; const extension: ExtensionModule = { async activate(api) { await api.notifications.show({ title: api.manifest.name, body: 'Active', level: 'info' }); }, }; export default extension; ``` Extensions declare `apiVersion: 1` in `manifest.json`, and Malini refuses an unsupported manifest before it activates the extension. The manifest's JSON schema is `@malini/extension-api/manifest.schema.json`, and `createTestHost` from `@malini/extension-api/test` runs an extension in tests. The extension API package is private for now and lives inside the Malini repository. --- Source: https://bestboy.dev/malini/docs/reference/events # Events > Every event Malini emits, when it fires, and the TypeScript shape of its payload. Malini announces everything that happens on the platform as an event on a `:` channel. Routines can trigger on any of them, and agents inside Malini can list them with the `events_list` tool. This page is generated from the contract in `apps/malini/src/contract/events.ts`, so it always matches the app. ## `chat:agent-event` Fires for every event an agent run streams in a chat, such as text, tool calls, approvals and the end of the run, in sequence order. Payload: `LiveEventEnvelope` ```ts interface AgentEventEnvelope { sessionId: string; runId: string; seq: number; event: Record; } type LiveEventEnvelope = AgentEventEnvelope & { ephemeral?: true }; ``` ## `chat:run-changes-captured` Fires after an agent run that changed files finishes and Malini has recorded the diff of that run. Payload: `RunChangesCapturedPayload` ```ts interface RunChangesCapturedPayload { sessionId: string; runId: string; } ``` ## `chat:checkpoint-restored` Fires after a workstream's files are restored to a chat checkpoint, rewinding the changes made after it. Payload: `CheckpointRestoredPayload` ```ts interface CheckpointRestoredPayload { workstreamId: string; checkpointId: string; sessionId: string; } ``` ## `chat:session-renamed` Fires when a chat gets a new name, from the user or from the Name chats job. Payload: `SessionRenamedPayload` ```ts interface SessionRenamedPayload { workstreamId: string; sessionId: string; name: string; } ``` ## `repositories:workstream-created` Fires when a new workstream exists: its git worktree is checked out and it is ready for chats. Payload: `WorktreeAddedPayload` ```ts interface GitProjectRecord { id: string; name: string; repoPath: string; defaultBranch: string; createdAt: string; } interface GitWorkstreamRecord { id: string; projectId: string; name: string; path: string; branch: string; baseBranch: string; status: string; createdAt: string; } interface WorktreeAddedPayload { workstreamId: string; project: GitProjectRecord; workstream: GitWorkstreamRecord; } ``` ## `repositories:workstream-removed` Fires when a workstream leaves the sidebar, with `archived` set when it was archived and `deleted` set when it was deleted. Payload: `WorktreeRemovedPayload` ```ts interface WorktreeRemovedPayload { workstreamId: string; worktreePath: string; archived?: true; deleted?: true; } ``` ## `repositories:workstream-files-changed` Fires when files change in a workstream's checkout, with the changed paths, or `null` paths when Malini could not tell which files changed. Payload: `WorkstreamFilesChangedPayload` ```ts interface WorkstreamFilesChangedPayload { workstreamId: string; changedAt: string; paths: string[] | null; } ``` ## `repositories:workstream-renamed` Fires when a workstream gets a new name. Payload: `WorkstreamRenamedPayload` ```ts interface WorkstreamRenamedPayload { workstreamId: string; name: string; } ``` ## `repositories:workstream-install-status` Fires as a new workstream's dependency install moves through its states: running, succeeded, failed, skipped or aborted. Payload: `WorkstreamInstallStatusPayload` ```ts type InstallStatus = 'running' | 'succeeded' | 'failed' | 'skipped' | 'aborted'; type InstallSkipReason = 'already-installed' | 'no-manifest' | 'unrecognized-project'; interface WorkstreamInstallStatusPayload { type: 'repositories:workstream-install-status'; workstreamId: string; status: InstallStatus; command?: string; reason?: InstallSkipReason; detail?: string; durationMs?: number; exitCode?: number; } ``` ## `repositories:clone-progress` Fires repeatedly while Malini clones a repository, with the stage and the fraction done, and once more when it is done. Payload: `CloneProgressPayload` ```ts interface CloneProgressPayload { repo_id: string; stage: 'fetch' | 'checkout' | 'done'; fraction: number; } ``` ## `pull-requests:local-base-synced` Fires after Malini merges a pull request and tries to bring the repository's local base branch up to date, with the outcome. Payload: `LocalBaseSyncedPayload` ```ts interface LocalBaseSyncedPayload { workstreamId: string | null; checkout: string; branch: string; outcome: 'advanced' | 'diverged' | 'failed'; detail?: string; } ``` ## `pull-requests:ship-changed` Fires whenever a workstream's shipping state changes: its pull request phase, the merge queue step, or a reason it needs you. Payload: `ShipChangedEvent` ```ts type ShipPhase = 'local' | 'draft' | 'queued' | 'needs-you'; type ShipStep = | 'waiting-for-agent' | 'opening-draft' | 'pushing' | 'marking-ready' | 'updating-branch' | 'updating-from-main' | 'fixing-checks' | 'addressing-review' | 'waiting-for-checks' | 'waiting-for-review' | 'waiting-for-github' | 'merging' | 'archiving'; type ShipNeedsYouReason = | 'reply-to-reviewer' | 'fix-gave-up' | 'merge-not-permitted' | 'pull-request-closed' | 'github-reconnect' | 'operation-in-progress'; type ShipNeedsYou = Readonly<{ reason: ShipNeedsYouReason; detail: string; jobRunId: string | null; }>; type ShipStateRecord = Readonly<{ workstreamId: string; phase: ShipPhase; step: ShipStep | null; pullRequestNumber: number | null; pullRequestUrl: string | null; needsYou: ShipNeedsYou | null; queuedAt: string | null; attempts: number; activeJobRunId: string | null; lastError: string | null; nextAttemptAt: string | null; updatedAt: string; }>; type ShipMerged = Readonly<{ pullRequestNumber: number; pullRequestUrl: string | null }>; type ShipChangedEvent = Readonly<{ workstreamId: string; state: ShipStateRecord | null; merged?: ShipMerged; }>; ``` ## `jobs:run-changed` Fires whenever a job run is queued, starts, needs you, or finishes as succeeded, failed or cancelled. Payload: `JobRunChangedEvent` ```ts type JobRunOrigin = 'merge-queue' | 'manual' | 'routine'; type JobRunStatus = 'queued' | 'running' | 'needs-you' | 'succeeded' | 'failed' | 'cancelled'; type JobRunRecord = Readonly<{ id: string; definitionId: string; title: string; workstreamId: string | null; repositoryId: string | null; sessionId: string | null; agentRunId: string | null; origin: JobRunOrigin; status: JobRunStatus; detail: string | null; chatSessionId: string | null; chatAfterRunId: string | null; createdAt: string; startedAt: string | null; finishedAt: string | null; }>; type JobRunChangedEvent = Readonly<{ run: JobRunRecord }>; ``` ## `jobs:definition-changed` Fires when a job is created, edited, reset to its defaults or deleted; `definition` is `null` after a delete. Payload: `JobDefinitionChangedEvent` ```ts type AgentReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'; type JobRunsIn = 'workstream' | 'new-workstream'; type JobOutput = 'none' | 'draft' | 'merge'; type JobBuiltin = | 'name-chats' | 'name-services' | 'fix-checks' | 'update-from-main' | 'address-review' | 'check-compatibility' | 'update-docs'; type JobDefinitionRecord = Readonly<{ id: string; name: string; instructions: string; model: string; effort: AgentReasoningEffort; runsIn: JobRunsIn; output: JobOutput; skillPresetId: string | null; builtin: JobBuiltin | null; createdAt: string; updatedAt: string; }>; type JobDefinitionChangedEvent = Readonly<{ definitionId: string; definition: JobDefinitionRecord | null; }>; ``` ## `routines:suggested` Fires when Malini notices a prompt you keep repeating and suggests turning it into a routine. Payload: `RoutineSuggestedEvent` ```ts type RoutineSuggestionStatus = 'open' | 'dismissed' | 'accepted'; type RoutinePromptEvidence = Readonly<{ kind: 'prompt'; text: string; runId?: string; sessionId?: string; workstreamId?: string; submittedAt?: string; }>; type RoutineSuggestionRecord = Readonly<{ id: string; clusterKey: string; status: RoutineSuggestionStatus; evidence: readonly RoutinePromptEvidence[]; createdAt: string; updatedAt: string; }>; type RoutineSuggestedEvent = Readonly<{ suggestion: RoutineSuggestionRecord; }>; ``` ## `routines:changed` Fires when a routine is created, updated, promoted, demoted or deleted; `routine` is `null` after a delete. Payload: `RoutineChangedEvent` ```ts type RoutineChange = 'created' | 'updated' | 'promoted' | 'demoted' | 'deleted'; type RoutineStatus = 'draft' | 'candidate' | 'routine'; type RoutineOrigin = 'user' | 'suggested'; type RoutineJsonValue = | string | number | boolean | null | readonly RoutineJsonValue[] | { readonly [key: string]: RoutineJsonValue }; type RoutineWorkflowRun = Readonly<{ workflow: string; input: Readonly>; }>; type RoutineCommandRun = Readonly<{ command: string; args: readonly RoutineJsonValue[]; }>; type RoutineJobRun = Readonly<{ job: string }>; type RoutineRun = RoutineWorkflowRun | RoutineCommandRun | RoutineJobRun; type RoutinePromptEvidence = Readonly<{ kind: 'prompt'; text: string; runId?: string; sessionId?: string; workstreamId?: string; submittedAt?: string; }>; type RoutineRunEvidence = Readonly<{ kind: 'run'; runId: string; sessionId?: string; workstreamId?: string; summary?: string; }>; type RoutineEvidence = RoutinePromptEvidence | RoutineRunEvidence; type RoutineRecord = Readonly<{ id: string; status: RoutineStatus; origin: RoutineOrigin; label: string; when: string; steps: readonly RoutineRun[]; evidence: readonly RoutineEvidence[]; createdAt: string; updatedAt: string; }>; type RoutineChangedEvent = Readonly<{ routineId: string; change: RoutineChange; routine: RoutineRecord | null; }>; ``` ## `routines:gated-run-changed` Fires when a routine run waits for your confirmation, and again when you confirm or reject it. Payload: `RoutineGatedRunChangedEvent` ```ts type RoutineJsonValue = | string | number | boolean | null | readonly RoutineJsonValue[] | { readonly [key: string]: RoutineJsonValue }; type RoutineGatedRunState = 'pending' | 'confirmed' | 'rejected'; type RoutineGatedRunRecord = Readonly<{ id: string; routineId: string; workstreamId: string; runKey: string; event: string; payload: RoutineJsonValue; state: RoutineGatedRunState; createdAt: string; decidedAt: string | null; }>; type RoutineGatedRunChangedEvent = Readonly<{ gatedRun: RoutineGatedRunRecord; }>; ``` ## `app:close-requested` Fires when you close the Malini window, so Malini can show what is still running and ask before it quits. Payload: `EmptyEventPayload` ```ts type EmptyEventPayload = Readonly>; ``` ## `app:scale-changed` Fires when the interface scale or the display the window sits on changes, so layouts can measure again. Payload: `EmptyEventPayload` ```ts type EmptyEventPayload = Readonly>; ``` ## `app:notification-opened` Fires when you click a Malini notification, with the chat it points to. Payload: `NotificationTarget` ```ts type NotificationTarget = Readonly<{ workstreamId: string; sessionId: string }>; ``` ## `app:streaming-changed` Fires when streamer mode turns on or off, because a screen recording or streaming app started or stopped. Payload: `StreamingChangedPayload` ```ts type StreamingChangedPayload = Readonly<{ streaming: boolean }>; ``` ## `app:updated` Fires once at startup after Malini updated to a new version whose events changed, with the old and new version and the events added, removed or whose payload changed. Payload: `AppUpdatedPayload` ```ts type ContractPayloadChange = Readonly<{ channel: string; before: string; after: string }>; type ContractChanges = Readonly<{ added: readonly string[]; removed: readonly string[]; changed: readonly ContractPayloadChange[]; }>; type AppUpdatedPayload = Readonly<{ from: string; to: string; changes: ContractChanges }>; ``` ## `app:update-staged` Fires when a new Malini version has downloaded and installs on the next restart, with that version. Payload: `UpdateStagedPayload` ```ts type UpdateStagedPayload = Readonly<{ version: string }>; ``` ## `browser:state-changed` Fires whenever a workstream's browser tab changes its page, title, icon, loading state or history. Payload: `BrowserState` ```ts type BrowserState = Readonly<{ workstreamId: string; tabId: string; url: string; title: string; favicon: string | null; loading: boolean; canGoBack: boolean; canGoForward: boolean; }>; ``` ## `browser:tab-opened` Fires when a new browser tab opens in a workstream, for example when an agent opens a page. Payload: `BrowserTabArgs` ```ts type BrowserTabArgs = Readonly<{ workstreamId: string; tabId: string }>; ``` ## `stats:resources-sampled` Fires every few seconds with a sample of the memory and CPU that Malini, its agents, browser tabs and containers use. Payload: `ResourceSample` ```ts type ResourceKind = 'malini' | 'browser' | 'agents' | 'tools' | 'docker'; type ResourceUsage = Readonly<{ kind: ResourceKind; memoryBytes: number; cpuPercent: number; processes: number; }>; type ResourceSample = Readonly<{ sampledAt: number; memoryBytes: number; cpuPercent: number; processes: number; usage: readonly ResourceUsage[]; }>; ``` ## `stats:workstream-running-changed` Fires when what keeps running in any workstream changes, with every workstream that still runs background services, tasks or containers. Payload: `readonly WorkstreamRunning[]` ```ts type RunningItemOrigin = 'agent' | 'docker'; type RunningItemKind = 'service' | 'task'; type RunningItem = Readonly<{ id: string; origin: RunningItemOrigin; name: string; kind: RunningItemKind; detail: string; group: string | null; startedAt: number; ports: readonly number[]; }>; type WorkstreamTurn = Readonly<{ endedAt: number; outcome: string | null; need: string | null; }>; type WorkstreamRunning = Readonly<{ workstreamId: string; items: readonly RunningItem[]; behind: readonly string[]; turn: WorkstreamTurn | null; }>; ```