From fa0346921a4592fd5031d650a2585c4961cae371 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 24 Mar 2026 12:37:13 +0700 Subject: [PATCH] feat(skills): add paseo chat coordination --- skills/paseo-chat/SKILL.md | 133 +++++++++ skills/paseo-chat/bin/chat.sh | 415 +++++++++++++++++++++++++++++ skills/paseo-orchestrator/SKILL.md | 77 ++++++ 3 files changed, 625 insertions(+) create mode 100644 skills/paseo-chat/SKILL.md create mode 100755 skills/paseo-chat/bin/chat.sh diff --git a/skills/paseo-chat/SKILL.md b/skills/paseo-chat/SKILL.md new file mode 100644 index 000000000..37ec4e4de --- /dev/null +++ b/skills/paseo-chat/SKILL.md @@ -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 diff --git a/skills/paseo-chat/bin/chat.sh b/skills/paseo-chat/bin/chat.sh new file mode 100755 index 000000000..d8c9a18d6 --- /dev/null +++ b/skills/paseo-chat/bin/chat.sh @@ -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 diff --git a/skills/paseo-orchestrator/SKILL.md b/skills/paseo-orchestrator/SKILL.md index 8c51cdb7d..c04c6eb1e 100644 --- a/skills/paseo-orchestrator/SKILL.md +++ b/skills/paseo-orchestrator/SKILL.md @@ -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 +-[-] +``` + +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.