Skill: dickwu/workplace (version 1.0.0)

--- name: workplace description: > Manage multiple workplaces (project directories) with multi-agent orchestration, isolated memory, and inter-agent communication. Use when the user mentions: workplace init/list/switch/scan/status/agents/export/import, managing projects, switching between codebases, multi-agent workflows, agent handoff, kernel agent, workspace structure, deploy environments, or any variation of "workplace" commands. Also triggered by /workplace slash command. Auto-detects .git folders as workplaces. Each workplace has its own agents, memory, skills, and deployment configs in a .workplace/ directory. Syncs context to Cursor, Claude Code, and OpenCode. Interactive Telegram/Discord UI with inline buttons for switching workplaces, starting agents, and deploying. user-invocable: true --- # Workplace Skill Manage multiple project workplaces with per-workspace agents, isolated memory, and Swarm-style agent orchestration. ## /workplace Command (Telegram / Slash) Hierarchical navigation with parent → child drill-down. - **`/workplace`** or **`/workplace list`** → Show top-level view: parent workspaces and standalone workplaces as buttons. Parents show `(N)` child count. Current workspace marked with ✓. - **Click a parent button** → Drill into children. Shows child buttons + "Use parent" + "← Back". - **`/workplace <name>`** → If standalone or child, switch directly. If parent with children, show drill-in. - **`/workplace parent:child`** → Direct switch using colon syntax (e.g. `log-stream:logstream`). - **`/workplace status`** → Current workspace card with parent, linked, agents, deploy envs. - **`/workplace agents`** → Agent list with start/stop buttons. ### Colon Syntax `/workplace log-stream:logstream` resolves parent by name, then finds child under that parent. Supports quick switching without navigating menus. ### Context Switching When the user switches workplaces (via button click, name, or colon syntax): 1. Update `~/.openclaw/workspace/.workplaces/current.json` with the selected UUID and path 2. Update `lastActive` in `registry.json` 3. Load the new workspace's `.workplace/config.json` for context 4. Send confirmation: name, path, parent (if any), linked workplaces, agent list 5. Check `sessions.json` for the target workplace UUID: - **Has sessions** → show session buttons ("Continue: {label}" + "New chat session") - **No sessions** → auto-create a new session, confirm in the switch message 6. Subsequent messages in the session should be aware of the active workspace context Read `current.json` at the start of any workplace operation to know which workspace is active. See [telegram-ui.md](references/telegram-ui.md) for full button layouts, callback routing, and platform fallbacks. ## Quick Reference | Command | Action | |---------|--------| | `workplace init [path]` | Initialize workplace (scan existing or set up new) | | `workplace list` | List all workplaces (inline buttons to switch) | | `workplace switch <name\|uuid>` | Switch active workplace | | `workplace scan [path]` | Discover .git workplaces in subdirectories | | `workplace link <path>` | Link a related workplace | | `workplace unlink <path\|uuid>` | Remove a linked workplace | | `workplace status` | Current workplace info + agent status | | `workplace agents` | List agents in current workplace | | `workplace agent start <name>` | Start an agent (runs as sub-agent) | | `workplace agent stop <name>` | Stop a running agent | | `workplace kernel start` | Start persistent kernel agent | | `workplace kernel stop` | Stop kernel agent | | `workplace export [zip\|json]` | Export workplace config | | `workplace import <file>` | Import workplace from export | | `workplace load <path\|name\|uuid>` | Load/open a registered workplace for quick access | | `workplace unload <name\|uuid>` | Unload a workplace from the loaded set | | `workplace loaded` | List all currently loaded workplaces | | `workplace delete <name\|uuid>` | Remove from registry | | `workplace deploy <env>` | Show/run deploy instructions | | `workplace sync <ide>` | Generate context for cursor/claude/opencode/all | | `workplace sessions` | List chat sessions for current workplace | | `workplace session new [label]` | Create a new chat session | | `workplace session continue [label]` | Resume a saved session | | `workplace session delete <label>` | Delete a saved session | | `workplace session rename <old> <new>` | Rename a session | ## Architecture ### Registry Central registry at `~/.openclaw/workspace/.workplaces/`: - `registry.json` — all known workplaces with UUID, path, hostname, links - `current.json` — currently active workplace - `loaded.json` — workplaces currently "open/loaded" for quick access and cross-workspace ops ### Per-Workplace Structure Each project gets a `.workplace/` directory: ``` .workplace/ ├── config.json # UUID, name, path, hostname, linked, parent ├── agents/*.md # Agent role definitions (kernel.md always present) ├── memory/ # Isolated daily logs (YYYY-MM-DD.md) ├── skills/ # Workplace-specific skills (user-managed via git) ├── chat.md # Inter-agent communication ├── structure.json # Auto-scanned file tree ├── full-tree.md # Full tree with parent + linked workplaces (by hostname) ├── process-status.json # Agent runtime states and errors └── deploy/ # Deployment docs: dev.md, main.md, pre.md ``` ### Loaded Workplaces `loaded.json` tracks which workplaces are currently "open". This is distinct from the registry (all known workplaces) and current (the one active workplace). Loaded workplaces are the set you're actively working with — useful for cross-workspace agent orchestration, quick switching, and context awareness. ```json [ { "uuid": "74cdd6fd-...", "name": "log-stream", "path": [REDACTED_SECRET], "loadedAt": "2026-02-17T22:05:00Z", "source": "manual" } ] ``` Fields: - `uuid` — workplace UUID (matches registry) - `name` — display name - `path` — absolute filesystem path - `loadedAt` — ISO timestamp when loaded - `source` — how it was loaded (`manual`, `auto`, `linked`) Manage via `scripts/loaded_workplaces.sh` (list/load/unload/status). ### Session Management Per-workplace chat sessions are tracked in `~/.openclaw/workspace/.workplaces/sessions.json`. Each workplace UUID maps to an array of sessions (sessionId, label, timestamps) and an `activeSession` pointer. When switching workplaces: 1. If the target has saved sessions → show "Continue" buttons + "New chat" button 2. If no saved sessions → auto-create a new session 3. Session transcripts are stored as OpenClaw `.jsonl` files On "Continue": set `activeSession`, load recent context from transcript. On "New chat": generate a new session entry, prompt for optional label. See [telegram-ui.md](references/telegram-ui.md) for full button layouts and callback routing. ### Workplace Detection - Any directory with `.git/` is a potential workplace - Submodules included as nested workplaces - Parent workplace auto-detected from parent directories - Manual linking via `workplace link` ## Workflows ### Initialize a Workplace 1. Run `scripts/init_workplace.sh <path> [--name <name>] [--desc <desc>]` 2. For existing projects: scan file structure, read `*.md` files, analyze project type, suggest agents 3. For empty folders: ask project name, description, language/framework, roles needed 4. Creates `.workplace/` structure, registers in central registry, sets as current 5. See [init-guide.md](references/init-guide.md) for full flow details ### Agent System Agents are defined as `.md` files in `.workplace/agents/` with YAML frontmatter (name, role, triggers, handoff_to). Run agents via `sessions_spawn` with system prompts built from their definitions + workplace context. - See [agent-system.md](references/agent-system.md) for agent creation, Swarm handoff, and runtime details ### Inter-Agent Communication Agents communicate via `chat.md` using a structured message protocol. The Rust file-watcher server monitors changes and outputs parsed messages as JSON lines. - See [chat-protocol.md](references/chat-protocol.md) for message format spec ### Rust File-Watcher Server Binary at `assets/bin/workplace-server-{os}-{arch}`. Build from source with `scripts/build.sh`. ```bash # Start server for a workplace workplace-server /path/to/project # Server outputs JSON lines to stdout for each new chat.md message {"timestamp":"...","sender":"coder","recipient":"reviewer","broadcast":[],"message":"...","line_number":1} ``` ### Export/Import - **ZIP**: Full `.workplace/` folder (memory excluded by default) - **JSON**: Config + agent definitions + deploy docs as portable manifest - Import generates a new UUID to avoid collisions ## Chat UI (Telegram / Discord) On platforms with inline buttons, `workplace list` shows a clickable switcher. `workplace agents` shows start/stop buttons per agent. `workplace deploy` shows environment buttons. See [telegram-ui.md](references/telegram-ui.md) for message formats, button components, and callback handling. Fallback: numbered text lists on platforms without button support (WhatsApp, Signal). ## IDE Integration Sync workplace context to external coding tools: - **Cursor** → `.cursor/rules/workplace.mdc` (MDC with frontmatter) - **Claude Code** → `CLAUDE.md` (markdown, marker-based updates) - **OpenCode** → `opencode.jsonc` instructions field Run `workplace sync all` to update all detected IDEs, or target one: `workplace sync cursor`. See [ide-sync.md](references/ide-sync.md) for implementation details. ## Scripts | Script | Purpose | |--------|---------| | `scripts/init_workplace.sh` | Initialize .workplace/ in a directory | | `scripts/scan_workplaces.sh` | Find .git workplaces under a path | | `scripts/loaded_workplaces.sh` | Manage loaded/open workplaces (list/load/unload/status) | | `scripts/build.sh` | Build Rust server for current platform | ## Supermemory Integration Each workplace uses its UUID as `containerTag` for supermemory operations: - Kernel agent saves structure summaries and project facts - All workplace memories are isolated by containerTag - Enables cross-session project state awareness ## Command Details See [commands.md](references/commands.md) for full command reference with examples.

path: README.md
content: # multi-workplace

