Local-first AI workspace desktop app for Korean knowledge/document operations.
Tauri 2 + Rust + React 19 + TypeScript. The current version is defined by the
synced app manifests (package.json, src-tauri/tauri.conf.json,
src-tauri/Cargo.toml, and src-tauri/maru-cli/Cargo.toml).
Maru is the author SSOT for a single user's ~/workspace/work/ — it edits
markdown with byte-identical frontmatter, ingests an inbox, runs bundled Claude
Code skills, drives Korean document (HWPX/DOCX/PDF) operations, and visualizes
the vault as a knowledge graph. Releases before v0.3.0 shipped under the name
Anchor; the M0 rename (kr.maru.desktop, ~/.maru/) landed in v0.3.0.
| Phase | State | Outcome |
|---|---|---|
| 0 — Hardening | ✅ shipped | Open existing workspaces safely. Frontmatter byte-identical round-trip. Multi-workspace registry. ko/en parity. |
| 0.5 — UI polish | ✅ shipped | Topbar, sidebar with type filters + recents, command palette (⌘K), Pretendard Korean typography, light/dark. |
| 1A — Killer feature MVP | ✅ shipped | Doc-selection reliability, frontmatter inline edit (InspectorPane), wikilink autocomplete (Korean IME-aware) + click-to-navigate, typed neighborhood pane, in-memory nav history (⌘[ / ⌘]). |
| 1B — Rich editor / git | ✅ feature-complete | Git status badge + commit-from-app (file list + per-file diff + syntax color). Rayon-parallel workspace scan + cache-backed warm startup. Multi-tab editor (⌘1..⌘8 / ⌘W, dirty stash). BlockNote rich + source + preview modes persist independently per split pane; ⌘W closes the focused editor or terminal tab. Browser smoke e2e. Deferred: monorepo extraction. |
| 2 — Inbox + AI | ✅ write loop live | Polling scan + notify watcher + Korean date parser + Claude CLI bridge + classifier + Gmail via gws, plus InboxPane (classify/accept/reject/process, approval gate, Maru labels, a/r/p, bulk actions, mission log tails). |
| 2.5 — Tree + Cursor shell + Terminal | ✅ shipped | Separate Documents and Finder-style Files workspaces, file tree + direct-child list + inline preview, copy/move/rename/duplicate/system-Trash operations, Cursor-style activity rail, split panes (⌘D), and one resizable bottom/right Panel that switches between persistent Terminal and Graph tabs. Rust-native alacritty_terminal PTY tabs (Claude/Codex/Kimi/Kiro/Shell), independent Terminal/Graph themes, layered ~/.maru/settings.json + <workspace>/.maru/workspace-state.json, signed auto-update, native menu bar. |
| 3 — Unified document ops (M1–M7) | ✅ W1–W6 + skills SSOT | Operations Catalog mode (ops_catalog::scan, fs watcher, Hub HTTP read + ETag/offline fallback, drilldown + Reveal), Hub Library client + template-aware new doc, Writing Guideline sidebar. Rust skill_host owns tiers (core/public/private/imported/managed), doctor validation, dirty/reconcile. maru-hub backs shared catalog (REST + Alembic + seeds). |
| 4 — Document Studio + Templates | ✅ W7–W12 | 7-step Studio mode (source → template → guideline → sections → HWP fields → export → package). create_document frontmatter prefill, M4 export pipeline (export/ plan/validate/dispatch, docx/hwpx/pdf + sha256 manifest), HWPX field map (hwpx slots + template_fill), 개조식 inline lint (linter/gaejosik). |
| 5 — Evidence Binder | ✅ W14 implemented | Schema-v2 local binder state under <workspace>/.maru/binder/<doc-id>.json; revision-guarded atomic mutations; full binary sha256 identity; typed sidecar status; BU-scoped candidates; explicit section/KPI/checklist targets, local verification, submission selection, undo, and orphan handling. W15 Hub evidence-index integration and W16–W18 Deck Studio remain planned. |
| D — Concept-map Diagram mode | ✅ shipped (Phase 0–7, hardened) + Report Pattern Studio (v8) | diagram mode: HWP-style 9-tab ribbon, 13 node kinds, 4-port edges, smart-guide snap, 11 templates, version history, viewport culling for 1000-node smoothness. Report Pattern Studio: v8 schema (report datasets + pattern views, one-time v7 backup at .maru/diagrams/backups/), typed table editing, pattern gallery + conversion preview, codec-registry import/export (lossless/structural/visual), and "Insert/Update in report" — managed maru-diagram:v1 Markdown blocks with rendered assets at attachments/diagrams/<docId>/. Storage: diagrams/*.cmd.json, .maru/diagram-patterns/. New commands: diagram_backup_document, diagram_pattern_save/list/delete, diagram_write_report_asset. See docs/diagram.md. |
| 8 — Knowledge graph | ✅ 8a/8b/8c + V6 shipped | graph mode: stable Sigma WebGL + Graphology multi-directed model, Barnes-Hut ForceAtlas2 worker, visibility reducers, 10k+ node target, vault/workspace sources, canonical Local targets, local depth/direction controls, background insights, saved views, reviewed relationship writes, incremental cache/watch refresh. V6: canvas-first floating controls, one progressive-disclosure tools drawer, dark neutral Obsidian-style defaults, selectable accent/color modes, dense-graph visual LOD, zero-size container recovery, and a persistent Graph tab in the shared bottom/right Panel. Managed writes remain schema-gated, revision-checked, snapshotted, and atomic. See docs/graph.md. |
| M0 — Anchor → Maru rename | ✅ shipped (v0.3.0) | Full rename across app id, dirs, CLI, tap. One-time on-disk migration (~/.anchor → ~/.maru, com.anchor.app → com.maru.app) with back-compat symlink; .maruignore preferred with .anchorignore fallback read. |
Rule SSOTs live in the work repo at
~/workspace/work/_meta/rules/{frontmatter-schema,document-lifecycle,hub-contract,evidence-policy}.md.
The deeper "what's next + how to continue" reference is ROADMAP.md.
The activity rail exposes eighteen top-level modes (Settings opens as an in-app overlay, so it is not an app mode). Diagram and Graph default on; E2E Flow is flag-gated.
| Mode | Label (ko / en) | What it does |
|---|---|---|
pkm |
문서 / Docs | Default. Markdown/HTML document tree, editor, and right utility rail. |
scratchpad |
스크래치패드 / Scratchpad | Three-pane memo and temporary-result workspace with a virtual folder tree, searchable file list, Rich/Source/Preview editing, autosave, recovery, and cleanup. |
files |
파일 / Files | Finder-style workspace with folder tree, direct-child list, inline Markdown/HTML editor, binary previews, search/filter/sort, and filesystem operations. |
inbox |
인박스 / Inbox | Configured drop / pending / processed / Files / Gmail sections with classify + a/r/p. |
comms |
메시지 / Messages | Multichannel comms settings (Telegram auth/mapping, source config, macOS migration). |
meetings |
회의록 / Meetings | Transcript + auto-summary intake and the meeting-notes review workbench. |
today |
오늘 / Today | Today planner (src/components/today/) over file-backed tasks with Google Tasks/Calendar links: prepare / execute / review stages plus calendar sync. |
tasks |
태스크 / Tasks | Standalone task & schedule management: full task list, month/week/day calendar, detail editing, and AI runs. |
dashboard |
대시보드 / Dashboard | At-a-glance workspace overview: today summary, tasks, schedule, catalog signals, inbox, agents, drafts, git, recents, and sync, with in-mode drilldowns and deep links into the owning modes. |
drafts |
아이디어 / Ideation | Ideation hub for durable ideas, implementation drafts, promotion, gap analysis, and the recurring-skill Automation scheduler. See docs/drafts.md. |
gap |
갭 분석 / Gap Analysis | Diffs promoted drafts against their frozen baselines, classifies the human edits, and feeds the tendencies back into draft prompts. See docs/gap-analysis.md. |
agents |
에이전트 / Agents | Every AI job Maru can run: status, chat, run, stop, backend, permission mode, prompt, schedule, plus user-created agents. The 대화 tab talks to the agent's backend in-app instead of through a PTY, and routes a turn into a task, a memo, or an approved proposal. See docs/agents.md. |
catalog |
카탈로그 / Catalog | M1 Operations Catalog — deadlines, in-flight approvals, unlinked evidence, inbox pending. |
studio |
스튜디오 / Studio | M2 Document Studio 7-step authoring wizard. See docs/studio.md. |
diagram |
다이어그램 / Diagram | Concept-map editor. See docs/diagram.md. |
graph |
그래프 / Graph | Vault knowledge graph. See docs/graph.md. |
sites |
사이트 / Sites | Left-rail site switcher with an embedded native browser pane. |
e2e |
E2E 플로우 / E2E Flow | Hidden end-to-end flow console (flag-gated e2eFlowEnabled). |
Settings > Jobs includes a separate Workspace Sync section for the global dot
workspace. It reads dot's versioned JSON status API and manages both sync
profiles without adding them to the workspace-local .maru/jobs.json registry:
- Cloud mirror status, owner, propagation policy, filters, push/pull schedules, pause/resume, preview, run, and bounded log tail.
- Mac peer SSH target, bidirectional schedule with quarantined deletions, doctor/diff, preview/run, secret allow patterns, and host-path allowlist. dot limits deletion to baseline-recorded paths and caps new profiles at 100 paths per run.
- Confirmed Homebrew install/update when dot is missing or older than the minimum compatible JSON API version.
The permanent bottom status bar shows setup, manual, scheduled, syncing, paused, or attention state and opens Settings > Jobs when clicked. Maru invokes fixed dot arguments directly, never through a shell; mutating actions are serialized and destructive or secret-expanding changes require confirmation.
.html and .htm documents open in the document editor with three modes
(Markdown files keep their own rich/source/preview modes and saved
preference):
- Visual (default) — sandboxed WYSIWYG editing with a formatting toolbar (undo/redo, paragraph/heading styles, bold/italic/underline/strike, lists, link/unlink, clear formatting).
- Source — lossless raw HTML editing.
- Preview — sandboxed, read-only rendering.
Preservation guarantees and limits:
- YAML frontmatter, the doctype,
<html>,<head>(including styles and scripts),<body>attributes, and the surrounding document shell are preserved byte-for-byte. After an actual Visual edit, only the body contents may receive browser HTML normalization; when Visual mode is opened without editing, the source stays byte-identical. - HTML fragments edit as fragments. Full documents whose
<body>boundary cannot be parsed disable Visual mode with a direct Source-mode action. - Visual mode is limited to documents up to 2 MiB (UTF-8) or 20,000 DOM nodes; larger documents fall back to Source and Preview.
Sandbox behavior:
- Scripts, inline event handlers, meta refresh, nested frames, and forms are preserved in the source but never executed: the editing/preview surface is a runtime-only iframe clone that strips them and injects a Content-Security-Policy blocking scripts, connections, workers, objects, frames, form submission, and remote resources. Runtime safety markup and rewritten asset URLs are never serialized back to the file.
- Documents containing scripts, event handlers, custom elements, forms, or embedded content require a one-time confirmation (per tab and current source digest) before Visual editing; source edits and external revisions re-arm the confirmation.
Local assets:
- Relative asset URLs (CSS, images, fonts, media),
data:, andblob:URLs load through the Tauri asset protocol, authorized read-only and scoped to the document's own directory inside the owning registered workspace (prepare_html_editor_assets). Network resources and paths escaping the workspace are blocked. - File operations (rename/move, duplicate, trash, manual and automatic snapshots) preserve the original HTML extension, including case, and the vault scanner/watcher recognize HTML extensions case-insensitively.
Six hwped_* commands (src-tauri/src/hwped.rs, wrappers in
src/lib/hwped.ts) spawn the hwp binary (hwp-cli >= 0.8.7) behind the
hwp-editor HwpEngine contract: read / render / edit / compose / validate /
capabilities. Fixed argv, no shell, 60s timeout; documents cross as
workspace paths or base64; edit ops arrive as JS-side opsToArgv fragments.
Backend-only for now — embedding the editor UI into a mode is a later
decision. See docs/hwp-editor.md.
Files is independent from the Documents pane. It scans files, empty folders, and symlinks as first-class entries, then presents a resizable folder tree, the current folder's direct children, and a resizable editor/preview pane. Searching within a folder includes its descendants; normal browsing remains direct-child only.
- Single-click selects and previews. Markdown opens Rich/Source/Preview and HTML opens Visual/Source/Preview directly in Files, with explicit Save and the same in-memory draft shared with Docs. Double-click opens a folder, a document in Docs, or another file in its external app.
- Directory and multi-selection summaries plus image/PDF/Office/media/archive viewers remain available for non-document selections.
- Multi-select, range select, keyboard navigation, drag/drop, rename, duplicate, Maru-internal cut/copy/paste, and new-folder creation are supported.
- Delete moves entries to the operating system Trash. Risky deletes require confirmation, and deletes containing unsaved open documents are blocked.
- Managed workspaces are read-only in Files. Read-only and delegated capability policies are enforced again by the Rust command layer.
- Rename/move uses the existing
.maru-rename-txnjournal and scan-time recovery. Collisions never overwrite an existing item; copy/paste chooses a-copysuffix.
Three surfaces close the loop between AI generation and the confirmed vault:
- Drafts + scheduler — AI skill runs (scheduled from the Agents mode, or
manual) emit task/idea/implementation draft artifacts that Maru
ingests into an unconfirmed store: the resolved
$MARU_DRAFTSbodies (scratchpad/drafts/by default) plus.maru/drafts/index.jsonmetadata. Accepting a draft promotes it into the vault through the approval gate. The Drafts mode is the Ideation hub: durable ideas can be created, edited, and moved through their lifecycle there, while the Scratchpad pane remains focused on memos and temporary results. See docs/drafts.md. - Gap analysis — promotion freezes a baseline copy; the Gap mode diffs the
promoted document against it, classifies each human edit (external info /
direct edit / cross-doc reference / formatting), and appends the summary to
.maru/gap-log.jsonl. A digest of recent log entries is attached to new extract-tasks schedule prompts so future drafts need fewer edits. See docs/gap-analysis.md. - KG reference visualization —
kg_document_refsmaps which vault notes a document references (wikilinks + title/alias mentions), computed on demand and cached under.maru/kg-cache/. The editor highlights referenced titles inline; the graph can focus the referenced neighborhood. See docs/kg-references.md.
Maru ships the desktop app and CLI as separate artifacts. On macOS, both are
distributed through the STAIxBWLB/homebrew-cask tap:
brew tap STAIxBWLB/homebrew-cask
# Desktop app only:
brew install --cask maru-workspace
# Standalone CLI only. Installs the executable as `maru`:
brew install maru-cli
maru --versionThe app cask installs Maru.app and does not create a CLI symlink. The CLI
formula installs only the standalone maru executable. The desktop app keeps
using signed Tauri updater metadata from GitHub Releases; Homebrew users can
also upgrade via brew upgrade --cask maru-workspace and brew upgrade maru-cli.
For repo-local management shortcuts:
make cli-install
make cli-smoke
make release-preflight
make homebrew-update RELEASE_TAG=v$(node -p "require('./package.json').version") HOMEBREW_TAP_DIR=../homebrew-cask┌─────────────────────────────────────────────────────────────┐
│ Tauri Webview (src/) │
│ React 19 + Radix UI + BlockNote + marked + DOMPurify │
│ Sigma WebGL + Graphology · CodeMirror · alacritty canvas │
│ │
│ Activity rail (18 modes): │
│ Dashboard / Docs / Scratchpad / Files / Inbox / Messages │
│ Meetings / Today / Tasks / Drafts / Gap / Agents / │
│ Catalog / Studio / Diagram / Graph / Sites / E2E │
│ Files/Scratchpad tree + list + editor + Terminal/Graph Panel│
└──────────────────────────────┬──────────────────────────────┘
│ Tauri IPC
┌──────────────────────────────▼──────────────────────────────┐
│ Rust core (src-tauri/src/) │
│ workspace scan — walkdir + .maruignore + cached index │
│ frontmatter/ — line-by-line YAML edit (preserves order) │
│ document.rs — read/save/create/version + field patch │
│ git.rs — status/commit/diff via shell-out │
│ vault_list.rs / vault.rs / vault_graph.rs / vault_guard.rs │
│ inbox.rs / inbox_watcher.rs / inbox_classifier.rs │
│ gmail_gws.rs / outlook_mso.rs / telegram_io.rs / ai_router │
│ ops_catalog/ · hub_client/ · export/ · studio/ · diagram/ │
│ skill_host/ · agent_host/ · terminal/ · linter/ │
│ maru_dir.rs — layered settings + .maru rules/templates │
│ maru_migration.rs — one-time Anchor → Maru on-disk rename │
└──────┬─────────────────────────────────────────────────────┘
│ stdio bridge
┌──────▼────────┐ ┌────────────────────┐
│ MCP server │ │ User's Claude Code │
│ (Node sidecar) │ │ CLI (~/.maru/skills)│
└───────────────┘ └────────────────────┘
Module boundary rules:
- Rust core owns workspace FS / cache / git / frontmatter / inbox
scan/watch/classification / Gmail
gws+ Outlook + Telegram bridges / layered Maru settings / Claude inbox subprocess / integrated terminal PTY + screen model / skills registry (skill_host) / agent proposal host (agent_host) / ops catalog / export pipeline / Studio + Diagram + Graph backends. - React handles only BlockNote / command palette / neighborhood / graph layout worker / diagram canvas. No business logic.
- Node sidecar (
sidecars/maru-mcp/) holds the local read-first MCP server. - Deferred (Phase 4 original plan, on hold): a Whisper (Korean large-v3) Python sidecar + MediaPipe voice/gesture editing. Not shipped — Phase 4 was repurposed to Document Studio. Voice/gesture remains a future track.
- Terminal and Graph share one VS Code-style Panel that docks at the bottom or right, persists its active tab/size/position, and exposes independent theme selectors. The terminal workspace mounts eagerly and keeps its textarea/canvas identity across collapse and Graph tab switches. macOS first-mouse activation focuses a terminal in one click; activation clicks are not forwarded to a TUI, while Shift always forces local selection.
- Each PTY session streams ordered, generation-tagged frames through a Tauri Channel. The frontend acknowledges applied frames, the backend keeps at most two frames in flight, hidden sessions do not serialize or paint frames, and a sequence/dimension mismatch requests a full resync.
- Frames use a palette plus compact
[text,width,style]cells. Release gates cap a 120x30 full frame at 100 KiB and a dirty-row patch at 4 KiB. - The Rust Alacritty model owns selection and copy semantics, including scrollback coordinates, soft wraps, wide CJK cells, resize reflow, semantic word selection, and line selection. The canvas keeps an optimistic drag overlay for immediate feedback.
- Frontend input is queued before spawn, microtask-batched, and sent in strict order. Normal key/text/paste delivery reads mirrored terminal modes without contending with the output parser's screen-model lock.
- The main Tauri window explicitly uses
backgroundThrottling: "throttle". On macOS 14+, this prevents WebKit's default full suspend/unload behavior for an inactive hidden or minimized main view while retaining rate limiting. It does not guarantee continuity across system sleep, Spaces, app switching, or compositor transitions. - This is a configuration contract in
src-tauri/tauri.conf.json, protected byscripts/tauri-window-policy.test.mjs, and the test is static configuration validation rather than runtime proof. Keep the policy when changing the main window; a native macOS sleep/wake or hide/show smoke test remains required.
Maru treats Claude Code, Codex, Kimi, and Kiro as first-class AI runtimes
(ai.defaultRuntime, terminal launchers, and per-agent command overrides).
Every AI feature runs as a named agent that binds a skill to one of those
runtimes, so the backend is a per-feature choice rather than a global one. The
agents mode manages them; the Settings tab below covers the runtime accounts
those agents dispatch to. See docs/agents.md.
- The Settings AI 런타임 / AI Runtimes tab (
system.tab.agents) shows per-agent sub-tabs with authentication status (Connected / Not connected / Not installed), account details (version, provider, login method, organization, email), a "Run login" action that launches the provider's login command in the integrated terminal, launch-command overrides (binary path + extra args), and quota/usage windows where the provider exposes them. - The main window footer renders an agent usage bar with one chip per agent (usage percent + reset countdown); it polls on an interval and on window focus, stays hidden when no agent reports usage, and clicking a chip opens the Agents settings tab.
- Backend commands:
agents_account_status(per-agent install/auth probe, honoring command overrides) andagents_usage_status(cached quota/usage windows,forcefor a user-initiated refresh). - The opt-in command-palette hook action installs reversible Maru lifecycle
entries in the selected Claude settings scope and a marker-managed block in
$KIMI_CODE_HOME/config.toml(default~/.kimi-code/config.toml). Kimi'sSessionStartstdin supplies the native session id used to resume a restored terminal tab; uninstall removes only Maru entries and preserves the user's other hooks and TOML content.
- Catalog watcher + auto-refresh — notify recursively watches
inbox/items/,tasks/{active,calendar}, every BU's02-admin-approvals/+03-evidence-cert/+.maru/bu-config.yaml; bursts are debounced 500 ms and the React pane re-queries in another 300 ms. - Hub catalog read path —
hub_client::http::fetch_with_cacheGETs/api/v1/{templates,guidelines,glossary,...}with ETag revalidation, falls back to<workspace>/.maru/cache/hub/on any network error. - Drilldown dialog + Reveal — Catalog row → modal showing frontmatter + manifest + README excerpt + sibling paths + "Finder에서 보기".
- Real-workspace timing —
MARU_CATALOG_BENCH_WORKSPACE=~/workspace/work cargo test --lib -- --ignored catalog_real_workspace_smokeindexed 110 entries across 4 BUs in 986 ms (30× under the 30-second budget). - Template-aware new doc —
⌘ ⇧ N/ palette "Hub 템플릿으로 새 문서" → BU/category filter → template picker prefills body + title + docType → optional guideline multi-select →maru:template/maru:business_unit/maru:guidelinesprovenance. - Guideline sidebar — opening a doc created via the template flow surfaces its guideline bodies in the right-pane
BookOpentab.
The above gates run fully offline (cache fallback). To exercise the live Hub read path:
# maru-hub: start FastAPI + sqlite (or run docker compose up -d db for Postgres)
cd dev/maru-hub
uv run python -m scripts.seed_catalog
MARU_HUB_DATABASE_URL=sqlite:///tmp/maru-hub.db uv run uvicorn maru_hub.main:app --port 8017
# maru: flip workspace.config.yaml
# hub.enabled: true
# hub.endpoint: http://127.0.0.1:8017/api/v1
# hub.api_token_ref: ~/workspace/work/.maru/secrets/hub-token (empty file is fine for local)Then in Maru: open Catalog mode → footer "마지막 스캔" populates; ⌘ ⇧ N → toggle "Hub 템플릿에서 시작" → templates load from Hub; pick business-plan-default → body prefills with slot hints.
After Phase 6 W21 ships, the same operator procedure will also exercise the POST /api/v1/documents/{id}/finalize round-trip — Maru pushes the approved markdown body + rendered artifacts + linked evidence binaries to Hub, and the document then appears under the Hub Finalized tab inside Catalog mode.
The active plan lives in ROADMAP.md — a 7-module (M1–M7) decomposition with weekly deliverables (W1–W34+) plus the Diagram and Graph side tracks. Phases 0–5, the Diagram mode, and the Phase 8 graph mode (8a/8b/8c) are shipped. See CHANGELOG.md for the release-by-release history.
Each phase is defined in outcomes the user actually exercises. No phase exists just to grow infrastructure. The entry gate for each phase is the verification of the previous one.
Git has run ahead of the linear W-plan: Phase 8 (graph mode) shipped before the remaining Phase 5 evidence work. The nearest pending items:
- W15 Evidence index — use the W14 full-sha binding identity for Hub
evidence_indexlookup and reuse hints. Keep binaries local; send only sha256 and metadata through the approval-gated Hub path. Entry files:src-tauri/src/evidence_binder.rs,src-tauri/src/hub_client/*,src/components/evidence/*. - W16–W18 Deck Studio (M6) — a
Decksmode wrapping the gpt-images-deck wizard with the bundled 14-style catalog (skills/docs/slide-decks/). - Phase 6 (W19–W22) Approval + Finalize-to-Hub — submission gates, gate-state polling, and the approval-gated
POST /api/v1/documents/{id}/finalizewrite path (the only Maru client path allowed to carry body/binary payloads). Requires a matchinghub_client/safety.rspre-flight. - Phase 7 (W23–W26) Certification & KPI bundle — Hub-backed certification vault, KPI composer, and PDF bundle assembly.
Maru stays the author SSOT — drafting and editing always happen under
~/workspace/work/. Maru Hub becomes the published SSOT the moment an
approval route closes. Two write paths land on Hub from Maru:
POST /api/v1/documents/sync(planned M7 caller) — drafting metadata only:document_uri,body_sha256,frontmatter, and the evidence link graph. No body, no binary. Used for cross-BU lookups and "이미 동기화된 초안" hints.POST /api/v1/documents/{id}/finalize(Phase 6 W21) — approval-gated canonical push. The instantsubmission_gate.stateflips toapproved, Maru auto-calls finalize with the full markdown body, every rendered artifact in the M4 manifest (docx/hwpx/pdf), and the binary bytes of every evidence file linked viafrontmatter.evidence_links. On201, the local frontmatterstatusflips toarchived-hub:<finalized_id>@v<N>.
Gate submits made while the Hub is disabled or unreachable persist in
<workspace>/.maru/queue/hub/ (one JSON per request) and drain FIFO via
the hub_queue_drain command — exposed in the Catalog footer as a queue
depth badge with a retry action.
pnpm install
# Browser dev (mocked Tauri):
pnpm dev
# Native Tauri dev (cleans stale local app bundles first):
pnpm tauri:dev
# Type check:
pnpm typecheck
# i18n lint (ko/en parity + hardcoded UI string scan; also in make verify):
pnpm lint:i18n
# ESLint (four correctness rules over src/ + e2e/; also in make verify):
pnpm lint
# Rust format check and lint gate (both also in make verify):
make fmt-check
make clippy
# Production build:
pnpm build
# Regenerate every icon from the canonical vector sources:
pnpm icons:generate
# Verify that committed web, desktop, Windows, iOS, and Android icons are current:
pnpm icons:check
# Full verification (typecheck + ESLint + release-version sync + guards + unit
# tests + Rust fmt/clippy + frontend build):
make verify
# Full verify plus release-only CLI and debug Tauri checks:
make release-checks
# Smoke the real installed AI CLIs. Every provider unit test drives a fake
# shell script, so this is the only check that touches the actual integration:
# --version, auth classification, skills-gate/account-probe agreement,
# usage-vs-capability, and real-binary permission parsing. Uninstalled backends are skipped.
# Run it when touching agent_host/provider.rs, skill_host/dispatch.rs,
# agent_host/status.rs, or terminal/mod.rs. It is deliberately NOT part of
# make verify, which must stay hermetic.
make verify-integration
MARU_CLI_SMOKE_ROUNDTRIP=1 make verify-integration # + one live prompt per backend
# Signed native release build:
make tauri-build
# Raw pnpm build still requires explicit updater signing env:
export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/maru-updater.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat ~/.tauri/maru-updater.key.password)"
pnpm tauri:build
# Prune oversized local Tauri debug artifacts (also runs from tauri:dev/build):
pnpm clean:tauri-debug
# Checks once every 24h and prunes src-tauri/target/debug when artifacts exceed 4GiB.
# Rust unit + integration tests:
cd src-tauri && cargo test
# or: make test-rust (cargo test --lib)
# Frontend unit tests:
pnpm test
# End-to-end:
pnpm test:e2e
# Local Maru MCP sidecar smoke:
MARU_MCP_WORKSPACE="$PWD" node sidecars/maru-mcp/index.mjs
# Skills registry doctor / reconcile:
cargo run --manifest-path src-tauri/Cargo.toml -p maru-cli --bin maru-cli -- --version
cargo run --manifest-path src-tauri/Cargo.toml -p maru-cli --bin maru-cli -- doctor --json
cargo run --manifest-path src-tauri/Cargo.toml -p maru-cli --bin maru-cli -- skills dirty --json
cargo run --manifest-path src-tauri/Cargo.toml -p maru-cli --bin maru-cli -- skills reconcile <name-or-id> --accept --dry-run
cargo run --manifest-path src-tauri/Cargo.toml -p maru-cli --bin maru-cli -- skills import /path/to/skill --copy
# Bench workspace scan on a real workspace:
cd src-tauri && cargo test --release bench_scan_real_workspace \
-- --ignored --nocapture --test-threads=1
# → MARU_BENCH_WORKSPACE=/some/path overrides the default ~/workspace/workCodex skill sync writes to $CODEX_HOME/skills when CODEX_HOME is set, as
it is for isolated Orca account profiles. Without that variable, it uses the
standard ~/.codex/skills directory.
CI runs make verify (typecheck + ESLint + release-version sync + guards + unit
tests + Rust fmt-check and clippy + frontend build) and make test-e2e on
pull requests via
.github/workflows/ci.yml. Documentation-only changes do not start CI. A push
to main first compares the pushed tree with its associated PR head and checks
that the latest CI PR #<number> run for that exact head succeeded. The stable
run name keeps the check PR-specific even when GitHub's workflow-run API omits
its pull_requests association. Only that exact-tree case skips the expensive
steps; direct pushes, stale merge bases, missing checks, and API failures run
the full suite. Version-changing PRs run
make release-checks instead of make verify, adding CLI and debug Tauri
checks without repeating verify, frontend build, or E2E.
typecheck covers four TypeScript projects — src/, the node config files,
e2e/, and scripts/ — so a type error in a Playwright spec or a build script
fails the gate instead of surfacing at run time. rust-toolchain.toml pins the
Rust toolchain, so make fmt-check and make clippy resolve the same compiler
on every machine and in CI rather than following whatever stable happens to
be. A failing e2e in CI uploads a Playwright trace with the playwright-report
artifact; the trace keeps the action timeline and the failing stack, but not DOM
snapshots, screenshots, or the network log (see the comment in
playwright.config.ts for why, and for when to turn them back on).
.github/workflows/release-preflight.yml is a manual recovery gate. It keeps
the intentionally exhaustive make release-preflight path but no longer
duplicates PR verification automatically when a version tag is pushed.
Skills deploy independently of app releases and live in their own repo,
STAIxBWLB/skills. Every push there
that touches bundle content verifies, packages, signs (same minisign key as
the app updater), and uploads immutable assets to that repo's
skills-channel prerelease. The app checks that channel after launch and
every 6 hours, applies new bundles automatically when the local skills are
clean and runtime-compatible, and shows an update-available notification
otherwise; maru skills update --check|--apply [--repair-env] and the
Skills UI cover manual flows. The binary embeds only a frozen
src-tauri/skills-bootstrap/ snapshot as the offline first-run fallback;
refresh it from the newest bundle with make skills-bootstrap-refresh when
cutting an app release.
Publishing a GitHub Release (a v* tag; the skills channel is excluded)
triggers .github/workflows/release-bundles.yml.
The workflow validates the tag, synchronized version surfaces, locked Cargo
metadata, and required secrets once before starting platform runners. It then
builds native Tauri bundles concurrently on macOS ARM, macOS Intel, Ubuntu,
and Windows and uploads the generated .app / .dmg, .deb / .rpm /
.AppImage, .exe, and .msi assets to that same release. Each macOS app job
also builds maru-cli from the populated target cache, packages a tarball
containing a maru executable, and uploads
maru-cli_<version>_darwin_{aarch64,x86_64}.tar.gz plus SHA256 files.
Platform jobs never update latest.json. After all four jobs succeed, one
finalizer validates the complete 20-asset pre-manifest set, reads the seven
updater signatures, and publishes a single 11-platform latest.json consumed
by startup auto-update and the native Check for Updates... action. The same
finalizer updates Homebrew only after the manifest succeeds. This single-writer
design removes the release-asset race that previously required serial builds.
For Developer ID builds, Tauri first notarizes, staples and uploads the app updater artifact without creating a DMG. The workflow then builds each DMG in a staging path, submits it as a separate notarization artifact, staples and validates its ticket, and runs Gatekeeper's primary-signature check. Only a DMG that passes all checks is uploaded to the public release. A failed container check fails that macOS matrix leg and blocks the Homebrew tap update.
macOS bundles must be code signed before publishing. The repository default
remains explicit ad-hoc signing (bundle.macOS.signingIdentity = "-") for local
development, but the release workflow fails closed unless the complete
Developer ID and notarization secret set is present. Configure these GitHub
Secrets before publishing a release:
APPLE_CERTIFICATE— base64 encoded Developer ID Application.p12APPLE_CERTIFICATE_PASSWORDKEYCHAIN_PASSWORDAPPLE_IDAPPLE_PASSWORD— app-specific passwordAPPLE_TEAM_ID
The release workflow imports APPLE_CERTIFICATE only inside the macOS signing
prep step, and it sends Apple notarization env vars only to the Developer ID
build branch. It intentionally does not pass unset Apple secrets into
tauri-apps/tauri-action, because empty environment variables make Tauri try
to import or notarize with blank credentials.
Minimum Apple Developer setup for direct distribution:
-
Create a
Developer ID Applicationcertificate. The default Maru bundle intentionally has no managed entitlement or HTTP/HTTPS browser-role declaration, so it does not require an App ID or provisioning profile. -
Install the downloaded
.cerinto Keychain Access, then export it with its private key as a password-protected.p12. -
Encode the
.p12and set the release secrets:tmp_cert_b64="$(mktemp)" openssl base64 -A -in DeveloperIDApplication.p12 -out "$tmp_cert_b64" gh secret set APPLE_CERTIFICATE --repo STAIxBWLB/maru --body-file "$tmp_cert_b64" rm "$tmp_cert_b64" gh secret set APPLE_CERTIFICATE_PASSWORD --repo STAIxBWLB/maru gh secret set KEYCHAIN_PASSWORD --repo STAIxBWLB/maru gh secret set APPLE_ID --repo STAIxBWLB/maru gh secret set APPLE_PASSWORD --repo STAIxBWLB/maru gh secret set APPLE_TEAM_ID --repo STAIxBWLB/maru
-
Confirm release readiness without printing secret values:
make macos-distribution-check make macos-distribution-local-check
For a local notarization smoke test, keep Apple files under
~/workspace/work/.maru/secrets/apple/:
DeveloperIDApplication.p12AuthKey_<APPLE_API_KEY_ID>.p8certificate-passwordapi-issuer-id- optional
api-key-id(defaults to theAuthKey_<id>.p8filename) - optional
keychain-password(generated locally if missing)
Then run:
make macos-notarize-local TARGET=aarch64-apple-darwinKeep the Tauri updater secrets (TAURI_SIGNING_PRIVATE_KEY,
TAURI_SIGNING_PRIVATE_KEY_PASSWORD) in place; they sign updater metadata and
are separate from Apple Developer ID signing. The workflow fails on partial
Apple signing configuration instead of silently producing an unintended ad-hoc
macOS release.
Browser-passkey packaging is a separate, fail-closed opt-in. The default
src-tauri/tauri.conf.json omits both the managed entitlement and HTTP/HTTPS
browser-role metadata; the runtime checks the effective code signature with
SecTaskCopyValueForEntitlement and returns unsupported before loading or
constructing Apple's browser passkey manager. This keeps tauri dev, CI,
ad-hoc bundles, and older supported macOS releases launchable.
src-tauri/tauri.passkeys.conf.json is an active, opt-in overlay. The app
bundle contains one Mach-O executable (maru) plus an executable
Contents/Resources/maru-cli shell wrapper that dispatches to
../MacOS/maru --maru-cli; the standalone/Homebrew maru-cli Mach-O lives in
the separate src-tauri/maru-cli workspace package, outside the app package's
binary targets. This prevents Tauri from applying the
managed entitlement to an unprovisioned helper. Incoming HTTP/HTTPS open
events are filtered into a bounded memory queue, emitted as
sites://open-requested, and can be drained after cold start. The Safari
fallback opens com.apple.Safari directly so Maru cannot recursively invoke
itself when registered as the default HTTP/HTTPS handler.
Passkey-enabled distribution requires an Apple-approved managed capability, a Developer ID provisioning profile, and notarization. The provisioned build also launches into Sites because Apple requires a browser surface on launch. The full runbook, including Apple's review criteria and the stop condition when the capability is not offered for Developer ID distribution, is docs/macos-passkeys.md:
export MARU_MACOS_PROVISIONING_PROFILE=/absolute/path/to/Maru.provisionprofile
export APPLE_SIGNING_IDENTITY='Developer ID Application: Example (TEAMID)'
export APPLE_TEAM_ID=TEAMID
make macos-passkey-readiness-check
make macos-passkey-notarized-buildThe normal release workflow never selects this overlay. Calling tauri build
with the overlay directly, without the staging and readiness checks, is
unsupported.
Release asset versions come from package.json, src-tauri/tauri.conf.json,
the root and CLI Cargo manifests, and their package entries in
src-tauri/Cargo.lock. make release-version-check validates all of them with
locked Cargo metadata. The release preparation job additionally requires the
published tag to equal v<package version> and stops before any platform build
if the versions or tag disagree.
The finalizer in release-bundles.yml pushes the tap update automatically once
the platform builds and updater manifest finish. The make homebrew-* targets
below are for verifying that result, or for recovering by hand if finalization
failed:
make homebrew-update-commit RELEASE_TAG=v$(node -p "require('./package.json').version") HOMEBREW_TAP_DIR=../homebrew-cask
make homebrew-audit HOMEBREW_TAP_DIR=../homebrew-cask
make homebrew-fetch HOMEBREW_TAP_DIR=../homebrew-caskAfter downloading the release DMG, verify Gatekeeper-facing state on macOS:
xcrun stapler validate Maru_*.dmg
spctl -a -vv -t open --context context:primary-signature Maru_*.dmg
codesign --verify --deep --strict --verbose=4 /Applications/Maru.app
spctl -a -vv -t exec /Applications/Maru.appAn AI workspace is any folder containing .md (or .markdown, .html, .htm) files.
The primary private workspace owns one Scratchpad root. ideation/,
memos/, and the configurable drafts collection are durable, Git-tracked
content; only temp/ is disposable and Git-ignored. The Drafts mode owns
ideation files and implementation drafts; the Scratchpad pane lists only
memos and temporary results.
<work>/scratchpad/
ideation/{seeds,developing,proposals,_archive}/
memos/
drafts/ # scratchpad.drafts_subdir default; headless producers use $MARU_DRAFTS
temp/{claude,codex,kiro,kimi,runtime}/
workspace.config.yaml may set paths.scratchpad and the
scratchpad.{ideation_subdir,memos_subdir,temp_subdir,drafts_subdir,editable_extensions,temp_stale_days,ideation_review_days,editable_max_bytes}
policy, plus drafts.promote_dir for the document-promotion suggestion
(default: _incoming). The promote directory must be a safe relative path
outside scratchpad and .maru; it does not restrict an explicit target path
entered in the promote dialog. drafts_subdir defaults to drafts, must be a
safe relative path, and cannot overlap another collection. Maru exposes the
resolved root as MARU_SCRATCHPAD, the resolved draft collection as
MARU_DRAFTS, its disposable
subdirectory as MARU_TEMP, and places Claude runtime files below
$MARU_TEMP/runtime/claude through CLAUDE_CODE_TMPDIR.
Scratchpad edits support .md, .markdown, and .txt. Ideation files older
than the review threshold are flagged for review but never cleaned
automatically. Temp cleanup is an explicit, per-file system-Trash operation.
Legacy .maru/memos migration is also explicit and verifies each destination
before removing its source. The pane keeps recovery drafts locally when it
cannot flush safely and never mutates tracked files merely by opening.
Verify the frontend contract with pnpm test, pnpm typecheck,
pnpm lint:i18n, and pnpm build; Rust containment, revision, migration,
cleanup, and watcher behavior is covered by cargo test --lib scratchpad.
Private workspace is the required default. Public workspace is optional and means a provider-managed shared root, not internet publishing. V1 capability support is registry-only: Maru stores non-secret provider metadata in workspaces.json, maps a manually entered provider role to coarse capabilities, intersects that with a filesystem writability probe, and gates direct writes in the UI and Rust commands. OAuth, Microsoft Graph, Google Drive, and Nextcloud live API checks are deferred.
Supported public providers are Local, Google Drive, OneDrive, SharePoint, Nextcloud, Obsidian, and Unknown. workspace.config.yaml accepts:
paths:
private: ~/workspace/work
public:
- label: Team Drive
path: ~/gdrive-workspace/work
provider: googleDrive
providerId: shared-drive-id
writePolicy: direct
role: contentManager
- label: Reference Site
path: ~/shared/reference
provider: sharePoint
writePolicy: readOnly
role: Can viewMaru stores user/global preferences at:
~/.maru/
settings.json # UI/theme/layout/window/split/terminal/explorer/file-queue/AI defaults
Maru stores workspace-local state and resources at:
<workspace>/
.maru/
cache/ # disposable workspace index for warm startup
workspace-state.json # collapsed folders, initialization flags, binary patterns, overrides
versions/ # snapshots created via the "Version" button
studio/ # per-document Studio wizard state
binder/ # per-document Evidence Binder state
.maruignore # optional, gitignore-style segment patterns (falls back to legacy .anchorignore)
<workspace>/.maru/settings.json is a legacy migration input only. Maru reads
it when present to build effective settings, but new workspaces do not get that
file.
.maruignore example for the user's ~/workspace/work:
node_modules
.venv
dist
build
target
.next
.turbo
.cache
.maru/cache
- Filesystem is authoritative. The fingerprinted cache (
<workspace>/.maru/cache/workspace-index-v3.json) is disposable. Warm reconciliation reuses unchanged entries and reparses changed files only; React state remains derived. - Frontmatter key order + comments preserved. A single-field patch must never disturb the order or comments of any other key (verified by cargo test).
src-tauri/src/frontmatter/ops.rsis the only allowed write path. - Crash-safe rename.
.maru-rename-txn/staging dir + recovery on the next workspace scan. - Dynamic relationship detection. Any frontmatter field containing
[[wikilink]]is treated as a relationship. No hard-coded field lists. - Symlinks inside the workspace are honored. Deliberate user-created symlinks (e.g.
~/workspace/work/inbox/downloads → ~/gdrive-workspace/...) are considered part of the workspace. Maru uses lexical containment, notcanonicalize(). - Managed vault writes are schema-gated + snapshotted.
write_policy: "managed"writes passvault_guard::validate_managed_writeand take a snapshot before mutation; note deletion stays MCP-only.
Out of scope for v1 by explicit decision:
- Semantic / embedding search (keyword + wikilink + git-grep cover 10k notes).
- Cloud sync, maru account, default telemetry (opt-in only).
- Mobile (Tauri 2 mobile is unstable; Obsidian owns mobile for now).
- Public marketplace server (cloned sources carrying a
maru.source.jsonmanifest are schema-validated on install and rolled back on failure; the manifestsignedflag is a metadata check, not cryptographic signature verification — no server, no moderation policy). - iMessage / Slack ingestion (permission pain > value).
- Multi-user collab, CRDT, realtime (single user, single device, git for history).
- PDF annotation, OCR (file-extracted text is enough).
- Agent-autonomous edits as default behavior. Autonomy is staged behind disposable workspaces, protected writes, approval policy, and audit events.
- iCloud / Dropbox workspace awareness (user's responsibility).
- Unsigned / ad-hoc auto-updater feeds (updates are accepted only through signed GitHub Release artifacts).
No license file is currently published. All rights reserved unless a license is added.