skills/butler/SKILL.md
Use when the user wants to interact with Discord via the ghost butler CLI — send `[butler:<user>]`-prefixed messages, read threads, bind a worktree's home channel, or dispatch a project task (vault-aware orchestrator that creates a Discord thread, posts /bind + pointer, and atomically writes thread metadata back into the task page). Trigger phrases include "send to Discord", "派 <task-id>", "dispatch <task-id>", "bind this channel", "read thread <id>".
npx skillsauth add ai4life-institute/ghost-in-the-shell butlerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
PM-semantic Discord CLI for vault-style worktree sessions. Talks Discord REST
directly using ghost's bot token; no Gateway connection. Stamps every outgoing
message with a 📨 **[butler:<user>]** … prefix so ghost's gateway loop
recognizes it as vault-dispatched (and won't echo it back to the CLI).
| Command | Purpose |
|---|---|
| ghost butler whoami | Bot identity + resolved outgoing user + bound home channel |
| ghost butler bind <channel_id> | Bind a Discord channel as this worktree's home channel |
| ghost butler unbind | Clear this worktree's home channel binding |
| ghost butler home | Show this worktree's home channel binding |
| ghost butler config-onboarding | Write ~/.gits/butler-onboarding.json (guild + category for new-worktree channels) |
| ghost butler send [target] <content> | Post a [butler:<user>]-prefixed message; target defaults to home channel; use - for stdin |
| ghost butler dispatch <task-id> [--phase plan\|impl] | Vault-aware orchestrator (see section below) |
| ghost butler read-thread <tid> [--limit N] | Read messages chronologically |
For raw Discord resource ops (no butler prefix, no worktree identity
resolution), use ghost discord {thread,channel,message} <verb> — see
ghost discord --help.
ghost butler resolves the outgoing user from the caller's cwd git
worktree, in this order:
--user <name> / --as <name> flagBUTLER_USER=<name> env var<name>/workvault-<name>getpass.getuser() fallback; identity must come from the
worktree, never the OS userSo in vault-weiliu/ you're weiliu, in vault-kathy/ you're kathy.
Identity is per-cwd, not per-OS-user — supports multi-human shared machines.
ghost butler dispatch <task-id> — vault-aware orchestratorAtomically: open a Discord thread for a project task, post /bind then the
pointer message, write thread: / dispatched: / dispatch_msg_id: /
status: back into the task frontmatter in one operation, and rollback
(archive the just-created thread) if any step after thread creation fails.
Then lint and print a summary.
This is the only way the vault session should dispatch project tasks.
Using raw ghost discord thread create + ghost butler send for project
work leaves the thread↔task binding floating in your head instead of in the
file.
If the user says "派 <X>" but <X> is ambiguous, the orchestrator lists
candidate paths and exits non-zero; ask the user which one they meant.
ghost butler dispatch <task-id> [--phase plan|impl] [--account <name|auto>] [--model <name>]
<task-id> — 6-char task id (preferred — unique) or a fuzzy filename
fragment (substring match against task file basenames).--phase — defaults to plan. Use impl only when the user has explicitly
green-lit a previous plan-phase response.--account — defaults to auto (omitted is the same). Pin a Claude
account by name to force the dispatched binding onto it, or leave as
auto to let the local-JSONL load-balancer
(gits.core.account_load.pick_account) choose the least-loaded
launchable one. Resolution precedence: this flag > the task page's
account: frontmatter field > auto-pick. The resolved concrete name
is appended to the /bind message as --account=<name>; the engine
never sees auto. When the task page's account: was null, the
resolved name is written back into frontmatter alongside thread: /
dispatched:.--model — pin the CLI model for the worker (alias like sonnet/haiku
or a full model ID) so cheap tasks don't burn the strongest model.
Resolution precedence: this flag > the task page's model: frontmatter
field (which, unlike account:, IS read on input — model grade is a
property of the task) > none (account's default model). The resolved
name rides the /bind message as --model=<name> and applies to fresh
claude launches only; the value actually used is stamped back into the
page's model: field.The auto-picker uses local JSONL transcripts only — no OAuth Usage
API. It compares cost-weighted utilization (load / weight) across the
5h and 7d windows and picks the lowest, with tiebreaks by live binding
count then oldest last_used. It skips any account with no
resolvable credential (no readable .credentials.json and no macOS
keychain entry), so dispatch never lands on an account claude can't
launch. Set per-account capacity with gits account set-weight <name> <N> (e.g. 20 for Max 20x, 6 for Team 6x) — without this the picker
assumes 1.0 and gits account list warns.
When an account is temporarily unusable (e.g. it hit its weekly rate
limit), bench it: ghost account bench <name> [--until "YYYY-MM-DD HH:MM"] [--for 3d|12h|45m] (no flag = until unbench). The picker
hard-skips benched accounts — same severity as the credential gate;
do NOT use a tiny set-weight as a soft bench (weight only biases the
score; the picker can still select it). Expiry is lazy (no cron needed);
ghost account list shows ⛔ while the bench is active, and an
explicit dispatch --account <name> pin still overrides with a warning.
Run from inside any vault-like worktree. The orchestrator resolves the repo
root from cwd via git rev-parse --show-toplevel.
Before dispatching, the vault session should have already locked operator-level design decisions WITH the operator and baked them into the task page as constraints, not as "executor's call".
<task-id> — id if known, else a
distinctive title fragment.--phase. Default plan. Use impl only when the user has
explicitly green-lit a previous plan-phase response.ghost butler dispatch <task-id> [--phase ...]; capture stdout. The
summary block at the end is what to relay to the user.Each exits non-zero with a clear stderr message:
<task-id> matches zero or multiple filesstatus: is not draft (refuse to re-dispatch silently; the user
must change status manually or pick a different action)personas: is missing or empty (schema requires it)ghost butler bind <channel_id> firstghost butler whoami fails (token / network / install issue)…and during execution:
/bind or pointer fails → archive thread, exitLint failed but dispatch landed. Thread and frontmatter writeback both
succeeded; lint flagged one of: a frontmatter field missing post-write
(unexpected — file a bug), thread: URL tid mismatch (stale state from a
prior attempt that didn't roll back), or ghost butler read-thread returned
fewer than 2 messages even after the retry budget (usually Discord latency;
if persistent, something is wrong with the thread). Run
ghost butler read-thread <tid> manually before retrying.
"dispatch failed". Run ghost butler whoami directly. Common causes:
token rotation (re-read ~/.gits/config.env), bot lost access to the home
channel, network blip.
"frontmatter writeback failed; archived thread". The task page is
unwritable or its parent dir is read-only. Fix the perms, then re-dispatch —
the just-created thread was rolled back, so the task is back to
status: draft and re-dispatch is safe.
Wrong channel. Dispatch always uses the worktree's bound home channel; it intentionally ignores any thread/channel info on the task page itself. Your dispatched work goes under your own channel regardless of which project the task belongs to.
Dispatch is not fire-and-forget. After a task lands in a Discord thread, the
executor will eventually reply (plan → question → diff), and the butler
session needs to notice and surface those replies to the operator with the
right urgency. The recipe below is dispatch's downstream half — the two
belong together. It is a session-level pattern, not a CLI verb; there is
no ghost butler monitor command.
The dashboard this section produces is Claude-facing triage. For the
operator-facing periodic digest convention — different audience, lower
cadence, same lifecycle — see ### Operator status reports below. The
two run in parallel.
Lifecycle rule. Session-internal automation should be lifecycle-tied to observable need — not "always on in case." Run the monitor iff at least one task is in a
dispatched*state; stop it when the last one transitions out. Generalizes beyond this recipe — same rule applies to background agents, watchers, and any other always-on poll.
| Trigger | Action |
|---|---|
| Session start: grep Projects/*/tasks/**/*.md for status: dispatched finds matches | CronCreate the monitor; persist returned job-id to .vault-session/monitor-cron-id |
| ghost butler dispatch succeeds AND .vault-session/monitor-cron-id is absent | Same — start monitor cron and persist its id |
| Monitor poll observes the last dispatched* task transitioned out (→ review / done / cancelled) | Self-terminates: CronDelete <that-job-id>, remove .vault-session/monitor-cron-id, exit (baked into the prompt below) |
Monitor scope is the current Claude session; concurrent vault sessions each
run their own. Race on .vault-session/poll-state.json is theoretical edge
case (YAGNI for v1).
Invoke via Claude Code's /loop skill at 1-minute interval — /loop wraps
CronCreate underneath, so if /loop is not loaded you can call
CronCreate directly with the same prompt body and a */1 * * * *
schedule. Capture the returned job-id into .vault-session/monitor-cron-id
so the monitor can self-delete on its final poll.
The [butler:<your-butler-user>] substring below is operator-specific —
substitute the user returned by ghost butler whoami at session-start time
(e.g. [butler:weiliu] in vault-weiliu/, [butler:kathy] in
vault-kathy/).
Scan all task pages under Projects/*/tasks/**/*.md whose frontmatter
`status` starts with `dispatched`. For each: extract the thread id from
the `thread:` frontmatter URL, run `ghost butler read-thread <tid>
--limit 20`, and identify the latest non-bot message. Classify each task
as one of: **waiting-on-me** (latest substantive message has no
`[butler:<your-butler-user>]` prefix — i.e. executor responded and I
haven't replied), **waiting-on-executor** (latest substantive message
has `[butler:<your-butler-user>]` prefix — I dispatched / replied,
executor hasn't responded yet), or **idle** (no messages in >12h).
If the scan finds **0 tasks** in `dispatched*` state AND a monitor cron
is currently running (its job-id stored at
`.vault-session/monitor-cron-id`), invoke `CronDelete <that-job-id>`,
remove the file, and exit — polling has no remaining purpose.
(Demonstrates the lifecycle-tied rule: no dispatched task → no monitor.)
Otherwise, print a compact dashboard:
```
=== thread monitor [HH:MM UTC] ===
Total active dispatched: N waiting-on-me: M waiting-on-executor: K
- [[id]] (status, area) → who-on, last-msg <Hh ago>: "<first-80-chars-of-last-msg>"
...
```
If any task is **newly** in waiting-on-me state (compared to the prior
snapshot — store snapshots at `.vault-session/poll-state.json`, creating
the dir if needed; key by tid, value `{last_msg_id, last_who, last_ts}`),
flag with a loud header `⚠️ NEW: <id> needs reply` so I notice. If no
state change since last check, print just `=== thread monitor [HH:MM UTC]
=== no change` and stop. Do NOT take any action on the threads — just
report.
When the operator explicitly says "you're the CEO" (or one of the trigger phrases below), the read-only monitor above evolves into an autonomous driver: same lifecycle, same dynamic enumeration, with an action layer on top — auto-ack tactical executor questions, auto-merge green PRs, auto-dispatch dependent tasks. Without an explicit trigger, default behavior stays monitor-only.
Why this section exists (2026-05-18 lesson). An earlier session-only driver prompt hardcoded a list of thread IDs at cron-creation time. As new tasks dispatched throughout the night, their thread IDs never entered the cron's view, so the driver silently skipped ~6 executors waiting on plan-phase Qs for 7+ hours. The fix is dynamic enumeration via grep on
^status: dispatchedin task frontmatter at every fire — never hardcode thread IDs in the prompt.
| Language | Phrase examples | |---|---| | English | "you're the CEO", "drive autonomously", "auto-merge what's green", "I'm going to sleep — keep moving" | | Chinese | "你是 CEO, 自己做主", "能自己做主就自己做主", "我要睡觉了, 你推进", "自动驱动" |
Driver inherits the monitor's lifecycle rule (run iff at least one task
is dispatched*; self-delete when none remain) and adds a CEO-mode
on/off transition. The driver replaces the monitor — they do not
coexist; the 10-min driver cadence already covers the monitor's report
work plus actions.
| Trigger | Action |
|---|---|
| Operator says a CEO-mode trigger phrase AND ≥1 task is dispatched* | If a monitor cron is running, CronDelete <monitor-job-id> and remove .vault-session/monitor-cron-id. Then CronCreate the driver (prompt below, 10-min schedule); persist returned job-id to .vault-session/driver-cron-id. Initialise .vault-session/driver-state.json. |
| Operator says "退出 CEO 模式" / "back to monitor" / "stop driver" | CronDelete <driver-job-id>, remove .vault-session/driver-cron-id. If ≥1 task is still dispatched*, recreate the monitor cron per the section above. |
| Driver's own scan finds 0 tasks in dispatched* state | Self-terminate: CronDelete <driver-job-id>, remove the file, exit. (Same rule as monitor — no dispatched task → no driver.) |
*/10 * * * *, or 3-59/10 * * * * to offset from the top of the hour.
10 min keeps polling cost trivial and leaves room for operator messages
on Discord to land between fires; a 1-min cadence (the monitor's)
over-polls when actions are involved. Operators with stronger latency
needs can override the cron expression.
ALL of the following must hold before the driver merges a PR:
gh pr view <N> --json statusCheckRollup shows every required
check green (advisory checks may remain in-progress).mergeable: MERGEABLE (no conflicts).<vault-root>/log.md and merge.assert True, empty it("...", () => {}))
fail the bar; escalate.DROP TABLE,
column-remove, or destructive migration: escalate.See [[feedback-ceo-mode-merge-autonomy]] memory for the original
rationale.
The substring [butler:<your-butler-user>] below is operator-specific
— substitute the user returned by ghost butler whoami at
session-start time.
Silent unless an action is taken or an escalation is needed.
STEP 1 — Enumerate in-flight tasks DYNAMICALLY (the KEY FIX):
in_flight=$(grep -lr "^status: dispatched" Projects/*/tasks/ 2>/dev/null)
for f in $in_flight; do
extract `id:` and `thread:` from frontmatter
done
# NEVER hardcode thread IDs in this prompt — they go stale within hours.
STEP 2 — Check open PRs from every source repo bound to this vault.
Source-repo list is the placeholder table in <vault-root>/MACHINES.md
(e.g. <ghost-repo>, <ai4stock-repo>, <vibo-repo>, <stock-arena-repo>);
resolve each to its GitHub org/repo via that table.
For each bound repo:
gh -R <org>/<repo> pr list --state open \
--json number,title,mergeable,statusCheckRollup,headRefName,additions,deletions,files
For each open PR:
- If the quality bar above holds → gh pr merge <N> --squash --delete-branch
- If CONFLICTING → dispatch a rebase task to that branch
- If CI red AND cause is a lockfile cascade from a just-merged neighbour PR
→ dispatch a rebase task
STEP 3 — For each in-flight task's thread:
ghost butler read-thread <tid> --limit 5
Filter out 🔧 tool-call streaming + scan-agent "no new gaps" noise.
If the executor surfaced plan-phase questions:
- Check the task page for an `## Operator answers` section
(pre-baked defaults).
- If pre-baked → ack with a short pointer: "spec updated, re-read .md, go".
- If not pre-baked AND the questions are tactical (not strategic) →
ack the executor's own recommended defaults inline; cite the
escalation rule.
- If strategic OR spend-bearing OR a hard block → ESCALATE (see below).
If the executor reports STOP / unrecoverable → ESCALATE.
If the executor opened a PR → it will get picked up in STEP 2 on the
next fire.
STEP 4 — Push-forward (dependency chain):
If a PR just merged AND a task with status `draft` exists whose
`parent_id` matches the merged task's id:
ghost butler dispatch <dependent-id>
(Example: M1 w3g3hs merges → immediately dispatch M2 zxvm49.)
STEP 5 — Vault hygiene:
Read .vault-session/driver-state.json; update merge_count_since_last_commit
and last_fire_ts. If merge_count_since_last_commit ≥ 3 OR
(now - last_vault_commit_ts) > 1h:
cd <vault-root> && git add -A && git commit -m "..." && git push
reset merge_count_since_last_commit = 0; set last_vault_commit_ts = now
Don't let task-page status flips and memory updates pile up uncommitted.
STEP 6 — Heartbeat (always, even on no-op):
Append one line to .vault-session/driver-heartbeat.log:
[<ISO8601 now>] fire | pr_open=N | in_flight=M | actions=K
Meta-fix for the 2026-05-18 silent-failure mode — operators can
`tail -20` to confirm the driver is alive.
CONFLICTING / DIRTY mergeable status.master / main — always via PR.master / main).git -C for read
only; never write).block-source-repo-mutations,
block-long-thread-message, block-spaces-in-md-filenames).<vault-root>/log.md and your memory index before acting.When the driver must surface to the operator, emit one line:
🚨 ESCALATE: <task-id> — <reason>; my recommendation: <action>; awaiting your call
…and append the same line to <vault-root>/log.md. Do not proceed past the escalation point on the affected task until the operator answers (other tasks may continue in parallel). Per [[feedback-operator-never-runs-commands]], phrase the recommendation as a decision (A/B/C choices), not as a shell command the operator should run.
⚙️ <action> on <target> form.log.md entry + stop on the escalated task.[[feedback-ceo-mode-merge-autonomy]] memory and in vault
CLAUDE.md; consolidating to a single source is future work —
flagged here so the drift risk is visible.#### Monitor prompt (paste-ready) above — the read-only baseline this section upgrades.### Operator status reports below — operator-facing periodic digest; runs alongside the driver (orthogonal lifecycles).ghost butler dispatch <task-id> — the upstream half.docs/dispatch-lifecycle.md — full plan → greenlight → acceptance flow.ghost butler dispatch section above — the upstream half.### Operator status reports below — operator-facing companion to this
Claude-internal monitor; runs in parallel on a separate cron.docs/dispatch-lifecycle.md — full dispatch → plan → greenlight →
acceptance → archive flow.The monitor recipe above produces a Claude-internal triage dashboard — it tells me which threads need a reply and explicitly says "Do NOT take any action — just report." That dashboard is for Claude, not the human; surfacing it on every poll would drown the operator in micro-events.
While tasks are in flight, send the operator a separate, lower-cadence digest — a concise structured status report on a fixed schedule, instead of pinging on every executor event. Operator's own words on the day this convention was set (2026-05-19):
"你不用把每一次小的进展跟我说,你每10分钟给我一个status update — 你做了 什么,还没做什么,下一步要做什么,有什么要等我批准的。"
Lifecycle rule. Same as the monitor: run iff at least one task is
dispatched*; self-terminate when the last one transitions out. The digest is orthogonal to the monitor and to the CEO-mode driver — it keeps firing whichever of those is active, because the digest's audience (human) is different from theirs (Claude).
| Trigger | Action |
|---|---|
| Session start: grep Projects/*/tasks/**/*.md for status: dispatched finds matches AND .vault-session/operator-digest-cron-id is absent | CronCreate the digest (prompt below, schedule 7-59/10 * * * *); persist returned job-id to .vault-session/operator-digest-cron-id |
| ghost butler dispatch succeeds AND .vault-session/operator-digest-cron-id is absent | Same — start digest cron and persist its id |
| Digest's own scan finds 0 tasks in dispatched* state | Self-terminate: CronDelete <that-job-id>, remove .vault-session/operator-digest-cron-id, exit (baked into the prompt below) |
| Monitor↔driver transition (operator says CEO-mode trigger, or "back to monitor") | Digest cron is untouched — it's orthogonal to the monitor/driver swap. |
The digest cron runs in parallel with the monitor (and with the CEO-mode driver if active). Each writes a different state file, so there is no contention:
| Cron | State file | Cadence | Audience |
|---|---|---|---|
| Monitor | .vault-session/poll-state.json | */1 * * * * | Claude (triage) |
| CEO driver | .vault-session/driver-state.json | */10 * * * * or 3-59/10 * * * * | Claude (actions) |
| Operator digest | .vault-session/operator-digest-state.json | 7-59/10 * * * * | operator |
The 7- offset keeps the digest off the top of the hour and clear of
the driver's 3- offset when both are active. Invoke via CronCreate
directly (mirroring how /loop wraps it for the monitor); persist the
returned job-id into .vault-session/operator-digest-cron-id so the
digest can self-delete on its final fire.
The digest is fundamentally a second cron, not a fold into the monitor. Folding forces "emit-on-every-Nth-fire" state inside the monitor and entangles two different audiences' cadences; the separation is cheaper. (An advanced executor may fold if the duplication becomes a measurable cost — document the local choice if you do.)
The emoji structure is the invariant. Prose around the emojis matches the operator's language; the layout is identical in any language.
[[feedback-operator-never-runs-commands]]). If nothing is blocked, say
so explicitly: "🟡 Awaiting operator — none."No-change case. If the signature of {task-id → {status, last_substantive_msg_id}} is byte-for-byte identical to the prior
digest's signature, send a single line — not a padded block:
=== status digest [HH:MM UTC] === no change since last report
The "substantive" filter is the same one the CEO-mode driver applies: skip 🔧 tool-call streaming and scan-agent "no new gaps" noise. Without this filter, one tool-call flip would defeat no-change detection and spam the operator.
First fire. No prior state exists. Emit a full block treating every
in-flight task as "▶️ Next" with Done empty, then seed
operator-digest-state.json so subsequent fires can compute deltas.
Multiple tasks. One short line per task per part. Surface blockers prominently — put 🟡 entries at the top of Awaiting operator, ordered oldest-blocked first.
Language detection. Read the home channel's recent activity once per
fire (ghost butler read-thread <home-channel-id> --limit 10; the verb
accepts channel ids — it is a thin passthrough to ghost discord thread read). Take the most recent operator-authored message (i.e. NOT
[butler:...]-prefixed) within the last 24h; use its language. Fall back
to English if none.
The substring [butler:<your-butler-user>] and the <home-channel-id>
below are operator-specific — substitute the values returned by
ghost butler whoami at session-start time.
Operator-facing periodic status digest. Silent unless ≥1 task is
`dispatched*`. This is NOT the Claude-internal monitor dashboard (see
`#### Monitor prompt (paste-ready)` above); the two run in parallel and
serve different audiences.
STEP 1 — Enumerate in-flight tasks DYNAMICALLY (never hardcode ids):
in_flight=$(grep -lr "^status: dispatched" Projects/*/tasks/ 2>/dev/null)
If empty AND `.vault-session/operator-digest-cron-id` exists:
CronDelete <its contents>; rm that file; exit.
(Same lifecycle rule as the monitor — no dispatched task → no digest.)
If empty AND no cron-id file: exit silently (defensive).
STEP 2 — Parse each in-flight task page's frontmatter for `id`, `status`,
`thread`, `personas`. Extract the thread id from the `thread:` URL.
STEP 3 — Per task, fetch recent thread activity:
ghost butler read-thread <tid> --limit 10
Filter out 🔧 tool-call streaming and scan-agent "no new gaps" noise;
the last surviving message is the task's `last_substantive_msg_id`.
STEP 4 — Compute the digest signature:
sig = sha256(sorted([(id, status, last_substantive_msg_id) ...]))
Load `.vault-session/operator-digest-state.json` (create empty {} if
missing). Compare sig against `state.last_signature`.
STEP 5 — Compose the digest payload (operator's language, emoji invariant):
If sig == state.last_signature:
payload = "=== status digest [HH:MM UTC] === no change since last report"
Else if state is empty (first fire):
Full block; every task under ▶️ Next; Done is empty.
Else (delta against state.snapshot):
- status flip / PR merge / executor confirmation → ✅ Done
- executor produced a new substantive message → ⏳ In progress
- executor surfaced a question OR status == `dispatched-blocked`
→ 🟡 Awaiting operator, phrased as A/B/C; cite the task-page
section that frames the decision
- dispatched but otherwise unchanged → one summary line under
⏳ In progress
▶️ Next: one short line per task — roughly who, roughly when.
If nothing is blocked: "🟡 **Awaiting operator** — none."
Surface 🟡 entries oldest-blocked first.
STEP 6 — Detect operator language:
ghost butler read-thread <home-channel-id> --limit 10
Find the most recent message NOT prefixed `📨 **[butler:` within the
last 24h. Use its language for the prose around the emojis. Default to
English if none.
STEP 7 — Post to home channel:
printf '%s' "<payload>" | ghost butler send -
(No target → defaults to bound home channel. The `-` means stdin.)
STEP 8 — Persist state atomically (write-to-tmp + rename):
state.last_signature = sig
state.last_report_ts = <ISO8601 now>
state.snapshot = {id: {status, last_substantive_msg_id} ...}
→ `.vault-session/operator-digest-state.json`
STEP 9 — Output to Claude's own stdout: nothing.
The operator's digest is the side-effect; this cron is silent to Claude.
=== status digest 23:47 UTC ===
✅ Done
- [[a3kqp2]] (auth-rewrite) — plan greenlit; executor moved to impl
⏳ In progress
- [[s7r2qx]] (discord-bot) — executor writing the new SKILL.md section
- [[zxvm49]] (data-ingest) — CI running on PR #142
▶️ Next
- s7r2qx → executor opens PR; I review and merge if green
- zxvm49 → if CI passes, auto-merge per CEO-mode quality bar
🟡 Awaiting operator
- [[m1w3g3]] (billing) — drop legacy `tax_id` column in the migration?
(A) drop now, (B) keep one release, (C) escalate to legal.
See task page §"Open questions".
#### Monitor prompt (paste-ready) above — Claude-internal triage
dashboard; this digest is its operator-facing counterpart.#### Upgrade: CEO-mode autonomous driver above — the digest keeps
firing alongside the driver (orthogonal lifecycles).ghost butler dispatch <task-id> — the upstream half.read-thread).onboard-worktree
skill (see ../onboard-worktree/SKILL.md).ghost butler --help — full flag reference for every verbghost discord --help — raw Discord transport primitives (no prefix, no
worktree identity)../onboard-worktree/SKILL.md — recipe for creating new contributor
worktrees + channelstools
Use when the user wants to onboard a new contributor to a vault-style repo by creating a personal git worktree, a dedicated Discord channel, and wiring ghost's /bind so messages in that channel route to a Claude session in the worktree. Trigger phrases include "onboard <name>", "给 <name> 创建 worktree", "给 <name> 开个 worktree", "新建 worktree for <name>", "<name> 接入 vault", "set up <name> on this vault".
development
Query today's news, run NotebookLM to generate briefing, post to API
testing
Fetch latest news articles from configured sources and save to data/news.db
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.