An [OpenClaw](https://github.com/openclaw/openclaw) skill for managing multiple project workplaces with multi-agent orchestration, isolated memory, and inter-agent communication.

## What It Does

- **Detect & manage workplaces** — auto-discovers `.git` directories as workplaces, tracks them in a central registry
- **Multi-agent per workspace** — define custom role-based agents (coder, reviewer, devops, etc.) per project
- **Swarm orchestration** — agents hand off tasks to each other with full context (Claude Swarm pattern)
- **Inter-agent communication** — lightweight Rust file-watcher server monitors `chat.md` for real-time message routing
- **Isolated memory** — each workplace gets its own memory, supermemory container, and context
- **IDE integration** — generates context files for Cursor, Claude Code, and OpenCode
- **Export/import** — share workplace configs via ZIP or JSON

## Quick Start

### Install

```bash
# Clone into your OpenClaw skills directory
git clone [REDACTED_SECRET]:dickwu/multi-workplace.git ~/.openclaw/workspace/skills/multi-workplace
```

Or install via ClawHub (when published):
```
/skill install multi-workplace
```

### Initialize a Workspace

```
workplace init /path/to/your/project
```

For existing projects, it scans the file structure, reads markdown files, and suggests appropriate agents. For new projects, it walks you through setup.

### Core Commands

```
workplace list              # List all workplaces (clickable switch)
workplace switch my-app     # Switch active workplace
workplace scan ~/projects   # Discover workplaces in subdirectories
workplace status            # Current workplace info + agent status
workplace agents            # List agents in current workplace
workplace agent start coder # Start the coder agent
workplace kernel start      # Start persistent structure watcher
workplace deploy dev        # Show dev deployment instructions
workplace export zip        # Export workplace config

# Loaded workplaces — keep workplaces "open" for quick access
workplace load /path/to/project   # Load a registered workplace
workplace loaded                  # List currently loaded workplaces
workplace unload my-app           # Remove from loaded set
```

## Per-Workplace Structure

Each project gets a `.workplace/` directory:

```
project/
└── .workplace/
    ├── config.json          # UUID, name, settings, linked workplaces
    ├── agents/              # Agent role definitions
    │   ├── kernel.md        # Always present — structure watcher
    │   ├── coder.md         # Custom agents...
    │   └── reviewer.md
    ├── memory/              # Isolated daily logs
    ├── skills/              # Workspace-specific skills (git-managed)
    ├── chat.md              # Inter-agent communication
    ├── structure.json       # Auto-scanned file tree
    ├── full-tree.md         # Full tree with parent + linked workplaces
    ├── process-status.json  # Agent runtime states
    └── deploy/              # Deployment docs
        ├── dev.md
        ├── main.md
        └── pre.md
```

## Agent System

### Defining Agents

Create a `.md` file in `.workplace/agents/`:

```markdown
---
name: coder
role: "Senior backend developer"
triggers: ["code", "implement", "fix", "refactor"]
handoff_to: ["reviewer", "tester"]
---

# Coder Agent

## Role
You are a senior developer working on this project.

## Instructions
- Follow existing patterns in the codebase
- Write tests for new functionality
- Hand off to reviewer when implementation is complete
```

### Swarm Orchestration

Agents communicate via `chat.md` using a simple protocol:

```
[coder-to-reviewer]: Review needed for auth module. Files: src/auth.rs, src/middleware.rs
[reviewer-to-coder] @ [developer, manager]: Approved with suggestions — see inline comments
```

The Rust file-watcher server monitors `chat.md` and triggers the appropriate agent when a message arrives.

## IDE Integration

### Cursor

Run `workplace sync cursor` to generate `.cursor/rules/workplace.mdc` from your workplace config and agent definitions. This gives Cursor awareness of:

- Project structure and file tree
- Agent roles and conventions
- Deployment configs
- Linked workplaces

### Claude Code

Run `workplace sync claude` to generate/update `CLAUDE.md` at your project root with workplace context — structure, agent roles, conventions, and deploy info.

### OpenCode

Run `workplace sync opencode` to add workplace context to your `opencode.jsonc` instructions.

### How It Works

The `workplace sync` command reads your `.workplace/` config and generates IDE-specific context files. Each IDE gets the same information in its native format:

| IDE | Config File | Format |
|-----|------------|--------|
| Cursor | `.cursor/rules/workplace.mdc` | MDC with frontmatter |
| Claude Code | `CLAUDE.md` | Markdown |
| OpenCode | `opencode.jsonc` → instructions | JSONC |

Re-run `workplace sync <ide>` after changing agents or config to keep IDE context updated.

## Rust File-Watcher Server

The inter-agent communication server is a lightweight Rust binary (~1.4MB) that watches `chat.md` for changes and outputs parsed messages as JSON lines.

### Pre-built Binaries

| Platform | Binary |
|----------|--------|
| macOS (Apple Silicon) | `assets/bin/workplace-server-darwin-arm64` |
| macOS (Intel) | `assets/bin/workplace-server-darwin-x86_64` |
| Linux (x86_64) | `assets/bin/workplace-server-linux-x86_64` |
| Linux (ARM64) | `assets/bin/workplace-server-linux-arm64` |

### Build from Source

```bash
# Requires Rust toolchain
scripts/build.sh
```

### Usage

```bash
workplace-server /path/to/project
# Outputs JSON lines:
# {"timestamp":"...","sender":"coder","recipient":"reviewer","broadcast":[],"message":"...","line_number":1}
```

## Central Registry

All workplaces are tracked in `~/.openclaw/workspace/.workplaces/`:

```
.workplaces/
├── registry.json    # All known workplaces
├── current.json     # Currently active workplace
├── loaded.json      # Workplaces currently "open" for quick access
└── shared-skills/   # Skills shared across workplaces
```

### Loaded Workplaces

The **loaded** set is distinct from the registry and the current workspace:

- **Registry** — every workplace ever initialized (all known)
- **Current** — the single active workspace you're working in right now
- **Loaded** — workplaces you're actively working with (open for quick switching, cross-workspace ops, agent orchestration)

Use `workplace load` to open a registered workplace, `workplace loaded` to see what's open, and `workplace unload` to close one. Loaded workplaces persist across sessions via `loaded.json`.

## Supermemory

Each workplace uses its UUID as a `containerTag` for supermemory, providing:

- Isolated memory per project
- Cross-session awareness of project state
- Structure summaries maintained by the kernel agent

## Architecture

See the [architecture document](../../multi-workplace-arch.md) for full design details.

## Requirements

- [OpenClaw](https://github.com/openclaw/openclaw) (for agent orchestration and supermemory)
- Git (for workplace detection)
- Rust toolchain (only if building the server from source)

## License

MIT


path: assets/templates/config.json
content: {
  "uuid": "__UUID__",
  "name": "__NAME__",
  "path": "__PATH__",
  "hostname": "__HOSTNAME__",
  "created": "__CREATED__",
  "description": "",
  "language": "",
  "framework": "",
  "parent": null,
  "linked": [],
  "agents": ["kernel"],
  "deployEnvs": ["dev", "main", "pre"],
  "settings": {
    "ignorePatterns": [
      "node_modules", "vendor", ".next", "dist", "build",
      "target", "__pycache__", ".venv", ".git", ".workplace"
    ]
  }
}


path: assets/templates/deploy/dev.md
content: # Development Environment

## Prerequisites

<!-- List tools, SDKs, env vars needed -->

## Setup

```bash
# Clone and install dependencies
# ...
```

## Run

```bash
# Start dev server
# ...
```

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| | | |

## Notes

<!-- Dev-specific gotchas, hot reload, debug flags, etc. -->


path: assets/templates/deploy/main.md
content: # Production Environment

## Prerequisites

<!-- List tools, access, credentials needed -->

## Build

```bash
# Production build steps
# ...
```

## Deploy

```bash
# Deployment commands
# ...
```

## Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| | | |

## Rollback

```bash
# How to rollback if something goes wrong
# ...
```

## Health Check

<!-- How to verify the deployment is healthy -->

## Notes

<!-- Production-specific concerns: scaling, monitoring, alerts -->


path: assets/templates/deploy/pre.md
content: # Pre-Production / Staging Environment

## Prerequisites

<!-- List tools, access needed -->

## Build

```bash
# Staging build steps (may differ from prod)
# ...
```

## Deploy

```bash
# Staging deployment commands
# ...
```

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| | | |

## Testing Checklist

- [ ] Smoke tests pass
- [ ] Integration tests pass
- [ ] Performance acceptable
- [ ] No regressions from previous release

## Promote to Production

<!-- Steps to promote this staging build to production -->

## Notes

<!-- Staging-specific concerns: test data, feature flags, access -->


path: assets/templates/kernel.md
content: ---
name: kernel
role: "Workplace kernel — persistent structure watcher and memory manager"
triggers: ["kernel", "structure", "update tree", "scan files"]
handoff_to: []
persistent: true
---

# Kernel Agent

## Role

You are the kernel agent for **{workplace_name}**. You run persistently in the background to maintain awareness of the project's file structure and save key information to long-term memory.

## Context

- Working directory: `{workplace_path}`
- Workplace UUID: `{workplace_uuid}`
- Structure file: `.workplace/structure.json`
- Full tree: `.workplace/full-tree.md`

## Responsibilities

1. **Structure tracking** — Scan the project directory and update `structure.json` with the current file tree. Ignore patterns defined in `config.json` → `settings.ignorePatterns`.

2. **Full tree** — Rebuild `.workplace/full-tree.md` including parent and linked workplaces. Key sections by hostname.

3. **Supermemory sync** — Save structure summaries and key project facts to supermemory using `containerTag: {workplace_uuid}`.

4. **Cleanup** — Remove stale entries from `structure.json` for files that no longer exist. Clear outdated supermemory entries.

5. **Status reporting** — Write current status to `.workplace/process-status.json`:
   ```json
   {
     "kernel": {
       "status": "running|idle|error",
       "lastRun": "ISO-8601",
       "error": null
     }
   }
   ```

## On Start

1. Read `config.json` for settings and ignore patterns
2. Scan project directory recursively
3. Generate `structure.json`
4. Build `full-tree.md` (check parent dir and linked workplaces)
5. Save summary to supermemory (containerTag = workplace UUID)
6. Report completion and stop

## On Error

Write error details to `.workplace/process-status.json` with current progress so work can be resumed.


path: references/agent-system.md
content: # Agent System

## Agent Definitions

Each agent is a Markdown file in `.workplace/agents/` with YAML frontmatter:

```markdown
---
name: coder
role: "Senior developer specializing in backend systems"
triggers: ["code", "implement", "fix", "refactor", "build"]
handoff_to: ["reviewer", "tester"]
persistent: false
---

# Coder Agent

## Role
You are a senior developer working on {workplace_name}.

## Context
- Working directory: {workplace_path}
- Project structure: (loaded from structure.json)
- Deploy config: (loaded from deploy/)

## Instructions
- Write clean, well-tested code
- Follow existing patterns in the codebase
- When done, hand off to reviewer for code review
- If tests need writing, hand off to tester

## Handoff Protocol
When handing off, write to chat.md:
[coder-to-reviewer]: Review needed for {description}. Files: {list}. Context: {summary}
```

### Frontmatter Fields

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Agent identifier (lowercase, alphanumeric + hyphens) |
| `role` | Yes | One-line role description |
| `triggers` | No | Keywords that activate this agent |
| `handoff_to` | No | Agents this one can delegate to |
| `persistent` | No | If true, runs continuously (like kernel) |

## Runtime

### Starting an Agent

1. Read the agent's `.md` file
2. Parse YAML frontmatter for metadata
3. Read `.workplace/config.json` for workplace context
4. Read `.workplace/structure.json` for file tree
5. Build system prompt:

```
You are {role} for the {workplace_name} project.

Workplace: {path}
UUID: {uuid}

Project Structure:
{structure.json contents, summarized}

{Body of agent .md file}

Communication:
- Write to .workplace/chat.md to communicate with other agents
- Format: [{your_name}-to-{target}]: message
- Broadcast: [{your_name}-to-{target}] @ [agent1, agent2]: message
- Read chat.md for messages directed to you

Process Status:
- Write your status to .workplace/process-status.json
- On completion: set status to "completed" and stop
- On error: set status to "error" with details
```

6. `sessions_spawn` with this system prompt as the task
7. Update `process-status.json`:

```json
{
  "coder": {
    "status": "running",
    "sessionKey": "agent:main:subagent:...",
    "startedAt": "2026-02-17T10:00:00Z",
    "task": "Implement auth module",
    "error": null
  }
}
```

### Agent Lifecycle

1. **Start** → spawned via `sessions_spawn`
2. **Running** → executes task, communicates via `chat.md`
3. **Handoff** → writes message to `chat.md`, orchestrator starts target agent
4. **Complete** → reports completion, updates `process-status.json`, session ends
5. **Error** → writes error to `process-status.json` with progress for resume

## Swarm Orchestration (Claude Swarm Pattern)

### Handoff Flow

```
User: "Implement the auth module"
  │
  ▼
[Coder Agent] starts
  │ writes code
  │ writes to chat.md: [coder-to-reviewer]: Review auth module
  │
  ▼
[Orchestrator] detects message via Rust server
  │ reads chat.md for context
  │ starts Reviewer Agent with handoff context
  │
  ▼
[Reviewer Agent] starts
  │ reviews code
  │ writes to chat.md: [reviewer-to-coder] @ [developer]: Approved
  │
  ▼
[Orchestrator] detects response
  │ notifies original session
```

### Orchestrator Role

The main OpenClaw session acts as orchestrator:

1. Start the Rust file-watcher server for the workplace
2. Monitor server stdout (JSON lines) for new messages
3. When a handoff message is detected:
   - Read the full `chat.md` for conversation context
   - Start the target agent with the handoff context included
4. When an agent completes, update `process-status.json`

### Context Passing

When handing off, include:
- **What was done** — summary of completed work
- **What's needed** — clear description of the next task
- **Files involved** — specific paths that were modified or need attention
- **Decision context** — why certain choices were made

## process-status.json

Tracks all agent and server states:

```json
{
  "server": {
    "status": "running",
    "pid": 12345,
    "startedAt": "2026-02-17T10:00:00Z",
    "lastEvent": "2026-02-17T10:05:00Z",
    "error": null
  },
  "kernel": {
    "status": "running",
    "sessionKey": "agent:main:subagent:abc123",
    "startedAt": "2026-02-17T10:00:00Z",
    "task": "Structure monitoring",
    "error": null
  },
  "coder": {
    "status": "completed",
    "sessionKey": "agent:main:subagent:def456",
    "startedAt": "2026-02-17T10:01:00Z",
    "completedAt": "2026-02-17T10:15:00Z",
    "task": "Implement auth module",
    "error": null
  }
}
```

### Status Values

| Status | Meaning |
|--------|---------|
| `running` | Agent is actively working |
| `idle` | Agent exists but not started |
| `completed` | Agent finished its task |
| `error` | Agent encountered an error (see `error` field) |
| `stopped` | Agent was manually stopped |


path: references/chat-protocol.md
content: # Chat Protocol

## Overview

Agents communicate via `.workplace/chat.md`, a plain-text append-only log. The Rust file-watcher server monitors this file and outputs parsed messages as JSON lines to stdout.

## Message Format

### Direct Message

```
[sender-to-recipient]: message content here
```

Example:
```
[coder-to-reviewer]: Please review changes in src/auth.rs — added JWT validation
```

### Broadcast Message

```
[sender-to-recipient] @ [target1, target2]: message content here
```

Example:
```
[reviewer-to-coder] @ [developer, manager]: Approved with minor suggestions — see inline comments
```

The broadcast targets receive the message as observers. The primary recipient is expected to act.

### Acknowledgment

Use the broadcast syntax to acknowledge receipt:

```
[reviewer-to-coder] @ [developer]: Acknowledged — starting review
```

## Parsing Rules

1. Lines must start with `[` to be treated as messages
2. Sender and recipient names: alphanumeric + hyphens + underscores
3. The `-to-` separator is required between sender and recipient
4. Broadcast targets are comma-separated inside `@ [...]`
5. Everything after `: ` is the message body
6. Empty lines and lines not matching the pattern are ignored (can be used for comments)

## Regex Pattern

```
^\[([a-zA-Z0-9_-]+)-to-([a-zA-Z0-9_-]+)\](?:\s*@\s*\[([^\]]*)\])?\s*:\s*(.+)$
```

## JSON Output (from Rust server)

The file-watcher server outputs one JSON object per line for each detected message:

```json
{
  "timestamp": "2026-02-17T10:05:00.123Z",
  "sender": "coder",
  "recipient": "reviewer",
  "broadcast": [],
  "message": "Please review changes in src/auth.rs",
  "line_number": 42
}
```

### Fields

| Field | Type | Description |
|-------|------|-------------|
| `timestamp` | string | ISO-8601 when the message was detected |
| `sender` | string | Name of the sending agent |
| `recipient` | string | Name of the target agent |
| `broadcast` | string[] | Additional agents who should see this message |
| `message` | string | Message content |
| `line_number` | number | Line number in chat.md |

## Multi-line Messages

For longer messages, use continuation lines (indented):

```
[coder-to-reviewer]: Review needed for auth module
  Changes:
  - Added JWT validation in src/auth.rs
  - Updated middleware in src/middleware.rs
  - Added tests in tests/auth_test.rs
```

Note: The current server implementation treats each non-empty line independently. Multi-line messages are a convention — the first line is parsed as the message, continuation lines are ignored by the parser but visible in the raw file.

## Best Practices

1. **Be specific** — include file paths, line numbers, context
2. **Summarize context** — don't assume the recipient knows what you did
3. **Use broadcast for visibility** — keep stakeholders informed
4. **Acknowledge receipt** — confirm you're working on a handoff
5. **Keep it append-only** — don't edit or delete previous messages
6. **Use comments for notes** — lines not matching the pattern are ignored

## File Management

- `chat.md` is append-only during a session
- Clear/archive between sessions if needed
- The kernel agent may archive old messages to `.workplace/memory/`


path: references/commands.md
content: # Command Reference

## workplace init [path]

Initialize a new workplace. If path is omitted, uses current directory.

**Empty directory:**
```
> workplace init /path/to/new-project --name my-app
```
Ask the user for: project name, description, language/framework, agent roles, deploy environments.

**Existing project:**
```
> workplace init .
```
1. Run `scripts/scan_workplaces.sh` to check if `.git` exists
2. Run `scripts/init_workplace.sh <path>` to create `.workplace/`
3. Scan all `*.md` files in the project for context
4. Read `structure.json` to understand the codebase
5. Suggest agents based on project analysis (e.g., Node.js project → suggest coder, tester, devops)
6. Confirm with user, then create agent `.md` files in `.workplace/agents/`

## workplace list

List all registered workplaces. Show as inline buttons for switching.

```bash
jq '.[] | "\(.name) (\(.uuid | .[0:8])...) — \(.path)"' ~/.openclaw/workspace/.workplaces/registry.json
```

Use `message` action with inline buttons where `callback_data` = `workplace switch <uuid>`.

## workplace switch <name|uuid>

Switch the active workplace.

1. Find workplace in `registry.json` by name or UUID (partial match OK)
2. Update `~/.openclaw/workspace/.workplaces/current.json`
3. Update `lastActive` in registry
4. Load `.workplace/config.json` for context
5. Report: name, path, active agents, last activity

## workplace scan [path]

Discover workplaces under a path.

```bash
scripts/scan_workplaces.sh <path> --max-depth 5
# Add --register to auto-register discovered workplaces
scripts/scan_workplaces.sh <path> --register
```

## workplace link <path>

Link a related workplace to the current one.

1. Read current workplace from `current.json`
2. Resolve target path, find its `.workplace/config.json`
3. Add target UUID to current workplace's `config.json` → `linked[]`
4. Rebuild `full-tree.md`

## workplace unlink <path|uuid>

Remove a linked workplace.

1. Find target in current workplace's `config.json` → `linked[]`
2. Remove the UUID
3. Rebuild `full-tree.md`

## workplace status

Show current workplace details.

```
📁 my-app (a1b2c3d4-...)
   Path: /Users/dev/projects/my-app
   Host: macbook-pro
   Parent: parent-project (if any)
   Linked: [list of linked workplaces]
   
   Agents:
   - kernel: running (since 2026-02-17T10:00:00Z)
   - coder: idle
   - reviewer: idle
   
   Last activity: 2 hours ago
```

Read from `config.json` + `process-status.json`.

## workplace agents

List agents in current workplace with their roles and status.

```bash
# List agent files
ls .workplace/agents/*.md
# Parse frontmatter for each
```

## workplace agent start <name>

Start an agent by name.

1. Read `.workplace/agents/<name>.md`
2. Parse frontmatter (role, triggers, handoff_to)
3. Read `.workplace/structure.json` for context
4. Build system prompt from agent definition + workplace context
5. `sessions_spawn` with the constructed prompt
6. Update `process-status.json` with agent status
7. If multiple agents, start the Rust file-watcher server for `chat.md`

## workplace agent stop <name>

Stop a running agent.

1. Find the agent's session via `subagents(action=list)`
2. `subagents(action=kill, target=<session>)`
3. Update `process-status.json`

## workplace kernel start

Start the kernel agent (persistent mode).

1. Read `.workplace/agents/kernel.md`
2. Spawn as sub-agent with instructions to:
   - Scan and update `structure.json`
   - Rebuild `full-tree.md`
   - Save structure to supermemory (containerTag = workplace UUID)
3. Start the Rust file-watcher server in background

## workplace kernel stop

Stop the kernel agent and file-watcher server.

## workplace export [zip|json]

**ZIP export:**
```bash
cd /path/to/project
zip -r workplace-export.zip .workplace/ -x ".workplace/memory/*"
```

**JSON export:**
Combine `config.json`, agent definitions, deploy docs, and skills list into a single JSON manifest.

## workplace import <file>

1. Detect format (zip or JSON)
2. Extract/parse contents
3. Generate new UUID
4. Create `.workplace/` at target path
5. Register in central registry
6. Attempt to re-link related workplaces if found locally

## workplace load <path|name|uuid>

Load/open a registered workplace so it stays available for quick access and cross-workspace operations.

Loaded workplaces are tracked in `~/.openclaw/workspace/.workplaces/loaded.json`.

```bash
scripts/loaded_workplaces.sh load <path|name|uuid>
```

1. Resolve the target by path, UUID, or name from `registry.json`
2. Verify `.workplace/config.json` exists at the resolved path
3. Add entry to `loaded.json` with UUID, name, path, timestamp, and source
4. If not already linked to the current workplace, optionally add to `linked[]`

**Example:**
```
> workplace load /Users/dev/opensource/log-stream
✅ Loaded: log-stream
   UUID: 74cdd6fd-...
   Path: /Users/dev/opensource/log-stream
```

## workplace unload <name|uuid>

Remove a workplace from the loaded set.

```bash
scripts/loaded_workplaces.sh unload <name|uuid>
```

Does not delete the workplace or its `.workplace/` directory — just removes it from the loaded/open set.

## workplace loaded

List all currently loaded workplaces.

```bash
scripts/loaded_workplaces.sh list
```

Shows name, UUID, path, and when each was loaded.

## workplace delete <name|uuid>

1. Find in registry by name or UUID
2. Confirm with user
3. Remove from `registry.json`
4. Optionally delete `.workplace/` directory (ask first)
5. Clean supermemory entries (containerTag = UUID)

## workplace sync <ide>

Generate IDE context files from workplace config. Target: `cursor`, `claude`, `opencode`, or `all`.

```
> workplace sync cursor    # Generate .cursor/rules/workplace.mdc
> workplace sync claude    # Generate/update CLAUDE.md
> workplace sync opencode  # Update opencode.jsonc instructions
> workplace sync all       # Sync all detected IDEs
```

See [ide-sync.md](ide-sync.md) for full implementation details and file formats.

## workplace deploy <env>

Show deployment instructions for an environment.

1. Read `.workplace/deploy/<env>.md`
2. Present to user
3. Optionally execute commands if user confirms


path: references/ide-sync.md
content: # IDE Sync Guide

## Overview

`workplace sync <ide>` generates context files so external coding tools (Cursor, Claude Code, OpenCode) can understand the project's workplace config, agents, and conventions.

## Cursor

### Output: `.cursor/rules/workplace.mdc`

```bash
mkdir -p .cursor/rules
```

Generate MDC format:

```mdc
---
description: Workplace context for {workplace_name}
globs:
alwaysApply: true
---

# {workplace_name}

## Project
- **Path:** {path}
- **Language:** {language}
- **Framework:** {framework}
- **Description:** {description}

## Structure
{structure.json summarized as tree}

## Agents
{for each agent in .workplace/agents/:}
### {name} — {role}
{agent instructions summary}

## Deployment
{summary of deploy/{dev,main,pre}.md}

## Conventions
- Inter-agent communication via .workplace/chat.md
- Agent definitions in .workplace/agents/*.md
- Structure tracking in .workplace/structure.json
```

### Implementation Steps

1. Read `.workplace/config.json`
2. Read `.workplace/structure.json` — convert to indented tree
3. Read each `.workplace/agents/*.md` — extract frontmatter + key instructions
4. Read `.workplace/deploy/*.md` — summarize
5. Write `.cursor/rules/workplace.mdc` with MDC frontmatter

## Claude Code

### Output: `CLAUDE.md` (project root)

If `CLAUDE.md` already exists, append a workplace section. If not, create it.

**Marker comments** to enable updates without clobbering user content:

```markdown
<!-- WORKPLACE:START -->
# Workplace: {workplace_name}

## Project
...

## Agents
...

## Structure
...
<!-- WORKPLACE:END -->
```

### Implementation Steps

1. Read `.workplace/config.json`
2. Read agents and structure (same as Cursor)
3. If `CLAUDE.md` exists:
   - Check for `<!-- WORKPLACE:START -->` marker
   - If found: replace content between START/END markers
   - If not found: append section at the end
4. If `CLAUDE.md` doesn't exist: create with workplace section
5. Write `CLAUDE.md`

## OpenCode

### Output: `opencode.jsonc` → instructions field

OpenCode uses a `instructions` field in its config. Add workplace context there.

### Implementation Steps

1. Read existing `opencode.jsonc` (or create minimal one)
2. Read workplace config, agents, structure
3. Build instructions string with workplace context
4. Update or create the `instructions` field
5. Write `opencode.jsonc` (preserve comments if possible — use sed/awk rather than JSON parse)

### Example

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "instructions": "## Workplace: my-app\n\nPath: /path/to/project\nAgents: coder, reviewer\n\n..."
}
```

## Sync All

`workplace sync all` runs sync for all three IDEs that are detected:

1. Check if `.cursor/` exists or `cursor` is installed → sync cursor
2. Check if `claude` CLI is installed → sync claude
3. Check if `opencode.jsonc` exists or `opencode` is installed → sync opencode

## Re-sync

Run `workplace sync <ide>` after:
- Adding or modifying agents
- Changing `config.json` settings
- Updating deploy docs
- Re-scanning structure

The sync command is idempotent — safe to run multiple times.

## Shared Context

All three IDEs get the same core information:

1. **Project identity** — name, path, language, framework, description
2. **File structure** — summarized from `structure.json`
3. **Agent roles** — who does what, handoff patterns
4. **Deploy info** — how to run in each environment
5. **Conventions** — chat.md protocol, agent file locations

The format adapts to each IDE's native config style.


path: references/init-guide.md
content: # Init Guide

## Overview

`workplace init` sets up a `.workplace/` directory in a project folder. The flow differs for empty vs existing projects.

## Empty Folder Init

### Information to Gather

Ask the user (don't overwhelm — prioritize):

1. **Project name** — what to call this workplace
2. **Description** — brief project summary
3. **Language/framework** — primary tech stack
4. **Agent roles** — what kind of agents are needed (suggest based on project type)
5. **Deploy environments** — dev/main/pre or custom
6. **Linked workplaces** — related projects (optional)

### Steps

```bash
# 1. Create the directory if needed
mkdir -p /path/to/project

# 2. Initialize git
cd /path/to/project && git init

# 3. Run init script
scripts/init_workplace.sh /path/to/project --name "my-app" --desc "Description"

# 4. Create custom agents based on user input
# Write .md files to .workplace/agents/

# 5. Update config.json with language/framework info
```

### Agent Suggestions by Project Type

| Project Type | Suggested Agents |
|-------------|------------------|
| Web app (React/Vue/Next) | coder, reviewer, designer, tester |
| API/Backend | coder, reviewer, tester, devops |
| Library/Package | coder, reviewer, docs-writer |
| Mobile app | coder, reviewer, tester, designer |
| Data/ML | coder, data-engineer, reviewer |
| Monorepo | coder, reviewer, devops, coordinator |
| Static site | coder, content-writer, designer |

These are suggestions — always confirm with the user and support fully custom roles.

## Existing Project Init

### Steps

1. **Check for `.git`**
   ```bash
   scripts/scan_workplaces.sh /path/to/project --max-depth 1
   ```

2. **Run init script**
   ```bash
   scripts/init_workplace.sh /path/to/project
   ```

3. **Scan and analyze**
   - Read `structure.json` (generated by init script)
   - Find and read key `.md` files:
     ```bash
     find /path/to/project -name "*.md" -maxdepth 3 \
       -not -path "*/node_modules/*" \
       -not -path "*/vendor/*" \
       -not -path "*/.workplace/*"
     ```
   - Look for: README.md, CONTRIBUTING.md, package.json, Cargo.toml, go.mod, pyproject.toml, etc.

4. **Analyze project type**
   - Detect language from file extensions and config files
   - Detect framework from dependencies
   - Identify monorepo patterns (workspaces, lerna, nx)
   - Check for CI/CD configs (.github/workflows, Jenkinsfile, etc.)

5. **Suggest agents**
   - Based on project type (see table above)
   - Based on existing contribution patterns
   - Present suggestions to user for confirmation

6. **Create agents**
   - Write `.md` files for confirmed agents
   - Customize instructions based on project analysis
   - Include project-specific context (tech stack, patterns, conventions)

7. **Update deploy docs**
   - Pre-fill `deploy/dev.md` from README setup instructions if available
   - Set language/framework in config.json

8. **Register and set current**
   - Already handled by `init_workplace.sh`

## Post-Init

After initialization:

1. Suggest running `workplace kernel start` to begin structure monitoring
2. Remind user to:
   - Customize agent definitions in `.workplace/agents/`
   - Fill in deploy docs in `.workplace/deploy/`
   - Add workplace-specific skills to `.workplace/skills/`
   - Consider adding `.workplace/memory/` to `.gitignore`

## Re-Init (Related Workplaces)

When a user runs init on a folder that has a parent or linked workplace:

1. Detect parent `.workplace/` in ancestor directories
2. Auto-set `parent` in `config.json`
3. Add the new workplace's UUID to parent's `linked[]`
4. Rebuild `full-tree.md` for both workplaces


path: references/telegram-ui.md
content: # Telegram UI Integration

## /workplace Command — Hierarchical Navigation

### Top Level (no args or `/workplace`)

Show **parent workspaces and standalone workplaces** as the first level. Group by: top-level items (no parent) first.

Read `registry.json`, separate into:
- **Parents / standalone**: entries where `parent == null`
- **Children**: entries where `parent != null` (shown when user drills into a parent)

**Top-level message:**

```
📁 **Workplaces**
Current: **{current_name}** {current_path_short}
```

**Buttons:** One row per top-level workspace. Parent workplaces show children count.

```json
{
  "blocks": [{
    "type": "buttons",
    "buttons": [
      {"label": "📂 log-stream (2)", "style": "primary"},
      {"label": "🔧 multi-workplace ✓", "style": "secondary", "disabled": true}
    ]
  }]
}
```

- Current workspace (or its parent): `disabled: true`, `style: "secondary"`, append ` ✓`
- Parent workplaces: show `(N)` child count
- Standalone workplaces: no count

### Drill into Parent

When user clicks a parent workspace button (e.g. "📂 log-stream (2)"), show its children:

```
📂 **log-stream** — parent workspace
`/Users/.../opensource/log-stream`
```

**Buttons:** One per child + a "Use parent" option + back button.

```json
{
  "blocks": [{
    "type": "buttons",
    "buttons": [
      {"label": "⚙️ logstream", "style": "primary"},
      {"label": "🌐 logstream-dashboard ✓", "style": "secondary", "disabled": true},
      {"label": "📂 Use log-stream (parent)", "style": "secondary"},
      {"label": "← Back", "style": "secondary"}
    ]
  }]
}
```

- Current child: `disabled: true` with ` ✓`
- "Use parent" button: switches context to the parent workspace itself
- "← Back" button: returns to top-level list

### Colon Syntax — Direct Navigation

Support `parent:child` syntax for quick switching without menus:

```
/workplace log-stream:logstream          → switch to logstream under log-stream
/workplace log-stream:logstream-dashboard → switch to logstream-dashboard
/workplace log-stream                     → show log-stream's children (drill-in)
/workplace multi-workplace                → switch directly (standalone, no children)
```

**Resolution logic:**
1. If input contains `:`, split into `parent:child`
2. Find parent by name in registry (fuzzy match OK)
3. Find child by name where `child.parent == parent.uuid`
4. Switch to child

If no `:`, check if the name matches a parent with children → show drill-in view.
If it matches a standalone or child → switch directly.

### Switch Confirmation

After switching, send:

```
✅ Switched to **logstream**
📂 `/Users/.../log-stream/logstream`
📂 Parent: log-stream
🔗 Linked: logstream-dashboard

Agents: kernel, rust-dev, sdk-dev, reviewer, publisher
```

### Button Callback Routing

| Button text | Action |
|---|---|
| `📂 {parent} (N)` | Show parent's children (drill-in) |
| `⚙️/🌐/🔧 {name}` | Switch to that workspace |
| `📂 Use {name} (parent)` | Switch to parent workspace |
| `← Back` | Show top-level list |
| `📂 {name}` (loaded view) | Switch to loaded workplace |
| `➕ Load workplace` | Prompt for path/name |
| `➖ Unload workplace` | Show unload picker |
| `❌ {name}` | Unload that workplace |
| `▶️ Start {agent}` | `workplace agent start {agent}` |
| `⏹ Stop {agent}` | `workplace agent stop {agent}` |
| `▶️ Continue: {label}` | Set as active session, resume |
| `✨ New chat session` | Create new session, prompt for label |
| `🗑 Manage sessions` | Show session management view |

### Agent and Deploy Buttons

Same as before — shown after switching or via `/workplace agents` / `/workplace deploy`:

```json
{
  "blocks": [
    {"type": "text", "text": "**Agents for logstream:**"},
    {"type": "buttons", "buttons": [
      {"label": "▶️ Start rust-dev", "style": "success"},
      {"label": "▶️ Start reviewer", "style": "success"},
      {"label": "▶️ Start sdk-dev", "style": "success"},
      {"label": "🔄 Start kernel", "style": "primary"}
    ]}
  ]
}
```

### Loaded Workplaces

For `/workplace loaded`:

```
📂 **Loaded Workplaces** (2)
Active: **multi-workplace**
```

**Buttons:** One row per loaded workplace + management buttons.

```json
{
  "blocks": [
    {"type": "text", "text": "📂 **Loaded Workplaces** (2)\nActive: **multi-workplace**"},
    {"type": "buttons", "buttons": [
      {"label": "📂 log-stream", "style": "primary"},
      {"label": "🔧 multi-workplace ✓", "style": "secondary", "disabled": true}
    ]},
    {"type": "buttons", "buttons": [
      {"label": "➕ Load workplace", "style": "success"},
      {"label": "➖ Unload workplace", "style": "danger"}
    ]}
  ]
}
```

- Current workplace: `disabled: true` with ` ✓`
- Clicking a loaded workplace switches to it
- "➕ Load workplace" prompts for path/name/uuid
- "➖ Unload workplace" shows loaded list with unload buttons

### Load Confirmation

After loading a workplace:

```
✅ Loaded: **log-stream**
📂 `/Users/.../opensource/log-stream`
🔗 Also linked to current workplace

Loaded workplaces: 2
```

### Unload Flow

When user clicks "➖ Unload workplace", show loaded workplaces with unload buttons:

```json
{
  "blocks": [
    {"type": "text", "text": "Select workspace to unload:"},
    {"type": "buttons", "buttons": [
      {"label": "❌ log-stream", "style": "danger"},
      {"label": "← Back", "style": "secondary"}
    ]}
  ]
}
```

### Button Callback Routing (Loaded)

| Button text | Action |
|---|---|
| `📂 {name}` (in loaded view) | Switch to that loaded workplace |
| `➕ Load workplace` | Prompt for path/name |
| `➖ Unload workplace` | Show unload picker |
| `❌ {name}` | Unload that workplace |

### Status Card

For `/workplace status`:

```
📁 **logstream** (93cb20c8...)
📂 `/Users/.../log-stream/logstream`
🖥️ Host: dsgnmac2
📂 Parent: log-stream (74cdd6fd...)
🔗 Linked: logstream-dashboard

**Agents:**
🟢 kernel — persistent structure watcher
⚪ rust-dev — Rust systems developer
⚪ reviewer — code reviewer
⚪ publisher — release manager

**Loaded:** log-stream, multi-workplace
**Deploy:** dev | main | pre
```

## Session Management (Per-Workplace Chat Sessions)

Each workplace can have saved OpenClaw chat sessions. When switching workplaces, the user is offered to continue an existing session or start fresh.

### Session Storage

Sessions are tracked in `~/.openclaw/workspace/.workplaces/sessions.json`:

```json
{
  "<workplace-uuid>": {
    "sessions": [
      {
        "sessionId": "7cf414ae-01e9-4347-8211-2d948170b718",
        "label": "rust refactor",
        "created": "2026-02-17T18:00:00Z",
        "lastActive": "2026-02-17T22:30:00Z"
      }
    ],
    "activeSession": "7cf414ae-01e9-4347-8211-2d948170b718"
  }
}
```

Fields:
- `sessionId` — OpenClaw session ID (maps to a `.jsonl` transcript)
- `label` — user-given or auto-generated label describing the session
- `created` — ISO timestamp when session was created
- `lastActive` — ISO timestamp of last activity
- `activeSession` — the session to resume by default

### Switch Confirmation with Session Buttons

When switching to a workplace that has saved sessions, show session options **after** the switch confirmation:

```
✅ Switched to **logstream**
📂 `/Users/.../log-stream/logstream`
📂 Parent: log-stream

💬 **Chat Sessions:**
```

**Buttons:**

```json
{
  "blocks": [
    {"type": "text", "text": "💬 **Chat Sessions:**"},
    {"type": "buttons", "buttons": [
      {"label": "▶️ Continue: rust refactor", "style": "primary"},
      {"label": "▶️ Continue: bug fixes", "style": "secondary"}
    ]},
    {"type": "buttons", "buttons": [
      {"label": "✨ New chat session", "style": "success"},
      {"label": "🗑 Manage sessions", "style": "secondary"}
    ]}
  ]
}
```

- Most recent / active session: `style: "primary"`
- Other saved sessions: `style: "secondary"`
- "✨ New chat session": creates a new session, prompts for optional label
- "🗑 Manage sessions": shows delete/rename options

### Switch to Workplace with No Sessions

When switching to a workplace with no saved sessions, auto-create one:

```
✅ Switched to **multi-workplace**
📂 `/Users/.../workspace/multi-workplace`

💬 New chat session started.
```

A new session entry is created in `sessions.json` with an auto-label based on workplace name + date.

### New Chat Session Flow

When user clicks "✨ New chat session":

1. Create a new session entry in `sessions.json` with a generated sessionId (UUID)
2. Set it as `activeSession`
3. Confirm:

```
✨ **New chat session** for **logstream**
Session: `a1b2c3d4-...`

💡 Reply with a label (e.g. "api redesign") or I'll auto-name it.
```

### Continue Session Flow

When user clicks "▶️ Continue: {label}":

1. Set that session as `activeSession` in `sessions.json`
2. Update `lastActive`
3. Load recent context from the session transcript if available
4. Confirm:

```
▶️ Resuming **rust refactor** for **logstream**
Last active: 2h ago
```

### Manage Sessions

When user clicks "🗑 Manage sessions":

```
💬 **Sessions for logstream** (2)
```

**Buttons:**

```json
{
  "blocks": [
    {"type": "buttons", "buttons": [
      {"label": "✏️ rust refactor", "style": "secondary"},
      {"label": "✏️ bug fixes", "style": "secondary"}
    ]},
    {"type": "buttons", "buttons": [
      {"label": "🗑 Delete a session", "style": "danger"},
      {"label": "← Back", "style": "secondary"}
    ]}
  ]
}
```

- "✏️ {label}": rename that session (prompt for new label)
- "🗑 Delete a session": show sessions with delete buttons
- "← Back": return to session list

### Delete Session Flow

```json
{
  "blocks": [
    {"type": "text", "text": "Select session to delete:"},
    {"type": "buttons", "buttons": [
      {"label": "❌ rust refactor", "style": "danger"},
      {"label": "❌ bug fixes", "style": "danger"},
      {"label": "← Back", "style": "secondary"}
    ]}
  ]
}
```

After deletion, remove from `sessions.json`. If it was the `activeSession`, clear it.

### Button Callback Routing (Sessions)

| Button text | Action |
|---|---|
| `▶️ Continue: {label}` | Set as active session, resume |
| `✨ New chat session` | Create new session entry |
| `🗑 Manage sessions` | Show session management view |
| `✏️ {label}` | Prompt to rename session |
| `🗑 Delete a session` | Show delete picker |
| `❌ {label}` (in delete view) | Delete that session |
| `← Back` (in session views) | Return to session list |

### Session Context in Workplace Status

`/workplace status` includes active session info:

```
📁 **logstream** (93cb20c8...)
...
💬 Active session: rust refactor (2h ago)
📝 Total sessions: 3
```

## Platform Fallback

On platforms without inline buttons (WhatsApp, Signal), show hierarchical text:

```
📁 Workplaces (current: logstream)

1. 📂 log-stream (parent)
   1a. ⚙️ logstream ← current
   1b. 🌐 logstream-dashboard
2. 🔧 multi-workplace

Reply with number (e.g. "1b") or name (e.g. "log-stream:logstream-dashboard")
```

For `/workplace loaded` on platforms without buttons:

```
📂 Loaded Workplaces (2)
Active: multi-workplace

1. log-stream — /Users/.../opensource/log-stream
2. multi-workplace — /Users/.../workspace/multi-workplace ← current

Commands: "workplace load <path>" / "workplace unload <name>"
```


path: scripts/build.sh
content: #!/usr/bin/env bash
set -euo pipefail

# build.sh — Build the workplace file-watcher server
# Usage: build.sh [--target <rust-target>]

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
SERVER_DIR="$SCRIPT_DIR/rust-server"
BIN_DIR="$SKILL_DIR/assets/bin"

# Source Rust env
if [[ -f "$HOME/.cargo/env" ]]; then
  source "$HOME/.cargo/env"
fi

if ! command -v cargo &>/dev/null; then
  echo "❌ Rust toolchain not found. Install via: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
  exit 1
fi

# Detect platform
ARCH="$(uname -m)"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"

case "$ARCH" in
  x86_64) ARCH_SUFFIX="x86_64" ;;
  arm64|aarch64) ARCH_SUFFIX="arm64" ;;
  *) ARCH_SUFFIX="$ARCH" ;;
esac

PLATFORM="${OS}-${ARCH_SUFFIX}"
BINARY_NAME="workplace-server-${PLATFORM}"

echo "🔨 Building workplace-server for $PLATFORM..."

# Parse optional target
RUST_TARGET=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --target) RUST_TARGET="$2"; shift 2 ;;
    *) shift ;;
  esac
done

cd "$SERVER_DIR"

if [[ -n "$RUST_TARGET" ]]; then
  cargo build --release --target "$RUST_TARGET"
  RELEASE_BIN="target/$RUST_TARGET/release/workplace-server"
else
  cargo build --release
  RELEASE_BIN="target/release/workplace-server"
fi

if [[ ! -f "$RELEASE_BIN" ]]; then
  echo "❌ Build failed — binary not found at $RELEASE_BIN"
  exit 1
fi

mkdir -p "$BIN_DIR"
cp "$RELEASE_BIN" "$BIN_DIR/$BINARY_NAME"
chmod +x "$BIN_DIR/$BINARY_NAME"

echo "✅ Built: $BIN_DIR/$BINARY_NAME"
echo "   Size: $(du -h "$BIN_DIR/$BINARY_NAME" | cut -f1)"


path: scripts/init_workplace.sh
content: #!/usr/bin/env bash
set -euo pipefail

# init_workplace.sh — Initialize a .workplace/ directory in a project folder
# Usage: init_workplace.sh <path> [--name <name>] [--desc <description>]

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
TEMPLATES="$SKILL_DIR/assets/templates"
REGISTRY_DIR="$HOME/.openclaw/workspace/.workplaces"

# --- Parse args ---
TARGET_PATH=""
WP_NAME=""
WP_DESC=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --name) WP_NAME="$2"; shift 2 ;;
    --desc) WP_DESC="$2"; shift 2 ;;
    *) TARGET_PATH="$1"; shift ;;
  esac
done

if [[ -z "$TARGET_PATH" ]]; then
  echo "Usage: init_workplace.sh <path> [--name <name>] [--desc <description>]"
  exit 1
fi

TARGET_PATH="$(cd "$TARGET_PATH" 2>/dev/null && pwd || echo "$TARGET_PATH")"

# Create target if it doesn't exist
mkdir -p "$TARGET_PATH"
TARGET_PATH="$(cd "$TARGET_PATH" && pwd)"

# Check if already initialized
if [[ -d "$TARGET_PATH/.workplace" ]]; then
  echo "⚠️  Workplace already initialized at $TARGET_PATH/.workplace"
  echo "    UUID: $(jq -r '.uuid' "$TARGET_PATH/.workplace/config.json" 2>/dev/null || echo 'unknown')"
  exit 0
fi

# --- Check if target has .git or is a parent folder ---
HAS_GIT=false
IS_PARENT=false
CHILD_DIRS=()

if [[ -d "$TARGET_PATH/.git" || -f "$TARGET_PATH/.git" ]]; then
  HAS_GIT=true
fi

if [[ "$HAS_GIT" == false ]]; then
  # No .git — check if subdirs have .git (making this a parent workspace)
  for subdir in "$TARGET_PATH"/*/; do
    subdir="$(cd "$subdir" 2>/dev/null && pwd || true)"
    [[ -z "$subdir" ]] && continue
    if [[ -d "$subdir/.git" || -f "$subdir/.git" ]]; then
      IS_PARENT=true
      CHILD_DIRS+=("$subdir")
    fi
  done

  if [[ "$IS_PARENT" == true ]]; then
    echo "📂 No .git found at $TARGET_PATH"
    echo "   Detected as parent workspace with ${#CHILD_DIRS[@]} child repo(s):"
    for cd in "${CHILD_DIRS[@]}"; do
      echo "   - $(basename "$cd")"
    done
    echo ""
  fi
fi

# --- Generate values ---
WP_UUID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
WP_HOSTNAME="$(hostname -s 2>/dev/null || hostname)"
WP_CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

if [[ -z "$WP_NAME" ]]; then
  WP_NAME="$(basename "$TARGET_PATH")"
fi

# --- Detect parent workplace ---
PARENT_UUID=""
PARENT_DIR="$(dirname "$TARGET_PATH")"
while [[ "$PARENT_DIR" != "/" ]]; do
  if [[ -f "$PARENT_DIR/.workplace/config.json" ]]; then
    PARENT_UUID="$(jq -r '.uuid' "$PARENT_DIR/.workplace/config.json" 2>/dev/null || true)"
    break
  fi
  PARENT_DIR="$(dirname "$PARENT_DIR")"
done

# --- Create .workplace structure ---
WP_DIR="$TARGET_PATH/.workplace"
mkdir -p "$WP_DIR"/{agents,memory,skills,deploy}

# Config
sed -e "s|__UUID__|$WP_UUID|g" \
    -e "s|__NAME__|$WP_NAME|g" \
    -e "s|__PATH__|$TARGET_PATH|g" \
    -e "s|__HOSTNAME__|$WP_HOSTNAME|g" \
    -e "s|__CREATED__|$WP_CREATED|g" \
    "$TEMPLATES/config.json" > "$WP_DIR/config.json"

# Set description if provided
if [[ -n "$WP_DESC" ]]; then
  jq --arg desc "$WP_DESC" '.description = $desc' "$WP_DIR/config.json" > "$WP_DIR/config.tmp" \
    && mv "$WP_DIR/config.tmp" "$WP_DIR/config.json"
fi

# Set parent if found
if [[ -n "$PARENT_UUID" ]]; then
  jq --arg parent "$PARENT_UUID" '.parent = $parent' "$WP_DIR/config.json" > "$WP_DIR/config.tmp" \
    && mv "$WP_DIR/config.tmp" "$WP_DIR/config.json"
fi

# Mark as parent workspace if no .git but has children
if [[ "$IS_PARENT" == true ]]; then
  jq '.type = "parent"' "$WP_DIR/config.json" > "$WP_DIR/config.tmp" \
    && mv "$WP_DIR/config.tmp" "$WP_DIR/config.json"
fi

# Kernel agent
cp "$TEMPLATES/kernel.md" "$WP_DIR/agents/kernel.md"

# Deploy templates
cp "$TEMPLATES/deploy/dev.md" "$WP_DIR/deploy/dev.md"
cp "$TEMPLATES/deploy/main.md" "$WP_DIR/deploy/main.md"
cp "$TEMPLATES/deploy/pre.md" "$WP_DIR/deploy/pre.md"

# Empty chat.md
touch "$WP_DIR/chat.md"

# Initial process-status.json
cat > "$WP_DIR/process-status.json" << 'EOF'
{}
EOF

# --- Scan structure ---
IGNORE_PATTERN='node_modules|vendor|\.next|dist|build|target|__pycache__|\.venv|\.git|\.workplace'

generate_structure() {
  local dir="$1"
  local prefix="$2"
  find "$dir" -maxdepth 1 -mindepth 1 -not -name '.*' 2>/dev/null | sort | while read -r entry; do
    local name="$(basename "$entry")"
    # Skip ignored patterns
    if echo "$name" | grep -qE "^($IGNORE_PATTERN)$"; then
      continue
    fi
    if [[ -d "$entry" ]]; then
      echo "{\"path\":\"${prefix}${name}/\",\"type\":\"dir\"}"
      generate_structure "$entry" "${prefix}${name}/"
    else
      echo "{\"path\":\"${prefix}${name}\",\"type\":\"file\"}"
    fi
  done
}

echo "[" > "$WP_DIR/structure.json"
generate_structure "$TARGET_PATH" "" | paste -sd',' - >> "$WP_DIR/structure.json"
echo "]" >> "$WP_DIR/structure.json"

# Fix JSON (handle empty case)
if [[ "$(wc -l < "$WP_DIR/structure.json")" -le 2 ]]; then
  echo "[]" > "$WP_DIR/structure.json"
fi

# --- Generate full-tree.md ---
# Workspace-level view: list this workplace and its linked/parent workplaces (no file trees)
{
  echo "# Full Workspace Tree"
  echo ""
  echo "## Host: $WP_HOSTNAME"
  echo ""
  echo "### $WP_NAME ($WP_UUID)"
  echo "\`$TARGET_PATH\`"

  # Parent workplace
  if [[ -n "$PARENT_UUID" ]]; then
    PARENT_NAME="$(jq -r '.name // "unknown"' "$PARENT_DIR/.workplace/config.json" 2>/dev/null || echo "unknown")"
    echo ""
    echo "**Parent:** $PARENT_NAME ($PARENT_UUID)"
    echo "\`$PARENT_DIR\`"
  fi

  # Linked workplaces (read from registry)
  if [[ -f "$REGISTRY_DIR/registry.json" ]]; then
    # Get linked UUIDs from config (if config already written)
    if [[ -f "$WP_DIR/config.json" ]]; then
      LINKED_UUIDS="$(jq -r '.linked[]? // empty' "$WP_DIR/config.json" 2>/dev/null)"
      if [[ -n "$LINKED_UUIDS" ]]; then
        echo ""
        echo "**Linked:**"
        echo "$LINKED_UUIDS" | while IFS= read -r luuid; do
          LNAME="$(jq -r --arg u "$luuid" '.[] | select(.uuid == $u) | .name // "unknown"' "$REGISTRY_DIR/registry.json" 2>/dev/null)"
          LPATH="$(jq -r --arg u "$luuid" '.[] | select(.uuid == $u) | .path // "unknown"' "$REGISTRY_DIR/registry.json" 2>/dev/null)"
          echo "- $LNAME ($luuid) \`$LPATH\`"
        done
      fi
    fi
  fi

  # Sibling workplaces (other .workplace dirs in the same parent directory)
  SIBLINGS_DIR="$(dirname "$TARGET_PATH")"
  FOUND_SIBLINGS=false
  for sibling in "$SIBLINGS_DIR"/*/; do
    sibling="$(cd "$sibling" 2>/dev/null && pwd || true)"
    [[ -z "$sibling" ]] && continue
    [[ "$sibling" == "$TARGET_PATH" ]] && continue
    if [[ -f "$sibling/.workplace/config.json" ]]; then
      if [[ "$FOUND_SIBLINGS" == false ]]; then
        echo ""
        echo "**Siblings:**"
        FOUND_SIBLINGS=true
      fi
      SNAME="$(jq -r '.name // "unknown"' "$sibling/.workplace/config.json" 2>/dev/null)"
      SUUID="$(jq -r '.uuid // "unknown"' "$sibling/.workplace/config.json" 2>/dev/null)"
      echo "- $SNAME ($SUUID) \`$sibling\`"
    fi
  done
} > "$WP_DIR/full-tree.md"

# --- Register in central registry ---
mkdir -p "$REGISTRY_DIR"

if [[ ! -f "$REGISTRY_DIR/registry.json" ]]; then
  echo "[]" > "$REGISTRY_DIR/registry.json"
fi

# Add to registry
jq --arg uuid "$WP_UUID" \
   --arg name "$WP_NAME" \
   --arg path "$TARGET_PATH" \
   --arg hostname "$WP_HOSTNAME" \
   --arg created "$WP_CREATED" \
   --arg parent "$PARENT_UUID" \
   '. += [{
     "uuid": $uuid,
     "name": $name,
     "path": $path,
     "hostname": $hostname,
     "created": $created,
     "parent": (if $parent == "" then null else $parent end),
     "linked": [],
     "lastActive": $created
   }]' "$REGISTRY_DIR/registry.json" > "$REGISTRY_DIR/registry.tmp" \
  && mv "$REGISTRY_DIR/registry.tmp" "$REGISTRY_DIR/registry.json"

# Set as current
cat > "$REGISTRY_DIR/current.json" << EOF
{"uuid":"$WP_UUID","path":"$TARGET_PATH"}
EOF

# --- Summary ---
echo ""
echo "✅ Workplace initialized!"
echo "   Name:     $WP_NAME"
echo "   UUID:     $WP_UUID"
echo "   Path:     $TARGET_PATH"
echo "   Hostname: $WP_HOSTNAME"
if [[ -n "$PARENT_UUID" ]]; then
  echo "   Parent:   $PARENT_UUID"
fi
echo ""
echo "   .workplace/"
echo "   ├── config.json"
echo "   ├── agents/kernel.md"
echo "   ├── memory/"
echo "   ├── skills/"
echo "   ├── deploy/{dev,main,pre}.md"
echo "   ├── chat.md"
echo "   ├── structure.json"
echo "   ├── full-tree.md"
echo "   └── process-status.json"

# --- Auto-init children if parent workspace ---
if [[ "$IS_PARENT" == true && ${#CHILD_DIRS[@]} -gt 0 ]]; then
  echo ""
  echo "🔄 Initializing child workplaces..."
  echo ""

  CHILD_UUIDS=()

  for child_dir in "${CHILD_DIRS[@]}"; do
    CHILD_NAME="$(basename "$child_dir")"

    if [[ -d "$child_dir/.workplace" ]]; then
      echo "   ⚠️  $CHILD_NAME — already initialized"
      CHILD_UUID="$(jq -r '.uuid' "$child_dir/.workplace/config.json" 2>/dev/null || true)"
      CHILD_UUIDS+=("$CHILD_UUID")

      # Update parent reference if not set
      EXISTING_PARENT="$(jq -r '.parent // empty' "$child_dir/.workplace/config.json" 2>/dev/null || true)"
      if [[ -z "$EXISTING_PARENT" ]]; then
        jq --arg parent "$WP_UUID" '.parent = $parent' "$child_dir/.workplace/config.json" > "$child_dir/.workplace/config.tmp" \
          && mv "$child_dir/.workplace/config.tmp" "$child_dir/.workplace/config.json"
        echo "      → Set parent to $WP_NAME ($WP_UUID)"
      fi
    else
      # Init child — reuse this script recursively
      "$0" "$child_dir" --name "$CHILD_NAME"
      CHILD_UUID="$(jq -r '.uuid' "$child_dir/.workplace/config.json" 2>/dev/null || true)"
      CHILD_UUIDS+=("$CHILD_UUID")

      # Set parent reference on the child
      jq --arg parent "$WP_UUID" '.parent = $parent' "$child_dir/.workplace/config.json" > "$child_dir/.workplace/config.tmp" \
        && mv "$child_dir/.workplace/config.tmp" "$child_dir/.workplace/config.json"
    fi
  done

  # Cross-link all children with each other
  if [[ ${#CHILD_UUIDS[@]} -gt 1 ]]; then
    echo ""
    echo "🔗 Cross-linking ${#CHILD_UUIDS[@]} child workplaces..."

    for child_dir in "${CHILD_DIRS[@]}"; do
      if [[ ! -f "$child_dir/.workplace/config.json" ]]; then
        continue
      fi
      CHILD_UUID="$(jq -r '.uuid' "$child_dir/.workplace/config.json" 2>/dev/null)"

      for other_uuid in "${CHILD_UUIDS[@]}"; do
        if [[ "$other_uuid" != "$CHILD_UUID" && -n "$other_uuid" ]]; then
          # Add to linked if not already there
          ALREADY_LINKED="$(jq --arg u "$other_uuid" '[.linked[]? | select(. == $u)] | length' "$child_dir/.workplace/config.json" 2>/dev/null || echo "0")"
          if [[ "$ALREADY_LINKED" == "0" ]]; then
            jq --arg u "$other_uuid" '.linked += [$u]' "$child_dir/.workplace/config.json" > "$child_dir/.workplace/config.tmp" \
              && mv "$child_dir/.workplace/config.tmp" "$child_dir/.workplace/config.json"
          fi
        fi
      done
    done
    echo "   ✅ All children linked"
  fi

  # Add all children to parent's linked list
  for cuuid in "${CHILD_UUIDS[@]}"; do
    if [[ -n "$cuuid" ]]; then
      ALREADY_LINKED="$(jq --arg u "$cuuid" '[.linked[]? | select(. == $u)] | length' "$WP_DIR/config.json" 2>/dev/null || echo "0")"
      if [[ "$ALREADY_LINKED" == "0" ]]; then
        jq --arg u "$cuuid" '.linked += [$u]' "$WP_DIR/config.json" > "$WP_DIR/config.tmp" \
          && mv "$WP_DIR/config.tmp" "$WP_DIR/config.json"
      fi
    fi
  done

  # Regenerate full-tree.md for parent (now with children)
  {
    echo "# Full Workspace Tree"
    echo ""
    echo "## Host: $WP_HOSTNAME"
    echo ""
    echo "### $WP_NAME ($WP_UUID) [parent]"
    echo "\`$TARGET_PATH\`"
    echo ""
    echo "**Children:**"
    for child_dir in "${CHILD_DIRS[@]}"; do
      if [[ -f "$child_dir/.workplace/config.json" ]]; then
        CNAME="$(jq -r '.name // "unknown"' "$child_dir/.workplace/config.json" 2>/dev/null)"
        CUUID="$(jq -r '.uuid // "unknown"' "$child_dir/.workplace/config.json" 2>/dev/null)"
        echo "- $CNAME ($CUUID) \`$child_dir\`"
      fi
    done
  } > "$WP_DIR/full-tree.md"

  # Regenerate full-tree.md for each child (now with parent + siblings)
  for child_dir in "${CHILD_DIRS[@]}"; do
    if [[ ! -f "$child_dir/.workplace/config.json" ]]; then
      continue
    fi
    CNAME="$(jq -r '.name' "$child_dir/.workplace/config.json" 2>/dev/null)"
    CUUID="$(jq -r '.uuid' "$child_dir/.workplace/config.json" 2>/dev/null)"
    {
      echo "# Full Workspace Tree"
      echo ""
      echo "## Host: $WP_HOSTNAME"
      echo ""
      echo "### $CNAME ($CUUID)"
      echo "\`$child_dir\`"
      echo ""
      echo "**Parent:** $WP_NAME ($WP_UUID) \`$TARGET_PATH\`"

      # Siblings
      FOUND_SIBS=false
      for sib_dir in "${CHILD_DIRS[@]}"; do
        [[ "$sib_dir" == "$child_dir" ]] && continue
        if [[ -f "$sib_dir/.workplace/config.json" ]]; then
          if [[ "$FOUND_SIBS" == false ]]; then
            echo ""
            echo "**Siblings:**"
            FOUND_SIBS=true
          fi
          SNAME="$(jq -r '.name // "unknown"' "$sib_dir/.workplace/config.json" 2>/dev/null)"
          SUUID="$(jq -r '.uuid // "unknown"' "$sib_dir/.workplace/config.json" 2>/dev/null)"
          echo "- $SNAME ($SUUID) \`$sib_dir\`"
        fi
      done
    } > "$child_dir/.workplace/full-tree.md"
  done

  echo ""
  echo "✅ Parent workspace with ${#CHILD_DIRS[@]} children initialized and linked"

  # Set current back to parent
  cat > "$REGISTRY_DIR/current.json" << EOF
{"uuid":"$WP_UUID","path":"$TARGET_PATH"}
EOF
fi


path: scripts/loaded_workplaces.sh
content: #!/usr/bin/env bash
set -euo pipefail

# loaded_workplaces.sh — Manage loaded/open workplaces
# Usage:
#   loaded_workplaces.sh list                          — List all loaded workplaces
#   loaded_workplaces.sh load <path|uuid>              — Load a workplace (must be registered)
#   loaded_workplaces.sh unload <name|uuid>            — Unload a workplace
#   loaded_workplaces.sh status                        — Summary of loaded workplaces
#
# Loaded workplaces are tracked in ~/.openclaw/workspace/.workplaces/loaded.json
# They represent workplaces that are currently "open" and available for quick access,
# agent orchestration, and cross-workspace operations.

REGISTRY_DIR="$HOME/.openclaw/workspace/.workplaces"
REGISTRY="$REGISTRY_DIR/registry.json"
LOADED="$REGISTRY_DIR/loaded.json"

# Ensure files exist
mkdir -p "$REGISTRY_DIR"
[[ -f "$REGISTRY" ]] || echo "[]" > "$REGISTRY"
[[ -f "$LOADED" ]] || echo "[]" > "$LOADED"

ACTION="${1:-list}"
shift || true

case "$ACTION" in
  list)
    COUNT=$(jq 'length' "$LOADED")
    if [[ "$COUNT" == "0" ]]; then
      echo "No workplaces currently loaded."
      exit 0
    fi
    echo "📂 Loaded workplaces ($COUNT):"
    echo ""
    jq -r '.[] | "  • \(.name) (\(.uuid | .[0:8])...)\n    Path: \(.path)\n    Loaded: \(.loadedAt)\n"' "$LOADED"
    ;;

  load)
    TARGET="${1:-}"
    if [[ -z "$TARGET" ]]; then
      echo "Usage: loaded_workplaces.sh load <path|uuid|name>"
      exit 1
    fi

    # Resolve target — try as path first, then uuid, then name
    RESOLVED=""
    if [[ -d "$TARGET" ]]; then
      TARGET_PATH="$(cd "$TARGET" && pwd)"
      RESOLVED=$(jq -r --arg p "$TARGET_PATH" '.[] | select(.path == $p) | @json' "$REGISTRY" 2>/dev/null | head -1)
    fi
    if [[ -z "$RESOLVED" ]]; then
      RESOLVED=$(jq -r --arg t "$TARGET" '.[] | select(.uuid == $t or (.uuid | startswith($t)) or .name == $t) | @json' "$REGISTRY" 2>/dev/null | head -1)
    fi

    if [[ -z "$RESOLVED" ]]; then
      echo "❌ Workplace not found in registry: $TARGET"
      echo "   Run 'workplace init <path>' or 'workplace scan <path> --register' first."
      exit 1
    fi

    WP_UUID=$(echo "$RESOLVED" | jq -r '.uuid')
    WP_NAME=$(echo "$RESOLVED" | jq -r '.name')
    WP_PATH=$(echo "$RESOLVED" | jq -r '.path')

    # Check if already loaded
    ALREADY=$(jq --arg u "$WP_UUID" '[.[] | select(.uuid == $u)] | length' "$LOADED")
    if [[ "$ALREADY" != "0" ]]; then
      echo "⚠️  $WP_NAME is already loaded."
      exit 0
    fi

    # Check that .workplace/config.json exists
    if [[ ! -f "$WP_PATH/.workplace/config.json" ]]; then
      echo "❌ No .workplace/ found at $WP_PATH"
      echo "   Initialize it first: workplace init $WP_PATH"
      exit 1
    fi

    # Add to loaded.json
    NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    jq --arg uuid "$WP_UUID" \
       --arg name "$WP_NAME" \
       --arg path "$WP_PATH" \
       --arg now "$NOW" \
       '. += [{
         "uuid": $uuid,
         "name": $name,
         "path": $path,
         "loadedAt": $now,
         "source": "manual"
       }]' "$LOADED" > "$LOADED.tmp" && mv "$LOADED.tmp" "$LOADED"

    echo "✅ Loaded: $WP_NAME"
    echo "   UUID: $WP_UUID"
    echo "   Path: $WP_PATH"
    ;;

  unload)
    TARGET="${1:-}"
    if [[ -z "$TARGET" ]]; then
      echo "Usage: loaded_workplaces.sh unload <name|uuid>"
      exit 1
    fi

    # Find in loaded.json
    MATCH=$(jq -r --arg t "$TARGET" '.[] | select(.uuid == $t or (.uuid | startswith($t)) or .name == $t) | .uuid' "$LOADED" | head -1)

    if [[ -z "$MATCH" ]]; then
      echo "❌ Not found in loaded workplaces: $TARGET"
      exit 1
    fi

    WP_NAME=$(jq -r --arg u "$MATCH" '.[] | select(.uuid == $u) | .name' "$LOADED")

    jq --arg u "$MATCH" '[.[] | select(.uuid != $u)]' "$LOADED" > "$LOADED.tmp" && mv "$LOADED.tmp" "$LOADED"

    echo "✅ Unloaded: $WP_NAME ($MATCH)"
    ;;

  status)
    COUNT=$(jq 'length' "$LOADED")
    echo "📊 Loaded Workplaces: $COUNT"
    echo ""
    if [[ "$COUNT" != "0" ]]; then
      jq -r '.[] | "  \(.name) — \(.path)"' "$LOADED"
    fi

    # Show current active
    if [[ -f "$REGISTRY_DIR/current.json" ]]; then
      CURRENT_UUID=$(jq -r '.uuid' "$REGISTRY_DIR/current.json")
      CURRENT_NAME=$(jq -r --arg u "$CURRENT_UUID" '.[] | select(.uuid == $u) | .name // "unknown"' "$REGISTRY")
      echo ""
      echo "  Active: $CURRENT_NAME ($CURRENT_UUID)"
    fi
    ;;

  *)
    echo "Usage: loaded_workplaces.sh <list|load|unload|status>"
    exit 1
    ;;
esac


path: scripts/rust-server/Cargo.toml
content: [package]
name = "workplace-server"
version = "0.1.0"
edition = "2021"
description = "File-watcher server for inter-agent communication via chat.md"

[[bin]]
name = "workplace-server"
path = "src/main.rs"

[dependencies]
notify = "7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
regex = "1"
ctrlc = "3"

[profile.release]
opt-level = "s"
lto = true
strip = true


path: scripts/rust-server/src/main.rs
content: use chrono::Utc;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::time::Duration;

#[derive(Debug, Serialize, Deserialize)]
struct ChatMessage {
    timestamp: String,
    sender: String,
    recipient: String,
    broadcast: Vec<String>,
    message: String,
    line_number: usize,
}

#[derive(Debug, Serialize, Deserialize)]
struct ProcessStatus {
    #[serde(default)]
    server: ServerStatus,
}

#[derive(Debug, Serialize, Deserialize, Default)]
struct ServerStatus {
    status: String,
    pid: u32,
    #[serde(rename = "startedAt")]
    started_at: String,
    #[serde(rename = "lastEvent")]
    last_event: Option<String>,
    error: Option<String>,
}

fn parse_chat_line(line: &str) -> Option<ChatMessage> {
    // Pattern: [sender-to-recipient]: message
    // Pattern: [sender-to-recipient] @ [target1, target2]: message
    let re = Regex::new(
        r"^\[([a-zA-Z0-9_-]+)-to-([a-zA-Z0-9_-]+)\](?:\s*@\s*\[([^\]]*)\])?\s*:\s*(.+)$",
    )
    .ok()?;

    let caps = re.captures(line.trim())?;

    let sender = caps.get(1)?.as_str().to_string();
    let recipient = caps.get(2)?.as_str().to_string();

    let broadcast: Vec<String> = caps
        .get(3)
        .map(|m| {
            m.as_str()
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect()
        })
        .unwrap_or_default();

    let message = caps.get(4)?.as_str().to_string();

    Some(ChatMessage {
        timestamp: Utc::now().to_rfc3339(),
        sender,
        recipient,
        broadcast,
        message,
        line_number: 0,
    })
}

fn write_process_status(workplace_dir: &Path, status: &str, error: Option<&str>) {
    let status_path = workplace_dir.join("process-status.json");

    // Read existing status or create new
    let mut proc_status: serde_json::Value =
        if let Ok(content) = fs::read_to_string(&status_path) {
            serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
        } else {
            serde_json::json!({})
        };

    proc_status["server"] = serde_json::json!({
        "status": status,
        "pid": std::process::id(),
        "startedAt": Utc::now().to_rfc3339(),
        "lastEvent": Utc::now().to_rfc3339(),
        "error": error,
    });

    if let Ok(json) = serde_json::to_string_pretty(&proc_status) {
        let _ = fs::write(&status_path, json);
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: workplace-server <path-to-workplace-dir>");
        eprintln!("  Watches .workplace/chat.md for inter-agent messages");
        std::process::exit(1);
    }

    let workplace_dir = PathBuf::from(&args[1]);
    let wp_internal = workplace_dir.join(".workplace");
    let chat_file = wp_internal.join("chat.md");

    if !wp_internal.exists() {
        eprintln!(
            "Error: No .workplace directory found at {}",
            workplace_dir.display()
        );
        std::process::exit(1);
    }

    // Create chat.md if it doesn't exist
    if !chat_file.exists() {
        if let Err(e) = fs::write(&chat_file, "") {
            eprintln!("Error creating chat.md: {}", e);
            std::process::exit(1);
        }
    }

    // Graceful shutdown
    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();
    ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    })
    .expect("Error setting Ctrl-C handler");

    // Write initial status
    write_process_status(&wp_internal, "running", None);

    eprintln!(
        "🔄 Workplace server started — watching {}",
        chat_file.display()
    );

    // Track file position to only read new lines
    let mut last_pos: u64 = fs::metadata(&chat_file)
        .map(|m| m.len())
        .unwrap_or(0);
    let mut line_count: usize = BufReader::new(fs::File::open(&chat_file).unwrap_or_else(|_| {
        eprintln!("Error opening chat.md");
        std::process::exit(1);
    }))
    .lines()
    .count();

    // Set up file watcher
    let (tx, rx) = channel();

    let mut watcher: RecommendedWatcher =
        Watcher::new(tx, Config::default().with_poll_interval(Duration::from_millis(500)))
            .unwrap_or_else(|e| {
                eprintln!("Error creating watcher: {}", e);
                write_process_status(&wp_internal, "error", Some(&format!("{}", e)));
                std::process::exit(1);
            });

    if let Err(e) = watcher.watch(&chat_file, RecursiveMode::NonRecursive) {
        eprintln!("Error watching chat.md: {}", e);
        write_process_status(&wp_internal, "error", Some(&format!("{}", e)));
        std::process::exit(1);
    }

    while running.load(Ordering::SeqCst) {
        match rx.recv_timeout(Duration::from_secs(1)) {
            Ok(Ok(_event)) => {
                // File changed — read new lines
                let file = match fs::File::open(&chat_file) {
                    Ok(f) => f,
                    Err(e) => {
                        eprintln!("Error opening chat.md: {}", e);
                        continue;
                    }
                };

                let current_len = file.metadata().map(|m| m.len()).unwrap_or(0);

                if current_len < last_pos {
                    // File was truncated — reset
                    last_pos = 0;
                    line_count = 0;
                }

                if current_len > last_pos {
                    let mut reader = BufReader::new(&file);
                    if let Err(e) = reader.seek(SeekFrom::Start(last_pos)) {
                        eprintln!("Error seeking: {}", e);
                        continue;
                    }

                    let mut new_lines = Vec::new();
                    for line in reader.lines() {
                        match line {
                            Ok(l) => {
                                if !l.trim().is_empty() {
                                    new_lines.push(l);
                                }
                            }
                            Err(e) => {
                                eprintln!("Error reading line: {}", e);
                                break;
                            }
                        }
                    }

                    for line in &new_lines {
                        line_count += 1;
                        if let Some(mut msg) = parse_chat_line(line) {
                            msg.line_number = line_count;
                            if let Ok(json) = serde_json::to_string(&msg) {
                                println!("{}", json);
                                // Flush stdout for immediate output
                                use std::io::Write;
                                let _ = io::stdout().flush();
                            }
                        }
                    }

                    last_pos = current_len;

                    // Update status
                    write_process_status(&wp_internal, "running", None);
                }
            }
            Ok(Err(e)) => {
                eprintln!("Watch error: {}", e);
                write_process_status(
                    &wp_internal,
                    "error",
                    Some(&format!("Watch error: {}", e)),
                );
            }
            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
                // Normal timeout — continue loop
            }
            Err(e) => {
                eprintln!("Channel error: {}", e);
                break;
            }
        }
    }

    eprintln!("🛑 Workplace server shutting down");
    write_process_status(&wp_internal, "stopped", None);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_message() {
        let line = "[coder-to-reviewer]: Please review the auth module";
        let msg = parse_chat_line(line).unwrap();
        assert_eq!(msg.sender, "coder");
        assert_eq!(msg.recipient, "reviewer");
        assert!(msg.broadcast.is_empty());
        assert_eq!(msg.message, "Please review the auth module");
    }

    #[test]
    fn test_parse_broadcast_message() {
        let line = "[reviewer-to-coder] @ [developer, manager]: Approved with minor changes";
        let msg = parse_chat_line(line).unwrap();
        assert_eq!(msg.sender, "reviewer");
        assert_eq!(msg.recipient, "coder");
        assert_eq!(msg.broadcast, vec!["developer", "manager"]);
        assert_eq!(msg.message, "Approved with minor changes");
    }

    #[test]
    fn test_parse_single_broadcast() {
        let line = "[coder-to-reviewer] @ [developer]: Done with implementation";
        let msg = parse_chat_line(line).unwrap();
        assert_eq!(msg.broadcast, vec!["developer"]);
    }

    #[test]
    fn test_parse_invalid_line() {
        assert!(parse_chat_line("just some random text").is_none());
        assert!(parse_chat_line("").is_none());
        assert!(parse_chat_line("# Header").is_none());
    }

    #[test]
    fn test_parse_with_special_chars_in_message() {
        let line = "[agent1-to-agent2]: Check `src/main.rs` — found 3 issues!";
        let msg = parse_chat_line(line).unwrap();
        assert_eq!(msg.message, "Check `src/main.rs` — found 3 issues!");
    }
}


path: scripts/scan_workplaces.sh
content: #!/usr/bin/env bash
set -euo pipefail

# scan_workplaces.sh — Find all .git directories under a root path
# Usage: scan_workplaces.sh [path] [--max-depth N] [--register]
# Output: JSON array of discovered workplaces

REGISTRY_DIR="$HOME/.openclaw/workspace/.workplaces"
SCAN_PATH="."
MAX_DEPTH=5
DO_REGISTER=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --max-depth) MAX_DEPTH="$2"; shift 2 ;;
    --register) DO_REGISTER=true; shift ;;
    *) SCAN_PATH="$1"; shift ;;
  esac
done

SCAN_PATH="$(cd "$SCAN_PATH" 2>/dev/null && pwd || echo "$SCAN_PATH")"

# Ignore patterns for find
PRUNE_DIRS="node_modules|vendor|\.next|dist|build|target|__pycache__|\.venv"

# Find all .git directories
RESULTS="[]"

while IFS= read -r git_dir; do
  WP_PATH="$(dirname "$git_dir")"
  WP_NAME="$(basename "$WP_PATH")"
  HAS_WORKPLACE=false
  WP_UUID=""

  if [[ -d "$WP_PATH/.workplace" ]]; then
    HAS_WORKPLACE=true
    WP_UUID="$(jq -r '.uuid // empty' "$WP_PATH/.workplace/config.json" 2>/dev/null || true)"
  fi

  # Check for submodules
  IS_SUBMODULE=false
  if [[ -f "$git_dir" ]]; then
    # .git is a file (gitdir pointer) = submodule
    IS_SUBMODULE=true
  fi

  RESULTS="$(echo "$RESULTS" | jq --arg path "$WP_PATH" \
    --arg name "$WP_NAME" \
    --argjson hasWorkplace "$HAS_WORKPLACE" \
    --argjson isSubmodule "$IS_SUBMODULE" \
    --arg uuid "$WP_UUID" \
    '. += [{
      "path": $path,
      "name": $name,
      "hasWorkplace": $hasWorkplace,
      "isSubmodule": $isSubmodule,
      "uuid": (if $uuid == "" then null else $uuid end)
    }]')"
done < <(find "$SCAN_PATH" -maxdepth "$MAX_DEPTH" \
  -name ".git" \
  -not -path "*/node_modules/*" \
  -not -path "*/vendor/*" \
  -not -path "*/.next/*" \
  -not -path "*/dist/*" \
  -not -path "*/build/*" \
  -not -path "*/target/*" \
  -not -path "*/__pycache__/*" \
  -not -path "*/.venv/*" \
  2>/dev/null | sort)

# Output
echo "$RESULTS" | jq '.'

# Summary
COUNT=$(echo "$RESULTS" | jq 'length')
INIT_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.hasWorkplace)] | length')
NEW_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.hasWorkplace | not)] | length')
SUB_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.isSubmodule)] | length')

echo "" >&2
echo "📊 Found $COUNT workplaces ($INIT_COUNT initialized, $NEW_COUNT new, $SUB_COUNT submodules)" >&2

# Register if requested
if [[ "$DO_REGISTER" == true ]]; then
  mkdir -p "$REGISTRY_DIR"
  if [[ ! -f "$REGISTRY_DIR/registry.json" ]]; then
    echo "[]" > "$REGISTRY_DIR/registry.json"
  fi

  REGISTERED=0
  echo "$RESULTS" | jq -c '.[] | select(.hasWorkplace)' | while IFS= read -r wp; do
    UUID="$(echo "$wp" | jq -r '.uuid')"
    # Check if already registered
    EXISTS="$(jq --arg uuid "$UUID" '[.[] | select(.uuid == $uuid)] | length' "$REGISTRY_DIR/registry.json")"
    if [[ "$EXISTS" == "0" && "$UUID" != "null" ]]; then
      WP_PATH="$(echo "$wp" | jq -r '.path')"
      WP_NAME="$(echo "$wp" | jq -r '.name')"
      WP_HOSTNAME="$(hostname -s 2>/dev/null || hostname)"
      WP_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
      jq --arg uuid "$UUID" \
         --arg name "$WP_NAME" \
         --arg path "$WP_PATH" \
         --arg hostname "$WP_HOSTNAME" \
         --arg now "$WP_NOW" \
         '. += [{
           "uuid": $uuid,
           "name": $name,
           "path": $path,
           "hostname": $hostname,
           "created": $now,
           "parent": null,
           "linked": [],
           "lastActive": $now
         }]' "$REGISTRY_DIR/registry.json" > "$REGISTRY_DIR/registry.tmp" \
        && mv "$REGISTRY_DIR/registry.tmp" "$REGISTRY_DIR/registry.json"
      REGISTERED=$((REGISTERED + 1))
      echo "  ✅ Registered: $WP_NAME ($UUID)" >&2
    fi
  done
fi


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)