---
name: Ops Desk
slug: ops-desk
category: Automation
description: Ops Desk turns blocked tasks, staged drafts, invoices, and approvals into a ranked queue for working down one item at a time. Use it when you need to clear standing backlog items from an owner’s desk.
github: "https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-desk"
language: Shell
stars: 186
forks: 21
install: "npx degit https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-desk ~/.claude/skills/ops-desk"
installs_to: ~/.claude/skills/ops-desk
source_path: claude-ops/skills/ops-desk/SKILL.md
collection_size: 25
category_size: 2032
collection_url: "https://dirskills.com/collections/Lifecycle-Innovations-Limited/claude-ops"
added: 2026-09-06T05:20:36.823Z
last_synced: 2026-09-06T05:20:36.823Z
canonical_url: "https://dirskills.com/skills/ops-desk"
---

# Ops Desk

Ops Desk turns blocked tasks, staged drafts, invoices, and approvals into a ranked queue for working down one item at a time. Use it when you need to clear standing backlog items from an owner’s desk.

**Install:**

```bash
npx degit https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-desk ~/.claude/skills/ops-desk
```

## README

# OPS ► DESK SWEEP

Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Workflow` are missing, follow Rule 10 in `ops-rules` (Hermes: numbered options / two-turn Telegram card; `delegate_task`).

Goal: everything sitting on the owner's desk — pending decisions, staged drafts awaiting
approval, unpaid invoices to chase, documents to sign, blocked issues needing an owner
action — becomes a **ranked queue of ready-to-approve action packages**, then gets worked
down one item at a time. The success metric is the same spirit as inbox-zero: **an empty
desk**, with every remaining item either executed, decided, or parked with an explicit
reason and a reminder.

## When to use ops-desk vs ops-inbox

- `/ops:ops-inbox` — fresh inbound comms (who wrote me, what needs a reply).
- `/ops:ops-desk` — the standing backlog (what am I blocking, what decision is waiting on
  me, what draft is staged, what must I sign/pay/approve). Run it after an inbox pass, or
  whenever the owner says "what's left / get this off my desk".

## Step 1 — Build the item list (unless passed explicitly in ``)

1. **Task tracker (SSOT).** Whichever the box uses, in order of preference:
   - **Paperclip** — `paperclip issue list -C <company-id>` for each company in
     `${CLAUDE_PLUGIN_DATA_DIR}/preferences.json → paperclip.companies[]`, falling back to
     `paperclip company list`. Keep `status=blocked|todo|in_progress` issues whose
     title/description implies an OWNER action — matcher: `decision`, `owner decision`,
     `approve`, `sign`, `pay`, `chase`, `staged`, `awaiting owner`, `Rule-6`.
   - **Linear** — `mcp__linear__list_issues` filtered to the owner as assignee, when
     Paperclip is absent.
   - **GitHub** — `gh issue list --assignee @me` as last resort.
2. **Inbox residue.** Genuine NEEDS_REPLY plus todo/action-labeled emails surviving the
   most recent `/ops:ops-inbox` pass (run `bin/ops-inbox-scan` with `OIS_NO_REFRESH=1` if
   one already ran this session).
3. **Dedupe** against items already handled this session, and collapse duplicate issues
   tracking the same underlying ask (mirror boards, Linear↔Paperclip twins).
4. Shape each item as `{key, refs: [issue identifiers], hint}` where `hint` states the
   concrete question to answer AND what must be verified (never trust the issue title
   alone).

## Step 2 — Run the gather workflow (READ-ONLY, batched)

Invoke the **`Workflow`** tool with the script below and `args = [items]`. Hard rules the
script encodes:

- **Agents are READ-ONLY.** No sends, archives, payments, signatures, mark-reads, or
  tracker mutations from inside the workflow — they read, verify, and prepare only. All
  execution happens later in the main session under the outbound-approval gate.
- **Batched fan-out (3 at a time).** A wide parallel burst can rate-limit (429) the
  model-account pool — pooled CLIProxyAPI setups especially. Batches of 3 finish nearly as fast
  and never starve the rest of the fleet.
- **Verification over trust.** Every load-bearing claim in a package must come from an
  actual read (issue body, thread, sqlite row) with dates/ids cited in `status_summary`.
  An item that turns out already handled comes back as `action_type=already_done` with
  evidence.
- **Partial failure → resume.** If some agents die (rate limits, transient errors), fix
  nothing and relaunch with `resumeFromRunId` — completed agents replay from cache.

```js
export const meta = {
  name: 'desk-sweep',
  description: 'Fan out read-only context agents over open decision/action items and return ready-to-approve action packages',
  phases: [
    { title: 'Gather', detail: 'one read-only agent per item, batches of 3' },
    { title: 'Synthesize', detail: 'order into an approval queue: quick wins first' },
  ],
}

const ACTION_SCHEMA = {
  type: 'object', additionalProperties: false,
  required: ['key', 'title', 'status_summary', 'recommendation', 'action_type'],
  properties: {
    key: { type: 'string' },
    title: { type: 'string' },
    status_summary: { type: 'string', description: '2-4 sentences: what this is, current state, evidence (dates/ids)' },
    recommendation: { type: 'string', description: 'single concrete next action with an approvable default' },
    action_type: { type: 'string', enum: ['send_email', 'send_whatsapp', 'decision', 'sign', 'pay', 'chase', 'info_only', 'archive', 'already_done'] },
    draft: { type: ['object', 'null'], additionalProperties: false, properties: {
      channel: { type: 'string', enum: ['email', 'whatsapp'] },
      to: { type: 'string' }, cc: { type: 'string' }, subject: { type: 'string' },
      body: { type: 'string', description: "FULL final text in the owner's voice + thread language" },
      reply_to_message_id: { type: 'string' },
    }},
    decision_options: { type: 'array', items: { type: 'string' } },
    blockers: { type: 'array', items: { type: 'string' } },
    effort_minutes: { type: 'number' },
    confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
  },
}

