feat(skills): add paseo chat coordination

This commit is contained in:
Mohamed Boudra
2026-03-24 12:37:13 +07:00
parent af8509d012
commit fa0346921a
3 changed files with 625 additions and 0 deletions

133
skills/paseo-chat/SKILL.md Normal file
View File

@@ -0,0 +1,133 @@
---
name: paseo-chat
description: Use chat rooms through a chat helper. Use when the user says "chat room", "room", "coordinate through chat", "shared mailbox", or wants agents to communicate asynchronously.
user-invocable: true
---
# Paseo Chat Skill
This skill teaches how to use chat.
Use `skills/paseo-chat/bin/chat.sh`.
**User's arguments:** $ARGUMENTS
---
## Prerequisites
Load the **Paseo skill** first if you need CLI guidance for launching or messaging agents.
## Rules
When using chat:
- create a room with `chat.sh room create` if you need a new room
- inspect available rooms with `chat.sh room list` and `chat.sh room show`
- post with `chat.sh post`
- read with `chat.sh read`
- keep reads bounded, usually `--limit 10` or `--limit 20`
- check chat often while working
Do not read or write the backing files directly. Use `chat.sh`.
Mentions and replies are active:
- if you post `@agent-id` in a message, chat will notify that agent immediately
- if you use `--reply-to`, chat will notify the author of the replied-to message immediately
- notifications are sent with `paseo send --no-wait`
## Command Surface
### Create a room
```bash
skills/paseo-chat/bin/chat.sh room create \
--room issue-456 \
--title "Issue 456 coordination" \
--purpose "Coordinate implementation and review"
```
### List rooms
```bash
skills/paseo-chat/bin/chat.sh room list
```
### Show room details
```bash
skills/paseo-chat/bin/chat.sh room show --room issue-456
```
### Post a message
```bash
skills/paseo-chat/bin/chat.sh post \
--room issue-456 \
--body "I traced the failure to relay auth. Investigating config loading now."
```
Author defaults:
- `--agent-id` if provided
- otherwise `$PASEO_AGENT_ID`
- otherwise `manual`
Optional reply:
```bash
skills/paseo-chat/bin/chat.sh post \
--room issue-456 \
--reply-to msg-001 \
--body "I can take that next."
```
Direct mention:
```bash
skills/paseo-chat/bin/chat.sh post \
--room issue-456 \
--body "@agent-beta can you verify the relay path next?"
```
### Read recent messages
```bash
skills/paseo-chat/bin/chat.sh read --room issue-456 --limit 10
```
### Filter reads
```bash
skills/paseo-chat/bin/chat.sh read --room issue-456 --agent-id 123abc
skills/paseo-chat/bin/chat.sh read --room issue-456 --since 2026-03-24T10:00:00Z
```
## Defaults
When creating a room:
- choose a short explicit room id
- prefer slugs like `issue-456`, `pr-143-review`, `feature/relay-cleanup`
- give the room a clear title and purpose
When using a room:
- read only a bounded window before acting
- post updates when they would help another agent or your future self
- use `--reply-to` when responding to a specific message
- use `@agent-id` when you want to get a specific agent's attention
- check chat frequently enough that shared coordination actually works
Typical things to post:
- status updates
- blockers
- handoffs
- review findings
- important context and memories another agent may need later
## Your Job
1. Understand whether you should use an existing room or create a new one
2. Choose a room id and title
3. Create the room with `chat.sh room create` if needed
4. Read the room with bounded history
5. Post clearly
6. Use `--reply-to` when replying to a specific message
7. Use `@agent-id` when you want to notify someone directly

415
skills/paseo-chat/bin/chat.sh Executable file
View File

