Multi-repo workspace tools for Claude Code: batches, worktrees, tests, ship, memory, guard hooks and cheap helper agents.

The way we work with Claude Code at Dynamia, packaged as a plugin.
AI coding gets expensive and risky when the model does the boring parts: juggling git across repos, re-running tests, re-reading the same files. This plugin is a workflow that takes those parts away from the model and puts guard rails around the risky ones.
ms-workspace handles git, tests and bookkeeping, so the model spends tokens thinking, not running git status.It has 19 skills, 4 read-only helper agents, guard hooks, a standard-library Python tool (ms-workspace) and a status line.
/plugin marketplace add dynamiatools/claude-plugins
/plugin install dynamia@dynamia-tools
Then, in a new session, run /dynamia:setup once. A plugin cannot set your status line, permissions or ~/.claude/CLAUDE.md, so this skill does it: it shows a preview, asks, makes a backup, adds the status line and permission rules, the global instructions (~/.claude/DYNAMIA.md) and puts ms-workspace in ~/.local/bin. ms-workspace doctor checks the result; /dynamia:setup --remove undoes it. Updates need no new setup (/plugin update dynamia).
once per machine once per project per task when you say so
/setup /workspace-init -> /batch -> work -> /test -> /integrate -> /ship
(workspace.toml) worktrees agents t0 merge + t1 gate, sync,
push or PR
/dynamia:workspace-init. Writes .dynamia/workspace.toml: the repos, their base branches, dependency order and test commands. Why: every other skill reads this one file, so none of them has to guess how to test or where to push./dynamia:batch. Creates one isolated git worktree per repo on a branch int/<batch>. For a small, one-repo task on a clean checkout it can recommend a plain branch instead, and it asks you. Why: your main checkout stays clean, and several tasks can run side by side.ms-workspace agent new), so agents never edit the same files. While you work, /dynamia:test runs t0 (only what changed; a pass is remembered, so repeating it is instant) and triages failures. /dynamia:dev-env registers the environment variables the tests need./dynamia:integrate. Merges the agent branches into int/<batch> in dependency order, names conflicting files, then runs t1 (affected modules plus the ones marked critical). Why: conflicts show up locally, before anything is published./dynamia:ship. Shows the plan (target branch, commits, diffstat), asks, then scans for secrets (/dynamia:gate), merges a moved base branch, re-runs t1 and the end-to-end command and pushes or opens a PR. Never force-pushes, never targets a release branch. Why: publishing is the one step that stays human. The permission prompt of ship is the confirmation.ms-workspace undo <batch> reverts the last ship with a new commit; ms-workspace clean removes finished batches.Day to day, around the flow: /dynamia:report (plan usage, where tokens went, flaky tests), /dynamia:memory and /dynamia:recall (notes and past sessions), /dynamia:release (what waits for qa or main), /dynamia:open (a batch in your IDE).
These read workspace.toml. Without one the tool answers no .dynamia/workspace.toml found; run /dynamia:workspace-init first. A one-repo workspace is enough (it is made inside a git repo and ignored locally).
| Skill | What it does | Why it exists |
|---|---|---|
/dynamia:workspace-init | Creates or updates the workspace: finds repos, clones missing ones, detects stacks and base branches | The single entry point; the rest of the flow depends on its config |
/dynamia:batch | Starts a batch: one worktree per repo | Isolation: no task touches a shared checkout |
/dynamia:test | Runs t0 (changed), t1 (whole batch) or t2 (end to end); triages failures | Cheap, cached test levels so the model does not run or read whole suites |
/dynamia:integrate | Merges agent branches into the batch, reports conflicts, runs t1 | Makes parallel agents safe |
/dynamia:ship | Plan, sync, tests, push or PR. Only when you ask | Controlled publishing in one place |
/dynamia:dev-env | Registers dev environment variables per repo or for all | Tests and e2e run with the right variables, kept out of the model's hands |
/dynamia:open | Opens a batch or an agent's worktrees in VS Code or IntelliJ | Review the work where you are used to reading code |
/dynamia:memory | Saves and finds durable notes: decisions, lessons, gotchas, handoffs | The log says what happened; notes say what was learned |
/dynamia:recall | Full-text search over past sessions, notes and the log | "Where did we decide X?" without re-reading transcripts |
/dynamia:release | What waits to go from the base branch to qa or main; release notes | Release prep without manual git archaeology |
/dynamia:report | Plan usage and pace, where tokens went, slow or flaky tests | Spend the plan on purpose. Usage works without a workspace; the per-batch history needs one |
| Skill | What it does | Why it exists |
|---|---|---|
/dynamia:setup | Installs or removes the status line, permissions and global instructions | Things a plugin cannot configure by itself |
/dynamia:gate | Scans what would be sent for secrets, conflict markers, huge files and debug leftovers | Read-only; works in a plain repo too, and ship runs it for you |
/dynamia:http | API checks with assertions, and local mock servers for third-party APIs | Test endpoints and integrations without the real service. Smoke checks before shipping need a workspace |
/dynamia:git-status | Compact status card of the current repo | A quick, read-only look at where things stand |
/dynamia:sys | Machine load, disk, memory, ports and leftover processes | Find who holds a port or what was left running |
/dynamia:tmux-session | Starts a tmux session with Claude Code and Remote Control in a project | Work from another device |
/dynamia:code-tunnel | Opens a Cloudflare tunnel to the remote code viewer for the current repo | Share a read-only view of the code |
/dynamia:powers-installers | Installs, checks and removes agent helpers (uv, rtk, codebase-memory, graphify, headroom); shows the plan first | One reviewed script instead of ad-hoc installs |
Read-only helpers. The first three run on Haiku so the main model does not spend tokens on reading and summarizing.
| Agent | Use |
|---|---|
dynamia:scout | Finds where code lives: paths and one line each |
dynamia:test-triage | Classifies a failing run (code, test, flaky, environment, config, build) and points to the file |
dynamia:summarizer | Condenses diffs, history and logs; drafts commit messages in each repo's style |
dynamia:boss | Opus architecture review of a batch, diff or design: verdict, blocking and should-fix findings |
They work without being invoked.
ship, undo, clean, anything that deletes or runs from configuration always asks.git push, open or merge PRs, or kill processes, including through bash -c, eval or cd. It fails closed and logs every blocked attempt. It stops mistakes, not a determined adversary.readguard warns before re-reading an unchanged file; outguard cuts a Bash output over 30,000 characters to its first and last lines and saves the rest to a file.ms-statusline records the 5h and weekly usage; skills check it before long work and ask you when it is high.<project>/
├── CLAUDE.md
├── api/ web/ ... the repos (multi-repo workspace)
└── .dynamia/
├── workspace.toml the config
└── workspace/ working-trees/ (the batches), logs, memory; never the repos
.dynamia/ is excluded through .git/info/exclude, so the repo stays clean..dynamia/.CLAUDE.md is never edited unasked.name = "shop"
[repos.framework]
base = "next"
[repos.framework.test]
unit = "mvn -T1C -B test"
[repos.erp]
base = "next"
deps = ["framework"]
critical = ["sources/modulos/modulo-pagos"] # always tested in t1
[e2e]
command = "docker compose -f e2e/docker-compose.yml up --abort-on-container-exit"
repo = "erp"
Every key is in skills/workspace-init/references/workspace.example.toml.
ms-workspace command, the full ship steps, API checks, mock server, Docker and process tools, observability, hooks, how to add a skill, and the roadmap.python3 -m unittest discover -s tests from this folder (about 45 s).MIT, Copyright (c) 2026 Dynamia Soluciones.
hooks/register.tsx 175 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { DevVar, Draft } from '../types'
5import { addArgv, listArgv, parseAdd, parseListing, rmArgv } from './devenv'
6import { panelInput, panelPress, panelSelect, registerPanelCommands, registerPanels } from './panels'
7
8const vars = atom({ plugin: 'dynamia', key: 'vars' } as const, [] as DevVar[])
9const repos = atom({ plugin: 'dynamia', key: 'repos' } as const, [] as string[])
10const draft = atom({ plugin: 'dynamia', key: 'draft' } as const, { name: '', value: '', repo: '' } as Draft)
11const message = atom({ plugin: 'dynamia', key: 'message' } as const, '')
12
13const ALL = 'all'
14
15// One place that calls the command-line tool, which owns the edits to workspace.toml.
16async function tool($: EngineInterface, argv: string[]) {
17 const out = await $.process.run(argv)
18
19 return { ok: out.exitCode === 0, text: (out.stdout || out.stderr).trim() }
20}
21
22async function refresh($: EngineInterface) {
23 const out = await tool($, listArgv($.plugin.root))
24 const listing = out.ok ? parseListing(out.text) : null
25 if (!listing) {
26 await update($, message, () => out.text.split('\n')[0] || 'No workspace here.')
27 await update($, vars, () => [])
28
29 return false
30 }
31 await update($, vars, () => listing.vars)
32 await update($, repos, () => listing.repos)
33
34 return true
35}
36
37async function add($: EngineInterface) {
38 const d = await read($, draft)
39 if (!d.name || d.value === '') {
40 await update($, message, () => 'Type a NAME and a value first.')
41
42 return
43 }
44 const out = await tool($, addArgv($.plugin.root, d))
45 await update($, message, () => out.text.split('\n')[0] ?? '')
46 if (out.ok) await update($, draft, one => ({ ...one, name: '', value: '' }))
47 await refresh($)
48}
49
50export const register: Register = on => {
51 registerPanels(on)
52
53 on('session.start', async ($, e, next) => {
54 await $.command.register({
55 name: 'dynamia-env-panel',
56 description: 'Development variables of the workspace: a panel, or dynamia-env-panel NAME=value [repo], rm NAME [repo], list',
57 })
58 await registerPanelCommands($)
59
60 return next(e)
61 })
62
63 on('command.run', { command: 'dynamia-env-panel' }, async ($, e) => {
64 const args = e.args.trim()
65 const root = $.plugin.root
66 if (args === '') {
67 await refresh($)
68 await $.ui.open({ id: 'dev-env', title: 'Dynamia env', focus: true })
69
70 return { text: 'Dynamia env panel opened.' }
71 }
72 if (args === 'list') {
73 const out = await tool($, [`${root}/bin/ms-workspace`, 'dev-env', 'list'])
74
75 return { text: out.text }
76 }
77 if (args.startsWith('rm ')) {
78 const [, name = '', scope = ''] = args.split(/\s+/)
79 const out = await tool($, rmArgv(root, name, scope))
80 await refresh($)
81
82 return { text: out.text }
83 }
84 await refresh($)
85 const parsed = parseAdd(args, await read($, repos))
86 if (!parsed) return { text: 'Usage: dynamia-env-panel NAME=value [repo] | rm NAME [repo] | list' }
87 const out = await tool($, addArgv(root, parsed))
88 await refresh($)
89
90 return { text: out.text }
91 })
92
93 on('ui.input', async ($, e, next) => {
94 if (e.plugin !== 'dynamia') return next(e)
95 if (e.element === 'name') await update($, draft, one => ({ ...one, name: e.value.trim().toUpperCase() }))
96 if (e.element === 'value') await update($, draft, one => ({ ...one, value: e.value }))
97 if (e.kind === 'submit') await add($)
98 await panelInput($, e.element, e.value, e.kind === 'submit')
99
100 return next(e)
101 })
102
103 on('ui.select', async ($, e, next) => {
104 if (e.plugin === 'dynamia' && e.element === 'repo') {
105 await update($, draft, one => ({ ...one, repo: e.value === ALL ? '' : e.value }))
106 }
107 if (e.plugin === 'dynamia') await panelSelect($, e.element, e.value)
108
109 return next(e)
110 })
111
112 on('ui.press', async ($, e, next) => {
113 if (e.plugin !== 'dynamia') return next(e)
114 if (e.element === 'add') await add($)
115 if (e.element.startsWith('rm:')) {
116 const [, scope = '', name = ''] = e.element.split(':')
117 const out = await tool($, rmArgv($.plugin.root, name, scope))
118 await update($, message, () => out.text.split('\n')[0] ?? '')
119 await refresh($)
120 }
121 await panelPress($, e.element)
122
123 return next(e)
124 })
125
126 on('ui.render', { component: 'Pane', requestId: 'dev-env' }, async ($, e) => {
127 const { Box, Text, Button, Input, Select } = $.ui.resolve(e)
128 const list = await read($, vars)
129 const names = await read($, repos)
130 const d = await read($, draft)
131 const note = await read($, message)
132 const room = Math.max(1, (e.viewport?.rows ?? 24) - 9)
133 const options = [{ value: ALL, label: 'all repos' }, ...names.map(name => ({ value: name, label: name }))]
134
135 const width = Math.min(32, Math.max(8, ...list.map(v => v.name.length)))
136 const scopes = [...new Set(list.map(v => v.scope))]
137 const shown = list.slice(0, room)
138
139 return (
140 <Box flexDirection="column" gap={1}>
141 <Box flexDirection="column">
142 <Text bold>Development variables</Text>
143 <Text dimColor>Saved in workspace.toml as plain text. Not for production secrets.</Text>
144 </Box>
145 {list.length === 0 && <Text dimColor>No variables yet. Add the first one below.</Text>}
146 {scopes.map(scope => (
147 <Box key={scope} flexDirection="column">
148 <Text bold color="cyan">
149 {scope}
150 </Text>
151 {shown
152 .filter(v => v.scope === scope)
153 .map(v => (
154 <Box key={`${scope}:${v.name}`} flexDirection="row" gap={1}>
155 <Button key={`rm:${v.scope}:${v.name}`} label="✕" onPress={() => {}} />
156 <Text>{v.name.padEnd(width)}</Text>
157 <Text dimColor>{v.secret ? '••••••••' : v.value}</Text>
158 </Box>
159 ))}
160 </Box>
161 ))}
162 {list.length > room && <Text dimColor>{list.length - room} more: /dynamia-env-panel list</Text>}
163 <Box flexDirection="column">
164 <Text bold>Add a variable</Text>
165 <Input key="name" label="NAME " placeholder="API_URL" value={d.name} onSubmit={() => {}} />
166 <Input key="value" label="value" placeholder="http://localhost:8080" value={d.value} onSubmit={() => {}} submitLabel="add" />
167 {names.length > 0 && <Select key="repo" label="for " options={options} value={d.repo || ALL} onSelect={() => {}} />}
168 <Button key="add" label="Add" variant="primary" onPress={() => {}} />
169 </Box>
170 {note !== '' && <Text color="yellow">{note}</Text>}
171 </Box>
172 )
173 })
174}
175hooks/devenv.ts 44 lines1import type { DevVar } from '../types'
2
3export type Listing = { repos: string[]; vars: DevVar[] }
4
5// What `ms-workspace dev-env list --json` printed, or null when it was not that (an error text, a missing workspace).
6export function parseListing(stdout: string): Listing | null {
7 try {
8 const data = JSON.parse(stdout) as Partial<Listing>
9 if (!Array.isArray(data.repos) || !Array.isArray(data.vars)) return null
10
11 return { repos: data.repos, vars: data.vars }
12 } catch {
13 return null
14 }
15}
16
17// `NAME=value [repo]` as typed after /dev-env; the repo is the last word when it is a known repo.
18export function parseAdd(args: string, repos: readonly string[]): { name: string; value: string; repo: string } | null {
19 const eq = args.indexOf('=')
20 if (eq < 1) return null
21 const name = args.slice(0, eq).trim()
22 let value = args.slice(eq + 1).trim()
23 let repo = ''
24 const last = value.split(/\s+/).pop() ?? ''
25 if (last && repos.includes(last) && value.includes(' ')) {
26 repo = last
27 value = value.slice(0, value.length - last.length).trim()
28 }
29
30 return { name, value, repo }
31}
32
33export function addArgv(root: string, d: { name: string; value: string; repo: string }): string[] {
34 return [`${root}/bin/ms-workspace`, 'dev-env', 'add', `${d.name}=${d.value}`, ...(d.repo ? ['--repo', d.repo] : [])]
35}
36
37export function rmArgv(root: string, name: string, scope: string): string[] {
38 return [`${root}/bin/ms-workspace`, 'dev-env', 'rm', name, ...(scope && scope !== 'all' ? ['--repo', scope] : [])]
39}
40
41export function listArgv(root: string): string[] {
42 return [`${root}/bin/ms-workspace`, 'dev-env', 'list', '--json']
43}
44hooks/panels.tsx 146 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Note } from './views'
5import { addNoteArgv, forgetArgv, logArgv, notesArgv, parseNotes } from './views'
6
7const logLines = atom({ plugin: 'dynamia', key: 'logLines' } as const, [] as string[])
8const noteRows = atom({ plugin: 'dynamia', key: 'noteRows' } as const, [] as Note[])
9const noteDraft = atom({ plugin: 'dynamia', key: 'noteDraft' } as const, { type: 'lesson', text: '' })
10const panelNote = atom({ plugin: 'dynamia', key: 'panelNote' } as const, '')
11
12const TYPES = ['decision', 'lesson', 'gotcha', 'handoff']
13
14async function run($: EngineInterface, argv: string[]) {
15 const out = await $.process.run(argv)
16
17 return { ok: out.exitCode === 0, text: (out.stdout || out.stderr).trim() }
18}
19
20async function refreshLogs($: EngineInterface, rows: number) {
21 const out = await run($, logArgv($.plugin.root, Math.max(5, rows)))
22 await update($, logLines, () => (out.ok ? out.text.split('\n') : []))
23 await update($, panelNote, () => (out.ok ? '' : out.text.split('\n')[0] ?? ''))
24}
25
26async function refreshNotes($: EngineInterface) {
27 const out = await run($, notesArgv($.plugin.root))
28 await update($, noteRows, () => (out.ok ? parseNotes(out.text) : []))
29 await update($, panelNote, () => (out.ok ? '' : out.text.split('\n')[0] ?? ''))
30}
31
32async function addNote($: EngineInterface) {
33 const d = await read($, noteDraft)
34 if (!d.text.trim()) {
35 await update($, panelNote, () => 'Type the note first.')
36
37 return
38 }
39 const out = await run($, addNoteArgv($.plugin.root, d.type, d.text))
40 await update($, panelNote, () => out.text.split('\n')[0] ?? '')
41 if (out.ok) await update($, noteDraft, one => ({ ...one, text: '' }))
42 await refreshNotes($)
43}
44
45// The ui.input / ui.select / ui.press hooks live in register.tsx (one per event); they call these.
46export async function panelInput($: EngineInterface, element: string, value: string, submit: boolean) {
47 if (element === 'note') await update($, noteDraft, one => ({ ...one, text: value }))
48 if (element === 'note' && submit) await addNote($)
49}
50
51export async function panelSelect($: EngineInterface, element: string, value: string) {
52 if (element === 'note-type') await update($, noteDraft, one => ({ ...one, type: value }))
53}
54
55export async function panelPress($: EngineInterface, element: string) {
56 if (element === 'logs-refresh') await refreshLogs($, 20)
57 if (element === 'note-add') await addNote($)
58 if (element.startsWith('forget:')) {
59 const out = await run($, forgetArgv($.plugin.root, element.slice('forget:'.length)))
60 await update($, panelNote, () => out.text.split('\n')[0] ?? '')
61 await refreshNotes($)
62 }
63}
64
65// Called from the single session.start hook in register.tsx (the engine refuses a second one).
66export async function registerPanelCommands($: EngineInterface) {
67 await $.command.register({ name: 'dynamia-logs-panel', description: 'Recent actions of the workspace log (ms-workspace log)' })
68 await $.command.register({ name: 'dynamia-memory-panel', description: 'Workspace memory notes: browse, add, forget' })
69}
70
71export const registerPanels: Register = on => {
72 on('command.run', { command: 'dynamia-logs-panel' }, async $ => {
73 await refreshLogs($, 20)
74 await $.ui.open({ id: 'dynamia-logs', title: 'Dynamia logs', focus: true })
75
76 return { text: 'Dynamia logs panel opened.' }
77 })
78
79 on('command.run', { command: 'dynamia-memory-panel' }, async $ => {
80 await refreshNotes($)
81 await $.ui.open({ id: 'dynamia-memory', title: 'Dynamia memory', focus: true })
82
83 return { text: 'Dynamia memory panel opened.' }
84 })
85
86 on('ui.render', { component: 'Pane', requestId: 'dynamia-logs' }, async ($, e) => {
87 const { Box, Text, Button } = $.ui.resolve(e)
88 const lines = await read($, logLines)
89 const note = await read($, panelNote)
90 const room = Math.max(3, (e.viewport?.rows ?? 24) - 6)
91
92 return (
93 <Box flexDirection="column" gap={1}>
94 <Box flexDirection="column">
95 <Text bold>Workspace log</Text>
96 <Text dimColor>Latest actions, newest last. Same as: ms-workspace log</Text>
97 </Box>
98 {lines.length === 0 && <Text dimColor>Nothing logged yet.</Text>}
99 <Box flexDirection="column">
100 {lines.slice(-room).map((line, i) => (
101 <Text key={i}>{line}</Text>
102 ))}
103 </Box>
104 <Button key="logs-refresh" label="Refresh" onPress={() => {}} />
105 {note !== '' && <Text color="yellow">{note}</Text>}
106 </Box>
107 )
108 })
109
110 on('ui.render', { component: 'Pane', requestId: 'dynamia-memory' }, async ($, e) => {
111 const { Box, Text, Button, Input, Select } = $.ui.resolve(e)
112 const rows = await read($, noteRows)
113 const d = await read($, noteDraft)
114 const note = await read($, panelNote)
115 const room = Math.max(3, (e.viewport?.rows ?? 24) - 12)
116
117 return (
118 <Box flexDirection="column" gap={1}>
119 <Box flexDirection="column">
120 <Text bold>Workspace memory</Text>
121 <Text dimColor>Decisions, lessons, gotchas and handoffs. Same as: ms-workspace memory</Text>
122 </Box>
123 {rows.length === 0 && <Text dimColor>No notes yet. Add the first one below.</Text>}
124 <Box flexDirection="column">
125 {rows.slice(0, room).map(n => (
126 <Box key={n.name} flexDirection="row" gap={1}>
127 <Button key={`forget:${n.name}`} label="✕" onPress={() => {}} />
128 <Text color="cyan">{n.type.padEnd(8)}</Text>
129 <Text>{n.title}</Text>
130 {n.batch !== '' && <Text dimColor>[{n.batch}]</Text>}
131 </Box>
132 ))}
133 </Box>
134 {rows.length > room && <Text dimColor>{rows.length - room} more: /dynamia-memory-panel needs a taller pane, or ms-workspace memory list</Text>}
135 <Box flexDirection="column">
136 <Text bold>Add a note</Text>
137 <Select key="note-type" label="type" options={TYPES.map(t => ({ value: t, label: t }))} value={d.type} onSelect={() => {}} />
138 <Input key="note" label="note" placeholder="Why we chose X over Y" value={d.text} onSubmit={() => {}} submitLabel="add" />
139 <Button key="note-add" label="Add" variant="primary" onPress={() => {}} />
140 </Box>
141 {note !== '' && <Text color="yellow">{note}</Text>}
142 </Box>
143 )
144 })
145}
146hooks/views.ts 29 lines1export type Note = { type: string; batch: string; title: string; name: string }
2
3// A line of `ms-workspace memory list`: "type batch title (name)"; anything else (like "(no notes)") is dropped.
4export function parseNotes(stdout: string): Note[] {
5 const notes: Note[] = []
6 for (const line of stdout.split('\n')) {
7 const m = /^(\S+)\s+(\S+)\s+(.*?)\s+\(([^()\s]+)\)$/.exec(line.trim())
8 if (m) notes.push({ type: m[1]!, batch: m[2] === '-' ? '' : m[2]!, title: m[3]!, name: m[4]! })
9 }
10
11 return notes
12}
13
14export function logArgv(root: string, limit: number): string[] {
15 return [`${root}/bin/ms-workspace`, 'log', '-n', String(limit)]
16}
17
18export function notesArgv(root: string): string[] {
19 return [`${root}/bin/ms-workspace`, 'memory', 'list', '-n', '200']
20}
21
22export function forgetArgv(root: string, name: string): string[] {
23 return [`${root}/bin/ms-workspace`, 'memory', 'forget', name]
24}
25
26export function addNoteArgv(root: string, type: string, text: string): string[] {
27 return [`${root}/bin/ms-workspace`, 'memory', 'add', '--type', type, text]
28}
29types/index.d.ts 20 lines1export type DevVar = { scope: string; name: string; value: string; secret: boolean }
2export type Draft = { name: string; value: string; repo: string }
3export type Note = { type: string; batch: string; title: string; name: string }
4export type NoteDraft = { type: string; text: string }
5
6declare module 'claude-code' {
7 interface PluginState {
8 dynamia: {
9 vars: DevVar[]
10 repos: string[]
11 draft: Draft
12 message: string
13 logLines: string[]
14 noteRows: Note[]
15 noteDraft: NoteDraft
16 panelNote: string
17 }
18 }
19}
20