{"slug": "show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older", "title": "Show HN: Agent Office (Slack for AI Agents) – Similar to Grok Bot but older", "summary": "Agent Office, a multi-agent workspace manager built on Pi, orchestrates AI coding agents with tick-based scheduling, priority queues, inbox IPC, cross-agent file access, watchdog monitoring, cron jobs, optional Docker sandbox isolation, and declarative YAML configuration. The tool, similar to Claude Code or OpenClaw, supports teams like basic-team, OpenServ team, and feature team, and requires pnpm install and environment setup. It offers in-process or Docker sandbox modes and includes a Web UI.", "body_md": "Multi-agent workspace manager built on [Pi](https://github.com/badlogic/pi-mono). Orchestrates AI coding agents — similar to Claude Code or OpenClaw — with tick-based scheduling, priority queues, inbox IPC, cross-agent file access, watchdog monitoring, proactive cron jobs, optional Docker sandbox isolation, and declarative YAML configuration.\n\n## agent-office.mp4\n\nTry one of these examples to get up and running quickly. Set env vars in the project root `.env`\n\n(not inside Docker — the host forwards them to containers).\n\n**Basic team** — PM, coder, and reviewer (uses GitHub Copilot OAuth, no API keys):\n\n```\npnpm install\ncp -r examples/basic-team/ ~/.agent-office/offices/basic-team/\npnpm dev oauth login github-copilot --office basic-team\npnpm dev start --office basic-team --sandbox docker\n```\n\n**OpenServ team** — idea scout, team lead, agent dev, and token launcher:\n\n```\npnpm install\ncp .env.example .env\nmkdir -p ~/.agent-office/offices/openserv-team\ncp examples/openserv-team/office.yaml ~/.agent-office/offices/openserv-team/office.yaml\npnpm dev start --office openserv-team --sandbox docker\nOPENAI_API_KEY=\nWALLET_PRIVATE_KEY=          # EVM wallet key for openserv-labs/skills agents\n```\n\n**Feature team** — task-driven development with Kanban board:\n\n```\ncp -r examples/feature-team/ ~/.agent-office/offices/feature-team/\npnpm dev start --office feature-team\n```\n\nSee [ examples/](/baturyilmaz/agent-office/blob/main/examples) for more details — each has a README describing the setup.\n\n[Architecture](#architecture)[Quick Start](#quick-start)[OAuth Authentication](#oauth-authentication)[Multi-Office Architecture](#multi-office-architecture)[Sandbox Modes](#sandbox-modes)[Commands](#commands)[Agent Tools](#agent-tools)[Concepts](#concepts)[Prompt Inspection](#prompt-inspection)[Cost Tracking](#cost-tracking)[Web UI](#web-ui)[End-to-End Examples](#end-to-end-examples)[Project Structure](#project-structure)[Dependencies](#dependencies)[Development](#development)\n\n``` php\ngraph TD\n    YAML[office.yaml] --> WS\n    CLI[CLI + Web UI] --> WS[Workspace]\n\n    WS --> SCH[Scheduler\\ntick loop]\n    WS --> BUS[MessageBus\\ninboxes]\n    WS --> WD[Watchdog\\nheartbeat]\n    WS --> CRON[CronService\\nscheduled jobs]\n    WS --> TS[TaskService\\nKanban board]\n\n    WS -->|in-process| A[Agent A\\nPi · tools · skills]\n    WS -->|in-process| B[Agent B\\nPi · tools · skills]\n\n    WS -->|Docker sandbox| HA[Host API\\nHTTP :13000]\n    HA <-->|HTTP| SA[Sandbox A\\nDocker · Pi · proxy tools]\n    HA <-->|HTTP| SB[Sandbox B\\nDocker · Pi · proxy tools]\n\n    BUS --> A\n    BUS --> B\n    BUS --> HA\n```\n\n**Core flow:** `office.yaml`\n\n(auto-spawn) / CLI / Web UI / Cron / Agent cron tools / Task notifications -> Workspace -> Scheduler tick -> drain inbox -> dispatch to Pi Agent -> agent runs tools -> response streamed to UI.\n\nEach agent is a full Pi coding agent with its own filesystem workspace, skills, and injected tools (`message_user`\n\n, `post_channel`\n\n, `message_agent`\n\n, `list_agents`\n\n, `read_agent_file`\n\n, `authenticated_fetch`\n\n, `cron_add`\n\n, `cron_remove`\n\n, `cron_list`\n\n, `task_create`\n\n, `task_update`\n\n, `task_list`\n\n, `task_get`\n\n, `task_delete`\n\n, `read_skill`\n\n, `skill_search`\n\n, `skill_install`\n\n, `skill_remove`\n\n, `skill_create`\n\n). The scheduler runs a tick loop that serves agents by priority, one message per tick per agent, non-blocking.\n\nAgents can run **in-process** (default) or inside **Docker containers** for full process-level isolation.\n\n```\npnpm install\n\n# Configure .env\ncp .env.example .env   # then fill in your keys\n\n# Create an office\npnpm dev office create my-team\n\n# Start (Web UI auto-starts)\npnpm dev start --office my-team\n\n# Start with Docker sandbox isolation\npnpm dev start --office my-team --sandbox docker\n```\n\nCreate a `.env`\n\nfile with your provider API keys. Each model requires its corresponding provider key:\n\n```\n# Model API Keys (required for agents using these models)\nOPENAI_API_KEY=sk-...                    # For OpenAI models (gpt-4o, etc.)\nANTHROPIC_API_KEY=sk-...                 # For Anthropic models (Claude, etc.)\nGEMINI_API_KEY=...                       # For Google Gemini models\nXAI_API_KEY=...                          # For xAI Grok models\n\n# Optional: Custom secret refs for office.yaml agents\n# MY_GH_TOKEN=ghp_...                     # Host env vars for authenticated_fetch secrets\n```\n\n**Authentication:** Each model needs credentials. You can use either API keys (`.env`\n\n) or OAuth:\n\n**API keys**— set in`.env`\n\n(e.g.`OPENAI_API_KEY=sk-...`\n\n). Required when the model's provider has no OAuth credentials.**OAuth**— authenticate via provider CLIs before starting. OAuth tokens auto-refresh and don't require`.env`\n\nkeys.\n\n```\n# Option A: API keys in .env\necho \"ANTHROPIC_API_KEY=sk-...\" >> .env\n\n# Option B: OAuth login (requires provider CLI installed)\npnpm dev oauth login anthropic --office my-team\npnpm dev oauth list --office my-team\n```\n\nWhen both OAuth credentials and an API key exist for a provider, you can switch between them per-agent in the Web UI. See [OAuth Authentication](#oauth-authentication) for details.\n\nSee the **Dynamic Model Discovery** section below for how to browse available models and their requirements in the Web UI.\n\nAs an alternative to API keys in `.env`\n\n, agents can authenticate with model providers via OAuth. This uses the provider's own CLI login flow — the agent-office CLI orchestrates the browser-based OAuth handshake and stores credentials per office.\n\n| Provider ID | Name | Flow Type | Requires |\n|---|---|---|---|\n`anthropic` |\nAnthropic | Code paste | Anthropic CLI |\n`openai-codex` |\nOpenAI | Callback server | OpenAI Codex CLI |\n`github-copilot` |\nGitHub Copilot | Code paste | GitHub Copilot CLI |\n`google-gemini-cli` |\nGoogle Gemini CLI | Callback server | Gemini CLI |\n`google-antigravity` |\nAntigravity | Callback server | Antigravity CLI |\n\n**Code paste** providers open a browser URL and prompt you to paste back an auth code. **Callback server** providers start a local HTTP server and complete the flow automatically.\n\n```\n# Login — interactive OAuth flow (opens browser)\npnpm dev oauth login <provider> --office <id>\n\n# List — show all providers and credential status\npnpm dev oauth list --office <id>\n\n# Logout — remove stored credentials\npnpm dev oauth logout <provider> --office <id>\n```\n\n**Example session:**\n\n``` bash\n$ pnpm dev oauth login anthropic --office my-team\n[oauth] Logging in to Anthropic...\n[oauth] Open this URL to authenticate:\n  https://console.anthropic.com/oauth/...\nPaste the authorization code: ****\n[oauth] Credentials saved for Anthropic.\n\n$ pnpm dev oauth list --office my-team\n  ✓ anthropic              Anthropic\n  ✗ openai-codex           OpenAI\n  ✗ github-copilot         GitHub Copilot\n  ✗ google-gemini-cli      Google Gemini CLI\n  ✗ google-antigravity     Antigravity\n\n  Login:   pnpm dev oauth login <provider> --office my-team\n  Logout:  pnpm dev oauth logout <provider> --office my-team\n```\n\nCredentials are stored at `~/.agent-office/offices/<id>/oauth/<provider>.json`\n\nand auto-refresh when tokens expire.\n\nSet the `auth`\n\nfield on an agent to use OAuth instead of an API key:\n\n```\nagents:\n  designer:\n    model: anthropic:claude-sonnet-4-20250514\n    auth: \"oauth:anthropic\" # use OAuth credentials\n  reviewer:\n    model: openai:gpt-4o\n    auth: \"oauth:openai-codex\" # use OAuth credentials\n  analyst:\n    model: google:gemini-2.0-flash\n    # no auth field — falls back to GEMINI_API_KEY from .env\n```\n\nThe `auth`\n\nfield format is `oauth:<provider-id>`\n\n. When set, the agent uses stored OAuth credentials with automatic token refresh instead of a static API key.\n\nWhen OAuth credentials exist for an agent's model provider, the Web UI Config tab shows an **Auth** selector to switch between \"API Key\" and \"OAuth\" modes. The UI also displays all authenticated providers as green badges with one-click removal.\n\nThe auth selector only appears when credentials are available — if no OAuth login has been done for a provider, agents use API keys by default.\n\n| Method | Path | Description |\n|---|---|---|\n`GET` |\n`/api/oauth/providers` |\nList all providers with authentication status |\n`GET` |\n`/api/oauth/status/:id` |\nCheck if credentials exist for a provider |\n`DELETE` |\n`/api/oauth/:id` |\nRemove stored credentials for a provider |\n\nEach office represents a company or team with shared identity, env vars, and secrets. Offices live under `~/.agent-office/offices/<id>/`\n\n.\n\n```\n# Create with default display name (same as id)\npnpm dev office create acme\n\n# Create with a custom display name\npnpm dev office create acme --name \"Acme Corp\"\n```\n\nOffice IDs must be path-safe: lowercase letters, digits, hyphens, underscores (matching `[a-z0-9][a-z0-9_-]*`\n\n). The display name (`office.name`\n\nin YAML) is free-form.\n\nDefine your office once in `~/.agent-office/offices/<id>/office.yaml`\n\nand agents auto-spawn on startup.\n\n```\n# ~/.agent-office/offices/acme/office.yaml\noffice:\n  name: Acme Corp\n  description: \"We build AI-powered widgets\"\n  env:\n    SHARED_API_URL: https://api.acme.com\n  secrets:\n    SHARED_TOKEN: ${ACME_TOKEN}\n  cron:\n    standup:\n      schedule: \"0 9 * * 1-5\"\n      report_channel: general\n      tasks:\n        - title: \"Daily standup\"\n          assignee: pm\n\nagents:\n  designer:\n    model: anthropic:claude-sonnet-4-20250514\n    priority: normal # idle | low | normal | high | critical (or 0-4)\n    thinking: low # off | minimal | low | medium | high | xhigh\n    description: \"Frontend designer — builds HTML/CSS\"\n    prompt_inline: |\n      You are a frontend designer specializing in responsive layouts.\n      Focus on clean, semantic HTML and modern CSS.\n    skills:\n      - nichochar/web-skills\n    auth: \"oauth:anthropic\" # optional — use OAuth instead of API key\n    api_key_ref: MY_CUSTOM_KEY # optional — host env var name for model key override\n    env: # non-sensitive, passed as Docker --env (agent overrides office)\n      LOG_LEVEL: debug\n      WORKSPACE_NAME: designer\n    secrets: # sensitive, ${VAR} refs only — delivered via authenticated_fetch\n      GITHUB_TOKEN: ${MY_GH_TOKEN}\n    disclose_secrets: true # show secret names in system prompt (default: false)\n    permissions:\n      office_cron: true # allow managing office-level cron jobs\n\n  reviewer:\n    model: openai:gpt-4.1\n    priority: high\n    thinking: medium\n    description: \"Code reviewer\"\n```\n\nOffice-level `env`\n\nand `secrets`\n\nare inherited by all agents. Agent-level values override office-level.\n\nAll agent fields are optional. Agents are spawned sequentially in declaration order; if one fails, the rest still start. Model availability depends on your provider account — replace the `model`\n\nvalue with your preferred `provider:model-id`\n\nif the default is unavailable.\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n`model` |\nstring | `anthropic:claude-sonnet-4-20250514` |\n`provider:model-id` |\n`priority` |\nstring | number | `normal` |\nPriority name or 0-4 |\n`thinking` |\nstring | `low` |\n`off` / `minimal` / `low` / `medium` / `high` / `xhigh` |\n`description` |\nstring | `\"\"` |\nVisible to other agents |\n`prompt_inline` |\nstring | (none) |\nCustom instructions (inline text, appended to base prompt) |\n`cwd` |\nstring | `~/.agent-office/offices/<id>/agents/<name>/workspace` |\nWorking directory |\n`skills` |\nstring[] | `[]` |\nGitHub sources to auto-install (`owner/repo` ) |\n`auth` |\nstring | (none — uses API key) |\nAuth mode: `oauth:<provider-id>` for OAuth (see\n|\n`api_key_ref` |\nstring | (auto from provider) |\nHost env var name for model API key |\n`env` |\nmap | `{}` |\nNon-sensitive env vars (Docker `--env` , supports `${VAR}` refs) |\n`secrets` |\nmap | `{}` |\nSecret refs in `${VAR}` format (delivered via `authenticated_fetch` ) |\n`disclose_secrets` |\nboolean | `false` |\nShow secret names in system prompt |\n`cron` |\nmap | `{}` |\nNamed cron jobs (see\n|\n\n`reports_to`\n\n*(none — reports to user)*[Hierarchy](#hierarchy))`permissions`\n\n`{}`\n\n[Permissions](#permissions),[Tool Policy](#tool-policy))`prompt_mode`\n\n`\"full\"`\n\n`full`\n\n(all blocks) or `minimal`\n\n(base + identity + custom only)`on_demand_skills`\n\n`true`\n\n`read_skill`\n\n`heartbeat`\n\n*(none)*[Heartbeat](#heartbeat))**Task tools** (`task_create`\n\n, `task_update`\n\n, `task_list`\n\n, `task_get`\n\n, `task_delete`\n\n) are available to all in-process agents by default. Restrict access via `permissions.tools.deny`\n\n. See [Task Management](#task-management).\n\nAgents can run proactively via heartbeats — periodic messages that prompt agents to check for work or run maintenance without external triggers.\n\n```\nagents:\n  monitor:\n    heartbeat:\n      interval_ms: 60000\n      prompt: \"Check for pending work and report status\"\n      active_hours:\n        start: \"09:00\"\n        end: \"17:00\"\n```\n\n| Field | Required | Default | Description |\n|---|---|---|---|\n`interval_ms` |\nyes | — | Interval in milliseconds between heartbeats |\n`prompt` |\nno | (default) |\nCustom prompt text for heartbeat messages |\n`active_hours` |\nno | (none) |\nRestrict heartbeats to a time window |\n\nHeartbeat messages are injected with `from: \"__heartbeat__\"`\n\nand formatted as `[Heartbeat]\\n<prompt>`\n\n. Busy agents (status `running`\n\n) are skipped.\n\nThe `permissions`\n\nfield controls which privileged operations an agent may perform:\n\n| Permission | Type | Default | Description |\n|---|---|---|---|\n`office_cron` |\nboolean | `false` |\nAllow managing office-level cron jobs via `cron_add` /`cron_remove` |\n\nPermissions are validated at config parse time. Unknown keys or non-boolean values are rejected.\n\nThe `permissions.tools`\n\nfield restricts which tools an agent may use:\n\n```\nagents:\n  restricted-bot:\n    permissions:\n      tools:\n        deny: [cron_add, cron_remove] # blacklist — all except these\n        # OR\n        # allow: [message_agent, list_agents]  # whitelist — only these\n```\n\n— blacklist: agent has all tools except the listed ones.`deny`\n\n— whitelist: agent has only the listed tools.`allow`\n\n- Cannot specify both\n`allow`\n\nand`deny`\n\n— validation error at parse time. - Default (no\n`tools`\n\nfield): all tools available. **Server-side enforcement:** in Docker sandbox mode, denied tools also return HTTP 403 on the corresponding Host API endpoint (e.g.`/api/cron-add`\n\nreturns`403 Tool denied by policy`\n\n).\n\nDefaults: `office_cron`\n\nis **false**; tools are **all allowed** unless `allow`\n\nor `deny`\n\nis set. Setting both `allow`\n\nand `deny`\n\nis a validation error.\n\nView permissions in the Web UI or edit via the API without editing YAML manually:\n\n```\nagent permission show bot\nagent permission set bot office_cron true\nagent permission set bot tools deny cron_add,cron_remove\nagent permission clear bot office_cron\nagent permission clear bot tools\n```\n\nChanges are saved to `office.yaml`\n\n. Run `office reload --force`\n\nto apply.\n\nThe `reports_to`\n\nfield defines a manager for each agent, creating an org tree. Agents without `reports_to`\n\nreport directly to the user. The hierarchy is injected into the system prompt so each agent knows its manager, peers, and direct reports.\n\n```\nagents:\n  lead:\n    description: \"Team lead\"\n  coder:\n    reports_to: lead\n  reviewer:\n    reports_to: lead\n```\n\nValidation rules:\n\n- Must reference a valid agent name (same\n`[a-zA-Z0-9_-]+`\n\nformat) - Self-reference is rejected\n- Cycles are detected and rejected (e.g. A reports to B, B reports to A)\n- Unknown agent references are rejected\n\nHierarchy changes trigger agent restarts (prompts are recomposed with updated context).\n\nCommands automatically keep `office.yaml`\n\nin sync:\n\npersists the agent to YAML (use`hire`\n\n`--ephemeral`\n\nto skip)removes the agent from YAML`fire`\n\nupdates the agent's`skill add/remove`\n\n`skills`\n\narray in YAML (GitHub source model)\n\n`skills.sh`\n\npackage installs (`skill_search`\n\n/ `skill_install`\n\ntools or UI install) write files under `agents/<agent>/skills`\n\nbut do not auto-edit `office.yaml`\n\n.\n\nAll writes are atomic (temp file + rename) and serialized through a two-layer lock (in-process queue + cross-process file lock) per office.\n\n```\n# API command strings (UI has equivalent controls):\noffice reload              # Spawn new agents from YAML, skip already-running\noffice reload --force      # Kill and re-spawn agents with changed config\noffice validate            # Dry-run: parse + validate without spawning\noffice path                # Print path to office.yaml\n```\n\nAgents can run proactively on schedules via per-agent cron jobs. The host-side `CronService`\n\nmanages timers and creates structured tasks via `TaskService`\n\n— giving cron-triggered work full Kanban visibility, dependency chaining, and completion reporting.\n\nBreaking change:`message`\n\nand`targets`\n\nfields have been replaced by a`tasks`\n\narray. Each task requires`title`\n\nand`assignee`\n\n.\n\n```\n# In office.yaml under the agents section:\nagents:\n  standup-bot:\n    model: anthropic:claude-sonnet-4-20250514\n    cron:\n      daily-standup:\n        schedule: \"0 9 * * 1-5\" # 5-field only (min hour dom month dow)\n        timezone: \"America/New_York\" # optional, default UTC\n        catch_up: once # optional: \"skip\" (default) | \"once\"\n        enabled: true # optional, default true\n        report_channel: general # optional, post completion summary to this channel\n        tasks:\n          - title: \"Daily standup report\"\n            description: \"Report your status for today's standup\"\n            assignee: standup-bot\n```\n\n| Field | Required | Default | Description |\n|---|---|---|---|\n`schedule` |\nyes | — | 5-field cron expression (`@daily` /`@hourly` rejected) |\n`tasks` |\nyes | — | Array of task templates; each requires `title` and `assignee` |\n`timezone` |\nno | `UTC` |\nIANA timezone for schedule evaluation |\n`catch_up` |\nno | `skip` |\n`skip` = ignore missed fires on restart; `once` = fire one catch-up task |\n`enabled` |\nno | `true` |\nSet `false` to pause without removing |\n`report_channel` |\nno | (none) |\nChannel name to post task completion summaries to |\n\nEach task in the `tasks`\n\narray supports:\n\n| Field | Required | Description |\n|---|---|---|\n`title` |\nyes | Task title shown in Kanban board |\n`assignee` |\nyes | Agent name to assign the task to |\n`description` |\nno | Detailed instructions for the assignee |\n`parent_id` |\nno | Parent task ID (T-prefixed) to nest under |\n`report_channel` |\nno | Per-task channel override for completion notification |\n\nJob names must match `[a-zA-Z0-9_-]+`\n\n. Each agent can have 0-N named jobs (max 10 per agent via tools).\n\n**Task chaining:** Multiple tasks in a single cron job are automatically chained — each task depends on the previous one completing. The chain fires with `CRITICAL`\n\npriority.\n\n**Catch-up behavior:** On restart, if `catch_up: once`\n\nand a fire was missed since the last run, one immediate task chain is created. First-ever run (no prior state) never catches up. State persists to `~/.agent-office/offices/<id>/cron/state.json`\n\n.\n\n**Safety guards:** Tasks are always created regardless of agent status — they queue in the agent's inbox. A global dispatch cap of 60 cron jobs per minute prevents misconfigured schedules from flooding the task queue.\n\n```\n# API command strings (UI has equivalent controls):\ncron list                                          # List all cron jobs\ncron status [agent]                                # Detailed job status\ncron add <agent> <job> \"<schedule>\" [--apply]      # Add a job (prompts for task title/assignee)\ncron remove <agent> <job> [--apply]                # Remove a job\ncron trigger <agent> <job>                         # Fire immediately\ncron enable <agent> <job> [--apply]                # Re-enable a paused job\ncron disable <agent> <job> [--apply]               # Pause a job\n```\n\nWithout `--apply`\n\n, commands write to `office.yaml`\n\nonly — run `office reload`\n\nto activate. With `--apply`\n\n, changes take effect immediately if the agent is running.\n\nChange detection uses normalized config comparison (resolved model, numeric priority, sorted skills, trimmed prompt) so cosmetic YAML differences like `normal`\n\nvs `2`\n\nor reordered skills don't trigger false warnings.\n\nIn addition to per-agent cron, you can define office-level cron jobs that create task chains across multiple agents:\n\n```\n# In office.yaml under the office section:\noffice:\n  cron:\n    standup:\n      schedule: \"0 9 * * 1-5\"\n      timezone: \"America/New_York\"\n      report_channel: general\n      tasks:\n        - title: \"Daily standup report\"\n          description: \"Report your status for today's standup\"\n          assignee: pm\n        - title: \"Standup review\"\n          description: \"Review and summarize the standup reports\"\n          assignee: lead\n    weekly-review:\n      schedule: \"0 17 * * 5\"\n      tasks:\n        - title: \"Weekly progress summary\"\n          description: \"Summarize this week's progress\"\n          assignee: pm\n```\n\n| Field | Required | Default | Description |\n|---|---|---|---|\n`schedule` |\nyes | — | 5-field cron expression |\n`tasks` |\nyes | — | Array of task templates; each requires `title` and `assignee` |\n`timezone` |\nno | `UTC` |\nIANA timezone for schedule evaluation |\n`catch_up` |\nno | `skip` |\n`skip` = ignore missed fires on restart; `once` = fire one catch-up task |\n`enabled` |\nno | `true` |\nSet `false` to pause without removing |\n`report_channel` |\nno | (none) |\nChannel name to post task completion summaries to |\n\nAssignee names are validated at parse time. Typos fail fast:\n\n```\n[office] office.cron.standup: unknown assignee agent \"codre\"\n```\n\n**Activation:** YAML edits require `office reload`\n\nto take effect. Commands with `--apply`\n\ntake effect immediately.\n\nOffice cron commands:\n\n```\ncron add office <job> \"<schedule>\"           # prompts for task title/assignee\ncron remove office <job>\ncron trigger office <job>                    # fire immediately\n```\n\nOffice jobs appear in `cron list`\n\nwith an `[office]`\n\nscope tag. The global 60/minute dispatch cap applies.\n\nIn addition to operator-managed cron (Web UI/API), agents can self-manage cron jobs via three built-in tools: `cron_add`\n\n, `cron_remove`\n\n, and `cron_list`\n\n. `cron_trigger`\n\nremains operator-only.\n\n**Agent scope** (default) — agents manage their own jobs with no special permission. Max 10 jobs per agent.\n\n```\nagent calls cron_add:\n  name: \"nightly-report\"\n  schedule: \"0 22 * * *\"\n  tasks:\n    - title: \"Generate nightly summary report\"\n      assignee: \"self\"\n\n-> Cron job \"nightly-report\" saved and activated (At 10:00 PM).\n```\n\n**Office scope** — requires `permissions: { office_cron: true }`\n\nin office.yaml. Tasks can be assigned to any agent.\n\n```\nagent calls cron_add:\n  name: \"standup\"\n  schedule: \"0 9 * * 1-5\"\n  scope: \"office\"\n  tasks:\n    - title: \"PM standup report\"\n      description: \"Report your status\"\n      assignee: \"pm\"\n    - title: \"Coder standup report\"\n      description: \"Report your status\"\n      assignee: \"coder\"\n\n-> Cron job \"standup\" saved and activated (At 09:00 AM, Monday through Friday).\n```\n\n**Visibility:** `cron_list`\n\nshows all office-level jobs plus only the calling agent's own agent-scope jobs. No cross-agent visibility.\n\n**Error handling:** Malformed or invalid `office.yaml`\n\nreturns a tool error — no silent success. Validation errors (bad schedule, unknown assignees), parse failures, and permission denials all produce explicit error messages.\n\n**Audit trail:** Every action (success, denial, or error) is logged to `<officeDir>/logs/cron-audit.jsonl`\n\nand printed to stdout with `[cron-audit]`\n\nprefix.\n\n**Security:** Agent-scope writes are isolated to the calling agent's YAML section (identity derived from auth token). All mutations run under `withOfficeLock`\n\nwith race-free activation from the same parsed document.\n\nAgents can create, assign, and track tasks through a shared Kanban-style task system. The `TaskService`\n\nmanages task state, enforces status transitions, resolves dependency chains, and dispatches notifications via the message bus.\n\nTask tools (`task_create`\n\n, `task_update`\n\n, `task_list`\n\n, `task_get`\n\n, `task_delete`\n\n) are registered as default tools for all agents. Restrict access per agent via `permissions.tools.deny`\n\n. Task proxy endpoints are available via Host API.\n\n**Cron integration:** Cron jobs now create tasks instead of sending messages. Tasks fired by cron are tagged with `createdBy: \"__cron__\"`\n\nand appear in the Kanban board with `CRITICAL`\n\npriority. Use `task_list`\n\nwith `createdBy: \"__cron__\"`\n\nto query them. Task completion can trigger a channel notification via the `report_channel`\n\nfield on the task or on the parent cron job definition.\n\nTasks follow a Kanban status flow with enforced transitions:\n\n```\nwaiting → todo → in_progress → done\n                             → failed\n```\n\n| Status | Allowed transitions |\n|---|---|\n`waiting` |\n`todo` |\n`todo` |\n`in_progress` |\n`in_progress` |\n`done` , `failed` |\n`done` |\n(terminal) |\n`failed` |\n(terminal) |\n\nTasks can be deleted from any state. Deleting a task cleans up dependency references and auto-unblocks dependent tasks.\n\n**Dependency behavior:** Tasks created with `dependsOn`\n\nstart in `waiting`\n\nregardless of the requested status. When all dependencies reach `done`\n\n, the `TaskService`\n\nauto-transitions the blocked task to `todo`\n\nand sends a `[Task Ready]`\n\nnotification to the assignee.\n\n**Notifications:** New task assignments dispatch `[New Task]`\n\nmessages. Dependency resolution dispatches `[Task Ready]`\n\nmessages. Both are sent from `__task__`\n\nvia the message bus.\nThe message bus applies a dedicated higher limit for `__task__`\n\nnotifications (`40`\n\nmessages / `30s`\n\n) so task events are less likely to be dropped under bursty updates.\n\n**Audit trail:** All task mutations are logged to `<officeDir>/logs/task-audit.jsonl`\n\n.\n\n**Persistence:** Task state is stored at `<officeDir>/tasks/tasks.json`\n\n.\n\nAgents interact with tasks via four built-in tools:\n\n```\nagent calls task_create:\n  title: \"Implement login page\"\n  description: \"Build login form with email/password fields and validation\"\n  assignee: \"coder\"\n  dependsOn: []\n\n-> Created task T-a1b2c3 (status: todo)\n-> [New Task] notification sent to coder\nagent calls task_update:\n  id: \"T-a1b2c3\"\n  status: \"done\"\n  result: \"Implemented login with email/password auth\"\n\n-> Task T-a1b2c3 updated to done\n-> Dependent tasks auto-transition to todo\nphp\nagent calls task_list:\n  assignee: \"coder\"\n  status: \"in_progress\"\n\n-> Returns filtered list of tasks\nphp\nagent calls task_get:\n  id: \"T-a1b2c3\"\n\n-> Returns full task details (title, description, status, assignee, dependencies, timestamps)\ntask list [--assignee <agent>] [--status <status>]   # List/filter tasks\ntask board                                            # Kanban board view\ntask get <id>                                         # Show task details\n```\n\nThe Web UI includes a Kanban board accessible from the sidebar \"Tasks\" item. Columns: waiting, todo, in_progress, done. Filter by agent using the segmented control. Click a task card to view full details or delete it.\n\nIf you have a legacy `~/.agent-office/agents.yaml`\n\n, migrate to the multi-office format:\n\n```\n# Preview what will happen\npnpm dev office migrate --name my-team --dry-run\n\n# Run the migration (copies data, renames agents.yaml → agents.yaml.bak)\npnpm dev office migrate --name my-team\n\n# Verify everything works\npnpm dev start --office my-team\n\n# Clean up old files (prompts for confirmation)\npnpm dev office migrate --name my-team --finalize\n```\n\nStarting with a legacy `agents.yaml`\n\npresent will fail with a migration prompt.\n\nagent-office supports two execution modes for agents:\n\n```\npnpm dev start --office my-team                  # or explicitly:\npnpm dev start --office my-team --sandbox none\n```\n\nAgents run in the same Node.js process as the scheduler. Simple, fast, zero setup. Tools call directly into the message bus and filesystem.\n\n**Best for:** development, single-user setups, trusted agent code.\n\n```\npnpm dev start --office my-team --sandbox docker\n```\n\nEach agent runs inside an isolated Docker container with hardened security. Agents communicate with the host via HTTP through the Host API.\n\n**Best for:** untrusted agent code, multi-tenant environments, production deployments.\n\n**Requirements:** Docker must be installed and running.\n\n**Build tooling:** The sandbox image includes `python3`\n\n, `make`\n\n, and `g++`\n\nso agents can `npm install`\n\npackages with native addons (node-gyp).\n\n```\nHost Process                        Docker Container (per agent)\n+---------------------------+       +-----------------------------+\n| Workspace                 |       | sandbox-entry.ts            |\n| Scheduler + MessageBus    |       | Pi Agent + coding tools     |\n| Host API server (:13000)  |<-HTTP>| Proxy tools (HTTP->Host)    |\n| DockerProvider            |       | HTTP server (:3100)         |\n| Watchdog                  |       | Heartbeat loop (5s)         |\n+---------------------------+       +-----------------------------+\n```\n\n**Workspace** generates a unique auth token per agent and registers it with the Host API.**DockerProvider** builds the`pi-sandbox`\n\nDocker image (once), then runs a container per agent with:`--cap-drop=ALL`\n\n— no Linux capabilities`--security-opt no-new-privileges`\n\n— no privilege escalation`--user 1000:1000`\n\n— non-root user- Volume mount: host workspace directory ->\n`/workspace`\n\nin container\n\n**sandbox-entry.ts**(inside container) creates a Pi Agent with:- Local coding tools (read, write, edit, bash, grep, find, ls) scoped to\n`/workspace`\n\n- Proxy tools that forward\n`message_user`\n\n,`post_channel`\n\n,`message_agent`\n\n,`list_agents`\n\n,`read_agent_file`\n\n,`authenticated_fetch`\n\n,`task_create`\n\n,`task_update`\n\n,`task_list`\n\n,`task_get`\n\n,`task_delete`\n\n,`read_skill`\n\n,`skill_search`\n\n,`skill_install`\n\n,`skill_remove`\n\n,`skill_create`\n\nto the Host API over HTTP\n\n- Local coding tools (read, write, edit, bash, grep, find, ls) scoped to\n**Host API** authenticates requests via Bearer token, executes them against the message bus / filesystem, and returns results.**Prompt flow:** Host sends`POST /prompt`\n\nto container -> agent processes -> container sends`POST /api/prompt-done`\n\nback to host.**Heartbeat:** Container sends`POST /api/heartbeat`\n\nevery 5 seconds. Watchdog monitors these for stuck detection.\n\n| Protection | Mechanism |\n|---|---|\n| Process isolation | Separate Docker container per agent |\n| No root access | `--user 1000:1000` , `--cap-drop=ALL` , `no-new-privileges` |\n| Filesystem isolation | Only the agent's own workspace is mounted |\n| Secret isolation | Model API key via `GET /api/secrets` (memory-only, never in Docker env) |\n| Tool secret isolation | Per-agent secrets resolved host-side via `authenticated_fetch` — never enter container |\n| Output redaction | Two-layer: sandbox-side + host-side redaction of secrets in events and fetch responses |\n| SSRF protection | Two-layer: literal IP check + DNS resolution (blocks private, loopback, link-local, IPv4-mapped IPv6) |\n| Cross-agent file access | Proxied through Host API with path traversal guards |\n| Authentication | Unique per-agent Bearer token on all endpoints (except `/health` ) |\n| Message integrity | Server derives sender identity from token, never trusts body |\n| Idempotency | `messageId` -based deduplication with 5-minute TTL |\n| Request limits | 64 KB message-agent body, 1 MB general body, 1 MB file response |\n| Prompt timeout | 5-minute timeout on prompt completion |\n\n```\n# Terminal 1: Start with Docker sandbox\npnpm dev start --office acme --sandbox docker\n\n# API command strings (UI has equivalent controls):\nhire designer --model anthropic:claude-sonnet-4-20250514 --desc \"Frontend designer\"\n# → [agent:designer] Started in sandbox (http://localhost:13100)\n\nhire reviewer --model openai:gpt-4.1 --desc \"Code reviewer\"\n# → [agent:reviewer] Started in sandbox (http://localhost:13101)\n\nsend designer \"Create a responsive landing page with hero section\"\n# → designer works inside its Docker container, edits files in /workspace\n# → Files persist at ~/.agent-office/offices/acme/agents/designer/workspace/ on the host\n\nsend reviewer \"Review designer's index.html and send feedback\"\n# → reviewer uses read_agent_file (proxied via Host API) to read designer's files\n# → reviewer uses message_agent (proxied via Host API) to send feedback to designer\n```\n\nVerify files created by sandboxed agents persist on the host:\n\n```\nls ~/.agent-office/offices/acme/agents/designer/workspace/\n# index.html  styles.css  ...\n```\n\nThe Host API runs on port 13000 (configurable) and provides the bridge between sandboxed agents and the host system.\n\n| Method | Path | Purpose |\n|---|---|---|\n`GET` |\n`/api/secrets` |\nFetch secrets (model API key + tool secrets) at container boot |\n`POST` |\n`/api/message-user` |\nSend a DM to the human user (egress, idempotent) |\n`POST` |\n`/api/post-channel` |\nPost a message to a channel (egress, rate-limited) |\n`POST` |\n`/api/message-agent` |\nForward message to another agent's inbox |\n`GET` |\n`/api/agents` |\nList all agents (name, status, description) |\n`GET` |\n`/api/agent-file?agent=X&path=Y` |\nRead file from another agent's workspace |\n`POST` |\n`/api/authenticated-fetch` |\nHost-proxied HTTP request with secret injection |\n`POST` |\n`/api/cron-add` |\nAdd or update a cron job (auth required, identity from token) |\n`POST` |\n`/api/cron-remove` |\nRemove a cron job (auth required, identity from token) |\n`POST` |\n`/api/cron-list` |\nList cron jobs visible to the calling agent (auth required) |\n`POST` |\n`/api/prompt-done` |\nNotify host that a prompt completed |\n`POST` |\n`/api/agent-event` |\nForward agent events to host (redacted) |\n`POST` |\n`/api/heartbeat` |\nUpdate agent heartbeat timestamp |\n`POST` |\n`/api/task-create` |\nCreate a task (auth required) |\n`POST` |\n`/api/task-update` |\nUpdate a task (auth required) |\n`POST` |\n`/api/task-list` |\nList tasks (auth required) |\n`POST` |\n`/api/task-get` |\nGet task details (auth required) |\n`POST` |\n`/api/task-delete` |\nDelete a task (auth required) |\n`POST` |\n`/api/read-skill` |\nRead full skill content (auth required) |\n`POST` |\n`/api/skill-search` |\nSearch skills registry (auth required) |\n`POST` |\n`/api/skill-install` |\nInstall a skill from registry (auth required) |\n`POST` |\n`/api/skill-remove` |\nRemove an installed skill (auth required) |\n`POST` |\n`/api/skill-create` |\nCreate a custom skill (auth required) |\n`POST` |\n`/api/tool-count` |\nReport agent tool count (auth required) |\n\nAll endpoints require `Authorization: Bearer <token>`\n\nheader. The token is generated per agent by the host and injected into the container as an environment variable. Model API keys are never passed as Docker env vars — they are fetched via `GET /api/secrets`\n\nat boot and stored in memory only.\n\nRuntime operations are available through two surfaces:\n\n**Typed REST API**— dedicated endpoints for each operation (e.g.`POST /api/agents`\n\nto hire,`DELETE /api/agents/:name`\n\nto fire,`PATCH /api/agents/:name/prompt`\n\nto update prompt). See[REST API Endpoints](#rest-api-endpoints)for the full list.— structured endpoint for sending messages to agents (`POST /api/send`\n\n`{ \"agent\": \"<name>\", \"message\": \"<text>\" }`\n\n).\n\nThe Web UI has dedicated controls (buttons, forms, modals) for common operations — hire, fire, send, cron, reload — that call these typed endpoints internally.\n\nThe table below lists all available operations and their descriptions:\n\n| Command | Description |\n|---|---|\n`hire <name> [options]` |\nCreate a new agent (persists to YAML unless `--ephemeral` ) |\n`roster` |\nShow all agents with status table |\n`send <agent> <message>` |\nQueue a message for an agent |\n`fire <agent>` |\nStop and remove an agent (removes from YAML) |\n`status` |\nShow scheduler, watchdog, and resource state |\n`skill add <agent> <source>` |\nInstall skills from GitHub source (`owner/repo` , legacy flow) |\n`skill list <agent>` |\nList installed skills |\n`skill remove <agent> <name>` |\nRemove a legacy GitHub-source skill |\n`agent env set <agent> <KEY> <VALUE>` |\nSet env var in `office.yaml` |\n`agent env unset <agent> <KEY>` |\nRemove env var from `office.yaml` |\n`agent secret-ref set <agent> <KEY> <ENV>` |\nSet secret ref in `office.yaml` |\n`agent secret-ref unset <agent> <KEY>` |\nRemove secret ref from `office.yaml` |\n`agent config show <agent>` |\nShow agent config (secrets redacted) |\n`agent prompt show <agent>` |\nShow effective prompt (version/hash) |\n`agent prompt set <agent> <text>` |\nSet custom prompt (`prompt_inline` only) |\n`agent prompt append <agent> <text>` |\nAppend to custom prompt (`prompt_inline` only) |\n`agent prompt clear <agent>` |\nRemove prompt config (both inline and file ref) |\n`agent permission show <agent>` |\nShow agent permissions (office_cron, tools) |\n`agent permission set <agent> office_cron <bool>` |\nSet `office_cron` permission (true/false) |\n`agent permission set <agent> tools allow|deny <t>` |\nSet tools allow/deny list (comma-separated) |\n`agent permission clear <agent> office_cron` |\nClear `office_cron` permission |\n`agent permission clear <agent> tools` |\nClear tools permissions |\n`agent hierarchy show <agent>` |\nShow agent's manager, peers, and direct reports |\n`org chart` |\nDisplay full org tree (user at root) |\n`office reload [--force]` |\nRe-apply `office.yaml` (force kills changed agents) |\n`office validate` |\nDry-run: parse + validate YAML without spawning |\n`office path` |\nPrint path to `office.yaml` |\n`cron list` |\nList all cron jobs |\n`cron status [agent]` |\nDetailed cron job status |\n`cron add <agent> <job> \"<sched>\" <msg> [--apply]` |\nAdd a cron job |\n`cron remove <agent> <job> [--apply]` |\nRemove a cron job |\n`cron trigger <agent> <job>` |\nFire a cron job immediately |\n`cron enable <agent> <job> [--apply]` |\nRe-enable a paused job |\n`cron disable <agent> <job> [--apply]` |\nPause a cron job |\n`cron add office <job> \"<sched>\" <msg> --targets a,b` |\nAdd an office-level cron job (applies immediately) |\n`cron remove office <job>` |\nRemove an office-level cron job (applies immediately) |\n`cron trigger office <job>` |\nFire an office cron job immediately |\n`task list [--assignee X] [--status S]` |\nList tasks with optional filters |\n`task board` |\nShow Kanban board view |\n`task get <id>` |\nShow task details |\n`prompt report <agent>` |\nShow prompt composition (block sizes, tool count, mode) |\n`cost status` |\nSession token and cost totals (resets on restart) |\n`cost today [--agent <name>]` |\nPersistent token and cost totals for today |\n`cost report --days <n> [--agent <name>]` |\nHistorical usage over last N days |\n`oauth login <provider> --office <id>` |\nInteractive OAuth login for a provider |\n`oauth logout <provider> --office <id>` |\nRemove OAuth credentials for a provider |\n`oauth list --office <id>` |\nList all providers and credential status |\n\n```\nhire <name>\n  --model <provider:id>     Model (default: anthropic:claude-sonnet-4-20250514)\n  --priority <0-4>          0=IDLE, 1=LOW, 2=NORMAL, 3=HIGH, 4=CRITICAL\n  --thinking <level>        off, minimal, low, medium, high, xhigh\n  --cwd <path>              Custom workspace dir\n  --desc <text>             Agent description (visible to other agents)\n  --prompt <text>           Custom system prompt\n  --api-key-ref <ENV_NAME>  Host env var for model API key override\n  --env <KEY=VALUE>         Non-sensitive env var (repeatable)\n  --secret-ref <KEY=ENV>    Secret ref mapping (repeatable)\n  --ephemeral               Don't persist to office.yaml\n```\n\nThe hire modal in the Web UI displays all available models dynamically, grouped by provider. Instead of a hardcoded list, you can browse:\n\n**700+ available models** from 10+ providers (Anthropic, OpenAI, Google, xAI, Mistral, etc.)**Model metadata**: reasoning capability, context window, input/output costs** Provider grouping**: Easy navigation by provider (anthropic, openai, google, etc.)\n\n**How it works:**\n\n- Web UI calls\n`GET /api/models`\n\nendpoint - Backend fetches available models and groups them by provider\n- Models are displayed with metadata for easy selection\n- Fallback to default model if fetch fails\n\n**API Endpoint:**\n\n```\nGET /api/models\nAuth: Session cookie required\nResponse:\n{\n  \"providers\": [\"anthropic\", \"openai\", \"google\", ...],\n  \"models\": {\n    \"anthropic\": [\n      {\n        \"id\": \"claude-opus-4-6\",\n        \"name\": \"Claude Opus 4.6\",\n        \"provider\": \"anthropic\",\n        \"reasoning\": true,\n        \"contextWindow\": 200000,\n        \"maxTokens\": 4096,\n        \"cost\": { \"input\": 3, \"output\": 15 }\n      },\n      ...\n    ],\n    \"openai\": [...],\n    ...\n  }\n}\n```\n\n**Example:** When you click \"Hire\" in the UI, you'll see all models grouped like:\n\n```\nAnthropic\n  └─ Claude Opus 4.6 (reasoning: ✓, context: 200k, cost: $3-15/MTok)\n  └─ Claude Sonnet 4 (reasoning: ✗, context: 200k, cost: $3-15/MTok)\n  └─ Claude Haiku 3.5 (reasoning: ✗, context: 200k, cost: $0.8-4/MTok)\n\nOpenAI\n  └─ GPT-4o (reasoning: ✓, context: 128k, cost: $5-15/MTok)\n  └─ GPT-4o mini (reasoning: ✗, context: 128k, cost: $0.15-0.6/MTok)\n  └─ ...\npnpm dev start\n  --office <name>           Office to load (required)\n  --tick-interval <ms>      Scheduler tick interval (default: 2000)\n  --sandbox <mode>          Sandbox mode: none | docker (default: none)\n  --no-ui                   Run headless without the web UI\n```\n\nMigration note:The interactive`ao>`\n\nREPL has been removed. All runtime commands are now available through the Web UI controls and typed REST API endpoints. Use`--no-ui`\n\nfor headless operation; send`SIGINT`\n\n/`SIGTERM`\n\nto shut down.\n\nRuntime commands (everything in the table above) can be executed through two surfaces:\n\n**Web UI**— dedicated controls (buttons, forms, modals) for common operations: hire, fire, send messages, cron management, office reload, org chart. Some data (tasks, cost, permissions, skills) is displayed read-only. There is no free-text command prompt in the UI.**REST API**— typed endpoints per resource (e.g.`POST /api/agents`\n\n,`DELETE /api/agents/:name`\n\n,`PATCH /api/agents/:name/prompt`\n\n), plus`POST /api/send`\n\nfor agent messages. Callable via`curl`\n\n, scripts, or browser DevTools. See[REST API Endpoints](#rest-api-endpoints).\n\nOne-shot CLI commands (`office create`\n\n, `office validate`\n\n, `office migrate`\n\n, `oauth login/logout/list`\n\n, `start`\n\n) are run in the terminal and are not part of the runtime API.\n\nWith `--no-ui`\n\n, the dashboard and API server are not started — runtime commands are unavailable for that process.\n\nThe Web UI server exposes typed REST endpoints for all operations. All mutating endpoints require session cookie + CSRF headers (`Origin`\n\n+ `X-Requested-With: XMLHttpRequest`\n\n).\n\n**Auth & SSE:**\n\n| Method | Path | Description |\n|---|---|---|\n`POST` |\n`/api/auth` |\nAuthenticate with bootstrap token, set session |\n`GET` |\n`/api/events` |\nSSE event stream (real-time updates) |\n`GET` |\n`/api/state` |\nFull workspace state snapshot |\n`GET` |\n`/api/status` |\nScheduler and agent status overview |\n`GET` |\n`/api/hierarchy` |\nOrg chart hierarchy data |\n`GET` |\n`/api/manifest` |\nUI build manifest |\n\n**Agents:**\n\n| Method | Path | Description |\n|---|---|---|\n`POST` |\n`/api/agents` |\nHire a new agent |\n`GET` |\n`/api/agents/:name` |\nGet agent details |\n`DELETE` |\n`/api/agents/:name` |\nFire an agent |\n`POST` |\n`/api/send` |\nSend a message to an agent |\n`GET` |\n`/api/agents/:name/inbox` |\nGet agent inbox queue |\n`GET` |\n`/api/agents/:name/messages` |\nGet agent DM history |\n`DELETE` |\n`/api/agents/:name/messages` |\nClear agent DM history |\n`GET` |\n`/api/agents/:name/files` |\nList agent workspace files |\n`GET` |\n`/api/agents/:name/files/content` |\nRead a file from agent workspace |\n`PATCH` |\n`/api/agents/:name/prompt` |\nSet, append, or clear agent prompt |\n`PATCH` |\n`/api/agents/:name/permissions` |\nUpdate agent permissions |\n`PATCH` |\n`/api/agents/:name/env` |\nSet or unset agent env var |\n`PATCH` |\n`/api/agents/:name/secret-refs` |\nSet or unset agent secret ref |\n`PATCH` |\n`/api/agents/:name/auth` |\nSet or clear agent auth mode |\n`PATCH` |\n`/api/agents/:name/manager` |\nSet or clear agent manager |\n`PATCH` |\n`/api/agents/:name/heartbeat` |\nSet agent heartbeat config |\n`DELETE` |\n`/api/agents/:name/heartbeat` |\nClear agent heartbeat config |\n`GET` |\n`/api/agents/:name/peers` |\nList peer agents with conversations |\n`GET` |\n`/api/agents/:name/peers/:peer/messages` |\nRead inter-agent conversation |\n`GET` |\n`/api/agents/:name/skills` |\nList agent installed skills |\n`GET` |\n`/api/agents/:name/skills/search` |\nSearch skills registry |\n`POST` |\n`/api/agents/:name/skills/install` |\nInstall a skill for an agent |\n`DELETE` |\n`/api/agents/:name/skills/:skill` |\nRemove an installed skill |\n\n**Cron:**\n\n| Method | Path | Description |\n|---|---|---|\n`GET` |\n`/api/cron` |\nList all cron jobs |\n`POST` |\n`/api/agents/:name/cron` |\nAdd a cron job for an agent |\n`DELETE` |\n`/api/agents/:name/cron/:job` |\nRemove an agent cron job |\n`PATCH` |\n`/api/agents/:name/cron/:job` |\nEnable or disable an agent cron job |\n`POST` |\n`/api/agents/:name/cron/:job/trigger` |\nTrigger an agent cron job |\n`POST` |\n`/api/cron/office` |\nAdd an office-level cron job |\n`DELETE` |\n`/api/cron/office/:job` |\nRemove an office-level cron job |\n`POST` |\n`/api/cron/office/:job/trigger` |\nTrigger an office-level cron job |\n\n**Tasks:**\n\n| Method | Path | Description |\n|---|---|---|\n`GET` |\n`/api/tasks` |\nList tasks with filters |\n`POST` |\n`/api/tasks` |\nCreate a task |\n`GET` |\n`/api/tasks/board` |\nGet Kanban board data |\n`GET` |\n`/api/tasks/:id` |\nGet task details |\n`PATCH` |\n`/api/tasks/:id` |\nUpdate task status/data |\n`DELETE` |\n`/api/tasks/:id` |\nDelete a task |\n\n**Channels:**\n\n| Method | Path | Description |\n|---|---|---|\n`POST` |\n`/api/channels` |\nCreate a channel |\n`PATCH` |\n`/api/channels/:name` |\nUpdate channel members/desc |\n`DELETE` |\n`/api/channels/:name` |\nDelete a channel |\n`POST` |\n`/api/channels/:name/send` |\nSend a message to a channel |\n`GET` |\n`/api/channels/:name/messages` |\nGet channel message history |\n`DELETE` |\n`/api/channels/:name/messages` |\nClear channel history |\n\n**Office & Scheduler:**\n\n| Method | Path | Description |\n|---|---|---|\n`POST` |\n`/api/office/apply` |\nApply office.yaml changes |\n`GET` |\n`/api/office/validate` |\nValidate office.yaml |\n`GET` |\n`/api/office/path` |\nGet office.yaml file path |\n`POST` |\n`/api/scheduler/start` |\nStart the scheduler |\n`POST` |\n`/api/scheduler/stop` |\nStop the scheduler |\n\n**Metrics:**\n\n| Method | Path | Description |\n|---|---|---|\n`GET` |\n`/api/cost` |\nCost and token usage data |\n\n**OAuth:**\n\n| Method | Path | Description |\n|---|---|---|\n`GET` |\n`/api/oauth/providers` |\nList all providers with authentication status |\n`GET` |\n`/api/oauth/status/:id` |\nCheck credential status for a provider |\n`DELETE` |\n`/api/oauth/:id` |\nRemove stored credentials for a provider |\n\nAgents communicate through explicit tool calls. Agent text output is internal thinking — not visible to the user. All outward communication uses egress tools (`message_user`\n\n, `post_channel`\n\n), messaging tools (`message_agent`\n\n, `list_agents`\n\n, `read_agent_file`\n\n, `authenticated_fetch`\n\n), cron tools (`cron_add`\n\n, `cron_remove`\n\n, `cron_list`\n\n), task tools (`task_create`\n\n, `task_update`\n\n, `task_list`\n\n, `task_get`\n\n, `task_delete`\n\n), and skill tools (`read_skill`\n\n, `skill_search`\n\n, `skill_install`\n\n, `skill_remove`\n\n, `skill_create`\n\n). Tool schemas are defined once in `src/agent/tools/contracts.ts`\n\n. Both in-process and sandboxed agents expose the full tool set.\n\nThe task system automatically notifies the task creator when a task's status changes. When an agent calls `task_update`\n\nto transition a task, `TaskService`\n\nsends a system message (from `__task__`\n\n) to the creator with the new status, result summary, and task reference. This eliminates \"silent completion\" without relying on agents to remember to send `message_agent`\n\nmanually.\n\n**Automatic notifications are sent for these transitions:**\n\n| Status | Notification |\n|---|---|\n`in_progress` |\n`[Task Started]` — creator knows work has begun |\n`review` |\n`[Task In Review]` — creator knows review is pending |\n`done` |\n`[Task Completed]` — creator receives result summary |\n\nNotifications are skipped when the creator is a system address (`__user__`\n\n, `__cron__`\n\n, etc.) or when the creator and assignee are the same agent.\n\n**Agent-to-agent requests** (without the task system) still require the agent to `message_agent`\n\nthe requester with results. The base prompt (`base-v1.md`\n\n) instructs agents accordingly.\n\nDiscover all agents in the workspace with their name, status, and description. Agents are instructed to call this first when given a task to find collaborators.\n\nSend a direct message to another agent's inbox. Messages are delivered on the next scheduler tick as a new prompt prefixed with `[Message from sender]`\n\nand a footer `[To reply, call message_agent with to=\"sender\"]`\n\n.\n\n**Channel context:** Messages delivered through public channels include channel context: `[Posted in #channel. Other members: agent1, agent2]`\n\n. This lets agents know they're in a shared conversation and who else can see the message. Channel replies use `[To reply in #channel, post in the channel]`\n\ninstead of the direct `message_agent`\n\nfooter.\n\nOptional parameters:\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n`originTaskId` |\nstring | — | Related task ID for correlation tracking |\n\nReturns delivery confirmation: `{ queued: true }`\n\non success, or `{ queued: false, reason: \"...\" }`\n\non failure (e.g. `rate_limited`\n\n).\n\n```\ncopywriter calls message_agent:\n  to: \"designer\"\n  message: \"Here's the landing page copy: ...\"\n\n-> Message lands in designer's inbox\n-> Next tick delivers it as: [Message from copywriter]\\nHere's the landing page copy: ...\\n\\n[To reply, call message_agent with to=\"copywriter\"]\n-> Designer starts working\n```\n\nSend a message to the human user. This is the **only** way an agent communicates with the user — agent text output is internal thinking and not visible. DMs are persisted via the egress service to both SQLite (`dm_messages`\n\ntable) and JSONL (`user-dm.jsonl`\n\n), with idempotency via deterministic `egressId`\n\n(SHA-256 from `idempotencyKey`\n\n).\n\n``` php\nagent calls message_user:\n  message: \"The login page is ready for review.\"\n\n-> DM persisted to SQLite + JSONL\n-> state_changed SSE broadcast triggers UI refresh\n-> Real-time: tool_execution_end SSE event invalidates React Query cache for immediate display\n```\n\n**Idempotency:** When called with the same `idempotencyKey`\n\n(derived from the tool call's internal request ID), duplicate writes are prevented. SQLite `INSERT OR IGNORE`\n\ngates the JSONL write, ensuring exactly-once persistence even under retries.\n\n**Validation:** Empty messages and messages exceeding 64 KB are rejected.\n\nPost a message to a named channel. All channel members see the message in their `channel-<name>.jsonl`\n\nsession files. Bus notifications are sent to other members (not self). Optional `mentions`\n\narray targets bus delivery to specific members only.\n\n```\nagent calls post_channel:\n  channel: \"general\"\n  message: \"The API endpoint is deployed.\"\n  mentions: [\"reviewer\"]\n\n-> JSONL written to all members' sessions/channel-general.jsonl\n-> Bus notification sent to reviewer only (not self)\n```\n\n**Rate limiting:** 5 messages per 30-second window per agent per channel. `__user__`\n\nposts bypass the rate limit.\n\n**Hop count:** Messages carry a `hopCount`\n\nfield incremented on each delivery. Posts are rejected when `hopCount >= 5`\n\nto prevent infinite loops.\n\n**Role assignment:** Posts from `__user__`\n\nget `role: \"user\"`\n\n, all others get `role: \"assistant\"`\n\n.\n\nRead files directly from another agent's workspace without needing to ask them. Path traversal is blocked for security.\n\n``` php\nreviewer calls read_agent_file:\n  agent: \"designer\"\n  path: \"index.html\"\n\n-> Returns contents of ~/.agent-office/offices/<id>/agents/designer/workspace/index.html\n```\n\nIn Docker sandbox mode, this tool is proxied through the Host API. The agent sends an HTTP request to the host, which reads the file on disk and returns the content. The sandboxed agent never has direct filesystem access to other agents' workspaces.\n\nMake HTTP requests to external APIs using pre-configured secrets. The secret is injected server-side and never exposed to the agent process — the agent only knows the secret *name*, not its value.\n\n```\nagent calls authenticated_fetch:\n  url: \"https://api.github.com/user/repos\"\n  secretName: \"GITHUB_TOKEN\"\n  method: \"GET\"\n\n-> Host resolves GITHUB_TOKEN to the actual value from process.env\n-> Host injects Authorization: Bearer ghp_... header\n-> Host makes the outbound HTTPS request\n-> Host redacts secret value from response body\n-> Agent receives: HTTP 200 OK\\n\\n[{\"id\":1,\"name\":\"my-repo\",...}]\n```\n\n-\n**Configuration**— secrets are declared in`office.yaml`\n\nusing`${VAR}`\n\nrefs:\n\n```\nagents:\n  my-agent:\n    model: anthropic:claude-sonnet-4-20250514\n    secrets:\n      GITHUB_TOKEN: ${MY_GH_TOKEN}\n      SLACK_TOKEN: ${MY_SLACK_TOKEN}\n    disclose_secrets: true # agent sees names, never values\n```\n\n-\n**Resolution**— at spawn time,`${MY_GH_TOKEN}`\n\nis resolved from`process.env`\n\n. Missing refs fail fast with a clear error. The resolved values are stored in memory on the host, never written to disk or Docker env vars. -\n**Tool injection**— the`authenticated_fetch`\n\ntool is automatically added to agents that have at least one secret configured. No secrets = no tool. -\n**Execution**— when the agent calls the tool:** In-process:**the host tool resolves the secret, validates the request (SSRF, HTTPS, headers), makes the fetch, and redacts the secret from the response.**Docker sandbox:** the proxy tool forwards the request to`POST /api/authenticated-fetch`\n\non the Host API. The host resolves the secret, makes the outbound request, redacts the response, and returns it. The secret never enters the container.\n\n-\n**Response redaction**— before the response reaches the agent, the secret value is scrubbed from both the response body and headers. This prevents reflection attacks where an upstream endpoint echoes back the`Authorization`\n\nheader.\n\n| Protection | Detail |\n|---|---|\n| HTTPS required | Only `https://` URLs allowed (localhost exempt in dev) |\n| SSRF (literal) | Blocks private IPs: `10.x` , `172.16-31.x` , `192.168.x` , `127.x` , `169.254.x` , `0.0.0.0` |\n| SSRF (DNS) | Resolves hostnames via `dns.resolve4` /`resolve6` , checks all IPs — catches `evil.com → 127.0.0.1` |\n| SSRF (IPv6) | Blocks `::1` , `fc00::/7` , `fe80::/10` , IPv4-mapped forms (`::ffff:7f00:1` , `::ffff:127.0.0.1` ) |\n| Auth header injection | Auth header set after user headers — cannot be overridden by the agent |\n| Blocked headers | `Host` , `Content-Length` , `Transfer-Encoding` , `Connection` , `Cookie` are silently stripped |\n| Header name allowlist | Only `Authorization` , `X-API-Key` , `Api-Key` allowed as auth header names |\n| Reserved secrets | `MODEL_API_KEY` cannot be used with `authenticated_fetch` (prevents exfiltration) |\n| Size limits | Request body: 1 MB, Response body: 5 MB |\n| Timeout | 30-second timeout on outbound requests |\n| Response redaction | Secret value scrubbed from response body and headers before agent sees it |\n| Agent isolation | Each agent can only access its own secrets — agent A cannot use agent B's tokens |\n\nSecrets are only usable through two host-side paths:\n\n**Model auth**—`MODEL_API_KEY`\n\nis consumed by the agent runtime's`getApiKey()`\n\ncallback to authenticate with model providers (Anthropic, OpenAI, etc.)**HTTP calls**— tool secrets (`GITHUB_TOKEN`\n\n, etc.) are consumed via`authenticated_fetch`\n\n, where the host injects the secret into outbound requests\n\nIn both cases, the raw secret value is **never exposed** to agent code — it's not in `process.env`\n\n, not on disk, and not in Docker env vars. The agent only knows the secret *name*.\n\nThis means if a project inside the agent workspace needs a raw key (e.g. an SDK that reads `process.env.X_API_KEY`\n\n), secrets won't work for that. Use `env`\n\ninstead:\n\n`secrets` |\n`env` |\n|\n|---|---|---|\n| Agent can read value | No | Yes (visible in `process.env` / bash) |\n| Usable by SDKs/CLIs | No — only via `authenticated_fetch` |\nYes — available as env var |\n| Appears in Docker env | No | Yes (`--env` ) |\n| Redacted from logs | Yes (response + event redaction) | No |\nRequires `${VAR}` format |\nYes | Yes (supports `${VAR}` and literals) |\n\n**Rule of thumb:** use `secrets`\n\nwhen the agent only needs to make authenticated HTTP calls (API tokens, webhooks). Use `env`\n\nwhen workspace code needs the raw value (SDK clients, CLI tools, build scripts) — but accept that the agent can read it.\n\nThe `auth`\n\nparameter controls how the secret is injected into the request:\n\n| Mode | Header value | Example |\n|---|---|---|\n`bearer` (default) |\n`Bearer <secret>` |\n`Authorization: Bearer ghp_abc123` |\n`token` |\n`token <secret>` |\n`Authorization: token ghp_abc123` |\n`raw` |\n`<secret>` |\n`X-API-Key: ghp_abc123` |\n\n```\n# Custom auth mode example:\nagent calls authenticated_fetch:\n  url: \"https://api.service.com/data\"\n  secretName: \"SERVICE_KEY\"\n  auth: { mode: \"raw\", headerName: \"X-API-Key\" }\n\n-> Header injected: X-API-Key: <resolved secret value>\n```\n\nAdd or update a cron job. Agent scope (default) manages the calling agent's own jobs. Office scope requires `office_cron`\n\npermission.\n\n```\nagent calls cron_add:\n  name: \"daily-check\"\n  schedule: \"0 9 * * *\"\n  tasks:\n    - title: \"Run daily health check\"\n      assignee: \"self\"\n\n-> Cron job \"daily-check\" saved and activated (At 09:00 AM).\n```\n\nRemove a cron job by name. Scope defaults to agent.\n\n``` php\nagent calls cron_remove:\n  name: \"daily-check\"\n\n-> Cron job \"daily-check\" removed.\n```\n\nList active cron jobs. Shows all office-level jobs plus only the calling agent's own agent-scope jobs.\n\n``` php\nagent calls cron_list:\n  scope: \"all\"\n\n-> [agent] daily-check  0 9 * * * (At 09:00 AM)   next: 2025-01-15T09:00:00.000Z  tasks: 1\n   [office] standup     0 9 * * 1-5 (...)          next: 2025-01-13T09:00:00.000Z  tasks: 2\n```\n\nLoad full skill content on demand (enabled by default; set `on_demand_skills: false`\n\nfor eager mode).\n\nWhen on-demand mode is active, the agent's system prompt contains only skill summaries (name + description). The agent calls `read_skill`\n\nto fetch the full markdown content when needed.\n\n``` php\nagent calls read_skill:\n  name: \"web-skills\"\n\n-> Returns full SKILL.md content for the skill\n-> Errors with list of available skill names if not found\n```\n\nSearch the skills.sh registry for installable packages.\n\n``` php\nagent calls skill_search:\n  query: \"web scraping\"\n  limit: 5\n\n-> Returns matching packages in owner/repo@skill-name format\n```\n\nInstall a skills.sh package into the agent's skills directory.\n\n``` php\nagent calls skill_install:\n  package: \"owner/repo@skill-name\"\n\n-> Skill installed to agents/<agent>/skills/<skill-name>\n```\n\nRemove a project-installed skill by name. Legacy GitHub-sourced skills must be removed via the CLI `skill remove`\n\ncommand.\n\n``` php\nagent calls skill_remove:\n  name: \"skill-name\"\n\n-> Skill removed from agents/<agent>/skills/\n```\n\nCreate a new custom skill scaffold in the agent's skills directory.\n\n```\nagent calls skill_create:\n  name: \"my-skill\"\n  description: \"Short trigger description\"\n  instructions: \"Step-by-step workflow\"\n  when_to_use: \"When the user asks for X\"\n\n-> Skill scaffold created at agents/<agent>/skills/my-skill/\n```\n\nCreate a task with title, description, and assignee. Optional `dependsOn`\n\narray specifies task IDs that must complete first.\n\n```\nagent calls task_create:\n  title: \"Implement login page\"\n  description: \"Build login form with email/password and validation\"\n  assignee: \"coder\"\n  dependsOn: [\"T-abc123\"]\n\n-> Created T-def456 (status: waiting — waiting on T-abc123)\n```\n\nTasks with unmet dependencies start as `waiting`\n\n. Tasks with no dependencies start as `todo`\n\n.\n\nUpdate task status, reassign, or record a result. Status transitions are validated (see [Task Lifecycle](#task-lifecycle)).\n\n```\nagent calls task_update:\n  id: \"T-def456\"\n  status: \"done\"\n  result: \"Implemented login with validation\"\n\n-> Task updated. Dependent tasks auto-transition to todo.\n```\n\nList tasks with optional filters by assignee, status, priority, or creator.\n\n``` php\nagent calls task_list:\n  assignee: \"coder\"\n  status: \"in_progress\"\n\n-> Returns in_progress tasks assigned to coder\nphp\nagent calls task_list:\n  createdBy: \"__cron__\"\n\n-> Returns all tasks created by cron jobs\n```\n\nGet full task details by ID.\n\n``` php\nagent calls task_get:\n  id: \"T-def456\"\n\n-> Returns: title, description, status, assignee, dependsOn, timestamps, result\n```\n\nDelete a task permanently by ID. Cleans up dependency references — any task that depended on the deleted task has that dependency removed and may auto-unblock.\n\n``` php\nagent calls task_delete:\n  id: \"T-def456\"\n\n-> Task T-def456 deleted. Dependent tasks auto-unblocked.\nsrc/agent/tools/\n  contracts.ts              Single source of truth (name, label, description, parameters)\n  fetch-helpers.ts          Shared SSRF protection, URL validation, auth header builder\n  message-user.ts           message_user — host implementation (egress-impl.messageUser)\n  post-channel.ts           post_channel — host implementation (egress-impl.postChannel)\n  message-agent.ts          Host implementation (bus.send)\n  list-agents.ts            Host implementation (direct listFn call)\n  read-agent-file.ts        Host implementation (direct fs access)\n  authenticated-fetch.ts    Host implementation (outbound fetch with secret injection)\n  task-create.ts            task_create — host implementation\n  task-update.ts            task_update — host implementation\n  task-list.ts              task_list — host implementation\n  task-get.ts               task_get — host implementation\n  task-delete.ts            task_delete — host implementation\n  task-impl.ts              Shared task tool logic\n  read-skill.ts             read_skill — host implementation\n  skill-search.ts           skill_search — host implementation\n  skill-install.ts          skill_install — host implementation\n  skill-remove.ts           skill_remove — host implementation\n  skill-create.ts           skill_create — host implementation\n  skill-impl.ts             Shared skill tool logic\n  cron-add.ts               cron_add — host implementation\n  cron-remove.ts            cron_remove — host implementation\n  cron-list.ts              cron_list — host implementation\n  cron-impl.ts              Shared cron tool logic\n  policy.ts                 Tool policy (allow/deny filtering)\n  proxy/\n    message-user.ts         message_user — proxy implementation (HTTP POST /api/message-user)\n    post-channel.ts         post_channel — proxy implementation (HTTP POST /api/post-channel)\n    message-agent.ts        Sandbox implementation (HTTP POST /api/message-agent)\n    list-agents.ts          Sandbox implementation (HTTP GET /api/agents)\n    read-agent-file.ts      Sandbox implementation (HTTP GET /api/agent-file)\n    authenticated-fetch.ts  Sandbox implementation (HTTP POST /api/authenticated-fetch)\n    task-create.ts          task_create — proxy implementation (HTTP)\n    task-update.ts          task_update — proxy implementation (HTTP)\n    task-list.ts            task_list — proxy implementation (HTTP)\n    task-get.ts             task_get — proxy implementation (HTTP)\n    task-delete.ts          task_delete — proxy implementation (HTTP)\n    read-skill.ts           read_skill — proxy implementation (HTTP)\n    skill-search.ts         skill_search — proxy implementation (HTTP)\n    skill-install.ts        skill_install — proxy implementation (HTTP)\n    skill-remove.ts         skill_remove — proxy implementation (HTTP)\n    skill-create.ts         skill_create — proxy implementation (HTTP)\n    cron-add.ts             cron_add — proxy implementation (HTTP)\n    cron-remove.ts          cron_remove — proxy implementation (HTTP)\n    cron-list.ts            cron_list — proxy implementation (HTTP)\n    index.ts                Barrel export + HostFetch type\n```\n\nIn-process agents use the host implementations directly. Sandboxed agents use the proxy implementations, which forward requests to the Host API over HTTP. Both share the same tool contracts and validation helpers to prevent drift.\n\nEvery agent receives a **layered system prompt** composed from nine ordered layers:\n\n**Base prompt**(`src/agent/prompts/base-v1.md`\n\n) — always included, never overridden. Covers:- Communication model: agent text = internal thinking (not visible to user);\n`message_user`\n\n= agent→user;`post_channel`\n\n= agent→channel;`message_agent`\n\n= agent→agent - Agent-to-agent messaging (tools, messaging protocol, reply-loop avoidance, workflow rules, reporting)\n- Execution protocol (Plan → Act → Verify → Report)\n- Workspace discipline and persistence discipline\n- No invented details — do not fabricate external systems, links, IDs, or integrations; ask or state unknown\n- Operating context awareness — treat the office as your environment; do not assume facts not in prompt context or tool output\n- Quality bar (verify before claiming done, report assumptions)\n- Safety constitution (no independent goals, no self-modification, no replication, no exfiltration, safety over completion, human oversight first)\n- Instruction precedence (system rules > office config > custom instructions > file injections)\n\n- Communication model: agent text = internal thinking (not visible to user);\n**Office context**— office name and description (e.g. \"You work at Acme Corp. We build AI-powered widgets\"). Only present when an office has a display name.**Hierarchy**— manager, peers, and direct reports derived from`reports_to`\n\nfields. Only present when hierarchy data exists. See[Hierarchy](#hierarchy).**Runtime context**— available env var names, secret names (when`disclose_secrets: true`\n\n), active cron job summaries. Lists are sorted for deterministic hashing.**Identity**— agent name, description, workspace path.** Custom instructions**— the`prompt_inline`\n\ncontent from`office.yaml`\n\n, appended under a`## Custom Instructions`\n\nheader.**Skills**— summaries only by default (on-demand via`read_skill`\n\n), or full content when`on_demand_skills: false`\n\n. See[Skills](#skills).\n\n**Prompt source:** use `prompt_inline`\n\nto provide custom instructions as inline text. The legacy `prompt`\n\nfield is no longer supported — use `prompt_inline`\n\ninstead.\n\nWith `prompt_mode: minimal`\n\n, only base, identity, and custom layers are included (office, hierarchy, runtime, and skills are skipped).\n\nEach prompt is versioned (`v1`\n\n) and hashed (SHA-256, first 12 hex chars) for traceability. The hash is logged on agent spawn. An `.effective-prompt.md`\n\nsnapshot is written to the agent directory on every spawn/reload for debugging.\n\nCustom instructions are **append-only** — they add your content after the base prompt. All agents always receive messaging rules, tool guidance, and safety instructions regardless of custom prompt content.\n\nNote:`agent prompt show <agent>`\n\ndisplays the prompt text but excludes runtime-loaded skills. Use`prompt report <agent>`\n\nfor the authoritative composed-block view with accurate character counts.\n\nThe scheduler runs a `setInterval`\n\ntick loop (default 2s). Each tick:\n\n- Sorts agents by priority (CRITICAL=4 first, IDLE=0 last)\n- Skips agents currently running (\n`status === \"running\"`\n\n) - Drains each agent's inbox, delivers the highest-priority message\n- Dispatches non-blocking — all agents run concurrently via async I/O\n- Re-queues remaining messages for the next tick\n\n```\n--tick-interval <ms>    Configure via CLI flag (default: 2000)\n```\n\n| Level | Value | Use case |\n|---|---|---|\n`IDLE` |\n0 | Background tasks, monitoring |\n`LOW` |\n1 | Review, optimization |\n`NORMAL` |\n2 | Standard work (default) |\n`HIGH` |\n3 | Primary agents, user-facing |\n`CRITICAL` |\n4 | Urgent, time-sensitive |\n\nHigher-priority agents are always served first. One message per tick per agent prevents starvation.\n\nEach office gets an isolated directory, and each agent within it gets its own workspace:\n\n```\n~/.agent-office/\n  offices/\n    acme/\n      office.yaml           # office + agent definitions\n      .lock                 # per-office config lock\n      cron/\n        state.json          # cron job state\n      tasks/\n        tasks.json          # task store\n      logs/\n        cron-audit.jsonl    # agent cron tool audit trail\n        task-audit.jsonl    # task mutation audit trail\n        usage-cost.jsonl    # per-agent token usage + cost records\n      agents/\n        designer/\n          workspace/              # agent's cwd — all file tools scoped here\n            memory/\n              MEMORY.md           # agent memory (private, writable)\n            logs/                 # daily activity logs (YYYY-MM-DD.md)\n            instructions/         # user instruction files (SOUL.md, CONTEXT.md, IDENTITY.md)\n          sessions/               # JSONL session history (system-managed)\n            user-dm.jsonl         # user↔agent DMs\n            agent-reviewer.jsonl  # inter-agent conversations\n            channel-general.jsonl # channel conversations\n          skills/                 # installed skill directories\n            .sources.json         # skill folder → GitHub source mapping\n          .effective-prompt.md    # generated snapshot (do not edit)\n        reviewer/\n          workspace/\n          sessions/\n          skills/\n    defi-lab/\n      office.yaml\n      agents/\n        ...\n```\n\nAll file tools (read, write, edit, bash) are scoped to the agent's workspace directory. Agents can read each other's files via `read_agent_file`\n\nbut cannot write to them.\n\nIn Docker sandbox mode, the workspace directory is volume-mounted into the container at `/workspace`\n\n. File changes made inside the container persist on the host.\n\nMarkdown files loaded from each agent's `skills/`\n\ndirectory and injected into the system prompt. Skills work in both in-process and Docker sandbox modes.\n\nThere are two skill models:\n\n- GitHub source (legacy +\n`office.yaml`\n\nsync):`skill add <agent> <owner/repo>`\n\n`skill remove <agent> <name>`\n\n- skills.sh package (project-local install):\n`skill_search`\n\n/`skill_install`\n\ntools- Web UI Skills Manager install field (\n`owner/repo@skill-name`\n\n)\n\nGitHub source model can be declared in `office.yaml`\n\n:\n\n```\n# office.yaml — skills auto-install on startup\nagents:\n  designer:\n    skills:\n      - nichochar/web-skills\n# API command strings — GitHub source model (updates office.yaml)\nskill add designer nichochar/web-skills\nskill list designer\nskill remove designer web-tools\n```\n\nA `.sources.json`\n\nfile in each agent's skills directory maps installed skill folders back to their GitHub source, so `skill remove`\n\ncan clean up `office.yaml`\n\nentries when the last skill from a source is removed. Registry installs track package mapping in `.registry-map.json`\n\n.\n\n`skill_remove`\n\ntool is project-skill only. If a skill is legacy GitHub-sourced, remove it through CLI `skill remove <agent> <name>`\n\n.\n\n**On-demand loading (default):** Skill summaries (name + description) are included in the prompt and agents call [ read_skill](#read_skill) to fetch full content when needed. This reduces prompt size for agents with many or large skills. Set\n\n`on_demand_skills: false`\n\nto inject full skill content into the system prompt (eager mode).Periodic heartbeat checks (default: every 10s). If an agent's last heartbeat exceeds the stuck threshold (default: 120s), it aborts and re-initializes with a fresh Pi instance. Every agent event resets the heartbeat timer.\n\nFor Docker-sandboxed agents, heartbeats are received via `POST /api/heartbeat`\n\nfrom the container (every 5s) and fed into the watchdog through the same monitoring path.\n\nWatchdog behavior is configurable via `WorkspaceConfig.watchdog`\n\n(all fields optional):\n\n| Parameter | Default | Description |\n|---|---|---|\n`checkIntervalMs` |\n`10000` |\nHow often the watchdog checks heartbeats |\n`stuckThresholdMs` |\n`120000` |\nTime without heartbeat before declaring agent stuck |\n`maxRestarts` |\n`5` |\nMax restarts before marking agent as dead |\n`healthyResetMs` |\n`600000` |\nTime healthy before resetting restart counter |\n\nInbox queues and DM records are persisted to SQLite so they survive process restarts. Requires **Node.js 22+** (`node:sqlite`\n\n). DM conversations are **dual-written** to both SQLite (`dm_messages`\n\ntable) and JSONL session files — SQLite is the primary source for UI DM display, while JSONL enables agent self-service lookup via `read_file`\n\n/`grep`\n\n. Inter-agent and channel messages are JSONL-only (see [Session History](#session-history)).\n\n| What | DB location | Behavior |\n|---|---|---|\n| Inbox queue | `<officeDir>/messages/messages.sqlite` |\nPending messages restored on agent register; popped messages deleted; `fire <agent>` purges all. |\n| DM records | Same DB file | Written by egress service with deterministic `egress_id` for idempotency. SQLite `INSERT OR IGNORE` gates JSONL writes. |\n\nThe database is created automatically on first `start()`\n\n. WAL mode, `busy_timeout=5000`\n\n, and `synchronous=NORMAL`\n\nare set for safe concurrent reads and crash resilience. If `node:sqlite`\n\nis unavailable, startup fails with a clear error message.\n\nThe `MessageBus`\n\nsupports `sendWithOutcome()`\n\nwhich returns `{ queued: boolean; reason?: string }`\n\ninstead of void. Messages carry envelope fields (`correlationId`\n\n, `originTaskId`\n\n) for tracking.\n\nConversation history is stored as JSONL files in each agent's `sessions/`\n\ndirectory (system-managed, agents must not write to it). Three session types are supported:\n\n| File name | Scope |\n|---|---|\n`user-dm.jsonl` |\nUser-to-agent DMs |\n`agent-<peer>.jsonl` |\nInter-agent conversations |\n`channel-<name>.jsonl` |\nChannel conversations |\n\nEach line is a JSON object: `{\"ts\":\"ISO8601\",\"role\":\"user|assistant\",\"from\":\"sender\",\"text\":\"content\",\"egressId\":\"...\"}`\n\n. The `egressId`\n\nfield (present on egress-written records) enables idempotent deduplication.\n\n**Dual write (inter-agent):** Inter-agent messages are written to both the sender's and receiver's session directories, so each agent has a complete local copy of the conversation.\n\n**Dual write (DMs):** User-agent DM conversations are written to both SQLite (`dm_messages`\n\ntable) and JSONL (`user-dm.jsonl`\n\n) by the egress service (`message_user`\n\ntool). Each record carries a deterministic `egress_id`\n\n(SHA-256 from `idempotencyKey`\n\n) for deduplication — SQLite `INSERT OR IGNORE`\n\nprevents duplicates and gates the JSONL write. SQLite serves as the primary source for UI DM display (`GET /api/agents/:name/messages`\n\n). JSONL enables agents to search and read their DM history via `read_file`\n\n/`grep`\n\n.\n\n**Rotation:** Session files are rotated at 500 lines, keeping the last 400 lines to prevent unbounded growth.\n\n**Agent access:** Agents use their native `read_file`\n\n, `grep`\n\n, and `ls`\n\ntools to search and read session history from their `sessions/`\n\ndirectory. There are no dedicated session tools — the base prompt instructs agents about the directory layout.\n\n**Write guard:** The `sessions/`\n\ndirectory is system-managed. Agents are instructed not to write to it.\n\n**Channels** are defined in `office.yaml`\n\nunder `office.channels`\n\n:\n\n```\noffice:\n  name: my-team\n  channels:\n    general:\n      members: [pm, coder, reviewer]\n      description: Main discussion channel\n    design:\n      members: [pm, designer]\n```\n\nIf no `general`\n\nchannel is defined, a fallback is created with all agents as members. Channel membership is refreshed on `office reload`\n\n.\n\n`POST /api/channels/:name/send`\n\n— broadcast or mention-targeted channel send.\n\n**Channel management API** (all require session cookie + CSRF headers):\n\n`POST /api/channels`\n\n— create a new channel. Body:`{ name, members: string[], description?: string }`\n\n. Returns`201`\n\non success. Validates name (not reserved, matches`[a-zA-Z0-9_-]+`\n\n), members (must be known agents, non-empty, no duplicates).`PATCH /api/channels/:name`\n\n— update an existing channel. Body:`{ members?: string[], description?: string }`\n\n. Merges with existing config. Returns`200`\n\n.`DELETE /api/channels/:name`\n\n— delete a channel. Returns`200`\n\n. Deleting`general`\n\nis rejected with`400 { error: \"cannot_delete_default_channel\" }`\n\n. If the deleted channel is currently selected in the UI, the client falls back to the default conversation channel or the Tasks system view.\n\nAll channel mutations persist to `office.yaml`\n\natomically (lock + temp file + rename) and immediately refresh the in-memory channel map with a `state_changed`\n\nSSE broadcast. No office restart is required.\n\n**Channel ID vs label:** The API uses raw channel names (e.g., `general`\n\n). The UI displays `#general`\n\nas a label but sends the raw name in API calls. The server normalizes `#`\n\n-prefixed names for backward compatibility (e.g., `%23general`\n\n→ `general`\n\n).\n\nInspect the composed system prompt for any running agent:\n\n```\nprompt report bot\n\n=== Prompt Report: bot ===\n\nMode: full\nVersion: v1\n\nBase prompt           2,847 chars\nOffice block            156 chars\nRuntime block           312 chars\nIdentity block           89 chars\nCustom prompt         1,204 chars\nSkills                3,421 chars\n──────────────────────────────────\nTotal                 8,029 chars\n\nTools: 15 registered\nSkills: 2 loaded (web-skills, code-review)\n```\n\nUse this to check prompt size after truncation and confirm tool/skill counts.\n\nA full `.effective-prompt.md`\n\nsnapshot is also generated per agent on every spawn/reload at `<officeDir>/agents/<name>/.effective-prompt.md`\n\n. Add `.effective-prompt.md`\n\nto `.gitignore`\n\n— it is generated, not source.\n\nAgent-office tracks per-agent token usage and cost from model responses.\n\n```\ncost status\n=== Cost Status (session) ===\nTotal tokens: 12,450   Cost: $0.0832\n  bot:    8,200 tokens  $0.0614\n  helper: 4,250 tokens  $0.0218\n\ncost today\ncost today --agent bot\ncost report --days 7\ncost report --days 30 --agent bot\n```\n\n— in-memory session totals. Resets on gateway restart.`cost status`\n\n— persistent totals for the current day.`cost today`\n\n— historical totals over the last N calendar days.`cost report --days <n>`\n\n- All commands accept\n`--agent <name>`\n\nto filter to a single agent. - Usage records are stored at\n`~/.agent-office/offices/<id>/logs/usage-cost.jsonl`\n\n(append-only JSONL).\n\nThe `start`\n\ncommand starts a web UI dashboard automatically (disable with `--no-ui`\n\n):\n\n```\n[ui] Dashboard: http://127.0.0.1:3847/#token=<bootstrap>\n```\n\nOpen the printed URL to authenticate with the one-time bootstrap token.\n\nThe dashboard API uses a session-cookie flow with CSRF protection:\n\n- Open the\n`#token=<bootstrap>`\n\nURL — the UI extracts the token from the URL fragment. `POST /api/auth`\n\nwith`{ \"token\": \"<bootstrap>\" }`\n\nplus`Origin`\n\nand`X-Requested-With: XMLHttpRequest`\n\nheaders.- Server validates the one-time token, invalidates it, and returns a\n`Set-Cookie: ao_session=<id>; HttpOnly; SameSite=Strict`\n\nheader. - All subsequent API calls use the session cookie. Mutating endpoints require\n`Origin`\n\n(must match`http://127.0.0.1:<port>`\n\n) and`X-Requested-With: XMLHttpRequest`\n\nheaders for CSRF protection.\n\nThis is separate from the sandbox Host API auth (bearer token per agent, described in [Host API Endpoints](#host-api-endpoints)).\n\n**Slack-style layout**— sidebar with channels (#general), direct messages per agent, Cron management, Heartbeat management, Files browser, and a Tasks Kanban view**Kanban board**— task board with columns (waiting → todo → in_progress → done), per-agent filter, and task deletion from detail view** Agent DMs**— conversation threads per agent with message input, tabbed view (Messages, Internal, Files, Prompt, Skills, Configure), and clear history via three-dot menu**Internal conversations**— read-only viewer for agent-to-agent messages with peer selector dropdown and disabled message input** Agent detail**— skills tab for viewing installed skills per agent** Agent fire**— comprehensive cleanup with impact modal showing affected tasks, cron jobs, and channel memberships before confirmation** Dynamic model selection**— Hire modal displays all 700+ available models from pi-ai, grouped by provider with metadata (reasoning capability, context window, costs). Auto-updates when pi-ai upgrades.**OAuth auth selector**— per-agent Config tab shows auth mode toggle (API Key / OAuth) when OAuth credentials exist for the agent's model provider, plus authenticated provider badges with one-click credential removal**Heartbeat management**— top-level page (`/heartbeat`\n\n) with card-based dashboard showing configured heartbeats, next run times, active hours, and add/edit/remove via modal**Cron management**— top-level sidebar item with dedicated cron view, human-friendly schedule builder (hourly/daily/weekly/custom), report channel selector, loading states, and delete confirmation**Files browser**— centralized page (`/files`\n\n) to browse all agents' workspace files**Debug logs**— live event capture panel with source/kind/agent filters, preset views (All, Errors, Tools, Messages, Task/Cron), group-by-agent mode, and JSONL export**Org chart**— dedicated page (`/org-chart`\n\n) with interactive hierarchy visualization and agent profile drawer**Cost dashboard**— dedicated page (`/cost`\n\n) with per-agent token usage and cost breakdown**Office settings**— dedicated page (`/settings`\n\n) with channel management (create, edit members/description, delete), read-only scheduler status, config reload/validate**URL-based navigation**— React Router v7 with bookmarkable URLs, browser back/forward, and deep linking to any view** Real-time updates**— SSE event stream with unread badges and queue depth indicators\n\n| Env var | Default | Description |\n|---|---|---|\n`UI_PORT` |\n`3847` |\nDashboard HTTP port |\n\nThe server binds to `127.0.0.1`\n\nonly (never exposed to the network). Auth uses HttpOnly session cookies with CSRF protection.\n\n```\npnpm ui:build     # Type-check + Vite production build\npnpm ui:lint      # ESLint + single-component-per-file check\npnpm ui:check     # TypeScript type check only\n```\n\nThe frontend lives in `ui/`\n\n(Vite + React 19 + Mantine 7 + React Router v7). During dev, `pnpm -C ui dev`\n\nstarts the Vite dev server with API proxy to the backend.\n\nThree agents collaborate on a landing page, all running in-process:\n\n```\nhire designer --model openai:gpt-5.2-codex --desc \"Frontend designer — builds HTML/CSS\"\nhire copywriter --model openai:gpt-5.2-codex --desc \"Copywriter — writes marketing copy\"\nhire reviewer --model openai:gpt-5.2-codex --desc \"Code reviewer — reviews quality\"\n```\n\nWhat happens:\n\n**copywriter** writes copy, uses`list_agents`\n\nto discover designer, sends via`message_agent`\n\n**designer** receives the message, builds`index.html`\n\nwith the copy- You send:\n`@reviewer Review designer's work and send feedback`\n\n**reviewer** calls`list_agents`\n\n, uses`read_agent_file`\n\nto read designer's HTML, sends feedback via`message_agent`\n\n**designer** applies fixes,**copywriter** reports completion to the user\n\nAll coordination is autonomous after the initial prompt.\n\nIsolated agents working on a Node.js API project:\n\n```\n# Start with Docker isolation\npnpm dev start --office my-team --sandbox docker\nhire backend --model anthropic:claude-sonnet-4-20250514 --desc \"Backend developer — writes Node.js APIs\"\n# → Container started with --cap-drop=ALL, --user 1000:1000\n\nhire tester --model anthropic:claude-sonnet-4-20250514 --desc \"QA engineer — writes and runs tests\"\n\nsend backend \"Build a REST API for a todo app with CRUD endpoints using Express\"\n```\n\nWhat happens behind the scenes:\n\n**DockerProvider** builds the`pi-sandbox`\n\nimage (once, cached)- Two containers start on ports 13100 and 13101\n**backend** agent runs inside its container:- Uses\n`bash`\n\n,`write_file`\n\n,`edit_file`\n\ntools locally in`/workspace`\n\n- Creates\n`server.js`\n\n,`package.json`\n\n, route files - Files appear at\n`~/.agent-office/offices/<id>/agents/backend/workspace/`\n\non host\n\n- Uses\n- You send:\n`@tester Review backend's code and write tests`\n\n**tester** calls`list_agents`\n\n(proxy -> Host API -> returns agent list)**tester** calls`read_agent_file`\n\n(proxy -> Host API -> reads backend's files from host disk)**tester** writes test files in its own`/workspace`\n\n**tester** sends feedback to**backend** via`message_agent`\n\n(proxy -> Host API -> message bus)\n\nEach agent is fully isolated — a misbehaving agent cannot crash the host, read secrets, or access another agent's filesystem directly.\n\nAn agent uses pre-configured secrets to interact with the GitHub API — the secret never touches the agent process:\n\n``` js\n# Set the host env var with your GitHub PAT\nexport MY_GH_TOKEN=\"ghp_...\"\n```\n\n**Option A: Via Web UI/API**\n\n```\nhire github-bot --model anthropic:claude-sonnet-4-20250514 \\\n    --desc \"GitHub integration bot\" \\\n    --secret-ref GITHUB_TOKEN=MY_GH_TOKEN\n\nsend github-bot \"List my GitHub repos using authenticated_fetch with secretName GITHUB_TOKEN\"\n```\n\n**Option B: Via office.yaml**\n\n```\n# ~/.agent-office/offices/my-team/office.yaml\noffice:\n  name: My Team\n\nagents:\n  github-bot:\n    model: anthropic:claude-sonnet-4-20250514\n    description: \"GitHub integration bot\"\n    secrets:\n      GITHUB_TOKEN: ${MY_GH_TOKEN}\n    disclose_secrets: true\noffice reload\nsend github-bot \"List my GitHub repos\"\n```\n\nWhat happens:\n\n**Spawn:**`${MY_GH_TOKEN}`\n\nis resolved from`process.env`\n\n(fails fast if not set)**Tool injection:**`authenticated_fetch`\n\nis automatically added because the agent has secrets**Agent calls tool:**\n\n```\nauthenticated_fetch(url: \"https://api.github.com/user/repos\", secretName: \"GITHUB_TOKEN\")\n```\n\n**Host resolves secret**, injects`Authorization: Bearer ghp_...`\n\n, makes the HTTPS request**Response redacted**—`ghp_...`\n\nvalue is scrubbed from the response body before the agent sees it**Agent processes** the clean JSON response and reports results to the user\n\nThe agent never sees `ghp_...`\n\n— only the name `GITHUB_TOKEN`\n\n. In Docker sandbox mode, the secret never enters the container at all.\n\nThree agents collaborate with Kanban-style task management:\n\n```\nsend task-manager \"Build a login page with email/password auth\"\n```\n\nWhat happens:\n\n**task-manager** creates two tasks with dependencies:`T-xxx`\n\n: \"Implement login page\" → assigned to**coder**(status:`todo`\n\n)`T-yyy`\n\n: \"Review login page\" → assigned to**reviewer**,`dependsOn: [T-xxx]`\n\n(status:`waiting`\n\n)\n\n**coder** receives`[New Task]`\n\nnotification, implements the feature, marks task`done`\n\n**TaskService** detects dependency resolved → moves review task to`todo`\n\n**reviewer** receives`[Task Ready]`\n\nnotification, reviews code, marks task`done`\n\n- Track progress:\n`task board`\n\nvia API, or Tasks Kanban view in the Web UI\n\nSee [ examples/feature-team/](/baturyilmaz/agent-office/blob/main/examples/feature-team) for the full\n\n`office.yaml`\n\n.\n\n```\nsrc/\n  index.ts                    CLI entry + startup\n  workspace.ts                Central facade (wires scheduler, bus, watchdog, sandbox)\n  types.ts                    Shared types (Priority, AgentConfig, OfficeYaml, OfficeContext, etc.)\n  constants.ts                Shared constants, office path helpers, officeId validation\n\n  config/\n    office-yaml.ts            Office loader, validator, mutations, env/secret merge\n    office-yaml-mutations.ts  Office YAML mutation helpers (add/remove agents, cron, etc.)\n    yaml-utils.ts             Shared validation, cron extraction, atomic writes\n    yaml-validation.ts        YAML schema validation (agent names, office IDs, cron fields)\n    hierarchy.ts              Agent hierarchy helpers (manager lookup, org traversal)\n    env-substitution.ts       ${VAR} env ref resolution with validation\n    lock.ts                   Two-layer lock (in-process queue + cross-process file lock)\n\n  security/\n    redact.ts                 Secret redaction (text + deep object walker)\n\n  skills/\n    fetch.ts                  Skill fetching, source map, reverse lookup\n    registry.ts               Skills registry (install/remove/search/list)\n\n  agent/\n    handle.ts                 Agent lifecycle (init, prompt, steer, abort, destroy)\n    handle-init.ts            initInProcessAgent / initSandboxAgent factory functions\n    prompt.ts                 Convenience wrapper over prompt-manager\n    workspace-scaffold.ts    Workspace directory scaffold (memory/, logs/)\n    prompts/\n      base-v1.md              Versioned base prompt (messaging, tools, safety)\n      base-v1.ts              TS companion (reads .md, exports PROMPT_VERSION)\n      prompt-manager.ts       Layered composition + deterministic hashing\n      prompt-loader.ts        XOR prompt resolution (inline vs file)\n      effective-prompt.ts     .effective-prompt.md snapshot writer\n      truncate.ts             Prompt truncation (head/tail split, per-block limits)\n    skills/\n      on-demand.ts            Skill summary extraction for on-demand mode\n    entrypoints/\n      sandbox-entry.ts        Standalone process for Docker containers\n    tools/\n      contracts.ts            Shared tool metadata (name, label, description, parameters)\n      fetch-helpers.ts        Shared SSRF, URL validation, auth header builder\n      index.ts                Barrel re-export for host-side tools\n      message-user.ts         message_user — host implementation (egress-impl.messageUser)\n      post-channel.ts         post_channel — host implementation (egress-impl.postChannel)\n      message-agent.ts        message_agent — host implementation (bus.send)\n      list-agents.ts          list_agents — host implementation (direct call)\n      read-agent-file.ts      read_agent_file — host implementation (local fs)\n      authenticated-fetch.ts  authenticated_fetch — host implementation (secret injection + fetch)\n      task-create.ts          task_create — host implementation\n      task-update.ts          task_update — host implementation\n      task-list.ts            task_list — host implementation\n      task-get.ts             task_get — host implementation\n      task-delete.ts          task_delete — host implementation\n      task-impl.ts            Shared task tool logic\n      policy.ts               Tool policy (allow/deny filtering)\n      read-skill.ts           read_skill — host implementation\n      skill-create.ts         skill_create — host implementation\n      skill-install.ts        skill_install — host implementation\n      skill-remove.ts         skill_remove — host implementation\n      skill-search.ts         skill_search — host implementation\n      skill-impl.ts           Shared skill tool logic\n      cron-impl.ts            Shared cron tool logic (add/remove/list)\n      cron-add.ts             cron_add — host implementation\n      cron-remove.ts          cron_remove — host implementation\n      cron-list.ts            cron_list — host implementation\n      proxy/\n        index.ts              Barrel + HostFetch type\n        message-user.ts       message_user — proxy implementation (HTTP)\n        post-channel.ts       post_channel — proxy implementation (HTTP)\n        message-agent.ts      message_agent — proxy implementation (HTTP)\n        list-agents.ts        list_agents — proxy implementation (HTTP)\n        read-agent-file.ts    read_agent_file — proxy implementation (HTTP)\n        authenticated-fetch.ts  authenticated_fetch — proxy implementation (HTTP)\n        task-create.ts        task_create — proxy implementation (HTTP)\n        task-update.ts        task_update — proxy implementation (HTTP)\n        task-list.ts          task_list — proxy implementation (HTTP)\n        task-get.ts           task_get — proxy implementation (HTTP)\n        task-delete.ts        task_delete — proxy implementation (HTTP)\n        cron-add.ts           cron_add — proxy implementation (HTTP)\n        cron-remove.ts        cron_remove — proxy implementation (HTTP)\n        cron-list.ts          cron_list — proxy implementation (HTTP)\n        read-skill.ts         read_skill — proxy implementation (HTTP)\n        skill-create.ts       skill_create — proxy implementation (HTTP)\n        skill-install.ts      skill_install — proxy implementation (HTTP)\n        skill-remove.ts       skill_remove — proxy implementation (HTTP)\n        skill-search.ts       skill_search — proxy implementation (HTTP)\n\n  egress/\n    types.ts                  EgressContext, EgressDeps, EgressResult, constants (MAX_HOPS, rate limits)\n    egress-impl.ts            messageUser + postChannel — shared persist-then-notify logic\n\n  sandbox/\n    types.ts                  SandboxProvider interface, SandboxMode, SandboxStartOpts\n    host-api.ts               HTTP server for sandbox-to-host communication\n    host-api-handlers.ts      Core Host API route handlers (tools, prompt, secrets)\n    host-api-ext-handlers.ts  Extended Host API handlers (tasks, cron, skills)\n    docker-provider.ts        Docker container lifecycle (build, run, stop, health)\n    Dockerfile                Container image definition (node:22-slim, non-root)\n    package.json              Sandbox-specific npm dependencies\n    index.ts                  Barrel export\n\n  tasks/\n    types.ts                  Task, TaskStatus, STATUS_TRANSITIONS, TaskFilter\n    task-store.ts             Persistence (~/.agent-office/offices/<id>/tasks/tasks.json)\n    task-service.ts           Task orchestrator (create, update, dependency resolution, notifications)\n    task-audit.ts             Audit logger (logs/task-audit.jsonl)\n\n  cron/\n    types.ts                  CronJobConfig, CronJobState, CronJobEntry\n    cron-parser.ts            Thin wrapper over cron-parser (5-field only)\n    cron-store.ts             State persistence (~/.agent-office/cron/state.json)\n    cron-service.ts           Timer orchestrator (setTimeout per job, catch-up, dispatch cap)\n    cron-audit.ts             Audit logger (JSONL + stdout [cron-audit])\n\n  scheduler/\n    scheduler.ts              Tick-based priority scheduler\n    watchdog.ts               Heartbeat monitor + stuck detection\n    heartbeat.ts              Heartbeat system (periodic proactive agent wake-up)\n\n  messages/\n    types.ts                  PersistedInbox, DmRecord interfaces\n    message-store.ts          SQLite-backed inbox + DM persistence (node:sqlite, Node 22+)\n    session-key.ts            Session key helpers (sessionKey, parseSessionKey)\n\n  sessions/\n    session-writer.ts         JSONL append + rotation utility (500 lines max, keeps last 400)\n\n  transport/\n    local.ts                  In-process priority inbox queues (with SQLite persist hooks)\n    message-bus.ts            Bus wrapper over transport (store integration, pop, purge)\n\n  auth/\n    oauth-store.ts            OAuth credential persistence (load/save/path, atomic writes)\n    oauth-resolver.ts         Dynamic getApiKey callback (auto-refresh) + sync resolver\n\n  commands/\n    oauth-login.ts            OAuth CLI: login (interactive), logout, list providers\n    office-apply.ts           Apply office.yaml + reload/validate/path commands\n    hire.ts                   Agent creation with YAML auto-sync\n    roster.ts                 Agent status table\n    send.ts                   Message queueing\n    fire.ts                   Agent teardown with YAML auto-sync\n    status.ts                 Scheduler/watchdog overview\n    skill.ts                  Skill install/remove with YAML + source map sync\n    agent-config.ts           Per-agent env/secret-ref/prompt commands + config show\n    cron.ts                   Cron CLI handlers (add/remove/enable/disable/list/status/trigger)\n    task.ts                   Task CLI handlers (list/board/get)\n    migrate.ts                Two-step legacy migration (copy + finalize)\n    prompt-report.ts          Prompt report command (block sizes, tool count)\n    cost.ts                   Cost status/today/report commands\n\n  metrics/\n    usage-tracker.ts          Usage/cost JSONL tracker (record, read, summarize)\n\n  ui/\n    server.ts               HTTP server (:3847), SSE streaming, static file serving\n    routes.ts               REST API route definitions (typed endpoints) + getModelsResponse()\n    types.ts                UI-specific type definitions\n    event-buffer.ts         SSE event buffering and batching\n    manifest.ts             UI build manifest loader\n    handlers/\n      agent-config.handler.ts     Agent config (prompt, permissions, env, secrets, heartbeat)\n      agent-core.handler.ts       Agent CRUD (hire, fire, detail)\n      agent-files.handler.ts      Agent workspace file listing/reading\n      agent-messaging.handler.ts  Agent DMs, inbox, peer conversations\n      agent-skills.handler.ts     Agent skill install/remove/search\n      analytics.handler.ts        Cost metrics\n      auth.handler.ts             Session auth + CSRF\n      channels.handler.ts         Channel CRUD + messaging\n      cron-agent.handler.ts       Per-agent cron jobs\n      cron-office.handler.ts      Office-level cron jobs\n      oauth.handler.ts            OAuth provider listing + credential removal\n      office.handler.ts           Office apply/validate/path + scheduler\n      sse.handler.ts              SSE event streaming\n      state.handler.ts            Bootstrap state + status\n      tasks.handler.ts            Task CRUD + board\n    api/\n      types.ts              Shared API types (ModelInfo, ModelCost, ModelsResponse)\n      use-models.ts         React Query hook for fetching GET /api/models with 5-min stale time\n\nui/src/\n  routes.tsx                  createBrowserRouter route definitions (all app routes)\n  main.tsx                    App entry — RouterProvider + Mantine + QueryClient providers\n  components/\n    layout/\n      RootLayout.tsx          Top-level layout: auth, SSE, bootstrap, sidebar + Outlet\n      app-state-context.ts    AppStateContext + useAppState() hook (BootstrapState for pages)\n      app-actions-context.ts  AppActionsContext + useAppActions() hook (openAgentProfile)\n    slack/\n      SlackSidebar.tsx        Sidebar with useNavigate/useLocation (URL-based active state)\n      ChannelView.tsx         DM + channel conversation thread view (tabbed: Messages, Internal, Files, Prompt, Skills, Configure)\n      MessageInput.tsx        Chat input with mentions, supports disabled mode for read-only views\n      ...                     Other shared UI components\n    agent-detail/\n      PeerConversations.tsx   Read-only inter-agent conversation viewer with peer selector\n      ...                     Agent config/prompt/skills panels\n    heartbeat/\n      HeartbeatCard.tsx       Card component with avatar, interval, next run, active hours\n      HeartbeatForm.tsx       Modal form for adding/editing heartbeat config\n      HeartbeatDetailModal.tsx  Detail modal with edit/remove actions\n  pages/\n    tasks/\n      KanbanBoard.tsx         /tasks — task board with columns and per-agent filter\n    cron/\n      CronChannelView.tsx     /cron — cron job management with schedule builder\n    heartbeat/\n      HeartbeatView.tsx       /heartbeat — heartbeat management dashboard\n    files/\n      AllFilesPanel.tsx       /files — centralized file browser for all agents\n    dm/\n      DmView.tsx              /dm/:agentName — wrapper that extracts param → ChannelView\n    channel/\n      ConversationView.tsx    /channels/:name — wrapper that extracts param → ChannelView\n    cost/\n      CostPanel.tsx           /cost — per-agent token usage and cost breakdown\n    settings/\n      SettingsPanel.tsx       /settings — office settings and channel management\n    debug/\n      OfficeDebugPanel.tsx    /debug — live debug log capture panel\n    org-chart/\n      OrgChartPanel.tsx       /org-chart — interactive agent hierarchy visualization\n\ntest/\n  office-yaml.test.ts        Office config: officeId validation, load, validate, merge, mutations, lock\n  agent-config.test.ts       Per-agent env/secret-ref/prompt CLI commands + config show\n  env-substitution.test.ts   ${VAR} resolution, missing vars, reserved keys\n  hierarchy.test.ts          Agent hierarchy helpers, manager lookup\n  redact.test.ts             Secret redaction (text, deep objects, edge cases)\n  docker-provider.test.ts    Docker provider (mocked execFile + fetch)\n  authenticated-fetch.test.ts  authenticated_fetch tool + SSRF + auth modes + redaction\n  host-api.test.ts           Host API endpoints, auth, secrets, prompt correlation\n  host-api-cron.test.ts      Host API cron endpoints: auth, isolation, parity\n  host-api-tasks.test.ts     Task proxy Host API endpoints (create/update/list/get)\n  tool-contracts.test.ts     Verifies host + proxy tools share contracts\n  tool-policy.test.ts        Tool policy allow/deny filtering + server-side enforcement\n  sandbox-validation.test.ts CLI --sandbox option validation\n  tools.test.ts              Host-side tool behavior\n  skill-tools.test.ts        Skill tool behavior (create, install, remove, search)\n  skills-registry.test.ts    Skills registry (install, remove, search, list)\n  scheduler.test.ts          Tick loop, priority ordering\n  watchdog.test.ts           Heartbeat, stuck detection, restart\n  heartbeat.test.ts          Heartbeat system (interval, active hours, dispatch)\n  message-bus.test.ts        Inbox routing, rate limiting\n  message-bus-persistence.test.ts  SQLite persist/restore, pop, purge\n  message-store.test.ts      MessageStore CRUD, ordering, pagination\n  local-transport.test.ts    Priority queue ordering\n  handle-skills.test.ts      Skill paths for in-process + sandbox agents\n  cron-parser.test.ts        Cron expression parsing, timezone, describeCron\n  cron-store.test.ts         State persistence round-trip, atomic writes\n  cron-service.test.ts       Timer lifecycle, catch-up, dispatch cap, busy skip\n  cron-commands.test.ts      Cron CLI add/remove/enable/disable + validation\n  cron-tools.test.ts         Cron tool impl: validation, scopes, permissions, audit, limits\n  prompt.test.ts             System prompt composition\n  prompt-manager.test.ts     Prompt composition, layering, hashing, office block, determinism\n  prompt-loader.test.ts      Prompt source resolution (inline, file, path safety)\n  effective-prompt.test.ts   Effective prompt snapshot generation\n  truncate.test.ts           Prompt truncation (head/tail split, per-block limits)\n  workspace-scaffold.test.ts Workspace scaffold (memory/, logs/ directory creation)\n  office-cron.test.ts        Office-level cron lifecycle, targets, broadcast, state keys\n  task-service.test.ts       Task creation, status transitions, dependencies, notifications\n  task-store.test.ts         Task persistence, filtering\n  task-tools.test.ts         Task tool behavior + audit\n  on-demand-skills.test.ts   Skill summaries, read_skill tool, proxy\n  prompt-report.test.ts      Prompt report command output\n  usage-tracker.test.ts      Usage JSONL recording, reading, filtering\n  cost-commands.test.ts      Cost status/today/report formatting\n  cli-behavior.test.ts       CLI flag/option validation\n  session-context.test.ts    Session key helpers (sessionKey, parseSessionKey)\n  chat-feed-routing.test.ts  SSE event routing (chat-relevant vs suppressed)\n  command-parser.test.ts     Chat command parsing (slash commands, natural language)\n  debug-capture-store.test.ts  Debug capture store (event buffering, filtering)\n  debug-helpers.test.ts      Debug helper utilities\n  ui-parity.test.ts          UI API parity (REST endpoints match command coverage)\n  ui-send-message.test.ts    UI send message endpoint behavior\n  ui-server.test.ts          UI HTTP server lifecycle, routes, SSE, SSE payload contracts\n  no-ui-option.test.ts       --no-ui CLI option behavior\n  egress-impl.test.ts        Egress service: messageUser/postChannel persistence, idempotency, rate limiting\n  use-events-invalidation.test.ts  React Query cache invalidation on message_user SSE events\n```\n\n| Package | Purpose |\n|---|---|\n`@mariozechner/pi-agent-core` |\nPi agent runtime |\n`@mariozechner/pi-coding-agent` |\nCoding tools (read, write, edit, bash, grep, find, ls) + skills |\n`@mariozechner/pi-ai` |\nModel registry + streaming |\n`@sinclair/typebox` |\nTool parameter schemas |\n`commander` |\nCLI argument parsing |\n`dotenv` |\nLoad `.env` into `process.env` |\n\n| `cron-parser`\n\n| Cron expression parsing (next/prev fire times) |\n| `yaml`\n\n| YAML parsing with comment-preserving Document API |\n| `proper-lockfile`\n\n| Cross-process file locking for per-office config safety |\n\n```\npnpm install          # Install dependencies\npnpm build            # TypeScript type check (tsc --noEmit) + UI build (Vite)\npnpm lint:check       # ESLint\npnpm test             # Run test suite (vitest) — ~1000 tests\npnpm test:watch       # Run tests in watch mode\npnpm dev start        # Run in dev mode (tsx)\n```\n\nTests live in `test/`\n\n(one file per module, `<feature>.test.ts`\n\nnaming).\n\nHost API tests (`test/host-api.test.ts`\n\n, `test/host-api-cron.test.ts`\n\n) require port binding and are skipped by default. Run them when available:\n\n```\nHOST_API_TESTS=1 pnpm exec vitest run test/host-api.test.ts test/host-api-cron.test.ts\n```\n\nRequires Node 22+ and Docker (for sandbox mode).", "url": "https://wpnews.pro/news/show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older", "canonical_source": "https://github.com/baturyilmaz/agent-office", "published_at": "2026-08-21 13:04:59+00:00", "updated_at": "2026-08-21 13:14:46.081436+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Agent Office", "Pi", "Claude Code", "OpenClaw", "GitHub Copilot", "OpenServ", "Docker"], "alternates": {"html": "https://wpnews.pro/news/show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older", "markdown": "https://wpnews.pro/news/show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older.md", "text": "https://wpnews.pro/news/show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older.txt", "jsonld": "https://wpnews.pro/news/show-hn-agent-office-slack-for-ai-agents-similar-to-grok-bot-but-older.jsonld"}}