@@ -0,0 +1,415 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
chat.sh room create --room ROOM [--title TITLE] [--purpose PURPOSE]
chat.sh room list
chat.sh room show --room ROOM
chat.sh post --room ROOM (--body TEXT | --body-file PATH) [--agent-id ID] [--agent-name NAME] [--reply-to MESSAGE_ID]
chat.sh read --room ROOM [--limit N] [--since ISO_TIMESTAMP] [--agent-id ID]
Options:
--root PATH Override chat root (default: ~/.paseo/chat)
EOF
exit 1
}
yaml_quote() {
local value="${1//\'/\'\'}"
printf "'%s'" "$value"
}
require_command() {
local name="$1"
command -v "$name" >/dev/null 2>&1 || {
echo "Error: required command not found: $name" >&2
exit 1
}
}
sanitize_room_path() {
local room="$1"
if [[ -z "$room" || "$room" == /* || "$room" == *".."* ]]; then
echo "Error: invalid room id: $room" >&2
exit 1
fi
}
load_body() {
local inline_value="$1"
local file_value="$2"
if [[ -n "$inline_value" && -n "$file_value" ]]; then
echo "Error: use either --body or --body-file, not both" >&2
exit 1
fi
if [[ -n "$file_value" ]]; then
[[ -f "$file_value" ]] || { echo "Error: --body-file not found: $file_value" >&2; exit 1; }
cat "$file_value"
return
fi
if [[ -n "$inline_value" ]]; then
printf '%s' "$inline_value"
return
fi
echo "Error: one of --body or --body-file is required" >&2
exit 1
}
frontmatter_value() {
local key="$1"
local file="$2"
awk -F': ' -v search_key="$key" '
BEGIN { in_frontmatter = 0 }
/^---$/ {
if (in_frontmatter == 0) {
in_frontmatter = 1
next
}
exit
}
in_frontmatter == 1 && $1 == search_key {
value = substr($0, length(search_key) + 3)
gsub(/^'\''|'\''$/, "", value)
print value
exit
}
' "$file"
}
print_message() {
local file="$1"
local created_at agent_id agent_name reply_to message_id
created_at="$(frontmatter_value "created_at" "$file")"
agent_id="$(frontmatter_value "agent_id" "$file")"
agent_name="$(frontmatter_value "agent_name" "$file")"
reply_to="$(frontmatter_value "reply_to" "$file")"
message_id="$(frontmatter_value "message_id" "$file")"
printf '### %s · %s' "$created_at" "$agent_id"
if [[ -n "$agent_name" ]]; then
printf ' (%s)' "$agent_name"
fi
printf '\n'
printf 'message_id: %s\n' "$message_id"
if [[ -n "$reply_to" ]]; then
printf 'reply_to: %s\n' "$reply_to"
fi
printf '\n'
awk '
BEGIN { delim_count = 0 }
/^---$/ {
delim_count += 1
next
}
delim_count >= 2 {
print
}
' "$file"
printf '\n'
}
require_room() {
local room="$1"
local room_dir="$rooms_root/$room"
[[ -d "$room_dir" ]] || { echo "Error: room not found: $room" >&2; exit 1; }
}
find_message_file_by_id() {
local room="$1"
local message_id="$2"
find "$rooms_root/$room/messages" -name '*.md' -type f -print0 |
while IFS= read -r -d '' file; do
if [[ "$(frontmatter_value "message_id" "$file")" == "$message_id" ]]; then
printf '%s\n' "$file"
return 0
fi
done
}
extract_mentions() {
local body="$1"
printf '%s\n' "$body" | grep -oE '@[A-Za-z0-9._:-]+' | sed 's/^@//' | awk '!seen[$0]++'
}
notify_agent() {
local target_agent_id="$1"
local room="$2"
local sender_agent_id="$3"
local sender_agent_name="$4"
local message_id="$5"
local reason="$6"
if [[ -z "$target_agent_id" || "$target_agent_id" == "manual" || "$target_agent_id" == "$sender_agent_id" ]]; then
return
fi
local sender_label="$sender_agent_id"
if [[ -n "$sender_agent_name" ]]; then
sender_label="$sender_agent_name ($sender_agent_id)"
fi
local notification="You have a new chat message.
Room: $room
From: $sender_label
Message ID: $message_id
Reason: $reason
To read it:
skills/paseo-chat/bin/chat.sh read --room $room --limit 10"
"$paseo_bin" send --no-wait "$target_agent_id" "$notification" >/dev/null 2>&1 || true
}
require_command uuidgen
chat_root="${HOME}/.paseo/chat"
paseo_bin="${PASEO_CHAT_PASEO_BIN:-paseo}"
if [[ $# -lt 1 ]]; then
usage
fi
if [[ "$1" == "--root" ]]; then
[[ $# -ge 3 ]] || usage
chat_root="$2"
shift 2
fi
rooms_root="${chat_root}/rooms"
mkdir -p "$rooms_root"
command_name="$1"
shift
case "$command_name" in
room)
[[ $# -ge 1 ]] || usage
room_subcommand="$1"
shift
case "$room_subcommand" in
create)
room=""
title=""
purpose=""
while [[ $# -gt 0 ]]; do
case "$1" in
--room) room="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--purpose) purpose="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done
[[ -n "$room" ]] || { echo "Error: --room is required" >&2; exit 1; }
sanitize_room_path "$room"
room_dir="$rooms_root/$room"
messages_dir="$room_dir/messages"
mkdir -p "$messages_dir"
if [[ -z "$title" ]]; then
title="$room"
fi
created_at="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
room_file="$room_dir/room.md"
if [[ ! -f "$room_file" ]]; then
printf '%s\n' \
"---" \
"room: $(yaml_quote "$room")" \
"title: $(yaml_quote "$title")" \
"purpose: $(yaml_quote "$purpose")" \
"created_at: $(yaml_quote "$created_at")" \
"---" \
"" \
"# $(printf '%s' "$title")" \
"" \
"$(printf '%s' "$purpose")" > "$room_file"
fi
printf 'created room %s\n' "$room"
;;
list)
while IFS= read -r room_file; do
room_id="${room_file#"$rooms_root"/}"
room_id="${room_id%/room.md}"
printf '%s\n' "$room_id"
done < <(find "$rooms_root" -name room.md -type f | sort)
;;
show)
room=""
while [[ $# -gt 0 ]]; do
case "$1" in
--room) room="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done
[[ -n "$room" ]] || { echo "Error: --room is required" >&2; exit 1; }
sanitize_room_path "$room"
require_room "$room"
room_file="$rooms_root/$room/room.md"
title="$(frontmatter_value "title" "$room_file")"
purpose="$(frontmatter_value "purpose" "$room_file")"
created_at="$(frontmatter_value "created_at" "$room_file")"
message_count="$(find "$rooms_root/$room/messages" -name '*.md' -type f | wc -l | tr -d ' ')"
printf 'room: %s\n' "$room"
printf 'title: %s\n' "$title"
printf 'purpose: %s\n' "$purpose"
printf 'created_at: %s\n' "$created_at"
printf 'messages: %s\n' "$message_count"
;;
*)
usage
;;
esac
;;
post)
room=""
body_input=""
body_file=""
agent_id="${PASEO_AGENT_ID:-manual}"
agent_name=""
reply_to=""
while [[ $# -gt 0 ]]; do
case "$1" in
--room) room="$2"; shift 2 ;;
--body) body_input="$2"; shift 2 ;;
--body-file) body_file="$2"; shift 2 ;;
--agent-id) agent_id="$2"; shift 2 ;;
--agent-name) agent_name="$2"; shift 2 ;;
--reply-to) reply_to="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done
[[ -n "$room" ]] || { echo "Error: --room is required" >&2; exit 1; }
sanitize_room_path "$room"
require_room "$room"
body="$(load_body "$body_input" "$body_file")"
created_at="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
created_at_file="$(date -u +'%Y-%m-%dT%H-%M-%SZ')"
message_id="msg-$(uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-' | cut -c1-8)"
agent_slug="$(printf '%s' "$agent_id" | tr '/ :@' '_' | tr -cd '[:alnum:]_.-')"
[[ -n "$agent_slug" ]] || agent_slug="manual"
message_file="$rooms_root/$room/messages/${created_at_file}__${agent_slug}__${message_id}.md"
printf '%s\n' \
"---" \
"room: $(yaml_quote "$room")" \
"message_id: $(yaml_quote "$message_id")" \
"agent_id: $(yaml_quote "$agent_id")" \
"agent_name: $(yaml_quote "$agent_name")" \
"created_at: $(yaml_quote "$created_at")" \
"reply_to: $(yaml_quote "$reply_to")" \
"---" \
"" \
"$body" > "$message_file"
notified_agents=()
if [[ -n "$reply_to" ]]; then
reply_file="$(find_message_file_by_id "$room" "$reply_to" || true)"
if [[ -n "$reply_file" ]]; then
reply_target_agent_id="$(frontmatter_value "agent_id" "$reply_file")"
notify_agent "$reply_target_agent_id" "$room" "$agent_id" "$agent_name" "$message_id" "reply to $reply_to"
notified_agents+=("$reply_target_agent_id")
fi
fi
while IFS= read -r mentioned_agent_id; do
[[ -n "$mentioned_agent_id" ]] || continue
already_notified=false
for existing_id in "${notified_agents[@]}"; do
if [[ "$existing_id" == "$mentioned_agent_id" ]]; then
already_notified=true
break
fi
done
if [[ "$already_notified" == false ]]; then
notify_agent "$mentioned_agent_id" "$room" "$agent_id" "$agent_name" "$message_id" "direct mention"
notified_agents+=("$mentioned_agent_id")
fi
done < <(extract_mentions "$body")
printf '%s\n' "$message_id"
;;
read)
room=""
limit="20"
since=""
agent_id_filter=""
while [[ $# -gt 0 ]]; do
case "$1" in
--room) room="$2"; shift 2 ;;
--limit) limit="$2"; shift 2 ;;
--since) since="$2"; shift 2 ;;
--agent-id) agent_id_filter="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
done
[[ -n "$room" ]] || { echo "Error: --room is required" >&2; exit 1; }
sanitize_room_path "$room"
require_room "$room"
messages_dir="$rooms_root/$room/messages"
mapfile -t all_files < <(find "$messages_dir" -name '*.md' -type f | sort)
filtered_files=()
for file in "${all_files[@]}"; do
created_at="$(frontmatter_value "created_at" "$file")"
file_agent_id="$(frontmatter_value "agent_id" "$file")"
if [[ -n "$since" && "$created_at" < "$since" ]]; then
continue
fi
if [[ -n "$agent_id_filter" && "$file_agent_id" != "$agent_id_filter" ]]; then
continue
fi
filtered_files+=("$file")
done
start_index=0
if [[ "$limit" != "all" ]]; then
if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
echo "Error: --limit must be a number or 'all'" >&2
exit 1
fi
if [[ "${#filtered_files[@]}" -gt "$limit" ]]; then
start_index=$((${#filtered_files[@]} - limit))
fi
fi
for ((index = start_index; index < ${#filtered_files[@]}; index += 1)); do
print_message "${filtered_files[$index]}"
if [[ $index -lt $((${#filtered_files[@]} - 1)) ]]; then
printf -- '---\n\n'
fi
done
;;
*)
usage
;;
esac

View File

@@ -43,6 +43,32 @@ Before any agent starts working, align with the user on logistics:
These questions prevent wasted work. A perfectly implemented feature in the wrong branch or without a PR is still a failure.
## Chat Rooms
When agents need asynchronous coordination, shared status, or a lightweight handoff channel, use chat rooms.
If you decide chat would help:
- create a room for the task
- tell agents the exact room id
- tell agents to load the `paseo-chat` skill
- tell agents to use `skills/paseo-chat/bin/chat.sh` for reading and posting
- tell agents to post memories, useful context, findings, blockers, and handoffs there
- tell agents to check chat very often while they are working
Use chat when:
- multiple agents need to coordinate without constant orchestrator relays
- review findings should be visible to other agents
- a task benefits from a shared running log
When you launch planning, implementation, investigation, or review agents and chat is in play:
- require them to add their findings to the room
- require them to read recent chat before acting
- require them to post back when they discover something another agent may need
Chat is not just passive storage. Mentions and replies trigger direct notifications through chat, so agents can actively get each other's attention.
Do not assume chat is required for every orchestration. Use it when it adds coordination value.
## Agent Types
Implementation and review are not the only agent roles. Use the right agent for the job:
@@ -55,6 +81,50 @@ Implementation and review are not the only agent roles. Use the right agent for
Don't limit yourself to implement → review. Explore first if you need context. Get a second opinion if the design is tricky. The user is paying for thoroughness, not speed.
## Naming Agents
Name sub-agents in kebab-case and include both role and scope.
Use this format:
```text
<role>-<scope>[-<slice>]
```
Examples:
- `plan-issue-456`
- `impl-issue-456`
- `review-issue-456`
- `test-issue-456`
- `qa-issue-456`
- `refactor-relay-auth`
- `investigate-ci-flake`
- `explore-agent-chat`
- `verify-pr-143`
- `impl-issue-456-api`
Approved role prefixes:
- `plan`
- `impl`
- `review`
- `test`
- `qa`
- `verify`
- `investigate`
- `explore`
- `refactor`
Rules:
- always use kebab-case
- no spaces
- no emoji
- no brackets
- no vague names
- always include the task scope
- add a final slice only when needed to disambiguate
Good names make chat, mentions, logs, and agent lists much easier to use.
## How to Write Agent Prompts
This is the most important skill. A good prompt produces good work. A bad prompt produces wasted time.
@@ -125,6 +195,13 @@ Agents start with **zero knowledge** of your conversation. Everything they need
- What decisions were made
- What was already tried
If agents should coordinate through chat, include:
- the room id
- the instruction to load `paseo-chat`
- what they should post there
- that they should check it very often
- that they should use `@agent-id` and replies when they want to notify someone directly
## Always Review
After every implementation agent finishes, spin up a **review agent** to independently verify the work.