SLOPSHOPPER

dynamia

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

panecommandprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · dynamia
│ ┃ Dynamia env ✕ › fix the failing auth test and add an audit log call │ ┃ Development variables │ ┃ Saved in workspace.toml as plain text. Not ⏺ Read(src/auth.ts) │ ┃ for production secrets. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ No variables yet. Add the first one below. ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Add a variable ⎿ 3 pass, 1 fail │ ┃ NAME : API_URL │ ┃ value: http://localhost:8080 ⏎ add ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ [ Add ] │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ No workspace here. │ › /dynamia-env-panel │ ⎿ dynamia: Dynamia env panel opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Dynamia env
Development variables Saved in workspace.toml as plain text. Not for production secrets. No variables yet. Add the first one below. Add a variable NAME : API_URL value: http://localhost:8080 ⏎ add [ Add ] No workspace here.
Pane · Dynamia logs
Workspace log Latest actions, newest last. Same as: ms-workspace log [ Refresh ]
Pane · Dynamia memory
Workspace memory Decisions, lessons, gotchas and handoffs. Same as: ms-workspace memory No notes yet. Add the first one below. Add a note type: lesson ▾ note: Why we chose X over Y ⏎ add [ Add ]
README

dynamia

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.

  • Let a tool do the plumbing. ms-workspace handles git, tests and bookkeeping, so the model spends tokens thinking, not running git status.
  • Work in batches. Each task gets its own copy of the repos; nothing half-done reaches your main checkout.
  • Guard the dangerous stuff. Hooks block secrets, pushes from agents and runaway output.
  • Spend tokens on purpose. Cheap helper agents search and summarize; reports show where the plan went.
  • Humans publish. Agents commit locally. You decide what ships.

It has 19 skills, 4 read-only helper agents, guard hooks, a standard-library Python tool (ms-workspace) and a status line.

Install

/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).

The workflow

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
  1. Set up the workspace (once per project): /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.
  2. Open a batch (per task): /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.
  3. Work. Optionally give each parallel agent its own worktree (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.
  4. Join the agents' work: /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.
  5. Ship, only when you ask: /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.
  6. Afterwards. 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).

Skills

They need a workspace

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).

SkillWhat it doesWhy it exists
/dynamia:workspace-initCreates or updates the workspace: finds repos, clones missing ones, detects stacks and base branchesThe single entry point; the rest of the flow depends on its config
/dynamia:batchStarts a batch: one worktree per repoIsolation: no task touches a shared checkout
/dynamia:testRuns t0 (changed), t1 (whole batch) or t2 (end to end); triages failuresCheap, cached test levels so the model does not run or read whole suites
/dynamia:integrateMerges agent branches into the batch, reports conflicts, runs t1Makes parallel agents safe
/dynamia:shipPlan, sync, tests, push or PR. Only when you askControlled publishing in one place
/dynamia:dev-envRegisters dev environment variables per repo or for allTests and e2e run with the right variables, kept out of the model's hands
/dynamia:openOpens a batch or an agent's worktrees in VS Code or IntelliJReview the work where you are used to reading code
/dynamia:memorySaves and finds durable notes: decisions, lessons, gotchas, handoffsThe log says what happened; notes say what was learned
/dynamia:recallFull-text search over past sessions, notes and the log"Where did we decide X?" without re-reading transcripts
/dynamia:releaseWhat waits to go from the base branch to qa or main; release notesRelease prep without manual git archaeology
/dynamia:reportPlan usage and pace, where tokens went, slow or flaky testsSpend the plan on purpose. Usage works without a workspace; the per-batch history needs one

They work anywhere

SkillWhat it doesWhy it exists
/dynamia:setupInstalls or removes the status line, permissions and global instructionsThings a plugin cannot configure by itself
/dynamia:gateScans what would be sent for secrets, conflict markers, huge files and debug leftoversRead-only; works in a plain repo too, and ship runs it for you
/dynamia:httpAPI checks with assertions, and local mock servers for third-party APIsTest endpoints and integrations without the real service. Smoke checks before shipping need a workspace
/dynamia:git-statusCompact status card of the current repoA quick, read-only look at where things stand
/dynamia:sysMachine load, disk, memory, ports and leftover processesFind who holds a port or what was left running
/dynamia:tmux-sessionStarts a tmux session with Claude Code and Remote Control in a projectWork from another device
/dynamia:code-tunnelOpens a Cloudflare tunnel to the remote code viewer for the current repoShare a read-only view of the code
/dynamia:powers-installersInstalls, checks and removes agent helpers (uv, rtk, codebase-memory, graphify, headroom); shows the plan firstOne reviewed script instead of ad-hoc installs

Agents

Read-only helpers. The first three run on Haiku so the main model does not spend tokens on reading and summarizing.

AgentUse
dynamia:scoutFinds where code lives: paths and one line each
dynamia:test-triageClassifies a failing run (code, test, flaky, environment, config, build) and points to the file
dynamia:summarizerCondenses diffs, history and logs; drafts commit messages in each repo's style
dynamia:bossOpus architecture review of a batch, diff or design: verdict, blocking and should-fix findings

Guard rails

They work without being invoked.

  • Narrow permissions. Reading and local commands are allowed. ship, undo, clean, anything that deletes or runs from configuration always asks.
  • The guard. Inside a batch, agents cannot 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.
  • Token hooks. 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.
  • Plan usage. ms-statusline records the 5h and weekly usage; skills check it before long work and ask you when it is high.
  • Handoffs. Before compaction and at the end of a session a note is saved, so the next session resumes where this one stopped.

The workspace

<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
  • Inside a git repo it is a one-repo workspace. .dynamia/ is excluded through .git/info/exclude, so the repo stays clean.
  • Outside a repo it is a multi-repo workspace; the clones go next to .dynamia/.
  • The repo's own 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.

More

  • REFERENCE.md: every 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.
  • Tests: python3 -m unittest discover -s tests from this folder (about 45 s).

License

MIT, Copyright (c) 2026 Dynamia Soluciones.

Source 5 files
hooks/register.tsx 175 lines
1import { 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}
175
hooks/devenv.ts 44 lines
1import 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}
44
hooks/panels.tsx 146 lines
1import { 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}
146
hooks/views.ts 29 lines
1export 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}
29
types/index.d.ts 20 lines
1export 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