phase('Gather')
const ITEMS = (typeof args === 'string' ? JSON.parse(args) : args) || []
if (!ITEMS.length) throw new Error('desk-sweep needs args: [{key, refs, hint}]')

const CHUNK = 3
const packages = []
for (let i = 0; i < ITEMS.length; i += CHUNK) {
  const batch = ITEMS.slice(i, i + CHUNK)
  const res = (await parallel(batch.map(it => () =>
    agent(
      `You are a READ-ONLY desk-sweep context agent. You MUST NOT send, archive, pay, sign, ` +
      `mark-read, or mutate ANYTHING — no mail sends, no POSTs to send/archive endpoints, no ` +
      `tracker mutations. Read, search, and prepare only.\n\n` +
      `ITEM key="${it.key}": ${it.hint}\nReferences: ${JSON.stringify(it.refs || [])}\n\n` +
      `Gather context read-only, every shell command with an explicit timeout ` +
      `("timeout --signal=TERM --kill-after=5s 30s <cmd>"): the task tracker record(s) for each ` +
      `ref incl. comments; Gmail via gog (search then thread get -j; messages nest under ` +
      `.thread.messages); the WhatsApp bridge store read-only via sqlite3 (merge a person's @lid ` +
      `and @s.whatsapp.net chats; resolve names via contacts); local files if referenced.\n\n` +
      `RULES: verify every load-bearing claim with an actual read; if already handled, return ` +
      `action_type=already_done with evidence. Drafts must be the FULL final text in the owner's ` +
      `voice and the thread's language, no AI tone. Decisions get 2-4 concrete options, ` +
      `recommended first. Your final output MUST be the structured action package.`,
      { label: `gather:${it.key}`, phase: 'Gather', schema: ACTION_SCHEMA }
    )
  ))).filter(Boolean)
  packages.push(...res)
  log(`gathered ${packages.length}/${ITEMS.length}`)
}

phase('Synthesize')
return await agent(
  `You are READ-ONLY. Order these action packages into an approval queue: quick wins first ` +
  `(low effort, high confidence, drafts ready), then decisions, then heavy/blocked. Drop ` +
  `nothing; flag conflicts between packages.\n\n` + JSON.stringify(packages, null, 2) +
  `\n\nReturn {queue: [same packages verbatim + "rank"], conflicts: [], stats: {total, ` +
  `ready_drafts, decisions, already_done}}.`,
  { label: 'synthesize', phase: 'Synthesize',
    schema: { type: 'object', additionalProperties: true, required: ['queue'] } }
)
```

## Step 3 — Work the approval queue (main session, outbound gate)

Per package, in rank order:

- **`already_done` / `archive` / `info_only`** → report in one line, archive/close
  immediately (tracker status → done). These are the free wins; do them without asking.
- **`send_email` / `send_whatsapp` / `chase`** → PER-DRAFT APPROVAL exactly as in
  `/ops:ops-inbox`: ONE `AskUserQuestion` per draft, single-select `[Send]` `[Edit]`
  `[Skip]`, the `preview` carrying the FULL text plus a short "Reasoning / facts verified"
  block (≤10 short lines — split longer drafts). After `[Send]`: send (email via `gog
  gmail send`, WhatsApp via the bridge), then archive the thread and update the tracker.
  Where an out-of-band send token is enforced (e.g. an outbound-comms hook), remind the
  owner ONCE up front — not per item.
- **`decision`** → present the options (recommended first) via `AskUserQuestion`, record
  the outcome in the tracker, execute any follow-through it implies.
- **`sign` / `pay`** → prepare only: surface the exact link/document/amount and what to
  check before signing/paying. NEVER sign or move money autonomously (Rule 5).

## Step 4 — Wrap up

Report: sent / decided / closed / still-open-with-reason. Schedule reminders
(`CronCreate`, non-outbound) for anything parked with a follow-up horizon. The task
tracker is the SSOT — every touched item gets its status updated there before the
summary prints.

## Agent Teams support

If `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` is set, use **Agent Teams** when fanning out
the gather workflow across independent item sources (Paperclip per company, Linear, GitHub,
Gmail/WhatsApp residue, etc.) in parallel. This enables:

- Gather agents share context mid-flight (one surfaces a high-priority payment →
  the ranker can prioritize before the desk-sweep plan is emitted)
- You can steer scope in real time ("skip Linear, focus on Paperclip only")
- Agents report progress as each gather round completes, so the main session can
  start the approval-queue step earlier on the items already returned

**Team setup** (only when the flag is enabled, and only for genuinely parallel
sources — a single-source desk sweep stays inline):

```
TeamCreate("ops-desk")
Agent(team_name="ops-desk", name="paperclip-gather", prompt="Gather owner-action items across all configured Paperclip companies")
Agent(team_name="ops-desk", name="linear-gather", prompt="Gather Linear items assigned to the owner")
Agent(team_name="ops-desk", name="inbox-residue", prompt="Gmail/WhatsApp NEEDS_REPLY + todo-labeled items from the last inbox pass")
Agent(team_name="ops-desk", name="github-chases", prompt="Open PRs awaiting owner review, blocked issues")
```

After the team reports, the main session runs `Step 3` (work the approval queue) on
the merged, ranked list. Each `SendMessage` from a gather agent is a structured action
package — never an outbound send (Rule 6: per-draft outbound approval gate stays in
the main session, not in the team).

If the flag is NOT set, fall back to standard fire-and-forget `Agent` subagents (one
per source) — same shape, slower wall-clock because no shared context.
